Skip to content

Commit 2365070

Browse files
Merge pull request #1356 from Gustav-Simonsson/faster_tests2
Speed up test suite
2 parents 8394e27 + c21e9b9 commit 2365070

11 files changed

Lines changed: 684 additions & 491 deletions

‎scripts/check-dead-exports.test.ts‎

Lines changed: 64 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
loadGuardConfig,
2020
parseAllowlistText,
2121
parseGuardConfig,
22+
parseInjectedScannedFiles,
2223
parseTsPruneLine,
2324
tsPruneSpawn,
2425
validateAllowlistEntry,
@@ -28,6 +29,19 @@ import {
2829

2930
const repoRoot = join(import.meta.dir, "..");
3031

32+
// tsc --listFilesOnly costs ~1.5s per spawn and the count is deterministic
33+
// for a clean tree. The floor tests and the end-to-end probe share one run
34+
// through this cache; each caller computes it on first use, so no test
35+
// depends on another having run.
36+
let cachedScannedFileCount: number | undefined;
37+
function liveScannedFileCount(): number {
38+
cachedScannedFileCount ??= countScannedFiles(
39+
repoRoot,
40+
loadGuardConfig().tsconfig,
41+
);
42+
return cachedScannedFileCount;
43+
}
44+
3145
// A probe dead export in one of the scoped files must fail the guard: the
3246
// exact-name exemptions cover only the remaining deferred-cleanup flags
3347
// (the usage-formatter flags were removed with their exports by the CL-6815
@@ -310,6 +324,17 @@ describe("pinned scan invocation", () => {
310324
expect(() => parseGuardConfig(null)).toThrow();
311325
expect(() => parseGuardConfig([])).toThrow();
312326
});
327+
328+
test("the scanned-files injection accepts no override or a positive integer", () => {
329+
expect(parseInjectedScannedFiles(undefined)).toBeUndefined();
330+
expect(parseInjectedScannedFiles("1217")).toBe(1217);
331+
});
332+
333+
test("the scanned-files injection fails closed on a bogus value", () => {
334+
for (const bad of ["", "0", "-5", "1.5", "abc"]) {
335+
expect(() => parseInjectedScannedFiles(bad)).toThrow();
336+
}
337+
});
313338
});
314339

