Skip to content

Commit 0d00951

Browse files
authored
Shuffle before other plugins' collection hooks (#746)
Fixes #701. Make pytest-randomly run first, even before other plugin’s collection hooks marked "tryfirst", by turning the hook into a hook wrapper. This ensures that pytest-django, and other plugins that group tests with a stable sort, apply their grouping on top of the shuffled order, making the final order reproducible from the seed. Now we require pytest 8 and pluggy 1.0, the first version to support hook wrappers.
1 parent 817091d commit 0d00951

5 files changed

Lines changed: 85 additions & 14 deletions

File tree

‎CHANGELOG.rst‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,23 @@ Unreleased
77

88
* Support Python 3.15.
99

10+
* Shuffle tests before other plugins’ ``pytest_collection_modifyitems`` hooks run.
11+
12+
Previously, pytest-randomly used a plain ``tryfirst`` hook, so another plugin using ``tryfirst``, such as pytest-django, could reorder tests before or after pytest-randomly’s shuffle, depending on the plugin registration order.
13+
Since that order can vary between environments, this would make the final test order non-repeatable.
14+
15+
Now, pytest-randomly marks its hook as a hook wrapper, making it run before any other plugin’s ``pytest_collection_modifyitems`` hooks, regardless of registration order.
16+
Plugins that group tests with a stable sort can apply their grouping on top of the shuffled order, making the final test order reproducible from the seed.
17+
18+
This is a low-impact breaking change because it you may not be able to reprduce a test order from the last pytest-randomly version by reusing a ``--randomly-seed`` value.
19+
20+
`PR #746 <https://github.com/pytest-dev/pytest-randomly/pull/746>`__.
21+
Thanks to milssky for the report in `Issue #701 <https://github.com/pytest-dev/pytest-randomly/issues/701>`__.
22+
23+
* Require pytest 8+, the first version to require a version of pluggy that supports hook wrappers, as used by the above fix.
24+
25+
`PR #746 <https://github.com/pytest-dev/pytest-randomly/pull/746>`__.
26+
1027
* Reset `Polyfactory <https://polyfactory.litestar.dev/>`__\’s default random state at the start of every test, if it is installed.
1128

1229
Thanks to Rahul Kumar in `PR #735 <https://github.com/pytest-dev/pytest-randomly/issues/735>`__.

‎pyproject.toml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ classifiers = [
3737
"Typing :: Typed",
3838
]
3939
dependencies = [
40-
"pytest",
40+
"pytest>=8",
4141
]
4242
urls.Changelog = "https://github.com/pytest-dev/pytest-randomly/blob/main/CHANGELOG.rst"
4343
urls.Funding = "https://adamj.eu/books/"

‎src/pytest_randomly/__init__.py‎

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import argparse
44
import random
5-
from collections.abc import Callable
5+
from collections.abc import Callable, Generator
66
from functools import lru_cache
77
from importlib.metadata import entry_points
88
from itertools import groupby
@@ -205,11 +205,21 @@ def pytest_runtest_teardown(item: Item) -> None:
205205
_reseed(item.config, (_crc32(item.nodeid) + 1) % 2**32)
206206

207207

208-
@hookimpl(tryfirst=True)
209-
def pytest_collection_modifyitems(config: Config, items: list[Item]) -> None:
210-
if not config.getoption("randomly_reorganize"):
211-
return
208+
@hookimpl(wrapper=True, tryfirst=True)
209+
def pytest_collection_modifyitems(
210+
config: Config, items: list[Item]
211+
) -> Generator[None, None, None]:
212+
# Reorganize within this wrapper, before the yield, so it runs before all
213+
# non-wrapper implementations of this hook, regardless of plugin
214+
# registration order. Plugins that group tests with a stable sort, like
215+
# pytest-django, then apply their grouping on top of the shuffled order,
216+
# making the final order reproducible from the seed.
217+
if config.getoption("randomly_reorganize"):
218+
_reorganize_items(config, items)
219+
return (yield)
212220

221+
222+
def _reorganize_items(config: Config, items: list[Item]) -> None:
213223
seed = _reseed(config)
214224

215225
modules_items: list[tuple[ModuleType | None, list[Item]]] = []

‎tests/test_pytest_randomly.py‎

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -409,6 +409,50 @@ def test_d():
409409
]
410410

411411

412+
def test_reordered_before_other_tryfirst_hook(ourtester):
413+
"""
414+
The reorganization should run before other plugins’
415+
pytest_collection_modifyitems hooks, even tryfirst ones registered after
416+
pytest-randomly, so plugins that group tests with a stable sort, like
417+
pytest-django, preserve the shuffled order within their groups.
418+
"""
419+
ourtester.makeconftest(
420+
"""
421+
import pytest
422+
423+
@pytest.hookimpl(tryfirst=True)
424+
def pytest_collection_modifyitems(items):
425+
items.sort(key=lambda item: 0 if "db" in item.name else 1)
426+
"""
427+
)
428+
ourtester.makepyfile(
429+
test_one="""
430+
def test_db_a():
431+
pass
432+
433+
def test_db_b():
434+
pass
435+
436+
def test_plain_c():
437+
pass
438+
439+
def test_plain_d():
440+
pass
441+
"""
442+
)
443+
args = ["-v", "--randomly-seed=15"]
444+
445+
out = ourtester.runpytest(*args)
446+
447+
out.assert_outcomes(passed=4, failed=0)
448+
assert out.outlines[9:13] == [
449+
"test_one.py::test_db_a PASSED",
450+
"test_one.py::test_db_b PASSED",
451+
"test_one.py::test_plain_d PASSED",
452+
"test_one.py::test_plain_c PASSED",
453+
]
454+
455+
412456
def test_doctests_reordered(ourtester):
413457
ourtester.makepyfile(
414458
test_one="""

‎uv.lock‎

Lines changed: 8 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)