Split monolithic test.sx into composable test specs: - test-framework.sx: deftest/defsuite macros + assertion helpers - test-eval.sx: core evaluator + primitives (81 tests) - test-parser.sx: parser + serializer + round-trips (39 tests) - test-router.sx: route matching from router.sx (18 tests) - test-render.sx: HTML adapter rendering (23 tests) Runners auto-discover specs and test whatever bootstrapped code is available. Usage: `run.js eval parser router` or just `run.js`. Legacy mode (`--legacy`) still runs monolithic test.sx. Router tests use bootstrapped functions (sx_ref.py / sx-browser.js) because the hand-written evaluator's flat-dict env model doesn't support set! mutation across lambda closure boundaries. JS: 161/161. Python: 159/161 (2 parser escape bugs found). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
603 lines
18 KiB
Plaintext
603 lines
18 KiB
Plaintext
;; ==========================================================================
|
|
;; test.sx — Self-hosting SX test suite (backward-compatible entry point)
|
|
;;
|
|
;; This file includes the test framework and core eval tests inline.
|
|
;; It exists for backward compatibility — runners that load "test.sx"
|
|
;; get the same 81 tests as before.
|
|
;;
|
|
;; For modular testing, runners should instead load:
|
|
;; 1. test-framework.sx (macros + assertions)
|
|
;; 2. One or more test specs: test-eval.sx, test-parser.sx,
|
|
;; test-router.sx, test-render.sx, etc.
|
|
;;
|
|
;; Platform functions required:
|
|
;; 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
|
|
;;
|
|
;; Usage:
|
|
;; ;; Host injects platform functions into env, then:
|
|
;; (eval-file "test.sx" env)
|
|
;;
|
|
;; The same test.sx runs on every host — Python, JavaScript, etc.
|
|
;; ==========================================================================
|
|
|
|
|
|
;; --------------------------------------------------------------------------
|
|
;; 1. Test framework macros
|
|
;; --------------------------------------------------------------------------
|
|
;;
|
|
;; deftest and defsuite are macros that make test.sx directly executable.
|
|
;; The host provides try-call (error catching), reporting, and suite
|
|
;; context — everything else is pure SX.
|
|
|
|
(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
|
|
;; --------------------------------------------------------------------------
|
|
;;
|
|
;; These are regular functions (not special forms). They use the `assert`
|
|
;; primitive underneath but provide better error messages.
|
|
|
|
(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)
|
|
;; Implemented via predicate dispatch since type-of is a platform
|
|
;; function not available in all hosts. Uses nested if to avoid
|
|
;; Scheme-style cond detection for 2-element predicate calls.
|
|
;; Boolean checked before number (subtypes on some platforms).
|
|
(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"))))
|
|
|
|
|
|
;; ==========================================================================
|
|
;; 3. Test suites — SX testing SX
|
|
;; ==========================================================================
|
|
|
|
|
|
;; --------------------------------------------------------------------------
|
|
;; 3a. 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})))
|
|
|
|
|
|
;; --------------------------------------------------------------------------
|
|
;; 3b. 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))))
|
|
|
|
|
|
;; --------------------------------------------------------------------------
|
|
;; 3c. 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))))
|
|
|
|
|
|
;; --------------------------------------------------------------------------
|
|
;; 3d. 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")))))
|
|
|
|
|
|
;; --------------------------------------------------------------------------
|
|
;; 3e. 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))))))
|
|
|
|
|
|
;; --------------------------------------------------------------------------
|
|
;; 3f. 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}))))
|
|
|
|
|
|
;; --------------------------------------------------------------------------
|
|
;; 3g. 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"))))
|
|
|
|
|
|
;; --------------------------------------------------------------------------
|
|
;; 3h. 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)))
|
|
|
|
|
|
;; --------------------------------------------------------------------------
|
|
;; 3i. 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))))))
|
|
|
|
|
|
;; --------------------------------------------------------------------------
|
|
;; 3j. 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")))))
|
|
|
|
|
|
;; --------------------------------------------------------------------------
|
|
;; 3k. Components
|
|
;; --------------------------------------------------------------------------
|
|
|
|
(defsuite "components"
|
|
(deftest "defcomp creates component"
|
|
(defcomp ~test-comp (&key title)
|
|
(div title))
|
|
;; Component is bound and not nil
|
|
(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)))))
|
|
|
|
|
|
;; --------------------------------------------------------------------------
|
|
;; 3l. 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)))))
|
|
|
|
|
|
;; --------------------------------------------------------------------------
|
|
;; 3m. 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))))
|
|
|
|
|
|
;; --------------------------------------------------------------------------
|
|
;; 3n. 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.
|
|
)
|
|
|
|
|
|
;; --------------------------------------------------------------------------
|
|
;; 3o. 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))))
|