This commit is contained in:
@@ -0,0 +1,161 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { ChildProcess, spawn } from "node:child_process";
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import test from "node:test";
|
||||
import { BuildEngine } from "./buildEngine";
|
||||
import {
|
||||
commit,
|
||||
createRepository,
|
||||
loadFixture,
|
||||
} from "./testing/repositoryFixture";
|
||||
|
||||
const waitForExit = (child: ChildProcess): Promise<string> =>
|
||||
new Promise((resolve, reject) => {
|
||||
let stdout = "";
|
||||
let stderr = "";
|
||||
child.stdout?.on("data", (chunk) => {
|
||||
stdout += chunk;
|
||||
});
|
||||
child.stderr?.on("data", (chunk) => {
|
||||
stderr += chunk;
|
||||
});
|
||||
child.on("error", reject);
|
||||
child.on("exit", (code) => {
|
||||
if (code === 0) resolve(stdout.trim());
|
||||
else reject(new Error(`worker exited ${code}: ${stderr}`));
|
||||
});
|
||||
});
|
||||
|
||||
const waitFor = async (predicate: () => boolean): Promise<void> => {
|
||||
const deadline = Date.now() + 10_000;
|
||||
while (!predicate()) {
|
||||
if (Date.now() > deadline) throw new Error("Timed out waiting for workers.");
|
||||
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||
}
|
||||
};
|
||||
|
||||
test("concurrent writers converge on one verified immutable release", async () => {
|
||||
const root = createRepository();
|
||||
const output = fs.mkdtempSync(path.join(os.tmpdir(), "concurrent-release-"));
|
||||
const barrier = path.join(output, "barrier");
|
||||
const worker = path.resolve("src/backend/testing/concurrentBuildWorker.ts");
|
||||
try {
|
||||
const children = [1, 2].map(() =>
|
||||
spawn(process.execPath, ["--import", "tsx", worker, root, output, barrier], {
|
||||
cwd: process.cwd(),
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
}),
|
||||
);
|
||||
const completions = children.map(waitForExit);
|
||||
await waitFor(
|
||||
() =>
|
||||
fs
|
||||
.readdirSync(output)
|
||||
.filter((entry) => entry.startsWith("barrier.") && entry.endsWith(".ready"))
|
||||
.length === 2,
|
||||
);
|
||||
fs.writeFileSync(`${barrier}.go`, "");
|
||||
const checksums = await Promise.all(completions);
|
||||
assert.equal(new Set(checksums).size, 1);
|
||||
|
||||
const releases = fs.readdirSync(path.join(output, "releases"));
|
||||
assert.equal(releases.length, 1);
|
||||
new BuildEngine(output).verifyRelease(releases[0]);
|
||||
assert.equal(
|
||||
fs.readdirSync(output).some((entry) => entry.startsWith(".release-")),
|
||||
false,
|
||||
);
|
||||
} finally {
|
||||
fs.rmSync(root, { recursive: true, force: true });
|
||||
fs.rmSync(output, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("partial rendering failure leaves no release or temporary directory", () => {
|
||||
const root = createRepository();
|
||||
const output = fs.mkdtempSync(path.join(os.tmpdir(), "partial-release-"));
|
||||
try {
|
||||
const template = path.join(
|
||||
root,
|
||||
"packages/site-definition/.theme/templates/home.liquid",
|
||||
);
|
||||
fs.appendFileSync(template, "\n{{ undefined.value }}\n");
|
||||
commit(root, "break rendering");
|
||||
const fixture = loadFixture(root, "partial-failure");
|
||||
try {
|
||||
const engine = new BuildEngine(output);
|
||||
assert.throws(() => engine.build(fixture.input), /undefined|not defined/i);
|
||||
assert.equal(fs.existsSync(engine.buildDirectory(fixture.input.artifactBuildId)), false);
|
||||
assert.equal(
|
||||
fs.readdirSync(output).some((entry) => entry.startsWith(".release-")),
|
||||
false,
|
||||
);
|
||||
} finally {
|
||||
fixture.manager.dispose();
|
||||
}
|
||||
} finally {
|
||||
fs.rmSync(root, { recursive: true, force: true });
|
||||
fs.rmSync(output, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("staging and current pointers promote only checksum-verified releases", () => {
|
||||
const root = createRepository();
|
||||
const output = fs.mkdtempSync(path.join(os.tmpdir(), "promotion-release-"));
|
||||
const first = loadFixture(root, "first");
|
||||
try {
|
||||
const engine = new BuildEngine(output);
|
||||
const firstResult = engine.build(first.input);
|
||||
engine.stage(first.input.artifactBuildId);
|
||||
engine.activate(first.input.artifactBuildId);
|
||||
|
||||
const css = path.join(
|
||||
root,
|
||||
"packages/site-definition/.theme/assets/styles/theme.css",
|
||||
);
|
||||
fs.appendFileSync(css, "\n.second-release{display:block}\n");
|
||||
commit(root, "second release");
|
||||
const second = loadFixture(root, "second");
|
||||
try {
|
||||
const secondResult = engine.build(second.input);
|
||||
engine.stage(second.input.artifactBuildId);
|
||||
assert.equal(path.basename(fs.realpathSync(engine.currentDirectory())), first.input.artifactBuildId);
|
||||
assert.equal(path.basename(fs.realpathSync(engine.stagingDirectory())), second.input.artifactBuildId);
|
||||
|
||||
engine.activate(second.input.artifactBuildId);
|
||||
assert.equal(path.basename(fs.realpathSync(engine.currentDirectory())), second.input.artifactBuildId);
|
||||
assert.equal(path.basename(fs.realpathSync(engine.stagingDirectory())), second.input.artifactBuildId);
|
||||
|
||||
fs.appendFileSync(path.join(firstResult.outputDirectory, "index.html"), "tamper");
|
||||
assert.throws(() => engine.activate(first.input.artifactBuildId), /E_RELEASE_CHECKSUM/);
|
||||
assert.equal(path.basename(fs.realpathSync(engine.currentDirectory())), second.input.artifactBuildId);
|
||||
assert.ok(fs.existsSync(secondResult.outputDirectory));
|
||||
} finally {
|
||||
second.manager.dispose();
|
||||
}
|
||||
} finally {
|
||||
first.manager.dispose();
|
||||
fs.rmSync(root, { recursive: true, force: true });
|
||||
fs.rmSync(output, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("repeated identical builds retain identical artifact checksums", () => {
|
||||
const root = createRepository();
|
||||
const output = fs.mkdtempSync(path.join(os.tmpdir(), "idempotent-release-"));
|
||||
const fixture = loadFixture(root, "idempotent");
|
||||
try {
|
||||
const engine = new BuildEngine(output);
|
||||
const checksums = Array.from(
|
||||
{ length: 20 },
|
||||
() => engine.build(fixture.input).artifactChecksum,
|
||||
);
|
||||
assert.equal(new Set(checksums).size, 1);
|
||||
} finally {
|
||||
fixture.manager.dispose();
|
||||
fs.rmSync(root, { recursive: true, force: true });
|
||||
fs.rmSync(output, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user