Skip to content

Commit 7b31a01

Browse files
tcoratgerclaude
andauthored
test(types/participation): expand coverage with edge and complex cases (leanEthereum#773)
Cover simple, edge, and complex configurations for AggregationBits and ValidatorIndices conversions: boundary indices at LIMIT - 1, full and sparse registries, duplicate and unsorted inputs, trimming behavior on round-trip, and exact-message assertions on every error path. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent dfd4648 commit 7b31a01

1 file changed

Lines changed: 190 additions & 27 deletions

File tree

tests/lean_spec/types/test_participation.py

Lines changed: 190 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -14,21 +14,47 @@
1414
class TestAggregationBitsToValidatorIndices:
1515
"""Convert an aggregation bitlist to the validator indices it encodes."""
1616

17-
def test_reject_empty_aggregation_bits(self) -> None:
18-
"""No bits set must raise."""
17+
def test_reject_zero_length_bits(self) -> None:
18+
"""A zero-length bitlist raises with the exact empty-aggregation message."""
19+
bits = AggregationBits(data=[])
20+
with pytest.raises(AssertionError) as exc_info:
21+
bits.to_validator_indices()
22+
assert str(exc_info.value) == "Aggregated attestation must reference at least one validator"
23+
24+
def test_reject_all_false_bits(self) -> None:
25+
"""A bitlist with no bits set raises with the exact empty-aggregation message."""
1926
bits = AggregationBits(data=[Boolean(False), Boolean(False), Boolean(False)])
20-
with pytest.raises(
21-
AssertionError,
22-
match="Aggregated attestation must reference at least one validator",
23-
):
27+
with pytest.raises(AssertionError) as exc_info:
2428
bits.to_validator_indices()
29+
assert str(exc_info.value) == "Aggregated attestation must reference at least one validator"
30+
31+
def test_single_bit_set_at_position_zero(self) -> None:
32+
"""Only bit 0 set yields the single index 0."""
33+
bits = AggregationBits(data=[Boolean(True)])
34+
assert bits.to_validator_indices() == ValidatorIndices(data=[ValidatorIndex(0)])
2535

26-
def test_single_bit_set(self) -> None:
27-
"""One bit set yields a single-index list."""
36+
def test_single_bit_set_in_middle(self) -> None:
37+
"""One non-leading bit set yields a single-index list at that position."""
2838
bits = AggregationBits(data=[Boolean(False), Boolean(True), Boolean(False)])
2939
assert bits.to_validator_indices() == ValidatorIndices(data=[ValidatorIndex(1)])
3040

31-
def test_multiple_bits_set(self) -> None:
41+
def test_single_bit_set_at_last_valid_position(self) -> None:
42+
"""Bitlist of length LIMIT with only the final bit set yields index LIMIT - 1."""
43+
data = [Boolean(False)] * (int(VALIDATOR_REGISTRY_LIMIT) - 1) + [Boolean(True)]
44+
bits = AggregationBits(data=data)
45+
assert bits.to_validator_indices() == ValidatorIndices(
46+
data=[ValidatorIndex(int(VALIDATOR_REGISTRY_LIMIT) - 1)]
47+
)
48+
49+
def test_all_bits_set_at_full_limit(self) -> None:
50+
"""All LIMIT bits set yields every validator index from 0 to LIMIT - 1 inclusive."""
51+
bits = AggregationBits(data=[Boolean(True)] * int(VALIDATOR_REGISTRY_LIMIT))
52+
expected = ValidatorIndices(
53+
data=[ValidatorIndex(i) for i in range(int(VALIDATOR_REGISTRY_LIMIT))]
54+
)
55+
assert bits.to_validator_indices() == expected
56+
57+
def test_multiple_bits_set_ascending(self) -> None:
3258
"""Multiple bits set yield indices in ascending order."""
3359
bits = AggregationBits(
3460
data=[Boolean(True), Boolean(False), Boolean(True), Boolean(True), Boolean(False)]
@@ -37,34 +63,88 @@ def test_multiple_bits_set(self) -> None:
3763
data=[ValidatorIndex(0), ValidatorIndex(2), ValidatorIndex(3)]
3864
)
3965

66+
def test_two_bits_set_at_extremes(self) -> None:
67+
"""Bits at positions 0 and LIMIT - 1 produce the two extreme indices in order."""
68+
data = (
69+
[Boolean(True)]
70+
+ [Boolean(False)] * (int(VALIDATOR_REGISTRY_LIMIT) - 2)
71+
+ [Boolean(True)]
72+
)
73+
bits = AggregationBits(data=data)
74+
assert bits.to_validator_indices() == ValidatorIndices(
75+
data=[ValidatorIndex(0), ValidatorIndex(int(VALIDATOR_REGISTRY_LIMIT) - 1)]
76+
)
77+
78+
def test_alternating_even_positions(self) -> None:
79+
"""Bits true at every even position from 0 to 10 yield exactly those indices."""
80+
data = [Boolean(i % 2 == 0) for i in range(11)]
81+
bits = AggregationBits(data=data)
82+
assert bits.to_validator_indices() == ValidatorIndices(
83+
data=[ValidatorIndex(i) for i in (0, 2, 4, 6, 8, 10)]
84+
)
85+
86+
def test_trailing_false_bits_are_ignored(self) -> None:
87+
"""Trailing false bits after the last set bit do not influence the index list."""
88+
data = [Boolean(False)] * 10
89+
data[2] = Boolean(True)
90+
bits = AggregationBits(data=data)
91+
assert bits.to_validator_indices() == ValidatorIndices(data=[ValidatorIndex(2)])
92+
4093

4194
class TestValidatorIndicesToAggregationBits:
4295
"""Convert a list of validator indices to an aggregation bitlist."""
4396

4497
def test_reject_empty_indices(self) -> None:
45-
"""An empty index list must raise."""
98+
"""An empty index list raises with the exact empty-aggregation message."""
4699
indices = ValidatorIndices(data=[])
47-
with pytest.raises(
48-
AssertionError,
49-
match="Aggregated attestation must reference at least one validator",
50-
):
100+
with pytest.raises(AssertionError) as exc_info:
51101
indices.to_aggregation_bits()
102+
assert str(exc_info.value) == "Aggregated attestation must reference at least one validator"
103+
104+
def test_single_index_zero(self) -> None:
105+
"""Single index 0 produces a one-bit bitlist with the only bit set."""
106+
indices = ValidatorIndices(data=[ValidatorIndex(0)])
107+
assert indices.to_aggregation_bits() == AggregationBits(data=[Boolean(True)])
108+
109+
def test_single_index_at_last_valid_position(self) -> None:
110+
"""Single index LIMIT - 1 produces a LIMIT-length bitlist with only the final bit true."""
111+
indices = ValidatorIndices(data=[ValidatorIndex(int(VALIDATOR_REGISTRY_LIMIT) - 1)])
112+
expected_data = [Boolean(False)] * (int(VALIDATOR_REGISTRY_LIMIT) - 1) + [Boolean(True)]
113+
assert indices.to_aggregation_bits() == AggregationBits(data=expected_data)
52114

53115
def test_reject_index_at_limit(self) -> None:
54-
"""An index equal to VALIDATOR_REGISTRY_LIMIT exceeds the bitfield range."""
55-
indices = ValidatorIndices(data=[ValidatorIndex(VALIDATOR_REGISTRY_LIMIT)])
56-
with pytest.raises(
57-
AssertionError, match="Validator index out of range for aggregation bits"
58-
):
116+
"""An index equal to LIMIT exceeds the bitfield range and raises exact message."""
117+
indices = ValidatorIndices(data=[ValidatorIndex(int(VALIDATOR_REGISTRY_LIMIT))])
118+
with pytest.raises(AssertionError) as exc_info:
59119
indices.to_aggregation_bits()
120+
assert str(exc_info.value) == "Validator index out of range for aggregation bits"
60121

61-
def test_reject_index_above_limit(self) -> None:
62-
"""An index above VALIDATOR_REGISTRY_LIMIT exceeds the bitfield range."""
63-
indices = ValidatorIndices(data=[ValidatorIndex(int(VALIDATOR_REGISTRY_LIMIT) + 100)])
64-
with pytest.raises(
65-
AssertionError, match="Validator index out of range for aggregation bits"
66-
):
122+
def test_reject_index_just_above_limit(self) -> None:
123+
"""An index of LIMIT + 1 raises the exact out-of-range message."""
124+
indices = ValidatorIndices(data=[ValidatorIndex(int(VALIDATOR_REGISTRY_LIMIT) + 1)])
125+
with pytest.raises(AssertionError) as exc_info:
67126
indices.to_aggregation_bits()
127+
assert str(exc_info.value) == "Validator index out of range for aggregation bits"
128+
129+
def test_reject_index_far_above_limit(self) -> None:
130+
"""An index well above LIMIT raises the exact out-of-range message."""
131+
indices = ValidatorIndices(data=[ValidatorIndex(2**20)])
132+
with pytest.raises(AssertionError) as exc_info:
133+
indices.to_aggregation_bits()
134+
assert str(exc_info.value) == "Validator index out of range for aggregation bits"
135+
136+
def test_one_bad_index_poisons_the_batch(self) -> None:
137+
"""A mix of valid indices and one out-of-range index raises the out-of-range message."""
138+
indices = ValidatorIndices(
139+
data=[
140+
ValidatorIndex(0),
141+
ValidatorIndex(1),
142+
ValidatorIndex(int(VALIDATOR_REGISTRY_LIMIT)),
143+
]
144+
)
145+
with pytest.raises(AssertionError) as exc_info:
146+
indices.to_aggregation_bits()
147+
assert str(exc_info.value) == "Validator index out of range for aggregation bits"
68148

69149
def test_deduplicates_repeated_indices(self) -> None:
70150
"""Repeated indices collapse to a single set bit per position."""
@@ -75,6 +155,13 @@ def test_deduplicates_repeated_indices(self) -> None:
75155
data=[Boolean(False), Boolean(True), Boolean(False), Boolean(True)]
76156
)
77157

