diff --git a/CHANGELOG.md b/CHANGELOG.md index e952df9..15d22fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +### Fixed +- The `least_duration` algorithm now splits deterministically regardless of the starting test order, even in cases where identically named test cases exist. ## [0.10.0] - 2024-10-16 ### Added diff --git a/src/pytest_split/algorithms.py b/src/pytest_split/algorithms.py index 8c47bd4..c7cb611 100644 --- a/src/pytest_split/algorithms.py +++ b/src/pytest_split/algorithms.py @@ -63,7 +63,7 @@ def __call__( # Sort by name to ensure it's always the same order items_with_durations_indexed = sorted( - items_with_durations_indexed, key=lambda tup: str(tup[0]) + items_with_durations_indexed, key=lambda tup: tup[0].nodeid ) # sort in ascending order diff --git a/tests/test_algorithms.py b/tests/test_algorithms.py index b352db7..c8203e3 100644 --- a/tests/test_algorithms.py +++ b/tests/test_algorithms.py @@ -1,5 +1,6 @@ import itertools from collections import namedtuple +from dataclasses import dataclass from typing import TYPE_CHECKING import pytest @@ -17,6 +18,17 @@ item = namedtuple("item", "nodeid") # noqa: PYI024 +@dataclass +class DummyPytestItem: + name: str + nodeid: str + + def __repr__(self) -> str: + return "<{} {}>".format(self.__class__.__name__, getattr(self, "name", None)) + + def __eq__(self, value: object) -> bool: + self.nodeid == value.nodeid + class TestAlgorithms: @pytest.mark.parametrize("algo_name", Algorithms.names()) def test__split_test(self, algo_name): @@ -140,6 +152,45 @@ def test__algorithms_members_derived_correctly(self): for a in Algorithms.names(): assert issubclass(Algorithms[a].value.__class__, AlgorithmBase) + def test__split_tests_correctly_same_names_with_real_items(self, tmp_path): + """Test that least_duration algorithm works correctly with real pytest Items + that have same names but different paths.""" + items = [ + DummyPytestItem( + name="test_something_a", nodeid="dir_a/test.py::test_something_a" + ), + DummyPytestItem( + name="test_something_a", nodeid="dir_b/test.py::test_something_a" + ), + DummyPytestItem( + name="test_something_b", nodeid="dir_a/test.py::test_something_b" + ), + DummyPytestItem( + name="test_something_b", nodeid="dir_b/test.py::test_something_b" + ), + ] + + first_randomization = (0, 1, 2, 3) + second_randomization = (1, 0, 3, 2) + + expected_groups = [[items[0], items[1]], [items[2], items[3]]] + + durations = {item.nodeid: 1 for item in items} + + algo = Algorithms["least_duration"].value + split_number = 2 + + for randomization in (first_randomization, second_randomization): + randomized_items = [items[index] for index in randomization] + splits = algo( + splits=split_number, items=randomized_items, durations=durations + ) + + for index, group in enumerate(splits): + assert ( + sorted(group.selected, key=lambda item: item.nodeid) + == expected_groups[index] + ) class MyAlgorithm(AlgorithmBase): def __call__(self, a, b, c):