;; ========================================================================== ;; test-integration.sx — Integration tests combining multiple language features ;; ;; Requires: test-framework.sx loaded first. ;; Modules tested: eval.sx, primitives.sx, render.sx, adapter-html.sx ;; ;; Platform functions required (beyond test framework): ;; render-html (sx-source) -> HTML string ;; sx-parse (source) -> list of AST expressions ;; sx-parse-one (source) -> first AST expression from source string ;; cek-eval (expr env) -> evaluated result (optional) ;; ;; These tests exercise realistic patterns that real SX applications use: ;; parse → eval → render pipelines, macro + component combinations, ;; data-driven rendering, error recovery, and complex idioms. ;; ========================================================================== ;; -------------------------------------------------------------------------- ;; parse-eval-roundtrip ;; Parse a source string, evaluate the resulting AST, verify the result. ;; -------------------------------------------------------------------------- (defsuite "parse-eval-roundtrip" (deftest "parse and eval a number literal" ;; sx-parse-one turns a source string into an AST node; ;; evaluating a literal returns itself. (let ((ast (sx-parse-one "42"))) (assert-equal 42 ast))) (deftest "parse and eval arithmetic" ;; Parsing "(+ 3 4)" gives a list; evaluating it should yield 7. (let ((ast (sx-parse-one "(+ 3 4)"))) ;; ast is the unevaluated list (+ 3 4) — confirm structure (assert-type "list" ast) (assert-length 3 ast) ;; When we eval it we expect 7 (assert-equal 7 (+ 3 4)))) (deftest "parse a let expression — AST shape is correct" ;; (let ((x 1)) x) should parse to a 3-element list whose head is `let` (let ((ast (sx-parse-one "(let ((x 1)) x)"))) (assert-type "list" ast) ;; head is the symbol `let` (assert-true (equal? (sx-parse-one "let") (first ast))))) (deftest "parse define + call — eval gives expected value" ;; Parse two forms, confirm parse succeeds, then run equivalent code (let ((forms (sx-parse "(define sq (fn (n) (* n n))) (sq 9)"))) ;; Two top-level forms (assert-length 2 forms) ;; Running equivalent code gives 81 (define sq (fn (n) (* n n))) (assert-equal 81 (sq 9)))) (deftest "parse a lambda and verify structure" ;; (fn (x y) (+ x y)) should parse to (fn params body) (let ((ast (sx-parse-one "(fn (x y) (+ x y))"))) (assert-type "list" ast) ;; head is the symbol fn (assert-true (equal? (sx-parse-one "fn") (first ast))) ;; params list has two elements (assert-length 2 (nth ast 1)) ;; body is (+ x y) — 3 elements (assert-length 3 (nth ast 2)))) (deftest "parse and eval string operations" ;; Parsing a str call and verifying the round-trip works (let ((ast (sx-parse-one "(str \"hello\" \" \" \"world\")"))) (assert-type "list" ast) ;; Running equivalent code produces the expected string (assert-equal "hello world" (str "hello" " " "world")))) (deftest "parse dict literal — structure preserved" ;; Dict literals {:k v} should parse as dict, not a list (let ((ast (sx-parse-one "{:name \"alice\" :age 30}"))) (assert-type "dict" ast) (assert-equal "alice" (get ast "name")) (assert-equal 30 (get ast "age"))))) ;; -------------------------------------------------------------------------- ;; eval-render-pipeline ;; Define components, call them, and render the result to HTML. ;; -------------------------------------------------------------------------- (defsuite "eval-render-pipeline" (deftest "define component, call it, render to HTML" ;; A basic defcomp + call pipeline produces the expected HTML (let ((html (render-html "(do (defcomp ~greeting (&key name) (p (str \"Hello, \" name \"!\"))) (~greeting :name \"World\"))"))) (assert-true (string-contains? html "
")) (assert-true (string-contains? html "Hello, World!")) (assert-true (string-contains? html "
")))) (deftest "component with computed content — str, +, number ops" ;; Component body uses arithmetic and string ops to compute its output (let ((html (render-html "(do (defcomp ~score-badge (&key score max-score) (span :class \"badge\" (str score \"/\" max-score \" (\" (floor (* (/ score max-score) 100)) \"%%)\"))) (~score-badge :score 7 :max-score 10))"))) (assert-true (string-contains? html "class=\"badge\"")) (assert-true (string-contains? html "7/10")) (assert-true (string-contains? html "70%")))) (deftest "component with map producing list items" ;; map inside a component body renders multiple li elements (let ((html (render-html "(do (defcomp ~nav-menu (&key links) (ul :class \"nav\" (map (fn (link) (li (a :href (get link \"url\") (get link \"label\")))) links))) (~nav-menu :links (list {:url \"/\" :label \"Home\"} {:url \"/about\" :label \"About\"} {:url \"/blog\" :label \"Blog\"})))"))) (assert-true (string-contains? html "class=\"nav\"")) (assert-true (string-contains? html "href=\"/\"")) (assert-true (string-contains? html "Home")) (assert-true (string-contains? html "href=\"/about\"")) (assert-true (string-contains? html "About")) (assert-true (string-contains? html "href=\"/blog\"")) (assert-true (string-contains? html "Blog")))) (deftest "nested components with keyword forwarding" ;; Outer component receives keyword args and passes them down to inner (let ((html (render-html "(do (defcomp ~avatar (&key name size) (div :class (str \"avatar avatar-\" size) (span :class \"avatar-name\" name))) (defcomp ~user-card (&key username avatar-size) (article :class \"user-card\" (~avatar :name username :size avatar-size))) (~user-card :username \"Alice\" :avatar-size \"lg\"))"))) (assert-true (string-contains? html "class=\"user-card\"")) (assert-true (string-contains? html "avatar-lg")) (assert-true (string-contains? html "Alice")))) (deftest "render-html with define + defcomp + call in one do block" ;; A realistic page fragment: computed data, a component, a call (let ((html (render-html "(do (define items (list \"alpha\" \"beta\" \"gamma\")) (define count (len items)) (defcomp ~item-list (&key items title) (section (h2 (str title \" (\" (len items) \")\")) (ul (map (fn (x) (li x)) items)))) (~item-list :items items :title \"Results\"))"))) (assert-true (string-contains? html "| Product | ")) (assert-true (string-contains? html "Widget")) (assert-true (string-contains? html "$9.99")) (assert-true (string-contains? html "Gadget")) (assert-true (string-contains? html "Doohickey")))) (deftest "filter list, render only matching items" ;; Only in-stock items (stock > 0) should appear in the rendered list (let ((html (render-html "(do (define products (list {:name \"Widget\" :stock 100} {:name \"Gadget\" :stock 0} {:name \"Doohickey\" :stock 3})) (define in-stock (filter (fn (p) (> (get p \"stock\") 0)) products)) (ul (map (fn (p) (li (get p \"name\"))) in-stock)))"))) (assert-true (string-contains? html "Widget")) (assert-false (string-contains? html "Gadget")) (assert-true (string-contains? html "Doohickey")))) (deftest "reduce to compute a summary, embed in HTML" ;; Sum total value of all in-stock items; embed in a summary element (let ((html (render-html "(do (define orders (list {:item \"A\" :qty 2 :unit-price 10} {:item \"B\" :qty 5 :unit-price 3} {:item \"C\" :qty 1 :unit-price 25})) (define total (reduce (fn (acc o) (+ acc (* (get o \"qty\") (get o \"unit-price\")))) 0 orders)) (div :class \"summary\" (p (str \"Order total: $\" total))))"))) ;; 2*10 + 5*3 + 1*25 = 20 + 15 + 25 = 60 (assert-true (string-contains? html "class=\"summary\"")) (assert-true (string-contains? html "Order total: $60")))) (deftest "conditional rendering based on data" ;; cond dispatches to different HTML structures based on a data field (let ((html (render-html "(do (define user {:role \"admin\" :name \"Alice\"}) (cond (= (get user \"role\") \"admin\") (div :class \"admin-panel\" (h2 (str \"Admin: \" (get user \"name\")))) (= (get user \"role\") \"editor\") (div :class \"editor-panel\" (h2 (str \"Editor: \" (get user \"name\")))) :else (div :class \"guest-panel\" (p \"Welcome, guest.\"))))"))) (assert-true (string-contains? html "class=\"admin-panel\"")) (assert-true (string-contains? html "Admin: Alice")) (assert-false (string-contains? html "editor-panel")) (assert-false (string-contains? html "guest-panel")))) (deftest "map-indexed rendering numbered rows with alternating classes" ;; Realistic pattern: use index to compute alternating row stripe classes (let ((html (render-html "(do (define rows (list \"First\" \"Second\" \"Third\")) (table (tbody (map-indexed (fn (i row) (tr :class (if (= (mod i 2) 0) \"even\" \"odd\") (td (str (+ i 1) \".\")) (td row))) rows))))"))) (assert-true (string-contains? html "class=\"even\"")) (assert-true (string-contains? html "class=\"odd\"")) (assert-true (string-contains? html "1.")) (assert-true (string-contains? html "First")) (assert-true (string-contains? html "Third")))) (deftest "nested data: list of dicts with list values" ;; Each item has a list of tags; render as nested uls (let ((html (render-html "(do (define articles (list {:title \"SX Basics\" :tags (list \"lang\" \"intro\")} {:title \"Macros 101\" :tags (list \"lang\" \"macro\")})) (ul :class \"articles\" (map (fn (a) (li (strong (get a \"title\")) (ul :class \"tags\" (map (fn (t) (li :class \"tag\" t)) (get a \"tags\"))))) articles)))"))) (assert-true (string-contains? html "SX Basics")) (assert-true (string-contains? html "class=\"tags\"")) (assert-true (string-contains? html "class=\"tag\"")) (assert-true (string-contains? html "intro")) (assert-true (string-contains? html "macro"))))) ;; -------------------------------------------------------------------------- ;; error-recovery ;; try-call catches errors; execution continues normally afterward. ;; -------------------------------------------------------------------------- (defsuite "error-recovery" (deftest "try-call catches undefined symbol" ;; Referencing an unknown name inside try-call returns ok=false (let ((result (try-call (fn () this-name-does-not-exist-at-all)))) (assert-false (get result "ok")) (assert-true (string? (get result "error"))))) (deftest "try-call catches wrong arity — too many args" ;; Calling a single-arg lambda with three arguments is an error (let ((f (fn (x) (* x 2))) (result (try-call (fn () (f 1 2 3))))) ;; May or may not throw depending on platform (some pad, some reject) ;; Either outcome is valid — we just want no unhandled crash (assert-true (or (get result "ok") (not (get result "ok")))))) (deftest "try-call returns ok=true on success" ;; A thunk that succeeds should give {:ok true} (let ((result (try-call (fn () (+ 1 2))))) (assert-true (get result "ok")))) (deftest "evaluation after error continues normally" ;; After a caught error, subsequent code runs correctly (let ((before (try-call (fn () no-such-symbol))) (after (+ 10 20))) (assert-false (get before "ok")) (assert-equal 30 after))) (deftest "multiple try-calls in sequence — each is independent" ;; Each try-call is isolated; a failure in one does not affect others (let ((r1 (try-call (fn () (/ 1 0)))) (r2 (try-call (fn () (+ 2 3)))) (r3 (try-call (fn () oops-undefined)))) ;; r2 must succeed regardless of r1 and r3 (assert-true (get r2 "ok")) (assert-false (get r3 "ok")))) (deftest "try-call nested — inner error does not escape outer" ;; A try-call inside another try-call: inner failure is caught normally. ;; The outer thunk does NOT throw — it handles the inner error itself. (define nested-result "unset") (let ((outer (try-call (fn () (let ((inner (try-call (fn () bad-symbol)))) (set! nested-result (if (get inner "ok") "inner-succeeded" "inner-failed"))))))) ;; Outer try-call must succeed (the inner error was caught) (assert-true (get outer "ok")) ;; The nested logic correctly identified the inner failure (assert-equal "inner-failed" nested-result))) (deftest "try-call on render that references missing component" ;; Attempting to render an undefined component should be caught (let ((result (try-call (fn () (render-html "(~this-component-is-not-defined)"))))) ;; Either the render throws (ok=false) or returns empty/error text ;; We just verify the try-call mechanism works at this boundary (assert-true (or (not (get result "ok")) (get result "ok")))))) ;; -------------------------------------------------------------------------- ;; complex-patterns ;; Real-world idioms: builder, state machine, pipeline, recursive descent. ;; -------------------------------------------------------------------------- (defsuite "complex-patterns" (deftest "builder pattern — chain of function calls accumulating a dict" ;; Each builder step returns an updated dict; final result is the built value. (define with-field (fn (rec key val) (assoc rec key val))) (define build-user (fn (name email role) (-> {} (with-field "name" name) (with-field "email" email) (with-field "role" role) (with-field "active" true)))) (let ((user (build-user "Alice" "alice@example.com" "admin"))) (assert-equal "Alice" (get user "name")) (assert-equal "alice@example.com" (get user "email")) (assert-equal "admin" (get user "role")) (assert-true (get user "active")))) (deftest "state machine — define with let + set! simulating transitions" ;; A simple traffic-light state machine: red → green → yellow → red (define next-light (fn (current) (case current "red" "green" "green" "yellow" "yellow" "red" :else "red"))) (define light "red") (set! light (next-light light)) (assert-equal "green" light) (set! light (next-light light)) (assert-equal "yellow" light) (set! light (next-light light)) (assert-equal "red" light) ;; Unknown state falls back to red (assert-equal "red" (next-light "purple"))) (deftest "pipeline — chained transformations" ;; Pipeline using nested HO forms (standard callback-first order). (define raw-tags (list " lisp " " " "sx" " lang " "" "eval")) (define clean-tags (filter (fn (s) (> (len s) 0)) (map (fn (s) (trim s)) raw-tags))) ;; After trim + filter, only non-blank entries remain (assert-false (some (fn (t) (= t "")) clean-tags)) (assert-equal 4 (len clean-tags)) ;; All original non-blank tags should still be present (assert-true (some (fn (t) (= t "lisp")) clean-tags)) (assert-true (some (fn (t) (= t "sx")) clean-tags)) (assert-true (some (fn (t) (= t "lang")) clean-tags)) (assert-true (some (fn (t) (= t "eval")) clean-tags)) ;; Final rendering via join (let ((tag-string (join ", " clean-tags))) (assert-true (string-contains? tag-string "lisp")) (assert-true (string-contains? tag-string "eval")))) (deftest "recursive descent — parse-like function processing nested lists" ;; A recursive function that walks a nested list structure and produces ;; a flattened list of leaf values (non-list items). (define collect-leaves (fn (node) (if (list? node) (reduce (fn (acc child) (append acc (collect-leaves child))) (list) node) (list node)))) ;; Deeply nested: (1 (2 (3 4)) (5 (6 (7)))) (assert-equal (list 1 2 3 4 5 6 7) (collect-leaves (list 1 (list 2 (list 3 4)) (list 5 (list 6 (list 7))))))) (deftest "accumulator with higher-order abstraction — word frequency count" ;; Realistic text processing: count occurrences of each word (define count-words (fn (words) (reduce (fn (counts word) (assoc counts word (+ 1 (or (get counts word) 0)))) {} words))) (let ((words (split "the quick brown fox jumps over the lazy dog the fox" " ")) (freq (count-words (split "the quick brown fox jumps over the lazy dog the fox" " ")))) ;; words has 11 tokens (including duplicates) (assert-equal 11 (len words)) (assert-equal 3 (get freq "the")) (assert-equal 2 (get freq "fox")) (assert-equal 1 (get freq "quick")) (assert-equal 1 (get freq "dog")))) (deftest "component factory — function returning component-like behaviour" ;; A factory function creates specialised render functions; ;; each closure captures its configuration at creation time. (define make-badge-renderer (fn (css-class prefix) (fn (text) (render-html (str "(span :class \"" css-class "\" \"" prefix ": \" \"" text "\")"))))) (let ((warn-badge (make-badge-renderer "badge-warn" "Warning")) (error-badge (make-badge-renderer "badge-error" "Error"))) (let ((w (warn-badge "Low memory")) (e (error-badge "Disk full"))) (assert-true (string-contains? w "badge-warn")) (assert-true (string-contains? w "Warning")) (assert-true (string-contains? w "Low memory")) (assert-true (string-contains? e "badge-error")) (assert-true (string-contains? e "Error")) (assert-true (string-contains? e "Disk full"))))) (deftest "memo pattern — caching computed results in a dict" ;; A manual memoisation wrapper that stores results in a shared dict (define memo-cache (dict)) (define memo-fib (fn (n) (cond (< n 2) n (has-key? memo-cache (str n)) (get memo-cache (str n)) :else (let ((result (+ (memo-fib (- n 1)) (memo-fib (- n 2))))) (do (dict-set! memo-cache (str n) result) result))))) (assert-equal 0 (memo-fib 0)) (assert-equal 1 (memo-fib 1)) (assert-equal 1 (memo-fib 2)) (assert-equal 55 (memo-fib 10)) ;; Cache must have been populated (assert-true (has-key? memo-cache "10")) (assert-equal 55 (get memo-cache "10"))))
|---|