diff --git a/pytest_django/plugin.py b/pytest_django/plugin.py index 9bab8971..0df25b0e 100644 --- a/pytest_django/plugin.py +++ b/pytest_django/plugin.py @@ -11,6 +11,7 @@ import os import pathlib import sys +import threading import types from collections.abc import Generator from contextlib import AbstractContextManager @@ -64,6 +65,8 @@ # ############### pytest hooks ################ +urlconf_lock = threading.Lock() + @pytest.hookimpl() def pytest_addoption(parser: pytest.Parser) -> None: @@ -646,20 +649,20 @@ def _django_set_urlconf(request: pytest.FixtureRequest) -> Generator[None, None, import django.conf from django.urls import clear_url_caches, set_urlconf - urls = validate_urls(marker) - original_urlconf = django.conf.settings.ROOT_URLCONF - django.conf.settings.ROOT_URLCONF = urls - clear_url_caches() - set_urlconf(None) + with urlconf_lock: + urls = validate_urls(marker) + original_urlconf = django.conf.settings.ROOT_URLCONF + django.conf.settings.ROOT_URLCONF = urls + clear_url_caches() + set_urlconf(None) yield if marker: - django.conf.settings.ROOT_URLCONF = original_urlconf - # Copy the pattern from - # https://github.com/django/django/blob/main/django/test/signals.py#L152 - clear_url_caches() - set_urlconf(None) + with urlconf_lock: + django.conf.settings.ROOT_URLCONF = original_urlconf + clear_url_caches() + set_urlconf(None) @pytest.fixture(autouse=True, scope="session") diff --git a/tests/test_urls.py b/tests/test_urls.py index 78c55d5c..847b3e43 100644 --- a/tests/test_urls.py +++ b/tests/test_urls.py @@ -107,3 +107,47 @@ def test_something_else(): result = django_pytester.runpytest_subprocess() assert result.ret == 0 + + +@pytest.mark.django_project( + extra_settings=""" + ROOT_URLCONF = "empty" + """ +) +def test_urls_concurrent(django_pytester: DjangoPytester) -> None: + "Test that the URL cache clearing is thread-safe with pytest-xdist." + pytest.importorskip("xdist") + + django_pytester.makepyfile( + empty="urlpatterns = []", + urls1=""" + from django.urls import path + urlpatterns = [path('url1/', lambda r: None, name='url1')] + """, + urls2=""" + from django.urls import path + urlpatterns = [path('url2/', lambda r: None, name='url2')] + """, + ) + + django_pytester.create_test_module( + """ + import pytest + from django.urls import reverse, NoReverseMatch + + @pytest.mark.urls('urls1') + def test_urls1(): + reverse('url1') + with pytest.raises(NoReverseMatch): + reverse('url2') + + @pytest.mark.urls('urls2') + def test_urls2(): + reverse('url2') + with pytest.raises(NoReverseMatch): + reverse('url1') + """ + ) + + result = django_pytester.runpytest_subprocess("-n", "2") + assert result.ret == 0