diff --git a/analysis/analyze.py b/analysis/analyze.py index c344ce4..8ae1292 100644 --- a/analysis/analyze.py +++ b/analysis/analyze.py @@ -663,6 +663,14 @@ def extract_foundry_failure(payload: Dict[str, Any]) -> Tuple[Optional[str], Opt return None, None, None if str(payload.get("event") or "").strip() == "failure": + failure_type = str(payload.get("failure_type") or "").strip() + target = str(payload.get("target") or "").strip() + selector = str(payload.get("selector") or "").strip() + if failure_type == "handler_assertion" and target and selector: + # Current Foundry records handler failures as a target address and + # selector. The address alone collapses every assertion on a handler. + return f"{target}:{selector}", ts_value, "foundry-failure-event" + # Prefer the per-invariant identity so distinct invariant failures are # counted as distinct bugs. The `target` field is the harness contract # (e.g. "CryticToFoundry"), which is identical across every invariant and diff --git a/analysis/tests/test_analyze_foundry_parser.py b/analysis/tests/test_analyze_foundry_parser.py index b1e4d57..fdda6fb 100644 --- a/analysis/tests/test_analyze_foundry_parser.py +++ b/analysis/tests/test_analyze_foundry_parser.py @@ -85,6 +85,28 @@ def test_parses_fail_on_assert_failure_events(self): self.assertAlmostEqual(events[0].elapsed_seconds, 0.0) self.assertAlmostEqual(events[1].elapsed_seconds, 3.0) + def test_distinguishes_current_handler_assertions_without_a_following_pulse(self): + log_path = self.write_log( + [ + '{"timestamp":100,"event":"pulse","metrics":{"unique_failures":0,"broken_assertions":0}}', + '{"timestamp":101,"event":"failure","failure_type":"handler_assertion","target":"0xabc","selector":"0x11111111","reason":"assertion failed"}', + '{"timestamp":102,"event":"failure","failure_type":"handler_assertion","target":"0xabc","selector":"0x22222222","reason":"assertion failed"}', + '{"timestamp":103,"event":"failure","failure_type":"handler_assertion","target":"0xabc","selector":"0x11111111","reason":"assertion failed"}', + ] + ) + + events = analyze.parse_foundry_log(log_path, "run-1", "i-1", "foundry-git-test") + self.assertEqual( + [event.event for event in events], + ["0xabc:0x11111111", "0xabc:0x22222222"], + ) + self.assertEqual( + [event.source for event in events], + ["foundry-failure-event", "foundry-failure-event"], + ) + self.assertAlmostEqual(events[0].elapsed_seconds, 1.0) + self.assertAlmostEqual(events[1].elapsed_seconds, 2.0) + def test_promotes_broken_handler_metrics_to_bug_events(self): log_path = self.write_log( [