2026-10-28 19:17:39 +01:00

264 lines
11 KiB
Clojure

;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS SUBSIDIARY SL
(ns performance-tests.render-wasm.builder-test
"Tests canonical scene construction and the shared seeded number generator.
Fixed random sequences preserve scene draws during generator extraction."
(:require
[app.common.uuid :as uuid]
[benches.render-wasm.builder :as b :include-macros true]
[benches.render-wasm.declarations :as decl]
[benches.render-wasm.random :as random]
[benches.render-wasm.scenes.rects]
[benches.render-wasm.snapshot :as snapshot]
[cljs.test :as t :include-macros true]
[performance-tests.render-wasm.test-helpers :as helpers]))
(defn- build
([] (build {}))
([params] ((:build (decl/registered-scene :rects)) (merge {:seed 42 :count 5} params))))
(defn- add-tile
"Helper defined outside a scene scope, called from inside one."
[index]
(b/rect [:tile index] {:x (* 10 index)}))
(t/deftest repeatable-builds-are-identical
(t/is (= (build) (build)))
(t/is (not= (build) (build {:seed 43}))))
(t/deftest shared-generator-retains-scene-draws-and-independent-state
(doseq [[seed expected] [[0 [1144304738 1416247 958946056 627933444 2007157716]]
[42 [2581720956 1925393290 3661312704 2876485805 750819978]]
[4294967295 [3850105811 813802916 3073704848 4054706436 3630262831]]]]
(let [first-source (random/create seed)
second-source (random/create seed)]
(t/is (= expected (mapv #(* 4294967296 %) (repeatedly 5 first-source))))
(t/is (= expected (mapv #(* 4294967296 %) (repeatedly 5 second-source))))
(t/is (every? #(and (<= 0 %) (< % 1)) (repeatedly 1000 first-source)))))
(let [rng (:rng @(b/start {:seed 42}))]
(t/is (= [2581720956 1925393290 3661312704 2876485805 750819978]
(mapv #(* 4294967296 %) (repeatedly 5 rng))))))
(t/deftest shared-generator-rejects-invalid-seeds
(doseq [seed [-1 4294967296 1.5 nil false "42" js/NaN js/Infinity]]
(t/is (= ::random/invalid-seed
(:type (helpers/failure-data #(random/create seed)))))))
(t/deftest rectangle-workload-keeps-default-count
(let [instance (build {:seed 42 :count 1000})
rects (helpers/root-shapes instance)]
(t/is (= 1000 (count rects)))
(t/is (< 1 (count (distinct (map :x rects)))))))
(t/deftest default-workload-is-canonical
(let [instance (build {:count 25})
rects (helpers/root-shapes instance)]
(t/is (= 25 (count rects)))
(t/is (every? #(= :rect (:type %)) rects))
(t/is (every? (fn [rect]
(and (= 1 (count (:fills rect)))
(= 1 (count (:strokes rect)))
(= 10 (:stroke-width (first (:strokes rect))))
(= :center (:stroke-alignment (first (:strokes rect))))))
rects))
(t/is (every? (fn [rect]
(and (re-matches #"^#[0-9a-f]{6}$" (:fill-color (first (:fills rect))))
(<= 0.1 (:fill-opacity (first (:fills rect))) 0.999)
(every? #(<= 0 % 24) [(:r1 rect) (:r2 rect) (:r3 rect) (:r4 rect)])))
rects))))
(t/deftest geometry-is-page-relative-and-in-range
(let [params {:seed 7 :count 40 :width 800 :height 600 :min-size 5 :max-size 15}
instance (build params)
rects (helpers/root-shapes instance)]
(t/is (every? #(<= 0 (:x %) 800) rects))
(t/is (every? #(<= 0 (:y %) 600) rects))
(t/is (every? #(<= 5 (:width %) 15) rects))
(t/is (every? #(<= 5 (:height %) 15) rects))
(t/is (= 800 (get-in instance [:objects uuid/zero :width])))
(t/is (= 600 (get-in instance [:objects uuid/zero :height])))))
(t/deftest labels-are-snapshot-local
(let [instance (b/scene {:seed 1}
(b/rect)
(b/rect :hero {})
(b/rect [:tile 1] {}))
refs (:refs instance)
children (helpers/child-ids instance uuid/zero)]
(t/is (= #{:hero [:tile 1]} (set (keys refs))))
(t/is (= (get refs :hero) (second children)))
(t/is (= (get refs [:tile 1]) (nth children 2)))))
(t/deftest attrs-override-generated-values
(let [instance (b/scene {:seed 1
:defaults {:rect {:x (b/gen-int 0 100)}}}
(b/rect {:x 7 :fills []}))
rect (first (helpers/root-shapes instance))]
(t/is (= 7 (:x rect)))
(t/is (= [] (:fills rect)))))
(t/deftest ordinary-forms-work-inside-the-scope
(let [instance (b/scene {:seed 1}
(let [first-id (b/rect)]
(t/is (uuid? first-id)))
(doseq [index (range 3)]
(add-tile index))
(b/rect :last {:x 99}))
refs (:refs instance)
rects (helpers/root-shapes instance)]
(t/is (= 5 (count rects)))
(t/is (= [0 10 20]
(mapv #(:x (get-in instance [:objects (get refs [:tile %])]))
(range 3))))
(t/is (= 99 (:x (get-in instance [:objects (get refs :last)]))))))
(t/deftest duplicate-labels-are-rejected
(let [data (helpers/failure-data
#(b/scene {:seed 1}
(b/rect :dup {})
(b/rect :dup {})))]
(t/is (= ::b/duplicate-label (:type data)))
(t/is (= :dup (:label data)))))
(t/deftest failed-builds-leak-no-state
(let [clean (build)]
(helpers/failure-data #(b/scene {:seed 99}
(b/rect :dup {})
(b/rect :dup {})))
(t/is (= clean (build)))))
(t/deftest rect-outside-scope-is-rejected
(let [data (helpers/failure-data #(b/rect))]
(t/is (= ::b/outside-scope (:type data)))))
(t/deftest nested-scopes-are-rejected
(let [data (helpers/failure-data
#(b/scene {:seed 1}
(b/scene {:seed 2}
(b/rect))))]
(t/is (= ::b/nested-scope (:type data)))))
(t/deftest invalid-seeds-are-rejected
(t/is (= ::b/invalid-seed
(:type (helpers/failure-data #(b/scene {} (b/rect))))))
(t/is (= ::b/invalid-seed
(:type (helpers/failure-data #(b/scene {:seed -1} (b/rect))))))
(t/is (= ::b/invalid-seed
(:type (helpers/failure-data #(b/scene {:seed 4294967296} (b/rect)))))))
(t/deftest runtime-attrs-maps-are-resolved
(let [attrs {:x 11}
instance (b/scene {:seed 1}
(b/rect attrs))
rect (first (helpers/root-shapes instance))]
(t/is (= 11 (:x rect)))))
(t/deftest single-label-form-is-supported
(let [instance (b/scene {:seed 1}
(b/rect :solo))
id (get (:refs instance) :solo)]
(t/is (uuid? id))
(t/is (= [id] (helpers/child-ids instance uuid/zero)))))
(t/deftest built-snapshot-feeds-the-scene-api
(let [instance (b/scene {:seed 1}
(b/rect :hero {}))
order (snapshot/upload-order instance)
hero-id (snapshot/ref-id instance :hero)]
(t/is (= [uuid/zero hero-id] (mapv :id order)))))
(t/deftest invalid-labels-are-rejected
(let [data (helpers/failure-data #(b/scene {:seed 1} (b/rect "hero" {})))]
(t/is (= ::b/invalid-label (:type data)))))
(t/deftest protected-keys-cannot-be-overridden
(let [other (uuid/custom 9 9)
instance (b/scene {:seed 1}
(b/rect {:type :circle :id other :parent-id other}))
rect (first (helpers/root-shapes instance))]
(t/is (= :rect (:type rect)))
(t/is (not= other (:id rect)))
(t/is (= uuid/zero (:parent-id rect)))))
(t/deftest root-protected-keys-cannot-be-overridden
(let [other (uuid/custom 9 8)
instance (b/scene {:seed 1
:root {:id other :type :rect :width 10}}
(b/rect))
root (get-in instance [:objects uuid/zero])]
(t/is (= uuid/zero (:id root)))
(t/is (= :frame (:type root)))
(t/is (= 10 (:width root)))))
(t/deftest attrs-override-generated-fills
(let [fill {:fill-color "#ffffff" :fill-opacity 1}
instance (b/scene {:seed 1}
(b/rect {:fills [fill]}))
rect (first (helpers/root-shapes instance))]
(t/is (= [fill] (:fills rect)))))
(t/deftest generator-helpers-produce-expected-values
(let [instance (b/scene {:seed 3
:defaults {:rect {:opacity (b/gen-float 0.5 0.6)
:x (b/gen-one-of [4 5])
:r1 (b/gen-int 3 5)
:fills (b/gen-vector (b/gen-fill))}}}
(b/rect))
rect (first (helpers/root-shapes instance))]
(t/is (<= 0.5 (:opacity rect) 0.6))
(t/is (contains? #{4 5} (:x rect)))
(t/is (<= 3 (:r1 rect) 4))
(t/is (re-matches #"^#[0-9a-f]{6}$" (:fill-color (first (:fills rect)))))))
(t/deftest empty-one-of-is-rejected
(let [data (helpers/failure-data #(b/gen-one-of []))]
(t/is (= ::b/no-values (:type data)))))
(t/deftest circle-creates-ellipse-type
(let [instance (b/scene {:seed 1}
(b/circle {:x 10 :y 20 :width 30 :height 40}))
circle (first (helpers/root-shapes instance))]
(t/is (= :circle (:type circle)))
(t/is (= [10 20 30 40] [(:x circle) (:y circle) (:width circle) (:height circle)]))
(t/is (= 1 (count (helpers/child-ids instance uuid/zero))))))
(t/deftest circle-uses-circle-defaults
(let [instance (b/scene {:seed 3
:defaults {:circle {:x (b/gen-int 10 12)}}}
(b/circle))
circle (first (helpers/root-shapes instance))]
(t/is (contains? #{10 11} (:x circle)))))
(t/deftest circle-ignores-rect-defaults
(let [instance (b/scene {:seed 1
:defaults {:rect {:x 999}}}
(b/circle))
circle (first (helpers/root-shapes instance))]
(t/is (not= 999 (:x circle)))))
(t/deftest circle-geometry-is-page-relative-and-in-range
(let [instance (b/scene {:seed 7
:defaults {:circle {:x (b/gen-int 0 800)
:y (b/gen-int 0 600)
:width (b/gen-int 5 15)
:height (b/gen-int 5 15)}}}
(doseq [_ (range 40)]
(b/circle)))
circles (helpers/root-shapes instance)]
(t/is (= 40 (count circles)))
(t/is (every? #(<= 0 (:x %) 800) circles))
(t/is (every? #(<= 0 (:y %) 600) circles))
(t/is (every? #(<= 5 (:width %) 15) circles))
(t/is (every? #(<= 5 (:height %) 15) circles))))
(t/deftest circle-preserves-radius-attrs
(let [instance (b/scene {:seed 1}
(b/circle {:x 0 :y 0 :width 20 :height 20 :rx 5 :ry 7}))
circle (first (helpers/root-shapes instance))]
(t/is (= [5 7] [(:rx circle) (:ry circle)]))))