95 lines
3.4 KiB
JavaScript
95 lines
3.4 KiB
JavaScript
import { execFileSync } from "node:child_process";
|
|
import { appendFile, mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
import { isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
|
|
import { z } from "zod";
|
|
|
|
const root = process.cwd();
|
|
const historyDirectory = join(root, ".cache/ci-test-history");
|
|
const historyPath = join(historyDirectory, "test-results.json");
|
|
// Never let a failed or partial rerun leave a previous successful history file.
|
|
await rm(historyPath, { force: true });
|
|
const report = z
|
|
.object({
|
|
success: z.boolean(),
|
|
numFailedTests: z.number().int().nonnegative(),
|
|
testResults: z.array(
|
|
z.object({
|
|
name: z.string().min(1),
|
|
startTime: z.number().finite(),
|
|
endTime: z.number().finite(),
|
|
}),
|
|
),
|
|
})
|
|
.parse(JSON.parse(await readFile("test-results.json", "utf8")));
|
|
|
|
const files = report.testResults.map(({ name, startTime, endTime }) => {
|
|
const path = relative(root, resolve(root, name));
|
|
if (
|
|
path === "" ||
|
|
path === ".." ||
|
|
path.startsWith(".." + sep) ||
|
|
isAbsolute(path)
|
|
)
|
|
throw new Error(`Test report path is outside the checkout: ${name}`);
|
|
const duration = endTime - startTime;
|
|
if (!Number.isFinite(duration) || duration < 0)
|
|
throw new Error(`Test report has an invalid duration: ${name}`);
|
|
return { name: path.split(sep).join("/"), startTime: 0, endTime: duration };
|
|
});
|
|
const complete = process.env.SHARD_RESULT === "success";
|
|
const passed =
|
|
complete &&
|
|
process.env.MERGE_RESULT === "success" &&
|
|
report.success &&
|
|
report.numFailedTests === 0;
|
|
// Successful shards and merged reports must account for the complete portable
|
|
// inventory. Coverage thresholds alone cannot prove that every file ran.
|
|
if (passed && process.env.CI_VERIFY_TEST_INVENTORY === "true") {
|
|
const expected = execFileSync(
|
|
"git",
|
|
["ls-files", "-z", "--", "src", "tests"],
|
|
{ encoding: "utf8", maxBuffer: 8 * 1024 * 1024 },
|
|
)
|
|
.split("\0")
|
|
.filter((path) => path.endsWith(".test.ts"));
|
|
const actual = files.map(({ name }) => name);
|
|
const expectedSet = new Set(expected);
|
|
const actualSet = new Set(actual);
|
|
const missing = expected.filter((path) => !actualSet.has(path));
|
|
const unexpected = actual.filter((path) => !expectedSet.has(path));
|
|
if (missing.length || unexpected.length || actualSet.size !== actual.length)
|
|
throw new Error(
|
|
`Incomplete portable test inventory: ${JSON.stringify({ missing, unexpected, duplicateFiles: actual.length - actualSet.size })}`,
|
|
);
|
|
}
|
|
const summary = [
|
|
complete ? "## Merged test diagnostics" : "## Partial test diagnostics",
|
|
"",
|
|
`Shard result: ${process.env.SHARD_RESULT ?? "unknown"}; merge result: ${process.env.MERGE_RESULT ?? "unknown"}.`,
|
|
passed
|
|
? "Successful timing history is available."
|
|
: "This report is diagnostic only and is not a successful timing baseline.",
|
|
"",
|
|
"| Slowest file | Duration (ms) |",
|
|
"| --- | ---: |",
|
|
...files
|
|
.toSorted((left, right) => right.endTime - left.endTime)
|
|
.slice(0, 20)
|
|
.map(
|
|
({ name, endTime }) => `| ${name.replaceAll("|", "\\|")} | ${endTime} |`,
|
|
),
|
|
"",
|
|
].join("\n");
|
|
if (process.env.GITHUB_STEP_SUMMARY)
|
|
await appendFile(process.env.GITHUB_STEP_SUMMARY, summary);
|
|
else console.log(summary);
|
|
|
|
if (passed) {
|
|
await mkdir(historyDirectory, { recursive: true });
|
|
await writeFile(
|
|
historyPath,
|
|
JSON.stringify({ success: true, numFailedTests: 0, testResults: files }),
|
|
);
|
|
}
|