Skip to content

Commit 0738355

Browse files
committed
LFPlugin: use sub-plugins to deselect during collection
Fixes pytest-dev#5301.
1 parent a83114b commit 0738355

File tree

3 files changed

+104
-30
lines changed

3 files changed

+104
-30
lines changed

src/_pytest/cacheprovider.py

Lines changed: 73 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@
77
import json
88
import os
99
from collections import OrderedDict
10+
from typing import Dict
11+
from typing import Generator
1012
from typing import List
13+
from typing import Optional
1114

1215
import attr
1316
import py
@@ -16,10 +19,12 @@
1619
from .pathlib import Path
1720
from .pathlib import resolve_from_str
1821
from .pathlib import rm_rf
22+
from .reports import CollectReport
1923
from _pytest import nodes
2024
from _pytest._io import TerminalWriter
2125
from _pytest.config import Config
2226
from _pytest.main import Session
27+
from _pytest.python import Module
2328

2429
README_CONTENT = """\
2530
# pytest cache directory #
@@ -161,18 +166,83 @@ def _ensure_supporting_files(self):
161166
cachedir_tag_path.write_bytes(CACHEDIR_TAG_CONTENT)
162167

163168

169+
class LFPluginCollWrapper:
170+
def __init__(self, lfplugin: "LFPlugin"):
171+
self.lfplugin = lfplugin
172+
self._collected_at_least_one_failure = False
173+
174+
@pytest.hookimpl(hookwrapper=True)
175+
def pytest_make_collect_report(self, collector) -> Generator:
176+
lf_paths = self.lfplugin.last_failed_paths()
177+
if isinstance(collector, Session):
178+
out = yield
179+
res = out.get_result() # type: CollectReport
180+
181+
# Sort any lf-paths to the beginning.
182+
res.result = sorted(
183+
res.result, key=lambda x: 0 if Path(x.fspath) in lf_paths else 1
184+
)
185+
out.force_result(res)
186+
return
187+
188+
elif isinstance(collector, Module):
189+
if Path(collector.fspath) in lf_paths:
190+
out = yield
191+
res = out.get_result()
192+
193+
filtered_result = [
194+
x for x in res.result if x.nodeid in self.lfplugin.lastfailed
195+
]
196+
if filtered_result:
197+
res.result = filtered_result
198+
out.force_result(res)
199+
200+
if not self._collected_at_least_one_failure:
201+
self.lfplugin.config.pluginmanager.register(
202+
LFPluginCollSkipfiles(self.lfplugin), "lfplugin-collskip"
203+
)
204+
self._collected_at_least_one_failure = True
205+
return res
206+
yield
207+
208+
209+
class LFPluginCollSkipfiles:
210+
def __init__(self, lfplugin: "LFPlugin"):
211+
self.lfplugin = lfplugin
212+
213+
@pytest.hookimpl
214+
def pytest_make_collect_report(self, collector) -> Optional[CollectReport]:
215+
if isinstance(collector, Module):
216+
lf_paths = self.lfplugin.last_failed_paths()
217+
if Path(collector.fspath) not in lf_paths:
218+
self.lfplugin._skipped_files += 1
219+
220+
return CollectReport(
221+
collector.nodeid, "passed", longrepr=None, result=[]
222+
)
223+
return None
224+
225+
164226
class LFPlugin:
165227
""" Plugin which implements the --lf (run last-failing) option """
166228

167-
def __init__(self, config):
229+
def __init__(self, config: Config) -> None:
168230
self.config = config
169231
active_keys = "lf", "failedfirst"
170232
self.active = any(config.getoption(key) for key in active_keys)
171-
self.lastfailed = config.cache.get("cache/lastfailed", {})
233+
assert config.cache
234+
self.lastfailed = config.cache.get(
235+
"cache/lastfailed", {}
236+
) # type: Dict[str, bool]
172237
self._previously_failed_count = None
173238
self._report_status = None
174239
self._skipped_files = 0 # count skipped files during collection due to --lf
175240

241+
if config.getoption("lf"):
242+
config.pluginmanager.register(
243+
LFPluginCollWrapper(self), "lfplugin-collwrapper"
244+
)
245+
176246
def last_failed_paths(self):
177247
"""Returns a set with all Paths()s of the previously failed nodeids (cached).
178248
"""
@@ -185,19 +255,6 @@ def last_failed_paths(self):
185255
self._last_failed_paths = result
186256
return result
187257

188-
def pytest_ignore_collect(self, path):
189-
"""
190-
Ignore this file path if we are in --lf mode and it is not in the list of
191-
previously failed files.
192-
"""
193-
if self.active and self.config.getoption("lf") and path.isfile():
194-
last_failed_paths = self.last_failed_paths()
195-
if last_failed_paths:
196-
skip_it = Path(path) not in last_failed_paths
197-
if skip_it:
198-
self._skipped_files += 1
199-
return skip_it
200-
201258
def pytest_report_collectionfinish(self):
202259
if self.active and self.config.getoption("verbose") >= 0:
203260
return "run-last-failure: %s" % self._report_status
@@ -380,7 +437,7 @@ def pytest_cmdline_main(config):
380437

381438

382439
@pytest.hookimpl(tryfirst=True)
383-
def pytest_configure(config):
440+
def pytest_configure(config: Config) -> None:
384441
config.cache = Cache.for_config(config)
385442
config.pluginmanager.register(LFPlugin(config), "lfplugin")
386443
config.pluginmanager.register(NFPlugin(config), "nfplugin")

src/_pytest/config/__init__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -795,6 +795,11 @@ def __init__(self, pluginmanager, *, invocation_params=None) -> None:
795795
kwargs=dict(parser=self._parser, pluginmanager=self.pluginmanager)
796796
)
797797

798+
if False: # TYPE_CHECKING
799+
from _pytest.cacheprovider import Cache
800+
801+
self.cache = None # type: Optional[Cache]
802+
798803
@property
799804
def invocation_dir(self):
800805
"""Backward compatibility"""

testing/test_cacheprovider.py

Lines changed: 26 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -267,9 +267,9 @@ def test_3(): assert 0
267267
result = testdir.runpytest(str(p), "--lf")
268268
result.stdout.fnmatch_lines(
269269
[
270-
"collected 3 items / 1 deselected / 2 selected",
270+
"collected 2 items",
271271
"run-last-failure: rerun previous 2 failures",
272-
"*2 passed*1 desel*",
272+
"*= 2 passed in *",
273273
]
274274
)
275275
result = testdir.runpytest(str(p), "--lf")
@@ -304,7 +304,7 @@ def test_failedfirst_order(self, testdir):
304304
# Test order will be failing tests first
305305
result.stdout.fnmatch_lines(
306306
[
307-
"collected 2 items / 1 deselected / 1 selected",
307+
"collected 2 items",
308308
"run-last-failure: rerun previous 1 failure first",
309309
"test_b.py*",
310310
"test_a.py*",
@@ -320,7 +320,7 @@ def test_lastfailed_failedfirst_order(self, testdir):
320320
# Test order will be collection order; alphabetical
321321
result.stdout.fnmatch_lines(["test_a.py*", "test_b.py*"])
322322
result = testdir.runpytest("--lf", "--ff")
323-
# Test order will be failing tests firs
323+
# Test order will be failing tests first
324324
result.stdout.fnmatch_lines(["test_b.py*"])
325325
result.stdout.no_fnmatch_line("*test_a.py*")
326326

@@ -345,7 +345,7 @@ def test_a2(): assert 1
345345
result = testdir.runpytest("--lf", p2)
346346
result.stdout.fnmatch_lines(["*1 passed*"])
347347
result = testdir.runpytest("--lf", p)
348-
result.stdout.fnmatch_lines(["*1 failed*1 desel*"])
348+
result.stdout.fnmatch_lines(["collected 1 item", "*= 1 failed in *"])
349349

350350
def test_lastfailed_usecase_splice(self, testdir, monkeypatch):
351351
monkeypatch.setattr("sys.dont_write_bytecode", True)
@@ -671,7 +671,13 @@ def test_bar_2(): pass
671671
assert self.get_cached_last_failed(testdir) == ["test_foo.py::test_foo_4"]
672672

673673
result = testdir.runpytest("--last-failed")
674-
result.stdout.fnmatch_lines(["*1 failed, 1 deselected*"])
674+
result.stdout.fnmatch_lines(
675+
[
676+
"collected 1 item",
677+
"run-last-failure: rerun previous 1 failure (skipped 1 file)",
678+
"*= 1 failed in *",
679+
]
680+
)
675681
assert self.get_cached_last_failed(testdir) == ["test_foo.py::test_foo_4"]
676682

677683
# 3. fix test_foo_4, run only test_foo.py
@@ -682,7 +688,13 @@ def test_foo_4(): pass
682688
"""
683689
)
684690
result = testdir.runpytest(test_foo, "--last-failed")
685-
result.stdout.fnmatch_lines(["*1 passed, 1 deselected*"])
691+
result.stdout.fnmatch_lines(
692+
[
693+
"collected 1 item",
694+
"run-last-failure: rerun previous 1 failure",
695+
"*= 1 passed in *",
696+
]
697+
)
686698
assert self.get_cached_last_failed(testdir) == []
687699

