Files
scriptc/tests/harness/bootstrap-seed.test.ts
T
Chris Tate 5aae5531e0 Give the sanitized bootstrap seed job swap
The sanitized seed compiles the compiler's 700 MB LLVM module with one clang
process under AddressSanitizer. The compile peaks near 14 GB while the Node
process that emitted the module still holds about 6 GB, which exceeds the
runner's 16 GB of memory. The job now enlarges swap to 24 GB before it builds
the seed, as the sanitized emitter job does.
2026-10-22 14:30:37 -05:00

99 lines
4.5 KiB
TypeScript

import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { dirname, join } from "node:path";
import { load } from "js-yaml";
import { expect, test } from "vitest";
import { verifyBootstrapSeed } from "../../scripts/bootstrap-seed.mjs";
test("bootstrap artifacts require a complete seed from the same revision, lane, and path", () => {
const directory = mkdtempSync(join(tmpdir(), "scriptc-seed-contract-"));
try {
const metadata = { schema: 1, revision: "revision", sanitize: true, directory };
writeFileSync(join(directory, "bootstrap-seed.json"), JSON.stringify(metadata));
expect(() => verifyBootstrapSeed(directory, "revision", true)).toThrow("missing");
for (const path of [
"distribution/bin/scriptc",
"distribution/bin/scriptc.json",
".scriptc/distribution-seed/cli.ir.json",
".scriptc/distribution-seed/cli.ll",
".scriptc/distribution-seed/compiler.ffi.json",
]) {
const file = join(directory, path);
mkdirSync(dirname(file), { recursive: true });
writeFileSync(file, "fixture");
}
expect(() => verifyBootstrapSeed(directory, "revision", true)).not.toThrow();
expect(() => verifyBootstrapSeed(directory, "other", true)).toThrow("identity");
expect(() => verifyBootstrapSeed(directory, "revision", false)).toThrow("identity");
expect(() => verifyBootstrapSeed(directory, undefined, true)).toThrow("identity");
writeFileSync(
join(directory, "bootstrap-seed.json"),
JSON.stringify({ ...metadata, directory: "/different" }),
);
expect(() => verifyBootstrapSeed(directory, "revision", true)).toThrow("identity");
} finally {
rmSync(directory, { recursive: true, force: true });
}
});
test("CI restores each seed only into its matching instrumented bootstrap lane", () => {
const workflow = load(
readFileSync(new URL("../../.github/workflows/ci.yml", import.meta.url), "utf8"),
) as {
jobs: Record<
string,
{
needs?: string | string[];
strategy: { matrix: { flavor?: string[]; include?: { flavor: string }[] } };
env: Record<string, string>;
steps: {
uses?: string;
if?: string;
with?: Record<string, unknown>;
env?: Record<string, string>;
run?: string;
}[];
}
>;
};
const seed = workflow.jobs.bootstrap_seed!;
const checks = workflow.jobs.bootstrap!;
const dependencies = Array.isArray(checks.needs) ? checks.needs : [checks.needs];
expect(dependencies).toContain("bootstrap_seed");
expect(
seed.steps
.filter((step) => step.uses === "./.github/actions/native-packages")
.map((step) => step.with?.target),
).toEqual(["linux-x64-gnu", "wasm32-wasi"]);
for (const step of checks.steps.filter(
(step) => step.uses === "./.github/actions/native-packages",
)) {
expect(step.if).toBe("matrix.phase == 'commands'");
}
// Each bootstrap job restores the seed of a flavor the seed job builds.
const checkFlavors = new Set(checks.strategy.matrix.include?.map((job) => job.flavor));
expect([...checkFlavors].toSorted()).toEqual(seed.strategy.matrix.flavor);
expect(seed.env.SCRIPTC_SAN).toBe(checks.env.SCRIPTC_SAN);
const prepare = seed.steps.find((step) => step.run === "node scripts/bootstrap-seed.mjs")!;
const execute = checks.steps.find((step) =>
step.run?.split("\n").includes("node scripts/ci-native-bootstrap.mjs"),
)!;
// The sanitized seed's build needs more memory than the runner has: swap backs it.
const swap = seed.steps.findIndex((step) => step.run?.includes("swapon /mnt/swapfile"));
expect(seed.steps[swap]?.if).toBe("matrix.flavor == 'san'");
expect(swap).toBeLessThan(seed.steps.indexOf(prepare));
expect(prepare.env?.SCRIPTC_BOOTSTRAP_SEED_DIRECTORY).toBe("${{ runner.temp }}/native-bootstrap");
expect(execute.env?.SCRIPTC_BOOTSTRAP_SEED_DIRECTORY).toBe(
prepare.env?.SCRIPTC_BOOTSTRAP_SEED_DIRECTORY,
);
// Runner paths are available in step contexts, not job-level env.
for (const job of [seed, checks]) {
expect(Object.values(job.env).some((value) => value.includes("runner."))).toBe(false);
}
const upload = seed.steps.find((step) => step.uses === "actions/upload-artifact@v4")!;
const download = checks.steps.find((step) => step.uses === "actions/download-artifact@v4")!;
expect(upload.with?.name).toBe("bootstrap-seed-${{ matrix.flavor }}-${{ github.sha }}");
expect(download.with?.name).toBe(upload.with?.name);
expect(upload.with?.["if-no-files-found"]).toBe("error");
});