158+
def test_all_duplicate_input(self) -> None:
159+
"""An input of repeated index 5 yields a length-6 bitlist with only the final bit true."""
160+
indices = ValidatorIndices(data=[ValidatorIndex(5)] * 4)
161+
assert indices.to_aggregation_bits() == AggregationBits(
162+
data=[Boolean(False)] * 5 + [Boolean(True)]
163+
)
164+
78165
def test_handles_unsorted_input(self) -> None:
79166
"""Unsorted indices produce a positionally correct bitfield."""
80167
indices = ValidatorIndices(data=[ValidatorIndex(5), ValidatorIndex(1), ValidatorIndex(3)])
@@ -89,14 +176,90 @@ def test_handles_unsorted_input(self) -> None:
89176
]
90177
)
91178

179+
def test_unsorted_with_duplicates(self) -> None:
180+
"""Unsorted input with duplicates yields a length-6 bitlist set at 1, 3, and 5."""
181+
indices = ValidatorIndices(
182+
data=[
183+
ValidatorIndex(5),
184+
ValidatorIndex(1),
185+
ValidatorIndex(3),
186+
ValidatorIndex(5),
187+
ValidatorIndex(1),
188+
]
189+
)
190+
assert indices.to_aggregation_bits() == AggregationBits(
191+
data=[
192+
Boolean(False),
193+
Boolean(True),
194+
Boolean(False),
195+
Boolean(True),
196+
Boolean(False),
197+
Boolean(True),
198+
]
199+
)
200+
92201
def test_length_matches_max_index(self) -> None:
93-
"""Returned bitfield has length max_index + 1 no trailing zeros."""
202+
"""Returned bitfield has length max_index + 1 with no trailing zeros."""
94203
indices = ValidatorIndices(data=[ValidatorIndex(3)])
95204
assert indices.to_aggregation_bits() == AggregationBits(
96205
data=[Boolean(False), Boolean(False), Boolean(False), Boolean(True)]
97206
)
98207

