"""Tests for the OCaml SX bridge.""" import asyncio import os import sys import unittest # Add project root to path _project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../..")) if _project_root not in sys.path: sys.path.insert(0, _project_root) from shared.sx.ocaml_bridge import OcamlBridge, OcamlBridgeError, _escape, _parse_response, _serialize_for_ocaml class TestHelpers(unittest.TestCase): """Test helper functions (no subprocess needed).""" def test_escape_basic(self): self.assertEqual(_escape('hello'), 'hello') self.assertEqual(_escape('say "hi"'), 'say \\"hi\\"') self.assertEqual(_escape('a\\b'), 'a\\\\b') self.assertEqual(_escape('line\nbreak'), 'line\\nbreak') def test_parse_response_ok_empty(self): self.assertEqual(_parse_response("(ok)"), ("ok", None)) def test_parse_response_ok_number(self): self.assertEqual(_parse_response("(ok 3)"), ("ok", "3")) def test_parse_response_ok_string(self): kind, val = _parse_response('(ok "
hi
")') self.assertEqual(kind, "ok") self.assertEqual(val, "
hi
") def test_parse_response_error(self): kind, val = _parse_response('(error "something broke")') self.assertEqual(kind, "error") self.assertEqual(val, "something broke") def test_serialize_none(self): self.assertEqual(_serialize_for_ocaml(None), "nil") def test_serialize_bool(self): self.assertEqual(_serialize_for_ocaml(True), "true") self.assertEqual(_serialize_for_ocaml(False), "false") def test_serialize_number(self): self.assertEqual(_serialize_for_ocaml(42), "42") self.assertEqual(_serialize_for_ocaml(3.14), "3.14") def test_serialize_string(self): self.assertEqual(_serialize_for_ocaml("hello"), '"hello"') self.assertEqual(_serialize_for_ocaml('say "hi"'), '"say \\"hi\\""') def test_serialize_list(self): self.assertEqual(_serialize_for_ocaml([1, 2, 3]), "(list 1 2 3)") def test_serialize_dict(self): result = _serialize_for_ocaml({"a": 1}) self.assertEqual(result, "{:a 1}") class TestBridge(unittest.IsolatedAsyncioTestCase): """Integration tests — require the OCaml binary to be built.""" @classmethod def setUpClass(cls): # Check if binary exists from shared.sx.ocaml_bridge import _DEFAULT_BIN bin_path = os.path.abspath(_DEFAULT_BIN) if not os.path.isfile(bin_path): raise unittest.SkipTest( f"OCaml binary not found at {bin_path}. " f"Build with: cd hosts/ocaml && eval $(opam env) && dune build" ) async def asyncSetUp(self): self.bridge = OcamlBridge() await self.bridge.start() async def asyncTearDown(self): await self.bridge.stop() async def test_ping(self): self.assertTrue(await self.bridge.ping()) async def test_eval_arithmetic(self): result = await self.bridge.eval("(+ 1 2)") self.assertEqual(result, "3") async def test_eval_string(self): result = await self.bridge.eval('(str "hello" " " "world")') self.assertIn("hello world", result) async def test_render_simple(self): html = await self.bridge.render('(div (p "hello"))') self.assertEqual(html, "

hello

") async def test_render_attrs(self): html = await self.bridge.render('(div :class "card" (p "hi"))') self.assertIn('class="card"', html) self.assertIn("

hi

", html) async def test_render_void_element(self): html = await self.bridge.render("(br)") self.assertEqual(html, "
") async def test_load_source_defcomp(self): count = await self.bridge.load_source( '(defcomp ~test-card (&key title) (div :class "card" (h2 title)))' ) self.assertEqual(count, 1) html = await self.bridge.render('(~test-card :title "Hello")') self.assertIn("Hello", html) self.assertIn("card", html) async def test_reset(self): await self.bridge.load_source("(define x 42)") result = await self.bridge.eval("x") self.assertEqual(result, "42") await self.bridge.reset() with self.assertRaises(OcamlBridgeError): await self.bridge.eval("x") async def test_render_conditional(self): html = await self.bridge.render('(if true (p "yes") (p "no"))') self.assertEqual(html, "

yes

") async def test_render_let(self): html = await self.bridge.render('(let (x "hi") (p x))') self.assertEqual(html, "

hi

") async def test_render_map(self): html = await self.bridge.render( "(map (lambda (x) (li x)) (list \"a\" \"b\" \"c\"))" ) self.assertEqual(html, "
  • a
  • b
  • c
  • ") async def test_render_fragment(self): html = await self.bridge.render('(<> (p "a") (p "b"))') self.assertEqual(html, "

    a

    b

    ") async def test_eval_error(self): with self.assertRaises(OcamlBridgeError): await self.bridge.eval("(undefined-symbol-xyz)") async def test_render_component_with_children(self): await self.bridge.load_source( '(defcomp ~wrapper (&rest children) (div :class "wrap" children))' ) html = await self.bridge.render('(~wrapper (p "inside"))') self.assertIn("wrap", html) self.assertIn("

    inside

    ", html) async def test_render_macro(self): await self.bridge.load_source( "(defmacro unless (cond &rest body) (list 'if (list 'not cond) (cons 'do body)))" ) html = await self.bridge.render('(unless false (p "shown"))') self.assertEqual(html, "

    shown

    ") # ------------------------------------------------------------------ # ListRef regression tests — the `list` primitive returns ListRef # (mutable), not List (immutable). Macro expansions that construct # AST via `list` produce ListRef nodes. The renderer must handle # both List and ListRef at every structural match point. # ------------------------------------------------------------------ async def test_render_macro_generates_cond(self): """Macro that programmatically builds a (cond ...) with list.""" await self.bridge.load_source( "(defmacro pick (x) " " (list 'cond " " (list (list '= x 1) '(p \"one\")) " " (list (list '= x 2) '(p \"two\")) " " (list ':else '(p \"other\"))))" ) html = await self.bridge.render("(pick 2)") self.assertEqual(html, "

    two

    ") async def test_render_macro_generates_let(self): """Macro that programmatically builds a (let ...) with list.""" await self.bridge.load_source( "(defmacro with-greeting (name &rest body) " " (list 'let (list (list 'greeting (list 'str \"Hello \" name))) " " (cons 'do body)))" ) html = await self.bridge.render('(with-greeting "World" (p greeting))') self.assertEqual(html, "

    Hello World

    ") async def test_render_macro_nested_html_tags(self): """Macro expansion containing nested HTML tags via list.""" await self.bridge.load_source( "(defmacro card (title &rest body) " " (list 'div ':class \"card\" " " (list 'h2 title) " " (cons 'do body)))" ) html = await self.bridge.render('(card "Title" (p "content"))') self.assertIn('
    ', html) self.assertIn("

    Title

    ", html) self.assertIn("

    content

    ", html) async def test_render_eval_returns_listref(self): """Values created at runtime via (list ...) are ListRef.""" await self.bridge.load_source( "(define make-items (lambda () (list " ' (list "a") (list "b") (list "c"))))' ) html = await self.bridge.render( "(ul (map (lambda (x) (li (first x))) (make-items)))" ) self.assertIn("
  • a
  • ", html) self.assertIn("
  • b
  • ", html) self.assertIn("
  • c
  • ", html) if __name__ == "__main__": unittest.main()