stepwise.py 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122
  1. from typing import List
  2. from typing import Optional
  3. from typing import TYPE_CHECKING
  4. import pytest
  5. from _pytest import nodes
  6. from _pytest.config import Config
  7. from _pytest.config.argparsing import Parser
  8. from _pytest.main import Session
  9. from _pytest.reports import TestReport
  10. if TYPE_CHECKING:
  11. from _pytest.cacheprovider import Cache
  12. STEPWISE_CACHE_DIR = "cache/stepwise"
  13. def pytest_addoption(parser: Parser) -> None:
  14. group = parser.getgroup("general")
  15. group.addoption(
  16. "--sw",
  17. "--stepwise",
  18. action="store_true",
  19. default=False,
  20. dest="stepwise",
  21. help="exit on test failure and continue from last failing test next time",
  22. )
  23. group.addoption(
  24. "--sw-skip",
  25. "--stepwise-skip",
  26. action="store_true",
  27. default=False,
  28. dest="stepwise_skip",
  29. help="ignore the first failing test but stop on the next failing test.\n"
  30. "implicitly enables --stepwise.",
  31. )
  32. @pytest.hookimpl
  33. def pytest_configure(config: Config) -> None:
  34. if config.option.stepwise_skip:
  35. # allow --stepwise-skip to work on it's own merits.
  36. config.option.stepwise = True
  37. if config.getoption("stepwise"):
  38. config.pluginmanager.register(StepwisePlugin(config), "stepwiseplugin")
  39. def pytest_sessionfinish(session: Session) -> None:
  40. if not session.config.getoption("stepwise"):
  41. assert session.config.cache is not None
  42. # Clear the list of failing tests if the plugin is not active.
  43. session.config.cache.set(STEPWISE_CACHE_DIR, [])
  44. class StepwisePlugin:
  45. def __init__(self, config: Config) -> None:
  46. self.config = config
  47. self.session: Optional[Session] = None
  48. self.report_status = ""
  49. assert config.cache is not None
  50. self.cache: Cache = config.cache
  51. self.lastfailed: Optional[str] = self.cache.get(STEPWISE_CACHE_DIR, None)
  52. self.skip: bool = config.getoption("stepwise_skip")
  53. def pytest_sessionstart(self, session: Session) -> None:
  54. self.session = session
  55. def pytest_collection_modifyitems(
  56. self, config: Config, items: List[nodes.Item]
  57. ) -> None:
  58. if not self.lastfailed:
  59. self.report_status = "no previously failed tests, not skipping."
  60. return
  61. # check all item nodes until we find a match on last failed
  62. failed_index = None
  63. for index, item in enumerate(items):
  64. if item.nodeid == self.lastfailed:
  65. failed_index = index
  66. break
  67. # If the previously failed test was not found among the test items,
  68. # do not skip any tests.
  69. if failed_index is None:
  70. self.report_status = "previously failed test not found, not skipping."
  71. else:
  72. self.report_status = f"skipping {failed_index} already passed items."
  73. deselected = items[:failed_index]
  74. del items[:failed_index]
  75. config.hook.pytest_deselected(items=deselected)
  76. def pytest_runtest_logreport(self, report: TestReport) -> None:
  77. if report.failed:
  78. if self.skip:
  79. # Remove test from the failed ones (if it exists) and unset the skip option
  80. # to make sure the following tests will not be skipped.
  81. if report.nodeid == self.lastfailed:
  82. self.lastfailed = None
  83. self.skip = False
  84. else:
  85. # Mark test as the last failing and interrupt the test session.
  86. self.lastfailed = report.nodeid
  87. assert self.session is not None
  88. self.session.shouldstop = (
  89. "Test failed, continuing from this test next run."
  90. )
  91. else:
  92. # If the test was actually run and did pass.
  93. if report.when == "call":
  94. # Remove test from the failed ones, if exists.
  95. if report.nodeid == self.lastfailed:
  96. self.lastfailed = None
  97. def pytest_report_collectionfinish(self) -> Optional[str]:
  98. if self.config.getoption("verbose") >= 0 and self.report_status:
  99. return f"stepwise: {self.report_status}"
  100. return None
  101. def pytest_sessionfinish(self) -> None:
  102. self.cache.set(STEPWISE_CACHE_DIR, self.lastfailed)