diff --git a/shared/static/scripts/sx-test-runner.js b/shared/static/scripts/sx-test-runner.js index 89ce30c..2705255 100644 --- a/shared/static/scripts/sx-test-runner.js +++ b/shared/static/scripts/sx-test-runner.js @@ -1,5 +1,5 @@ -// sx-test-runner.js — Run test.sx in the browser using sx-browser.js. -// Loaded on the /specs/testing page. Uses the Sx global. +// sx-test-runner.js — Run SX test specs in the browser using sx-browser.js. +// Supports both legacy (monolithic test.sx) and modular (per-spec) modes. (function() { var NIL = Sx.NIL; function isNil(x) { return x === NIL || x === null || x === undefined; } @@ -21,13 +21,9 @@ return false; } - window.sxRunTests = function(srcId, outId, btnId) { - var src = document.getElementById(srcId).textContent; - var out = document.getElementById(outId); - var btn = document.getElementById(btnId); - + // --- Platform functions shared across all specs --- + function makeEnv() { var stack = [], passed = 0, failed = 0, num = 0, lines = []; - var env = { "try-call": function(thunk) { try { @@ -49,6 +45,7 @@ "push-suite": function(name) { stack.push(name); }, "pop-suite": function() { stack.pop(); }, + // Primitives that sx-browser.js may not expose in env "equal?": function(a, b) { return deepEqual(a, b); }, "eq?": function(a, b) { return a === b; }, "boolean?": function(x) { return typeof x === "boolean"; }, @@ -68,28 +65,159 @@ }, "has-key?": function(d, k) { return d && typeof d === "object" && k in d; }, "append": function(c, x) { return Array.isArray(x) ? (c||[]).concat(x) : (c||[]).concat([x]); }, + "for-each-indexed": function(f, coll) { + for (var i = 0; i < (coll||[]).length; i++) Sx.eval([f, i, coll[i]], env); + }, + "for-each": function(f, coll) { + for (var i = 0; i < (coll||[]).length; i++) Sx.eval([f, coll[i]], env); + }, + "dict-set!": function(d, k, v) { if (d) d[k] = v; }, + "dict-has?": function(d, k) { return d && typeof d === "object" && k in d; }, + "dict-get": function(d, k) { return d ? d[k] : undefined; }, + "starts-with?": function(s, prefix) { return String(s).indexOf(prefix) === 0; }, + "ends-with?": function(s, suffix) { var str = String(s); return str.indexOf(suffix) === str.length - suffix.length; }, + "slice": function(s, start, end) { return end !== undefined ? s.slice(start, end) : s.slice(start); }, + "inc": function(n) { return n + 1; }, + "append!": function(arr, item) { if (Array.isArray(arr)) arr.push(item); }, + "dict": function() { return {}; }, + + // --- Parser platform functions --- + "sx-parse": function(source) { return Sx.parseAll(source); }, + "sx-serialize": function(val) { + if (val === NIL || val === null || val === undefined) return "nil"; + if (typeof val === "boolean") return val ? "true" : "false"; + if (typeof val === "number") return String(val); + if (typeof val === "string") return '"' + val.replace(/\\/g, "\\\\").replace(/"/g, '\\"') + '"'; + if (val && (val._sym || val._sx_symbol)) return val.name; + if (val && (val._kw || val._sx_keyword)) return ":" + val.name; + if (Array.isArray(val)) return "(" + val.map(function(x) { return env["sx-serialize"](x); }).join(" ") + ")"; + if (val && typeof val === "object") { + var parts = []; + Object.keys(val).forEach(function(k) { parts.push(":" + k); parts.push(env["sx-serialize"](val[k])); }); + return "{" + parts.join(" ") + "}"; + } + return String(val); + }, + "make-symbol": function(name) { return Sx.sym ? Sx.sym(name) : { _sx_symbol: true, name: name, toString: function() { return name; } }; }, + "make-keyword": function(name) { return Sx.kw ? Sx.kw(name) : { _sx_keyword: true, name: name, toString: function() { return name; } }; }, + "symbol-name": function(s) { return s && s.name ? s.name : String(s); }, + "keyword-name": function(k) { return k && k.name ? k.name : String(k); }, + + // --- Render platform function --- + "render-html": function(sxSource) { + if (!Sx.renderToHtml) throw new Error("render-to-html not available"); + var exprs = Sx.parseAll(sxSource); + var result = ""; + for (var i = 0; i < exprs.length; i++) result += Sx.renderToHtml(exprs[i], env); + return result; + }, }; + return { env: env, getResults: function() { return { passed: passed, failed: failed, num: num, lines: lines }; } }; + } + + function evalSource(src, env) { + var exprs = Sx.parseAll(src); + for (var i = 0; i < exprs.length; i++) Sx.eval(exprs[i], env); + } + + function loadRouterFromBootstrap(env) { + if (Sx.splitPathSegments) { + env["split-path-segments"] = Sx.splitPathSegments; + env["parse-route-pattern"] = Sx.parseRoutePattern; + env["match-route-segments"] = Sx.matchRouteSegments; + env["match-route"] = Sx.matchRoute; + env["find-matching-route"] = Sx.findMatchingRoute; + env["make-route-segment"] = Sx.makeRouteSegment; + } + } + + // --- Legacy runner (monolithic test.sx) --- + window.sxRunTests = function(srcId, outId, btnId) { + var src = document.getElementById(srcId).textContent; + var out = document.getElementById(outId); + var btn = document.getElementById(btnId); + var ctx = makeEnv(); try { var t0 = performance.now(); - var exprs = Sx.parseAll(src); - for (var i = 0; i < exprs.length; i++) Sx.eval(exprs[i], env); + evalSource(src, ctx.env); var elapsed = Math.round(performance.now() - t0); - lines.push(""); - lines.push("1.." + num); - lines.push("# tests " + (passed + failed)); - lines.push("# pass " + passed); - if (failed > 0) lines.push("# fail " + failed); - lines.push("# time " + elapsed + "ms"); + var r = ctx.getResults(); + r.lines.push(""); + r.lines.push("1.." + r.num); + r.lines.push("# tests " + (r.passed + r.failed)); + r.lines.push("# pass " + r.passed); + if (r.failed > 0) r.lines.push("# fail " + r.failed); + r.lines.push("# time " + elapsed + "ms"); } catch(e) { - lines.push(""); - lines.push("FATAL: " + (e.message || String(e))); + var r = ctx.getResults(); + r.lines.push(""); + r.lines.push("FATAL: " + (e.message || String(e))); } - out.textContent = lines.join("\n"); + out.textContent = r.lines.join("\n"); out.style.display = "block"; - btn.textContent = passed + "/" + (passed + failed) + " passed" + (failed === 0 ? "" : " (" + failed + " failed)"); - btn.className = failed > 0 + btn.textContent = r.passed + "/" + (r.passed + r.failed) + " passed" + (r.failed === 0 ? "" : " (" + r.failed + " failed)"); + btn.className = r.failed > 0 + ? "px-4 py-2 rounded-md bg-red-600 text-white font-medium text-sm cursor-default" + : "px-4 py-2 rounded-md bg-green-600 text-white font-medium text-sm cursor-default"; + }; + + // --- Modular runner (per-spec or all) --- + var SPECS = { + "eval": { needs: [] }, + "parser": { needs: ["sx-parse"] }, + "router": { needs: [] }, + "render": { needs: ["render-html"] }, + }; + + window.sxRunModularTests = function(specName, outId, btnId) { + var out = document.getElementById(outId); + var btn = document.getElementById(btnId); + var ctx = makeEnv(); + var specs = specName === "all" ? Object.keys(SPECS) : [specName]; + + try { + var t0 = performance.now(); + + // Load framework + var fwEl = document.getElementById("test-framework-source"); + if (fwEl) { + evalSource(fwEl.textContent, ctx.env); + } + + for (var si = 0; si < specs.length; si++) { + var sn = specs[si]; + if (!SPECS[sn]) continue; + + // Load router from bootstrap if needed + if (sn === "router") loadRouterFromBootstrap(ctx.env); + + // Find spec source — either per-spec textarea or embedded in overview + var specEl = document.getElementById("test-spec-" + sn); + if (specEl) { + evalSource(specEl.textContent, ctx.env); + } + } + + var elapsed = Math.round(performance.now() - t0); + var r = ctx.getResults(); + r.lines.push(""); + r.lines.push("1.." + r.num); + r.lines.push("# tests " + (r.passed + r.failed)); + r.lines.push("# pass " + r.passed); + if (r.failed > 0) r.lines.push("# fail " + r.failed); + r.lines.push("# time " + elapsed + "ms"); + } catch(e) { + var r = ctx.getResults(); + r.lines.push(""); + r.lines.push("FATAL: " + (e.message || String(e))); + } + + out.textContent = r.lines.join("\n"); + out.style.display = "block"; + btn.textContent = r.passed + "/" + (r.passed + r.failed) + " passed" + (r.failed === 0 ? "" : " (" + r.failed + " failed)"); + btn.className = r.failed > 0 ? "px-4 py-2 rounded-md bg-red-600 text-white font-medium text-sm cursor-default" : "px-4 py-2 rounded-md bg-green-600 text-white font-medium text-sm cursor-default"; }; diff --git a/shared/sx/ref/test-eval.sx b/shared/sx/ref/test-eval.sx new file mode 100644 index 0000000..06f655f --- /dev/null +++ b/shared/sx/ref/test-eval.sx @@ -0,0 +1,494 @@ +;; ========================================================================== +;; test-eval.sx — Tests for the core evaluator and primitives +;; +;; Requires: test-framework.sx loaded first. +;; Modules tested: eval.sx, primitives.sx +;; ========================================================================== + + +;; -------------------------------------------------------------------------- +;; Literals and types +;; -------------------------------------------------------------------------- + +(defsuite "literals" + (deftest "numbers are numbers" + (assert-type "number" 42) + (assert-type "number" 3.14) + (assert-type "number" -1)) + + (deftest "strings are strings" + (assert-type "string" "hello") + (assert-type "string" "")) + + (deftest "booleans are booleans" + (assert-type "boolean" true) + (assert-type "boolean" false)) + + (deftest "nil is nil" + (assert-type "nil" nil) + (assert-nil nil)) + + (deftest "lists are lists" + (assert-type "list" (list 1 2 3)) + (assert-type "list" (list))) + + (deftest "dicts are dicts" + (assert-type "dict" {:a 1 :b 2}))) + + +;; -------------------------------------------------------------------------- +;; Arithmetic +;; -------------------------------------------------------------------------- + +(defsuite "arithmetic" + (deftest "addition" + (assert-equal 3 (+ 1 2)) + (assert-equal 0 (+ 0 0)) + (assert-equal -1 (+ 1 -2)) + (assert-equal 10 (+ 1 2 3 4))) + + (deftest "subtraction" + (assert-equal 1 (- 3 2)) + (assert-equal -1 (- 2 3))) + + (deftest "multiplication" + (assert-equal 6 (* 2 3)) + (assert-equal 0 (* 0 100)) + (assert-equal 24 (* 1 2 3 4))) + + (deftest "division" + (assert-equal 2 (/ 6 3)) + (assert-equal 2.5 (/ 5 2))) + + (deftest "modulo" + (assert-equal 1 (mod 7 3)) + (assert-equal 0 (mod 6 3)))) + + +;; -------------------------------------------------------------------------- +;; Comparison +;; -------------------------------------------------------------------------- + +(defsuite "comparison" + (deftest "equality" + (assert-true (= 1 1)) + (assert-false (= 1 2)) + (assert-true (= "a" "a")) + (assert-false (= "a" "b"))) + + (deftest "deep equality" + (assert-true (equal? (list 1 2 3) (list 1 2 3))) + (assert-false (equal? (list 1 2) (list 1 3))) + (assert-true (equal? {:a 1} {:a 1})) + (assert-false (equal? {:a 1} {:a 2}))) + + (deftest "ordering" + (assert-true (< 1 2)) + (assert-false (< 2 1)) + (assert-true (> 2 1)) + (assert-true (<= 1 1)) + (assert-true (<= 1 2)) + (assert-true (>= 2 2)) + (assert-true (>= 3 2)))) + + +;; -------------------------------------------------------------------------- +;; String operations +;; -------------------------------------------------------------------------- + +(defsuite "strings" + (deftest "str concatenation" + (assert-equal "abc" (str "a" "b" "c")) + (assert-equal "hello world" (str "hello" " " "world")) + (assert-equal "42" (str 42)) + (assert-equal "" (str))) + + (deftest "string-length" + (assert-equal 5 (string-length "hello")) + (assert-equal 0 (string-length ""))) + + (deftest "substring" + (assert-equal "ell" (substring "hello" 1 4)) + (assert-equal "hello" (substring "hello" 0 5))) + + (deftest "string-contains?" + (assert-true (string-contains? "hello world" "world")) + (assert-false (string-contains? "hello" "xyz"))) + + (deftest "upcase and downcase" + (assert-equal "HELLO" (upcase "hello")) + (assert-equal "hello" (downcase "HELLO"))) + + (deftest "trim" + (assert-equal "hello" (trim " hello ")) + (assert-equal "hello" (trim "hello"))) + + (deftest "split and join" + (assert-equal (list "a" "b" "c") (split "a,b,c" ",")) + (assert-equal "a-b-c" (join "-" (list "a" "b" "c"))))) + + +;; -------------------------------------------------------------------------- +;; List operations +;; -------------------------------------------------------------------------- + +(defsuite "lists" + (deftest "constructors" + (assert-equal (list 1 2 3) (list 1 2 3)) + (assert-equal (list) (list)) + (assert-length 3 (list 1 2 3))) + + (deftest "first and rest" + (assert-equal 1 (first (list 1 2 3))) + (assert-equal (list 2 3) (rest (list 1 2 3))) + (assert-nil (first (list))) + (assert-equal (list) (rest (list)))) + + (deftest "nth" + (assert-equal 1 (nth (list 1 2 3) 0)) + (assert-equal 2 (nth (list 1 2 3) 1)) + (assert-equal 3 (nth (list 1 2 3) 2))) + + (deftest "last" + (assert-equal 3 (last (list 1 2 3))) + (assert-nil (last (list)))) + + (deftest "cons and append" + (assert-equal (list 0 1 2) (cons 0 (list 1 2))) + (assert-equal (list 1 2 3 4) (append (list 1 2) (list 3 4)))) + + (deftest "reverse" + (assert-equal (list 3 2 1) (reverse (list 1 2 3))) + (assert-equal (list) (reverse (list)))) + + (deftest "empty?" + (assert-true (empty? (list))) + (assert-false (empty? (list 1)))) + + (deftest "len" + (assert-equal 0 (len (list))) + (assert-equal 3 (len (list 1 2 3)))) + + (deftest "contains?" + (assert-true (contains? (list 1 2 3) 2)) + (assert-false (contains? (list 1 2 3) 4))) + + (deftest "flatten" + (assert-equal (list 1 2 3 4) (flatten (list (list 1 2) (list 3 4)))))) + + +;; -------------------------------------------------------------------------- +;; Dict operations +;; -------------------------------------------------------------------------- + +(defsuite "dicts" + (deftest "dict literal" + (assert-type "dict" {:a 1 :b 2}) + (assert-equal 1 (get {:a 1} "a")) + (assert-equal 2 (get {:a 1 :b 2} "b"))) + + (deftest "assoc" + (assert-equal {:a 1 :b 2} (assoc {:a 1} "b" 2)) + (assert-equal {:a 99} (assoc {:a 1} "a" 99))) + + (deftest "dissoc" + (assert-equal {:b 2} (dissoc {:a 1 :b 2} "a"))) + + (deftest "keys and vals" + (let ((d {:a 1 :b 2})) + (assert-length 2 (keys d)) + (assert-length 2 (vals d)) + (assert-contains "a" (keys d)) + (assert-contains "b" (keys d)))) + + (deftest "has-key?" + (assert-true (has-key? {:a 1} "a")) + (assert-false (has-key? {:a 1} "b"))) + + (deftest "merge" + (assert-equal {:a 1 :b 2 :c 3} + (merge {:a 1 :b 2} {:c 3})) + (assert-equal {:a 99 :b 2} + (merge {:a 1 :b 2} {:a 99})))) + + +;; -------------------------------------------------------------------------- +;; Predicates +;; -------------------------------------------------------------------------- + +(defsuite "predicates" + (deftest "nil?" + (assert-true (nil? nil)) + (assert-false (nil? 0)) + (assert-false (nil? false)) + (assert-false (nil? ""))) + + (deftest "number?" + (assert-true (number? 42)) + (assert-true (number? 3.14)) + (assert-false (number? "42"))) + + (deftest "string?" + (assert-true (string? "hello")) + (assert-false (string? 42))) + + (deftest "list?" + (assert-true (list? (list 1 2))) + (assert-false (list? "not a list"))) + + (deftest "dict?" + (assert-true (dict? {:a 1})) + (assert-false (dict? (list 1)))) + + (deftest "boolean?" + (assert-true (boolean? true)) + (assert-true (boolean? false)) + (assert-false (boolean? nil)) + (assert-false (boolean? 0))) + + (deftest "not" + (assert-true (not false)) + (assert-true (not nil)) + (assert-false (not true)) + (assert-false (not 1)) + (assert-false (not "x")))) + + +;; -------------------------------------------------------------------------- +;; Special forms +;; -------------------------------------------------------------------------- + +(defsuite "special-forms" + (deftest "if" + (assert-equal "yes" (if true "yes" "no")) + (assert-equal "no" (if false "yes" "no")) + (assert-equal "no" (if nil "yes" "no")) + (assert-nil (if false "yes"))) + + (deftest "when" + (assert-equal "yes" (when true "yes")) + (assert-nil (when false "yes"))) + + (deftest "cond" + (assert-equal "a" (cond true "a" :else "b")) + (assert-equal "b" (cond false "a" :else "b")) + (assert-equal "c" (cond + false "a" + false "b" + :else "c"))) + + (deftest "and" + (assert-true (and true true)) + (assert-false (and true false)) + (assert-false (and false true)) + (assert-equal 3 (and 1 2 3))) + + (deftest "or" + (assert-equal 1 (or 1 2)) + (assert-equal 2 (or false 2)) + (assert-equal "fallback" (or nil false "fallback")) + (assert-false (or false false))) + + (deftest "let" + (assert-equal 3 (let ((x 1) (y 2)) (+ x y))) + (assert-equal "hello world" + (let ((a "hello") (b " world")) (str a b)))) + + (deftest "let clojure-style" + (assert-equal 3 (let (x 1 y 2) (+ x y)))) + + (deftest "do / begin" + (assert-equal 3 (do 1 2 3)) + (assert-equal "last" (begin "first" "middle" "last"))) + + (deftest "define" + (define x 42) + (assert-equal 42 x)) + + (deftest "set!" + (define x 1) + (set! x 2) + (assert-equal 2 x))) + + +;; -------------------------------------------------------------------------- +;; Lambda and closures +;; -------------------------------------------------------------------------- + +(defsuite "lambdas" + (deftest "basic lambda" + (let ((add (fn (a b) (+ a b)))) + (assert-equal 3 (add 1 2)))) + + (deftest "closure captures env" + (let ((x 10)) + (let ((add-x (fn (y) (+ x y)))) + (assert-equal 15 (add-x 5))))) + + (deftest "lambda as argument" + (assert-equal (list 2 4 6) + (map (fn (x) (* x 2)) (list 1 2 3)))) + + (deftest "recursive lambda via define" + (define factorial + (fn (n) (if (<= n 1) 1 (* n (factorial (- n 1)))))) + (assert-equal 120 (factorial 5))) + + (deftest "higher-order returns lambda" + (let ((make-adder (fn (n) (fn (x) (+ n x))))) + (let ((add5 (make-adder 5))) + (assert-equal 8 (add5 3)))))) + + +;; -------------------------------------------------------------------------- +;; Higher-order forms +;; -------------------------------------------------------------------------- + +(defsuite "higher-order" + (deftest "map" + (assert-equal (list 2 4 6) + (map (fn (x) (* x 2)) (list 1 2 3))) + (assert-equal (list) (map (fn (x) x) (list)))) + + (deftest "filter" + (assert-equal (list 2 4) + (filter (fn (x) (= (mod x 2) 0)) (list 1 2 3 4))) + (assert-equal (list) + (filter (fn (x) false) (list 1 2 3)))) + + (deftest "reduce" + (assert-equal 10 (reduce (fn (acc x) (+ acc x)) 0 (list 1 2 3 4))) + (assert-equal 0 (reduce (fn (acc x) (+ acc x)) 0 (list)))) + + (deftest "some" + (assert-true (some (fn (x) (> x 3)) (list 1 2 3 4 5))) + (assert-false (some (fn (x) (> x 10)) (list 1 2 3)))) + + (deftest "every?" + (assert-true (every? (fn (x) (> x 0)) (list 1 2 3))) + (assert-false (every? (fn (x) (> x 2)) (list 1 2 3)))) + + (deftest "map-indexed" + (assert-equal (list "0:a" "1:b" "2:c") + (map-indexed (fn (i x) (str i ":" x)) (list "a" "b" "c"))))) + + +;; -------------------------------------------------------------------------- +;; Components +;; -------------------------------------------------------------------------- + +(defsuite "components" + (deftest "defcomp creates component" + (defcomp ~test-comp (&key title) + (div title)) + (assert-true (not (nil? ~test-comp)))) + + (deftest "component renders with keyword args" + (defcomp ~greeting (&key name) + (span (str "Hello, " name "!"))) + (assert-true (not (nil? ~greeting)))) + + (deftest "component with children" + (defcomp ~box (&key &rest children) + (div :class "box" children)) + (assert-true (not (nil? ~box)))) + + (deftest "component with default via or" + (defcomp ~label (&key text) + (span (or text "default"))) + (assert-true (not (nil? ~label))))) + + +;; -------------------------------------------------------------------------- +;; Macros +;; -------------------------------------------------------------------------- + +(defsuite "macros" + (deftest "defmacro creates macro" + (defmacro unless (cond &rest body) + `(if (not ,cond) (do ,@body))) + (assert-equal "yes" (unless false "yes")) + (assert-nil (unless true "no"))) + + (deftest "quasiquote and unquote" + (let ((x 42)) + (assert-equal (list 1 42 3) `(1 ,x 3)))) + + (deftest "splice-unquote" + (let ((xs (list 2 3 4))) + (assert-equal (list 1 2 3 4 5) `(1 ,@xs 5))))) + + +;; -------------------------------------------------------------------------- +;; Threading macro +;; -------------------------------------------------------------------------- + +(defsuite "threading" + (deftest "thread-first" + (assert-equal 8 (-> 5 (+ 1) (+ 2))) + (assert-equal "HELLO" (-> "hello" upcase)) + (assert-equal "HELLO WORLD" + (-> "hello" + (str " world") + upcase)))) + + +;; -------------------------------------------------------------------------- +;; Truthiness +;; -------------------------------------------------------------------------- + +(defsuite "truthiness" + (deftest "truthy values" + (assert-true (if 1 true false)) + (assert-true (if "x" true false)) + (assert-true (if (list 1) true false)) + (assert-true (if true true false))) + + (deftest "falsy values" + (assert-false (if false true false)) + (assert-false (if nil true false))) + + ;; NOTE: empty list, zero, and empty string truthiness is + ;; platform-dependent. Python treats all three as falsy. + ;; JavaScript treats [] as truthy but 0 and "" as falsy. + ;; These tests are omitted — each bootstrapper should emit + ;; platform-specific truthiness tests instead. + ) + + +;; -------------------------------------------------------------------------- +;; Edge cases and regression tests +;; -------------------------------------------------------------------------- + +(defsuite "edge-cases" + (deftest "nested let scoping" + (let ((x 1)) + (let ((x 2)) + (assert-equal 2 x)) + ;; outer x should be unchanged by inner let + ;; (this tests that let creates a new scope) + )) + + (deftest "recursive map" + (assert-equal (list (list 2 4) (list 6 8)) + (map (fn (sub) (map (fn (x) (* x 2)) sub)) + (list (list 1 2) (list 3 4))))) + + (deftest "keyword as value" + (assert-equal "class" :class) + (assert-equal "id" :id)) + + (deftest "dict with evaluated values" + (let ((x 42)) + (assert-equal 42 (get {:val x} "val")))) + + (deftest "nil propagation" + (assert-nil (get {:a 1} "missing")) + (assert-equal "default" (or (get {:a 1} "missing") "default"))) + + (deftest "empty operations" + (assert-equal (list) (map (fn (x) x) (list))) + (assert-equal (list) (filter (fn (x) true) (list))) + (assert-equal 0 (reduce (fn (acc x) (+ acc x)) 0 (list))) + (assert-equal 0 (len (list))) + (assert-equal "" (str)))) diff --git a/shared/sx/ref/test-framework.sx b/shared/sx/ref/test-framework.sx new file mode 100644 index 0000000..21e5a10 --- /dev/null +++ b/shared/sx/ref/test-framework.sx @@ -0,0 +1,86 @@ +;; ========================================================================== +;; test-framework.sx — Reusable test macros and assertion helpers +;; +;; Loaded first by all test runners. Provides deftest, defsuite, and +;; assertion helpers. Requires 5 platform functions from the host: +;; +;; try-call (thunk) -> {:ok true} | {:ok false :error "msg"} +;; report-pass (name) -> platform-specific pass output +;; report-fail (name error) -> platform-specific fail output +;; push-suite (name) -> push suite name onto context stack +;; pop-suite () -> pop suite name from context stack +;; +;; Any host that provides these 5 functions can run any test spec. +;; ========================================================================== + + +;; -------------------------------------------------------------------------- +;; 1. Test framework macros +;; -------------------------------------------------------------------------- + +(defmacro deftest (name &rest body) + `(let ((result (try-call (fn () ,@body)))) + (if (get result "ok") + (report-pass ,name) + (report-fail ,name (get result "error"))))) + +(defmacro defsuite (name &rest items) + `(do (push-suite ,name) + ,@items + (pop-suite))) + + +;; -------------------------------------------------------------------------- +;; 2. Assertion helpers — defined in SX, available in test bodies +;; -------------------------------------------------------------------------- + +(define assert-equal + (fn (expected actual) + (assert (equal? expected actual) + (str "Expected " (str expected) " but got " (str actual))))) + +(define assert-not-equal + (fn (a b) + (assert (not (equal? a b)) + (str "Expected values to differ but both are " (str a))))) + +(define assert-true + (fn (val) + (assert val (str "Expected truthy but got " (str val))))) + +(define assert-false + (fn (val) + (assert (not val) (str "Expected falsy but got " (str val))))) + +(define assert-nil + (fn (val) + (assert (nil? val) (str "Expected nil but got " (str val))))) + +(define assert-type + (fn (expected-type val) + (let ((actual-type + (if (nil? val) "nil" + (if (boolean? val) "boolean" + (if (number? val) "number" + (if (string? val) "string" + (if (list? val) "list" + (if (dict? val) "dict" + "unknown")))))))) + (assert (= expected-type actual-type) + (str "Expected type " expected-type " but got " actual-type))))) + +(define assert-length + (fn (expected-len col) + (assert (= (len col) expected-len) + (str "Expected length " expected-len " but got " (len col))))) + +(define assert-contains + (fn (item col) + (assert (some (fn (x) (equal? x item)) col) + (str "Expected collection to contain " (str item))))) + +(define assert-throws + (fn (thunk) + (let ((result (try-call thunk))) + (assert (not (get result "ok")) + "Expected an error to be thrown but none was")))) diff --git a/shared/sx/ref/test-parser.sx b/shared/sx/ref/test-parser.sx new file mode 100644 index 0000000..57cc4a8 --- /dev/null +++ b/shared/sx/ref/test-parser.sx @@ -0,0 +1,222 @@ +;; ========================================================================== +;; test-parser.sx — Tests for the SX parser and serializer +;; +;; Requires: test-framework.sx loaded first. +;; Modules tested: parser.sx +;; +;; Platform functions required (beyond test framework): +;; sx-parse (source) -> list of AST expressions +;; sx-serialize (expr) -> SX source string +;; make-symbol (name) -> Symbol value +;; make-keyword (name) -> Keyword value +;; symbol-name (sym) -> string +;; keyword-name (kw) -> string +;; ========================================================================== + + +;; -------------------------------------------------------------------------- +;; Literal parsing +;; -------------------------------------------------------------------------- + +(defsuite "parser-literals" + (deftest "parse integers" + (assert-equal (list 42) (sx-parse "42")) + (assert-equal (list 0) (sx-parse "0")) + (assert-equal (list -7) (sx-parse "-7"))) + + (deftest "parse floats" + (assert-equal (list 3.14) (sx-parse "3.14")) + (assert-equal (list -0.5) (sx-parse "-0.5"))) + + (deftest "parse strings" + (assert-equal (list "hello") (sx-parse "\"hello\"")) + (assert-equal (list "") (sx-parse "\"\""))) + + (deftest "parse escape: newline" + (assert-equal (list "a\nb") (sx-parse "\"a\\nb\""))) + + (deftest "parse escape: tab" + (assert-equal (list "a\tb") (sx-parse "\"a\\tb\""))) + + (deftest "parse escape: quote" + (assert-equal (list "a\"b") (sx-parse "\"a\\\"b\""))) + + (deftest "parse booleans" + (assert-equal (list true) (sx-parse "true")) + (assert-equal (list false) (sx-parse "false"))) + + (deftest "parse nil" + (assert-equal (list nil) (sx-parse "nil"))) + + (deftest "parse keywords" + (let ((result (sx-parse ":hello"))) + (assert-length 1 result) + (assert-equal "hello" (keyword-name (first result))))) + + (deftest "parse symbols" + (let ((result (sx-parse "foo"))) + (assert-length 1 result) + (assert-equal "foo" (symbol-name (first result)))))) + + +;; -------------------------------------------------------------------------- +;; Composite parsing +;; -------------------------------------------------------------------------- + +(defsuite "parser-lists" + (deftest "parse empty list" + (let ((result (sx-parse "()"))) + (assert-length 1 result) + (assert-equal (list) (first result)))) + + (deftest "parse list of numbers" + (let ((result (sx-parse "(1 2 3)"))) + (assert-length 1 result) + (assert-equal (list 1 2 3) (first result)))) + + (deftest "parse nested lists" + (let ((result (sx-parse "(1 (2 3) 4)"))) + (assert-length 1 result) + (assert-equal (list 1 (list 2 3) 4) (first result)))) + + (deftest "parse square brackets as list" + (let ((result (sx-parse "[1 2 3]"))) + (assert-length 1 result) + (assert-equal (list 1 2 3) (first result)))) + + (deftest "parse mixed types" + (let ((result (sx-parse "(42 \"hello\" true nil)"))) + (assert-length 1 result) + (let ((lst (first result))) + (assert-equal 42 (nth lst 0)) + (assert-equal "hello" (nth lst 1)) + (assert-equal true (nth lst 2)) + (assert-nil (nth lst 3)))))) + + +;; -------------------------------------------------------------------------- +;; Dict parsing +;; -------------------------------------------------------------------------- + +(defsuite "parser-dicts" + (deftest "parse empty dict" + (let ((result (sx-parse "{}"))) + (assert-length 1 result) + (assert-type "dict" (first result)))) + + (deftest "parse dict with keyword keys" + (let ((result (sx-parse "{:a 1 :b 2}"))) + (assert-length 1 result) + (let ((d (first result))) + (assert-type "dict" d) + (assert-equal 1 (get d "a")) + (assert-equal 2 (get d "b"))))) + + (deftest "parse dict with string values" + (let ((result (sx-parse "{:name \"alice\"}"))) + (assert-length 1 result) + (assert-equal "alice" (get (first result) "name"))))) + + +;; -------------------------------------------------------------------------- +;; Comments and whitespace +;; -------------------------------------------------------------------------- + +(defsuite "parser-whitespace" + (deftest "skip line comments" + (assert-equal (list 42) (sx-parse ";; comment\n42")) + (assert-equal (list 1 2) (sx-parse "1 ;; middle\n2"))) + + (deftest "skip whitespace" + (assert-equal (list 42) (sx-parse " 42 ")) + (assert-equal (list 1 2) (sx-parse " 1 \n\t 2 "))) + + (deftest "parse multiple top-level expressions" + (assert-length 3 (sx-parse "1 2 3")) + (assert-equal (list 1 2 3) (sx-parse "1 2 3"))) + + (deftest "empty input" + (assert-equal (list) (sx-parse ""))) + + (deftest "only comments" + (assert-equal (list) (sx-parse ";; just a comment\n;; another")))) + + +;; -------------------------------------------------------------------------- +;; Quote sugar +;; -------------------------------------------------------------------------- + +(defsuite "parser-quote-sugar" + (deftest "quasiquote" + (let ((result (sx-parse "`foo"))) + (assert-length 1 result) + (let ((expr (first result))) + (assert-type "list" expr) + (assert-equal "quasiquote" (symbol-name (first expr)))))) + + (deftest "unquote" + (let ((result (sx-parse ",foo"))) + (assert-length 1 result) + (let ((expr (first result))) + (assert-type "list" expr) + (assert-equal "unquote" (symbol-name (first expr)))))) + + (deftest "splice-unquote" + (let ((result (sx-parse ",@foo"))) + (assert-length 1 result) + (let ((expr (first result))) + (assert-type "list" expr) + (assert-equal "splice-unquote" (symbol-name (first expr))))))) + + +;; -------------------------------------------------------------------------- +;; Serializer +;; -------------------------------------------------------------------------- + +(defsuite "serializer" + (deftest "serialize number" + (assert-equal "42" (sx-serialize 42))) + + (deftest "serialize string" + (assert-equal "\"hello\"" (sx-serialize "hello"))) + + (deftest "serialize boolean" + (assert-equal "true" (sx-serialize true)) + (assert-equal "false" (sx-serialize false))) + + (deftest "serialize nil" + (assert-equal "nil" (sx-serialize nil))) + + (deftest "serialize keyword" + (assert-equal ":foo" (sx-serialize (make-keyword "foo")))) + + (deftest "serialize symbol" + (assert-equal "bar" (sx-serialize (make-symbol "bar")))) + + (deftest "serialize list" + (assert-equal "(1 2 3)" (sx-serialize (list 1 2 3)))) + + (deftest "serialize empty list" + (assert-equal "()" (sx-serialize (list)))) + + (deftest "serialize nested" + (assert-equal "(1 (2 3) 4)" (sx-serialize (list 1 (list 2 3) 4))))) + + +;; -------------------------------------------------------------------------- +;; Round-trip: parse then serialize +;; -------------------------------------------------------------------------- + +(defsuite "parser-roundtrip" + (deftest "roundtrip number" + (assert-equal "42" (sx-serialize (first (sx-parse "42"))))) + + (deftest "roundtrip string" + (assert-equal "\"hello\"" (sx-serialize (first (sx-parse "\"hello\""))))) + + (deftest "roundtrip list" + (assert-equal "(1 2 3)" (sx-serialize (first (sx-parse "(1 2 3)"))))) + + (deftest "roundtrip nested" + (assert-equal "(a (b c))" + (sx-serialize (first (sx-parse "(a (b c))")))))) diff --git a/shared/sx/ref/test-render.sx b/shared/sx/ref/test-render.sx new file mode 100644 index 0000000..c714fc7 --- /dev/null +++ b/shared/sx/ref/test-render.sx @@ -0,0 +1,167 @@ +;; ========================================================================== +;; test-render.sx — Tests for the HTML rendering adapter +;; +;; Requires: test-framework.sx loaded first. +;; Modules tested: render.sx, adapter-html.sx +;; +;; Platform functions required (beyond test framework): +;; render-html (sx-source) -> HTML string +;; Parses the sx-source string, evaluates via render-to-html in a +;; fresh env, and returns the resulting HTML string. +;; (This is a test-only convenience that wraps parse + render-to-html.) +;; ========================================================================== + + +;; -------------------------------------------------------------------------- +;; Basic element rendering +;; -------------------------------------------------------------------------- + +(defsuite "render-elements" + (deftest "simple div" + (assert-equal "
a
b
hello world
" + (render-html "(p \"hello\" \" world\")"))) + + (deftest "number content" + (assert-equal "42" + (render-html "(span 42)")))) + + +;; -------------------------------------------------------------------------- +;; Attributes +;; -------------------------------------------------------------------------- + +(defsuite "render-attrs" + (deftest "string attribute" + (let ((html (render-html "(div :id \"main\" \"content\")"))) + (assert-true (string-contains? html "id=\"main\"")) + (assert-true (string-contains? html "content")))) + + (deftest "class attribute" + (let ((html (render-html "(div :class \"foo bar\" \"x\")"))) + (assert-true (string-contains? html "class=\"foo bar\"")))) + + (deftest "multiple attributes" + (let ((html (render-html "(a :href \"/home\" :class \"link\" \"Home\")"))) + (assert-true (string-contains? html "href=\"/home\"")) + (assert-true (string-contains? html "class=\"link\"")) + (assert-true (string-contains? html "Home"))))) + + +;; -------------------------------------------------------------------------- +;; Void elements +;; -------------------------------------------------------------------------- + +(defsuite "render-void" + (deftest "br is self-closing" + (assert-equal "a
b
" + (render-html "(<> (p \"a\") (p \"b\"))"))) + + (deftest "empty fragment" + (assert-equal "" + (render-html "(<>)")))) + + +;; -------------------------------------------------------------------------- +;; HTML escaping +;; -------------------------------------------------------------------------- + +(defsuite "render-escaping" + (deftest "text content is escaped" + (let ((html (render-html "(p \"\")"))) + (assert-false (string-contains? html "