Skip to content

Commit 7433faa

Browse files
authored
Fix CI test to actually use the Postgres database (#9954)
1 parent 3e40eb4 commit 7433faa

14 files changed

Lines changed: 135 additions & 59 deletions

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ max_supported_python = "3.14"
115115
keep_full_version = true
116116

117117
[tool.pytest.ini_options]
118-
addopts = "--tb=short --strict-markers -ra"
118+
addopts = "--tb=short --strict-markers -ra --no-migrations"
119119
testpaths = [ "tests" ]
120120
markers = [
121121
"requires_postgres: marks tests as requiring a PostgreSQL database backend",

rest_framework/test.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
# Note that we import as `DjangoRequestFactory` and `DjangoClient` in order
22
# to make it harder for the user to import the wrong thing without realizing.
33
import io
4+
from contextlib import contextmanager
45
from importlib import import_module
56

67
from django.conf import settings
78
from django.core.exceptions import ImproperlyConfigured
89
from django.core.handlers.wsgi import WSGIHandler
10+
from django.core.signals import request_finished, request_started
11+
from django.db import close_old_connections
912
from django.test import override_settings, testcases
1013
from django.test.client import Client as DjangoClient
1114
from django.test.client import ClientHandler
@@ -22,6 +25,21 @@ def force_authenticate(request, user=None, token=None):
2225
request._force_auth_token = token
2326

2427

28+
@contextmanager
29+
def _keep_connections_open():
30+
"""
31+
Prevent Django from closing the database connection while a request
32+
is dispatched, matching the behavior of Django's ClientHandler.
33+
"""
34+
request_started.disconnect(close_old_connections)
35+
request_finished.disconnect(close_old_connections)
36+
try:
37+
yield
38+
finally:
39+
request_started.connect(close_old_connections)
40+
request_finished.connect(close_old_connections)
41+
42+
2543
if requests is not None:
2644
class HeaderDict(requests.packages.urllib3._collections.HTTPHeaderDict):
2745
def get_all(self, key, default):
@@ -90,7 +108,8 @@ def start_response(wsgi_status, wsgi_headers, exc_info=None):
90108

91109
# Make the outgoing request via WSGI.
92110
environ = self.get_environ(request)
93-
wsgi_response = self.app(environ, start_response)
111+
with _keep_connections_open():
112+
wsgi_response = self.app(environ, start_response)
94113

95114
# Build the underlying urllib3.HTTPResponse
96115
raw_kwargs['body'] = io.BytesIO(b''.join(wsgi_response))

tests/conftest.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,31 @@
33
import dj_database_url
44
import django
55
import pytest
6+
from django.apps import apps
67
from django.core import management
8+
from django.core.management.color import no_style
9+
from django.db import connection
10+
11+
12+
@pytest.fixture
13+
def reset_sequences():
14+
"""
15+
Reset all database sequences so PKs start from 1.
16+
17+
PostgreSQL sequences are non-transactional and persist across
18+
TestCase's transaction rollbacks. Apply this fixture to test
19+
classes that rely on hardcoded PKs to keep them predictable
20+
regardless of execution order. No-op on SQLite.
21+
"""
22+
if connection.vendor != 'postgresql':
23+
return
24+
table_names = set(connection.introspection.table_names())
25+
models = [m for m in apps.get_models() if m._meta.db_table in table_names]
26+
sql_list = connection.ops.sequence_reset_sql(no_style(), models)
27+
if sql_list:
28+
with connection.cursor() as cursor:
29+
for sql in sql_list:
30+
cursor.execute(sql)
731

832

933
def pytest_addoption(parser):

tests/test_filters.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ def test_filter_queryset_raises_error(self):
4545

4646

4747
class SearchFilterModel(models.Model):
48-
title = models.CharField(max_length=20)
48+
title = models.CharField(max_length=25)
4949
text = models.CharField(max_length=100)
5050

5151

@@ -459,6 +459,7 @@ class Meta:
459459
fields = '__all__'
460460

461461

462+
@pytest.mark.usefixtures("reset_sequences")
462463
class SearchFilterM2MTests(TestCase):
463464
def setUp(self):
464465
# Sequence of title/text/attributes is:
@@ -657,6 +658,7 @@ class Meta:
657658
fields = '__all__'
658659

659660

661+
@pytest.mark.usefixtures("reset_sequences")
660662
class OrderingFilterTests(TestCase):
661663
def setUp(self):
662664
# Sequence of title/text is:
@@ -974,6 +976,7 @@ class Meta:
974976
fields = ('id', 'user')
975977

976978

979+
@pytest.mark.usefixtures("reset_sequences")
977980
class SensitiveOrderingFilterTests(TestCase):
978981
def setUp(self):
979982
for idx in range(3):

tests/test_generics.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ class SlugBasedInstanceView(InstanceView):
7676

7777

7878
# Tests
79+
@pytest.mark.usefixtures("reset_sequences")
7980
class TestRootView(TestCase):
8081
def setUp(self):
8182
"""
@@ -171,6 +172,7 @@ def test_post_error_root_view(self):
171172
EXPECTED_QUERIES_FOR_PUT = 2
172173

173174

175+
@pytest.mark.usefixtures("reset_sequences")
174176
class TestInstanceView(TestCase):
175177
def setUp(self):
176178
"""
@@ -334,6 +336,7 @@ def setUp(self):
334336
self.view = FKInstanceView.as_view()
335337

336338

339+
@pytest.mark.usefixtures("reset_sequences")
337340
class TestOverriddenGetObject(TestCase):
338341
"""
339342
Test cases for a RetrieveUpdateDestroyAPIView that does NOT use the
@@ -477,6 +480,7 @@ class Meta:
477480
return DynamicSerializer
478481

479482

483+
@pytest.mark.usefixtures("reset_sequences")
480484
class TestFilterBackendAppliedToViews(TestCase):
481485
def setUp(self):
482486
"""

tests/test_model_serializer.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -758,6 +758,7 @@ class DisplayValueModel(models.Model):
758758
color = models.ForeignKey(DisplayValueTargetModel, on_delete=models.CASCADE)
759759

760760

761+
@pytest.mark.usefixtures("reset_sequences")
761762
class TestRelationalFieldDisplayValue(TestCase):
762763
def setUp(self):
763764
DisplayValueTargetModel.objects.bulk_create([

tests/test_pagination.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1045,6 +1045,7 @@ class CursorPaginationModel(models.Model):
10451045
created = models.IntegerField()
10461046

10471047

1048+
@pytest.mark.usefixtures("reset_sequences")
10481049
class TestCursorPaginationWithValueQueryset(CursorPaginationTestsMixin, TestCase):
10491050
"""
10501051
Unit tests for `pagination.CursorPagination` for value querysets.

tests/test_permissions.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import unittest
33
from unittest import mock
44

5+
import pytest
56
from django.conf import settings
67
from django.contrib.auth.models import AnonymousUser, Group, Permission, User
78
from django.db import models
@@ -73,6 +74,7 @@ def basic_auth_header(username, password):
7374
return 'Basic %s' % base64_credentials
7475

7576

77+
@pytest.mark.usefixtures("reset_sequences")
7678
class ModelPermissionsIntegrationTests(TestCase):
7779
def setUp(self):
7880
User.objects.create_user('disallowed', 'disallowed@example.com', 'password')
@@ -325,6 +327,7 @@ def get_queryset(self):
325327
get_queryset_object_permissions_view = GetQuerysetObjectPermissionInstanceView.as_view()
326328

327329

330+
@pytest.mark.usefixtures("reset_sequences")
328331
@unittest.skipUnless('guardian' in settings.INSTALLED_APPS, 'django-guardian not installed')
329332
class ObjectPermissionsIntegrationTests(TestCase):
330333
"""
@@ -504,6 +507,7 @@ class DeniedObjectViewWithDetail(PermissionInstanceView):
504507
denied_object_view_with_detail = DeniedObjectViewWithDetail.as_view()
505508

506509

510+
@pytest.mark.usefixtures("reset_sequences")
507511
class CustomPermissionsTests(TestCase):
508512
def setUp(self):
509513
BasicModel(text='foo').save()

tests/test_prefetch_related.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import pytest
12
from django.contrib.auth.models import Group, User
23
from django.test import TestCase
34

@@ -18,6 +19,7 @@ class UserUpdate(generics.UpdateAPIView):
1819
serializer_class = UserSerializer
1920

2021

22+
@pytest.mark.usefixtures("reset_sequences")
2123
class TestPrefetchRelatedUpdates(TestCase):
2224
def setUp(self):
2325
self.user = User.objects.create(username='tom', email='tom@example.com')

0 commit comments

Comments
 (0)