315340
describe("scan coverage floor", () => {
@@ -321,14 +346,14 @@ describe("scan coverage floor", () => {
321346

322347
test("the live program file count clears the checked-in floor", () => {
323348
const config = loadGuardConfig();
324-
const scanned = countScannedFiles(repoRoot, config.tsconfig);
325-
expect(scanned).toBeGreaterThanOrEqual(config.minScannedFiles);
349+
expect(liveScannedFileCount()).toBeGreaterThanOrEqual(
350+
config.minScannedFiles,
351+
);
326352
}, 120_000);
327353

328354
test("the checked-in floor stays tight to the live count", () => {
329355
const config = loadGuardConfig();
330-
const scanned = countScannedFiles(repoRoot, config.tsconfig);
331-
expect(scanned).toBeLessThan(config.minScannedFiles * 1.1);
356+
expect(liveScannedFileCount()).toBeLessThan(config.minScannedFiles * 1.1);
332357
}, 120_000);
333358
});
334359

@@ -337,24 +362,58 @@ describe("scan coverage floor", () => {
337362
// scripts/ tree, the real guard runs as a subprocess, and the run must exit
338363
// nonzero naming the probe. The probe lives only for the test (never
339364
// committed) so the keep-alive check cannot itself become a dead export.
365+
//
366+
// The guard normally scans the full project via ts-prune (~5s); the probe's
367+
// temp tsconfig includes only the probe file, so this run pays only ts-prune
368+
// startup while still exercising the same wired-up gate (config load,
369+
// allowlist validation, node-spawned ts-prune, output parsing, coverage
370+
// floor, exit code).
340371
describe("violation end to end", () => {
341372
test("a temp dead export fails the guard, which names it", () => {
342373
const probeFile = `dead-export-guard-probe-${process.pid}.ts`;
343374
const probeName = `deadExportGuardProbe${process.pid}`;
344375
const probePath = join(repoRoot, "scripts", probeFile);
376+
const narrowTsconfigFile = `dead-export-guard-tsconfig-${process.pid}.json`;
377+
const narrowTsconfigPath = join(repoRoot, "scripts", narrowTsconfigFile);
345378
writeFileSync(probePath, `export const ${probeName} = 1;\n`);
379+
writeFileSync(
380+
narrowTsconfigPath,
381+
JSON.stringify({
382+
compilerOptions: {
383+
noEmit: true,
384+
target: "ESNext",
385+
module: "ESNext",
386+
moduleResolution: "bundler",
387+
skipLibCheck: true,
388+
},
389+
// Relative to the narrow tsconfig's own directory (scripts/).
390+
files: [probeFile],
391+
}),
392+
);
346393
try {
347394
const ran = spawnSync(
348395
process.execPath,
349396
["scripts/check-dead-exports.ts"],
350-
{ cwd: repoRoot, encoding: "utf8" },
397+
{
398+
cwd: repoRoot,
399+
encoding: "utf8",
400+
// The file-count subprocess is exercised by the floor tests above;
401+
// inject the same deterministic value so this run pays only
402+
// ts-prune.
403+
env: {
404+
...process.env,
405+
DEAD_EXPORT_GUARD_SCANNED_FILES: String(liveScannedFileCount()),
406+
DEAD_EXPORT_GUARD_TSCONFIG: `scripts/${narrowTsconfigFile}`,
407+
},
408+
},
351409
);
352410
expect(ran.status).toBe(1);
353411
expect(`${ran.stdout}\n${ran.stderr}`).toContain(
354412
`${probeFile}: ${probeName}`,
355413
);
356414
} finally {
357415
rmSync(probePath, { force: true });
416+
rmSync(narrowTsconfigPath, { force: true });
358417
}
359418
}, 120_000);
360419
});

‎scripts/check-dead-exports.ts‎

Lines changed: 52 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,18 @@ import { fileURLToPath } from "node:url";
1919
// gate fails closed when the scanned file count drops below that config's
2020
// floor.
2121

22+
// Test injection (same pattern as teardownDeadlineMs / maxScanBytes): a
23+
// subprocess test that needs the real gate without paying for the ~1.5s tsc
24+
// file-count run can pass the deterministic count itself. Production never
25+
// sets it, so the gate always computes the count in normal runs.
26+
const scannedFilesEnv = "DEAD_EXPORT_GUARD_SCANNED_FILES";
27+
28+
// Test injection (same pattern as scannedFilesEnv): a subprocess test that
29+
// needs the wired-up gate without paying for the ~5s full-project ts-prune
30+
// run can point the pinned scan at a smaller tsconfig. Production never sets
31+
// it, so the gate always scans the pinned tsconfig in normal runs.
32+
const tsconfigEnv = "DEAD_EXPORT_GUARD_TSCONFIG";
33+
2234
const here = dirname(fileURLToPath(import.meta.url));
2335
const repoRoot = dirname(here);
2436
const allowlistPath = join(here, "dead-export-allowlist.txt");
@@ -284,6 +296,22 @@ export function tsPruneSpawn(
284296
return { command: "node", args: [tsPruneBinPath, ...tsPruneArgs] };
285297
}
286298

299+
// Parses the test-injection env override. Undefined means compute the count
300+
// as usual; anything else must be a positive integer or the gate fails
301+
// closed rather than trusting a bogus number.
302+
export function parseInjectedScannedFiles(
303+
value: string | undefined,
304+
): number | undefined {
305+
if (value === undefined) return undefined;
306+
const parsed = Number(value);
307+
if (!Number.isInteger(parsed) || parsed <= 0) {
308+
throw new Error(
309+
`${scannedFilesEnv} must be a positive integer when set (test injection only)`,
310+
);
311+
}
312+
return parsed;
313+
}
314+
287315
// Counts the TypeScript files the pinned tsconfig pulls into its program via
288316
// tsc --listFilesOnly: the same project ts-prune analyzes. A narrowed
289317
// tsconfig (or a moved scan root) shrinks this count, and the gate fails
@@ -329,6 +357,19 @@ function main(): void {
329357
} catch (err) {
330358
fail(`dead-export guard: invalid guard config: ${(err as Error).message}`);
331359
}
360+
const overrideTsconfig = process.env[tsconfigEnv];
361+
if (overrideTsconfig !== undefined) {
362+
if (!existsSync(join(repoRoot, overrideTsconfig))) {
363+
fail(
364+
`dead-export guard: tsconfig override not found: ${overrideTsconfig}`,
365+
);
366+
}
367+
config = {
368+
...config,
369+
tsconfig: overrideTsconfig,
370+
tsPruneArgs: ["-p", overrideTsconfig],
371+
};
372+
}
332373
const allowlistText = readFileSync(allowlistPath, "utf8");
333374
const allowlistProblems = [
334375
...validateAllowlistText(allowlistText),
@@ -356,12 +397,17 @@ function main(): void {
356397
}
357398
const outcome = evaluateGuard(rules, String(pruned.stdout));
358399
let scannedFiles: number;
359-
try {
360-
scannedFiles = countScannedFiles(repoRoot, config.tsconfig);
361-
} catch (err) {
362-
fail(
363-
`dead-export guard: could not count scanned files: ${(err as Error).message}`,
364-
);
400+
const injectedCount = parseInjectedScannedFiles(process.env[scannedFilesEnv]);
401+
if (injectedCount !== undefined) {
402+
scannedFiles = injectedCount;
403+
} else {
404+
try {
405+
scannedFiles = countScannedFiles(repoRoot, config.tsconfig);
406+
} catch (err) {
407+
fail(
408+
`dead-export guard: could not count scanned files: ${(err as Error).message}`,
409+
);
410+
}
365411
}
366412
console.log(
367413
`dead-export guard: ${outcome.dead} consumer-less exports, ${outcome.dead - outcome.violations.length} allowlisted, ${outcome.violations.length} violations, ${scannedFiles} scanned files (floor ${config.minScannedFiles})`,

0 commit comments

Comments
 (0)