Skip to content

Commit 3e5a5b5

Browse files
committed
Make cache plugin always remember failed tests
1 parent 309152d commit 3e5a5b5

File tree

3 files changed

+68
-15
lines changed

3 files changed

+68
-15
lines changed

_pytest/cacheprovider.py

Lines changed: 11 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -105,27 +105,22 @@ def __init__(self, config):
105105
self.config = config
106106
active_keys = 'lf', 'failedfirst'
107107
self.active = any(config.getvalue(key) for key in active_keys)
108-
if self.active:
109-
self.lastfailed = config.cache.get("cache/lastfailed", {})
110-
else:
111-
self.lastfailed = {}
108+
self.lastfailed = config.cache.get("cache/lastfailed", {})
112109

113110
def pytest_report_header(self):
114111
if self.active:
115112
if not self.lastfailed:
116113
mode = "run all (no recorded failures)"
117114
else:
118-
mode = "rerun last %d failures%s" % (
119-
len(self.lastfailed),
115+
mode = "rerun previous failures%s" % (
120116
" first" if self.config.getvalue("failedfirst") else "")
121117
return "run-last-failure: %s" % mode
122118

123119
def pytest_runtest_logreport(self, report):
124-
if report.failed and "xfail" not in report.keywords:
120+
if report.passed and report.when == 'call':
121+
self.lastfailed.pop(report.nodeid, None)
122+
elif report.failed:
125123
self.lastfailed[report.nodeid] = True
126-
elif not report.failed:
127-
if report.when == "call":
128-
self.lastfailed.pop(report.nodeid, None)
129124

130125
def pytest_collectreport(self, report):
131126
passed = report.outcome in ('passed', 'skipped')
@@ -147,11 +142,11 @@ def pytest_collection_modifyitems(self, session, config, items):
147142
previously_failed.append(item)
148143
else:
149144
previously_passed.append(item)
150-
if not previously_failed and previously_passed:
145+
if not previously_failed:
151146
# running a subset of all tests with recorded failures outside
152147
# of the set of tests currently executing
153-
pass
154-
elif self.config.getvalue("lf"):
148+
return
149+
if self.config.getvalue("lf"):
155150
items[:] = previously_failed
156151
config.hook.pytest_deselected(items=previously_passed)
157152
else:
@@ -161,8 +156,9 @@ def pytest_sessionfinish(self, session):
161156
config = self.config
162157
if config.getvalue("cacheshow") or hasattr(config, "slaveinput"):
163158
return
164-
prev_failed = config.cache.get("cache/lastfailed", None) is not None
165-
if (session.testscollected and prev_failed) or self.lastfailed:
159+
160+
saved_lastfailed = config.cache.get("cache/lastfailed", {})
161+
if saved_lastfailed != self.lastfailed:
166162
config.cache.set("cache/lastfailed", self.lastfailed)
167163

168164

changelog/2621.feature

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
``--last-failed`` now remembers forever when a test has failed and only forgets it if it passes again. This makes it
2+
easy to fix a test suite by selectively running files and fixing tests incrementally.

testing/test_cache.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -437,3 +437,58 @@ def test_lastfailed_creates_cache_when_needed(self, testdir):
437437
testdir.makepyfile(test_errored='def test_error():\n assert False')
438438
testdir.runpytest('-q', '--lf')
439439
assert os.path.exists('.cache')
440+
441+
def get_cached_last_failed(self, testdir):
442+
config = testdir.parseconfigure()
443+
return sorted(config.cache.get("cache/lastfailed", {}))
444+
445+
def test_cache_cumulative(self, testdir):
446+
"""
447+
Test workflow where user fixes errors gradually file by file using --lf.
448+
"""
449+
# 1. initial run
450+
test_bar = testdir.makepyfile(test_bar="""
451+
def test_bar_1():
452+
pass
453+
def test_bar_2():
454+
assert 0
455+
""")
456+
test_foo = testdir.makepyfile(test_foo="""
457+
def test_foo_3():
458+
pass
459+
def test_foo_4():
460+
assert 0
461+
""")
462+
testdir.runpytest()
463+
assert self.get_cached_last_failed(testdir) == ['test_bar.py::test_bar_2', 'test_foo.py::test_foo_4']
464+
465+
# 2. fix test_bar_2, run only test_bar.py
466+
testdir.makepyfile(test_bar="""
467+
def test_bar_1():
468+
pass
469+
def test_bar_2():
470+
pass
471+
""")
472+
result = testdir.runpytest(test_bar)
473+
result.stdout.fnmatch_lines('*2 passed*')
474+
# ensure cache does not forget that test_foo_4 failed once before
475+
assert self.get_cached_last_failed(testdir) == ['test_foo.py::test_foo_4']
476+
477+
result = testdir.runpytest('--last-failed')
478+
result.stdout.fnmatch_lines('*1 failed, 3 deselected*')
479+
assert self.get_cached_last_failed(testdir) == ['test_foo.py::test_foo_4']
480+
481+
# 3. fix test_foo_4, run only test_foo.py
482+
test_foo = testdir.makepyfile(test_foo="""
483+
def test_foo_3():
484+
pass
485+
def test_foo_4():
486+
pass
487+
""")
488+
result = testdir.runpytest(test_foo, '--last-failed')
489+
result.stdout.fnmatch_lines('*1 passed, 1 deselected*')
490+
assert self.get_cached_last_failed(testdir) == []
491+
492+
result = testdir.runpytest('--last-failed')
493+
result.stdout.fnmatch_lines('*4 passed*')
494+
assert self.get_cached_last_failed(testdir) == []

0 commit comments

Comments
 (0)