688700
result = testdir.runpytest("--last-failed")
@@ -772,9 +784,9 @@ def test_1(i):
772784
result = testdir.runpytest("--lf")
773785
result.stdout.fnmatch_lines(
774786
[
775-
"collected 5 items / 3 deselected / 2 selected",
787+
"collected 2 items",
776788
"run-last-failure: rerun previous 2 failures (skipped 1 file)",
777-
"*2 failed*3 deselected*",
789+
"*= 2 failed in *",
778790
]
779791
)
780792

@@ -789,9 +801,9 @@ def test_3(): pass
789801
result = testdir.runpytest("--lf")
790802
result.stdout.fnmatch_lines(
791803
[
792-
"collected 5 items / 3 deselected / 2 selected",
804+
"collected 2 items",
793805
"run-last-failure: rerun previous 2 failures (skipped 2 files)",
794-
"*2 failed*3 deselected*",
806+
"*= 2 failed in *",
795807
]
796808
)
797809

@@ -831,9 +843,9 @@ def test_lastfailed_with_known_failures_not_being_selected(self, testdir):
831843
result = testdir.runpytest("--lf")
832844
result.stdout.fnmatch_lines(
833845
[
834-
"collected 1 item",
835-
"run-last-failure: 1 known failures not in selected tests (skipped 1 file)",
836-
"* 1 failed in *",
846+
"collected 2 items",
847+
"run-last-failure: 1 known failures not in selected tests",
848+
"* 1 failed, 1 passed in *",
837849
]
838850
)
839851

0 commit comments

Comments
 (0)