Compare commits

..
3 Commits
63 changed files with 571 additions and 549 deletions

No files matched your search

+2 -8
View File
@@ -647,16 +647,10 @@
data
library-ids)))
(def ^:const import-transaction-timeout-ms
"Ceiling for binfile import transactions (20 minutes). Interpolated
directly into SQL: compile-time constant, never user input."
(* 20 60 1000))
(defn configure-database-timeouts!
(defn disable-database-timeouts!
[cfg]
(let [conn (db/get-connection cfg)]
(db/exec-one! conn [(str "SET LOCAL idle_in_transaction_session_timeout = "
import-transaction-timeout-ms)])
(db/exec-one! conn ["SET LOCAL idle_in_transaction_session_timeout = 0"])
(db/exec-one! conn ["SET CONSTRAINTS ALL DEFERRED"])))
(defn process-file
+1 -1
View File
@@ -454,7 +454,7 @@
(defn- read-import-v1
[{:keys [::db/conn ::bfc/project-id ::bfc/profile-id ::bfc/input] :as cfg}]
(bfc/configure-database-timeouts! cfg)
(bfc/disable-database-timeouts! cfg)
(pu/with-open [input (zstd-input-stream input)
input (io/data-input-stream input)]
+96 -92
View File
@@ -529,85 +529,89 @@
(let [manifest (json/read reader :key-fn json/read-kebab-key)]
(decode-manifest manifest)))))
(def ^:private file-object-entry-categories
"Zip path segments that hold per-file object entries on the
`files/<file-id>/<category>/<object-id>.json` shape."
#{"media" "colors" "components" "typographies"})
(defn- match-media-entry-fn
[file-id]
(let [pattern (str "^files/" file-id "/media/([^/]+).json$")
pattern (re-pattern pattern)]
(fn [entry]
(when-let [[_ id] (re-matches pattern (zip-entry-name entry))]
{:entry entry
:id (parse-uuid id)}))))
(defn- index-entry-name
"Classify a single zip entry by the raw shape of its path and
accumulate it on the index.
(defn- match-color-entry-fn
[file-id]
(let [pattern (str "^files/" file-id "/colors/([^/]+).json$")
pattern (re-pattern pattern)]
(fn [entry]
(when-let [[_ id] (re-matches pattern (zip-entry-name entry))]
{:entry entry
:id (parse-uuid id)}))))
It replaces the per-file regex matchers with a single
classification pass over all entries. The `.json` suffix is matched
literally: the previous regexes left the dot unescaped, so crafted
paths like `files/<file-id>/tokensXjson` or `objects/x-json` matched
by accident; requiring the literal suffix ignores them (legitimate
exports always write a literal `.json` suffix). Unknown paths are
ignored."
[index ^String name entry]
(if-not (and name (str/ends-with? name ".json"))
index
(let [base (subs name 0 (- (count name) 5))
segs (str/split base "/")
seg-n (count segs)
seg-1 (nth segs 0 nil)
seg-2 (nth segs 1 nil)
seg-3 (nth segs 2 nil)]
(defn- match-component-entry-fn
[file-id]
(let [pattern (str "^files/" file-id "/components/([^/]+).json$")
pattern (re-pattern pattern)]
(fn [entry]
(when-let [[_ id] (re-matches pattern (zip-entry-name entry))]
{:entry entry
:id (parse-uuid id)}))))
(if-not (and (pos? seg-n) (every? #(pos? (count %)) segs))
index
(cond
;; objects/<object-id>.json
(and (= seg-n 2) (= seg-1 "objects"))
(update index :objects bfc/conj-vec
{:entry entry :id (parse-uuid seg-2)})
(defn- match-typography-entry-fn
[file-id]
(let [pattern (str "^files/" file-id "/typographies/([^/]+).json$")
pattern (re-pattern pattern)]
(fn [entry]
(when-let [[_ id] (re-matches pattern (zip-entry-name entry))]
{:entry entry
:id (parse-uuid id)}))))
;; files/<file-id>/tokens.json
(and (= seg-n 3) (= seg-1 "files") (= seg-3 "tokens"))
(update-in index [:tokens seg-2] bfc/conj-vec {:entry entry})
(defn- match-tokens-lib-entry-fn
[file-id]
(let [pattern (str "^files/" file-id "/tokens.json$")
pattern (re-pattern pattern)]
(fn [entry]
(when-let [[_] (re-matches pattern (zip-entry-name entry))]
{:entry entry}))))
;; files/<file-id>/thumbnails/<tag>/<page-id>/<frame-id>.json
(and (= seg-n 6) (= seg-1 "files") (= seg-3 "thumbnails"))
(update-in index [:thumbnails seg-2] bfc/conj-vec
{:entry entry
:tag (nth segs 3)
:page-id (parse-uuid (nth segs 4))
:frame-id (parse-uuid (nth segs 5))
:file-id seg-2})
(defn- match-thumbnail-entry-fn
[file-id]
(let [pattern (str "^files/" file-id "/thumbnails/([^/]+)/([^/]+)/([^/]+).json$")
pattern (re-pattern pattern)]
(fn [entry]
(when-let [[_ tag page-id frame-id] (re-matches pattern (zip-entry-name entry))]
{:entry entry
:tag tag
:page-id (parse-uuid page-id)
:frame-id (parse-uuid frame-id)
:file-id file-id}))))
;; files/<file-id>/pages/<page-id>.json
(and (= seg-n 4) (= seg-1 "files") (= seg-3 "pages"))
(update-in index [:pages seg-2] bfc/conj-vec
{:entry entry :id (parse-uuid (nth segs 3))})
(defn- match-page-entry-fn
[file-id]
(let [pattern (str "^files/" file-id "/pages/([^/]+).json$")
pattern (re-pattern pattern)]
(fn [entry]
(when-let [[_ id] (re-matches pattern (zip-entry-name entry))]
{:entry entry
:id (parse-uuid id)}))))
;; files/<file-id>/pages/<page-id>/<shape-id>.json
(and (= seg-n 5) (= seg-1 "files") (= seg-3 "pages"))
(update-in index [:shapes seg-2 (nth segs 3)] bfc/conj-vec
{:entry entry
:page-id (nth segs 3)
:id (parse-uuid (nth segs 4))})
(defn- match-shape-entry-fn
[file-id page-id]
(let [pattern (str "^files/" file-id "/pages/" page-id "/([^/]+).json$")
pattern (re-pattern pattern)]
(fn [entry]
(when-let [[_ id] (re-matches pattern (zip-entry-name entry))]
{:entry entry
:page-id page-id
:id (parse-uuid id)}))))
;; files/<file-id>/<category>/<object-id>.json
(and (= seg-n 4)
(= seg-1 "files")
(contains? file-object-entry-categories seg-3))
(update-in index [(keyword seg-3) seg-2] bfc/conj-vec
{:entry entry :id (parse-uuid (nth segs 3))})
:else
index)))))
(defn- index-entries
"Classify all the provided zip entries in a single pass and group
them by their path shape, so import consumers can lookup their
entries per file (and per page) instead of rescanning the whole
entry collection for every file and page."
[entries]
(reduce (fn [index entry]
(index-entry-name index (zip-entry-name entry) entry))
{}
entries))
(defn- match-storage-entry-fn
[]
(let [pattern "^objects/([^/]+).json$"
pattern (re-pattern pattern)]
(fn [entry]
(when-let [[_ id] (re-matches pattern (zip-entry-name entry))]
{:entry entry
:id (parse-uuid id)}))))
(defn- read-entry
[^ZipFile input entry]
@@ -640,8 +644,8 @@
(validate-plugin-data))))
(defn- read-file-media
[{:keys [::bfc/input ::entries-index]} file-id]
(->> (get-in entries-index [:media (str file-id)])
[{:keys [::bfc/input ::entries]} file-id]
(->> (keep (match-media-entry-fn file-id) entries)
(reduce (fn [result {:keys [id entry]}]
(let [object (->> (read-entry input entry)
(decode-media)
@@ -660,8 +664,8 @@
(not-empty)))
(defn- read-file-colors
[{:keys [::bfc/input ::entries-index]} file-id]
(->> (get-in entries-index [:colors (str file-id)])
[{:keys [::bfc/input ::entries]} file-id]
(->> (keep (match-color-entry-fn file-id) entries)
(reduce (fn [result {:keys [id entry]}]
(let [object (->> (read-entry input entry)
(decode-color)
@@ -674,7 +678,7 @@
(not-empty)))
(defn- read-file-components
[{:keys [::bfc/input ::entries-index]} file-id]
[{:keys [::bfc/input ::entries]} file-id]
(let [clean-component-post-decode
(fn [component]
(d/update-when component :objects
@@ -692,7 +696,7 @@
objects
objects))))]
(->> (get-in entries-index [:components (str file-id)])
(->> (keep (match-component-entry-fn file-id) entries)
(reduce (fn [result {:keys [id entry]}]
(let [object (->> (read-entry input entry)
(clean-component-pre-decode)
@@ -706,8 +710,8 @@
(not-empty))))
(defn- read-file-typographies
[{:keys [::bfc/input ::entries-index]} file-id]
(->> (get-in entries-index [:typographies (str file-id)])
[{:keys [::bfc/input ::entries]} file-id]
(->> (keep (match-typography-entry-fn file-id) entries)
(reduce (fn [result {:keys [id entry]}]
(let [object (->> (read-entry input entry)
(decode-typography)
@@ -720,16 +724,16 @@
(not-empty)))
(defn- read-file-tokens-lib
[{:keys [::bfc/input ::entries-index]} file-id]
(when-let [{:keys [entry]} (first (get-in entries-index [:tokens (str file-id)]))]
[{:keys [::bfc/input ::entries]} file-id]
(when-let [entry (d/seek (match-tokens-lib-entry-fn file-id) entries)]
(events/tap :progress {:section :tokens-lib :file-id file-id})
(->> (read-plain-entry input entry)
(decode-tokens-lib)
(validate-tokens-lib))))
(defn- read-file-shapes
[{:keys [::bfc/input ::entries-index] :as cfg} file-id page-id]
(->> (get-in entries-index [:shapes (str file-id) (str page-id)])
[{:keys [::bfc/input ::entries] :as cfg} file-id page-id]
(->> (keep (match-shape-entry-fn file-id page-id) entries)
(reduce (fn [result {:keys [id entry]}]
(let [object (->> (read-entry input entry)
(bfl/clean-shape-pre-decode)
@@ -742,8 +746,8 @@
(not-empty)))
(defn- read-file-pages
[{:keys [::bfc/input ::entries-index] :as cfg} file-id]
(->> (get-in entries-index [:pages (str file-id)])
[{:keys [::bfc/input ::entries] :as cfg} file-id]
(->> (keep (match-page-entry-fn file-id) entries)
(keep (fn [{:keys [id entry]}]
(let [page (->> (read-entry input entry)
(decode-page))
@@ -758,8 +762,8 @@
(d/ordered-map))))
(defn- read-file-thumbnails
[{:keys [::bfc/input ::entries-index] :as cfg} file-id]
(->> (get-in entries-index [:thumbnails (str file-id)])
[{:keys [::bfc/input ::entries] :as cfg} file-id]
(->> (keep (match-thumbnail-entry-fn file-id) entries)
(reduce (fn [result {:keys [page-id frame-id tag entry]}]
(let [object (->> (read-entry input entry)
(decode-file-thumbnail)
@@ -888,7 +892,7 @@
(bfc/upsert-file-library-sync! conn (assoc rel-params :synced-at timestamp)))))))
(defn- import-storage-objects
[{:keys [::bfc/input ::entries-index ::bfc/timestamp] :as cfg}]
[{:keys [::bfc/input ::entries ::bfc/timestamp] :as cfg}]
(events/tap :progress {:section :storage-objects})
;; IMPORTANT: we strongly do not reuse the main connection that can
@@ -899,7 +903,7 @@
;; what the storage subsystem registers in other parallel
;; transaction
(let [storage (sto/resolve cfg)
entries (:objects entries-index)]
entries (keep (match-storage-entry-fn) entries)]
(doseq [{:keys [id entry]} entries]
(let [object (-> (read-entry input entry)
@@ -1054,7 +1058,7 @@
(defn- import-files*
[{:keys [::manifest] :as cfg}]
(bfc/configure-database-timeouts! cfg)
(bfc/disable-database-timeouts! cfg)
(vswap! bfc/*state* update :index bfc/update-index (:files manifest) :id)
@@ -1122,7 +1126,7 @@
:hint "unable to perform in-place update with binfile containing more than 1 file"
:manifest manifest))
(bfc/configure-database-timeouts! cfg)
(bfc/disable-database-timeouts! cfg)
(let [ref-file (bfc/get-minimal-file cfg file-id ::db/for-update true)
file (first (get manifest :files))
@@ -1161,7 +1165,7 @@
:found (count entries))))
cfg (-> cfg
(assoc ::entries-index (index-entries entries))
(assoc ::entries entries)
(assoc ::manifest manifest)
(assoc ::bfc/timestamp timestamp))]
+1 -1
View File
@@ -2,7 +2,7 @@
;; 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 Sucursal en España SL
;; Copyright (c) KALEIDOS SUBSIDIARY SL
(ns app.graph.arrow
"Bulk Ladybug ingest through in-memory Arrow.
+1 -1
View File
@@ -2,7 +2,7 @@
;; 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 Sucursal en España SL
;; Copyright (c) KALEIDOS SUBSIDIARY SL
(ns app.graph.debug
"In-memory Ladybug sessions for the debug graph console."
+1 -1
View File
@@ -2,7 +2,7 @@
;; 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 Sucursal en España SL
;; Copyright (c) KALEIDOS SUBSIDIARY SL
(ns app.graph.ingest
"Penpot file -> Ladybug graph projection."
+1 -1
View File
@@ -2,7 +2,7 @@
;; 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 Sucursal en España SL
;; Copyright (c) KALEIDOS SUBSIDIARY SL
(ns app.graph.ladybug
"Ladybug access layer for graph-backed Penpot.
+1 -1
View File
@@ -2,7 +2,7 @@
;; 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 Sucursal en España SL
;; Copyright (c) KALEIDOS SUBSIDIARY SL
(ns app.graph.meta
"`GraphMeta`: the graph's own account of who built it and from what.
@@ -2,7 +2,7 @@
;; 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 Sucursal en España SL
;; Copyright (c) KALEIDOS SUBSIDIARY SL
(ns app.graph.projection.document
"Project a Penpot file-data map into Ladybug nodes and structural edges.
@@ -2,7 +2,7 @@
;; 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 Sucursal en España SL
;; Copyright (c) KALEIDOS SUBSIDIARY SL
(ns app.graph.projection.transforms
"Derived graph links: edges a reader could compute from the projected
+1 -1
View File
@@ -2,7 +2,7 @@
;; 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 Sucursal en España SL
;; Copyright (c) KALEIDOS SUBSIDIARY SL
(ns app.graph.report
(:require
+1 -1
View File
@@ -2,7 +2,7 @@
;; 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 Sucursal en España SL
;; Copyright (c) KALEIDOS SUBSIDIARY SL
(ns app.graph.schema
"Ladybug DDL facade for the graph-backed Penpot vertical slice.
+1 -1
View File
@@ -2,7 +2,7 @@
;; 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 Sucursal en España SL
;; Copyright (c) KALEIDOS SUBSIDIARY SL
(ns app.graph.schema.contract
"Deliberate choices in Penpot's graph schema, recorded as data.
+1 -1
View File
@@ -2,7 +2,7 @@
;; 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 Sucursal en España SL
;; Copyright (c) KALEIDOS SUBSIDIARY SL
(ns app.graph.schema.nodes
"Single source of truth for graph node tables.
+1 -1
View File
@@ -2,7 +2,7 @@
;; 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 Sucursal en España SL
;; Copyright (c) KALEIDOS SUBSIDIARY SL
(ns app.graph.schema.projection
"Derive Ladybug node column schemas from Penpot Malli sources.
+1 -1
View File
@@ -2,7 +2,7 @@
;; 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 Sucursal en España SL
;; Copyright (c) KALEIDOS SUBSIDIARY SL
(ns app.graph.schema.types
"Map Malli schemas to Ladybug column types.
+1 -1
View File
@@ -2,7 +2,7 @@
;; 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 Sucursal en España SL
;; Copyright (c) KALEIDOS SUBSIDIARY SL
(ns app.graph.schema.values
"Shape a Penpot value into the plain data its Ladybug column type wants.
+1 -1
View File
@@ -2,7 +2,7 @@
;; 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 Sucursal en España SL
;; Copyright (c) KALEIDOS SUBSIDIARY SL
(ns app.graph.stats
(:require
+1 -1
View File
@@ -2,7 +2,7 @@
;; 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 Sucursal en España SL
;; Copyright (c) KALEIDOS SUBSIDIARY SL
(ns app.graph.sync
"Incremental Ladybug graph updates from Penpot file-change events."
+1 -1
View File
@@ -2,7 +2,7 @@
;; 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 Sucursal en España SL
;; Copyright (c) KALEIDOS SUBSIDIARY SL
(ns app.rpc.commands.plugins
(:require
+1 -1
View File
@@ -2,7 +2,7 @@
;; 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 Sucursal en España SL
;; Copyright (c) KALEIDOS SUBSIDIARY SL
(ns app.storage.pending-gc
"A maintenance task that reclaims storage objects created in 'pending'
+1 -1
View File
@@ -2,7 +2,7 @@
;; 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 Sucursal en España SL
;; Copyright (c) KALEIDOS SUBSIDIARY SL
(ns app.tasks.demo-purge
"Task handler for delayed demo profile deletion. Submitted at demo
+87 -338
View File
@@ -17,7 +17,6 @@
[app.common.thumbnails :as thc]
[app.common.time :as ct]
[app.common.types.shape :as cts]
[app.common.types.tokens-lib :as ctob]
[app.common.uuid :as uuid]
[app.config :as cf]
[app.db :as db]
@@ -56,47 +55,46 @@
(:result out)))
(defn- prepare-simple-file
([profile] (prepare-simple-file profile 1))
([profile idx]
(let [page-id-1 (uuid/custom 1 1)
page-id-2 (uuid/custom 1 2)
shape-id (uuid/custom 2 1)
file (th/create-file* idx {:profile-id (:id profile)
:project-id (:default-project-id profile)
:is-shared false})]
(update-file!
:file-id (:id file)
:profile-id (:id profile)
:revn 0
:vern 0
:changes
[{:type :add-page
:name "test 1"
:id page-id-1}
{:type :add-page
:name "test 2"
:id page-id-2}])
[profile]
(let [page-id-1 (uuid/custom 1 1)
page-id-2 (uuid/custom 1 2)
shape-id (uuid/custom 2 1)
file (th/create-file* 1 {:profile-id (:id profile)
:project-id (:default-project-id profile)
:is-shared false})]
(update-file!
:file-id (:id file)
:profile-id (:id profile)
:revn 0
:vern 0
:changes
[{:type :add-page
:name "test 1"
:id page-id-1}
{:type :add-page
:name "test 2"
:id page-id-2}])
(update-file!
:file-id (:id file)
:profile-id (:id profile)
:revn 0
:vern 0
:changes
[{:type :add-obj
:page-id page-id-1
:id shape-id
:parent-id uuid/zero
:frame-id uuid/zero
:components-v2 true
:obj (cts/setup-shape
{:id shape-id
:name "image"
:frame-id uuid/zero
:parent-id uuid/zero
:type :rect})}])
(update-file!
:file-id (:id file)
:profile-id (:id profile)
:revn 0
:vern 0
:changes
[{:type :add-obj
:page-id page-id-1
:id shape-id
:parent-id uuid/zero
:frame-id uuid/zero
:components-v2 true
:obj (cts/setup-shape
{:id shape-id
:name "image"
:frame-id uuid/zero
:parent-id uuid/zero
:type :rect})}])
(dissoc file :data))))
(dissoc file :data)))
(def ^:private svg-raw-page-id (uuid/custom 1 1))
(def ^:private svg-raw-root-id (uuid/custom 3 1))
@@ -105,59 +103,58 @@
(defn- prepare-svg-raw-file
"A file containing an svg-raw subtree (an svg-raw parent with an
svg-raw child), which is what importing an SVG produces."
([profile] (prepare-svg-raw-file profile 1))
([profile idx]
(let [page-id svg-raw-page-id
root-id svg-raw-root-id
child-id svg-raw-child-id
[profile]
(let [page-id svg-raw-page-id
root-id svg-raw-root-id
child-id svg-raw-child-id
file (th/create-file* idx {:profile-id (:id profile)
:project-id (:default-project-id profile)
:is-shared false})]
(update-file!
:file-id (:id file)
:profile-id (:id profile)
:revn 0
:vern 0
:changes
[{:type :add-page
:name "page 1"
:id page-id}])
file (th/create-file* 1 {:profile-id (:id profile)
:project-id (:default-project-id profile)
:is-shared false})]
(update-file!
:file-id (:id file)
:profile-id (:id profile)
:revn 0
:vern 0
:changes
[{:type :add-page
:name "page 1"
:id page-id}])
(update-file!
:file-id (:id file)
:profile-id (:id profile)
:revn 0
:vern 0
:changes
[{:type :add-obj
:page-id page-id
:id root-id
:parent-id uuid/zero
:frame-id uuid/zero
:components-v2 true
:obj (cts/setup-shape
{:id root-id
:name "svg-root"
:frame-id uuid/zero
:parent-id uuid/zero
:type :svg-raw
:content {:tag :svg :attrs {} :content []}})}
{:type :add-obj
:page-id page-id
:id child-id
:parent-id root-id
:frame-id uuid/zero
:components-v2 true
:obj (cts/setup-shape
{:id child-id
:name "svg-text"
:frame-id uuid/zero
:parent-id root-id
:type :svg-raw
:content {:tag :text :attrs {} :content []}})}])
(update-file!
:file-id (:id file)
:profile-id (:id profile)
:revn 0
:vern 0
:changes
[{:type :add-obj
:page-id page-id
:id root-id
:parent-id uuid/zero
:frame-id uuid/zero
:components-v2 true
:obj (cts/setup-shape
{:id root-id
:name "svg-root"
:frame-id uuid/zero
:parent-id uuid/zero
:type :svg-raw
:content {:tag :svg :attrs {} :content []}})}
{:type :add-obj
:page-id page-id
:id child-id
:parent-id root-id
:frame-id uuid/zero
:components-v2 true
:obj (cts/setup-shape
{:id child-id
:name "svg-text"
:frame-id uuid/zero
:parent-id root-id
:type :svg-raw
:content {:tag :text :attrs {} :content []}})}])
(dissoc file :data))))
(dissoc file :data)))
(t/deftest import-binfile-v3-preserves-svg-raw-children
(let [profile (th/create-profile* 1)
@@ -1970,251 +1967,3 @@
d)))]
(t/is (= :validation (:type out)))
(t/is (= :max-file-size-reached (:code out))))))
(t/deftest import-configures-finite-transaction-idle-timeout
;; The binfile import transaction must set a finite ceiling for
;; idle_in_transaction_session_timeout (20 min) instead of disabling
;; it entirely: a stalled import must not retain a pool connection
;; without any upper bound. The ceiling must be scoped with SET
;; LOCAL: once the transaction ends, the pool session default must
;; be restored for subsequent transactions.
(let [pg-setting-sql ["SELECT setting FROM pg_settings WHERE name = 'idle_in_transaction_session_timeout'"]
in-tx (db/tx-run! th/*system*
(fn [cfg]
(bfc/configure-database-timeouts! cfg)
(:setting (db/exec-one! cfg pg-setting-sql))))
;; SET LOCAL must not leak past the transaction boundary
after (db/tx-run! th/*system*
(fn [cfg]
(:setting (db/exec-one! cfg pg-setting-sql))))]
(t/is (= "1200000" in-tx))
(t/is (= "300000" after))))
(def ^:private index-test-file-id "22222222-2222-2222-2222-222222222222")
(def ^:private index-test-page-id "44444444-4444-4444-4444-444444444444")
(defn- index-entries-of
[names]
(@#'v3/index-entries (map #(java.util.zip.ZipEntry. %) names)))
(t/deftest index-entries-classifies-entry-names
(let [f index-test-file-id
page index-test-page-id
index (index-entries-of
["manifest.json"
(str "files/" f ".json")
(str "files/" f "/plugin-data.json")
(str "files/" f "/tokens.json")
"objects/11111111-1111-1111-1111-111111111111.json"
(str "files/" f "/media/33333333-3333-3333-3333-333333333333.json")
(str "files/" f "/colors/not-a-uuid.json")
(str "files/" f "/components/comp.json")
(str "files/" f "/typographies/t.json")
(str "files/" f "/pages/" page ".json")
(str "files/" f "/pages/" page "/shape.json")
(str "files/" f "/thumbnails/medium/" page "/frame.json")])]
(t/is (= [(parse-uuid "11111111-1111-1111-1111-111111111111")]
(mapv :id (:objects index))))
(t/is (= #{f} (set (keys (:media index)))))
(t/is (= [(parse-uuid "33333333-3333-3333-3333-333333333333")]
(mapv :id (get (:media index) f))))
(t/is (= #{f} (set (keys (:colors index)))))
;; non-uuid ids are preserved as nil, same as the matchers do today
(t/is (= [nil] (mapv :id (get (:colors index) f))))
(t/is (= #{f} (set (keys (:components index)))))
(t/is (= #{f} (set (keys (:typographies index)))))
(t/is (= #{f} (set (keys (:pages index)))))
(t/is (= [(parse-uuid page)] (mapv :id (get (:pages index) f))))
(t/is (= #{f} (set (keys (:shapes index)))))
(t/is (= #{page} (set (keys (get (:shapes index) f)))))
(t/is (= [nil] (mapv :id (get (get (:shapes index) f) page))))
(t/is (= #{f} (set (keys (:thumbnails index)))))
(let [thumb (first (get (:thumbnails index) f))]
(t/is (= "medium" (:tag thumb)))
(t/is (= (parse-uuid page) (:page-id thumb)))
(t/is (= (parse-uuid "frame") (:frame-id thumb)))
(t/is (= f (:file-id thumb))))
(t/is (= #{f} (set (keys (:tokens index)))))))
(t/deftest index-entries-ignores-unknown-entry-names
;; unknown paths, wrong depth, wrong suffix or non-uuid file segments
;; are all ignored, same as today's anchored regexes
(t/is (empty? (index-entries-of ["manifest.json"])))
(t/is (empty? (index-entries-of [(str "files/" index-test-file-id ".json")])))
(t/is (empty? (index-entries-of [(str "files/" index-test-file-id "/plugin-data.json")])))
(t/is (empty? (index-entries-of ["objects/a/b.json"])))
(t/is (empty? (index-entries-of [(str "files/" index-test-file-id "/media/a/b.json")])))
(t/is (empty? (index-entries-of [(str "files/" index-test-file-id "/media/a.txt")])))
(t/is (empty? (index-entries-of [(str "files/" index-test-file-id "/unknown/a.json")])))
(t/is (empty? (index-entries-of [(str "files//media/a.json")]))))
(t/deftest index-entries-requires-literal-json-suffix
;; deliberate tightening vs today's regexes: the dot is unescaped in
;; `([^/]+).json$` / `tokens.json$`, so today `tokensXjson` and
;; `objects/x-json` DO match; the classifier requires a literal
;; `.json` suffix and ignores them
(t/is (empty? (index-entries-of [(str "files/" index-test-file-id "/tokensXjson")])))
(t/is (empty? (index-entries-of ["objects/x-json"]))))
(t/deftest import-binfile-v3-multiple-files-preserves-per-file-content
;; the entries index must attribute every zip entry to the file it
;; belongs to: with several files in the same manifest, pages and
;; shapes of one file must not leak into another. The file also
;; carries library content (color, typography, component and tokens
;; set) so every rewired consumer is exercised on its present path.
(let [profile (th/create-profile* 1)
simple (prepare-simple-file profile)
color-id (uuid/custom 5 1)
typo-id (uuid/custom 5 2)
comp-id (uuid/custom 5 3)
svg (prepare-svg-raw-file profile 2)
output (tmp/tempfile :suffix ".zip")]
(update-file!
:file-id (:id simple)
:profile-id (:id profile)
:revn 0
:vern 0
:changes
[{:type :add-color
:color {:id color-id
:name "import-color"
:color "#FF0000"}}
{:type :add-typography
:typography {:id typo-id
:name "import-typography"
:font-id "source-sans-pro"
:font-family "Source Sans Pro"
:font-variant-id "regular"
:font-size "16"
:font-weight "400"
:font-style "normal"
:line-height "1.4"
:letter-spacing "0"
:text-transform "none"}}
{:type :add-component
:id comp-id
:name "import-component"
:path ""
:main-instance-id (uuid/custom 2 1)
:main-instance-page (uuid/custom 1 1)}
{:type :set-tokens-lib
:tokens-lib (-> (ctob/make-tokens-lib)
(ctob/add-set (ctob/make-token-set :name "ImportSet")))}])
(v3/export-files!
(-> th/*system*
(assoc ::bfc/ids #{(:id simple) (:id svg)})
(assoc ::bfc/embed-assets false)
(assoc ::bfc/include-libraries false))
(io/output-stream output))
;; import returns the imported file ids plus the library link
;; resolution; the test only needs the ids
(let [result (:file-ids (-> th/*system*
(assoc ::bfc/project-id (:default-project-id profile))
(assoc ::bfc/profile-id (:id profile))
(assoc ::bfc/input output)
(v3/import-files!)))
files (map #(bfc/get-file th/*system* %) result)
svg-imported (some #(when (contains? (get-in % [:data :pages-index
svg-raw-page-id
:objects])
svg-raw-root-id)
%)
files)
simple-imported (some #(when (contains? (get-in % [:data :pages-index
(uuid/custom 1 1)
:objects])
(uuid/custom 2 1))
%)
files)]
(t/is (= 2 (count result)))
(t/is (= 2 (count (distinct result))))
(t/is (some? svg-imported))
(t/is (some? simple-imported))
;; the svg-raw file keeps its subtree on its own page (plus the
;; default page created by create-file)
(t/is (= [svg-raw-child-id]
(get-in svg-imported [:data :pages-index svg-raw-page-id
:objects svg-raw-root-id :shapes])))
(t/is (= 2 (count (get-in svg-imported [:data :pages-index]))))
;; the simple file keeps its default page plus its two pages and
;; its shape
(t/is (= 3 (count (get-in simple-imported [:data :pages-index]))))
(t/is (contains? (get-in simple-imported
[:data :pages-index (uuid/custom 1 1) :objects])
(uuid/custom 2 1)))
;; library content is restored on its present path: the
;; consumers' get-in keys must match the classifier buckets
(t/is (some? (get-in simple-imported [:data :colors color-id])))
(t/is (some? (get-in simple-imported [:data :typographies typo-id])))
(t/is (some? (get-in simple-imported [:data :components comp-id])))
(t/is (= ["ImportSet"]
(vec (ctob/get-set-names
(get-in simple-imported [:data :tokens-lib]))))))))
(t/deftest import-binfile-v3-restores-media-objects
;; storage objects, per-file media entries and object thumbnails
;; are classified through the entries index and restored end to end
(let [profile (th/create-profile* 1)
file (prepare-file-with-media profile)
thumb-page-id (uuid/custom 1 1)
thumb-frame-id (uuid/custom 6 1)
thumb-tag "medium"
output (tmp/tempfile :suffix ".zip")]
;; a thumbnail row backed by the same storage object as the media
;; object, so the export produces a thumbnails/ zip entry
(let [mobj (th/db-get :file-media-object {:file-id (:id file)})]
(db/insert! th/*system* :file-tagged-object-thumbnail
{:file-id (:id file)
:tag thumb-tag
:object-id (thc/fmt-object-id {:file-id (:id file)
:page-id thumb-page-id
:frame-id thumb-frame-id
:tag thumb-tag})
:media-id (:media-id mobj)}))
(v3/export-files!
(-> th/*system*
(assoc ::bfc/ids #{(:id file)})
(assoc ::bfc/embed-assets false)
(assoc ::bfc/include-libraries false))
(io/output-stream output))
;; import returns the imported file ids plus the library link
;; resolution; the test only needs the ids
(let [result (:file-ids (-> th/*system*
(assoc ::bfc/project-id (:default-project-id profile))
(assoc ::bfc/profile-id (:id profile))
(assoc ::bfc/input output)
(v3/import-files!)))
mobjs (db/query th/*system* :file-media-object
{:file-id (first result)})
thumbs (db/query th/*system* :file-tagged-object-thumbnail
{:file-id (first result)})]
(t/is (= 1 (count result)))
(t/is (pos? (count mobjs)))
(t/is (every? some? (map :media-id mobjs)))
;; the thumbnail is restored with its object-id rebuilt around
;; the new file id and a media-id that resolves to storage
(t/is (= 1 (count thumbs)))
(let [thumb (first thumbs)]
(t/is (= thumb-tag (:tag thumb)))
(t/is (= (str (first result) "/" thumb-page-id "/" thumb-frame-id "/" thumb-tag)
(:object-id thumb)))
(t/is (some? (:media-id thumb)))))))
+1 -1
View File
@@ -2,7 +2,7 @@
;; 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 Sucursal en España SL
;; Copyright (c) KALEIDOS SUBSIDIARY SL
(ns backend-tests.demo-test
(:require
@@ -2,7 +2,7 @@
;; 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 Sucursal en España SL
;; Copyright (c) KALEIDOS SUBSIDIARY SL
(ns backend-tests.graph-binder-gate-test
"Binder gate for the incremental-sync statement templates.
@@ -2,7 +2,7 @@
;; 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 Sucursal en España SL
;; Copyright (c) KALEIDOS SUBSIDIARY SL
(ns backend-tests.graph-sync-parity-test
"Cold projection and incremental sync are two implementations of one mapping,
@@ -2,7 +2,7 @@
;; 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 Sucursal en España SL
;; Copyright (c) KALEIDOS SUBSIDIARY SL
(ns backend-tests.passwords-test
(:require
+1 -1
View File
@@ -2,7 +2,7 @@
;; 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 Sucursal en España SL
;; Copyright (c) KALEIDOS SUBSIDIARY SL
(ns backend-tests.rpc-demo-test
(:require
@@ -2,7 +2,7 @@
;; 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 Sucursal en España SL
;; Copyright (c) KALEIDOS SUBSIDIARY SL
(ns backend-tests.rpc-plugins-test
(:require
+1 -1
View File
@@ -2,7 +2,7 @@
;; 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 Sucursal en España SL
;; Copyright (c) KALEIDOS SUBSIDIARY SL
(ns app.common.types.path.fit
"Curve fitting helpers."
@@ -2,7 +2,7 @@
;; 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 Sucursal en España SL
;; Copyright (c) KALEIDOS SUBSIDIARY SL
(ns app.common.types.path.selection
"Transforms selected path nodes and handlers."
@@ -2,7 +2,7 @@
;; 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 Sucursal en España SL
;; Copyright (c) KALEIDOS SUBSIDIARY SL
(ns app.common.types.tokens-status
(:require
@@ -2,7 +2,7 @@
;; 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 Sucursal en España SL
;; Copyright (c) KALEIDOS SUBSIDIARY SL
(ns common-tests.files-migrations-0026-test
(:require
@@ -2,7 +2,7 @@
;; 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 Sucursal en España SL
;; Copyright (c) KALEIDOS SUBSIDIARY SL
(ns common-tests.types.tokens-status-test
(:require
+1 -1
View File
@@ -2,7 +2,7 @@
;; 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 Sucursal en España SL
;; Copyright (c) KALEIDOS SUBSIDIARY SL
(ns app.auth
"Resolves the caller's session cookie to a real profile id.
+1 -1
View File
@@ -2,7 +2,7 @@
;; 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 Sucursal en España SL
;; Copyright (c) KALEIDOS SUBSIDIARY SL
(ns app.handlers.export
"Handle export jobs"
+1 -1
View File
@@ -2,7 +2,7 @@
;; 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 Sucursal en España SL
;; Copyright (c) KALEIDOS SUBSIDIARY SL
(ns app.handlers.jobs
"REST surface for export jobs, under `/api/export/jobs`.
+1 -1
View File
@@ -2,7 +2,7 @@
;; 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 Sucursal en España SL
;; Copyright (c) KALEIDOS SUBSIDIARY SL
(ns app.jobs
"Export job model and lifecycle.
+1 -1
View File
@@ -2,7 +2,7 @@
;; 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 Sucursal en España SL
;; Copyright (c) KALEIDOS SUBSIDIARY SL
(ns app.jobs.scheduler
"Admission control for export jobs.
+1 -1
View File
@@ -2,7 +2,7 @@
;; 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 Sucursal en España SL
;; Copyright (c) KALEIDOS SUBSIDIARY SL
(ns app.jobs.store
"Redis persistence for export jobs.
+1 -1
View File
@@ -2,7 +2,7 @@
;; 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 Sucursal en España SL
;; Copyright (c) KALEIDOS SUBSIDIARY SL
(ns app.jobs.utils
"Temp file ownership for export jobs.
+1 -1
View File
@@ -2,7 +2,7 @@
;; 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 Sucursal en España SL
;; Copyright (c) KALEIDOS SUBSIDIARY SL
(ns app.router
"Method + path dispatch.
+1 -1
View File
@@ -2,7 +2,7 @@
;; 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 Sucursal en España SL
;; Copyright (c) KALEIDOS SUBSIDIARY SL
(ns app.wasm.pool
"Pool of headless render workers.
+1 -1
View File
@@ -2,7 +2,7 @@
;; 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 Sucursal en España SL
;; Copyright (c) KALEIDOS SUBSIDIARY SL
(ns app.wasm.render
"Headless render pipeline: renders exports with the render-wasm Skia pipeline,
+1 -1
View File
@@ -2,7 +2,7 @@
;; 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 Sucursal en España SL
;; Copyright (c) KALEIDOS SUBSIDIARY SL
(ns app.wasm.worker
"Render worker entry point.
@@ -2,7 +2,7 @@
;; 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 Sucursal en España SL
;; Copyright (c) KALEIDOS SUBSIDIARY SL
(ns exporter-tests.export-shapes-test
"Chunking of the browser backend."
+1 -1
View File
@@ -2,7 +2,7 @@
;; 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 Sucursal en España SL
;; Copyright (c) KALEIDOS SUBSIDIARY SL
(ns exporter-tests.jobs-test
"Job state machine. Runs without redis: a store write with no connection is
@@ -2,7 +2,7 @@
;; 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 Sucursal en España SL
;; Copyright (c) KALEIDOS SUBSIDIARY SL
(ns exporter-tests.scheduler-test
"Admission control. A headless job leases one render worker for its whole run,
@@ -2,7 +2,7 @@
;; 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 Sucursal en España SL
;; Copyright (c) KALEIDOS SUBSIDIARY SL
(ns exporter-tests.wasm-pool-test
"Worker leasing, against a stub pool: `with-worker` must give the worker back
@@ -2,7 +2,7 @@
;; 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 Sucursal en España SL
;; Copyright (c) KALEIDOS SUBSIDIARY SL
(ns app.main.data.workspace.path.clipboard
(:require
@@ -2,7 +2,7 @@
;; 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 Sucursal en España SL
;; Copyright (c) KALEIDOS SUBSIDIARY SL
(ns app.main.ui.workspace.viewport.path-state
(:require
@@ -2,7 +2,7 @@
;; 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 Sucursal en España SL
;; Copyright (c) KALEIDOS SUBSIDIARY SL
(ns frontend-tests.logic.path-actions-test
(:require
@@ -2,7 +2,7 @@
;; 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 Sucursal en España SL
;; Copyright (c) KALEIDOS SUBSIDIARY SL
(ns frontend-tests.logic.path-clipboard-test
@@ -2,7 +2,7 @@
;; 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 Sucursal en España SL
;; Copyright (c) KALEIDOS SUBSIDIARY SL
(ns frontend-tests.logic.path-helpers-test
@@ -2,7 +2,7 @@
;; 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 Sucursal en España SL
;; Copyright (c) KALEIDOS SUBSIDIARY SL
(ns frontend-tests.logic.path-lifecycle-test
@@ -2,7 +2,7 @@
;; 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 Sucursal en España SL
;; Copyright (c) KALEIDOS SUBSIDIARY SL
(ns frontend-tests.logic.path-test-helpers
@@ -2,7 +2,7 @@
;; 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 Sucursal en España SL
;; Copyright (c) KALEIDOS SUBSIDIARY SL
(ns frontend-tests.logic.path-tools-test
@@ -2,7 +2,7 @@
;; 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 Sucursal en España SL
;; Copyright (c) KALEIDOS SUBSIDIARY SL
(ns frontend-tests.logic.wasm-modifiers-nil-id-test
"Reproduces the production crash \"Cannot read properties of null
@@ -2,7 +2,7 @@
;; 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 Sucursal en España SL
;; Copyright (c) KALEIDOS SUBSIDIARY SL
(ns frontend-tests.plugins.flex-test
(:require
@@ -2,7 +2,7 @@
;; 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 Sucursal en España SL
;; Copyright (c) KALEIDOS SUBSIDIARY SL
(ns frontend-tests.plugins.user-test
(:require
+8 -1
View File
@@ -124,14 +124,21 @@ macro_rules! with_current_shape {
#[cfg(test)]
pub(crate) struct TestRenderResourcesGuard {
prev: *mut RenderResources,
_lock: std::sync::MutexGuard<'static, ()>,
}
#[cfg(test)]
static TEST_RENDER_RESOURCES_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
#[cfg(test)]
impl TestRenderResourcesGuard {
pub(crate) fn install(resources: &mut RenderResources) -> Self {
let lock = TEST_RENDER_RESOURCES_LOCK
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
let prev = unsafe { RENDER_RESOURCES };
unsafe { RENDER_RESOURCES = resources as *mut _ };
Self { prev }
Self { prev, _lock: lock }
}
}
+316 -50
View File
@@ -268,7 +268,6 @@ fn difference(
.iter()
.filter(|s| path_a.contains(to_point(s.evaluate(TValue::Parametric(0.5)))))
.copied()
.map(|s| s.reverse())
.map(|b| (BezierSource::B, b)),
);
@@ -278,16 +277,53 @@ fn difference(
fn exclusion(segments_a: Vec<Bezier>, segments_b: Vec<Bezier>) -> Vec<(BezierSource, Bezier)> {
let mut result = Vec::new();
result.extend(segments_a.iter().copied().map(|b| (BezierSource::A, b)));
result.extend(
segments_b
.iter()
.copied()
.map(|s| s.reverse())
.map(|b| (BezierSource::B, b)),
);
result.extend(segments_b.iter().copied().map(|b| (BezierSource::B, b)));
result
}
// Mirrors `app.common.types.path.subpath/clockwise?`.
fn is_clockwise(path: &Path) -> bool {
let mut points: Vec<(f32, f32)> = Vec::new();
for segment in path.segments().iter() {
match *segment {
Segment::MoveTo(p) => {
if !points.is_empty() {
break;
}
points.push(p);
}
Segment::LineTo(p) => points.push(p),
Segment::CurveTo((_, _, p)) => points.push(p),
Segment::Close => break,
}
}
if points.len() < 3 {
return false;
}
let mut signed_area = 0.0f64;
for i in 0..points.len() {
let (x1, y1) = points[i];
let (x2, y2) = points[(i + 1) % points.len()];
signed_area += f64::from(x1) * f64::from(y2) - f64::from(x2) * f64::from(y1);
}
signed_area > 0.0
}
// The kept pieces of B must point the same way round as the kept pieces of A. Not the
// `path.bool/content-bool-pair` rule, which reverses intersection on same winding and
// relies on `subpath/merge-paths` flipping subpaths when it joins them.
fn should_reverse_b(bool_type: BoolType, a_is_clockwise: bool, path_b: &Path) -> bool {
let same_winding = a_is_clockwise == is_clockwise(path_b);
match bool_type {
BoolType::Union | BoolType::Intersection => !same_winding,
BoolType::Difference | BoolType::Exclusion => same_winding,
}
}
#[derive(Debug, Clone, PartialEq, Copy)]
enum BezierSource {
A,
@@ -305,17 +341,14 @@ fn pop_first_from_pool(pool: &mut BezierPool) -> Option<(BezierSource, Bezier)>
pool.iter_mut().find_map(|e| e.take())
}
// Find and remove the segment whose start point is closest to `end` within the
// appropriate threshold. Same-source segments use a tight threshold
// (INTERSECT_THRESHOLD_SAMEd) so we prefer staying on the same original path;
// cross-source segments use a wider threshold (INTERSECT_THRESHOLD_DIFFERENT)
// to allow switching paths at intersection points.
// Same-source candidates get a tighter threshold so we stay on the original path. A
// candidate that joins by its `end` points the wrong way, so we reverse it.
fn find_next_in_pool(
pool: &mut BezierPool,
end: DVec2,
source: BezierSource,
) -> Option<(BezierSource, Bezier)> {
let mut best_idx: Option<usize> = None;
let mut best: Option<(usize, bool)> = None;
let mut best_dist_sq = f64::MAX;
for (i, entry) in pool.iter().enumerate() {
@@ -327,16 +360,21 @@ fn find_next_in_pool(
} else {
INTERSECT_THRESHOLD_DIFFERENT as f64
};
let dx = bezier.start.x - end.x;
let dy = bezier.start.y - end.y;
let dist_sq = dx * dx + dy * dy;
if dist_sq <= threshold * threshold && dist_sq < best_dist_sq {
best_dist_sq = dist_sq;
best_idx = Some(i);
for (reversed, point) in [(false, bezier.start), (true, bezier.end)] {
let dx = point.x - end.x;
let dy = point.y - end.y;
let dist_sq = dx * dx + dy * dy;
if dist_sq <= threshold * threshold && dist_sq < best_dist_sq {
best_dist_sq = dist_sq;
best = Some((i, reversed));
}
}
}
best_idx.and_then(|i| pool[i].take())
let (idx, reversed) = best?;
pool[idx]
.take()
.map(|(src, bezier)| (src, if reversed { bezier.reverse() } else { bezier }))
}
fn push_bezier(result: &mut Vec<Segment>, bezier: &Bezier) {
@@ -410,32 +448,45 @@ fn beziers_to_segments(beziers: &[(BezierSource, Bezier)]) -> Vec<Segment> {
result
}
pub fn bool_from_shapes(bool_type: BoolType, children_ids: &[Uuid], shapes: ShapesPoolRef) -> Path {
if children_ids.is_empty() {
return Path::default();
fn bool_beziers(
bool_type: BoolType,
path_a: &Path,
a_is_clockwise: bool,
path_b: &Path,
) -> (Vec<(BezierSource, Bezier)>, bool) {
let (segs_a, mut segs_b) = split_segments(path_a, path_b);
if should_reverse_b(bool_type, a_is_clockwise, path_b) {
for segment in segs_b.iter_mut() {
*segment = segment.reverse();
}
}
let Some(child) = shapes.get(&children_ids[children_ids.len() - 1]) else {
let beziers = match bool_type {
BoolType::Union => union(path_a, segs_a, path_b, segs_b),
BoolType::Difference => difference(path_a, segs_a, path_b, segs_b),
BoolType::Intersection => intersection(path_a, segs_a, path_b, segs_b),
BoolType::Exclusion => exclusion(segs_a, segs_b),
};
(beziers, path_a.is_even_odd() || path_b.is_even_odd())
}
// Fold `paths` left to right; the first entry is the base operand.
fn bool_fold(bool_type: BoolType, paths: &[Path]) -> Path {
let Some((first, rest)) = paths.split_first() else {
return Path::default();
};
let mut current_path = child.to_path(shapes);
let mut current_path = first.clone();
// Every fold step chains A's fragments, which keep their direction, so the
// accumulated path keeps this winding. Carry it instead of re-reading it from the
// emitted segment list, whose subpath order and direction fall out of pool ordering.
let is_clockwise_a = is_clockwise(&current_path);
for idx in (0..children_ids.len() - 1).rev() {
let Some(other) = shapes.get(&children_ids[idx]) else {
continue;
};
let other_path = other.to_path(shapes);
let (segs_a, segs_b) = split_segments(&current_path, &other_path);
let is_even_odd = current_path.is_even_odd() || other_path.is_even_odd();
let beziers = match bool_type {
BoolType::Union => union(&current_path, segs_a, &other_path, segs_b),
BoolType::Difference => difference(&current_path, segs_a, &other_path, segs_b),
BoolType::Intersection => intersection(&current_path, segs_a, &other_path, segs_b),
BoolType::Exclusion => exclusion(segs_a, segs_b),
};
for other_path in rest {
let (beziers, is_even_odd) =
bool_beziers(bool_type, &current_path, is_clockwise_a, other_path);
current_path = Path::new(beziers_to_segments(&beziers)).with_even_odd(is_even_odd);
}
@@ -443,6 +494,16 @@ pub fn bool_from_shapes(bool_type: BoolType, children_ids: &[Uuid], shapes: Shap
current_path
}
pub fn bool_from_shapes(bool_type: BoolType, children_ids: &[Uuid], shapes: ShapesPoolRef) -> Path {
let paths: Vec<Path> = children_ids
.iter()
.rev()
.filter_map(|id| shapes.get(id).map(|child| child.to_path(shapes)))
.collect();
bool_fold(bool_type, &paths)
}
pub fn update_bool_to_path(shape: &mut Shape, shapes: ShapesPoolRef) {
let children_ids = shape.children_ids(true);
@@ -481,6 +542,7 @@ pub fn debug_render_bool_paths(
};
let mut current_path = child.to_path(shapes);
let is_clockwise_a = is_clockwise(&current_path);
for idx in (0..children_ids.len() - 1).rev() {
let Some(other) = shapes.get(&children_ids[idx]) else {
@@ -488,15 +550,12 @@ pub fn debug_render_bool_paths(
};
let other_path = other.to_path(shapes);
let (segs_a, segs_b) = split_segments(&current_path, &other_path);
let is_even_odd = current_path.is_even_odd() || other_path.is_even_odd();
let beziers = match bool_data.bool_type {
BoolType::Union => union(&current_path, segs_a, &other_path, segs_b),
BoolType::Difference => difference(&current_path, segs_a, &other_path, segs_b),
BoolType::Intersection => intersection(&current_path, segs_a, &other_path, segs_b),
BoolType::Exclusion => exclusion(segs_a, segs_b),
};
let (beziers, is_even_odd) = bool_beziers(
bool_data.bool_type,
&current_path,
is_clockwise_a,
&other_path,
);
current_path = Path::new(beziers_to_segments(&beziers)).with_even_odd(is_even_odd);
if idx == 0 {
@@ -572,3 +631,210 @@ pub fn debug_render_bool_paths(
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn linear(from: (f64, f64), to: (f64, f64)) -> Bezier {
Bezier::from_linear_coordinates(from.0, from.1, to.0, to.1)
}
fn polygon(points: &[(f32, f32)]) -> Path {
let mut segments = vec![Segment::MoveTo(points[0])];
segments.extend(points[1..].iter().map(|p| Segment::LineTo(*p)));
segments.push(Segment::Close);
Path::new(segments)
}
fn count(segments: &[Segment], f: fn(&Segment) -> bool) -> usize {
segments.iter().filter(|s| f(s)).count()
}
fn is_move_to(s: &Segment) -> bool {
matches!(s, Segment::MoveTo(_))
}
fn is_close(s: &Segment) -> bool {
matches!(s, Segment::Close)
}
fn ring_area(ring: &[(f32, f32)]) -> f64 {
let mut area = 0.0f64;
for i in 0..ring.len() {
let (x1, y1) = ring[i];
let (x2, y2) = ring[(i + 1) % ring.len()];
area += f64::from(x1) * f64::from(y2) - f64::from(x2) * f64::from(y1);
}
area / 2.0
}
fn signed_area(segments: &[Segment]) -> f64 {
let mut total = 0.0f64;
let mut ring: Vec<(f32, f32)> = Vec::new();
for segment in segments {
match *segment {
Segment::MoveTo(p) => {
total += ring_area(&ring);
ring.clear();
ring.push(p);
}
Segment::LineTo(p) | Segment::CurveTo((_, _, p)) => ring.push(p),
Segment::Close => {
total += ring_area(&ring);
ring.clear();
}
}
}
total + ring_area(&ring)
}
// Operands and expected results taken from the CLJS bool (`app.common.types.path.bool`)
// run on the same shapes: A clockwise, B and C counter-clockwise, all overlapping.
const A_CW: [(f32, f32); 4] = [
(100.0, 100.0),
(300.0, 100.0),
(300.0, 300.0),
(100.0, 300.0),
];
const B_CCW: [(f32, f32); 4] = [
(200.0, 200.0),
(200.0, 400.0),
(400.0, 400.0),
(400.0, 200.0),
];
const C_CCW: [(f32, f32); 4] = [(60.0, 240.0), (60.0, 360.0), (260.0, 360.0), (260.0, 240.0)];
#[test]
fn test_is_clockwise() {
let cw = Path::new(vec![
Segment::MoveTo((0.0, 0.0)),
Segment::LineTo((10.0, 0.0)),
Segment::LineTo((10.0, 10.0)),
Segment::LineTo((0.0, 10.0)),
Segment::Close,
]);
assert!(is_clockwise(&cw));
let ccw = Path::new(vec![
Segment::MoveTo((0.0, 0.0)),
Segment::LineTo((0.0, 10.0)),
Segment::LineTo((10.0, 10.0)),
Segment::LineTo((10.0, 0.0)),
Segment::Close,
]);
assert!(!is_clockwise(&ccw));
}
#[test]
fn test_should_reverse_b_only_depends_on_relative_winding() {
let cw = Path::new(vec![
Segment::MoveTo((0.0, 0.0)),
Segment::LineTo((10.0, 0.0)),
Segment::LineTo((10.0, 10.0)),
Segment::LineTo((0.0, 10.0)),
Segment::Close,
]);
let ccw = Path::new(vec![
Segment::MoveTo((0.0, 0.0)),
Segment::LineTo((0.0, 10.0)),
Segment::LineTo((10.0, 10.0)),
Segment::LineTo((10.0, 0.0)),
Segment::Close,
]);
assert!(should_reverse_b(BoolType::Difference, true, &cw));
assert!(!should_reverse_b(BoolType::Difference, true, &ccw));
assert!(!should_reverse_b(BoolType::Union, true, &cw));
assert!(should_reverse_b(BoolType::Union, true, &ccw));
}
// Fragments from #11482: two point the wrong way, so joining them start-to-start only
// left five open subpaths.
#[test]
fn test_beziers_to_segments_closes_reversed_fragments() {
let beziers = vec![
(
BezierSource::A,
linear((2764.00, -240.00), (2834.74, -110.71)),
),
(
BezierSource::A,
linear((2809.29, -85.26), (2693.26, -201.29)),
),
(
BezierSource::A,
linear((2718.71, -226.74), (2764.00, -240.00)),
),
(
BezierSource::B,
linear((2718.71, -226.74), (2834.74, -110.71)),
),
(
BezierSource::B,
linear((2809.29, -85.26), (2693.26, -201.29)),
),
];
let segments = beziers_to_segments(&beziers);
let moves = segments
.iter()
.filter(|s| matches!(s, Segment::MoveTo(_)))
.count();
let closes = segments
.iter()
.filter(|s| matches!(s, Segment::Close))
.count();
assert_eq!(moves, 2);
assert_eq!(closes, 2);
// 3 fragments in the first subpath, 2 in the second, each dropping its closing LineTo.
assert_eq!(segments.len(), 7);
}
// #11482: A clockwise, B counter-clockwise. Reference (CLJS):
// M100,100 L300,100 L300,200 L200,200 L200,300 L100,300 Z
#[test]
fn test_difference_with_opposite_winding_operand() {
let result = bool_fold(BoolType::Difference, &[polygon(&A_CW), polygon(&B_CCW)]);
let segments = result.segments();
assert_eq!(count(segments, is_move_to), 1);
assert_eq!(count(segments, is_close), 1);
assert!((signed_area(segments) - 30000.0).abs() < 1.0);
assert!(is_clockwise(&result));
}
// Reference (CLJS):
// M100,100 L300,100 L300,200 L400,200 L400,400 L200,400 L200,300 L100,300 Z
#[test]
fn test_union_with_opposite_winding_operand() {
let result = bool_fold(BoolType::Union, &[polygon(&A_CW), polygon(&B_CCW)]);
let segments = result.segments();
assert_eq!(count(segments, is_move_to), 1);
assert_eq!(count(segments, is_close), 1);
assert!((signed_area(segments) - 70000.0).abs() < 1.0);
assert!(is_clockwise(&result));
}
// The second fold step must compare against A's winding, not against the winding of
// the intermediate path, whose subpath order and direction fall out of pool ordering.
// Reference (CLJS): M100,100 L300,100 L300,200 L200,200 L200,240 L100,240 Z
#[test]
fn test_difference_folds_three_opposite_winding_operands() {
let result = bool_fold(
BoolType::Difference,
&[polygon(&A_CW), polygon(&B_CCW), polygon(&C_CCW)],
);
let segments = result.segments();
assert_eq!(count(segments, is_move_to), 1);
assert_eq!(count(segments, is_close), 1);
assert!((signed_area(segments) - 24000.0).abs() < 1.0);
assert!(is_clockwise(&result));
}
}
+5 -3
View File
@@ -173,9 +173,11 @@ main() {
fi
done
# Get tracked files matching our extensions
local files
files=$(git ls-files | grep -E "\.(${EXTENSIONS})$" || true)
# Get tracked files matching our extensions, excluding this script itself
# (it mentions the search string in its own usage docs).
local files script_name
script_name=$(basename "$0")
files=$(git ls-files | grep -E "\.(${EXTENSIONS})$" | grep -v -F "$script_name" || true)
if [[ -z "$files" ]]; then
log_warn "No tracked files found matching extensions: ${EXTENSIONS}"