diff --git a/CHANGELOG.rst b/CHANGELOG.rst index cdfdb1f..fd572dc 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -14,6 +14,10 @@ Unreleased `PR #687 `__. Thanks to Bryce Drennan for the suggestion in `Issue #600 `__ and initial implementation in `PR #617 `__. +* Move from MD5 to CRC32 for hashing test IDs, as it’s 5x faster and we don’t need cryptographic security. + + `Issue #686 `__. + 3.16.0 (2024-10-25) ------------------- diff --git a/src/pytest_randomly/__init__.py b/src/pytest_randomly/__init__.py index cab83c6..e877b1d 100644 --- a/src/pytest_randomly/__init__.py +++ b/src/pytest_randomly/__init__.py @@ -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 @@ -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) @@ -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) @@ -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) @@ -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 diff --git a/tests/test_pytest_randomly.py b/tests/test_pytest_randomly.py index fdeb26c..7d41a15 100644 --- a/tests/test_pytest_randomly.py +++ b/tests/test_pytest_randomly.py @@ -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", ] @@ -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", ] @@ -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", ] @@ -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", ] @@ -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", ] @@ -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", ] @@ -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 @@ -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) @@ -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 """ ) @@ -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' """ ) @@ -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' """ ) @@ -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' """ ) @@ -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 """ ) @@ -718,9 +726,9 @@ 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[:] = [] @@ -728,9 +736,9 @@ def fake_entry_points(*, group): 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), ]