Skip to content
Open
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ max_supported_python = "3.14"
keep_full_version = true

[tool.pytest.ini_options]
addopts = "--tb=short --strict-markers -ra"
addopts = "--tb=short --strict-markers -ra --no-migrations"
testpaths = [ "tests" ]
markers = [
"requires_postgres: marks tests as requiring a PostgreSQL database backend",
Expand Down
13 changes: 12 additions & 1 deletion rest_framework/test.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
from django.core.handlers.wsgi import WSGIHandler
from django.core.signals import request_finished, request_started
from django.db import close_old_connections
from django.test import override_settings, testcases
from django.test.client import Client as DjangoClient
from django.test.client import ClientHandler
Expand Down Expand Up @@ -89,8 +91,17 @@ def start_response(wsgi_status, wsgi_headers, exc_info=None):
raw_kwargs['original_response'] = MockOriginalResponse(wsgi_headers)

# Make the outgoing request via WSGI.
# Disconnect close_old_connections to prevent closing the
# database connection during tests, matching the behavior
# of Django's ClientHandler.
environ = self.get_environ(request)
wsgi_response = self.app(environ, start_response)
request_started.disconnect(close_old_connections)
request_finished.disconnect(close_old_connections)
try:
wsgi_response = self.app(environ, start_response)
finally:
request_started.connect(close_old_connections)
request_finished.connect(close_old_connections)
Copy link

Copilot AI Apr 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Temporarily disconnecting/reconnecting global request_started/request_finished signal receivers inside DjangoTestAdapter.send() is not thread-safe: concurrent requests in the same process could run while the handlers are disconnected, changing behavior outside this request. Also, if close_old_connections was connected with a dispatch_uid (or otherwise not matched by the disconnect() call), the connect() in finally can add a duplicate receiver. A safer approach is to avoid mutating global signals here (e.g., use Django’s ClientHandler for the WSGI call path, or otherwise ensure disconnect/connect is idempotent via the same dispatch_uid that Django uses).

Copilot uses AI. Check for mistakes.

# Build the underlying urllib3.HTTPResponse
raw_kwargs['body'] = io.BytesIO(b''.join(wsgi_response))
Expand Down
29 changes: 29 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,36 @@
import dj_database_url
import django
import pytest
from django.apps import apps
from django.core import management
from django.core.management.color import no_style
from django.db import connection
from django.test import TestCase, TransactionTestCase


@pytest.fixture(autouse=True)
def _reset_sequences(request):
"""Reset all database sequences so PKs start from 1 in each test.

PostgreSQL sequences are non-transactional and persist across
TestCase's transaction rollbacks. This fixture ensures every test
gets predictable PKs starting from 1 regardless of execution order.
No-op on SQLite and skipped for tests that don't use the database.
"""
if connection.vendor != 'postgresql':
return
# Only run for tests that actually have database access.
if not (request.cls and issubclass(request.cls, (TestCase, TransactionTestCase))):
if 'db' not in request.fixturenames and 'transactional_db' not in request.fixturenames:
return

table_names = set(connection.introspection.table_names())
models = [m for m in apps.get_models() if m._meta.db_table in table_names]
sql_list = connection.ops.sequence_reset_sql(no_style(), models)
if sql_list:
with connection.cursor() as cursor:
for sql in sql_list:
cursor.execute(sql)
Comment on lines +29 to +35
Copy link

Copilot AI Apr 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This autouse fixture runs a full table introspection and sequence reset for every DB-using test on PostgreSQL (table_names() + apps.get_models() + executing reset SQL). That can add significant overhead to the test suite. Consider computing the relevant model list once per session (or caching sql_list) and only executing the reset SQL per test, or scoping the sequence reset to only the specific apps/tables that need stable PKs (or, ideally, updating the affected tests to avoid hard-coded PK assumptions).

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Each test use different models so that won't work in our test suite



def pytest_addoption(parser):
Expand Down
2 changes: 1 addition & 1 deletion tests/test_filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ def test_filter_queryset_raises_error(self):


class SearchFilterModel(models.Model):
title = models.CharField(max_length=20)
title = models.CharField(max_length=25)
text = models.CharField(max_length=100)


Expand Down
40 changes: 20 additions & 20 deletions tests/test_relations_hyperlink.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ def setUp(self):
source.targets.add(target)

def test_relative_hyperlinks(self):
queryset = ManyToManySource.objects.all()
queryset = ManyToManySource.objects.order_by('pk')
serializer = ManyToManySourceSerializer(queryset, many=True, context={'request': None})
expected = [
{'url': '/manytomanysource/1/', 'name': 'source-1', 'targets': ['/manytomanytarget/1/']},
Expand All @@ -92,7 +92,7 @@ def test_relative_hyperlinks(self):
assert serializer.data == expected

def test_many_to_many_retrieve(self):
queryset = ManyToManySource.objects.all()
queryset = ManyToManySource.objects.order_by('pk')
serializer = ManyToManySourceSerializer(queryset, many=True, context={'request': request})
expected = [
{'url': 'http://testserver/manytomanysource/1/', 'name': 'source-1', 'targets': ['http://testserver/manytomanytarget/1/']},
Expand All @@ -109,7 +109,7 @@ def test_many_to_many_retrieve_prefetch_related(self):
serializer.data

def test_reverse_many_to_many_retrieve(self):
queryset = ManyToManyTarget.objects.all()
queryset = ManyToManyTarget.objects.order_by('pk')
serializer = ManyToManyTargetSerializer(queryset, many=True, context={'request': request})
expected = [
{'url': 'http://testserver/manytomanytarget/1/', 'name': 'target-1', 'sources': ['http://testserver/manytomanysource/1/', 'http://testserver/manytomanysource/2/', 'http://testserver/manytomanysource/3/']},
Expand All @@ -128,7 +128,7 @@ def test_many_to_many_update(self):
assert serializer.data == data

# Ensure source 1 is updated, and everything else is as expected
queryset = ManyToManySource.objects.all()
queryset = ManyToManySource.objects.order_by('pk')
serializer = ManyToManySourceSerializer(queryset, many=True, context={'request': request})
expected = [
{'url': 'http://testserver/manytomanysource/1/', 'name': 'source-1', 'targets': ['http://testserver/manytomanytarget/1/', 'http://testserver/manytomanytarget/2/', 'http://testserver/manytomanytarget/3/']},
Expand All @@ -145,7 +145,7 @@ def test_reverse_many_to_many_update(self):
serializer.save()
assert serializer.data == data
# Ensure target 1 is updated, and everything else is as expected
queryset = ManyToManyTarget.objects.all()
queryset = ManyToManyTarget.objects.order_by('pk')
serializer = ManyToManyTargetSerializer(queryset, many=True, context={'request': request})
expected = [
{'url': 'http://testserver/manytomanytarget/1/', 'name': 'target-1', 'sources': ['http://testserver/manytomanysource/1/']},
Expand All @@ -164,7 +164,7 @@ def test_many_to_many_create(self):
assert obj.name == 'source-4'

# Ensure source 4 is added, and everything else is as expected
queryset = ManyToManySource.objects.all()
queryset = ManyToManySource.objects.order_by('pk')
serializer = ManyToManySourceSerializer(queryset, many=True, context={'request': request})
expected = [
{'url': 'http://testserver/manytomanysource/1/', 'name': 'source-1', 'targets': ['http://testserver/manytomanytarget/1/']},
Expand All @@ -183,7 +183,7 @@ def test_reverse_many_to_many_create(self):
assert obj.name == 'target-4'

# Ensure target 4 is added, and everything else is as expected
queryset = ManyToManyTarget.objects.all()
queryset = ManyToManyTarget.objects.order_by('pk')
serializer = ManyToManyTargetSerializer(queryset, many=True, context={'request': request})
expected = [
{'url': 'http://testserver/manytomanytarget/1/', 'name': 'target-1', 'sources': ['http://testserver/manytomanysource/1/', 'http://testserver/manytomanysource/2/', 'http://testserver/manytomanysource/3/']},
Expand Down Expand Up @@ -215,7 +215,7 @@ def setUp(self):
source.save()

def test_foreign_key_retrieve(self):
queryset = ForeignKeySource.objects.all()
queryset = ForeignKeySource.objects.order_by('pk')
serializer = ForeignKeySourceSerializer(queryset, many=True, context={'request': request})
expected = [
{'url': 'http://testserver/foreignkeysource/1/', 'name': 'source-1', 'target': 'http://testserver/foreignkeytarget/1/'},
Expand All @@ -226,7 +226,7 @@ def test_foreign_key_retrieve(self):
assert serializer.data == expected

def test_reverse_foreign_key_retrieve(self):
queryset = ForeignKeyTarget.objects.all()
queryset = ForeignKeyTarget.objects.order_by('pk')
serializer = ForeignKeyTargetSerializer(queryset, many=True, context={'request': request})
expected = [
{'url': 'http://testserver/foreignkeytarget/1/', 'name': 'target-1', 'sources': ['http://testserver/foreignkeysource/1/', 'http://testserver/foreignkeysource/2/', 'http://testserver/foreignkeysource/3/']},
Expand All @@ -244,7 +244,7 @@ def test_foreign_key_update(self):
assert serializer.data == data

# Ensure source 1 is updated, and everything else is as expected
queryset = ForeignKeySource.objects.all()
queryset = ForeignKeySource.objects.order_by('pk')
serializer = ForeignKeySourceSerializer(queryset, many=True, context={'request': request})
expected = [
{'url': 'http://testserver/foreignkeysource/1/', 'name': 'source-1', 'target': 'http://testserver/foreignkeytarget/2/'},
Expand All @@ -267,7 +267,7 @@ def test_reverse_foreign_key_update(self):
assert serializer.is_valid()
# We shouldn't have saved anything to the db yet since save
# hasn't been called.
queryset = ForeignKeyTarget.objects.all()
queryset = ForeignKeyTarget.objects.order_by('pk')
new_serializer = ForeignKeyTargetSerializer(queryset, many=True, context={'request': request})
expected = [
{'url': 'http://testserver/foreignkeytarget/1/', 'name': 'target-1', 'sources': ['http://testserver/foreignkeysource/1/', 'http://testserver/foreignkeysource/2/', 'http://testserver/foreignkeysource/3/']},
Expand All @@ -279,7 +279,7 @@ def test_reverse_foreign_key_update(self):
assert serializer.data == data

# Ensure target 2 is update, and everything else is as expected
queryset = ForeignKeyTarget.objects.all()
queryset = ForeignKeyTarget.objects.order_by('pk')
serializer = ForeignKeyTargetSerializer(queryset, many=True, context={'request': request})
expected = [
{'url': 'http://testserver/foreignkeytarget/1/', 'name': 'target-1', 'sources': ['http://testserver/foreignkeysource/2/']},
Expand All @@ -296,7 +296,7 @@ def test_foreign_key_create(self):
assert obj.name == 'source-4'

# Ensure source 1 is updated, and everything else is as expected
queryset = ForeignKeySource.objects.all()
queryset = ForeignKeySource.objects.order_by('pk')
serializer = ForeignKeySourceSerializer(queryset, many=True, context={'request': request})
expected = [
{'url': 'http://testserver/foreignkeysource/1/', 'name': 'source-1', 'target': 'http://testserver/foreignkeytarget/1/'},
Expand All @@ -315,7 +315,7 @@ def test_reverse_foreign_key_create(self):
assert obj.name == 'target-3'

# Ensure target 4 is added, and everything else is as expected
queryset = ForeignKeyTarget.objects.all()
queryset = ForeignKeyTarget.objects.order_by('pk')
serializer = ForeignKeyTargetSerializer(queryset, many=True, context={'request': request})
expected = [
{'url': 'http://testserver/foreignkeytarget/1/', 'name': 'target-1', 'sources': ['http://testserver/foreignkeysource/2/']},
Expand Down Expand Up @@ -344,7 +344,7 @@ def setUp(self):
source.save()

def test_foreign_key_retrieve_with_null(self):
queryset = NullableForeignKeySource.objects.all()
queryset = NullableForeignKeySource.objects.order_by('pk')
serializer = NullableForeignKeySourceSerializer(queryset, many=True, context={'request': request})
expected = [
{'url': 'http://testserver/nullableforeignkeysource/1/', 'name': 'source-1', 'target': 'http://testserver/foreignkeytarget/1/'},
Expand All @@ -362,7 +362,7 @@ def test_foreign_key_create_with_valid_null(self):
assert obj.name == 'source-4'

# Ensure source 4 is created, and everything else is as expected
queryset = NullableForeignKeySource.objects.all()
queryset = NullableForeignKeySource.objects.order_by('pk')
serializer = NullableForeignKeySourceSerializer(queryset, many=True, context={'request': request})
expected = [
{'url': 'http://testserver/nullableforeignkeysource/1/', 'name': 'source-1', 'target': 'http://testserver/foreignkeytarget/1/'},
Expand All @@ -386,7 +386,7 @@ def test_foreign_key_create_with_valid_emptystring(self):
assert obj.name == 'source-4'

# Ensure source 4 is created, and everything else is as expected
queryset = NullableForeignKeySource.objects.all()
queryset = NullableForeignKeySource.objects.order_by('pk')
serializer = NullableForeignKeySourceSerializer(queryset, many=True, context={'request': request})
expected = [
{'url': 'http://testserver/nullableforeignkeysource/1/', 'name': 'source-1', 'target': 'http://testserver/foreignkeytarget/1/'},
Expand All @@ -405,7 +405,7 @@ def test_foreign_key_update_with_valid_null(self):
assert serializer.data == data

# Ensure source 1 is updated, and everything else is as expected
queryset = NullableForeignKeySource.objects.all()
queryset = NullableForeignKeySource.objects.order_by('pk')
serializer = NullableForeignKeySourceSerializer(queryset, many=True, context={'request': request})
expected = [
{'url': 'http://testserver/nullableforeignkeysource/1/', 'name': 'source-1', 'target': None},
Expand All @@ -428,7 +428,7 @@ def test_foreign_key_update_with_valid_emptystring(self):
assert serializer.data == expected_data

# Ensure source 1 is updated, and everything else is as expected
queryset = NullableForeignKeySource.objects.all()
queryset = NullableForeignKeySource.objects.order_by('pk')
serializer = NullableForeignKeySourceSerializer(queryset, many=True, context={'request': request})
expected = [
{'url': 'http://testserver/nullableforeignkeysource/1/', 'name': 'source-1', 'target': None},
Expand All @@ -449,7 +449,7 @@ def setUp(self):
source.save()

def test_reverse_foreign_key_retrieve_with_null(self):
queryset = OneToOneTarget.objects.all()
queryset = OneToOneTarget.objects.order_by('pk')
serializer = NullableOneToOneTargetSerializer(queryset, many=True, context={'request': request})
expected = [
{'url': 'http://testserver/onetoonetarget/1/', 'name': 'target-1', 'nullable_source': 'http://testserver/nullableonetoonesource/1/'},
Expand Down
Loading
Loading