diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4bc8b022..e8ab30ae 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -95,6 +95,11 @@ jobs: coverage run manage.py test coverage report --skip-covered + # The project test runner (webvirtcloud/test_runner.py) on one leg + - name: Run Test Suite In Parallel + if: matrix.python-version == '3.13' && matrix.database == 'sqlite' + run: python manage.py test --parallel + # The per-file report of one leg, to see what a change left untested - name: Store Coverage Report if: always() && matrix.python-version == '3.12' && matrix.database == 'sqlite' diff --git a/operations/tests/test_processes.py b/operations/tests/test_processes.py index 58652d65..c9587c8b 100644 --- a/operations/tests/test_processes.py +++ b/operations/tests/test_processes.py @@ -10,7 +10,7 @@ import time from django.contrib.auth import get_user_model from django.db import connections -from django.test import TransactionTestCase +from django.test import TransactionTestCase, tag from operations import claims, jobs from operations.models import Claim, Operation, WorkerLease @@ -49,6 +49,7 @@ def _submit(user_pk, results): os._exit(0) +@tag("processes") # run outside the --parallel pool (webvirtcloud/test_runner.py) class ProcessTestCase(TransactionTestCase): def setUp(self): self.ctx = multiprocessing.get_context("fork") diff --git a/webvirtcloud/settings.py.template b/webvirtcloud/settings.py.template index f0542cf1..a5f35510 100644 --- a/webvirtcloud/settings.py.template +++ b/webvirtcloud/settings.py.template @@ -178,7 +178,7 @@ LOGIN_URL = "/accounts/login/" if sys.argv[1:2] == ["test"]: PASSWORD_HASHERS = ["django.contrib.auth.hashers.MD5PasswordHasher"] -# --parallel on SQLite (WAL) and with the process tests +# manage.py test --parallel on SQLite (WAL) TEST_RUNNER = "webvirtcloud.test_runner.TestRunner" # Failed logins (django-axes): a user is locked for the address the logins diff --git a/webvirtcloud/test_runner.py b/webvirtcloud/test_runner.py index f2dcc9ed..08967f43 100644 --- a/webvirtcloud/test_runner.py +++ b/webvirtcloud/test_runner.py @@ -1,23 +1,18 @@ """manage.py test --parallel on the SQLite database in WAL mode.""" -import multiprocessing - -from django.test.runner import DiscoverRunner, ParallelTestSuite - - -def _init_worker(*args, **kwargs): - # Pool processes are daemonic, and a daemonic process may have no - # children; the operations process tests start workers of their own. - multiprocessing.current_process().daemon = False - ParallelTestSuite.init_worker(*args, **kwargs) - - -class _ParallelTestSuite(ParallelTestSuite): - init_worker = _init_worker +from django.test.runner import DiscoverRunner class TestRunner(DiscoverRunner): - parallel_test_suite = _ParallelTestSuite + def build_suite(self, *args, **kwargs): + suite = super().build_suite(*args, **kwargs) + if not isinstance(suite, self.parallel_test_suite): + return suite + # Pool workers are daemonic and may start no processes: the test + # cases tagged "processes" run in this process after the pool. + serial = [case for case in suite.subsuites if "processes" in getattr(next(iter(case)), "tags", ())] + suite.subsuites = [case for case in suite.subsuites if case not in serial] + return self.test_suite([suite, *serial]) def setup_databases(self, **kwargs): # Django clones a SQLite test database by copying its main file, but @@ -32,6 +27,8 @@ class TestRunner(DiscoverRunner): if connection.vendor == "sqlite": with connection.cursor() as cursor: cursor.execute("PRAGMA wal_checkpoint(TRUNCATE)") + if cursor.fetchone()[0]: + raise RuntimeError("The test database's WAL could not be written back before cloning") for index in range(parallel): connection.creation.clone_test_db( suffix=str(index + 1), verbosity=self.verbosity, keepdb=self.keepdb