Skip to content

Commit 7241def

Browse files
committed
WIP: LFPlugin: use sub-plugins to deselect during collection
Fixes pytest-dev#5301.
1 parent ac9bf7e commit 7241def

File tree

3 files changed

+105
-31
lines changed

3 files changed

+105
-31
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,9 +19,11 @@
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.config import Config
2125
from _pytest.main import Session
26+
from _pytest.python import Module
2227

2328
README_CONTENT = """\
2429
# pytest cache directory #
@@ -160,18 +165,83 @@ def _ensure_supporting_files(self):
160165
cachedir_tag_path.write_bytes(CACHEDIR_TAG_CONTENT)
161166

162167

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

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

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

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

380437

381438
@pytest.hookimpl(tryfirst=True)
382-
def pytest_configure(config):
439+
def pytest_configure(config: Config) -> None:
383440
config.cache = Cache.for_config(config)
384441
config.pluginmanager.register(LFPlugin(config), "lfplugin")
385442
config.pluginmanager.register(NFPlugin(config), "nfplugin")

src/_pytest/config/__init__.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -730,7 +730,7 @@ class InvocationParams:
730730
plugins = attr.ib()
731731
dir = attr.ib(type=Path)
732732

733-
def __init__(self, pluginmanager, *, invocation_params=None):
733+
def __init__(self, pluginmanager, *, invocation_params=None) -> None:
734734
from .argparsing import Parser, FILE_OR_DIR
735735

736736
if invocation_params is None:
@@ -759,6 +759,11 @@ def __init__(self, pluginmanager, *, invocation_params=None):
759759
kwargs=dict(parser=self._parser, pluginmanager=self.pluginmanager)
760760
)
761761

762+
if False: # TYPE_CHECKING
763+
from _pytest.cacheprovider import Cache
764+
765+
self.cache = None # type: Optional[Cache]
766+
762767
@property
763768
def invocation_dir(self):
764769
"""Backward compatibility"""

testing/test_cacheprovider.py

Lines changed: 26 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -261,9 +261,9 @@ def test_3(): assert 0
261261
result = testdir.runpytest(str(p), "--lf")
262262
result.stdout.fnmatch_lines(
263263
[
264-
"collected 3 items / 1 deselected / 2 selected",
264+
"collected 2 items",
265265
"run-last-failure: rerun previous 2 failures",
266-
"*2 passed*1 desel*",
266+
"*= 2 passed in *",
267267
]
268268
)
269269
result = testdir.runpytest(str(p), "--lf")
@@ -296,7 +296,7 @@ def test_failedfirst_order(self, testdir):
296296
# Test order will be failing tests first
297297
result.stdout.fnmatch_lines(
298298
[
299-
"collected 2 items / 1 deselected / 1 selected",
299+
"collected 2 items",
300300
"run-last-failure: rerun previous 1 failure first",
301301
"test_b.py*",
302302
"test_a.py*",
@@ -312,7 +312,7 @@ def test_lastfailed_failedfirst_order(self, testdir):
312312
# Test order will be collection order; alphabetical
313313
result.stdout.fnmatch_lines(["test_a.py*", "test_b.py*"])
314314
result = testdir.runpytest("--lf", "--ff")
315-
# Test order will be failing tests firs
315+
# Test order will be failing tests first
316316
result.stdout.fnmatch_lines(["test_b.py*"])
317317
result.stdout.no_fnmatch_line("*test_a.py*")
318318

@@ -337,7 +337,7 @@ def test_a2(): assert 1
337337
result = testdir.runpytest("--lf", p2)
338338
result.stdout.fnmatch_lines(["*1 passed*"])
339339
result = testdir.runpytest("--lf", p)
340-
result.stdout.fnmatch_lines(["*1 failed*1 desel*"])
340+
result.stdout.fnmatch_lines(["collected 1 item", "*= 1 failed in *"])
341341

342342
def test_lastfailed_usecase_splice(self, testdir, monkeypatch):
343343
monkeypatch.setattr("sys.dont_write_bytecode", True)
@@ -663,7 +663,13 @@ def test_bar_2(): pass
663663
assert self.get_cached_last_failed(testdir) == ["test_foo.py::test_foo_4"]
664664

665665
result = testdir.runpytest("--last-failed")
666-
result.stdout.fnmatch_lines(["*1 failed, 1 deselected*"])
666+
result.stdout.fnmatch_lines(
667+
[
668+
"collected 1 item",
669+
"run-last-failure: rerun previous 1 failure (skipped 1 file)",
670+
"*= 1 failed in *",
671+
]
672+
)
667673
assert self.get_cached_last_failed(testdir) == ["test_foo.py::test_foo_4"]
668674

669675
# 3. fix test_foo_4, run only test_foo.py
@@ -674,7 +680,13 @@ def test_foo_4(): pass
674680
"""
675681
)
676682
result = testdir.runpytest(test_foo, "--last-failed")
677-
result.stdout.fnmatch_lines(["*1 passed, 1 deselected*"])
683+
result.stdout.fnmatch_lines(
684+
[
685+
"collected 1 item",
686+
"run-last-failure: rerun previous 1 failure",
687+
"*= 1 passed in *",
688+
]
689+
)
678690
assert self.get_cached_last_failed(testdir) == []
679691

680692
result = testdir.runpytest("--last-failed")
@@ -747,9 +759,9 @@ def test_1(i):
747759
result = testdir.runpytest("--lf")
748760
result.stdout.fnmatch_lines(
749761
[
750-
"collected 5 items / 3 deselected / 2 selected",
762+
"collected 2 items",
751763
"run-last-failure: rerun previous 2 failures (skipped 1 file)",
752-
"*2 failed*3 deselected*",
764+
"*= 2 failed in *",
753765
]
754766
)
755767

@@ -764,9 +776,9 @@ def test_3(): pass
764776
result = testdir.runpytest("--lf")
765777
result.stdout.fnmatch_lines(
766778
[
767-
"collected 5 items / 3 deselected / 2 selected",
779+
"collected 2 items",
768780
"run-last-failure: rerun previous 2 failures (skipped 2 files)",
769-
"*2 failed*3 deselected*",
781+
"*= 2 failed in *",
770782
]
771783
)
772784

@@ -806,9 +818,9 @@ def test_lastfailed_with_known_failures_not_being_selected(self, testdir):
806818
result = testdir.runpytest("--lf")
807819
result.stdout.fnmatch_lines(
808820
[
809-
"collected 1 item",
810-
"run-last-failure: 1 known failures not in selected tests (skipped 1 file)",
811-
"* 1 failed in *",
821+
"collected 2 items",
822+
"run-last-failure: 1 known failures not in selected tests",
823+
"* 1 failed, 1 passed in *",
812824
]
813825
)
814826

0 commit comments

Comments
 (0)