git mv eval.sx, parser.sx, primitives.sx, render.sx, cek.sx, frames.sx, continuations.sx, callcc.sx, types.sx, special-forms.sx → spec/ Tests → spec/tests/ Both bootstrappers verified — find files via spec/ → web/ → shared/sx/ref/ Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
747 lines
24 KiB
Plaintext
747 lines
24 KiB
Plaintext
;; ==========================================================================
|
|
;; 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 "cond with 2-element predicate as first test"
|
|
;; Regression: cond misclassifies Clojure-style as scheme-style when
|
|
;; the first test is a 2-element list like (nil? x) or (empty? x).
|
|
;; The evaluator checks: is first arg a 2-element list? If yes, treats
|
|
;; as scheme-style ((test body) ...) — returning the arg instead of
|
|
;; evaluating the predicate call.
|
|
(assert-equal 0 (cond (nil? nil) 0 :else 1))
|
|
(assert-equal 1 (cond (nil? "x") 0 :else 1))
|
|
(assert-equal "empty" (cond (empty? (list)) "empty" :else "not-empty"))
|
|
(assert-equal "not-empty" (cond (empty? (list 1)) "empty" :else "not-empty"))
|
|
(assert-equal "yes" (cond (not false) "yes" :else "no"))
|
|
(assert-equal "no" (cond (not true) "yes" :else "no")))
|
|
|
|
(deftest "cond with 2-element predicate and no :else"
|
|
;; Same bug, but without :else — this is the worst case because the
|
|
;; bootstrapper heuristic also breaks (all clauses are 2-element lists).
|
|
(assert-equal "found"
|
|
(cond (nil? nil) "found"
|
|
(nil? "x") "other"))
|
|
(assert-equal "b"
|
|
(cond (nil? "x") "a"
|
|
(not false) "b")))
|
|
|
|
(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)))))
|
|
|
|
(deftest "multi-body lambda returns last value"
|
|
;; All body expressions must execute. Return value is the last.
|
|
;; Catches: sf-lambda using nth(args,1) instead of rest(args).
|
|
(let ((f (fn (x) (+ x 1) (+ x 2) (+ x 3))))
|
|
(assert-equal 13 (f 10))))
|
|
|
|
(deftest "multi-body lambda side effects via dict mutation"
|
|
;; Verify all body expressions run by mutating a shared dict.
|
|
(let ((state (dict "a" 0 "b" 0)))
|
|
(let ((f (fn ()
|
|
(dict-set! state "a" 1)
|
|
(dict-set! state "b" 2)
|
|
"done")))
|
|
(assert-equal "done" (f))
|
|
(assert-equal 1 (get state "a"))
|
|
(assert-equal 2 (get state "b")))))
|
|
|
|
(deftest "multi-body lambda two expressions"
|
|
;; Simplest case: two body expressions, return value is second.
|
|
(assert-equal 20
|
|
((fn (x) (+ x 1) (* x 2)) 10))
|
|
;; And with zero-arg lambda
|
|
(assert-equal 42
|
|
((fn () (+ 1 2) 42)))))
|
|
|
|
|
|
;; --------------------------------------------------------------------------
|
|
;; 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))))
|
|
|
|
(deftest "defcomp default affinity is auto"
|
|
(defcomp ~aff-default (&key x)
|
|
(div x))
|
|
(assert-equal "auto" (component-affinity ~aff-default)))
|
|
|
|
(deftest "defcomp affinity client"
|
|
(defcomp ~aff-client (&key x)
|
|
:affinity :client
|
|
(div x))
|
|
(assert-equal "client" (component-affinity ~aff-client)))
|
|
|
|
(deftest "defcomp affinity server"
|
|
(defcomp ~aff-server (&key x)
|
|
:affinity :server
|
|
(div x))
|
|
(assert-equal "server" (component-affinity ~aff-server)))
|
|
|
|
(deftest "defcomp affinity preserves body"
|
|
(defcomp ~aff-body (&key val)
|
|
:affinity :client
|
|
(span val))
|
|
;; Component should still render correctly
|
|
(assert-equal "client" (component-affinity ~aff-body))
|
|
(assert-true (not (nil? ~aff-body)))))
|
|
|
|
|
|
;; --------------------------------------------------------------------------
|
|
;; 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))))
|
|
|
|
|
|
;; --------------------------------------------------------------------------
|
|
;; Server-only tests — skip in browser (defpage, streaming functions)
|
|
;; These require forms.sx which is only loaded server-side.
|
|
;; --------------------------------------------------------------------------
|
|
|
|
(when (get (try-call (fn () stream-chunk-id)) "ok")
|
|
|
|
(defsuite "defpage"
|
|
(deftest "basic defpage returns page-def"
|
|
(let ((p (defpage test-basic :path "/test" :auth :public :content (div "hello"))))
|
|
(assert-true (not (nil? p)))
|
|
(assert-equal "test-basic" (get p "name"))
|
|
(assert-equal "/test" (get p "path"))
|
|
(assert-equal "public" (get p "auth"))))
|
|
|
|
(deftest "defpage content expr is unevaluated AST"
|
|
(let ((p (defpage test-content :path "/c" :auth :public :content (~my-comp :title "hi"))))
|
|
(assert-true (not (nil? (get p "content"))))))
|
|
|
|
(deftest "defpage with :stream"
|
|
(let ((p (defpage test-stream :path "/s" :auth :public :stream true :content (div "x"))))
|
|
(assert-equal true (get p "stream"))))
|
|
|
|
(deftest "defpage with :shell"
|
|
(let ((p (defpage test-shell :path "/sh" :auth :public :stream true
|
|
:shell (~my-layout (~suspense :id "data" :fallback (div "loading...")))
|
|
:content (~my-streamed :data data-val))))
|
|
(assert-true (not (nil? (get p "shell"))))
|
|
(assert-true (not (nil? (get p "content"))))))
|
|
|
|
(deftest "defpage with :fallback"
|
|
(let ((p (defpage test-fallback :path "/f" :auth :public :stream true
|
|
:fallback (div :class "skeleton" "loading")
|
|
:content (div "done"))))
|
|
(assert-true (not (nil? (get p "fallback"))))))
|
|
|
|
(deftest "defpage with :data"
|
|
(let ((p (defpage test-data :path "/d" :auth :public
|
|
:data (fetch-items)
|
|
:content (~items-list :items items))))
|
|
(assert-true (not (nil? (get p "data"))))))
|
|
|
|
(deftest "defpage missing fields are nil"
|
|
(let ((p (defpage test-minimal :path "/m" :auth :public :content (div "x"))))
|
|
(assert-nil (get p "data"))
|
|
(assert-nil (get p "filter"))
|
|
(assert-nil (get p "aside"))
|
|
(assert-nil (get p "menu"))
|
|
(assert-nil (get p "shell"))
|
|
(assert-nil (get p "fallback"))
|
|
(assert-equal false (get p "stream")))))
|
|
|
|
;; --------------------------------------------------------------------------
|
|
;; Multi-stream data protocol (from forms.sx)
|
|
;; --------------------------------------------------------------------------
|
|
|
|
(defsuite "stream-chunk-id"
|
|
(deftest "extracts stream-id from chunk"
|
|
(assert-equal "my-slot" (stream-chunk-id {"stream-id" "my-slot" "x" 1})))
|
|
|
|
(deftest "defaults to stream-content when missing"
|
|
(assert-equal "stream-content" (stream-chunk-id {"x" 1 "y" 2}))))
|
|
|
|
(defsuite "stream-chunk-bindings"
|
|
(deftest "removes stream-id from chunk"
|
|
(let ((bindings (stream-chunk-bindings {"stream-id" "slot" "name" "alice" "age" 30})))
|
|
(assert-equal "alice" (get bindings "name"))
|
|
(assert-equal 30 (get bindings "age"))
|
|
(assert-nil (get bindings "stream-id"))))
|
|
|
|
(deftest "returns all keys when no stream-id"
|
|
(let ((bindings (stream-chunk-bindings {"a" 1 "b" 2})))
|
|
(assert-equal 1 (get bindings "a"))
|
|
(assert-equal 2 (get bindings "b")))))
|
|
|
|
(defsuite "normalize-binding-key"
|
|
(deftest "converts underscores to hyphens"
|
|
(assert-equal "my-key" (normalize-binding-key "my_key")))
|
|
|
|
(deftest "leaves hyphens unchanged"
|
|
(assert-equal "my-key" (normalize-binding-key "my-key")))
|
|
|
|
(deftest "handles multiple underscores"
|
|
(assert-equal "a-b-c" (normalize-binding-key "a_b_c"))))
|
|
|
|
(defsuite "bind-stream-chunk"
|
|
(deftest "creates fresh env with bindings"
|
|
(let ((base {"existing" 42})
|
|
(chunk {"stream-id" "slot" "user-name" "bob" "count" 5})
|
|
(env (bind-stream-chunk chunk base)))
|
|
;; Base env bindings are preserved
|
|
(assert-equal 42 (get env "existing"))
|
|
;; Chunk bindings are added (stream-id removed)
|
|
(assert-equal "bob" (get env "user-name"))
|
|
(assert-equal 5 (get env "count"))
|
|
;; stream-id is not in env
|
|
(assert-nil (get env "stream-id"))))
|
|
|
|
(deftest "isolates env from base — bindings don't leak to base"
|
|
(let ((base {"x" 1})
|
|
(chunk {"stream-id" "s" "y" 2})
|
|
(env (bind-stream-chunk chunk base)))
|
|
;; Chunk bindings should not appear in base
|
|
(assert-nil (get base "y"))
|
|
;; Base bindings should be in derived env
|
|
(assert-equal 1 (get env "x")))))
|
|
|
|
(defsuite "validate-stream-data"
|
|
(deftest "valid: list of dicts"
|
|
(assert-true (validate-stream-data
|
|
(list {"stream-id" "a" "x" 1}
|
|
{"stream-id" "b" "y" 2}))))
|
|
|
|
(deftest "valid: empty list"
|
|
(assert-true (validate-stream-data (list))))
|
|
|
|
(deftest "invalid: single dict (not a list)"
|
|
(assert-equal false (validate-stream-data {"x" 1})))
|
|
|
|
(deftest "invalid: list containing non-dict"
|
|
(assert-equal false (validate-stream-data (list {"x" 1} "oops" {"y" 2})))))
|
|
|
|
|
|
;; --------------------------------------------------------------------------
|
|
;; Multi-stream end-to-end scenarios
|
|
;; --------------------------------------------------------------------------
|
|
|
|
(defsuite "multi-stream routing"
|
|
(deftest "stream-chunk-id routes different chunks to different slots"
|
|
(let ((chunks (list
|
|
{"stream-id" "stream-fast" "msg" "quick"}
|
|
{"stream-id" "stream-medium" "msg" "steady"}
|
|
{"stream-id" "stream-slow" "msg" "slow"}))
|
|
(ids (map stream-chunk-id chunks)))
|
|
(assert-equal "stream-fast" (nth ids 0))
|
|
(assert-equal "stream-medium" (nth ids 1))
|
|
(assert-equal "stream-slow" (nth ids 2))))
|
|
|
|
(deftest "bind-stream-chunk creates isolated envs per chunk"
|
|
(let ((base {"layout" "main"})
|
|
(chunk-a {"stream-id" "a" "title" "First" "count" 1})
|
|
(chunk-b {"stream-id" "b" "title" "Second" "count" 2})
|
|
(env-a (bind-stream-chunk chunk-a base))
|
|
(env-b (bind-stream-chunk chunk-b base)))
|
|
;; Each env has its own bindings
|
|
(assert-equal "First" (get env-a "title"))
|
|
(assert-equal "Second" (get env-b "title"))
|
|
(assert-equal 1 (get env-a "count"))
|
|
(assert-equal 2 (get env-b "count"))
|
|
;; Both share base
|
|
(assert-equal "main" (get env-a "layout"))
|
|
(assert-equal "main" (get env-b "layout"))
|
|
;; Neither leaks into base
|
|
(assert-nil (get base "title"))))
|
|
|
|
(deftest "normalize-binding-key applied to chunk keys"
|
|
(let ((chunk {"stream-id" "s" "user_name" "alice" "item_count" 3})
|
|
(bindings (stream-chunk-bindings chunk)))
|
|
;; Keys with underscores need normalizing for SX env
|
|
(assert-equal "alice" (get bindings "user_name"))
|
|
;; normalize-binding-key converts them
|
|
(assert-equal "user-name" (normalize-binding-key "user_name"))
|
|
(assert-equal "item-count" (normalize-binding-key "item_count"))))
|
|
|
|
(deftest "defpage stream flag defaults to false"
|
|
(let ((p (defpage test-no-stream :path "/ns" :auth :public :content (div "x"))))
|
|
(assert-equal false (get p "stream"))))
|
|
|
|
(deftest "defpage stream true recorded in page-def"
|
|
(let ((p (defpage test-with-stream :path "/ws" :auth :public
|
|
:stream true
|
|
:shell (~layout (~suspense :id "data"))
|
|
:content (~chunk :val val))))
|
|
(assert-equal true (get p "stream"))
|
|
(assert-true (not (nil? (get p "shell")))))))
|
|
|
|
) ;; end (when has-server-forms?)
|