fix: sabotage check treats failed scenario steps as INVALID

StepFailed is no AssertionError any more; sabotage.py counts a JUnit failure
named StepFailed as error, so an expected-red test only counts when its own
assertion fails.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Tobias KaupatandClaude Sonnet 5.5 committed 2026-10-20 10:06:04 +02:00
1 parent 9be33e3c26
commit cb35fb296d
2 files changed
+24 -5

No files matched your search

+6 -2
View File
@@ -41,8 +41,12 @@ def raise_interrupt(signum: int, _frame: object) -> None:
raise KeyboardInterrupt(f"received {signal.Signals(signum).name}")
class StepFailed(AssertionError):
pass
class StepFailed(Exception):
"""A scenario step could not run; not an assertion about the behaviour under test.
Deliberately no AssertionError: sabotage.py tells it apart from a real assertion
failure by the exception name in the JUnit report.
"""
class Scenario:
+18 -3
View File
@@ -35,6 +35,9 @@ EXPECTED_RED = (
)
STEP_FAILED = "StepFailed" # name of helpers.StepFailed in the JUnit report
class Invalid(Exception):
"""The check itself is broken; the suite result means nothing."""
@@ -58,13 +61,25 @@ def sabotage_tree(tmp: Path) -> None:
def red_tests(report: Path) -> tuple[set[str], set[str]]:
"""Names of tests with an assertion failure, and of tests with an error (setup etc.)."""
"""Names of tests with an assertion failure, and of tests with an error.
Errors are setup errors and failed scenario steps (StepFailed): neither is an assertion
about the behaviour under test.
"""
try:
cases = list(ET.parse(report).getroot().iter("testcase"))
except ET.ParseError as exc:
raise Invalid(f"pytest report is malformed or truncated: {exc}") from exc
failures = {c.get("name", "") for c in cases if c.find("failure") is not None}
errors = {c.get("name", "") for c in cases if c.find("error") is not None}
failures = set()
for case in cases:
failure = case.find("failure")
if failure is None:
continue
if STEP_FAILED in failure.get("message") or "":
errors.add(case.get("name", ""))
else:
failures.add(case.get("name", ""))
return failures, errors
@@ -108,7 +123,7 @@ def main() -> int:
failures, errors = red_tests(report)
broken = [name for name in EXPECTED_RED if name in errors]
if broken:
raise Invalid(f"expected tests errored instead of failing an assertion: {broken}")
raise Invalid(f"expected tests errored or hit a failed scenario step instead of failing an assertion: {broken}")
missing = [name for name in EXPECTED_RED if name not in failures]
for name in missing:
print(