Miguel de Benito Delgado 13af3fb323 🚧 WIP 27, improve logging and console reporting.
AI-assisted-by: deepseek-flash, gpt-6.1-sol
2026-10-29 13:58:32 +01:00

569 lines
29 KiB
Clojure

;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS INC
(ns performance-tests.render-wasm.artifacts-test
(:require
["fs" :as fs]
["os" :as os]
["path" :as path]
["worker_threads" :as workers]
[app.common.transit :as transit]
[benches.render-wasm.codec :as codec]
[benches.render-wasm.identity :as identity]
[benches.render-wasm.node.artifacts :as artifacts]
[benches.render-wasm.node.catalog :as catalog]
[cljs.test :as t :include-macros true]))
(def ^:private temporary-root (atom nil))
(def ^:private selection {:default-features? true :features []})
(def ^:private provenance {:git {:sha "test-revision" :dirty false}})
(t/use-fixtures :each
{:before #(reset! temporary-root
(fs/mkdtempSync (path/join (os/tmpdir) "penpot-render-artifacts-")))
:after #(fs/rmSync @temporary-root #js {:recursive true :force true})})
(defn- create-build
[]
(artifacts/create-build @temporary-root selection provenance))
(defn- write-file
[file content]
(fs/mkdirSync (path/dirname file) #js {:recursive true})
(fs/writeFileSync file content "utf8"))
(defn- complete-files
[build]
(write-file (:cases-path build) "module.exports = {};")
(write-file (path/join (:browser-dir build) "browser.js") "export default {};")
(write-file (path/join (:renderer-dir build) "render-wasm.js") "export default {};")
(fs/writeFileSync (path/join (:renderer-dir build) "render-wasm.wasm")
(js/Buffer.from #js [0 97 115 109 1 0 0 0]))
(write-file (path/join (:renderer-dir build) "shared.js") "export default {};")
(write-file (:index-path build) "<!doctype html><title>selected build</title>")
build)
(defn- ready-build
[]
(let [build (complete-files (create-build))]
(artifacts/write-manifest build (assoc (:manifest build) :status :ready))))
(defn- failure
[f]
(try
(f)
nil
(catch :default error error)))
(t/deftest frontend-root-is-independent-of-caller-cwd
(let [expected (artifacts/frontend-root)
previous (.cwd js/process)]
(try
(.chdir js/process @temporary-root)
(t/is (= expected (artifacts/frontend-root)))
(t/is (.isFile (fs/statSync (path/join expected "deps.edn"))))
(t/is (.isDirectory (fs/statSync
(path/join expected "performance" "benches" "render_wasm"))))
(finally
(.chdir js/process previous)))))
(t/deftest create-build-retains-its-initial-release-evidence
(let [selected {:default-features? false :features ["feature-one" "feature-two"]}
build (artifacts/create-build @temporary-root selected provenance)
manifest (transit/decode-str (fs/readFileSync (:manifest-path build) "utf8"))]
(t/is (re-matches #"[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}"
(:id build)))
(t/is (= (path/join (artifacts/managed-root @temporary-root) "builds" (:id build))
(:dir build)))
(t/is (= (path/join (:dir build) "build.transit") (:manifest-path build)))
(t/is (= (path/join (:dir build) "index.html") (:index-path build)))
(t/is (= manifest (:manifest build)))
(t/is (= #{:type :id :status :created-at :updated-at :functional :provenance}
(set (keys manifest))))
(t/is (= {:type :renderer-build :status :building}
(select-keys manifest [:type :status])))
(t/is (= {:mode :release :default-features? false
:features ["feature-one" "feature-two"] :cljs-profile-marks false}
(:functional manifest)))
(t/is (= provenance (:provenance manifest)))
(t/is (= (:created-at manifest) (.toISOString (js/Date. (:created-at manifest)))))
(t/is (= (:created-at manifest) (:updated-at manifest)))
(t/is (= "" (fs/readFileSync (:log-path build) "utf8")))
(t/is (= (path/join (:dir build) "cases.cjs") (:cases-path build)))
(t/is (.isDirectory (fs/statSync (:browser-dir build))))
(t/is (.isDirectory (fs/statSync (:renderer-dir build))))))
(t/deftest ready-build-loads-all-selected-paths
(let [build (ready-build)
loaded (artifacts/load-build @temporary-root (:id build))]
(t/is (= build loaded))
(t/is (= (path/join (:dir build) "index.html") (:index-path loaded)))))
(t/deftest complete-building-files-can-be-checked-before-publishing-ready
(let [build (complete-files (create-build))]
(t/is (= build (artifacts/check-files build)))
(t/is (= :building (:status (transit/decode-str
(fs/readFileSync (:manifest-path build) "utf8")))))
(t/is (= ::artifacts/not-ready
(:type (ex-data (failure #(artifacts/load-build @temporary-root (:id build)))))))))
(t/deftest incomplete-building-files-are-rejected-before-publishing-ready
(doseq [relative ["cases.cjs" "browser/browser.js" "renderer/render-wasm.js"
"renderer/render-wasm.wasm" "renderer/shared.js" "index.html"]]
(let [build (complete-files (create-build))]
(fs/unlinkSync (path/join (:dir build) relative))
(t/is (some? (failure #(artifacts/check-files build))) relative)
(t/is (= :building (:status (transit/decode-str
(fs/readFileSync (:manifest-path build) "utf8"))))))))
(t/deftest building-and-failed-builds-are-not-loadable-and-retain-evidence
(let [building (create-build)
failed (create-build)
log "renderer compilation failed\n"]
(fs/appendFileSync (:log-path failed) log "utf8")
(artifacts/write-manifest failed (assoc (:manifest failed) :status :failed
:failure {:stage :renderer :message "compile failed"}))
(doseq [build [building failed]]
(t/is (= ::artifacts/not-ready
(:type (ex-data (failure #(artifacts/load-build @temporary-root (:id build))))))))
(t/is (= :failed (:status (transit/decode-str
(fs/readFileSync (:manifest-path failed) "utf8")))))
(t/is (= log (fs/readFileSync (:log-path failed) "utf8")))
(t/is (fs/existsSync (:dir building)))))
(t/deftest stale-references-cannot-reopen-terminal-builds
(doseq [status [:ready :failed]]
(let [build (create-build)
terminal (artifacts/write-manifest build (assoc (:manifest build) :status status))
saved (fs/readFileSync (:manifest-path build) "utf8")]
(t/is (= terminal (artifacts/write-manifest terminal (:manifest terminal))))
(doseq [next-status [:building :ready :failed]]
(let [attempt (assoc (:manifest terminal) :status next-status :extra "changed")]
(t/is (= ::artifacts/terminal-build
(:type (ex-data (failure #(artifacts/write-manifest build attempt))))))))
(t/is (= saved (fs/readFileSync (:manifest-path build) "utf8"))))))
(t/deftest unencodable-update-keeps-the-last-manifest-and-log
(let [build (create-build)
saved (fs/readFileSync (:manifest-path build) "utf8")]
(fs/appendFileSync (:log-path build) "saved diagnostic\n" "utf8")
(t/is (some? (failure #(artifacts/write-manifest
build (assoc (:manifest build) :callback (fn [] nil))))))
(t/is (= saved (fs/readFileSync (:manifest-path build) "utf8")))
(t/is (= "saved diagnostic\n" (fs/readFileSync (:log-path build) "utf8")))
(t/is (= #{"browser" "renderer" "build.log" "build.transit"}
(set (array-seq (fs/readdirSync (:dir build))))))))
(t/deftest malformed-builds-cannot-replace-saved-manifests
(doseq [side [:current :replacement]]
(let [build (create-build)
valid (:manifest build)
malformed (dissoc valid :functional)
proposed (if (= :replacement side) malformed valid)]
(when (= :current side)
(fs/writeFileSync (:manifest-path build) (codec/encode-str malformed) "utf8"))
(let [saved (fs/readFileSync (:manifest-path build) "utf8")
error (failure #(artifacts/write-manifest build proposed))]
(t/is (= ::artifacts/invalid-manifest (:type (ex-data error))) (name side))
(t/is (= saved (fs/readFileSync (:manifest-path build) "utf8")) (name side))))))
(def ^:private manifest-reader
(str "const {parentPort, workerData} = require('worker_threads');"
"const fs = require('fs'); const stop = new Int32Array(workerData.stop);"
"let reads = 0; const errors = [];"
"function read() { try { JSON.parse(fs.readFileSync(workerData.path, 'utf8'));"
"reads++; } catch (error) { if (errors.length < 5) errors.push(error.message); } }"
"read(); parentPort.postMessage('ready');"
"while (Atomics.load(stop, 0) === 0) read();"
"parentPort.postMessage({reads, errors});"))
(defn- worker-message
[^js worker]
(js/Promise.
(fn [resolve reject]
(letfn [(received [value]
(.removeListener worker "error" failed)
(resolve value))
(failed [error]
(.removeListener worker "message" received)
(reject error))]
(.once worker "message" received)
(.once worker "error" failed)))))
(t/deftest ^:async manifest-replacements-never-expose-a-partial-file
(let [build (create-build)
stop (js/SharedArrayBuffer. 4)
^js worker (workers/Worker. manifest-reader
#js {:eval true
:workerData #js {:path (:manifest-path build) :stop stop}})]
(try
(await (worker-message worker))
(let [result (worker-message worker)]
(dotimes [n 24]
(artifacts/write-manifest
build (assoc (:manifest build) :progress n
:diagnostic (apply str (repeat (+ 8192 (* n 1024)) "evidence ")))))
(js/Atomics.store (js/Int32Array. stop) 0 1)
(let [^js observed (await result)]
(t/is (> (.-reads observed) 1) "a separate thread read during replacement")
(t/is (empty? (array-seq (.-errors observed)))
"every observed manifest remained a complete JSON document")))
(t/is (= 23 (:progress (transit/decode-str
(fs/readFileSync (:manifest-path build) "utf8")))))
(finally
(await (.terminate worker))))))
(t/deftest malformed-manifests-and-incomplete-builds-are-rejected
(doseq [change [{:functional []} {:type :run-record} {:id "another-build"}
{:status :unknown}]]
(let [build (ready-build)]
(fs/writeFileSync (:manifest-path build) (codec/encode-str (merge (:manifest build) change))
"utf8")
(t/is (= ::artifacts/unsupported-manifest
(:type (ex-data (failure #(artifacts/load-build @temporary-root (:id build)))))))))
(doseq [relative ["cases.cjs" "browser/browser.js" "renderer/render-wasm.js"
"renderer/render-wasm.wasm" "renderer/shared.js" "index.html"]]
(let [build (ready-build)]
(fs/unlinkSync (path/join (:dir build) relative))
(t/is (some? (failure #(artifacts/load-build @temporary-root (:id build)))) relative))))
(t/deftest saved-builds-validate-current-shapes
(let [build (ready-build)
manifest (select-keys (:manifest build)
[:type :id :status :created-at :updated-at :functional :provenance])]
(fs/writeFileSync (:manifest-path build) (codec/encode-str manifest) "utf8")
(t/is (= manifest (:manifest (artifacts/load-build @temporary-root (:id build)))))
(fs/writeFileSync (:manifest-path build)
(codec/encode-str (dissoc manifest :functional)) "utf8")
(t/is (= ::artifacts/unsupported-manifest
(:type (ex-data (failure #(artifacts/load-build @temporary-root (:id build)))))))))
(t/deftest build-environment-roundtrips-without-inventing-missing-evidence
(t/is (not (contains? (:manifest (create-build)) :environment)))
(doseq [environment [{} {"CFLAGS" "" "RUSTFLAGS" "-C\u001fopt-level=3"}]]
(let [build (complete-files (create-build))
ready (artifacts/write-manifest build (assoc (:manifest build)
:status :ready :environment environment))
saved (artifacts/load-build @temporary-root (:id ready))]
(t/is (= environment (get-in saved [:manifest :environment]))))))
(t/deftest build-environment-rejects-non-string-keys-and-values
(doseq [environment [nil [] {:CFLAGS ""} {"CFLAGS" nil} {"CFLAGS" 0}]]
(let [build (create-build)
saved (fs/readFileSync (:manifest-path build) "utf8")
cause (failure #(artifacts/write-manifest build (assoc (:manifest build) :environment environment)))]
(t/is (= ::artifacts/invalid-manifest (:type (ex-data cause))))
(t/is (= saved (fs/readFileSync (:manifest-path build) "utf8"))))))
(t/deftest ids-cannot-select-or-delete-outside-paths
(let [build (ready-build)
outside (path/join @temporary-root "outside.txt")]
(write-file outside "keep this")
(doseq [id [nil "" "." ".." "../outside.txt" "safe/../../outside.txt"
"..\\outside.txt" "/tmp/outside" "id%2fother"]]
(t/is (= ::artifacts/invalid-id
(:type (ex-data (failure #(artifacts/load-build @temporary-root id))))))
(t/is (= ::artifacts/invalid-id
(:type (ex-data (failure #(artifacts/remove-builds
@temporary-root [(:id build) id])))))))
(t/is (= build (artifacts/load-build @temporary-root (:id build))))
(t/is (= "keep this" (fs/readFileSync outside "utf8")))))
(t/deftest safe-non-uuid-ids-can-load-existing-builds
(let [build (ready-build)
id "retained-build_1"
dir (path/join (path/dirname (:dir build)) id)]
(fs/renameSync (:dir build) dir)
(fs/writeFileSync (path/join dir "build.transit")
(codec/encode-str (assoc (:manifest build) :id id)) "utf8")
(t/is (= id (:id (artifacts/load-build @temporary-root id))))
(t/is (= dir (:dir (artifacts/load-build @temporary-root id))))))
(t/deftest build-and-artifact-symlinks-cannot-escape-the-managed-root
(let [build (ready-build)
outside (path/join @temporary-root "outside.txt")]
(write-file outside "private")
(fs/unlinkSync (:cases-path build))
(fs/symlinkSync outside (:cases-path build))
(t/is (= ::artifacts/unsafe-path
(:type (ex-data (failure #(artifacts/check-files build))))))
(t/is (= ::artifacts/unsafe-path
(:type (ex-data (failure #(artifacts/load-build @temporary-root (:id build))))))))
(let [build (ready-build)
outside (path/join @temporary-root "outside-build")]
(fs/renameSync (:dir build) outside)
(fs/symlinkSync outside (:dir build) "dir")
(t/is (= ::artifacts/unsafe-path
(:type (ex-data (failure #(artifacts/check-files build))))))
(t/is (= ::artifacts/unsafe-path
(:type (ex-data (failure #(artifacts/load-build @temporary-root (:id build)))))))))
(t/deftest category-purge-unlinks-only-known-outputs-and-keeps-directories
(doseq [category [:builds :runs :all]]
(let [root (fs/mkdtempSync (path/join @temporary-root (str (name category) "-")))
building (complete-files (artifacts/create-build root selection provenance))
failed (artifacts/create-build root selection provenance)
managed (artifacts/managed-root root)
run-dir (path/join managed "runs" "group" "saved-run")
outputs ["build.transit" "build.log" "cases.cjs" "index.html"
"browser/browser.js" "renderer/render-wasm.js"
"renderer/render-wasm.wasm" "renderer/render-wasm.wasm.map"
"renderer/shared.js" "renderer/cargo-build.json"
"renderer/worker/render.js"]
boot (path/join managed "runner.cjs")
outside (path/join root "explicit-output" "run.transit")]
(artifacts/write-manifest failed (assoc (:manifest failed) :status :failed))
(doseq [file outputs]
(write-file (path/join (:dir building) file) "build output"))
(doseq [file ["run.log" "run.transit" "shot.png" "SHOT.PNG"]]
(write-file (path/join run-dir file) "run output"))
(doseq [dir ["run.log" "run.transit" "directory.PNG"]]
(write-file (path/join managed "runs" dir "keep.txt") "keep directory"))
(write-file (path/join managed "runs" "no-marker" "only.PNG") "screenshot")
(write-file (path/join run-dir "record.transit") "unrelated run file")
(write-file (path/join (:dir building) "custom" "build.log") "unrelated build file")
(write-file boot "boot executable")
(write-file outside "unmanaged")
(artifacts/purge root category)
(doseq [dir [(:dir building) (:dir failed) (:browser-dir building)
(:renderer-dir building) run-dir]]
(t/is (.isDirectory (fs/statSync dir))))
(doseq [file outputs]
(t/is (= (= category :runs) (fs/existsSync (path/join (:dir building) file)))))
(t/is (= (= category :runs) (fs/existsSync (:manifest-path failed))))
(doseq [file ["run.log" "run.transit" "shot.png" "SHOT.PNG"]]
(t/is (= (= category :builds) (fs/existsSync (path/join run-dir file)))))
(t/is (= (= category :builds)
(fs/existsSync (path/join managed "runs" "no-marker" "only.PNG"))))
(doseq [dir ["run.log" "run.transit" "directory.PNG"]]
(t/is (fs/existsSync (path/join managed "runs" dir "keep.txt"))))
(t/is (fs/existsSync (path/join run-dir "record.transit")))
(t/is (fs/existsSync (path/join (:dir building) "custom" "build.log")))
(t/is (= "boot executable" (fs/readFileSync boot "utf8")))
(t/is (= "unmanaged" (fs/readFileSync outside "utf8")))
(artifacts/purge root category))))
(t/deftest comparison-purge-unlinks-only-feature-comparison-evidence
(doseq [category [:comparisons :all]]
(let [root (fs/mkdtempSync (path/join @temporary-root (str (name category) "-")))
managed (artifacts/managed-root root)
build-file (path/join managed "builds" "saved" "build.transit")
ab-dir (path/join managed "feature-comparisons" "ab-1")
config-dir (path/join ab-dir "configs" "00")
edge-dir (path/join ab-dir "comparisons")
run-dir (path/join managed "runs" "saved")
outside (path/join root "unmanaged" "feature-comparison.transit")
outputs [["feature-comparison.log" ab-dir]
["feature-comparison.transit" ab-dir]
["run.log" config-dir]
["run.transit" config-dir]
["shot.PNG" config-dir]
["00__10.transit" edge-dir]]
kept [["notes.txt" ab-dir]
["keep.txt" config-dir]
["keep.txt" edge-dir]]]
(doseq [[file dir] outputs
:let [path (path/join dir file)]]
(write-file path "comparison output"))
(doseq [[file dir] kept]
(write-file (path/join dir file) "unrelated"))
(write-file (path/join managed "feature-comparisons" "keep.txt")
"unrelated root")
(write-file (path/join managed "feature-comparisons" "ab-1" "directory.PNG"
"keep.txt")
"keep directory")
(write-file build-file "retained build")
(write-file (path/join run-dir "run.transit") "standalone run")
(write-file outside "unmanaged")
(artifacts/purge root category)
(doseq [[file dir] outputs]
(t/is (not (fs/existsSync (path/join dir file))) (str category " " file)))
(doseq [[file dir] kept]
(t/is (fs/existsSync (path/join dir file)) (str category " " file)))
(t/is (= "unrelated root"
(fs/readFileSync (path/join managed "feature-comparisons" "keep.txt")
"utf8")))
(t/is (fs/existsSync (path/join ab-dir "directory.PNG" "keep.txt")))
(t/is (.isDirectory (fs/statSync ab-dir)))
(t/is (.isDirectory (fs/statSync config-dir)))
(t/is (.isDirectory (fs/statSync edge-dir)))
(t/is (= (= category :comparisons)
(fs/existsSync (path/join run-dir "run.transit")))
(str category " standalone run"))
(t/is (= (= category :comparisons) (fs/existsSync build-file))
(str category " retained build"))
(when (= category :comparisons)
(t/is (= "standalone run"
(fs/readFileSync (path/join run-dir "run.transit") "utf8")))
(t/is (= "retained build" (fs/readFileSync build-file "utf8"))))
(t/is (= "unmanaged" (fs/readFileSync outside "utf8"))))))
(t/deftest purge-follows-category-and-nested-directory-symlinks-and-stops-cycles
(let [managed (artifacts/managed-root @temporary-root)]
(fs/mkdirSync managed #js {:recursive true})
(doseq [category [:builds :runs]]
(let [outside (path/join @temporary-root (str "outside-" (name category)))
detached (path/join @temporary-root (str "detached-" (name category)))
output (if (= category :builds) "build.transit" "run.transit")
saved (path/join outside "keep.txt")
link (path/join managed (name category))]
(write-file saved "keep symlink target")
(write-file (path/join outside "saved" output) "output, not a parsed record")
(write-file (path/join detached output) "linked output")
(write-file (path/join detached "keep.txt") "unrelated")
(fs/symlinkSync outside (path/join outside "saved" "cycle") "dir")
(fs/symlinkSync detached (path/join outside "linked") "dir")
(fs/symlinkSync outside link "dir")
(artifacts/purge @temporary-root category)
(t/is (.isSymbolicLink (fs/lstatSync link)))
(t/is (.isSymbolicLink (fs/lstatSync (path/join outside "saved" "cycle"))))
(t/is (.isSymbolicLink (fs/lstatSync (path/join outside "linked"))))
(t/is (not (fs/existsSync (path/join outside "saved" output))))
(t/is (not (fs/existsSync (path/join detached output))))
(t/is (fs/existsSync (path/join detached "keep.txt")))
(t/is (= "keep symlink target" (fs/readFileSync saved "utf8")))))))
(t/deftest managed-root-symlink-cannot-create-or-delete-outside-the-frontend
(let [root (path/join @temporary-root "frontend")
managed (path/join root "target" "performance" "render-wasm")
outside (path/join @temporary-root "outside-managed")
build-log (path/join outside "builds" "retained" "build.log")
run-file (path/join outside "runs" "retained" "record.transit")]
(write-file build-log "outside build")
(write-file run-file "outside run")
(fs/mkdirSync (path/dirname managed) #js {:recursive true})
(fs/symlinkSync outside managed "dir")
(t/is (= ::artifacts/unsafe-path
(:type (ex-data (failure #(artifacts/managed-root root))))))
(t/is (= ::artifacts/unsafe-path
(:type (ex-data (failure #(artifacts/create-build root selection provenance))))))
(t/is (= ::artifacts/unsafe-path
(:type (ex-data (failure #(artifacts/purge root :all))))))
(t/is (= ::artifacts/unsafe-path
(:type (ex-data (failure #(artifacts/remove-builds root ["retained"]))))))
(t/is (= "outside build" (fs/readFileSync build-log "utf8")))
(t/is (= "outside run" (fs/readFileSync run-file "utf8")))
(t/is (= #{"retained"} (set (array-seq (fs/readdirSync (path/join outside "builds"))))))))
(t/deftest escaping-target-ancestor-is-rejected-before-the-managed-path-exists
(let [root (path/join @temporary-root "frontend-ancestor")
outside (path/join @temporary-root "outside-target")]
(fs/mkdirSync root)
(fs/mkdirSync outside)
(fs/symlinkSync outside (path/join root "target") "dir")
(t/is (= ::artifacts/unsafe-path
(:type (ex-data (failure #(artifacts/create-build root selection provenance))))))
(t/is (empty? (array-seq (fs/readdirSync outside))))))
(t/deftest create-build-rejects-an-escaping-builds-parent-before-writing-files
(let [managed (artifacts/managed-root @temporary-root)
outside (path/join @temporary-root "outside-builds")
saved (path/join outside "retained" "build.log")]
(write-file saved "keep external build")
(fs/mkdirSync managed #js {:recursive true})
(fs/symlinkSync outside (path/join managed "builds") "dir")
(t/is (= ::artifacts/unsafe-path
(:type (ex-data (failure #(artifacts/create-build
@temporary-root selection provenance))))))
(t/is (= "keep external build" (fs/readFileSync saved "utf8")))
(t/is (= #{"retained"} (set (array-seq (fs/readdirSync outside)))))))
(t/deftest remove-builds-cleans-selected-ids-and-keeps-directories-and-unrelated-files
(let [removed (ready-build)
kept (ready-build)
managed (artifacts/managed-root @temporary-root)
run (path/join managed "runs" "saved" "run.transit")
outside (path/join @temporary-root "outside" "keep.txt")
browser (path/join @temporary-root "linked-browser")
link (path/join managed "builds" "outside-link")]
(write-file run "keep run")
(write-file outside "keep target")
(write-file (path/join (:dir removed) "screenshot.PNG") "keep build screenshot")
(write-file (path/join (path/dirname outside) "build.transit") "linked build output")
(write-file (path/join browser "browser.js") "linked browser output")
(write-file (path/join browser "keep.js") "unrelated")
(fs/symlinkSync browser (path/join (path/dirname outside) "browser") "dir")
(fs/symlinkSync (path/dirname outside) link "dir")
(artifacts/remove-builds @temporary-root [(:id removed) "outside-link" "absent-build"])
(t/is (.isDirectory (fs/statSync (:dir removed))))
(t/is (not (fs/existsSync (:manifest-path removed))))
(t/is (not (fs/existsSync (:cases-path removed))))
(t/is (fs/existsSync (path/join (:dir removed) "screenshot.PNG")))
(t/is (.isSymbolicLink (fs/lstatSync link)))
(t/is (not (fs/existsSync (path/join link "build.transit"))))
(t/is (not (fs/existsSync (path/join browser "browser.js"))))
(t/is (fs/existsSync (path/join browser "keep.js")))
(t/is (= kept (artifacts/load-build @temporary-root (:id kept))))
(t/is (= "keep run" (fs/readFileSync run "utf8")))
(t/is (= "keep target" (fs/readFileSync outside "utf8")))))
(t/deftest inside-builds-resolves-symlink-ancestors-with-missing-output-components
(let [managed (artifacts/managed-root @temporary-root)
builds (path/join managed "builds")
outside (path/join @temporary-root "external-builds")
alias (path/join @temporary-root "build-alias")]
(t/is (artifacts/inside-builds? @temporary-root (path/join builds "future" "run")))
(fs/mkdirSync managed #js {:recursive true})
(fs/mkdirSync outside)
(fs/symlinkSync outside builds "dir")
(fs/symlinkSync builds alias "dir")
(doseq [output [builds outside (path/join builds "new" "run")
(path/join outside "new" "run") (path/join alias "new" "run")]]
(t/is (artifacts/inside-builds? @temporary-root output)))
(t/is (not (artifacts/inside-builds? @temporary-root (str outside "-neighbor/run"))))
(t/is (not (artifacts/inside-builds? @temporary-root (path/join managed "runs" "new"))))))
(defn- descriptor
[id]
{:id id
:scene :artifact-only
:scene-description "A descriptor supplied only by this build"
:scene-seed 42
:params {}
:view {:scale 1 :x 0 :y 0}
:context :fresh
:completion :render-full
:batch-size 1
:preparation {}})
(defn- write-catalog
[build loaded-id response]
(write-file
(:cases-path build)
(str "const identity = " (js/JSON.stringify (codec/encode-str {:build-id loaded-id})) ";"
"const response = " (js/JSON.stringify (codec/encode-str response)) ";"
"module.exports = {buildIdentity() {return identity;},"
"collectCases(encodedSelection) {return response;}};")))
(t/deftest catalogs-use-the-selected-commonjs-module-with-isolated-scope
(let [first-build (ready-build)
second-build (ready-build)
first-case (descriptor :artifact-only/first)
second-case (descriptor :artifact-only/second)
selection {:master-seed 42}]
(write-catalog first-build (:id first-build)
{:build-id (:id first-build) :cases [first-case]})
(write-catalog second-build (:id second-build)
{:build-id (:id second-build) :cases [second-case]})
(t/is (= [first-case] (catalog/collect first-build selection)))
(t/is (= [second-case] (catalog/collect second-build selection)))
(t/is (= [first-case] (catalog/collect first-build selection)))))
(t/deftest catalog-rejects-both-loaded-and-response-identity-mismatches
(doseq [boundary [:loaded :response]]
(let [build (ready-build)]
(write-catalog build (if (= boundary :loaded) "wrong-build" (:id build))
{:build-id (if (= boundary :response) "wrong-build" (:id build))
:cases [(descriptor :artifact-only/load)]})
(t/is (= ::identity/mismatch
(:type (ex-data (failure #(catalog/collect build {:master-seed 42})))))))))
(t/deftest catalog-rejects-malformed-descriptors-and-empty-responses
(doseq [cases [[(assoc (descriptor :artifact-only/load) :scene-seed -1)] [] nil]]
(let [build (ready-build)]
(write-catalog build (:id build) {:build-id (:id build) :cases cases})
(t/is (some? (failure #(catalog/collect build {:master-seed 42})))))))