mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-22 05:59:31 +08:00
The macOS lane of pull requests and pushes to main runs the darwin suite in three slices. Each slice runs differential.test.ts over a third of the corpus while the darwin contracts run beside it. vitest runs one file's cases in sequence, so a slice's corpus time is the sum of its case times: in three green main runs, 587-891 s (plain) and 800-1,731 s (sanitized), on 3-core runners whose contracts task ends after 46-180 s. ciTestPlan now runs slice i of n as case shards 2i-1 and 2i of 2n in two vitest processes: the first beside the side queue, the second in the side queue after the darwin contracts. The halves of all slices are the case shards of 2n slices, so every program still runs once. The comment above darwinFiles now says which macOS suite each CI event runs: pull requests and pushes to main run the darwin suite, and the nightly schedule and manual runs with macos_full run the full suite.
204 lines
8.0 KiB
JavaScript
204 lines
8.0 KiB
JavaScript
import { readFileSync } from "node:fs";
|
|
|
|
// Case-sharded harnesses run in every job without Vitest file sharding.
|
|
export const effectFiles = ["tests/harness/effect.test.ts", "tests/harness/effect4.test.ts"];
|
|
export const corpusFiles = [
|
|
"tests/harness/differential.test.ts",
|
|
"tests/harness/llvm-differential.test.ts",
|
|
"tests/harness/npm.test.ts",
|
|
"tests/harness/server.test.ts",
|
|
"tests/harness/test262.test.ts",
|
|
"tests/harness/three.test.ts",
|
|
];
|
|
const coverageFile = "tests/harness/coverage.test.ts";
|
|
const cacheFile = "packages/compiler/src/backend/native-toolchain.test.ts";
|
|
// Expensive integration suites run in their own vitest process, serially
|
|
// beside the corpus, avoiding worker contention between large compilations.
|
|
export const isolatedFiles = [
|
|
["self-hosting serialization", "tests/harness/self-hosting-serialization.test.ts"],
|
|
["self-hosting", "tests/harness/self-hosting.test.ts"],
|
|
["self-hosting LLVM emitter", "tests/harness/self-hosting-llvm-emitter.test.ts"],
|
|
["fetch", "tests/harness/fetch.test.ts"],
|
|
["surface manifest", "tests/harness/surface-manifest.test.ts"],
|
|
];
|
|
// Suites that split their own tests across the shards (SCRIPTC_TEST_SHARD,
|
|
// skipUnownedTest in tests/harness/shard.ts): every shard runs its share in
|
|
// its own vitest process.
|
|
export const caseShardedSideFiles = [["npm static", "tests/harness/npm-static.test.ts"]];
|
|
export const fileExclusions = [
|
|
...corpusFiles,
|
|
...effectFiles,
|
|
coverageFile,
|
|
cacheFile,
|
|
...isolatedFiles.map(([, file]) => file),
|
|
...caseShardedSideFiles.map(([, file]) => file),
|
|
"tests/harness/self-hosting-native-driver.test.ts",
|
|
];
|
|
|
|
// Milliseconds each side-queue file takes on a CI runner, per flavor, from the
|
|
// logs of green main runs (scripts/ci-test-costs.mjs refreshes it).
|
|
export const testCosts = JSON.parse(
|
|
readFileSync(new URL("./ci-test-costs.json", import.meta.url), "utf8"),
|
|
);
|
|
|
|
// The shard (1-based) of each measured side-queue file and isolated suite:
|
|
// longest first to the shard with the least work so far, counting about a
|
|
// second per file for vitest to import it. Ties go to the highest shard, so the
|
|
// longest files start from the last shard while the cost-balanced corpus and
|
|
// three.js cases (tests/harness/shard.ts) place their longest from the first.
|
|
// An isolated suite without a measurement weighs as much as the median
|
|
// measured file. Files without a measurement are left to vitest's --shard,
|
|
// which splits them by count.
|
|
export function sideOwners(flavor, count) {
|
|
const costs = testCosts[flavor];
|
|
const known = Object.values(costs).sort((a, b) => a - b);
|
|
const median = known.length === 0 ? 0 : known[Math.floor(known.length / 2)];
|
|
const weight = (file) => (costs[file] ?? median) + 1000;
|
|
const files = [...new Set([...Object.keys(costs), ...isolatedFiles.map(([, file]) => file)])];
|
|
files.sort((a, b) => weight(b) - weight(a) || (a < b ? -1 : a > b ? 1 : 0));
|
|
const loads = new Array(count).fill(0);
|
|
const owners = new Map();
|
|
for (const file of files) {
|
|
let lightest = count - 1;
|
|
for (let shard = count - 2; shard >= 0; shard--)
|
|
if (loads[shard] < loads[lightest]) lightest = shard;
|
|
loads[lightest] += weight(file);
|
|
owners.set(file, lightest + 1);
|
|
}
|
|
return owners;
|
|
}
|
|
|
|
// Pull requests and pushes to main run the full suite on Linux. Their macOS
|
|
// lane runs the darwin suite, which keeps the darwin clang/libc/kqueue
|
|
// oracles: the Node differential corpus, plus every file whose assertions run
|
|
// only on darwin or take darwin-specific branches (including the Mach-O
|
|
// library localization arm and the arm64 runtime string oracle).
|
|
// The nightly schedule and manual runs with macos_full run the full suite on
|
|
// macOS.
|
|
export const darwinCorpusFiles = ["tests/harness/differential.test.ts"];
|
|
export const darwinFiles = [
|
|
"packages/cli/test/bootstrap.test.ts",
|
|
"packages/cli/test/native-link-info.test.ts",
|
|
"packages/cli/test/native-output.test.ts",
|
|
"packages/cli/test/runtime-pack.test.ts",
|
|
"packages/compiler/test/debug-variables.test.ts",
|
|
"packages/compiler/test/native-codegen-integration.test.ts",
|
|
"packages/compiler/test/source-debugging.test.ts",
|
|
"packages/runtime-darwin-arm64/test/archive.test.ts",
|
|
"packages/runtime/test/string.test.ts",
|
|
"tests/harness/ffi-callback-pool.test.ts",
|
|
"tests/harness/ffi-catalog.test.ts",
|
|
"tests/harness/ffi-frameworks.test.ts",
|
|
"tests/harness/library-callbacks.test.ts",
|
|
"tests/harness/library-dispatch.test.ts",
|
|
"tests/harness/library-multi.test.ts",
|
|
"tests/harness/native-object-example.test.ts",
|
|
"tests/harness/runtime-tree-shaking.test.ts",
|
|
"tests/harness/terminal-geometry.test.ts",
|
|
];
|
|
|
|
export function ciTestPlan({ shard, flavor, workers = 4, suite = "full" }) {
|
|
const match = /^([1-9]\d*)\/([1-9]\d*)$/.exec(shard);
|
|
if (
|
|
!match ||
|
|
!Number.isSafeInteger(Number(match[1])) ||
|
|
!Number.isSafeInteger(Number(match[2])) ||
|
|
Number(match[1]) > Number(match[2])
|
|
)
|
|
throw new Error("invalid CI shard");
|
|
if (flavor !== "plain" && flavor !== "san") throw new Error("invalid CI flavor");
|
|
if (!Number.isInteger(workers) || workers < 1) throw new Error("invalid CI worker budget");
|
|
if (suite !== "full" && suite !== "darwin") throw new Error("invalid CI suite");
|
|
const index = Number(match[1]),
|
|
count = Number(match[2]);
|
|
const sideWorkers = workers === 1 ? 0 : 1;
|
|
const env = { SCRIPTC_TEST_SHARD: shard, SCRIPTC_SAN: flavor === "san" ? "1" : "" };
|
|
if (suite === "darwin") {
|
|
// vitest runs one file's cases in sequence, so the shard's corpus cases
|
|
// run as two halves in separate processes: case shards 2i-1 and 2i of
|
|
// 2n. The second half follows the darwin contracts in the side queue.
|
|
const half = (part) => ({
|
|
name: `case-sharded differential corpus ${part}/2`,
|
|
args: ["test", ...darwinCorpusFiles],
|
|
env: {
|
|
...env,
|
|
SCRIPTC_TEST_SHARD: `${2 * index - 2 + part}/${2 * count}`,
|
|
SCRIPTC_TEST_WORKERS: "1",
|
|
},
|
|
});
|
|
return {
|
|
sideWorkers,
|
|
corpus: half(1),
|
|
side: [
|
|
{
|
|
name: "darwin contracts",
|
|
args: ["test", "--passWithNoTests", "--shard=" + shard, ...darwinFiles],
|
|
env: { ...env, SCRIPTC_TEST_WORKERS: "1" },
|
|
},
|
|
half(2),
|
|
],
|
|
};
|
|
}
|
|
const owners = sideOwners(flavor, count);
|
|
const isolated = new Set(isolatedFiles.map(([, file]) => file));
|
|
const balanced = [...owners]
|
|
.filter(([file, owner]) => owner === index && !isolated.has(file))
|
|
.map(([file]) => file)
|
|
.sort();
|
|
const side = [
|
|
{
|
|
name: "file-sharded tests",
|
|
args: [
|
|
"test",
|
|
"--passWithNoTests",
|
|
"--shard=" + shard,
|
|
...[...new Set([...fileExclusions, ...owners.keys()])].map((file) => "--exclude=" + file),
|
|
],
|
|
env: { ...env, SCRIPTC_TEST_WORKERS: "1" },
|
|
},
|
|
];
|
|
if (balanced.length > 0)
|
|
side.push({
|
|
name: "cost-balanced tests",
|
|
args: ["test", "--passWithNoTests", ...balanced],
|
|
env: { ...env, SCRIPTC_TEST_WORKERS: "1" },
|
|
});
|
|
for (const [name, file] of isolatedFiles) {
|
|
if (owners.get(file) === index)
|
|
side.push({ name, args: ["test", file], env: { ...env, SCRIPTC_TEST_WORKERS: "1" } });
|
|
}
|
|
for (const [name, file] of caseShardedSideFiles)
|
|
side.push({ name, args: ["test", file], env: { ...env, SCRIPTC_TEST_WORKERS: "1" } });
|
|
// Frontend coverage and cache invalidation do not consume sanitizer state.
|
|
if (flavor === "plain") {
|
|
side.push({
|
|
name: "native-cache correctness",
|
|
args: ["test", cacheFile],
|
|
env: {
|
|
...env,
|
|
SCRIPTC_TEST_WORKERS: "1",
|
|
SCRIPTC_CACHE_TEST_SHARD: shard,
|
|
SCRIPTC_TEST_STABLE_TOOLCHAIN: "0",
|
|
},
|
|
});
|
|
side.push({
|
|
name: "coverage sweep",
|
|
args: ["test", coverageFile],
|
|
env: { ...env, SCRIPTC_TEST_WORKERS: "1" },
|
|
});
|
|
}
|
|
return {
|
|
sideWorkers,
|
|
corpus: {
|
|
name: "case-sharded corpus",
|
|
args: ["test", ...corpusFiles],
|
|
env: {
|
|
...env,
|
|
SCRIPTC_TEST_WORKERS: String(workers - sideWorkers),
|
|
SCRIPTC_LLVM_TEST_MODE: "dev",
|
|
},
|
|
},
|
|
side,
|
|
};
|
|
}
|