tests/pi_star_code: 32 KATs for Python-AST kernel

Backfills minimal-only coverage on arborist/pi_star/code.py (123
lines, 2026-05-10 zero-coverage sweep). KATs mirror the docstring's
equivalence classes: whitespace/indent/comment/quote-style/numeric-
formatting (1_000 ≡ 1000) invariance vs identifier/operator/arg-
order distinctness. Pins critical isinstance(bool) ordering (bool
subclasses int, must dispatch first to avoid encoding True as 1).
Pins lexical _fields sort (cross-Python-version determinism). Pins
position-field exclusion (lineno/col_offset never appear in
canonical output). Plus projective contract: re-canonicalizing the
S-expression output raises PiStarError (not valid Python).
This commit is contained in:
russell@unturf.com 2026-05-10 15:54:49 -04:00
parent e7bef5ff68
commit 593550b1e3
No known key found for this signature in database

228
tests/test_pi_star_code.py Normal file
View file

@ -0,0 +1,228 @@
"""code-py-ast@v1 π* tests.
Python source deterministic AST S-expression. KATs mirror the
docstring's equivalence-class examples: whitespace / comment /
quote-style / numeric-formatting invariance vs identifier and
operator distinctness. Plus invalid-input cone (non-Python,
non-bytes) and determinism.
Note: this projection is projective, not invertible the canonical
S-expression is NOT valid Python, so re-canonicalizing the output
raises (covered by an explicit test).
Backfills the test-coverage gap for arborist/pi_star/code.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
@pytest.fixture
def ps():
return get("code-py-ast@v1")
def _canon(ps, src: str) -> bytes:
return ps.canonicalize(src.encode("utf-8"))
# --- registry presence + metadata ------------------------------------
def test_registry_contains_code_py_ast():
ps = get("code-py-ast@v1")
assert ps.name == "code-py-ast"
assert ps.version == "v1"
assert ps.domain == "code"
# --- positive shape --------------------------------------------------
def test_empty_module(ps):
out = _canon(ps, "")
# Module(body=[] type_ignores=[]) — minimal AST shape.
assert out.startswith(b"(Module ")
assert b"body=[]" in out
def test_single_expression(ps):
out = _canon(ps, "1 + 2")
assert b"BinOp" in out
assert b"Add" in out
assert b"value=1" in out
assert b"value=2" in out
def test_assignment(ps):
out = _canon(ps, "x = 1")
assert b"Assign" in out
assert b"'x'" in out
assert b"value=1" in out
def test_function_def(ps):
out = _canon(ps, "def foo(a, b):\n return a + b\n")
assert b"FunctionDef" in out
assert b"'foo'" in out
assert b"Return" in out
# --- equivalence classes preserved -----------------------------------
@pytest.mark.parametrize(
"a,b",
[
# Whitespace / indentation
("x=1", "x = 1"),
("x = 1", "x = 1"),
# Comments
("x = 1", "x = 1 # comment"),
("# leading\nx = 1", "x = 1"),
# Quote style
("x = 'foo'", 'x = "foo"'),
# Trailing semicolons
("x = 1", "x = 1;"),
# Numeric formatting (underscores in int literals)
("x = 1000", "x = 1_000"),
],
)
def test_equivalent_sources_collapse(ps, a, b):
assert _canon(ps, a) == _canon(ps, b)
# --- equivalence classes kept distinct -------------------------------
@pytest.mark.parametrize(
"a,b",
[
# Identifier names
("x = 1", "y = 1"),
# Operator types
("a + b", "a - b"),
("a * b", "a / b"),
# Argument order in calls
("f(a, b)", "f(b, a)"),
# Literal vs variable
("x = 1", "x = 2"),
# String vs bytes
("x = 'a'", "x = b'a'"),
],
)
def test_distinct_sources_stay_distinct(ps, a, b):
assert _canon(ps, a) != _canon(ps, b)
# --- determinism + ordering ------------------------------------------
def test_repeated_canonicalize_is_stable(ps):
src = "class X:\n def f(self): return 42\n y = [1, 2, 3]\n"
a = _canon(ps, src)
b = _canon(ps, src)
c = _canon(ps, src)
assert a == b == c
def test_field_order_is_lexical(ps):
"""_canonical_sexp sorts node._fields for cross-version stability."""
out = _canon(ps, "x = 1")
# In an Assign node, lexical sort puts 'targets' before 'value'.
body_idx = out.index(b"Assign")
targets_idx = out.index(b"targets=", body_idx)
value_idx = out.index(b"value=", body_idx)
assert targets_idx < value_idx
# --- source-position fields excluded ---------------------------------
def test_line_position_does_not_change_canonical(ps):
"""lineno / col_offset are not in `_fields` so they're excluded;
the same statement at different line numbers canonicalizes the same."""
a = _canon(ps, "x = 1")
b = _canon(ps, "\n\nx = 1")
assert a == b
def test_position_fields_excluded(ps):
"""Source-position metadata MUST NOT appear in canonical output."""
out = _canon(ps, "x = 1\ny = 2\n")
# Confirm none of the excluded fields leaked into the encoding.
for excluded in (b"lineno=", b"col_offset=", b"end_lineno=", b"end_col_offset="):
assert excluded not in out, f"position field {excluded!r} leaked"
# --- invalid-input cone ----------------------------------------------
def test_non_python_raises(ps):
with pytest.raises(PiStarError, match="not valid Python"):
ps.canonicalize(b"def (")
def test_unbalanced_paren_raises(ps):
with pytest.raises(PiStarError, match="not valid Python"):
ps.canonicalize(b"x = (1 + 2")
def test_non_bytes_raises(ps):
with pytest.raises(PiStarError, match="expects bytes"):
ps.canonicalize("x = 1") # type: ignore[arg-type]
def test_bytearray_accepted(ps):
"""bytearray is an accepted subtype per the bytes/bytearray guard."""
out = ps.canonicalize(bytearray(b"x = 1"))
assert b"Assign" in out
assert b"value=1" in out
# --- projective (not invertible) -------------------------------------
def test_canonical_output_is_not_python_source(ps):
"""Re-canonicalizing the canonical S-expression raises — by design."""
canonical = _canon(ps, "x = 1")
# The S-expression starts with `(Module ...)` which isn't a Python
# expression. ast.parse rejects it.
with pytest.raises(PiStarError, match="not valid Python"):
ps.canonicalize(canonical)
# --- value types ------------------------------------------------------
def test_bool_serialized_as_bool_not_int(ps):
"""bool subclasses int — handler must dispatch on bool first."""
out = _canon(ps, "x = True")
# If bool fell through to int, we'd see `value=1`. Must see True.
assert b"value=True" in out
assert b"value=1" not in out
def test_none_handled(ps):
out = _canon(ps, "x = None")
assert b"value=None" in out
def test_float_literal(ps):
out = _canon(ps, "x = 3.14")
assert b"value=3.14" in out
def test_complex_literal(ps):
out = _canon(ps, "x = 1j")
assert b"value=1j" in out
def test_bytes_literal(ps):
out = _canon(ps, "x = b'hi'")
assert b"value=b'hi'" in out