Files
mono/shared/sexp/tests/test_parser.py
giles 0fb87e3b1c Phase 1: s-expression core library + test infrastructure
S-expression parser, evaluator, and primitive registry in shared/sexp/.
109 unit tests covering parsing, evaluation, special forms, lambdas,
closures, components (defcomp), and 60+ pure builtins.

Test infrastructure: Dockerfile.unit (tier 1, fast) and
Dockerfile.integration (tier 2, ffmpeg). Dev watch mode auto-reruns
on file changes. Deploy gate blocks push on test failure.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 13:26:18 +00:00

192 lines
5.3 KiB
Python

"""Tests for the s-expression parser."""
import pytest
from shared.sexp.parser import parse, parse_all, serialize, ParseError
from shared.sexp.types import Symbol, Keyword, NIL
# ---------------------------------------------------------------------------
# Atoms
# ---------------------------------------------------------------------------
class TestAtoms:
def test_integer(self):
assert parse("42") == 42
def test_negative_integer(self):
assert parse("-7") == -7
def test_float(self):
assert parse("3.14") == 3.14
def test_scientific(self):
assert parse("1e-3") == 0.001
def test_string(self):
assert parse('"hello world"') == "hello world"
def test_string_escapes(self):
assert parse(r'"line1\nline2"') == "line1\nline2"
assert parse(r'"tab\there"') == "tab\there"
assert parse(r'"say \"hi\""') == 'say "hi"'
def test_symbol(self):
assert parse("foo") == Symbol("foo")
def test_component_symbol(self):
s = parse("~card")
assert s == Symbol("~card")
assert s.is_component
def test_keyword(self):
assert parse(":class") == Keyword("class")
def test_true(self):
assert parse("true") is True
def test_false(self):
assert parse("false") is False
def test_nil(self):
assert parse("nil") is NIL
# ---------------------------------------------------------------------------
# Lists
# ---------------------------------------------------------------------------
class TestLists:
def test_empty_list(self):
assert parse("()") == []
def test_simple_list(self):
assert parse("(1 2 3)") == [1, 2, 3]
def test_mixed_list(self):
result = parse('(div :class "main")')
assert result == [Symbol("div"), Keyword("class"), "main"]
def test_nested_list(self):
result = parse("(a (b c) d)")
assert result == [Symbol("a"), [Symbol("b"), Symbol("c")], Symbol("d")]
def test_vector_sugar(self):
assert parse("[1 2 3]") == [1, 2, 3]
# ---------------------------------------------------------------------------
# Maps
# ---------------------------------------------------------------------------
class TestMaps:
def test_simple_map(self):
result = parse('{:a 1 :b 2}')
assert result == {"a": 1, "b": 2}
def test_nested_map(self):
result = parse('{:x {:y 3}}')
assert result == {"x": {"y": 3}}
def test_string_keys(self):
result = parse('{"name" "alice"}')
assert result == {"name": "alice"}
# ---------------------------------------------------------------------------
# Comments
# ---------------------------------------------------------------------------
class TestComments:
def test_line_comment(self):
assert parse("; comment\n42") == 42
def test_inline_comment(self):
result = parse("(a ; stuff\nb)")
assert result == [Symbol("a"), Symbol("b")]
# ---------------------------------------------------------------------------
# parse_all
# ---------------------------------------------------------------------------
class TestParseAll:
def test_multiple(self):
results = parse_all("1 2 3")
assert results == [1, 2, 3]
def test_multiple_lists(self):
results = parse_all("(a) (b)")
assert results == [[Symbol("a")], [Symbol("b")]]
def test_empty(self):
assert parse_all("") == []
assert parse_all(" ; only comments\n") == []
# ---------------------------------------------------------------------------
# Errors
# ---------------------------------------------------------------------------
class TestErrors:
def test_unterminated_list(self):
with pytest.raises(ParseError):
parse("(a b")
def test_unterminated_string(self):
with pytest.raises(ParseError):
parse('"hello')
def test_unexpected_closer(self):
with pytest.raises(ParseError):
parse(")")
def test_trailing_content(self):
with pytest.raises(ParseError):
parse("1 2")
# ---------------------------------------------------------------------------
# Serialization
# ---------------------------------------------------------------------------
class TestSerialize:
def test_int(self):
assert serialize(42) == "42"
def test_float(self):
assert serialize(3.14) == "3.14"
def test_string(self):
assert serialize("hello") == '"hello"'
def test_string_escapes(self):
assert serialize('say "hi"') == '"say \\"hi\\""'
def test_symbol(self):
assert serialize(Symbol("foo")) == "foo"
def test_keyword(self):
assert serialize(Keyword("class")) == ":class"
def test_bool(self):
assert serialize(True) == "true"
assert serialize(False) == "false"
def test_nil(self):
assert serialize(None) == "nil"
assert serialize(NIL) == "nil"
def test_list(self):
assert serialize([Symbol("a"), 1, 2]) == "(a 1 2)"
def test_empty_list(self):
assert serialize([]) == "()"
def test_dict(self):
result = serialize({"a": 1, "b": 2})
assert result == "{:a 1 :b 2}"
def test_roundtrip(self):
original = '(div :class "main" (p "hello") (span 42))'
assert serialize(parse(original)) == original