mirror of
https://github.com/penpot/penpot.git
synced 2026-04-25 11:18:36 +00:00
✨ Make the common fressian module more testable (#8859)
* ✨ Add exhaustive unit tests for app.common.fressian encode/decode Add a JVM-only test suite (41 tests, 172 assertions) for the fressian serialisation layer, covering: - All custom handlers: char, clj/keyword, clj/symbol, clj/vector, clj/set, clj/map, clj/seq, clj/ratio, clj/bigint, java/instant, OffsetDateTime, linked/map (order preserved), linked/set (order preserved) - Built-in types: nil, boolean, int, long, double (NaN, ±Inf, boundaries), String, byte[], UUID - Edge cases: empty collections, nil values, ArrayMap/HashMap size boundary, mixed key types - Penpot-domain structures: shape maps with UUID keys, nested objects maps - Correctness: encode→decode→encode idempotency, independent encode calls * ♻️ Extract fressian handler helpers to private top-level functions Extract adapt-write-handler, adapt-read-handler, and merge-handlers out of the letfn in add-handlers! into reusable private functions. Also creates xf:adapt-write-handler and xf:adapt-read-handler transducers and adds overwrite-read-handlers and overwrite-write-handlers for advanced handler override use cases.
This commit is contained in:
parent
bc47b992eb
commit
a81cded0aa
@ -118,6 +118,36 @@
|
||||
(d/ordered-map)
|
||||
(partition-all 2 (seq kvs)))))
|
||||
|
||||
|
||||
(defn- adapt-write-handler
|
||||
[{:keys [name class wfn]}]
|
||||
[class {name (reify WriteHandler
|
||||
(write [_ w o]
|
||||
(wfn name w o)))}])
|
||||
|
||||
(defn- adapt-read-handler
|
||||
[{:keys [name rfn]}]
|
||||
[name (reify ReadHandler
|
||||
(read [_ rdr _ _]
|
||||
(rfn rdr)))])
|
||||
|
||||
(defn- merge-handlers
|
||||
[m1 m2]
|
||||
(-> (merge m1 m2)
|
||||
(d/without-nils)))
|
||||
|
||||
(def ^:private
|
||||
xf:adapt-write-handler
|
||||
(comp
|
||||
(filter :wfn)
|
||||
(map adapt-write-handler)))
|
||||
|
||||
(def ^:private
|
||||
xf:adapt-read-handler
|
||||
(comp
|
||||
(filter :rfn)
|
||||
(map adapt-read-handler)))
|
||||
|
||||
(def ^:dynamic *write-handler-lookup* nil)
|
||||
(def ^:dynamic *read-handler-lookup* nil)
|
||||
|
||||
@ -126,36 +156,39 @@
|
||||
|
||||
(defn add-handlers!
|
||||
[& handlers]
|
||||
(letfn [(adapt-write-handler [{:keys [name class wfn]}]
|
||||
[class {name (reify WriteHandler
|
||||
(write [_ w o]
|
||||
(wfn name w o)))}])
|
||||
(let [write-handlers'
|
||||
(into {} xf:adapt-write-handler handlers)
|
||||
|
||||
(adapt-read-handler [{:keys [name rfn]}]
|
||||
[name (reify ReadHandler
|
||||
(read [_ rdr _ _]
|
||||
(rfn rdr)))])
|
||||
read-handlers'
|
||||
(into {} xf:adapt-read-handler handlers)
|
||||
|
||||
(merge-and-clean [m1 m2]
|
||||
(-> (merge m1 m2)
|
||||
(d/without-nils)))]
|
||||
write-handlers'
|
||||
(swap! write-handlers merge-handlers write-handlers')
|
||||
|
||||
(let [whs (into {}
|
||||
(comp
|
||||
(filter :wfn)
|
||||
(map adapt-write-handler))
|
||||
handlers)
|
||||
rhs (into {}
|
||||
(comp
|
||||
(filter :rfn)
|
||||
(map adapt-read-handler))
|
||||
handlers)
|
||||
cwh (swap! write-handlers merge-and-clean whs)
|
||||
crh (swap! read-handlers merge-and-clean rhs)]
|
||||
read-handlers'
|
||||
(swap! read-handlers merge-handlers read-handlers')]
|
||||
|
||||
(alter-var-root #'*write-handler-lookup* (constantly (-> cwh fres/associative-lookup fres/inheritance-lookup)))
|
||||
(alter-var-root #'*read-handler-lookup* (constantly (-> crh fres/associative-lookup)))
|
||||
nil)))
|
||||
(alter-var-root #'*write-handler-lookup*
|
||||
(constantly
|
||||
(-> write-handlers' fres/associative-lookup fres/inheritance-lookup)))
|
||||
|
||||
(alter-var-root #'*read-handler-lookup*
|
||||
(constantly (-> read-handlers' fres/associative-lookup)))
|
||||
|
||||
nil))
|
||||
|
||||
(defn overwrite-read-handlers
|
||||
[& handlers]
|
||||
(->> (into {} xf:adapt-read-handler handlers)
|
||||
(merge-handlers @read-handlers)
|
||||
(fres/associative-lookup)))
|
||||
|
||||
(defn overwrite-write-handlers
|
||||
[& handlers]
|
||||
(->> (into {} xf:adapt-write-handler handlers)
|
||||
(merge-handlers @write-handlers)
|
||||
(fres/associative-lookup)
|
||||
(fres/inheritance-lookup)))
|
||||
|
||||
(defn write-char
|
||||
[n w o]
|
||||
|
||||
526
common/test/common_tests/fressian_test.clj
Normal file
526
common/test/common_tests/fressian_test.clj
Normal file
@ -0,0 +1,526 @@
|
||||
;; 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 INC
|
||||
|
||||
(ns common-tests.fressian-test
|
||||
"Exhaustive unit tests for app.common.fressian encode/decode functions.
|
||||
|
||||
Tests cover every custom handler registered in the fressian namespace
|
||||
(char, java/instant, clj/ratio, clj/map, linked/map, clj/keyword,
|
||||
clj/symbol, clj/bigint, clj/set, clj/vector, clj/list, clj/seq,
|
||||
linked/set) plus the built-in Fressian primitives (nil, boolean,
|
||||
integer, long, double, string, bytes, UUID).
|
||||
|
||||
The file is JVM-only because Fressian is a JVM library."
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.fressian :as fres]
|
||||
[clojure.test :as t])
|
||||
(:import
|
||||
java.time.Instant
|
||||
java.time.OffsetDateTime
|
||||
java.time.ZoneOffset))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Helpers
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defn roundtrip
|
||||
"Encode then decode a value; the result must equal the original."
|
||||
[v]
|
||||
(-> v fres/encode fres/decode))
|
||||
|
||||
(defn roundtrip=
|
||||
"Returns true when encode→decode produces an equal value."
|
||||
[v]
|
||||
(= v (roundtrip v)))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Encode returns a byte array
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(t/deftest encode-returns-byte-array
|
||||
(t/is (bytes? (fres/encode nil)))
|
||||
(t/is (bytes? (fres/encode 42)))
|
||||
(t/is (bytes? (fres/encode "hello")))
|
||||
(t/is (bytes? (fres/encode {:a 1})))
|
||||
(t/is (bytes? (fres/encode [])))
|
||||
(t/is (pos? (alength ^bytes (fres/encode 0))))
|
||||
(t/testing "different values produce different byte arrays"
|
||||
(t/is (not= (vec (fres/encode 1)) (vec (fres/encode 2))))))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; nil
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(t/deftest nil-roundtrip
|
||||
(t/is (nil? (roundtrip nil))))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Booleans
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(t/deftest boolean-roundtrip
|
||||
(t/is (true? (roundtrip true)))
|
||||
(t/is (false? (roundtrip false))))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Integers and longs
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(t/deftest integer-roundtrip
|
||||
(t/is (= 0 (roundtrip 0)))
|
||||
(t/is (= 1 (roundtrip 1)))
|
||||
(t/is (= -1 (roundtrip -1)))
|
||||
(t/is (= 42 (roundtrip 42)))
|
||||
(t/is (= Integer/MAX_VALUE (roundtrip Integer/MAX_VALUE)))
|
||||
(t/is (= Integer/MIN_VALUE (roundtrip Integer/MIN_VALUE))))
|
||||
|
||||
(t/deftest long-roundtrip
|
||||
(t/is (= Long/MAX_VALUE (roundtrip Long/MAX_VALUE)))
|
||||
(t/is (= Long/MIN_VALUE (roundtrip Long/MIN_VALUE)))
|
||||
(t/is (= 1000000000000 (roundtrip 1000000000000))))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Doubles / floats
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(t/deftest double-roundtrip
|
||||
(t/is (= 0.0 (roundtrip 0.0)))
|
||||
(t/is (= 3.14 (roundtrip 3.14)))
|
||||
(t/is (= -2.718 (roundtrip -2.718)))
|
||||
(t/is (= Double/MAX_VALUE (roundtrip Double/MAX_VALUE)))
|
||||
(t/is (= Double/MIN_VALUE (roundtrip Double/MIN_VALUE)))
|
||||
(t/is (Double/isInfinite ^double (roundtrip Double/POSITIVE_INFINITY)))
|
||||
(t/is (Double/isInfinite ^double (roundtrip Double/NEGATIVE_INFINITY)))
|
||||
(t/is (Double/isNaN ^double (roundtrip Double/NaN))))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Strings
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(t/deftest string-roundtrip
|
||||
(t/is (= "" (roundtrip "")))
|
||||
(t/is (= "hello" (roundtrip "hello")))
|
||||
(t/is (= "hello world" (roundtrip "hello world")))
|
||||
(t/is (= "αβγδ" (roundtrip "αβγδ")))
|
||||
(t/is (= "emoji: 🎨" (roundtrip "emoji: 🎨")))
|
||||
(t/is (= (apply str (repeat 10000 "x")) (roundtrip (apply str (repeat 10000 "x"))))))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Characters (custom "char" handler)
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(t/deftest char-roundtrip
|
||||
(t/is (= \a (roundtrip \a)))
|
||||
(t/is (= \A (roundtrip \A)))
|
||||
(t/is (= \space (roundtrip \space)))
|
||||
(t/is (= \newline (roundtrip \newline)))
|
||||
(t/is (= \0 (roundtrip \0)))
|
||||
(t/is (= \ü (roundtrip \ü)))
|
||||
(t/testing "char type is preserved"
|
||||
(t/is (char? (roundtrip \x)))))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Keywords (custom "clj/keyword" handler)
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(t/deftest keyword-roundtrip
|
||||
(t/is (= :foo (roundtrip :foo)))
|
||||
(t/is (= :bar (roundtrip :bar)))
|
||||
(t/is (= :ns/foo (roundtrip :ns/foo)))
|
||||
(t/is (= :app.common.data/something (roundtrip :app.common.data/something)))
|
||||
(t/testing "keyword? is preserved"
|
||||
(t/is (keyword? (roundtrip :anything))))
|
||||
(t/testing "namespace is preserved"
|
||||
(let [kw :my-ns/my-name]
|
||||
(t/is (= (namespace kw) (namespace (roundtrip kw))))
|
||||
(t/is (= (name kw) (name (roundtrip kw)))))))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Symbols (custom "clj/symbol" handler)
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(t/deftest symbol-roundtrip
|
||||
(t/is (= 'foo (roundtrip 'foo)))
|
||||
(t/is (= 'bar (roundtrip 'bar)))
|
||||
(t/is (= 'ns/foo (roundtrip 'ns/foo)))
|
||||
(t/is (= 'clojure.core/map (roundtrip 'clojure.core/map)))
|
||||
(t/testing "symbol? is preserved"
|
||||
(t/is (symbol? (roundtrip 'anything))))
|
||||
(t/testing "namespace is preserved"
|
||||
(let [sym 'my-ns/my-name]
|
||||
(t/is (= (namespace sym) (namespace (roundtrip sym))))
|
||||
(t/is (= (name sym) (name (roundtrip sym)))))))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Vectors (custom "clj/vector" handler)
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(t/deftest vector-roundtrip
|
||||
(t/is (= [] (roundtrip [])))
|
||||
(t/is (= [1 2 3] (roundtrip [1 2 3])))
|
||||
(t/is (= [:a :b :c] (roundtrip [:a :b :c])))
|
||||
(t/is (= [nil nil nil] (roundtrip [nil nil nil])))
|
||||
(t/is (= [[1 2] [3 4]] (roundtrip [[1 2] [3 4]])))
|
||||
(t/is (= ["hello" :world 42] (roundtrip ["hello" :world 42])))
|
||||
(t/testing "vector? is preserved"
|
||||
(t/is (vector? (roundtrip [1 2 3])))))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Sets (custom "clj/set" handler)
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(t/deftest set-roundtrip
|
||||
(t/is (= #{} (roundtrip #{})))
|
||||
(t/is (= #{1 2 3} (roundtrip #{1 2 3})))
|
||||
(t/is (= #{:a :b :c} (roundtrip #{:a :b :c})))
|
||||
(t/is (= #{"x" "y"} (roundtrip #{"x" "y"})))
|
||||
(t/testing "set? is preserved"
|
||||
(t/is (set? (roundtrip #{:foo})))))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Maps (custom "clj/map" handler)
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(t/deftest small-map-roundtrip
|
||||
"Maps with fewer than 8 entries decode as PersistentArrayMap."
|
||||
(t/is (= {} (roundtrip {})))
|
||||
(t/is (= {:a 1} (roundtrip {:a 1})))
|
||||
(t/is (= {:a 1 :b 2} (roundtrip {:a 1 :b 2})))
|
||||
(t/is (= {:a 1 :b 2 :c 3 :d 4 :e 5 :f 6 :g 7} (roundtrip {:a 1 :b 2 :c 3 :d 4 :e 5 :f 6 :g 7})))
|
||||
(t/testing "map? is preserved"
|
||||
(t/is (map? (roundtrip {:x 1})))))
|
||||
|
||||
(t/deftest large-map-roundtrip
|
||||
"Maps with 8+ entries decode as PersistentHashMap (>= 16 kvs in list)."
|
||||
(let [large (into {} (map (fn [i] [(keyword (str "k" i)) i]) (range 20)))]
|
||||
(t/is (= large (roundtrip large)))
|
||||
(t/is (map? (roundtrip large)))))
|
||||
|
||||
(t/deftest map-with-mixed-keys-roundtrip
|
||||
(let [m {:keyword-key 1
|
||||
"string-key" 2
|
||||
42 3}]
|
||||
(t/is (= m (roundtrip m)))))
|
||||
|
||||
(t/deftest map-with-nil-value-roundtrip
|
||||
(t/is (= {:a nil :b 2} (roundtrip {:a nil :b 2}))))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Sequences (custom "clj/seq" handler)
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(t/deftest seq-roundtrip
|
||||
(let [s (seq [1 2 3])]
|
||||
(t/is (= (sequence s) (roundtrip s))))
|
||||
(let [s (map inc [1 2 3])]
|
||||
(t/is (= (sequence s) (roundtrip s))))
|
||||
(t/testing "result is a sequence"
|
||||
(t/is (seq? (roundtrip (seq [1 2 3]))))))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Ratio (custom "clj/ratio" handler)
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(t/deftest ratio-roundtrip
|
||||
(t/is (= 1/3 (roundtrip 1/3)))
|
||||
(t/is (= 22/7 (roundtrip 22/7)))
|
||||
(t/is (= -5/6 (roundtrip -5/6)))
|
||||
(t/is (= 1/1000000 (roundtrip 1/1000000)))
|
||||
(t/testing "ratio? is preserved"
|
||||
(t/is (ratio? (roundtrip 1/3)))))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; BigInt (custom "clj/bigint" handler)
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(t/deftest bigint-roundtrip
|
||||
(t/is (= 0N (roundtrip 0N)))
|
||||
(t/is (= 1N (roundtrip 1N)))
|
||||
(t/is (= -1N (roundtrip -1N)))
|
||||
(t/is (= 123456789012345678901234567890N (roundtrip 123456789012345678901234567890N)))
|
||||
(t/is (= -999999999999999999999999999999N (roundtrip -999999999999999999999999999999N)))
|
||||
(t/testing "bigint? is preserved"
|
||||
(t/is (instance? clojure.lang.BigInt (roundtrip 42N)))))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; java.time.Instant (custom "java/instant" handler)
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(t/deftest instant-roundtrip
|
||||
(let [now (Instant/now)]
|
||||
(t/is (= (.toEpochMilli now) (.toEpochMilli ^Instant (roundtrip now)))))
|
||||
(t/testing "epoch zero"
|
||||
(let [epoch (Instant/ofEpochMilli 0)]
|
||||
(t/is (= epoch (roundtrip epoch)))))
|
||||
(t/testing "far past"
|
||||
(let [past (Instant/ofEpochMilli -62135596800000)]
|
||||
(t/is (= past (roundtrip past)))))
|
||||
(t/testing "far future"
|
||||
(let [future (Instant/ofEpochMilli 32503680000000)]
|
||||
(t/is (= future (roundtrip future)))))
|
||||
(t/testing "result type is Instant"
|
||||
(t/is (instance? Instant (roundtrip (Instant/now))))))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; java.time.OffsetDateTime (written as "java/instant", read back as Instant)
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(t/deftest offset-date-time-roundtrip
|
||||
(t/testing "OffsetDateTime is written and decoded as Instant (millis preserved)"
|
||||
(let [odt (OffsetDateTime/now ZoneOffset/UTC)
|
||||
millis (.toEpochMilli (.toInstant odt))
|
||||
result (roundtrip odt)]
|
||||
(t/is (instance? Instant result))
|
||||
(t/is (= millis (.toEpochMilli ^Instant result)))))
|
||||
(t/testing "non-UTC offset"
|
||||
(let [odt (OffsetDateTime/now (ZoneOffset/ofHours 5))
|
||||
millis (.toEpochMilli (.toInstant odt))
|
||||
result (roundtrip odt)]
|
||||
(t/is (= millis (.toEpochMilli ^Instant result))))))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Ordered map (custom "linked/map" handler)
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(t/deftest ordered-map-roundtrip
|
||||
(t/is (= (d/ordered-map) (roundtrip (d/ordered-map))))
|
||||
(t/is (= (d/ordered-map :a 1) (roundtrip (d/ordered-map :a 1))))
|
||||
(t/is (= (d/ordered-map :a 1 :b 2 :c 3) (roundtrip (d/ordered-map :a 1 :b 2 :c 3))))
|
||||
(t/testing "ordered-map? is preserved"
|
||||
(t/is (d/ordered-map? (roundtrip (d/ordered-map :x 1 :y 2)))))
|
||||
(t/testing "insertion order is preserved"
|
||||
(let [om (d/ordered-map :c 3 :a 1 :b 2)
|
||||
rt (roundtrip om)]
|
||||
(t/is (= [:c :a :b] (vec (keys rt))))))
|
||||
(t/testing "large ordered-map"
|
||||
(let [om (reduce (fn [m i] (assoc m (keyword (str "k" i)) i))
|
||||
(d/ordered-map)
|
||||
(range 20))
|
||||
rt (roundtrip om)]
|
||||
(t/is (d/ordered-map? rt))
|
||||
(t/is (= om rt))
|
||||
(t/is (= (keys om) (keys rt))))))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Ordered set (custom "linked/set" handler)
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(t/deftest ordered-set-roundtrip
|
||||
(t/is (= (d/ordered-set) (roundtrip (d/ordered-set))))
|
||||
(t/is (= (d/ordered-set :a) (roundtrip (d/ordered-set :a))))
|
||||
(t/is (= (d/ordered-set :a :b :c) (roundtrip (d/ordered-set :a :b :c))))
|
||||
(t/testing "ordered-set? is preserved"
|
||||
(t/is (d/ordered-set? (roundtrip (d/ordered-set :x :y)))))
|
||||
(t/testing "insertion order is preserved"
|
||||
(let [os (d/ordered-set :c :a :b)
|
||||
rt (roundtrip os)]
|
||||
(t/is (= [:c :a :b] (vec rt)))))
|
||||
(t/testing "large ordered-set"
|
||||
(let [os (reduce conj (d/ordered-set) (range 20))
|
||||
rt (roundtrip os)]
|
||||
(t/is (d/ordered-set? rt))
|
||||
(t/is (= os rt)))))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; UUID (handled by built-in Fressian handlers)
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(t/deftest uuid-roundtrip
|
||||
(let [id (java.util.UUID/randomUUID)]
|
||||
(t/is (= id (roundtrip id))))
|
||||
(t/testing "nil UUID"
|
||||
(let [nil-uuid (java.util.UUID/fromString "00000000-0000-0000-0000-000000000000")]
|
||||
(t/is (= nil-uuid (roundtrip nil-uuid)))))
|
||||
(t/testing "max UUID"
|
||||
(let [max-uuid (java.util.UUID/fromString "ffffffff-ffff-ffff-ffff-ffffffffffff")]
|
||||
(t/is (= max-uuid (roundtrip max-uuid)))))
|
||||
(t/testing "specific well-known UUID"
|
||||
(let [id (java.util.UUID/fromString "550e8400-e29b-41d4-a716-446655440000")]
|
||||
(t/is (= id (roundtrip id)))))
|
||||
(t/testing "uuid? is preserved"
|
||||
(t/is (uuid? (roundtrip (java.util.UUID/randomUUID))))))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Nested and mixed structures
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(t/deftest nested-map-roundtrip
|
||||
(let [nested {:a {:b {:c 42 :d [1 2 3]} :e :keyword} :f "string"}]
|
||||
(t/is (= nested (roundtrip nested)))))
|
||||
|
||||
(t/deftest map-with-vector-values
|
||||
(let [m {:shapes [1 2 3] :colors [:red :green :blue]}]
|
||||
(t/is (= m (roundtrip m)))))
|
||||
|
||||
(t/deftest vector-of-maps
|
||||
(let [v [{:id 1 :name "a"} {:id 2 :name "b"} {:id 3 :name "c"}]]
|
||||
(t/is (= v (roundtrip v)))))
|
||||
|
||||
(t/deftest mixed-collection-types
|
||||
(let [data {:vec [1 2 3]
|
||||
:set #{:a :b :c}
|
||||
:map {:nested true}
|
||||
:kw :some/keyword
|
||||
:sym 'some/symbol
|
||||
:bigint 12345678901234567890N
|
||||
:ratio 22/7
|
||||
:str "hello"
|
||||
:num 42
|
||||
:bool true
|
||||
:nil-val nil}]
|
||||
(t/is (= data (roundtrip data)))))
|
||||
|
||||
(t/deftest deeply-nested-structure
|
||||
(let [data (reduce (fn [acc i] {:level i :child acc})
|
||||
{:leaf true}
|
||||
(range 20))]
|
||||
(t/is (= data (roundtrip data)))))
|
||||
|
||||
(t/deftest penpot-like-shape-map
|
||||
"Simulates a Penpot shape-like structure with UUIDs, keywords, and nested maps."
|
||||
(let [id (java.util.UUID/fromString "550e8400-e29b-41d4-a716-446655440001")
|
||||
frame-id (java.util.UUID/fromString "550e8400-e29b-41d4-a716-446655440002")
|
||||
shape {:id id
|
||||
:frame-id frame-id
|
||||
:type :rect
|
||||
:name "My Shape"
|
||||
:x 100.5
|
||||
:y 200.0
|
||||
:width 300.0
|
||||
:height 150.0
|
||||
:fills [{:fill-color "#FF0000" :fill-opacity 1.0}]
|
||||
:strokes []
|
||||
:hidden false
|
||||
:blocked false}]
|
||||
(t/is (= shape (roundtrip shape)))))
|
||||
|
||||
(t/deftest penpot-like-objects-map
|
||||
"Simulates a Penpot page objects map with multiple shapes."
|
||||
(let [ids (mapv #(java.util.UUID/fromString
|
||||
(format "550e8400-e29b-41d4-a716-%012d" %))
|
||||
(range 5))
|
||||
objs (into {} (map (fn [id] [id {:id id :type :rect :name (str id)}]) ids))
|
||||
data {:objects objs}]
|
||||
(t/is (= data (roundtrip data)))))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Idempotency: encode→decode→encode must yield equal bytes
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(t/deftest encode-idempotency
|
||||
(doseq [v [nil true false 0 1 -1 42 Long/MAX_VALUE 3.14 "" "hello"
|
||||
:kw :ns/kw 'sym 'ns/sym
|
||||
[] [1 2 3] #{} #{:a} {} {:a 1}
|
||||
1/3 42N]]
|
||||
(let [enc1 (fres/encode v)
|
||||
enc2 (-> v fres/encode fres/decode fres/encode)]
|
||||
(t/is (= (vec enc1) (vec enc2))
|
||||
(str "Idempotency failed for: " (pr-str v))))))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Multiple encode/decode roundtrips in sequence (regression / ordering)
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(t/deftest multiple-roundtrips-are-independent
|
||||
(t/testing "encoding multiple values independently does not cross-contaminate"
|
||||
(let [a (fres/encode {:key :val-a})
|
||||
b (fres/encode {:key :val-b})
|
||||
da (fres/decode a)
|
||||
db (fres/decode b)]
|
||||
(t/is (= {:key :val-a} da))
|
||||
(t/is (= {:key :val-b} db))
|
||||
(t/is (not= da db)))))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Edge cases: empty collections
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(t/deftest empty-collections-roundtrip
|
||||
(t/is (= {} (roundtrip {})))
|
||||
(t/is (= [] (roundtrip [])))
|
||||
(t/is (= #{} (roundtrip #{})))
|
||||
(t/is (= "" (roundtrip "")))
|
||||
(t/is (= (d/ordered-map) (roundtrip (d/ordered-map))))
|
||||
(t/is (= (d/ordered-set) (roundtrip (d/ordered-set)))))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Edge cases: collections containing nil
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(t/deftest collections-with-nil-roundtrip
|
||||
(t/is (= [nil] (roundtrip [nil])))
|
||||
(t/is (= [nil nil nil] (roundtrip [nil nil nil])))
|
||||
(t/is (= {:a nil :b nil} (roundtrip {:a nil :b nil})))
|
||||
(t/is (= [1 nil 3] (roundtrip [1 nil 3]))))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Edge cases: single-element collections
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(t/deftest single-element-collections
|
||||
(t/is (= [42] (roundtrip [42])))
|
||||
(t/is (= #{:only} (roundtrip #{:only})))
|
||||
(t/is (= {:only-key "only-val"} (roundtrip {:only-key "only-val"}))))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Edge cases: boundary map sizes (ArrayMap/HashMap threshold)
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(t/deftest map-size-boundary
|
||||
(t/testing "7-entry map (below threshold → ArrayMap)"
|
||||
(let [m (into {} (map (fn [i] [(keyword (str "k" i)) i]) (range 7)))]
|
||||
(t/is (= m (roundtrip m)))))
|
||||
(t/testing "8-entry map (at/above threshold → may become HashMap)"
|
||||
(let [m (into {} (map (fn [i] [(keyword (str "k" i)) i]) (range 8)))]
|
||||
(t/is (= m (roundtrip m)))))
|
||||
(t/testing "16-entry map (well above threshold)"
|
||||
(let [m (into {} (map (fn [i] [(keyword (str "k" i)) i]) (range 16)))]
|
||||
(t/is (= m (roundtrip m))))))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Edge cases: byte arrays
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(t/deftest byte-array-roundtrip
|
||||
(let [data (byte-array [0 1 2 3 127 -128 -1])]
|
||||
(t/is (= (vec data) (vec ^bytes (roundtrip data))))))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Ordered-map key ordering survives large number of keys
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(t/deftest ordered-map-key-ordering-stress
|
||||
(let [keys-in-order (mapv #(keyword (str "key-" (format "%03d" %))) (range 50))
|
||||
om (reduce (fn [m k] (assoc m k (name k))) (d/ordered-map) keys-in-order)
|
||||
rt (roundtrip om)]
|
||||
(t/is (= keys-in-order (vec (keys rt))))))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Ordered-set element ordering survives large number of elements
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(t/deftest ordered-set-element-ordering-stress
|
||||
(let [elems-in-order (mapv #(keyword (str "elem-" (format "%03d" %))) (range 50))
|
||||
os (reduce conj (d/ordered-set) elems-in-order)
|
||||
rt (roundtrip os)]
|
||||
(t/is (= elems-in-order (vec rt)))))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Complex Penpot-domain: ordered-map with UUID keys and shape values
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(t/deftest ordered-map-with-uuid-keys
|
||||
(let [ids (mapv #(java.util.UUID/fromString
|
||||
(format "550e8400-e29b-41d4-a716-%012d" %))
|
||||
(range 5))
|
||||
om (reduce (fn [m id] (assoc m id {:type :rect :id id}))
|
||||
(d/ordered-map)
|
||||
ids)
|
||||
rt (roundtrip om)]
|
||||
(t/is (d/ordered-map? rt))
|
||||
(t/is (= om rt))
|
||||
(t/is (= (keys om) (keys rt)))))
|
||||
@ -6,6 +6,7 @@
|
||||
|
||||
(ns common-tests.runner
|
||||
(:require
|
||||
#?(:clj [common-tests.fressian-test])
|
||||
[clojure.test :as t]
|
||||
[common-tests.buffer-test]
|
||||
[common-tests.colors-test]
|
||||
@ -81,6 +82,7 @@
|
||||
'common-tests.buffer-test
|
||||
'common-tests.colors-test
|
||||
'common-tests.data-test
|
||||
#?(:clj 'common-tests.fressian-test)
|
||||
'common-tests.files-changes-test
|
||||
'common-tests.files-migrations-test
|
||||
'common-tests.geom-align-test
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user