"""Tests for the s-expression parser.""" import pytest from shared.sx.parser import parse, parse_all, serialize, ParseError from shared.sx.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