diff --git a/.github/coverage.toml b/.github/coverage.toml index 9427f7a67e..c9e66a2e07 100644 --- a/.github/coverage.toml +++ b/.github/coverage.toml @@ -9,7 +9,7 @@ # patch -- "subprocess" (coverage 7.10+) starts measurement in every # child Python process. pytest-xdist's workers are such children # (execnet popen), so without it only the controller is measured -# and the -n auto run reports next to nothing. "_exit" saves the +# and the -n logical run reports next to nothing. "_exit" saves the # data before os._exit(), which skips atexit: execnet calls it on # a worker whose execution outlives shutdown, and pytest-timeout's # thread method on a hung test (#1387). @@ -26,7 +26,7 @@ # generation's code objects: measured on test_http_unit.py # alone, re-importing per test, peak RSS 0.40 GiB without # coverage, 4.17 GiB under sysmon and 0.41 GiB under ctrace. -# Under -n auto that growth killed the runner. +# Under xdist that growth killed the runner. [tool.coverage.run] source = [ diff --git a/.github/workflows/experimental.yml b/.github/workflows/experimental.yml index 4bc24c666d..1ff31c8ed4 100644 --- a/.github/workflows/experimental.yml +++ b/.github/workflows/experimental.yml @@ -388,7 +388,8 @@ jobs: timeout-minutes: 120 env: GRAAL_PYTHON_VM_ARGS: ${{ startsWith(matrix.python-version, 'graalpy') && '--vm.Xmx2g --vm.XX:+ExitOnOutOfMemoryError' || '' }} - WORKERS: ${{ startsWith(matrix.python-version, 'graalpy') && '3' || 'auto' }} + # #2571: 3.14t, which installs psutil, takes `logical` as unit-tests.yml `test` does. + WORKERS: ${{ startsWith(matrix.python-version, 'graalpy') && '3' || 'logical' }} # #1963: folded, so each line break reads as the space between two shards. SHARDS: >- ${{ startsWith(matrix.python-version, 'graalpy') && 'tests/const @@ -680,7 +681,8 @@ jobs: run: python -c "import os; print('cpu_count', os.cpu_count())" # `experimental`'s selection, the `ALL` tier (#1958), asked of - # tests/_tiers.py on the leg's own CPython, as `test` asks it. + # tests/_tiers.py on the leg's own CPython, as `test` asks it; the flags + # are `test`'s. - name: Run unit tests id: tests if: ${{ steps.install.outcome == 'success' }} @@ -692,7 +694,7 @@ jobs: echo "::${ANNOTATION_LEVEL} title=Could not compute the LATEST tier::tests._tiers.latest_tier_paths() failed -- see the traceback above." exit 1 fi - set -- -q -n auto --dist load \ + set -- -q -n logical --dist load \ --durations=30 --durations-min=1.0 --junitxml="$RUNNER_TEMP/junit.xml" \ --ignore=tests/integration \ --ignore-glob='*_runtime.py' \ diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index 86748b46de..6dd5474f91 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -452,6 +452,8 @@ jobs: # # `read -ra` splits the selection on whitespace, as an unquoted # expansion would, but with no pathname expansion of a node ID. + # + # `-n logical` for the reason in `test`'s `Run unit tests` (#2571). - name: Run full test suite id: suite if: ${{ steps.install.outcome == 'success' }} @@ -476,7 +478,7 @@ jobs: fi echo "Fixture-dependent selection: $selection" read -ra selection <<< "$selection" - python util/cap_file_size.py python -m pytest -q -n auto --dist load \ + python util/cap_file_size.py python -m pytest -q -n logical --dist load \ --durations=30 --durations-min=1.0 --junitxml="$RUNNER_TEMP/junit.xml" "${selection[@]}" # See the `test` job's step of the same name. @@ -716,6 +718,8 @@ jobs: # that would otherwise skip it silently (#845 finding 2). `set +e` covers # only that install; the pip upgrade and the `test` extra run under # `set -eu`, as a broken base install would otherwise read as "expected" (#845). + # + # `-n logical` for the reason in `test`'s `Run unit tests` (#2571). - name: Write the per-cell script run: | cat > "$RUNNER_TEMP/engine-cell.sh" <<'CELL' @@ -803,7 +807,7 @@ jobs: esac echo "Running: $TEST_PATHS" # shellcheck disable=SC2086 - if ! python util/cap_file_size.py python -m pytest -q -n auto --dist load $TEST_PATHS; then + if ! python util/cap_file_size.py python -m pytest -q -n logical --dist load $TEST_PATHS; then echo "::${ANNOTATION_LEVEL} title=$ENGINE tests failed on Python $PY::See the 'Python $PY ($ENGINE)' group in this step's log." exit 1 fi @@ -1060,7 +1064,7 @@ jobs: fi echo "Fixture-dependent selection: $selection" read -ra selection <<< "$selection" - python util/cap_file_size.py python -m pytest -q -n auto --dist load "${selection[@]}" + python util/cap_file_size.py python -m pytest -q -n logical --dist load "${selection[@]}" # #1672: see the `test` job's step of the same name. - name: KTLO summary @@ -1371,7 +1375,7 @@ jobs: exit 1 fi read -ra pins <<< "$pins" - python util/cap_file_size.py python -m pytest -q -n auto --dist load \ + python util/cap_file_size.py python -m pytest -q -n logical --dist load \ --durations=30 --durations-min=1.0 --junitxml="$RUNNER_TEMP/junit.xml" "${pins[@]}" # #2164: the script's default stall dump sits under `unittest-ordering`'s @@ -1443,9 +1447,9 @@ jobs: # Deliberately the whole suite, unlike `integration` (#715): `gate` runs # only under `gate-only: true`, never per PR push, and partitioning it # would trade away the "trust nothing, re-run everything" property this - # job exists for. `--dist load` as in the `test` job. + # job exists for. `-n logical --dist load` as in the `test` job. - name: Run full test suite - run: python util/cap_file_size.py python -m pytest -q -n auto --dist load + run: python util/cap_file_size.py python -m pytest -q -n logical --dist load # Ruleset 23497679's required_status_checks requires these contexts: this # job's `Required checks passed`, a `Compat Python` one per supported version, diff --git a/docs/source/contributing/workflows.rst b/docs/source/contributing/workflows.rst index 520654e75b..5a5f54b455 100644 --- a/docs/source/contributing/workflows.rst +++ b/docs/source/contributing/workflows.rst @@ -1189,7 +1189,7 @@ warnings: * GraalPy alone gets ``GRAAL_PYTHON_VM_ARGS=--vm.Xmx2g --vm.XX:+ExitOnOutOfMemoryError``, a 2 GiB heap for every GraalPy process, xdist workers included, and ``-n 3``, a CPU - of the runner's 4 left to the JIT and the runner; 3.14t keeps ``-n auto``. A + of the runner's 4 left to the JIT and the runner; 3.14t runs ``-n logical``. A worker's live heap grows with the tests it runs (`#1753 `__): in job 116172356218 all three reached 2.2-2.6 GB RSS in 16 minutes, and a ``MemoryError`` in pytest's own diff --git a/tests/conftest.py b/tests/conftest.py index 23f52fb299..4daa1adb5a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -559,7 +559,7 @@ def _tier_guard_lock_path() -> 'pathlib.Path': """Where the probe/scan mutex lives, one file per checkout. Keyed by :data:`tests._tiers.ROOT` rather than a fixed name, so two - worktrees of this repository each running their own ``-n auto`` session on + worktrees of this repository each running their own xdist session on the same host never share a lock file neither of them needs shared. """ @@ -600,7 +600,7 @@ def _tier_guard_lock(exclusive: 'bool') -> 'Iterator[None]': :class:`~tests.test_tier_guard_xdist.XdistSubprocessTests` deliberately writes a real violation to disk, under :data:`~tests._tiers.PROBE_MODULE_NAME`, to reproduce the tier guard's own collection-time diagnostic end to end. - Serially the two never overlap; under the ``-n auto --dist load`` this + Serially the two never overlap; under the ``--dist load`` this suite's CI jobs use, pytest-xdist may hand the two tests to different worker processes at the same moment, and the scan can then observe the probe mid-life and report a violation nobody committed -- a real defect in @@ -619,7 +619,7 @@ def _tier_guard_lock(exclusive: 'bool') -> 'Iterator[None]': A no-op wherever :data:`fcntl` is :data:`None` -- true only off POSIX, such as the Windows leg of :file:`.github/workflows/experimental.yml`, which runs - ``-n auto`` too. Nothing that has to stay correct there relies on the lock + ``--dist load`` too. Nothing that has to stay correct there relies on the lock (#2114): :func:`tests._tiers.suite_modules` never lists the probe, and :class:`~tests.test_tier_guard.SuiteIsCleanTests` skips it where this is a no-op. :func:`msvcrt.locking` is no substitute, since it has no shared mode. diff --git a/tests/project/test_coverage_rcfile.py b/tests/project/test_coverage_rcfile.py index 302d1fb39f..94c8b9b8e2 100644 --- a/tests/project/test_coverage_rcfile.py +++ b/tests/project/test_coverage_rcfile.py @@ -44,7 +44,7 @@ def test_every_pyproject_setting_is_repeated_unchanged(self) -> None: self.assertEqual(self.rcfile.get(section, {}).get(key), value) def test_the_xdist_workers_are_measured(self) -> None: - """Without these two keys an ``-n auto`` run measures only the controller.""" + """Without these two keys an ``-n logical`` run measures only the controller.""" run = self.rcfile['run'] self.assertIs(run.get('parallel'), True) self.assertIn('subprocess', run.get('patch', ())) diff --git a/tests/project/test_project_pins_once_1954.py b/tests/project/test_project_pins_once_1954.py index 39784bbd60..635c02d33a 100644 --- a/tests/project/test_project_pins_once_1954.py +++ b/tests/project/test_project_pins_once_1954.py @@ -65,7 +65,7 @@ def test_it_runs_on_latest_with_what_test_installs(self) -> None: def test_it_runs_under_the_caps(self) -> None: """xdist as ``test`` runs it, a 2 GiB file cap, and a step cap under the job's.""" run = str(step(JOB, STEP)['run']) - self.assertIn('python util/cap_file_size.py python -m pytest -q -n auto --dist load', run) + self.assertIn('python util/cap_file_size.py python -m pytest -q -n logical --dist load', run) self.assertIn(_tiers.PROJECT_PINS_SELECTOR, run) self.assertLess(step(JOB, STEP)['timeout-minutes'], job(*JOB)['timeout-minutes']) @@ -75,7 +75,7 @@ class TestEveryPinStillRunsWhereTheMatrixDoesNot(unittest.TestCase): def test_the_gate_runs_the_whole_suite(self) -> None: self.assertEqual(step(('unit-tests.yml', 'gate'), 'Run full test suite')['run'], - 'python util/cap_file_size.py python -m pytest -q -n auto --dist load') + 'python util/cap_file_size.py python -m pytest -q -n logical --dist load') def test_the_ordering_cell_runs_every_pin(self) -> None: """#2254: the ``project`` cells between them, as they cut ``tests/project`` in two.""" diff --git a/tests/project/test_resource_monitor_1740.py b/tests/project/test_resource_monitor_1740.py index f6dd67104d..4127fc4664 100644 --- a/tests/project/test_resource_monitor_1740.py +++ b/tests/project/test_resource_monitor_1740.py @@ -285,10 +285,11 @@ def test_graalpy_alone_gets_a_2g_heap_and_3_workers(self) -> None: graalpy = version.startswith('graalpy') with self.subTest(version=version): self.assertEqual(by_impl(env['GRAAL_PYTHON_VM_ARGS'], version), HEAP if graalpy else '') - self.assertEqual(by_impl(env['WORKERS'], version), GRAAL_WORKERS if graalpy else 'auto') + # #2571: 3.14t installs psutil, under which `auto` counts physical cores. + self.assertEqual(by_impl(env['WORKERS'], version), GRAAL_WORKERS if graalpy else 'logical') script = step(TESTS)['run'] self.assertIn('set -- -q -n "$WORKERS" --dist load', script) - self.assertNotIn('-n auto', script) + self.assertEqual(re.findall(r'-q -n (\S+)', script), ['"$WORKERS"']) def test_the_timeout_ends_the_tests_before_the_step_cap(self) -> None: script = step(TESTS)['run'] diff --git a/tests/project/test_sweeps_on_latest_only_1732.py b/tests/project/test_sweeps_on_latest_only_1732.py index fec9005ce2..667a5c5b97 100644 --- a/tests/project/test_sweeps_on_latest_only_1732.py +++ b/tests/project/test_sweeps_on_latest_only_1732.py @@ -20,7 +20,8 @@ gives either job, with a ``python`` on ``PATH`` that records its arguments, so what is checked is the selection bash builds, not what the text appears to say. :class:`TestTheIgnoresAreTheLatestTier` asks pytest itself what those ignores leave -out. +out. :class:`TestEveryXdistRunCountsLogicalCPUs` holds every xdist run in the +workflows but GraalPy's to ``-n logical`` (#2573). The owner's second ruling on #1732, option 2: nothing waits on the experimental legs, which with the caps GraalPy needs run for up to two hours. A job of @@ -32,6 +33,7 @@ from __future__ import annotations +import copy import os import pathlib import re @@ -46,11 +48,11 @@ from tests._support import bash_argv from tests.project.test_graalpy_shards_1963 import rest_ignores from tests.project.test_python_matrix_tiers_1633 import REQUIRED, is_experimental, job, legs, workflow -from tests.project.test_python_versions_1660 import ROOT, live_outputs, prose +from tests.project.test_python_versions_1660 import ROOT, live_outputs, prose, workflows from tests.project.test_release_gates import NULL, evaluate_expression if TYPE_CHECKING: - from typing import Any, Optional + from typing import Any, Iterator, Optional #: Test tier (#1958): checks the workflows' tier selections. __tier__ = 'LATEST' @@ -71,6 +73,12 @@ #: main's, ``experimental``'s are sized for GraalPy (88-113 minutes, estimate). #: The watcher's interpreter and its requests are capped too (#1938). CAPS = {JOB: (45, {}), EXPERIMENTAL: (140, {'python': 5, 'install': 10, 'watcher-python': 1, 'watcher': 1, 'tests': 120})} +#: A pytest-xdist worker count on a command line: ``-n X``, ``-nX``, ``--numprocesses X`` or +#: ``--numprocesses=X``, with or without ``--dist`` after it. +WORKERS_FLAG = re.compile(r'(?[^\s;|&]+)') +#: What ``-n auto`` and ``-n logical`` resolve to wherever it is set, as pytest-xdist's +#: ``pytest_xdist_auto_num_workers`` reads it first. +XDIST_ENV = 'PYTEST_XDIST_AUTO_NUM_WORKERS' #: The unit tier's own flags, which every leg passes. TIER_FLAGS = ('--ignore=tests/integration', '--ignore-glob=*_runtime.py', '--ignore-glob=*_regression.py') #: A ``python`` that appends each argument on a line of ``$ARGS_FILE``, then an @@ -284,7 +292,7 @@ def test_every_other_leg_runs_the_rest(self) -> None: continue want = list(expected) if is_experimental(leg): - # #1740: the experimental step's own worker count, 3 on GraalPy and `auto` on the rest. + # #1740: the experimental step's own worker count, 3 on GraalPy and `logical` on the rest. want[want.index('-n') + 1] = step_env(leg, EXPERIMENTAL)['WORKERS'] # #1753: and its replacements for crashed workers, after `--dist load`. want.insert(want.index('--dist') + 2, '--max-worker-restart=30') @@ -297,10 +305,11 @@ def test_every_test_leg_runs_a_worker_per_logical_cpu(self) -> None: """#2571: ``-n logical``, as ``auto`` counts physical cores wherever psutil is installed. PyPy's legs share the flag, and with no psutil there resolve it as ``auto`` would. + GraalPy's leg runs 3 workers (#1740). """ - for version, (leg, argv) in self.runs.items(): - if is_experimental(leg): + for version, (_, argv) in self.runs.items(): + if version.startswith('graalpy'): continue with self.subTest(python=version): args = pytest_args(argv) @@ -391,7 +400,7 @@ def setUpClass(cls) -> None: @staticmethod def flags() -> 'list[str]': - return ['-q', '-n', 'auto', '--dist', 'load', '--durations=30', '--durations-min=1.0', + return ['-q', '-n', 'logical', '--dist', 'load', '--durations=30', '--durations-min=1.0', f'--junitxml={RUNNER_TEMP}/junit.xml'] def test_the_all_selection_leaves_out_the_latest_modules(self) -> None: @@ -434,7 +443,98 @@ def test_the_parity_legs_run_the_all_tier(self) -> None: self.assertNotIn(self.latest, [leg['python-version'] for leg in found]) self.assertIn(_tiers.FIXTURE_ALL_TIER_SELECTOR, step(PARITY)['run']) self.assertEqual(pytest_args(run_step(legs(*PARITY)[0], PARITY)), - ['-q', '-n', 'auto', '--dist', 'load'] + self.all) + ['-q', '-n', 'logical', '--dist', 'load'] + self.all) + + +def pytest_commands(run: 'str') -> 'Iterator[str]': + """Each command of a ``run:`` script that names pytest, or sets the positional arguments a later one passes. + + A continued line is one command; a comment line is none. + + """ + for line in run.replace('\\\n', ' ').splitlines(): + if line.lstrip().startswith('#'): + continue + for command in re.split(r';|&&|\|', line): + if 'pytest' in command.lower() or re.match(r'\s*set\s+--(?:\s|$)', command): + yield command + + +def worker_counts(name: 'str', spec: 'dict[str, Any]') -> 'list[tuple[str, str]]': + """Each pytest-xdist worker count workflow ``name`` sets, with where it sets it. + + A :data:`WORKERS_FLAG` in a pytest command or a ``PYTEST_ADDOPTS`` gives its value, and an + :data:`XDIST_ENV` anywhere in a job gives that name. + + """ + found = [] # type: list[tuple[str, str]] + + def addopts(where: 'str', env: 'Any') -> None: + value = (env or {}).get('PYTEST_ADDOPTS') + if value is not None: + found.extend((where, match.group('value')) for match in WORKERS_FLAG.finditer(str(value))) + + addopts(name, spec.get('env')) + if XDIST_ENV in repr(spec.get('env')): + found.append((name, XDIST_ENV)) + for job_id, body in spec['jobs'].items(): + if XDIST_ENV in repr(body): + found.append((f'{name} {job_id}', XDIST_ENV)) + addopts(f'{name} {job_id}', body.get('env')) + for entry in body.get('steps') or (): + where = f"{name} {job_id}: {entry.get('name')}" + addopts(where, entry.get('env')) + for command in pytest_commands(str(entry.get('run', ''))): + found.extend((where, match.group('value')) for match in WORKERS_FLAG.finditer(command)) + return found + + +class TestEveryXdistRunCountsLogicalCPUs(unittest.TestCase): + """#2573: every pytest-xdist worker count in the workflows but ``experimental``'s is ``logical``, as ``test``'s is. + + ``experimental``'s is its ``$WORKERS``, 3 on GraalPy, which :class:`TestTheStepSelectsByLeg` checks per leg. + Only the workflows are read, so the frozen copies under tests/project are left out. + + """ + + #: ``experimental``'s count, the one that is not ``logical``. + OWN = (f'{EXPERIMENTAL[0]} {EXPERIMENTAL[1]}: {STEP}', '"$WORKERS"') + #: ``gate``'s step, which :meth:`test_each_spelling_of_another_count_is_caught` doctors. + GATE = ('unit-tests.yml', 'gate', INTEGRATION_STEP) + + @classmethod + def stray(cls, found: 'list[tuple[str, str]]') -> 'list[tuple[str, str]]': + return [entry for entry in found if entry[1] != 'logical' and entry != cls.OWN] + + def doctored(self, **change: 'Any') -> 'list[tuple[str, str]]': + """``gate``'s step with ``change`` applied, and what :func:`worker_counts` then finds astray.""" + name, job_id, step_name = self.GATE + spec = copy.deepcopy(workflow(name)) + entry = next(entry for entry in spec['jobs'][job_id]['steps'] if entry.get('name') == step_name) + self.assertIn('-n logical --dist load', entry['run']) + if 'flags' in change: + entry['run'] = entry['run'].replace('-n logical --dist load', change['flags']) + if 'env' in change: + entry['env'] = change['env'] + return self.stray(worker_counts(name, spec)) + + def test_every_xdist_run_passes_n_logical(self) -> None: + found = [entry for name, spec in workflows() for entry in worker_counts(name, spec)] + self.assertEqual(self.stray(found), []) + self.assertIn(self.OWN, found) + self.assertIn((f'{self.GATE[0]} {self.GATE[1]}: {self.GATE[2]}', 'logical'), found) + + def test_each_spelling_of_another_count_is_caught(self) -> None: + """A bare ``-n auto`` among them, with no ``--dist`` after it.""" + where = f'{self.GATE[0]} {self.GATE[1]}: {self.GATE[2]}' + for flags, value in (('-n auto', 'auto'), ('-nauto', 'auto'), ('-n 2 --dist load', '2'), + ('--numprocesses auto', 'auto'), ('--numprocesses=auto', 'auto')): + with self.subTest(flags=flags): + self.assertEqual(self.doctored(flags=flags), [(where, value)]) + with self.subTest(env='PYTEST_ADDOPTS'): + self.assertEqual(self.doctored(env={'PYTEST_ADDOPTS': '-n auto'}), [(where, 'auto')]) + with self.subTest(env=XDIST_ENV): + self.assertEqual(self.doctored(env={XDIST_ENV: '2'}), [(f'{self.GATE[0]} {self.GATE[1]}', XDIST_ENV)]) def triggers(spec: 'dict[str, Any]') -> 'dict[str, Any]': diff --git a/tests/project/test_windows_test_leg_2060.py b/tests/project/test_windows_test_leg_2060.py index 3a3e2faeb2..839829ae0b 100644 --- a/tests/project/test_windows_test_leg_2060.py +++ b/tests/project/test_windows_test_leg_2060.py @@ -15,9 +15,9 @@ an error. * :class:`TestItRunsOnWindows`: every ``run:`` is Git Bash's, none calls a Linux-only tool, and the checkout keeps LF line endings. -* :class:`TestTheTestsStep`: run as bash runs it, the step hands pytest all but - ``-n`` of ``test``'s flags and ignores the ``LATEST`` tier, and a tier it cannot - list fails the step with a warning. +* :class:`TestTheTestsStep`: run as bash runs it, the step hands pytest ``test``'s + flags and ignores the ``LATEST`` tier, and a tier it cannot list fails the step + with a warning. * :class:`TestItHoldsLatestBackNowhere`: the weekly tier job does not wait for a Windows build before it moves ``latest``. * :class:`TestTheReporters`: CI Reports and the CI Timing review read the job as a @@ -281,21 +281,20 @@ def test_it_runs_the_all_tier_with_tests_flags(self) -> None: self.assertEqual(result.returncode, 0, result.stdout + result.stderr) latest = list(_tiers.latest_tier_paths()) self.assertTrue(latest) - self.assertEqual(argv, ['-m', 'pytest', '-q', '-n', 'auto', '--dist', 'load', '--durations=30', + self.assertEqual(argv, ['-m', 'pytest', '-q', '-n', 'logical', '--dist', 'load', '--durations=30', '--durations-min=1.0', f'--junitxml={tmp}/junit.xml', '--ignore=tests/integration', '--ignore-glob=*_runtime.py', '--ignore-glob=*_regression.py', *(f'--ignore={path}' for path in latest)]) def test_its_flags_are_tests(self) -> None: - """``test``'s flags, but for ``-n``, which only ``test``'s step sets to ``logical``.""" - flags = re.compile(r"set -- -q -n (\w+) (--dist load .*?--ignore-glob='\*_regression\.py')", re.S) + """The flags as unit-tests.yml ``test``'s step writes them.""" + flags = re.compile(r"set -- -q -n \S+ --dist load .*?--ignore-glob='\*_regression\.py'", re.S) own = flags.search(step('Run unit tests')['run']) tests = [entry for entry in _workflows.steps('unit-tests.yml', 'test') if entry.get('name') == 'Run unit tests'] theirs = flags.search(tests[0]['run']) self.assertIsNotNone(own) self.assertIsNotNone(theirs) - self.assertEqual((own.group(1), theirs.group(1)), ('auto', 'logical')) # type: ignore[union-attr] - self.assertEqual(own.group(2).split(), theirs.group(2).split()) # type: ignore[union-attr] + self.assertEqual(own.group(0).split(), theirs.group(0).split()) # type: ignore[union-attr] def test_a_tier_it_cannot_list_fails_the_step_with_a_warning(self) -> None: result, argv, _ = self.run_step(FAIL_C='1') diff --git a/tests/test_tier_guard_xdist.py b/tests/test_tier_guard_xdist.py index 9b46bbee1e..5fd8e4b817 100644 --- a/tests/test_tier_guard_xdist.py +++ b/tests/test_tier_guard_xdist.py @@ -3,7 +3,7 @@ :func:`tests.conftest.pytest_collection_modifyitems` raises :exc:`pytest.UsageError` with a detailed diagnostic when a unit-tier module reads a generated capture. Under -``-n auto --dist load`` that message can be lost entirely: each xdist worker +xdist that message can be lost entirely: each xdist worker runs this hook inside its own nested :program:`pytest` session (:mod:`xdist.remote`), and that session's ``pytest_collection_finish`` fires from a ``finally:`` regardless of the ``UsageError``, so the controller sees what looks @@ -20,14 +20,14 @@ :class:`CollectionModifyItemsXdistTests` pins the hook's own logic directly and runs unconditionally. :class:`XdistSubprocessTests` reproduces the bug -end-to-end with a real violation and a real ``pytest -n auto --dist load`` +end-to-end with a real violation and a real ``pytest -n 2 --dist load`` invocation, and skips when :mod:`xdist` is not importable -- the repository venv this suite normally runs under deliberately has no xdist installed. A second, independent race lives at the same address. :mod:`tests.test_tier_guard`'s ``SuiteIsCleanTests`` walks the whole :file:`tests/` tree looking for exactly the kind of violation :class:`XdistSubprocessTests` deliberately writes to disk; under -``-n auto --dist load`` pytest-xdist may run the two in different worker processes +``--dist load`` pytest-xdist may run the two in different worker processes at the same moment, and the scan then reports a violation nobody committed -- measured on a real CI run, ``Integration Python 3.13``, one leg in five, 466s in. :func:`tests.conftest._tier_guard_lock` closes it with a readers-writer mutex: @@ -210,7 +210,7 @@ def setUp(self) -> None: # Exclusive for as long as the probe sits on disk: see # tests.conftest._tier_guard_lock for the SuiteIsCleanTests race this - # closes under -n auto --dist load. Entered manually, rather than with + # closes under --dist load. Entered manually, rather than with # a `with` block around the rest of setUp, because the hold has to # outlive this method and span the test body and cleanup too; released # only after the probe is gone -- addCleanup runs LIFO, so registering diff --git a/util/run_unittest_leg.py b/util/run_unittest_leg.py index 6a4dffc78d..097c057f1d 100644 --- a/util/run_unittest_leg.py +++ b/util/run_unittest_leg.py @@ -161,7 +161,7 @@ #: ``pytest-xdist`` installed (the ``test`` extra pulls it in, and this #: script's own CI job installs that extra for the other root modules' #: sake), its ``XdistSubprocessTests`` spawns a real, deliberately slow -#: ``pytest -n auto --dist load`` subprocess that the pytest-based +#: ``pytest -n 2 --dist load`` subprocess that the pytest-based #: ``test``/``integration``/``gate`` jobs already exercise -- duplicating #: that cost here would buy nothing against the ordering class of defect #: this script exists to catch.