Files
rose-ash/spec/tests/test-signals-advanced.sx
giles bdbf594bc8 Add 125 new tests: CEK-advanced, signals, integration (1063/1063)
New test files:
- test-cek-advanced.sx (63): deep nesting, complex calls, macro
  interaction, environment stress, edge cases
- test-signals-advanced.sx (24): signal types, computed chains,
  effects, batch, swap patterns
- test-integration.sx (38): parse-eval roundtrip, render pipeline,
  macro-render, data-driven rendering, error recovery, complex patterns

Bugs found:
- -> (thread-first) doesn't work with HO special forms (map, filter)
  because they're dispatched by name, not as env values. Documented
  as known limitation — use nested calls instead of ->.
- batch returns nil, not thunk's return value
- upcase not a primitive (use upper)

Data-first HO forms attempted but reverted — the swap logic in
ho-setup-dispatch caused subtle paren/nesting issues. Needs more
careful implementation in a future session.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 16:13:07 +00:00

297 lines
10 KiB
Plaintext

;; ==========================================================================
;; 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)))))