Skip to content

Commit 14d3081

Browse files
tcoratgerclaude
andauthored
test: add 100% coverage for BaseUint arithmetic operators (leanEthereum#530)
Closes leanEthereum#521. Adds 116 new tests (275 total) achieving 100% isolated coverage for types/uint.py (197 statements, 56 branches, 0 missing). Covers all previously uncovered operator paths: - Forward arithmetic type errors (__add__, __sub__, etc. with plain int) - Reverse arithmetic success paths (__radd__, __rsub__, etc.) - Exponentiation with modulo and reverse pow - Reverse divmod - Reverse bitwise delegation (__rand__, __ror__, __rxor__) - Reverse shift operators (__rlshift__, __rrshift__) - Comparison type errors (__lt__, __le__ with plain int) - __index__ returns plain int Spec audit confirms SSZ serialization, deserialization, ranges, and fixed-size properties are all correct per the SSZ specification. Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent d3118f5 commit 14d3081

1 file changed

Lines changed: 254 additions & 0 deletions

File tree

tests/lean_spec/types/test_uint.py

Lines changed: 254 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,10 @@ def test_bitwise_operators(uint_class: Type[BaseUint]) -> None:
206206

207207
with pytest.raises(TypeError):
208208
_ = a & 1
209+
with pytest.raises(TypeError):
210+
_ = a | 1
211+
with pytest.raises(TypeError):
212+
_ = a ^ 1
209213

210214

211215
@pytest.mark.parametrize("uint_class", ALL_UINT_TYPES)
@@ -420,3 +424,253 @@ def test_deserialize_stream_too_short(self, uint_class: Type[BaseUint]) -> None:
420424
stream = io.BytesIO(b"\x00" * (byte_length - 1))
421425
with pytest.raises(SSZSerializationError, match="expected .* bytes, got"):
422426
uint_class.deserialize(stream, scope=byte_length)
427+
428+
429+
class TestForwardArithmeticTypeErrors:
430+
"""Tests that forward arithmetic operators reject plain int operands.
431+
432+
When calling e.g. Uint64(5).__add__(3), the forward operator must raise
433+
TypeError because 3 is a plain int, not a BaseUint subclass.
434+
"""
435+
436+
@pytest.mark.parametrize("uint_class", ALL_UINT_TYPES)
437+
@pytest.mark.parametrize(
438+
"method, op_symbol",
439+
[
440+
("__add__", r"\+"),
441+
("__sub__", r"-"),
442+
("__mul__", r"\*"),
443+
("__floordiv__", r"//"),
444+
("__mod__", r"%"),
445+
],
446+
)
447+
def test_forward_operator_rejects_plain_int(
448+
self, uint_class: Type[BaseUint], method: str, op_symbol: str
449+
) -> None:
450+
"""Forward arithmetic operator raises TypeError when given a plain int."""
451+
# Call the dunder method directly with a plain int operand.
452+
with pytest.raises(TypeError, match=op_symbol):
453+
getattr(uint_class(5), method)(3)
454+
455+
456+
class TestReverseArithmeticSuccessPaths:
457+
"""Tests that reverse arithmetic operators succeed when both operands are BaseUint.
458+
459+
Calling the reverse dunder directly (e.g. Uint64(3).__radd__(Uint64(5)))
460+
exercises the success return path of each reverse operator.
461+
"""
462+
463+
@pytest.mark.parametrize("uint_class", ALL_UINT_TYPES)
464+
def test_radd_success(self, uint_class: Type[BaseUint]) -> None:
465+
"""Reverse add returns the correct sum when called directly."""
466+
# __radd__(other) computes other + self
467+
result = uint_class(3).__radd__(uint_class(5))
468+
assert result == uint_class(8)
469+
assert isinstance(result, uint_class)
470+
471+
@pytest.mark.parametrize("uint_class", ALL_UINT_TYPES)
472+
def test_rsub_success(self, uint_class: Type[BaseUint]) -> None:
473+
"""Reverse sub returns the correct difference when called directly."""
474+
# __rsub__(other) computes other - self
475+
result = uint_class(3).__rsub__(uint_class(10))
476+
assert result == uint_class(7)
477+
assert isinstance(result, uint_class)
478+
479+
@pytest.mark.parametrize("uint_class", ALL_UINT_TYPES)
480+
def test_rmul_success(self, uint_class: Type[BaseUint]) -> None:
481+
"""Reverse mul returns the correct product when called directly."""
482+
# __rmul__(other) computes other * self
483+
result = uint_class(3).__rmul__(uint_class(5))
484+
assert result == uint_class(15)
485+
assert isinstance(result, uint_class)
486+
487+
@pytest.mark.parametrize("uint_class", ALL_UINT_TYPES)
488+
def test_rfloordiv_success(self, uint_class: Type[BaseUint]) -> None:
489+
"""Reverse floordiv returns the correct quotient when called directly."""
490+
# __rfloordiv__(other) computes other // self
491+
result = uint_class(3).__rfloordiv__(uint_class(10))
492+
assert result == uint_class(3)
493+
assert isinstance(result, uint_class)
494+
495+
@pytest.mark.parametrize("uint_class", ALL_UINT_TYPES)
496+
def test_rmod_success(self, uint_class: Type[BaseUint]) -> None:
497+
"""Reverse mod returns the correct remainder when called directly."""
498+
# __rmod__(other) computes other % self
499+
result = uint_class(3).__rmod__(uint_class(10))
500+
assert result == uint_class(1)
501+
assert isinstance(result, uint_class)
502+
503+
504+
class TestPowAndRpow:
505+
"""Tests for exponentiation operators including modulo and reverse paths."""
506+
507+
@pytest.mark.parametrize("uint_class", ALL_UINT_TYPES)
508+
def test_pow_with_modulo(self, uint_class: Type[BaseUint]) -> None:
509+
"""Three-argument pow(base, exp, mod) validates the modulo and returns correct result."""
510+
# pow(2, 10, 100) == 1024 % 100 == 24
511+
result = pow(uint_class(2), 10, 100)
512+
assert result == uint_class(24)
513+
assert isinstance(result, uint_class)
514+
515+
@pytest.mark.parametrize("uint_class", ALL_UINT_TYPES)
516+
def test_pow_with_bool_modulo_raises(self, uint_class: Type[BaseUint]) -> None:
517+
"""Three-argument pow rejects a bool as the modulo operand."""
518+
with pytest.raises(TypeError, match=r"expected 'int' but got 'bool'"):
519+
pow(uint_class(2), 10, True)
520+
521+
@pytest.mark.parametrize("uint_class", ALL_UINT_TYPES)
522+
def test_rpow_success(self, uint_class: Type[BaseUint]) -> None:
523+
"""Reverse pow computes base ** self when called directly."""
524+
# __rpow__(base) computes base ** self => 2 ** 3 == 8
525+
result = uint_class(3).__rpow__(uint_class(2))
526+
assert result == uint_class(8)
527+
assert isinstance(result, uint_class)
528+
529+
@pytest.mark.parametrize("uint_class", ALL_UINT_TYPES)
530+
def test_rpow_rejects_bool(self, uint_class: Type[BaseUint]) -> None:
531+
"""Reverse pow rejects a bool as the base operand."""
532+
with pytest.raises(TypeError, match=r"expected 'int' but got 'bool'"):
533+
uint_class(3).__rpow__(True)
534+
535+
536+
class TestValidateIntOperand:
537+
"""Tests for _validate_int_operand which rejects bools and non-ints."""
538+
539+
@pytest.mark.parametrize("uint_class", ALL_UINT_TYPES)
540+
def test_pow_rejects_bool_exponent(self, uint_class: Type[BaseUint]) -> None:
541+
"""Exponentiation rejects a bool as the exponent."""
542+
with pytest.raises(TypeError, match=r"expected 'int' but got 'bool'"):
543+
uint_class(2) ** True
544+
545+
@pytest.mark.parametrize("uint_class", ALL_UINT_TYPES)
546+
def test_pow_rejects_string_exponent(self, uint_class: Type[BaseUint]) -> None:
547+
"""Exponentiation rejects a string as the exponent."""
548+
with pytest.raises(TypeError, match=r"expected 'int' but got 'str'"):
549+
uint_class(2) ** "3" # type: ignore[operator]
550+
551+
@pytest.mark.parametrize("uint_class", ALL_UINT_TYPES)
552+
def test_lshift_rejects_bool(self, uint_class: Type[BaseUint]) -> None:
553+
"""Left shift rejects a bool as the shift amount."""
554+
with pytest.raises(TypeError, match=r"expected 'int' but got 'bool'"):
555+
uint_class(1) << True
556+
557+
@pytest.mark.parametrize("uint_class", ALL_UINT_TYPES)
558+
def test_rshift_rejects_bool(self, uint_class: Type[BaseUint]) -> None:
559+
"""Right shift rejects a bool as the shift amount."""
560+
with pytest.raises(TypeError, match=r"expected 'int' but got 'bool'"):
561+
uint_class(8) >> True
562+
563+
564+
class TestDivmodEdgeCases:
565+
"""Tests for divmod type error and reverse divmod paths."""
566+
567+
@pytest.mark.parametrize("uint_class", ALL_UINT_TYPES)
568+
def test_divmod_rejects_plain_int(self, uint_class: Type[BaseUint]) -> None:
569+
"""Forward divmod raises TypeError when the divisor is a plain int."""
570+
with pytest.raises(TypeError, match="divmod"):
571+
divmod(uint_class(10), 3) # type: ignore[call-overload]
572+
573+
@pytest.mark.parametrize("uint_class", ALL_UINT_TYPES)
574+
def test_rdivmod_success(self, uint_class: Type[BaseUint]) -> None:
575+
"""Reverse divmod returns correct (quotient, remainder) when called directly."""
576+
# __rdivmod__(other) computes divmod(other, self) => divmod(10, 3) == (3, 1)
577+
q, r = uint_class(3).__rdivmod__(uint_class(10))
578+
assert q == uint_class(3)
579+
assert r == uint_class(1)
580+
assert isinstance(q, uint_class)
581+
assert isinstance(r, uint_class)
582+
583+
584+
class TestReverseBitwiseOperators:
585+
"""Tests for reverse bitwise operator delegation paths."""
586+
587+
@pytest.mark.parametrize("uint_class", ALL_UINT_TYPES)
588+
def test_rand_delegates_to_and(self, uint_class: Type[BaseUint]) -> None:
589+
"""Reverse AND delegates to forward AND and returns the correct result."""
590+
# __rand__ delegates to __and__
591+
result = uint_class(0b1100).__rand__(uint_class(0b1010))
592+
assert result == uint_class(0b1000)
593+
assert isinstance(result, uint_class)
594+
595+
@pytest.mark.parametrize("uint_class", ALL_UINT_TYPES)
596+
def test_ror_delegates_to_or(self, uint_class: Type[BaseUint]) -> None:
597+
"""Reverse OR delegates to forward OR and returns the correct result."""
598+
# __ror__ delegates to __or__
599+
result = uint_class(0b1100).__ror__(uint_class(0b1010))
600+
assert result == uint_class(0b1110)
601+
assert isinstance(result, uint_class)
602+
603+
@pytest.mark.parametrize("uint_class", ALL_UINT_TYPES)
604+
def test_rxor_delegates_to_xor(self, uint_class: Type[BaseUint]) -> None:
605+
"""Reverse XOR delegates to forward XOR and returns the correct result."""
606+
# __rxor__ delegates to __xor__
607+
result = uint_class(0b1100).__rxor__(uint_class(0b1010))
608+
assert result == uint_class(0b0110)
609+
assert isinstance(result, uint_class)
610+
611+
612+
class TestReverseShiftOperators:
613+
"""Tests for reverse left-shift and right-shift operator paths."""
614+
615+
@pytest.mark.parametrize("uint_class", ALL_UINT_TYPES)
616+
def test_rlshift_success(self, uint_class: Type[BaseUint]) -> None:
617+
"""Reverse left shift computes other << self."""
618+
# __rlshift__(other) computes other << self => 1 << 2 == 4
619+
result = uint_class(2).__rlshift__(1)
620+
assert result == uint_class(4)
621+
assert isinstance(result, uint_class)
622+
623+
@pytest.mark.parametrize("uint_class", ALL_UINT_TYPES)
624+
def test_rlshift_rejects_bool(self, uint_class: Type[BaseUint]) -> None:
625+
"""Reverse left shift rejects a bool operand."""
626+
with pytest.raises(TypeError, match=r"expected 'int' but got 'bool'"):
627+
uint_class(2).__rlshift__(True)
628+
629+
@pytest.mark.parametrize("uint_class", ALL_UINT_TYPES)
630+
def test_rrshift_success(self, uint_class: Type[BaseUint]) -> None:
631+
"""Reverse right shift computes other >> self."""
632+
# __rrshift__(other) computes other >> self => 8 >> 2 == 2
633+
result = uint_class(2).__rrshift__(8)
634+
assert result == uint_class(2)
635+
assert isinstance(result, uint_class)
636+
637+
@pytest.mark.parametrize("uint_class", ALL_UINT_TYPES)
638+
def test_rrshift_rejects_bool(self, uint_class: Type[BaseUint]) -> None:
639+
"""Reverse right shift rejects a bool operand."""
640+
with pytest.raises(TypeError, match=r"expected 'int' but got 'bool'"):
641+
uint_class(2).__rrshift__(True)
642+
643+
644+
class TestComparisonTypeErrors:
645+
"""Tests that comparison operators raise TypeError when given plain int operands.
646+
647+
The existing test_all_comparisons_with_other_types_raise_error uses the operator
648+
syntax (e.g., `uint < 10`) which for __lt__ and __le__ may be resolved by Python
649+
as int.__gt__ and int.__ge__ instead. Calling the dunder directly ensures the
650+
BaseUint implementation is exercised.
651+
"""
652+
653+
@pytest.mark.parametrize("uint_class", ALL_UINT_TYPES)
654+
def test_lt_rejects_plain_int(self, uint_class: Type[BaseUint]) -> None:
655+
"""Less-than raises TypeError when compared to a plain int directly."""
656+
with pytest.raises(TypeError, match="<"):
657+
uint_class(5).__lt__(10)
658+
659+
@pytest.mark.parametrize("uint_class", ALL_UINT_TYPES)
660+
def test_le_rejects_plain_int(self, uint_class: Type[BaseUint]) -> None:
661+
"""Less-than-or-equal raises TypeError when compared to a plain int directly."""
662+
with pytest.raises(TypeError, match="<="):
663+
uint_class(5).__le__(10)
664+
665+
666+
class TestIndexReturnsPlainInt:
667+
"""Tests that __index__ returns a plain int, not a BaseUint subclass."""
668+
669+
@pytest.mark.parametrize("uint_class", ALL_UINT_TYPES)
670+
def test_index_returns_plain_int(self, uint_class: Type[BaseUint]) -> None:
671+
"""__index__ returns a plain int so that built-in operations receive a raw integer."""
672+
result = uint_class(42).__index__()
673+
# The value must be correct.
674+
assert result == 42
675+
# The type must be plain int, not a BaseUint subclass.
676+
assert type(result) is int

0 commit comments

Comments
 (0)