diff --git a/changelog/13390.improvement.rst b/changelog/13390.improvement.rst new file mode 100644 index 00000000000..0128024816b --- /dev/null +++ b/changelog/13390.improvement.rst @@ -0,0 +1 @@ +Add an option to rerun all tests in files with last-failed diff --git a/src/_pytest/cacheprovider.py b/src/_pytest/cacheprovider.py index 1b236efdc9b..6302f933211 100755 --- a/src/_pytest/cacheprovider.py +++ b/src/_pytest/cacheprovider.py @@ -289,6 +289,7 @@ def sort_key(node: nodes.Item | nodes.Collector) -> bool: x for x in result if x.nodeid in lastfailed + or collector.nodeid in lastfailed # Include any passed arguments (not trivial to filter). or session.isinitpath(x.path) # Keep all sub-collectors. @@ -317,7 +318,7 @@ def pytest_make_collect_report( class LFPlugin: - """Plugin which implements the --lf (run last-failing) option.""" + """Plugin which implements the --lf (run -last-failing) option.""" def __init__(self, config: Config) -> None: self.config = config @@ -335,6 +336,16 @@ def __init__(self, config: Config) -> None: LFPluginCollWrapper(self), "lfplugin-collwrapper" ) + if config.getoption("flf"): + self._last_failed_paths = self.get_last_failed_paths() + _lastfailed = self.lastfailed.copy() + for lf in self.lastfailed: + _lastfailed[lf.split("::")[0]] = self.lastfailed[lf] + self.lastfailed = _lastfailed + config.pluginmanager.register( + LFPluginCollWrapper(self), "lfplugin-collwrapper" + ) + def get_last_failed_paths(self) -> set[Path]: """Return a set with all Paths of the previously failed nodeids and their parents.""" @@ -485,6 +496,14 @@ def pytest_addoption(parser: Parser) -> None: help="Rerun only the tests that failed " "at the last run (or all if none failed)", ) + group.addoption( + "--lff", + "--last-failed-files", + action="store_true", + dest="flf", + help="rerun all tests in files with last-failed tests" + "at the last run (or all if none failed)", + ) group.addoption( "--ff", "--failed-first", diff --git a/testing/test_cacheprovider.py b/testing/test_cacheprovider.py index 94bc55d3047..e5efd43137c 100644 --- a/testing/test_cacheprovider.py +++ b/testing/test_cacheprovider.py @@ -438,6 +438,38 @@ def test_fail(val): result = pytester.runpytest() result.stdout.fnmatch_lines(["*1 failed in*"]) + def test_lastfailed_for_whole_files( + self, pytester: Pytester, monkeypatch: MonkeyPatch + ) -> None: + monkeypatch.setattr("sys.dont_write_bytecode", True) + test_a = pytester.makepyfile( + test_a=""" + def test_1(): assert 0 + def test_2(): assert 0 + def test_3(): assert 1 + """, + test_b=""" + def test_b1(): assert 1 + def test_b2(): assert 1 + """, + ) + result = pytester.runpytest() + result.stdout.fnmatch_lines(["*2 failed*3 passed*"]) + result = pytester.runpytest("--lff") + result.stdout.fnmatch_lines(["*2 failed*1 passed*"]) + + pytester.path.joinpath(".pytest_cache", ".git").mkdir(parents=True) + result = pytester.runpytest("--lff", "--cache-clear") + result.stdout.fnmatch_lines(["*2 failed*3 passed*"]) + assert pytester.path.joinpath(".pytest_cache", "README.md").is_file() + assert pytester.path.joinpath(".pytest_cache", ".git").is_dir() + + # Run this again to make sure clear-cache is robust + if os.path.isdir(".pytest_cache"): + shutil.rmtree(".pytest_cache") + result = pytester.runpytest("--lff", "--cache-clear") + result.stdout.fnmatch_lines(["*2 failed*3 passed*"]) + @pytest.mark.parametrize("parent", ("directory", "package")) def test_terminal_report_lastfailed(self, pytester: Pytester, parent: str) -> None: if parent == "package":