Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/mutation-affected.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ name: Mutation Harness Canary
# On a harness diff the matrix is the canary (`LANE_CANARY`, selected by
# observed end-to-end runtime within the job budget — lane sources own no kernel,
# so derivation alone would select nothing) plus any kernel that same diff
# reaches through the import graph (scripts/mutation/ownership.ts): a kernel
# source, or a test that reaches one. Selected modules are sharded like the
# owns (scripts/mutation/ownership.ts): a kernel source, or a test that
# imports one. Selected modules are sharded like the
# weekly sweep, so the PR's wall clock is one module rather than their sum.

on:
Expand Down
20 changes: 20 additions & 0 deletions scripts/__tests__/mutation-scope-budget.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import path from 'node:path';
import { expect, test } from 'vitest';
import { KERNEL_MODULES, MAX_MUTATION_TEST_SCOPE_FILES } from '../mutation/modules.ts';
import { mutationTestScopes } from '../mutation/ownership.ts';

const repoRoot = path.resolve(import.meta.dirname, '../..');

test('every kernel mutation scope fits one weekly shard', { timeout: 30_000 }, () => {
const scopes = mutationTestScopes(repoRoot);
const oversized = KERNEL_MODULES.map((module) => ({
module: module.id,
testFiles: scopes.get(module.id)?.length ?? 0,
})).filter((scope) => scope.testFiles > MAX_MUTATION_TEST_SCOPE_FILES);
expect(
oversized,
`A kernel's owned tests exceed MAX_MUTATION_TEST_SCOPE_FILES (${MAX_MUTATION_TEST_SCOPE_FILES}). ` +
'Every shard replays the whole scope, so more shards do not help. Split the kernel in ' +
'scripts/mutation/modules.ts, or import it from fewer tests.',
).toEqual([]);
});
6 changes: 2 additions & 4 deletions scripts/mutation/envelope.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -429,15 +429,13 @@ test('a non-zero Stryker exit fails even when it wrote a report', () => {
fs.mkdirSync(path.join(root, 'bin'));
fs.symlinkSync(path.join(repoRoot, 'node_modules'), path.join(root, 'node_modules'));
fs.writeFileSync(path.join(root, 'stryker.config.json'), '{}');
fs.writeFileSync(
path.join(root, 'vitest.config.ts'),
`export default { test: { projects: [{ test: { name: 'unit-core', include: ['src/**/*.test.ts'] } }] } };`,
);
fs.writeFileSync(path.join(root, 'packages/kernel/src/errors.ts'), 'export const value = 1;');
fs.writeFileSync(
path.join(root, 'src/consumer.test.ts'),
"import { value } from '../packages/kernel/src/errors'; console.log(value);",
);
// Ownership reads the tracked import graph.
runCmdSync('git', ['add', 'src', 'packages'], { cwd: root });
fs.writeFileSync(
path.join(root, 'bin/pnpm'),
`#!/bin/sh
Expand Down
14 changes: 13 additions & 1 deletion scripts/mutation/modules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,18 @@ export const KERNEL_MODULES: readonly KernelModule[] = [
},
];

/**
* The most test files one kernel's mutation scope may hold. Every shard of a
* module replays its whole scope in Stryker's dry run before the first mutant,
* so this bounds a shard however many shards the module declares. Measured on
* ubuntu-latest (weekly run 40164641005): a 684-file scope dry-ran in 4–7 min
* and its shards finished in 9–19 min of the 22-minute step; a 1,471-file scope
* dry-ran in 8–11 min and a shard outran the step. The unit suite enforces it
* (`scripts/__tests__/mutation-scope-budget.test.ts`), so the PR that grows a
* scope past it fails instead of the next weekly sweep.
*/
export const MAX_MUTATION_TEST_SCOPE_FILES = 600;

export const ALL_MODULE_IDS: readonly ModuleId[] = KERNEL_MODULES.map((module) => module.id);

export function moduleById(id: ModuleId): KernelModule {
Expand All @@ -119,7 +131,7 @@ export function mutateGlobs(ids: readonly ModuleId[] = ALL_MODULE_IDS): string[]

/**
* The module a lane-tooling change proves itself against. Keep this to a real
* end-to-end sweep whose observed related-test scope fits the 30-minute job
* end-to-end sweep whose observed owned-test scope fits the 30-minute job
* budget; mutant count alone does not capture replay cost.
*/
export const LANE_CANARY: ModuleId = 'snapshot-occlusion';
Expand Down
98 changes: 78 additions & 20 deletions scripts/mutation/ownership.test.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
import assert from 'node:assert/strict';
import path from 'node:path';
import { test } from 'node:test';
import fs from 'node:fs';
import { mkdtempForTestSync } from '@agent-device/test-kit';
import { runCmdSync } from '@agent-device/host-kit/command';
import { fileURLToPath } from 'node:url';
import { KERNEL_MODULES } from './modules.ts';
import {
derivedAffectedModules,
importedBy,
isTestFile,
mutatedSources,
mutationTestFiles,
ownedTestFiles,
ownershipDeriver,
reachableFrom,
} from './ownership.ts';

const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');
Expand All @@ -31,30 +35,84 @@ test('a kernel is owned by the mirrored test that imports it directly', () => {
);
});

// The omission that hand-listed ownership could not see: this test asserts over
// normalizeError without importing packages/kernel/src/errors.ts itself.
test('a kernel is owned by tests that reach it indirectly', () => {
const deriver = ownershipDeriver(repoRoot);
// Reach is not ownership: nearly every test reaches the error kernel, and a
// reach-based scope is the whole suite. This test exercises edge detection only
// through the back runtime, so it does not measure scroll-edge-state.
test('a test that only reaches a kernel through other modules does not own it', () => {
const testFile = 'packages/daemon/src/execution/__tests__/back-runtime.test.ts';
assert.ok(!ownershipDeriver(repoRoot).ownersOf(testFile).includes('scroll-edge-state'));
assert.ok(!mutationTestFiles(['scroll-edge-state'], repoRoot).includes(testFile));
});

test('a file absent from the tracked graph imports nothing', () => {
assert.deepEqual([...importedBy('src/untracked-fixture.ts', repoRoot)], []);
});

test('a mutation scope holds no test another Vitest project owns', () => {
// The apple-runner suites need that project's host setup, which Stryker's run does not install.
assert.ok(
deriver.ownersOf('src/__tests__/daemon-error.test.ts').includes('kernel-errors'),
'daemon-error.test.ts exercises normalizeError but does not own kernel-errors',
!mutationTestFiles(['kernel-errors'], repoRoot).some((file) =>
file.startsWith('packages/platform-apple/src/runner/'),
),
);
assert.ok(
deriver
.ownersOf('packages/daemon/src/execution/__tests__/back-runtime.test.ts')
.includes('scroll-edge-state'),
'back-runtime.test.ts exercises edge detection but does not own scroll-edge-state',
!isTestFile('packages/platform-apple/src/runner/__tests__/index-dispatch-disclosure.test.ts'),
);
});

test('reachability includes its query root when the file is absent from the tracked graph', () => {
assert.deepEqual(
[...reachableFrom('src/untracked-fixture.ts', repoRoot)],
['src/untracked-fixture.ts'],
test('a test that takes a kernel through a barrel re-export owns it', () => {
// `@agent-device/ad-script` re-exports the codec this test exercises.
assert.ok(
mutationTestFiles(['target-annotation-serde'], repoRoot).includes(
'packages/selectors/src/target-evidence.test.ts',
),
);
});

test('ownership is complete: every test reaching a kernel owns it', () => {
test('a test that runs a kernel through its test support code owns it', () => {
// `capture()` in scroll-edge-state-fixtures.ts calls the kernel; this test imports only it.
const testFile =
'packages/capture-kit/src/snapshot/__tests__/scroll-edge-state-target-resolution.test.ts';
assert.ok(ownershipDeriver(repoRoot).ownersOf(testFile).includes('scroll-edge-state'));
assert.ok(mutationTestFiles(['scroll-edge-state'], repoRoot).includes(testFile));
});

test('barrel re-exports reach the module each imported name comes from, and only that one', () => {
const root = fs.realpathSync(mkdtempForTestSync('mutation-barrels-'));
try {
runCmdSync('git', ['init', '--quiet', root]);
const write = (file: string, source: string) => {
fs.mkdirSync(path.dirname(path.join(root, file)), { recursive: true });
fs.writeFileSync(path.join(root, file), source);
};
write('package.json', JSON.stringify({ name: 'barrel-fixture', type: 'module' }));
write('src/kernel.ts', 'export const value = 1;');
write('src/other.ts', 'export const unrelated = 2;');
write('src/renamed.ts', "export { value as renamed } from './kernel.ts';");
write('src/star.ts', "export * from './other.ts';\nexport * from './kernel.ts';");
write('src/forwarded.ts', "import { value } from './kernel.ts';\nexport { value };");
write(
'src/__tests__/helper.ts',
"import { value } from '../kernel.ts';\nexport const run = () => value;",
);
write('src/renamed.test.ts', "import { renamed } from './renamed.ts';");
write('src/star.test.ts', "import { value } from './star.ts';");
write('src/star-other.test.ts', "import { unrelated } from './star.ts';");
write('src/forwarded.test.ts', "import { value } from './forwarded.ts';");
write('src/helper.test.ts', "import { run } from './__tests__/helper.ts';");
runCmdSync('git', ['add', '.'], { cwd: root });
const reaches = (testFile: string) => importedBy(testFile, root).has('src/kernel.ts');
assert.equal(reaches('src/renamed.test.ts'), true, 'export { a as b } from');
assert.equal(reaches('src/star.test.ts'), true, 'export * from');
assert.equal(reaches('src/star-other.test.ts'), false, 'export * of a name the kernel lacks');
assert.equal(reaches('src/forwarded.test.ts'), true, 'import then export');
assert.equal(reaches('src/helper.test.ts'), true, 'test support code');
} finally {
fs.rmSync(root, { recursive: true, force: true });
}
});

test('ownership is complete: every test importing a kernel owns it', () => {
const owned = ownedTestFiles(repoRoot);
for (const module of KERNEL_MODULES) {
const sources = mutatedSources(module, repoRoot);
Expand All @@ -65,10 +123,10 @@ test('ownership is complete: every test reaching a kernel owns it', () => {
`${module.id} has no owning tests — its score cannot be attributed`,
);
for (const testFile of files) {
const reachable = reachableFrom(testFile, repoRoot);
const imported = importedBy(testFile, repoRoot);
assert.ok(
sources.some((source) => reachable.has(source)),
`${testFile} owns ${module.id} without reaching it`,
sources.some((source) => imported.has(source)),
`${testFile} owns ${module.id} without importing it`,
);
}
// The converse: a diff to an owning test selects the module on a PR.
Expand All @@ -89,7 +147,7 @@ test('non-kernel sources and non-tests are not owned', () => {
assert.ok(isTestFile('packages/kernel/src/errors-code-policy.test.ts'));
});

test('derived selection unions source ownership with test reachability', () => {
test('derived selection unions source ownership with test imports', () => {
assert.deepEqual(
derivedAffectedModules(
[
Expand Down
Loading
Loading