Skip to content

Commit 1b9259c

Browse files
committed
feat: short-circuit rule evaluation on HARD_FAIL
1 parent a3a8c79 commit 1b9259c

3 files changed

Lines changed: 30 additions & 5 deletions

File tree

gateframe/cli/trend.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,7 @@ def _print_report(report) -> None: # noqa: ANN001
3737

3838
for ct in report.contract_trends:
3939
runs = len(ct.run_summaries)
40-
avg_pass = (
41-
sum(s.pass_rate for s in ct.run_summaries) / runs if runs else 0.0
42-
)
40+
avg_pass = sum(s.pass_rate for s in ct.run_summaries) / runs if runs else 0.0
4341
flag = " [REGRESSION]" if ct.regressed else ""
4442
print(f"\n {ct.contract_name}{flag}")
4543
print(f" direction : {ct.direction}")

gateframe/core/contract.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,19 +40,23 @@ def __init__(
4040

4141
def validate(self, output: Any, **context: Any) -> ValidationResult: # noqa: ANN401
4242
failures: list[FailureResult] = []
43+
rules_run = 0
4344

4445
for rule in self.rules:
46+
rules_run += 1
4547
result = rule.validate(output, **context)
4648
if result is not None:
4749
failures.append(result)
50+
if result.failure_mode is FailureMode.HARD_FAIL:
51+
break
4852

4953
failure_modes = {f.failure_mode for f in failures}
5054

5155
return ValidationResult(
5256
passed=len(failures) == 0,
5357
contract_name=self.name,
5458
failures=failures,
55-
rules_applied=len(self.rules),
59+
rules_applied=rules_run,
5660
rules_failed=len(failures),
5761
has_hard_fail=FailureMode.HARD_FAIL in failure_modes,
5862
has_soft_fail=FailureMode.SOFT_FAIL in failure_modes,

tests/core/test_contract.py

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,13 +48,36 @@ def test_single_failure(self) -> None:
4848
assert result.failures[0].rule_name == "b"
4949

5050
def test_multiple_failures(self) -> None:
51-
contract = ValidationContract("test", [_FailingRule("a"), _FailingRule("b")])
51+
contract = ValidationContract(
52+
"test",
53+
[_FailingRule("a", FailureMode.SOFT_FAIL), _FailingRule("b", FailureMode.SOFT_FAIL)],
54+
)
5255
result = contract.validate({})
5356
assert result.passed is False
5457
assert len(result.failures) == 2
5558
assert result.rules_applied == 2
5659
assert result.rules_failed == 2
5760

61+
def test_hard_fail_short_circuits_remaining_rules(self) -> None:
62+
contract = ValidationContract(
63+
"test",
64+
[_FailingRule("a", FailureMode.HARD_FAIL), _FailingRule("b", FailureMode.SOFT_FAIL)],
65+
)
66+
result = contract.validate({})
67+
assert result.has_hard_fail is True
68+
assert result.rules_applied == 1
69+
assert result.rules_failed == 1
70+
assert result.failures[0].rule_name == "a"
71+
72+
def test_soft_fail_does_not_short_circuit(self) -> None:
73+
contract = ValidationContract(
74+
"test",
75+
[_FailingRule("a", FailureMode.SOFT_FAIL), _FailingRule("b", FailureMode.HARD_FAIL)],
76+
)
77+
result = contract.validate({})
78+
assert result.rules_applied == 2
79+
assert result.rules_failed == 2
80+
5881
def test_empty_rules_list(self) -> None:
5982
contract = ValidationContract("empty", [])
6083
result = contract.validate("anything")

0 commit comments

Comments
 (0)