Files
HealthLog/tests/unit-projects.ts
T
Marc-André Bombeck e9f89bbebf ci: shard both suites by measured duration and split the unit suite
Vitest's --shard cut the file list into slices of equal length, which gave
the integration shards 954 s to 1 449 s. A sequencer now deals files out by
their measured duration, plus a fixed per-file cost for import and setup,
longest first to the shard with the least work, and starts the longest files
first inside a shard. Timings live in tests/durations/*.json and are
regenerated from Vitest JSON reports with scripts/test-durations.mjs; every
CI shard uploads its report.

Integration shards run three workers each, one cloned database per worker.
The unit suite runs as three projects decided per file from its source:
guards and unit share a module graph inside a worker, isolated keeps a fresh
one for every file that mocks, spies, stubs or resets. In CI it is three
duration-balanced shards next to a job for lint, format, types and the
OpenAPI drift check.

The required check names stay as they were. "Lint, Typecheck & Test" is now
the job that answers for the static checks and every unit shard, the way
"integration" already answers for its shards. The browser-install guard
recognises a direct, flags-only vitest run as a run of the unit suite.
2026-10-22 05:39:35 +02:00

65 lines
2.7 KiB
TypeScript

/**
* Which unit-test files can share a module graph, decided from their source.
*
* Vitest isolates every test file by default: each one re-imports the
* application modules it touches. In this suite that import is most of the
* run (63 % of the 869 s CI wall time before the split), and most files do
* not need it, because they neither replace a module nor change process
* state. Those files run in projects with `isolate: false`, where a worker
* keeps its module cache from one file to the next; everything else keeps
* full isolation.
*
* A file needs isolation when its source does anything that outlives the
* file in a shared graph: mocks or spies on a module, stubbed env or
* globals, fake timers, a module reset, an assignment to `process.env` or a
* global, or a `__reset…ForTests` hook. The match is deliberately broad; a
* false positive costs a little speed, a false negative costs a flaky
* neighbour. A file that turns out to need isolation for a reason this list
* misses says so with a `// vitest-isolate` comment.
*/
import { readFileSync } from "node:fs";
const NEEDS_ISOLATION = new RegExp(
[
String.raw`\bvi\s*\.\s*(?:mock|doMock|unmock|doUnmock|hoisted|stubEnv|stubGlobal|unstubAllEnvs|unstubAllGlobals|spyOn|useFakeTimers|setSystemTime|resetModules|importMock)\b`,
String.raw`\bprocess\.env(?:\.\w+|\[[^\]]+\])\s*=(?!=)`,
String.raw`\bdelete\s+process\.env`,
String.raw`\b(?:globalThis|global|window)(?:\.\w+|\[[^\]]+\])\s*=(?!=)`,
String.raw`Object\.defineProperty\(\s*(?:globalThis|global|window|process)`,
String.raw`__\w*[Rr]eset\w*ForTests`,
String.raw`process\.chdir\(`,
String.raw`vitest-isolate`,
].join("|"),
);
export function needsIsolation(source: string): boolean {
return NEEDS_ISOLATION.test(source);
}
export interface UnitProjectFiles {
/** Structural guards that share a module graph. */
guards: string[];
/** Behavioural unit and component tests that share a module graph. */
shared: string[];
/** Everything that mocks, stubs or resets; one fresh graph per file. */
isolated: string[];
}
const GUARD_LOCATIONS =
/^(?:src\/__tests__\/|scripts\/__tests__\/|eslint-plugins\/|tests\/)/;
/** Split `files` (paths relative to `root`) into the three projects. */
export function classifyUnitFiles(
root: string,
files: readonly string[],
): UnitProjectFiles {
const result: UnitProjectFiles = { guards: [], shared: [], isolated: [] };
for (const file of files) {
const source = readFileSync(`${root}/${file}`, "utf8");
if (needsIsolation(source)) result.isolated.push(file);
else if (GUARD_LOCATIONS.test(file)) result.guards.push(file);
else result.shared.push(file);
}
return result;
}