stepwise.py 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130
  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. "
  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. if hasattr(session.config, "workerinput"):
  43. # Do not update cache if this process is a xdist worker to prevent
  44. # race conditions (#10641).
  45. return
  46. # Clear the list of failing tests if the plugin is not active.
  47. session.config.cache.set(STEPWISE_CACHE_DIR, [])
  48. class StepwisePlugin:
  49. def __init__(self, config: Config) -> None:
  50. self.config = config
  51. self.session: Optional[Session] = None
  52. self.report_status = ""
  53. assert config.cache is not None
  54. self.cache: Cache = config.cache
  55. self.lastfailed: Optional[str] = self.cache.get(STEPWISE_CACHE_DIR, None)
  56. self.skip: bool = config.getoption("stepwise_skip")
  57. def pytest_sessionstart(self, session: Session) -> None:
  58. self.session = session
  59. def pytest_collection_modifyitems(
  60. self, config: Config, items: List[nodes.Item]
  61. ) -> None:
  62. if not self.lastfailed:
  63. self.report_status = "no previously failed tests, not skipping."
  64. return
  65. # check all item nodes until we find a match on last failed
  66. failed_index = None
  67. for index, item in enumerate(items):
  68. if item.nodeid == self.lastfailed:
  69. failed_index = index
  70. break
  71. # If the previously failed test was not found among the test items,
  72. # do not skip any tests.
  73. if failed_index is None:
  74. self.report_status = "previously failed test not found, not skipping."
  75. else:
  76. self.report_status = f"skipping {failed_index} already passed items."
  77. deselected = items[:failed_index]
  78. del items[:failed_index]
  79. config.hook.pytest_deselected(items=deselected)
  80. def pytest_runtest_logreport(self, report: TestReport) -> None:
  81. if report.failed:
  82. if self.skip:
  83. # Remove test from the failed ones (if it exists) and unset the skip option
  84. # to make sure the following tests will not be skipped.
  85. if report.nodeid == self.lastfailed:
  86. self.lastfailed = None
  87. self.skip = False
  88. else:
  89. # Mark test as the last failing and interrupt the test session.
  90. self.lastfailed = report.nodeid
  91. assert self.session is not None
  92. self.session.shouldstop = (
  93. "Test failed, continuing from this test next run."
  94. )
  95. else:
  96. # If the test was actually run and did pass.
  97. if report.when == "call":
  98. # Remove test from the failed ones, if exists.
  99. if report.nodeid == self.lastfailed:
  100. self.lastfailed = None
  101. def pytest_report_collectionfinish(self) -> Optional[str]:
  102. if self.config.getoption("verbose") >= 0 and self.report_status:
  103. return f"stepwise: {self.report_status}"
  104. return None
  105. def pytest_sessionfinish(self) -> None:
  106. if hasattr(self.config, "workerinput"):
  107. # Do not update cache if this process is a xdist worker to prevent
  108. # race conditions (#10641).
  109. return
  110. self.cache.set(STEPWISE_CACHE_DIR, self.lastfailed)