Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ Unreleased
`PR #687 <https://github.com/pytest-dev/pytest-randomly/issues/687>`__.
Thanks to Bryce Drennan for the suggestion in `Issue #600 <https://github.com/pytest-dev/pytest-randomly/issues/600>`__ and initial implementation in `PR #617 <https://github.com/pytest-dev/pytest-randomly/pull/617>`__.

* Move from MD5 to CRC32 for hashing test IDs, as it’s 5x faster and we don’t need cryptographic security.

`Issue #686 <https://github.com/pytest-dev/pytest-randomly/issues/686>`__.

3.16.0 (2024-10-25)
-------------------

Expand Down
35 changes: 16 additions & 19 deletions src/pytest_randomly/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from itertools import groupby
from types import ModuleType
from typing import Any, Callable, TypeVar
from zlib import crc32

from _pytest.config import Config
from _pytest.config.argparsing import Parser
Expand Down Expand Up @@ -198,17 +199,17 @@ def pytest_report_header(config: Config) -> str:

def pytest_runtest_setup(item: Item) -> None:
if item.config.getoption("randomly_reset_seed"):
_reseed(item.config, int.from_bytes(_md5(item.nodeid), "big") - 1)
_reseed(item.config, _crc32(item.nodeid) - 1)


def pytest_runtest_call(item: Item) -> None:
if item.config.getoption("randomly_reset_seed"):
_reseed(item.config, int.from_bytes(_md5(item.nodeid), "big"))
_reseed(item.config, _crc32(item.nodeid))


def pytest_runtest_teardown(item: Item) -> None:
if item.config.getoption("randomly_reset_seed"):
_reseed(item.config, int.from_bytes(_md5(item.nodeid), "big") + 1)
_reseed(item.config, _crc32(item.nodeid) + 1)


@hookimpl(tryfirst=True)
Expand All @@ -227,11 +228,11 @@ def pytest_collection_modifyitems(config: Config, items: list[Item]) -> None:
)
)

def _module_key(module_item: tuple[ModuleType | None, list[Item]]) -> bytes:
def _module_key(module_item: tuple[ModuleType | None, list[Item]]) -> int:
module, _items = module_item
if module is None:
return _md5(f"{seed}::None")
return _md5(f"{seed}::{module.__name__}")
return _crc32(f"{seed}::None")
return _crc32(f"{seed}::{module.__name__}")

modules_items.sort(key=_module_key)

Expand All @@ -248,19 +249,19 @@ def _get_module(item: Item) -> ModuleType | None:
def _shuffle_by_class(items: list[Item], seed: int) -> list[Item]:
klasses_items: list[tuple[type[Any] | None, list[Item]]] = []

def _item_key(item: Item) -> bytes:
return _md5(f"{seed}::{item.nodeid}")
def _item_key(item: Item) -> int:
return _crc32(f"{seed}::{item.nodeid}")

for klass, group in groupby(items, _get_cls):
klass_items = list(group)
klass_items.sort(key=_item_key)
klasses_items.append((klass, klass_items))

def _cls_key(klass_items: tuple[type[Any] | None, list[Item]]) -> bytes:
def _cls_key(klass_items: tuple[type[Any] | None, list[Item]]) -> int:
klass, items = klass_items
if klass is None:
return _md5(f"{seed}::None")
return _md5(f"{seed}::{klass.__module__}.{klass.__qualname__}")
return _crc32(f"{seed}::None")
return _crc32(f"{seed}::{klass.__module__}.{klass.__qualname__}")

klasses_items.sort(key=_cls_key)

Expand All @@ -282,19 +283,15 @@ def reduce_list_of_lists(lists: list[list[T]]) -> list[T]:


@lru_cache
def _md5(string: str) -> bytes:
hasher = hashlib.md5(usedforsecurity=False)
hasher.update(string.encode())
return hasher.digest()
def _crc32(string: str) -> int:
return crc32(string.encode())


if have_faker: # pragma: no branch

