mirror of
https://github.com/penpot/penpot.git
synced 2026-10-28 21:40:34 +00:00
🚧 WIP 25.2 Safer deletion and symlink handling
AI-assisted-by: deepseek-flash, gpt-6.1-sol
This commit is contained in:
parent
c14ca9e508
commit
987fc38e6f
@ -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)))))
|
||||
|
||||
@ -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`,
|
||||
|
||||
@ -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"))
|
||||
|
||||
@ -9,8 +9,8 @@
|
||||
|
||||
Runs are saved under <frontend>/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))})))
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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") "<!doctype 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 (= "<!doctype html>"
|
||||
(fs/readFileSync (path/join dir "index.html") "utf8")))))
|
||||
|
||||
@ -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"))))))
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user