;; ========================================================================== ;; test-signals-advanced.sx — Stress tests for the reactive signal system ;; ;; Requires: test-framework.sx loaded first. ;; Modules tested: signals.sx (signal, deref, reset!, swap!, computed, ;; effect, batch) ;; ;; Note: Multi-expression lambda bodies are wrapped in (do ...) for ;; compatibility with evaluators that support only single-expression bodies. ;; ========================================================================== ;; -------------------------------------------------------------------------- ;; Signal basics extended ;; -------------------------------------------------------------------------- (defsuite "signal-basics-extended" (deftest "signal with nil initial value" (let ((s (signal nil))) (assert-true (signal? s)) (assert-nil (deref s)))) (deftest "signal with list value" (let ((s (signal (list 1 2 3)))) (assert-equal (list 1 2 3) (deref s)) (reset! s (list 4 5 6)) (assert-equal (list 4 5 6) (deref s)))) (deftest "signal with dict value" (let ((s (signal {:name "alice" :score 42}))) (assert-equal "alice" (get (deref s) "name")) (assert-equal 42 (get (deref s) "score")))) (deftest "signal with lambda value" (let ((fn-val (fn (x) (* x 2))) (s (signal nil))) (reset! s fn-val) ;; The stored lambda should be callable (assert-equal 10 ((deref s) 5)))) (deftest "multiple signals independent of each other" (let ((a (signal 1)) (b (signal 2)) (c (signal 3))) (reset! a 10) ;; b and c must be unchanged (assert-equal 10 (deref a)) (assert-equal 2 (deref b)) (assert-equal 3 (deref c)) (reset! b 20) (assert-equal 10 (deref a)) (assert-equal 20 (deref b)) (assert-equal 3 (deref c)))) (deftest "deref returns current value not a stale snapshot" (let ((s (signal "first"))) (let ((snap1 (deref s))) (reset! s "second") (let ((snap2 (deref s))) ;; snap1 holds the string "first" (immutable), snap2 is "second" (assert-equal "first" snap1) (assert-equal "second" snap2)))))) ;; -------------------------------------------------------------------------- ;; Computed chains ;; -------------------------------------------------------------------------- (defsuite "computed-chains" (deftest "chain of three computed signals" (let ((base (signal 2)) (doubled (computed (fn () (* 2 (deref base))))) (tripled (computed (fn () (* 3 (deref doubled)))))) ;; Initial: base=2 → doubled=4 → tripled=12 (assert-equal 4 (deref doubled)) (assert-equal 12 (deref tripled)) ;; Update propagates through the entire chain (reset! base 5) (assert-equal 10 (deref doubled)) (assert-equal 30 (deref tripled)))) (deftest "computed depending on multiple signals" (let ((x (signal 3)) (y (signal 4)) (hypo (computed (fn () ;; sqrt(x^2 + y^2) — Pythagorean hypotenuse (integer approx) (+ (* (deref x) (deref x)) (* (deref y) (deref y))))))) (assert-equal 25 (deref hypo)) (reset! x 0) (assert-equal 16 (deref hypo)) (reset! y 0) (assert-equal 0 (deref hypo)))) (deftest "computed with conditional logic" (let ((flag (signal true)) (a (signal 10)) (b (signal 99)) (result (computed (fn () (if (deref flag) (deref a) (deref b)))))) (assert-equal 10 (deref result)) (reset! flag false) (assert-equal 99 (deref result)) (reset! b 42) (assert-equal 42 (deref result)) (reset! flag true) (assert-equal 10 (deref result)))) (deftest "diamond dependency: A->B, A->C, B+C->D" ;; A change in A must propagate via both B and C to D, ;; but D must still hold a coherent (not intermediate) value. (let ((A (signal 1)) (B (computed (fn () (* 2 (deref A))))) (C (computed (fn () (* 3 (deref A))))) (D (computed (fn () (+ (deref B) (deref C)))))) ;; A=1 → B=2, C=3 → D=5 (assert-equal 2 (deref B)) (assert-equal 3 (deref C)) (assert-equal 5 (deref D)) ;; A=4 → B=8, C=12 → D=20 (reset! A 4) (assert-equal 8 (deref B)) (assert-equal 12 (deref C)) (assert-equal 20 (deref D)))) (deftest "computed returns nil when source signal is nil" (let ((s (signal nil)) (c (computed (fn () (let ((v (deref s))) (when (not (nil? v)) (* v 2))))))) (assert-nil (deref c)) (reset! s 7) (assert-equal 14 (deref c)) (reset! s nil) (assert-nil (deref c))))) ;; -------------------------------------------------------------------------- ;; Effect patterns ;; -------------------------------------------------------------------------- (defsuite "effect-patterns" (deftest "effect runs immediately on creation" (let ((ran (signal false))) (effect (fn () (reset! ran true))) (assert-true (deref ran)))) (deftest "effect re-runs when dependency changes" (let ((n (signal 0)) (calls (signal 0))) (effect (fn () (do (deref n) (swap! calls inc)))) ;; Initial run counts as 1 (assert-equal 1 (deref calls)) (reset! n 1) (assert-equal 2 (deref calls)) (reset! n 2) (assert-equal 3 (deref calls)))) (deftest "effect with multiple dependencies" (let ((a (signal "x")) (b (signal "y")) (calls (signal 0))) (effect (fn () (do (deref a) (deref b) (swap! calls inc)))) (assert-equal 1 (deref calls)) ;; Changing a triggers re-run (reset! a "x2") (assert-equal 2 (deref calls)) ;; Changing b also triggers re-run (reset! b "y2") (assert-equal 3 (deref calls)))) (deftest "effect cleanup function called on re-run" (let ((trigger (signal 0)) (cleanups (signal 0))) (effect (fn () (do (deref trigger) ;; Return a cleanup function (fn () (swap! cleanups inc))))) ;; First run — no previous cleanup to call (assert-equal 0 (deref cleanups)) ;; Second run — previous cleanup fires first (reset! trigger 1) (assert-equal 1 (deref cleanups)) ;; Third run — second cleanup fires (reset! trigger 2) (assert-equal 2 (deref cleanups)))) (deftest "effect tracks only actually-deref'd signals" ;; An effect that conditionally reads signal B should only re-run ;; for B changes when B is actually read (flag=true). (let ((flag (signal true)) (b (signal 0)) (calls (signal 0))) (effect (fn () (do (deref flag) (when (deref flag) (deref b)) (swap! calls inc)))) ;; Initial run reads both flag and b (assert-equal 1 (deref calls)) ;; flip flag to false — re-run, but now b is NOT deref'd (reset! flag false) (assert-equal 2 (deref calls)) ;; Changing b should NOT trigger another run (b wasn't deref'd last time) (reset! b 99) (assert-equal 2 (deref calls))))) ;; -------------------------------------------------------------------------- ;; Batch behavior ;; -------------------------------------------------------------------------- (defsuite "batch-behavior" (deftest "batch coalesces multiple signal updates into one effect run" (let ((a (signal 0)) (b (signal 0)) (run-count (signal 0))) (effect (fn () (do (deref a) (deref b) (swap! run-count inc)))) ;; Initial run (assert-equal 1 (deref run-count)) ;; Two writes inside a single batch → one effect run, not two (batch (fn () (do (reset! a 1) (reset! b 2)))) (assert-equal 2 (deref run-count)))) (deftest "nested batch — inner batch does not flush, outer batch does" (let ((s (signal 0)) (run-count (signal 0))) (effect (fn () (do (deref s) (swap! run-count inc)))) (assert-equal 1 (deref run-count)) (batch (fn () (batch (fn () (reset! s 1))) ;; Still inside outer batch — should not have fired yet (reset! s 2))) ;; Outer batch ends → exactly one more run (assert-equal 2 (deref run-count)) ;; Final value is the last write (assert-equal 2 (deref s)))) (deftest "batch with computed — computed updates once not per signal write" (let ((x (signal 0)) (y (signal 0)) (sum (computed (fn () (+ (deref x) (deref y))))) (recomps (signal 0))) ;; Track recomputations by wrapping via an effect (effect (fn () (do (deref sum) (swap! recomps inc)))) ;; Initial: effect + computed both ran once (assert-equal 1 (deref recomps)) (batch (fn () (do (reset! x 10) (reset! y 20)))) ;; sum must reflect both changes (assert-equal 30 (deref sum)) ;; effect re-ran at most once more (not twice) (assert-equal 2 (deref recomps)))) (deftest "batch executes the thunk" ;; batch runs the thunk for side effects; return value is implementation-defined (let ((s (signal 0))) (batch (fn () (reset! s 42))) (assert-equal 42 (deref s))))) ;; -------------------------------------------------------------------------- ;; Swap patterns ;; -------------------------------------------------------------------------- (defsuite "swap-patterns" (deftest "swap! with increment function" (let ((n (signal 0))) (swap! n inc) (assert-equal 1 (deref n)) (swap! n inc) (assert-equal 2 (deref n)))) (deftest "swap! with list append" (let ((items (signal (list)))) (swap! items (fn (l) (append l "a"))) (swap! items (fn (l) (append l "b"))) (swap! items (fn (l) (append l "c"))) (assert-equal (list "a" "b" "c") (deref items)))) (deftest "swap! with dict assoc" (let ((store (signal {}))) (swap! store (fn (d) (assoc d "x" 1))) (swap! store (fn (d) (assoc d "y" 2))) (assert-equal 1 (get (deref store) "x")) (assert-equal 2 (get (deref store) "y")))) (deftest "multiple swap! in sequence build up correct value" (let ((acc (signal 0))) (swap! acc + 10) (swap! acc + 5) (swap! acc - 3) (assert-equal 12 (deref acc))))) ;; -------------------------------------------------------------------------- ;; call-lambda + trampoline — event handler pattern ;; -------------------------------------------------------------------------- ;; ;; Regression: dom-on wraps Lambda event handlers in JS functions that ;; call callLambda. callLambda returns a Thunk (TCO), but the wrapper ;; never trampolined it, so the handler body (swap!, reset!, etc.) ;; never executed. Buttons rendered but clicks had no effect. ;; ;; These tests verify the pattern that dom-on uses: ;; (trampoline (call-lambda handler (list arg))) ;; must resolve thunks and execute side effects. (defsuite "call-lambda-trampoline-handlers" (deftest "call-lambda + trampoline executes signal mutation" (let ((count (signal 0)) (handler (fn () (swap! count + 1)))) (trampoline (call-lambda handler (list))) (assert-equal 1 (deref count)))) (deftest "call-lambda + trampoline with event arg" (let ((last-val (signal nil)) (handler (fn (e) (reset! last-val e)))) (trampoline (call-lambda handler (list "click-event"))) (assert-equal "click-event" (deref last-val)))) (deftest "call-lambda + trampoline executes multi-statement body" (let ((a (signal 0)) (b (signal 0)) (handler (fn () (reset! a 10) (reset! b 20)))) (trampoline (call-lambda handler (list))) (assert-equal 10 (deref a)) (assert-equal 20 (deref b)))) (deftest "repeated call-lambda accumulates side effects" (let ((count (signal 0)) (handler (fn () (swap! count + 1)))) (trampoline (call-lambda handler (list))) (trampoline (call-lambda handler (list))) (trampoline (call-lambda handler (list))) (assert-equal 3 (deref count)))) (deftest "call-lambda handler calling another lambda" (let ((log (signal (list))) (inner (fn (msg) (reset! log (append (deref log) (list msg))))) (outer (fn () (inner "hello") (inner "world")))) (trampoline (call-lambda outer (list))) (assert-equal (list "hello" "world") (deref log)))))