tests/pi_star_arithmetic: 56 KATs for exact-rational arithmetic kernel
Backfills zero dedicated coverage on arborist/pi_star/arithmetic.py (188 lines, 2026-05-10 zero-coverage sweep). KATs mirror the docstring's equivalence-class examples: integer-literal collapse, decimal → rational exactness (0.1 + 0.2 = 3/10), lowest-terms reduction, integer exponents. Negative cones: identifier, function call, non-integer exponent, division by zero, boolean literal, empty, non-bytes, syntax error. Plus round-trip idempotence and equivalence-class-id matching across equivalent inputs.
This commit is contained in:
parent
0cb943561a
commit
6c9bc047fd
1 changed files with 197 additions and 0 deletions
197
tests/test_pi_star_arithmetic.py
Normal file
197
tests/test_pi_star_arithmetic.py
Normal file
|
|
@ -0,0 +1,197 @@
|
|||
"""arithmetic@v1 π* tests.
|
||||
|
||||
Exact-rational arithmetic canonicalizer over Σ* → ℚ ∪ {⊥}. KATs
|
||||
mirror the docstring's equivalence-class examples, plus negative
|
||||
cones (identifier, function call, non-integer exponent, division
|
||||
by zero, boolean literal, empty, non-bytes, syntax error) and
|
||||
round-trip idempotence.
|
||||
|
||||
Backfills the test-coverage gap for arborist/pi_star/arithmetic.py
|
||||
identified during the 2026-05-10 zero-coverage sweep.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from arborist.pi_star import get
|
||||
from arborist.pi_star.protocol import (
|
||||
PiStarError,
|
||||
assert_round_trip,
|
||||
equivalence_class_id,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def ps():
|
||||
return get("arithmetic@v1")
|
||||
|
||||
|
||||
# --- registry presence + metadata ------------------------------------
|
||||
|
||||
|
||||
def test_registry_contains_arithmetic():
|
||||
ps = get("arithmetic@v1")
|
||||
assert ps.name == "arithmetic"
|
||||
assert ps.version == "v1"
|
||||
assert ps.domain == "arithmetic"
|
||||
|
||||
|
||||
# --- positive KATs (docstring equivalence classes) -------------------
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"expr,canonical",
|
||||
[
|
||||
# Integer literal collapse
|
||||
("1+2", "3/1"),
|
||||
("(1+2)*3", "9/1"),
|
||||
("100", "100/1"),
|
||||
("-2", "-2/1"),
|
||||
# Decimal → rational, exact
|
||||
("0.1", "1/10"),
|
||||
("0.5", "1/2"),
|
||||
("0.1+0.2", "3/10"),
|
||||
# Lowest-terms reduction
|
||||
("6/4", "3/2"),
|
||||
("10/5", "2/1"),
|
||||
# Unary
|
||||
("+7", "7/1"),
|
||||
("-(3+4)", "-7/1"),
|
||||
# Integer exponent
|
||||
("2**10", "1024/1"),
|
||||
("(-2)**3", "-8/1"),
|
||||
# Mixed
|
||||
("3/10 + 0.1", "2/5"),
|
||||
],
|
||||
)
|
||||
def test_positive_kats(ps, expr, canonical):
|
||||
assert ps.canonicalize(expr.encode("utf-8")) == canonical.encode("utf-8")
|
||||
|
||||
|
||||
# --- equivalence classes preserved -----------------------------------
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"a,b",
|
||||
[
|
||||
("1+2", "3"),
|
||||
("0.1", "1/10"),
|
||||
("0.5", "1/2"),
|
||||
("0.1+0.2", "0.3"),
|
||||
("(1+2)*3", "9"),
|
||||
("6/4", "3/2"),
|
||||
("-2", "-2/1"),
|
||||
("2*3", "(1+2)*2"),
|
||||
],
|
||||
)
|
||||
def test_equivalent_inputs_collapse(ps, a, b):
|
||||
assert ps.canonicalize(a.encode()) == ps.canonicalize(b.encode())
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"a,b",
|
||||
[
|
||||
("1+2", "3"),
|
||||
("0.1+0.2", "0.3"),
|
||||
("6/4", "3/2"),
|
||||
],
|
||||
)
|
||||
def test_equivalence_class_id_matches(ps, a, b):
|
||||
assert equivalence_class_id(ps, a.encode()) == equivalence_class_id(
|
||||
ps, b.encode()
|
||||
)
|
||||
|
||||
|
||||
# --- equivalence classes kept distinct -------------------------------
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"a,b",
|
||||
[
|
||||
("1+2", "4"),
|
||||
("0.1", "0.2"),
|
||||
("0.1+0.2", "0.30000000001"), # the float-drift form is distinct
|
||||
("2/3", "3/2"),
|
||||
("-2", "2"),
|
||||
],
|
||||
)
|
||||
def test_distinct_inputs_stay_distinct(ps, a, b):
|
||||
assert ps.canonicalize(a.encode()) != ps.canonicalize(b.encode())
|
||||
|
||||
|
||||
# --- round-trip idempotence ------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"expr",
|
||||
["0", "1", "-1", "3/10", "0.1+0.2", "(1+2)*3", "2**10", "-(3+4)"],
|
||||
)
|
||||
def test_round_trip(ps, expr):
|
||||
assert_round_trip(ps, expr.encode("utf-8"))
|
||||
|
||||
|
||||
def test_output_is_valid_input(ps):
|
||||
"""canonicalize output is itself valid input that re-canonicalizes."""
|
||||
first = ps.canonicalize(b"6/4")
|
||||
second = ps.canonicalize(first)
|
||||
assert first == second == b"3/2"
|
||||
|
||||
|
||||
# --- invalid-input cone ----------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"expr,fragment",
|
||||
[
|
||||
("x", "identifier"),
|
||||
("x + 1", "identifier"),
|
||||
("sqrt(2)", "function calls"),
|
||||
("max(1, 2)", "function calls"),
|
||||
("2 ** 0.5", "non-integer exponent"),
|
||||
("1/0", "division by zero"),
|
||||
("(2 + 3)/0", "division by zero"),
|
||||
("True", "boolean"),
|
||||
("False", "boolean"),
|
||||
("", "empty"),
|
||||
(" ", "empty"),
|
||||
],
|
||||
)
|
||||
def test_invalid_raises(ps, expr, fragment):
|
||||
with pytest.raises(PiStarError, match=fragment):
|
||||
ps.canonicalize(expr.encode("utf-8"))
|
||||
|
||||
|
||||
def test_syntax_error_raises(ps):
|
||||
with pytest.raises(PiStarError, match="not valid expression"):
|
||||
ps.canonicalize(b"1 + ")
|
||||
|
||||
|
||||
def test_unsupported_op_raises(ps):
|
||||
"""Comparison / bitwise / logical ops are rejected."""
|
||||
for expr in ("1 < 2", "1 & 2", "1 | 2"):
|
||||
with pytest.raises(PiStarError):
|
||||
ps.canonicalize(expr.encode("utf-8"))
|
||||
|
||||
|
||||
def test_non_bytes_input_raises(ps):
|
||||
with pytest.raises(PiStarError, match="expects bytes"):
|
||||
ps.canonicalize("3/10") # type: ignore[arg-type]
|
||||
with pytest.raises(PiStarError, match="expects bytes"):
|
||||
ps.canonicalize(42) # type: ignore[arg-type]
|
||||
|
||||
|
||||
def test_bytearray_accepted(ps):
|
||||
"""bytearray is an accepted subtype per the bytes/bytearray guard."""
|
||||
assert ps.canonicalize(bytearray(b"6/4")) == b"3/2"
|
||||
|
||||
|
||||
# --- determinism -----------------------------------------------------
|
||||
|
||||
|
||||
def test_repeated_canonicalize_is_stable(ps):
|
||||
expr = b"(0.1 + 0.2) * 3 - 1/2 + 7/14"
|
||||
a = ps.canonicalize(expr)
|
||||
b = ps.canonicalize(expr)
|
||||
c = ps.canonicalize(expr)
|
||||
assert a == b == c
|
||||
Loading…
Add table
Add a link
Reference in a new issue