@fixture(autouse=True)
def faker_seed(pytestconfig: Config, request: SubRequest) -> int:
print(type(request))
result: int = pytestconfig.getoption("randomly_seed") + int.from_bytes(
_md5(request.node.nodeid),
"big",
result: int = pytestconfig.getoption("randomly_seed") + _crc32(
request.node.nodeid
)
return result
68 changes: 38 additions & 30 deletions tests/test_pytest_randomly.py
Original file line number Diff line number Diff line change
Expand Up @@ -248,10 +248,10 @@ def test_it():

out.assert_outcomes(passed=4, failed=0)
assert out.outlines[9:13] == [
"test_c.py::test_it PASSED",
"test_b.py::test_it PASSED",
"test_a.py::test_it PASSED",
"test_d.py::test_it PASSED",
"test_c.py::test_it PASSED",
"test_a.py::test_it PASSED",
]


Expand All @@ -268,10 +268,10 @@ def test_it():

out.assert_outcomes(passed=4, failed=0)
assert out.outlines[9:13] == [
"test_c.py::test_it PASSED",
"test_b.py::test_it PASSED",
"test_a.py::test_it PASSED",
"test_d.py::test_it PASSED",
"test_c.py::test_it PASSED",
"test_a.py::test_it PASSED",
]


Expand Down Expand Up @@ -308,9 +308,9 @@ def test_d(self):
out.assert_outcomes(passed=4, failed=0)
assert out.outlines[9:13] == [
"test_one.py::D::test_d PASSED",
"test_one.py::B::test_b PASSED",
"test_one.py::C::test_c PASSED",
"test_one.py::A::test_a PASSED",
"test_one.py::C::test_c PASSED",
"test_one.py::B::test_b PASSED",
]


Expand Down Expand Up @@ -341,8 +341,8 @@ def test_d(self):
assert out.outlines[9:13] == [
"test_one.py::T::test_c PASSED",
"test_one.py::T::test_b PASSED",
"test_one.py::T::test_a PASSED",
"test_one.py::T::test_d PASSED",
"test_one.py::T::test_a PASSED",
]


Expand All @@ -368,10 +368,10 @@ def test_d():

out.assert_outcomes(passed=4, failed=0)
assert out.outlines[9:13] == [
"test_one.py::test_c PASSED",
"test_one.py::test_d PASSED",
"test_one.py::test_a PASSED",
"test_one.py::test_c PASSED",
"test_one.py::test_b PASSED",
"test_one.py::test_d PASSED",
]


Expand Down Expand Up @@ -402,10 +402,10 @@ def test_d():

out.assert_outcomes(passed=4, failed=0)
assert out.outlines[9:13] == [
"test_one.py::test_c PASSED",
"test_one.py::test_d PASSED",
"test_one.py::test_a PASSED",
"test_one.py::test_c PASSED",
"test_one.py::test_b PASSED",
"test_one.py::test_d PASSED",
]


Expand Down Expand Up @@ -528,7 +528,15 @@ def test_b():
assert 0
"""
)
out = ourtester.runpytest("-v", "--randomly-seed=1", "--stepwise")
out = ourtester.runpytest("-v", "--randomly-seed=8")
out.assert_outcomes(failed=2)
# Ensure test_b runs first
assert out.outlines[9:11] == [
"test_one.py::test_b FAILED",
"test_one.py::test_a FAILED",
]

out = ourtester.runpytest("--randomly-seed=8", "--stepwise")
out.assert_outcomes(failed=1)

# Now make test_b pass
Expand All @@ -543,9 +551,9 @@ def test_b():
"""
)
shutil.rmtree(ourtester.path / "__pycache__")
out = ourtester.runpytest("-v", "--randomly-seed=1", "--stepwise")
out = ourtester.runpytest("--randomly-seed=8", "--stepwise")
out.assert_outcomes(passed=1, failed=1)
out = ourtester.runpytest("-v", "--randomly-seed=1", "--stepwise")
out = ourtester.runpytest("--randomly-seed=8", "--stepwise")
out.assert_outcomes(failed=1)


Expand Down Expand Up @@ -579,11 +587,11 @@ def test_factory_boy(ourtester):
from factory.random import randgen

def test_a():
assert randgen.random() == 0.9988532989147809
assert randgen.random() == 0.17867277194477893


def test_b():
assert randgen.random() == 0.18032546798434612
assert randgen.random() == 0.8026272812225962
"""
)

Expand All @@ -599,10 +607,10 @@ def test_faker(ourtester):
fake = Faker()

def test_one():
assert fake.name() == 'Mrs. Lisa Ryan'
assert fake.name() == 'Kimberly Powell'

def test_two():
assert fake.name() == 'Kaitlyn Mitchell'
assert fake.name() == 'Thomas Moyer PhD'
"""
)

Expand All @@ -614,10 +622,10 @@ def test_faker_fixture(ourtester):
ourtester.makepyfile(
test_one="""
def test_one(faker):
assert faker.name() == 'Mrs. Lisa Ryan'
assert faker.name() == 'Kimberly Powell'

def test_two(faker):
assert faker.name() == 'Kaitlyn Mitchell'
assert faker.name() == 'Thomas Moyer PhD'
"""
)

Expand All @@ -634,10 +642,10 @@ def test_model_bakery(ourtester):
from model_bakery.random_gen import gen_slug

def test_a():
assert gen_slug(10) == 'XjpU5br7ej'
assert gen_slug(10) == 'whwhAKeQYE'

def test_b():
assert gen_slug(10) == 'xJHS-PD_WT'
assert gen_slug(10) == 'o2N4p5UAXd'
"""
)

Expand All @@ -651,10 +659,10 @@ def test_numpy(ourtester):
import numpy as np

def test_one():
assert np.random.rand() == 0.36687834264514585
assert np.random.rand() == 0.1610140063074521

def test_two():
assert np.random.rand() == 0.7050715833365834
assert np.random.rand() == 0.6896867238957805
"""
)

Expand Down Expand Up @@ -718,19 +726,19 @@ def fake_entry_points(*, group):
assert reseed.mock_calls == [
mock.call(1),
mock.call(1),
mock.call(116362448262735926321257785636175308268),
mock.call(116362448262735926321257785636175308269),
mock.call(116362448262735926321257785636175308270),
mock.call(2964001072),
mock.call(2964001073),
mock.call(2964001074),
]

reseed.mock_calls[:] = []
pytester.runpytest_inprocess("--randomly-seed=424242")
assert reseed.mock_calls == [
mock.call(424242),
mock.call(424242),
mock.call(116362448262735926321257785636175732509),
mock.call(116362448262735926321257785636175732510),
mock.call(116362448262735926321257785636175732511),
mock.call(2964425313),
mock.call(2964425314),
mock.call(2964425315),
]


Expand Down