99-
def test_roundtrip_through_aggregation_bits(self) -> None:
208+
def test_dense_full_registry(self) -> None:
209+
"""Every index from 0 to LIMIT - 1 yields a LIMIT-length all-true bitlist."""
210+
indices = ValidatorIndices(
211+
data=[ValidatorIndex(i) for i in range(int(VALIDATOR_REGISTRY_LIMIT))]
212+
)
213+
assert indices.to_aggregation_bits() == AggregationBits(
214+
data=[Boolean(True)] * int(VALIDATOR_REGISTRY_LIMIT)
215+
)
216+
217+
def test_sparse_maximum(self) -> None:
218+
"""Indices 0 and LIMIT - 1 yield a LIMIT-length bitlist with only those two bits set."""
219+
indices = ValidatorIndices(
220+
data=[ValidatorIndex(0), ValidatorIndex(int(VALIDATOR_REGISTRY_LIMIT) - 1)]
221+
)
222+
expected_data = (
223+
[Boolean(True)]
224+
+ [Boolean(False)] * (int(VALIDATOR_REGISTRY_LIMIT) - 2)
225+
+ [Boolean(True)]
226+
)
227+
assert indices.to_aggregation_bits() == AggregationBits(data=expected_data)
228+
229+
230+
class TestRoundTrip:
231+
"""Round-trip symmetry between AggregationBits and ValidatorIndices."""
232+
233+
def test_roundtrip_indices_to_bits_to_indices(self) -> None:
100234
"""Indices to bits and back yields the original input."""
101235
original = ValidatorIndices(data=[ValidatorIndex(1), ValidatorIndex(5), ValidatorIndex(7)])
102236
assert original.to_aggregation_bits().to_validator_indices() == original
237+
238+
def test_roundtrip_bits_to_indices_to_bits_preserves_last_true(self) -> None:
239+
"""Bits to indices and back is exact when the bitlist ends with a true bit."""
240+
original = AggregationBits(
241+
data=[
242+
Boolean(False),
243+
Boolean(True),
244+
Boolean(False),
245+
Boolean(True),
246+
Boolean(True),
247+
Boolean(False),
248+
Boolean(False),
249+
Boolean(True),
250+
]
251+
)
252+
assert original.to_validator_indices().to_aggregation_bits() == original
253+
254+
def test_roundtrip_bits_to_indices_to_bits_trims_trailing_false(self) -> None:
255+
"""Round-trip trims trailing false bits because output is sized by max index plus one."""
256+
original = AggregationBits(
257+
data=[Boolean(False), Boolean(True), Boolean(False), Boolean(False)]
258+
)
259+
trimmed = AggregationBits(data=[Boolean(False), Boolean(True)])
260+
assert original.to_validator_indices().to_aggregation_bits() == trimmed
261+
262+
def test_roundtrip_boundary_index(self) -> None:
263+
"""Round-trip starting from the highest valid index preserves the input exactly."""
264+
original = ValidatorIndices(data=[ValidatorIndex(int(VALIDATOR_REGISTRY_LIMIT) - 1)])
265+
assert original.to_aggregation_bits().to_validator_indices() == original

0 commit comments

Comments
 (0)