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:
Andrey Antukh 2026-04-14 10:48:58 +02:00 committed by GitHub
parent bc47b992eb
commit a81cded0aa
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 587 additions and 26 deletions

View File

@ -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]

View 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)))))

View File

@ -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