Skip to content

Commit 766fc23

Browse files
authored
Merge pull request #5185 from nicoddemus/lf-skip-files
--lf now skips colletion of files without failed tests
2 parents ebc0cea + 08734bd commit 766fc23

File tree

3 files changed

+97
-6
lines changed

3 files changed

+97
-6
lines changed

changelog/5172.feature.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
The ``--last-failed`` (``--lf``) option got smarter and will now skip entire files if all tests
2+
of that test file have passed in previous runs, greatly speeding up collection.

src/_pytest/cacheprovider.py

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,33 @@ def __init__(self, config):
158158
self.lastfailed = config.cache.get("cache/lastfailed", {})
159159
self._previously_failed_count = None
160160
self._report_status = None
161+
self._skipped_files = 0 # count skipped files during collection due to --lf
162+
163+
def last_failed_paths(self):
164+
"""Returns a set with all Paths()s of the previously failed nodeids (cached).
165+
"""
166+
result = getattr(self, "_last_failed_paths", None)
167+
if result is None:
168+
rootpath = Path(self.config.rootdir)
169+
result = {rootpath / nodeid.split("::")[0] for nodeid in self.lastfailed}
170+
self._last_failed_paths = result
171+
return result
172+
173+
def pytest_ignore_collect(self, path):
174+
"""
175+
Ignore this file path if we are in --lf mode and it is not in the list of
176+
previously failed files.
177+
"""
178+
if (
179+
self.active
180+
and self.config.getoption("lf")
181+
and path.isfile()
182+
and self.lastfailed
183+
):
184+
skip_it = Path(path) not in self.last_failed_paths()
185+
if skip_it:
186+
self._skipped_files += 1
187+
return skip_it
161188

162189
def pytest_report_collectionfinish(self):
163190
if self.active and self.config.getoption("verbose") >= 0:
@@ -206,9 +233,19 @@ def pytest_collection_modifyitems(self, session, config, items):
206233
items[:] = previously_failed + previously_passed
207234

208235
noun = "failure" if self._previously_failed_count == 1 else "failures"
236+
if self._skipped_files > 0:
237+
files_noun = "file" if self._skipped_files == 1 else "files"
238+
skipped_files_msg = " (skipped {files} {files_noun})".format(
239+
files=self._skipped_files, files_noun=files_noun
240+
)
241+
else:
242+
skipped_files_msg = ""
209243
suffix = " first" if self.config.getoption("failedfirst") else ""
210-
self._report_status = "rerun previous {count} {noun}{suffix}".format(
211-
count=self._previously_failed_count, suffix=suffix, noun=noun
244+
self._report_status = "rerun previous {count} {noun}{suffix}{skipped_files}".format(
245+
count=self._previously_failed_count,
246+
suffix=suffix,
247+
noun=noun,
248+
skipped_files=skipped_files_msg,
212249
)
213250
else:
214251
self._report_status = "no previously failed tests, "

testing/test_cacheprovider.py

Lines changed: 56 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -445,9 +445,9 @@ def test_b2():
445445
result = testdir.runpytest("--lf")
446446
result.stdout.fnmatch_lines(
447447
[
448-
"collected 4 items / 2 deselected / 2 selected",
449-
"run-last-failure: rerun previous 2 failures",
450-
"*2 failed, 2 deselected in*",
448+
"collected 2 items",
449+
"run-last-failure: rerun previous 2 failures (skipped 1 file)",
450+
"*2 failed in*",
451451
]
452452
)
453453

@@ -718,7 +718,7 @@ def test_bar_2():
718718
assert self.get_cached_last_failed(testdir) == ["test_foo.py::test_foo_4"]
719719

720720
result = testdir.runpytest("--last-failed")
721-
result.stdout.fnmatch_lines(["*1 failed, 3 deselected*"])
721+
result.stdout.fnmatch_lines(["*1 failed, 1 deselected*"])
722722
assert self.get_cached_last_failed(testdir) == ["test_foo.py::test_foo_4"]
723723

724724
# 3. fix test_foo_4, run only test_foo.py
@@ -779,6 +779,58 @@ def test_2():
779779
result = testdir.runpytest("--lf", "--cache-clear", "--lfnf", "none")
780780
result.stdout.fnmatch_lines(["*2 desel*"])
781781

782+
def test_lastfailed_skip_collection(self, testdir):
783+
"""
784+
Test --lf behavior regarding skipping collection of files that are not marked as
785+
failed in the cache (#5172).
786+
"""
787+
testdir.makepyfile(
788+
**{
789+
"pkg1/test_1.py": """
790+
import pytest
791+
792+
@pytest.mark.parametrize('i', range(3))
793+
def test_1(i): pass
794+
""",
795+
"pkg2/test_2.py": """
796+
import pytest
797+
798+
@pytest.mark.parametrize('i', range(5))
799+
def test_1(i):
800+
assert i not in (1, 3)
801+
""",
802+
}
803+
)
804+
# first run: collects 8 items (test_1: 3, test_2: 5)
805+
result = testdir.runpytest()
806+
result.stdout.fnmatch_lines(["collected 8 items", "*2 failed*6 passed*"])
807+
# second run: collects only 5 items from test_2, because all tests from test_1 have passed
808+
result = testdir.runpytest("--lf")
809+
result.stdout.fnmatch_lines(
810+
[
811+
"collected 5 items / 3 deselected / 2 selected",
812+
"run-last-failure: rerun previous 2 failures (skipped 1 file)",
813+
"*2 failed*3 deselected*",
814+
]
815+
)
816+
817+
# add another file and check if message is correct when skipping more than 1 file
818+
testdir.makepyfile(
819+
**{
820+
"pkg1/test_3.py": """
821+
def test_3(): pass
822+
"""
823+
}
824+
)
825+
result = testdir.runpytest("--lf")
826+
result.stdout.fnmatch_lines(
827+
[
828+
"collected 5 items / 3 deselected / 2 selected",
829+
"run-last-failure: rerun previous 2 failures (skipped 2 files)",
830+
"*2 failed*3 deselected*",
831+
]
832+
)
833+
782834

783835
class TestNewFirst(object):
784836
def test_newfirst_usecase(self, testdir):

0 commit comments

Comments
 (0)