You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

120 lines
4.1KB

  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. )
  31. @pytest.hookimpl
  32. def pytest_configure(config: Config) -> None:
  33. # We should always have a cache as cache provider plugin uses tryfirst=True
  34. if config.getoption("stepwise"):
  35. config.pluginmanager.register(StepwisePlugin(config), "stepwiseplugin")
  36. def pytest_sessionfinish(session: Session) -> None:
  37. if not session.config.getoption("stepwise"):
  38. assert session.config.cache is not None
  39. # Clear the list of failing tests if the plugin is not active.
  40. session.config.cache.set(STEPWISE_CACHE_DIR, [])
  41. class StepwisePlugin:
  42. def __init__(self, config: Config) -> None:
  43. self.config = config
  44. self.session: Optional[Session] = None
  45. self.report_status = ""
  46. assert config.cache is not None
  47. self.cache: Cache = config.cache
  48. self.lastfailed: Optional[str] = self.cache.get(STEPWISE_CACHE_DIR, None)
  49. self.skip: bool = config.getoption("stepwise_skip")
  50. def pytest_sessionstart(self, session: Session) -> None:
  51. self.session = session
  52. def pytest_collection_modifyitems(
  53. self, config: Config, items: List[nodes.Item]
  54. ) -> None:
  55. if not self.lastfailed:
  56. self.report_status = "no previously failed tests, not skipping."
  57. return
  58. # check all item nodes until we find a match on last failed
  59. failed_index = None
  60. for index, item in enumerate(items):
  61. if item.nodeid == self.lastfailed:
  62. failed_index = index
  63. break
  64. # If the previously failed test was not found among the test items,
  65. # do not skip any tests.
  66. if failed_index is None:
  67. self.report_status = "previously failed test not found, not skipping."
  68. else:
  69. self.report_status = f"skipping {failed_index} already passed items."
  70. deselected = items[:failed_index]
  71. del items[:failed_index]
  72. config.hook.pytest_deselected(items=deselected)
  73. def pytest_runtest_logreport(self, report: TestReport) -> None:
  74. if report.failed:
  75. if self.skip:
  76. # Remove test from the failed ones (if it exists) and unset the skip option
  77. # to make sure the following tests will not be skipped.
  78. if report.nodeid == self.lastfailed:
  79. self.lastfailed = None
  80. self.skip = False
  81. else:
  82. # Mark test as the last failing and interrupt the test session.
  83. self.lastfailed = report.nodeid
  84. assert self.session is not None
  85. self.session.shouldstop = (
  86. "Test failed, continuing from this test next run."
  87. )
  88. else:
  89. # If the test was actually run and did pass.
  90. if report.when == "call":
  91. # Remove test from the failed ones, if exists.
  92. if report.nodeid == self.lastfailed:
  93. self.lastfailed = None
  94. def pytest_report_collectionfinish(self) -> Optional[str]:
  95. if self.config.getoption("verbose") >= 0 and self.report_status:
  96. return f"stepwise: {self.report_status}"
  97. return None
  98. def pytest_sessionfinish(self) -> None:
  99. self.cache.set(STEPWISE_CACHE_DIR, self.lastfailed)