Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1from typing import List 

2from typing import Optional 

3 

4import pytest 

5from _pytest import nodes 

6from _pytest.config import Config 

7from _pytest.config.argparsing import Parser 

8from _pytest.main import Session 

9from _pytest.reports import TestReport 

10 

11 

12def pytest_addoption(parser: Parser) -> None: 

13 group = parser.getgroup("general") 

14 group.addoption( 

15 "--sw", 

16 "--stepwise", 

17 action="store_true", 

18 dest="stepwise", 

19 help="exit on test failure and continue from last failing test next time", 

20 ) 

21 group.addoption( 

22 "--stepwise-skip", 

23 action="store_true", 

24 dest="stepwise_skip", 

25 help="ignore the first failing test but stop on the next failing test", 

26 ) 

27 

28 

29@pytest.hookimpl 

30def pytest_configure(config: Config) -> None: 

31 config.pluginmanager.register(StepwisePlugin(config), "stepwiseplugin") 

32 

33 

34class StepwisePlugin: 

35 def __init__(self, config: Config) -> None: 

36 self.config = config 

37 self.active = config.getvalue("stepwise") 

38 self.session = None # type: Optional[Session] 

39 self.report_status = "" 

40 

41 if self.active: 

42 assert config.cache is not None 

43 self.lastfailed = config.cache.get("cache/stepwise", None) 

44 self.skip = config.getvalue("stepwise_skip") 

45 

46 def pytest_sessionstart(self, session: Session) -> None: 

47 self.session = session 

48 

49 def pytest_collection_modifyitems( 

50 self, session: Session, config: Config, items: List[nodes.Item] 

51 ) -> None: 

52 if not self.active: 

53 return 

54 if not self.lastfailed: 

55 self.report_status = "no previously failed tests, not skipping." 

56 return 

57 

58 already_passed = [] 

59 found = False 

60 

61 # Make a list of all tests that have been run before the last failing one. 

62 for item in items: 

63 if item.nodeid == self.lastfailed: 

64 found = True 

65 break 

66 else: 

67 already_passed.append(item) 

68 

69 # If the previously failed test was not found among the test items, 

70 # do not skip any tests. 

71 if not found: 

72 self.report_status = "previously failed test not found, not skipping." 

73 already_passed = [] 

74 else: 

75 self.report_status = "skipping {} already passed items.".format( 

76 len(already_passed) 

77 ) 

78 

79 for item in already_passed: 

80 items.remove(item) 

81 

82 config.hook.pytest_deselected(items=already_passed) 

83 

84 def pytest_runtest_logreport(self, report: TestReport) -> None: 

85 if not self.active: 

86 return 

87 

88 if report.failed: 

89 if self.skip: 

90 # Remove test from the failed ones (if it exists) and unset the skip option 

91 # to make sure the following tests will not be skipped. 

92 if report.nodeid == self.lastfailed: 

93 self.lastfailed = None 

94 

95 self.skip = False 

96 else: 

97 # Mark test as the last failing and interrupt the test session. 

98 self.lastfailed = report.nodeid 

99 assert self.session is not None 

100 self.session.shouldstop = ( 

101 "Test failed, continuing from this test next run." 

102 ) 

103 

104 else: 

105 # If the test was actually run and did pass. 

106 if report.when == "call": 

107 # Remove test from the failed ones, if exists. 

108 if report.nodeid == self.lastfailed: 

109 self.lastfailed = None 

110 

111 def pytest_report_collectionfinish(self) -> Optional[str]: 

112 if self.active and self.config.getoption("verbose") >= 0 and self.report_status: 

113 return "stepwise: %s" % self.report_status 

114 return None 

115 

116 def pytest_sessionfinish(self, session: Session) -> None: 

117 assert self.config.cache is not None 

118 if self.active: 

119 self.config.cache.set("cache/stepwise", self.lastfailed) 

120 else: 

121 # Clear the list of failing tests if the plugin is not active. 

122 self.config.cache.set("cache/stepwise", [])