Skip to content

Commit 07234f4

Browse files
tcoratgerclaude
andauthored
test: support anchored full-message match on expected rejection (leanEthereum#1026)
Add an opt-in exact-message field to the negative-path expectation and route all three substring matchers through one shared helper. Existing substring vectors keep working unchanged. Opt one representative aggregated-gossip rejection into the anchored full-message check to exercise the new path, satisfying the repo's full-message assertion rule where a message disambiguates a reason shared by several code paths. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 4f96b22 commit 07234f4

3 files changed

Lines changed: 43 additions & 21 deletions

File tree

packages/testing/src/consensus_testing/test_fixtures/base.py

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,43 @@ class ExpectedRejection(StrictBaseModel):
5858
Fill-time self-check only; never serialized into vectors.
5959
"""
6060

61+
exact_message: str | None = None
62+
"""
63+
Full exception message the rejection must equal.
64+
65+
Opt-in for negative paths where the message disambiguates one reason
66+
shared by several code paths.
67+
When set, the raised message must equal this string exactly.
68+
Fill-time self-check only; never serialized into vectors.
69+
"""
70+
71+
def assert_message_matches(self, exception: Exception, context: str) -> None:
72+
"""
73+
Check the raised message against the authored expectation.
74+
75+
The exact match takes precedence over the substring when both are set.
76+
77+
Args:
78+
exception: The exception the negative path raised.
79+
context: Caller label woven into the failure message.
80+
81+
Raises:
82+
AssertionError: When the message contradicts the expectation.
83+
"""
84+
actual_message = str(exception)
85+
if self.exact_message is not None and actual_message != self.exact_message:
86+
raise AssertionError(
87+
f"{context} failed with wrong error message.\n"
88+
f" Expected exact message: {self.exact_message!r}\n"
89+
f" Actual message: {actual_message!r}"
90+
)
91+
if self.message_substring is not None and self.message_substring not in actual_message:
92+
raise AssertionError(
93+
f"{context} failed with wrong error message.\n"
94+
f" Expected message containing: {self.message_substring!r}\n"
95+
f" Actual message: {actual_message!r}"
96+
)
97+
6198

6299
class ProofSetting(IntEnum):
63100
"""Aggregation proof regime emitted with each fixture."""
@@ -227,12 +264,7 @@ def assert_expected_outcome(self, exception_raised: Exception | None) -> None:
227264
)
228265

229266
# A wrong message means the rejection fired for the wrong reason.
230-
expected_substring = self.expected_rejection.message_substring
231-
if expected_substring is not None and expected_substring not in str(exception_raised):
232-
raise AssertionError(
233-
f"Expected exception message containing {expected_substring!r} "
234-
f"but got '{exception_raised}'"
235-
)
267+
self.expected_rejection.assert_message_matches(exception_raised, "Verifier")
236268

237269
def resolve_rejection_reason(self, exception_raised: Exception) -> RejectionReason:
238270
"""

packages/testing/src/consensus_testing/test_fixtures/fork_choice.py

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -494,13 +494,7 @@ def _generate_invalid_anchor(
494494
validator_index=ValidatorIndex(0),
495495
)
496496
except SpecRejectionError as exception:
497-
expected_substring = self.expected_rejection.message_substring
498-
if expected_substring is not None and expected_substring not in str(exception):
499-
raise AssertionError(
500-
"Store.from_anchor failed with wrong error.\n"
501-
f" Expected error containing: {expected_substring!r}\n"
502-
f" Actual error: {exception!r}"
503-
) from exception
497+
self.expected_rejection.assert_message_matches(exception, "Store.from_anchor")
504498
# Emit the language-neutral reason clients assert against.
505499
return ForkChoiceFixture(
506500
anchor_state=self.anchor_state,
@@ -530,13 +524,9 @@ def _classify_step_rejection(
530524
# Verify the failure reason matches when specified.
531525
expected_rejection = step.expected_rejection
532526
if expected_rejection is not None:
533-
expected_substring = expected_rejection.message_substring
534-
if expected_substring is not None and expected_substring not in str(exception):
535-
raise AssertionError(
536-
f"Step {step_index} ({type(step).__name__}) failed with wrong error.\n"
537-
f" Expected error containing: {expected_substring!r}\n"
538-
f" Actual error: {exception!r}"
539-
) from exception
527+
expected_rejection.assert_message_matches(
528+
exception, f"Step {step_index} ({type(step).__name__})"
529+
)
540530

541531
# Emit the language-neutral reason clients assert against.
542532
rejection_reason = classify_rejection(exception)

tests/consensus/lstar/fork_choice/test_gossip_aggregated_attestation_validation.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,7 @@ def test_aggregated_attestation_target_slot_mismatch_rejected(
158158
valid=False,
159159
expected_rejection=ExpectedRejection(
160160
reason=RejectionReason.TARGET_SLOT_MISMATCH,
161-
message_substring="Target checkpoint slot mismatch",
161+
exact_message="Target checkpoint slot mismatch",
162162
),
163163
),
164164
]

0 commit comments

Comments
 (0)