diff --git a/.travis.yml b/.travis.yml index 74579d7b3..f3c8db4d3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,10 +1,11 @@ language: python dist: xenial +cache: false jobs: fast_finish: true include: - - stage: baseline + - stage: test python: 3.6 env: - TOXENV=py36-dj20-postgres-xdist-coverage @@ -97,7 +98,7 @@ install: - pip install tox==3.9.0 script: - - tox + - tox --force-dep "pytest@git+https://github.com/blueyed/pytest@fixture-stack#egg=pytest" after_success: - | diff --git a/pytest_django/fixtures.py b/pytest_django/fixtures.py index b2cc82580..8d4c7be0a 100644 --- a/pytest_django/fixtures.py +++ b/pytest_django/fixtures.py @@ -36,28 +36,34 @@ def django_db_modify_db_settings_tox_suffix(): skip_if_no_django() - tox_environment = os.getenv("TOX_PARALLEL_ENV") - if tox_environment: - # Put a suffix like _py27-django21 on tox workers - _set_suffix_to_test_databases(suffix=tox_environment) + return os.getenv("TOX_PARALLEL_ENV") @pytest.fixture(scope="session") def django_db_modify_db_settings_xdist_suffix(request): skip_if_no_django() - xdist_suffix = getattr(request.config, "slaveinput", {}).get("slaveid") - if xdist_suffix: - # Put a suffix like _gw0, _gw1 etc on xdist processes - _set_suffix_to_test_databases(suffix=xdist_suffix) + return getattr(request.config, "slaveinput", {}).get("slaveid") @pytest.fixture(scope="session") def django_db_modify_db_settings_parallel_suffix( django_db_modify_db_settings_tox_suffix, - django_db_modify_db_settings_xdist_suffix, + django_db_modify_db_settings_xdist_suffix ): skip_if_no_django() + xdist_worker = django_db_modify_db_settings_xdist_suffix + tox_environment = django_db_modify_db_settings_tox_suffix + suffix_parts = [] + if tox_environment: + # Put a suffix like _py27-django21 on tox workers + suffix_parts.append(tox_environment) + if xdist_worker: + # Put a suffix like _gw0, _gw1 etc on xdist processes + suffix_parts.append(xdist_worker) + suffix = "_".join(suffix_parts) + if suffix: + _set_suffix_to_test_databases(suffix=suffix) @pytest.fixture(scope="session") @@ -84,7 +90,7 @@ def django_db_createdb(request): def django_db_setup( request, django_test_environment, - django_db_blocker, + _django_db_blocker, django_db_use_migrations, django_db_keepdb, django_db_createdb, @@ -101,15 +107,16 @@ def django_db_setup( if django_db_keepdb and not django_db_createdb: setup_databases_args["keepdb"] = True - with django_db_blocker.unblock(): + with _django_db_blocker.unblock(): db_cfg = setup_databases( verbosity=request.config.option.verbose, interactive=False, **setup_databases_args ) - def teardown_database(): - with django_db_blocker.unblock(): + yield + if not django_db_keepdb: + with _django_db_blocker.unblock(): try: teardown_databases(db_cfg, verbosity=request.config.option.verbose) except Exception as exc: @@ -119,9 +126,6 @@ def teardown_database(): ) ) - if not django_db_keepdb: - request.addfinalizer(teardown_database) - def _django_db_fixture_helper( request, django_db_blocker, transactional=False, reset_sequences=False @@ -191,7 +195,7 @@ def _set_suffix_to_test_databases(suffix): @pytest.fixture(scope="function") -def db(request, django_db_setup, django_db_blocker): +def db(request, django_db_blocker): """Require a django test database. This database will be setup with the default fixtures and will have @@ -429,13 +433,15 @@ def _live_server_helper(request): It will also override settings only for the duration of the test. """ if "live_server" not in request.fixturenames: + yield return request.getfixturevalue("transactional_db") live_server = request.getfixturevalue("live_server") live_server._live_server_modified_settings.enable() - request.addfinalizer(live_server._live_server_modified_settings.disable) + yield + live_server._live_server_modified_settings.disable() @contextmanager diff --git a/pytest_django/plugin.py b/pytest_django/plugin.py index e54a900ee..2430e46f4 100644 --- a/pytest_django/plugin.py +++ b/pytest_django/plugin.py @@ -462,7 +462,7 @@ def django_test_environment(request): @pytest.fixture(scope="session") -def django_db_blocker(): +def _django_db_blocker(): """Wrapper around Django's database access. This object can be used to re-enable database access. This fixture is used @@ -481,6 +481,28 @@ def django_db_blocker(): return _blocking_manager +@pytest.fixture(scope="session") +def django_db_blocker(django_db_setup, _django_db_blocker): # noqa: F811 + """Wrapper around _django_db_blocker to serve as convenience reference. + + The ``_django_db_blocker`` fixture must be available for the ``django_db_setup`` + fixture, so ``django_db_setup`` must request the ``_django_db_blocker`` fixture. But + in order for ``_django_db_blocker`` to be used, ``django_db_setup`` must also have + been executed, suggesting that ``_django_db_blocker`` should request + ``django_db_setup``, especially since it is possible for ``_django_db_blocker`` to + be needed when ``django_db_setup`` wouldn't normally have been run (e.g. if a test + isn't marked with ``pytest.mark.django_db``). + + This would normally cause a catch-22, but to circumvent this, the + `_django_db_blocker`` fixture is used behind the scenes, while ``django_db_blocker`` + serves as the fixture used by everything that would normally need the blocker (aside + from ``django_db_setup``). This fixture helps coordinate between both + ``django_db_setup`` and ``_django_db_blocker``, so that whenever + ``django_db_blocker`` gets used, it ensures ``django_db_setup`` is run first. + """ + return _django_db_blocker + + @pytest.fixture(autouse=True) def _django_db_marker(request): """Implement the django_db marker, internal to pytest-django. @@ -500,7 +522,7 @@ def _django_db_marker(request): @pytest.fixture(autouse=True, scope="class") -def _django_setup_unittest(request, django_db_blocker): +def _django_setup_unittest(request, _django_db_blocker): """Setup a django unittest, internal to pytest-django.""" if not django_settings_is_configured() or not is_django_unittest(request): yield @@ -525,7 +547,7 @@ def non_debugging_runtest(self): cls = request.node.cls - with django_db_blocker.unblock(): + with _django_db_blocker.unblock(): if _handle_unittest_methods: _restore_class_methods(cls) cls.setUpClass() @@ -736,7 +758,7 @@ def __exit__(self, exc_type, exc_value, traceback): class _DatabaseBlocker(object): """Manager for django.db.backends.base.base.BaseDatabaseWrapper. - This is the object returned by django_db_blocker. + This is the object returned by _django_db_blocker and django_db_blocker. """ def __init__(self): diff --git a/pytest_django_test/settings_base.py b/pytest_django_test/settings_base.py index 543ccaff5..050386299 100644 --- a/pytest_django_test/settings_base.py +++ b/pytest_django_test/settings_base.py @@ -1,5 +1,3 @@ -import os - import django ROOT_URLCONF = "pytest_django_test.urls" @@ -14,10 +12,6 @@ STATIC_URL = "/static/" SECRET_KEY = "foobar" -# Used to construct unique test database names to allow detox to run multiple -# versions at the same time -db_suffix = "_%s" % os.getuid() - MIDDLEWARE = [ "django.contrib.sessions.middleware.SessionMiddleware", "django.middleware.common.CommonMiddleware", diff --git a/pytest_django_test/settings_mysql_innodb.py b/pytest_django_test/settings_mysql_innodb.py index d6cad9210..1fa08885a 100644 --- a/pytest_django_test/settings_mysql_innodb.py +++ b/pytest_django_test/settings_mysql_innodb.py @@ -1,9 +1,9 @@ -from .settings_base import * # noqa: F403 +from .settings_base import * # noqa: F401 F403 DATABASES = { "default": { "ENGINE": "django.db.backends.mysql", - "NAME": "pytest_django" + db_suffix, # noqa: F405 + "NAME": "pytest_django_should_never_get_accessed", "HOST": "localhost", "USER": "root", "OPTIONS": {"init_command": "SET default_storage_engine=InnoDB"}, diff --git a/pytest_django_test/settings_mysql_myisam.py b/pytest_django_test/settings_mysql_myisam.py index 22e3a26d0..d0a89afac 100644 --- a/pytest_django_test/settings_mysql_myisam.py +++ b/pytest_django_test/settings_mysql_myisam.py @@ -1,9 +1,9 @@ -from pytest_django_test.settings_base import * # noqa: F403 +from .settings_base import * # noqa: F401 F403 DATABASES = { "default": { "ENGINE": "django.db.backends.mysql", - "NAME": "pytest_django" + db_suffix, # noqa: F405 + "NAME": "pytest_django_should_never_get_accessed", "HOST": "localhost", "USER": "root", "OPTIONS": {"init_command": "SET default_storage_engine=MyISAM"}, diff --git a/pytest_django_test/settings_postgres.py b/pytest_django_test/settings_postgres.py index e29e30a58..2598beec9 100644 --- a/pytest_django_test/settings_postgres.py +++ b/pytest_django_test/settings_postgres.py @@ -1,4 +1,4 @@ -from pytest_django_test.settings_base import * # noqa +from .settings_base import * # noqa: F401 F403 # PyPy compatibility try: @@ -12,7 +12,7 @@ DATABASES = { "default": { "ENGINE": "django.db.backends.postgresql_psycopg2", - "NAME": "pytest_django" + db_suffix, # noqa + "NAME": "pytest_django_should_never_get_accessed", "HOST": "localhost", "USER": "", } diff --git a/pytest_django_test/settings_sqlite.py b/pytest_django_test/settings_sqlite.py index da88c968b..8ace0293b 100644 --- a/pytest_django_test/settings_sqlite.py +++ b/pytest_django_test/settings_sqlite.py @@ -1,4 +1,4 @@ -from .settings_base import * # noqa +from .settings_base import * # noqa: F401 F403 DATABASES = { "default": { diff --git a/pytest_django_test/settings_sqlite_file.py b/pytest_django_test/settings_sqlite_file.py index abdf6c20c..a4e77ab11 100644 --- a/pytest_django_test/settings_sqlite_file.py +++ b/pytest_django_test/settings_sqlite_file.py @@ -1,6 +1,6 @@ import tempfile -from pytest_django_test.settings_base import * # noqa +from .settings_base import * # noqa: F401 F403 # This is a SQLite configuration, which uses a file based database for # tests (via setting TEST_NAME / TEST['NAME']). @@ -11,7 +11,7 @@ DATABASES = { "default": { "ENGINE": "django.db.backends.sqlite3", - "NAME": "/should_never_be_accessed", + "NAME": "/pytest_django_should_never_get_accessed", "TEST": {"NAME": _filename}, } } diff --git a/tests/test_db_access_in_repr.py b/tests/test_db_access_in_repr.py index 89158c40e..e17937e7f 100644 --- a/tests/test_db_access_in_repr.py +++ b/tests/test_db_access_in_repr.py @@ -5,7 +5,7 @@ def test_db_access_with_repr_in_report(django_testdir): from .app.models import Item - def test_via_db_blocker(django_db_setup, django_db_blocker): + def test_via_db_blocker(django_db_blocker): with django_db_blocker.unblock(): Item.objects.get(name='This one is not there') diff --git a/tests/test_db_setup.py b/tests/test_db_setup.py index 4da897688..fc5ef35e7 100644 --- a/tests/test_db_setup.py +++ b/tests/test_db_setup.py @@ -159,32 +159,34 @@ def test_xdist_with_reuse(django_testdir): from .app.models import Item - def _check(settings): + def _check(settings, worker_id): # Make sure that the database name looks correct db_name = settings.DATABASES['default']['NAME'] - assert db_name.endswith('_gw0') or db_name.endswith('_gw1') - + assert db_name == ( + 'test_pytest_django_should_never_get_accessed_inner_inner_{}' + .format(worker_id) + ) assert Item.objects.count() == 0 Item.objects.create(name='foo') assert Item.objects.count() == 1 @pytest.mark.django_db - def test_a(settings): - _check(settings) + def test_a(settings, worker_id): + _check(settings, worker_id) @pytest.mark.django_db - def test_b(settings): - _check(settings) + def test_b(settings, worker_id): + _check(settings, worker_id) @pytest.mark.django_db - def test_c(settings): - _check(settings) + def test_c(settings, worker_id): + _check(settings, worker_id) @pytest.mark.django_db - def test_d(settings): - _check(settings) + def test_d(settings, worker_id): + _check(settings, worker_id) """ ) @@ -270,7 +272,7 @@ def test_sqlite_database_renamed(self, django_testdir): from django.db import connections @pytest.mark.django_db - def test_a(): + def test_a(worker_id): (conn_db2, conn_default) = sorted( connections.all(), key=lambda conn: conn.alias, @@ -288,7 +290,7 @@ def test_a(): assert conn_db2.vendor == 'sqlite' db_name = conn_db2.creation._get_test_db_name() - assert db_name.startswith('test_custom_db_name_gw') + assert db_name == 'test_custom_db_name_{}'.format(worker_id) """ ) @@ -377,13 +379,15 @@ def test_db_with_tox_suffix(self, django_testdir, monkeypatch): from django.db import connections @pytest.mark.django_db - def test_inner(): + def test_inner(worker_id): (conn, ) = connections.all() assert conn.vendor == 'sqlite' db_name = conn.creation._get_test_db_name() - assert db_name.startswith('test_custom_db_name_py37-django22_gw') + assert db_name == 'test_custom_db_name_py37-django22_{}'.format( + worker_id, + ) """ ) diff --git a/tox.ini b/tox.ini index 84bbb9859..f2b023614 100644 --- a/tox.ini +++ b/tox.ini @@ -27,6 +27,8 @@ deps = postgres: psycopg2-binary coverage: coverage-enable-subprocess + pytest + pytest41: pytest>=4.1,<4.2 pytest41: attrs==17.4.0 xdist: pytest-xdist>=1.15