Skip to content

Commit 1712196

Browse files
Merge pull request #2621 from nicoddemus/cumulative-cache
Make cache plugin cumulative
2 parents e97fd5e + eb1bd34 commit 1712196

File tree

3 files changed

+112
-15
lines changed

3 files changed

+112
-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.when == 'call' and report.passed) or report.skipped:
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: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -437,3 +437,102 @@ 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 test_xfail_not_considered_failure(self, testdir):
442+
testdir.makepyfile('''
443+
import pytest
444+
@pytest.mark.xfail
445+
def test():
446+
assert 0
447+
''')
448+
result = testdir.runpytest()
449+
result.stdout.fnmatch_lines('*1 xfailed*')
450+
assert self.get_cached_last_failed(testdir) == []
451+
452+
def test_xfail_strict_considered_failure(self, testdir):
453+
testdir.makepyfile('''
454+
import pytest
455+
@pytest.mark.xfail(strict=True)
456+
def test():
457+
pass
458+
''')
459+
result = testdir.runpytest()
460+
result.stdout.fnmatch_lines('*1 failed*')
461+
assert self.get_cached_last_failed(testdir) == ['test_xfail_strict_considered_failure.py::test']
462+
463+
@pytest.mark.parametrize('mark', ['mark.xfail', 'mark.skip'])
464+
def test_failed_changed_to_xfail_or_skip(self, testdir, mark):
465+
testdir.makepyfile('''
466+
import pytest
467+
def test():
468+
assert 0
469+
''')
470+
result = testdir.runpytest()
471+
assert self.get_cached_last_failed(testdir) == ['test_failed_changed_to_xfail_or_skip.py::test']
472+
assert result.ret == 1
473+
474+
testdir.makepyfile('''
475+
import pytest
476+
@pytest.{mark}
477+
def test():
478+
assert 0
479+
'''.format(mark=mark))
480+
result = testdir.runpytest()
481+
assert result.ret == 0
482+
assert self.get_cached_last_failed(testdir) == []
483+
assert result.ret == 0
484+
485+
def get_cached_last_failed(self, testdir):
486+
config = testdir.parseconfigure()
487+
return sorted(config.cache.get("cache/lastfailed", {}))
488+
489+
def test_cache_cumulative(self, testdir):
490+
"""
491+
Test workflow where user fixes errors gradually file by file using --lf.
492+
"""
493+
# 1. initial run
494+
test_bar = testdir.makepyfile(test_bar="""
495+
def test_bar_1():
496+
pass
497+
def test_bar_2():
498+
assert 0
499+
""")
500+
test_foo = testdir.makepyfile(test_foo="""
501+
def test_foo_3():
502+
pass
503+
def test_foo_4():
504+
assert 0
505+
""")
506+
testdir.runpytest()
507+
assert self.get_cached_last_failed(testdir) == ['test_bar.py::test_bar_2', 'test_foo.py::test_foo_4']
508+
509+
# 2. fix test_bar_2, run only test_bar.py
510+
testdir.makepyfile(test_bar="""
511+
def test_bar_1():
512+
pass
513+
def test_bar_2():
514+
pass
515+
""")
516+
result = testdir.runpytest(test_bar)
517+
result.stdout.fnmatch_lines('*2 passed*')
518+
# ensure cache does not forget that test_foo_4 failed once before
519+
assert self.get_cached_last_failed(testdir) == ['test_foo.py::test_foo_4']
520+
521+
result = testdir.runpytest('--last-failed')
522+
result.stdout.fnmatch_lines('*1 failed, 3 deselected*')
523+
assert self.get_cached_last_failed(testdir) == ['test_foo.py::test_foo_4']
524+
525+
# 3. fix test_foo_4, run only test_foo.py
526+
test_foo = testdir.makepyfile(test_foo="""
527+
def test_foo_3():
528+
pass
529+
def test_foo_4():
530+
pass
531+
""")
532+
result = testdir.runpytest(test_foo, '--last-failed')
533+
result.stdout.fnmatch_lines('*1 passed, 1 deselected*')
534+
assert self.get_cached_last_failed(testdir) == []
535+
536+
result = testdir.runpytest('--last-failed')
537+
result.stdout.fnmatch_lines('*4 passed*')
538+
assert self.get_cached_last_failed(testdir) == []

0 commit comments

Comments
 (0)