diff --git a/frontend/performance/benches/render_wasm/node/artifacts.cljs b/frontend/performance/benches/render_wasm/node/artifacts.cljs index bb3b76df2a..870dc215b4 100644 --- a/frontend/performance/benches/render_wasm/node/artifacts.cljs +++ b/frontend/performance/benches/render_wasm/node/artifacts.cljs @@ -89,6 +89,12 @@ (recur (path/dirname existing)))) managed)) +(defn inside-builds? + "Checks the whole builds tree, resolving symlink ancestors of both paths." + [frontend-root output-path] + (contained? (files/get-real-path (path/join (managed-root frontend-root) "builds")) + (files/get-real-path output-path))) + (defn- build-reference [root id manifest] (let [dir (path/join (managed-root root) "builds" id)] @@ -210,31 +216,77 @@ {:type ::not-ready :id id :status (:status manifest)}))) (check-files (assoc build :manifest manifest)))) +(def ^:private build-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"]) + +(defn- directory? + [file] + (and (fs/existsSync file) (.isDirectory (fs/statSync file)))) + +(defn- unlink-if-present + [file] + (when-not (directory? file) + (try + (fs/unlinkSync file) + (catch :default error + (when-not (contains? #{"ENOENT" "ENOTDIR"} (.-code error)) + (throw error)))))) + +(defn- clean-build + [dir] + (when (directory? dir) + (doseq [file build-outputs] + (unlink-if-present (path/join dir file))))) + +(defn- purge-builds + [root] + (when (directory? root) + (doseq [id (array-seq (fs/readdirSync root))] + (clean-build (path/join root id))))) + +(defn- purge-runs + [root] + (loop [pending [root] + visited #{}] + (when-let [dir (peek pending)] + (let [pending (pop pending) + real (when (directory? dir) (fs/realpathSync dir))] + (if (or (nil? real) (contains? visited real)) + (recur pending visited) + (do + (unlink-if-present (path/join dir "run.log")) + (let [names (array-seq (fs/readdirSync dir))] + (doseq [file names + :when (str/ends-with? (str/lower-case file) ".png")] + (unlink-if-present (path/join dir file))) + (unlink-if-present (path/join dir "run.transit")) + (recur (into pending (map #(path/join dir %) names)) + (conj visited real))))))))) + (defn purge - "Deletes :builds, :runs or both (:all), including unfinished builds. + "Unlinks known outputs from :builds, :runs or both (:all). The caller must be idle: no build, server or run may still own these paths. - Boot executables and other files beside the managed subroots stay in place. - A selected subroot symlink is removed without deleting its target." + Follows directory symlinks; keeps directories and unrelated files." [root category] (let [managed (managed-root root)] (case category - :builds (fs/rmSync (path/join managed "builds") #js {:recursive true :force true}) - :runs (fs/rmSync (path/join managed "runs") #js {:recursive true :force true}) + :builds (purge-builds (path/join managed "builds")) + :runs (purge-runs (path/join managed "runs")) :all (do - (fs/rmSync (path/join managed "builds") #js {:recursive true :force true}) - (fs/rmSync (path/join managed "runs") #js {:recursive true :force true})) + (purge-builds (path/join managed "builds")) + (purge-runs (path/join managed "runs"))) (throw (ex-info "invalid renderer artifact category" {:type ::invalid-category :category category}))))) (defn remove-builds - "Deletes only the selected build IDs. The caller must be idle." + "Unlinks known outputs of selected build IDs. The caller must be idle." [root ids] (let [ids (mapv check-id ids) managed (managed-root root) builds (path/join managed "builds")] - (when (fs/existsSync builds) - (checked-path (fs/realpathSync managed) builds) - (doseq [id ids] - ;; rmSync removes a final symlink itself, not its target. - (fs/rmSync (path/join builds id) #js {:recursive true :force true}))))) + (doseq [id ids] + (clean-build (path/join builds id))))) diff --git a/frontend/performance/benches/render_wasm/node/files.cljs b/frontend/performance/benches/render_wasm/node/files.cljs index d4e006bc5b..d6a9ebe26e 100644 --- a/frontend/performance/benches/render_wasm/node/files.cljs +++ b/frontend/performance/benches/render_wasm/node/files.cljs @@ -7,7 +7,16 @@ (ns benches.render-wasm.node.files "Small filesystem helpers shared by benchmark storage writers." (:require - ["fs" :as fs])) + ["fs" :as fs] + ["path" :as path])) + +(defn get-real-path + "Resolves symlink ancestors even when the final path does not exist yet." + [file] + (let [file (path/resolve file)] + (if (fs/existsSync file) + (fs/realpathSync file) + (path/join (get-real-path (path/dirname file)) (path/basename file))))) (defn atomic-write "Writes (UTF-8) content to a temporary file and renames it over `file`, diff --git a/frontend/performance/benches/render_wasm/node/run.cljs b/frontend/performance/benches/render_wasm/node/run.cljs index c5df581063..e65e8978fb 100644 --- a/frontend/performance/benches/render_wasm/node/run.cljs +++ b/frontend/performance/benches/render_wasm/node/run.cljs @@ -646,18 +646,14 @@ (dissoc :screenshots :remove))) (defn ^:async execute - "Executes a prepared selection and owns the run home, host and browser. + "Executes a prepared selection in an allocated home and owns host and browser. Options are resolved CLI options. root is the frontend checkout. The record exists before on-created runs, so a browser startup failure still leaves a valid partial record. Returns {:run RunRecord :home home :exit-code n :error cause-or-nil}." - [root selected-build cases options signal on-progress on-created] - (let [opts (-> (normalize-options options) - (assoc :run-id (or (:run-id options) (str (random-uuid))))) - home (runs/create-home {:root root - :run-id (:run-id opts) - :output-dir (:output-dir opts)}) + [root home selected-build cases options signal on-progress on-created] + (let [opts (assoc (normalize-options options) :run-id (:run-id home)) run (result/create-run {:run-id (:run-id opts) :started-at (now) @@ -748,13 +744,13 @@ (defn ^:async run "Prepares or loads a build, collects matching cases and executes one run. - User output paths resolve before any build work. A build or catalog failure - before the plan exists creates no run record and rethrows with build facts." + The output home is reserved before build work so filesystem errors fail fast. + A build or catalog failure leaves the home without a run record and rethrows + with build facts." [root options signal on-progress on-created] - (let [opts (normalize-options options) - output-dir (runs/check-output-dir (:output-dir opts)) - run-id (str (random-uuid)) - opts (assoc opts :run-id run-id :output-dir output-dir)] + (let [opts (normalize-options options) + home (runs/create-home {:root root :run-id (str (random-uuid)) + :output-dir (:output-dir opts) :remove (:remove? opts)})] (try (let [selected-build (if-let [id (:build-id opts)] @@ -779,7 +775,7 @@ :build-id (:id selected-build) :log-path (:log-path selected-build)) cause))))] - (await (execute root selected-build cases opts signal on-progress on-created))) + (await (execute root home selected-build cases opts signal on-progress on-created))) (catch :default cause (when on-progress (on-progress (str "run: " (message-of cause) "\n")) diff --git a/frontend/performance/benches/render_wasm/node/runs.cljs b/frontend/performance/benches/render_wasm/node/runs.cljs index 00480a1852..dfce353b1a 100644 --- a/frontend/performance/benches/render_wasm/node/runs.cljs +++ b/frontend/performance/benches/render_wasm/node/runs.cljs @@ -9,8 +9,8 @@ Runs are saved under /target/performance/render-wasm/runs. - This can be overriden in the runner with `--output-dir PATH`. If done, this - path is not included among those purged when cleaning up." + --output-dir selects another destination. Purge cleans files reachable from + the default runs tree, including explicit destinations and directory links." (:require ["fs" :as fs] ["path" :as path] @@ -37,7 +37,7 @@ (when (str/blank? dir) (invalid-home "output directory requires a nonempty path" {})) (let [resolved (path/resolve dir)] - (when (fs/existsSync resolved) + (when (fs/lstatSync resolved #js {:throwIfNoEntry false}) (invalid-home "output directory already exists" {:path resolved})) resolved))) @@ -45,18 +45,16 @@ "Creates one run home and its empty log. The record is written separately after the plan exists. An explicit directory must not exist; a managed home uses the frontend root regardless of cwd." - [{:keys [root run-id output-dir]}] + [{:keys [root run-id output-dir remove]}] (check-id run-id) - (let [explicit (check-output-dir output-dir) - managed (when-not explicit (artifacts/managed-root root)) - runs-dir (when managed (path/join managed "runs")) - dir (or explicit (path/join runs-dir run-id)) + (let [dir (check-output-dir + (or output-dir (path/join (artifacts/managed-root root) "runs" run-id))) record-path (path/join dir "run.transit") log-path (path/join dir "run.log")] - (when managed - (fs/mkdirSync runs-dir #js {:recursive true})) + (when (and remove (artifacts/inside-builds? root dir)) + (invalid-home "--remove cannot use an output directory inside builds" {:path dir})) (try - (fs/mkdirSync dir) + (fs/mkdirSync dir #js {:recursive true}) (catch :default cause (invalid-home "cannot create run home" {:path dir :cause (or (ex-message cause) (str cause))}))) diff --git a/frontend/test/performance_tests/render_wasm/artifacts_test.cljs b/frontend/test/performance_tests/render_wasm/artifacts_test.cljs index 98e2a22321..39fb7da204 100644 --- a/frontend/test/performance_tests/render_wasm/artifacts_test.cljs +++ b/frontend/test/performance_tests/render_wasm/artifacts_test.cljs @@ -257,38 +257,74 @@ (t/is (= ::artifacts/unsafe-path (:type (ex-data (failure #(artifacts/load-build @temporary-root (:id build))))))))) -(t/deftest category-purge-deletes-only-the-requested-managed-subroots +(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 (artifacts/create-build root selection provenance) + building (complete-files (artifacts/create-build root selection provenance)) failed (artifacts/create-build root selection provenance) managed (artifacts/managed-root root) - run-file (path/join managed "runs" "saved-run" "record.transit") + 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 "unmanaged.txt")] + outside (path/join root "explicit-output" "run.transit")] (artifacts/write-manifest failed (assoc (:manifest failed) :status :failed)) - (write-file run-file "retained run") + (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) - (t/is (= (= category :runs) (fs/existsSync (:dir building)))) - (t/is (= (= category :runs) (fs/existsSync (:dir failed)))) - (t/is (= (= category :builds) (fs/existsSync run-file))) + (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 purge-removes-selected-subroot-symlinks-without-deleting-their-targets +(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 (not (fs/existsSync link))) + (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 @@ -336,23 +372,51 @@ (t/is (= "keep external build" (fs/readFileSync saved "utf8"))) (t/is (= #{"retained"} (set (array-seq (fs/readdirSync outside))))))) -(t/deftest remove-builds-keeps-other-builds-runs-and-symlink-targets - (let [removed (create-build) +(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" "record.transit") + 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 (not (fs/existsSync (:dir removed)))) - (t/is (not (fs/existsSync link))) + (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 diff --git a/frontend/test/performance_tests/render_wasm/cli_integration.cjs b/frontend/test/performance_tests/render_wasm/cli_integration.cjs index 3eed938318..f8c204ba78 100644 --- a/frontend/test/performance_tests/render_wasm/cli_integration.cjs +++ b/frontend/test/performance_tests/render_wasm/cli_integration.cjs @@ -162,6 +162,7 @@ function seedBuilds(fixture) { manifestText(id, status, "renderer-build"), ); fs.writeFileSync(path.join(directory, "build.log"), `${status} build\n`); + fs.writeFileSync(path.join(directory, "keep.txt"), "keep\n"); } } @@ -174,6 +175,9 @@ function seedRuns(fixture) { path.join(directory, "run.transit"), manifestText(id, status, "renderer-run"), ); + fs.writeFileSync(path.join(directory, "run.log"), "run\n"); + fs.writeFileSync(path.join(directory, "case-load.PNG"), "screenshot\n"); + fs.writeFileSync(path.join(directory, "keep.txt"), "keep\n"); } } @@ -399,10 +403,16 @@ test("purge deletes only the selected artifacts", testOptions, async (t) => { stdout: "", stderr: "", }); - assert.equal(fs.existsSync(fixture.builds), false); + for (const id of fs.readdirSync(fixture.builds)) { + assert.deepEqual(fs.readdirSync(path.join(fixture.builds, id)), [ + "keep.txt", + ]); + } assert.deepEqual(tree(fixture.runs), runs); assert.deepEqual( - tree(fixture.managed).filter(([name]) => name !== "runs"), + tree(fixture.managed).filter( + ([name]) => !["runs", "builds"].includes(name), + ), retained, ); @@ -413,10 +423,14 @@ test("purge deletes only the selected artifacts", testOptions, async (t) => { stdout: "", stderr: "", }); - assert.equal(fs.existsSync(fixture.runs), false); + for (const id of fs.readdirSync(fixture.runs)) { + assert.deepEqual(fs.readdirSync(path.join(fixture.runs, id)), ["keep.txt"]); + } assert.deepEqual(tree(fixture.builds), builds); assert.deepEqual( - tree(fixture.managed).filter(([name]) => name !== "builds"), + tree(fixture.managed).filter( + ([name]) => !["runs", "builds"].includes(name), + ), retained, ); @@ -426,11 +440,110 @@ test("purge deletes only the selected artifacts", testOptions, async (t) => { stdout: "", stderr: "", }); - assert.equal(fs.existsSync(fixture.builds), false); - assert.equal(fs.existsSync(fixture.runs), false); - assert.deepEqual(tree(fixture.managed), retained); + for (const root of [fixture.builds, fixture.runs]) { + for (const id of fs.readdirSync(root)) { + assert.deepEqual(fs.readdirSync(path.join(root, id)), ["keep.txt"]); + } + } + assert.deepEqual( + tree(fixture.managed).filter( + ([name]) => !["runs", "builds"].includes(name), + ), + retained, + ); }); +test( + "run reserves nested output before the compiler starts", + testOptions, + async (t) => { + const fixture = createFrontend(t); + const output = path.join(fixture.managed, "missing", "parents", "output"); + const marker = path.join(fixture.managed, "compiler-ran"); + fs.writeFileSync( + fixture.compiler, + `#!/usr/bin/env node +const fs = require("node:fs"); +if (!fs.statSync(${JSON.stringify(path.join(output, "run.log"))}).isFile()) process.exit(99); +fs.writeFileSync(${JSON.stringify(marker)}, "reserved"); +process.exit(23); +`, + { mode: 0o755 }, + ); + + const result = await runCli(fixture, ["run", "--output-dir", output]); + + assert.equal(result.exitCode, 1); + assert.equal(result.stdout, ""); + assert.equal(fs.readFileSync(marker, "utf8"), "reserved"); + assert.equal(fs.existsSync(path.join(output, "run.transit")), false); + }, +); + +test( + "run rejects invalid output parents before invoking the compiler", + testOptions, + async (t) => { + const fixture = createFrontend(t); + const parent = path.join(fixture.managed, "not-a-directory"); + const marker = path.join(fixture.managed, "compiler-ran"); + fs.writeFileSync(parent, "keep"); + fs.writeFileSync( + fixture.compiler, + `#!/usr/bin/env node +require("node:fs").writeFileSync(${JSON.stringify(marker)}, "ran"); +process.exit(23); +`, + { mode: 0o755 }, + ); + + const result = await runCli(fixture, [ + "run", + "--output-dir", + path.join(parent, "output"), + ]); + + assert.equal(result.exitCode, 1); + assert.match(result.stderr, /ENOTDIR|cannot create run home/); + assert.equal(fs.existsSync(marker), false); + assert.equal(fs.readFileSync(parent, "utf8"), "keep"); + }, +); + +test( + "run rejects --remove outputs inside builds including directory aliases", + testOptions, + async (t) => { + const fixture = createFrontend(t); + const marker = path.join(fixture.managed, "compiler-ran"); + const alias = path.join(path.dirname(fixture.managed), "build-alias"); + fs.mkdirSync(fixture.builds); + fs.symlinkSync(fixture.builds, alias, "dir"); + fs.writeFileSync( + fixture.compiler, + `#!/usr/bin/env node +require("node:fs").writeFileSync(${JSON.stringify(marker)}, "ran"); +process.exit(23); +`, + { mode: 0o755 }, + ); + + for (const root of [fixture.builds, alias]) { + const output = path.join(root, "missing", "output"); + const result = await runCli(fixture, [ + "run", + "--remove", + "--output-dir", + output, + ]); + assert.equal(result.exitCode, 1); + assert.match(result.stderr, /--remove.*inside builds/); + assert.equal(fs.existsSync(output), false); + } + assert.equal(fs.existsSync(marker), false); + }, +); + test( "malformed purge categories leave all files intact", testOptions, diff --git a/frontend/test/performance_tests/render_wasm/run_test.cljs b/frontend/test/performance_tests/render_wasm/run_test.cljs index cac383c18f..995ec62171 100644 --- a/frontend/test/performance_tests/render_wasm/run_test.cljs +++ b/frontend/test/performance_tests/render_wasm/run_test.cljs @@ -12,6 +12,7 @@ [app.common.transit :as transit] [benches.render-wasm.node.artifacts :as artifacts] [benches.render-wasm.node.run :as run] + [benches.render-wasm.node.runs :as runs] [benches.render-wasm.result :as result] [cljs.test :as t :include-macros true] [clojure.string :as str])) @@ -181,6 +182,8 @@ (execute fake cases opts (js/AbortController.))) ([_fake cases opts controller] (run/execute (artifacts/frontend-root) + (runs/create-home {:root (artifacts/frontend-root) :run-id (:run-id opts) + :output-dir (:output-dir opts)}) (selected-build (build-dir) "test-build") cases opts (.-signal controller) nil nil))) @@ -460,12 +463,15 @@ _ (fs/writeFileSync (path/join dir "index.html") "") fake (fake-browser {}) out (path/join @temporary-root "remove-run") - result (await (run/execute root (selected-build dir id) + home (runs/create-home {:root root :run-id "remove-run" + :output-dir out :remove true}) + result (await (run/execute root home (selected-build dir id) [(collected :case/a :fresh)] (assoc (options fake out) :remove? true) (.-signal (js/AbortController.)) nil nil))] (t/is (fs/existsSync (get-in result [:home :record-path]))) - (t/is (not (fs/existsSync dir))) + (t/is (fs/existsSync dir)) + (t/is (not (fs/existsSync (path/join dir "index.html")))) (t/is (= :completed (get-in result [:run :termination :reason]))) (t/is (zero? (:exit-code result))))) @@ -488,7 +494,9 @@ ;; attempts have reached the saved record. (fs/renameSync record prior) (fs/mkdirSync record)))) - response (await (run/execute root (selected-build dir id) + home (runs/create-home {:root root :run-id "failed-write" + :output-dir out :remove true}) + response (await (run/execute root home (selected-build dir id) [(collected :case/a :fresh)] (assoc (options fake out) :remove true) (.-signal (js/AbortController.)) nil nil)) @@ -498,4 +506,5 @@ (t/is (every? zero? (vals (get-in saved [:remaining :case/a])))) (t/is (some? (:error response))) (t/is (= 1 (:exit-code response))) - (t/is (fs/existsSync dir)))) + (t/is (= "" + (fs/readFileSync (path/join dir "index.html") "utf8"))))) diff --git a/frontend/test/performance_tests/render_wasm/runs_test.cljs b/frontend/test/performance_tests/render_wasm/runs_test.cljs index 201c7a29f9..044d2c0b07 100644 --- a/frontend/test/performance_tests/render_wasm/runs_test.cljs +++ b/frontend/test/performance_tests/render_wasm/runs_test.cljs @@ -102,3 +102,29 @@ (runs/create-home {:root @temporary-root :run-id id})))) (t/is (thrown-with-msg? js/Error #"nonempty path" (runs/check-output-dir " ")))) + +(t/deftest explicit-home-creates-missing-parents + (let [out (path/join @temporary-root "missing" "parents" "run") + home (runs/create-home {:root @temporary-root :run-id "nested" + :output-dir out})] + (t/is (.isDirectory (fs/statSync out))) + (t/is (= "" (fs/readFileSync (:log-path home) "utf8"))) + (t/is (thrown-with-msg? js/Error #"already exists" + (runs/create-home {:root @temporary-root :run-id "again" + :output-dir out}))))) + +(t/deftest remove-rejects-output-inside-builds-before-creating-it + (let [builds (path/join (artifacts/managed-root @temporary-root) "builds") + alias (path/join @temporary-root "build-alias")] + (fs/mkdirSync builds #js {:recursive true}) + (fs/symlinkSync builds alias "dir") + (doseq [out [(path/join builds "direct") + (path/join alias "missing" "nested")]] + (t/is (thrown-with-msg? js/Error #"--remove.*inside builds" + (runs/create-home {:root @temporary-root :run-id "blocked" + :output-dir out :remove true}))) + (t/is (not (fs/existsSync out)))) + (let [out (path/join @temporary-root "builds-other" "allowed")] + (runs/create-home {:root @temporary-root :run-id "allowed" + :output-dir out :remove true}) + (t/is (fs/existsSync (path/join out "run.log"))))))