diff --git a/scripts/coverage-exempt.ts b/scripts/coverage-exempt.ts new file mode 100644 index 0000000000..8f20f54424 --- /dev/null +++ b/scripts/coverage-exempt.ts @@ -0,0 +1,41 @@ +/** + * Heavy suites the coverage aggregate runs uninstrumented in a parallel gate. + * Membership contract: a suite qualifies only when every coverage-measured + * file it executes in-process (`coverage.include` spans package src trees; + * typert generator src is threshold-excluded in vitest.config.ts) is already + * fully covered by other suites, so removing it from the instrumented run + * changes no threshold outcome. The aggregate still runs every listed suite + * plain beside the instrumented gate, so correctness signal is unchanged — + * only the v8 instrumentation tax on compiler- and subprocess-heavy fixtures + * is dropped. + */ + +/** One coverage-exempt suite: a Vitest CLI filter and its exclude glob. */ +export interface CoverageExemptSuite { + /** Positional file filter selecting the suite in the uninstrumented gate. */ + readonly filter: string + /** Exclude glob removing the suite from the instrumented gate. */ + readonly exclude: string +} + +/** + * Set to `1` by the instrumented coverage gate; vitest.config.ts then drops + * the exempt suites from every project. CLI `--exclude` cannot express this: + * it does not reach per-project include resolution. + */ +export const COVERAGE_EXEMPT_ENV = 'DSH_COVERAGE_EXEMPT_HEAVY' + +/** Coverage-exempt heavy suites; keep filter and exclude selecting the same files. */ +export const coverageExemptHeavySuites: readonly CoverageExemptSuite[] = [ + // Whole-workspace compiler analysis per case — the lane's longest tail. + // Generator src is threshold-excluded; tools-catalog's registry and + // tool-cordis imports are fully covered by those packages' own tests. + { + filter: 'packages/typert/generator/tests/', + exclude: 'packages/typert/generator/tests/**', + }, + // Real child-process fixtures over scripts/ sources, which coverage never measures. + { filter: 'scripts/install-lefthook.spec.ts', exclude: 'scripts/install-lefthook.spec.ts' }, + { filter: 'scripts/oxlint-contract.spec.ts', exclude: 'scripts/oxlint-contract.spec.ts' }, + { filter: 'scripts/change-scope.spec.ts', exclude: 'scripts/change-scope.spec.ts' }, +] diff --git a/scripts/run-gates.ts b/scripts/run-gates.ts index 46bc72c833..0289625959 100644 --- a/scripts/run-gates.ts +++ b/scripts/run-gates.ts @@ -9,6 +9,7 @@ import { spawn } from 'node:child_process' import { availableParallelism } from 'node:os' import { resolve } from 'node:path' import { performance } from 'node:perf_hooks' +import { COVERAGE_EXEMPT_ENV, coverageExemptHeavySuites } from './coverage-exempt.ts' /** A named aggregate exposed by the gate runner. */ export type Mode = @@ -202,7 +203,7 @@ export function gatesForMode(selected: Mode): Gate[] { pnpmScript('duplication', 'duplication'), ] case 'ci-coverage': - return [coverageGate()] + return coverageGates() case 'ci-snapshot': return [pnpmScript('build', 'build'), snapshotGate()] case 'ci-artifacts': @@ -248,7 +249,7 @@ function ciPrimaryGates(): Gate[] { pnpmScript('typecheck', 'typecheck'), lintGate(), pnpmScript('duplication', 'duplication'), - coverageGate(), + ...coverageGates(), ...nodeCompatSmokeGates(), snapshotGate(), ...docSyncLeafGates(), @@ -407,15 +408,30 @@ function lintGate(): Gate { : { displayCommand: `DSH_OXLINT_THREADS=${raw} pnpm run lint` }) } -function coverageGate(): Gate { - return pnpmExec('coverage', [ - 'vitest', - 'run', - '--coverage', - ...positiveIntArg('DSH_COVERAGE_MAX_WORKERS', '--maxWorkers'), - ], { - label: 'test:coverage', - }) +// The heavy suites run uninstrumented beside the thresholded gate: their +// compiler- and subprocess-bound fixtures pay a multiple of their runtime +// under v8 instrumentation while contributing nothing the thresholds need +// (membership contract in scripts/coverage-exempt.ts). +function coverageGates(): Gate[] { + return [ + pnpmExec('coverage', [ + 'vitest', + 'run', + '--coverage', + ...positiveIntArg('DSH_COVERAGE_MAX_WORKERS', '--maxWorkers'), + ], { + label: 'test:coverage', + env: { [COVERAGE_EXEMPT_ENV]: '1' }, + }), + pnpmExec('coverage-exempt-heavy', [ + 'vitest', + 'run', + ...coverageExemptHeavySuites.map(suite => suite.filter), + ...positiveIntArg('DSH_COVERAGE_MAX_WORKERS', '--maxWorkers'), + ], { + label: 'test:coverage-exempt-heavy', + }), + ] } // Example and package snapshots boot their bins in `lib` mode (built artifacts under plain Node, diff --git a/vitest.config.ts b/vitest.config.ts index c49f55bea8..d31dca5d3f 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -1,5 +1,6 @@ import tsconfigPaths from 'vite-tsconfig-paths' import { defineConfig } from 'vitest/config' +import { COVERAGE_EXEMPT_ENV, coverageExemptHeavySuites } from './scripts/coverage-exempt.ts' // Resolution facade shared by every plugin instance below: tsconfig.base.json // has no include, which vite-tsconfig-paths treats as match-all, so its paths @@ -37,6 +38,12 @@ const testIncludes = [ 'scripts/**/*.spec.ts', ] +// The instrumented coverage gate sets this env; the exempt heavy suites then +// run beside it uninstrumented (membership contract in scripts/coverage-exempt.ts). +const coverageExemptExcludes = process.env[COVERAGE_EXEMPT_ENV] === '1' + ? coverageExemptHeavySuites.map(suite => suite.exclude) + : [] + // These suites exercise process-global state, process APIs, or timing-sensitive process I/O // that worker threads cannot isolate reliably under aggregate gate contention. // Keep the narrow exception in forks while the rest of the inventory avoids per-file processes. @@ -73,6 +80,7 @@ export default defineConfig({ exclude: [ ...windowsUnsupportedPackages.map(path => `${path}/tests/**/*.spec.ts`), ...processBoundTests, + ...coverageExemptExcludes, ], }, }, @@ -83,7 +91,10 @@ export default defineConfig({ pool: 'forks', setupFiles: ['./scripts/test-invariants.ts'], include: processBoundTests, - exclude: windowsUnsupportedPackages.map(path => `${path}/tests/**/*.spec.ts`), + exclude: [ + ...windowsUnsupportedPackages.map(path => `${path}/tests/**/*.spec.ts`), + ...coverageExemptExcludes, + ], }, }, ],