@mutineerjs/mutineer 0.2.2 → 0.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -7
- package/dist/core/__tests__/module.spec.js +66 -3
- package/dist/core/__tests__/sfc.spec.d.ts +1 -0
- package/dist/core/__tests__/sfc.spec.js +76 -0
- package/dist/core/__tests__/variant-utils.spec.d.ts +1 -0
- package/dist/core/__tests__/variant-utils.spec.js +93 -0
- package/dist/runner/__tests__/args.spec.d.ts +1 -0
- package/dist/runner/__tests__/args.spec.js +225 -0
- package/dist/runner/__tests__/cache.spec.d.ts +1 -0
- package/dist/runner/__tests__/cache.spec.js +180 -0
- package/dist/runner/__tests__/changed.spec.d.ts +1 -0
- package/dist/runner/__tests__/changed.spec.js +227 -0
- package/dist/runner/__tests__/cleanup.spec.d.ts +1 -0
- package/dist/runner/__tests__/cleanup.spec.js +41 -0
- package/dist/runner/__tests__/config.spec.d.ts +1 -0
- package/dist/runner/__tests__/config.spec.js +71 -0
- package/dist/runner/__tests__/coverage-resolver.spec.d.ts +1 -0
- package/dist/runner/__tests__/coverage-resolver.spec.js +171 -0
- package/dist/runner/__tests__/pool-executor.spec.d.ts +1 -0
- package/dist/runner/__tests__/pool-executor.spec.js +213 -0
- package/dist/runner/__tests__/tasks.spec.d.ts +1 -0
- package/dist/runner/__tests__/tasks.spec.js +95 -0
- package/dist/runner/__tests__/variants.spec.d.ts +1 -0
- package/dist/runner/__tests__/variants.spec.js +259 -0
- package/dist/runner/args.d.ts +5 -0
- package/dist/runner/args.js +7 -0
- package/dist/runner/config.js +2 -2
- package/dist/runner/coverage-resolver.d.ts +21 -0
- package/dist/runner/coverage-resolver.js +96 -0
- package/dist/runner/jest/__tests__/pool.spec.d.ts +1 -0
- package/dist/runner/jest/__tests__/pool.spec.js +212 -0
- package/dist/runner/jest/__tests__/worker-runtime.spec.d.ts +1 -0
- package/dist/runner/jest/__tests__/worker-runtime.spec.js +148 -0
- package/dist/runner/orchestrator.js +43 -295
- package/dist/runner/pool-executor.d.ts +17 -0
- package/dist/runner/pool-executor.js +143 -0
- package/dist/runner/shared/__tests__/mutant-paths.spec.d.ts +1 -0
- package/dist/runner/shared/__tests__/mutant-paths.spec.js +66 -0
- package/dist/runner/shared/__tests__/redirect-state.spec.d.ts +1 -0
- package/dist/runner/shared/__tests__/redirect-state.spec.js +56 -0
- package/dist/runner/tasks.d.ts +12 -0
- package/dist/runner/tasks.js +25 -0
- package/dist/runner/variants.d.ts +17 -2
- package/dist/runner/variants.js +33 -0
- package/dist/runner/vitest/__tests__/redirect-loader.spec.js +4 -0
- package/dist/utils/__tests__/logger.spec.d.ts +1 -0
- package/dist/utils/__tests__/logger.spec.js +61 -0
- package/dist/utils/__tests__/normalizePath.spec.d.ts +1 -0
- package/dist/utils/__tests__/normalizePath.spec.js +22 -0
- package/package.json +3 -1
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { render } from 'ink';
|
|
2
|
+
import { createElement } from 'react';
|
|
3
|
+
import { Progress } from '../utils/progress.js';
|
|
4
|
+
import { computeSummary, printSummary } from '../utils/summary.js';
|
|
5
|
+
import { saveCacheAtomic } from './cache.js';
|
|
6
|
+
import { cleanupMutineerDirs } from './cleanup.js';
|
|
7
|
+
import { PoolSpinner } from '../utils/PoolSpinner.js';
|
|
8
|
+
import { createLogger } from '../utils/logger.js';
|
|
9
|
+
const log = createLogger('pool-executor');
|
|
10
|
+
/**
|
|
11
|
+
* Execute all mutant tasks through the worker pool.
|
|
12
|
+
* Handles worker init, progress display, signal handling, and cleanup.
|
|
13
|
+
*/
|
|
14
|
+
export async function executePool(opts) {
|
|
15
|
+
const { tasks, adapter, cache, concurrency, cwd } = opts;
|
|
16
|
+
const workerCount = Math.max(1, Math.min(concurrency, tasks.length));
|
|
17
|
+
const progress = new Progress(tasks.length, {
|
|
18
|
+
mode: opts.progressMode === 'bar' ? 'bar' : 'list',
|
|
19
|
+
stream: 'stderr',
|
|
20
|
+
});
|
|
21
|
+
const mutationStartTime = Date.now();
|
|
22
|
+
// Ensure we only finish once
|
|
23
|
+
let finished = false;
|
|
24
|
+
const finishOnce = () => {
|
|
25
|
+
if (finished)
|
|
26
|
+
return;
|
|
27
|
+
finished = true;
|
|
28
|
+
const durationMs = Date.now() - mutationStartTime;
|
|
29
|
+
progress.finish();
|
|
30
|
+
const summary = computeSummary(cache);
|
|
31
|
+
printSummary(summary, cache, durationMs);
|
|
32
|
+
if (opts.minKillPercent !== undefined) {
|
|
33
|
+
const killRateString = summary.killRate.toFixed(2);
|
|
34
|
+
const thresholdString = opts.minKillPercent.toFixed(2);
|
|
35
|
+
if (summary.killRate < opts.minKillPercent) {
|
|
36
|
+
const note = summary.evaluated === 0 ? ' No mutants were executed.' : '';
|
|
37
|
+
log.error(`Mutation kill rate ${killRateString}% did not meet required ${thresholdString}% threshold.${note}`);
|
|
38
|
+
process.exitCode = 1;
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
log.info(`Mutation kill rate ${killRateString}% meets required ${thresholdString}% threshold`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
// Initialize worker pool
|
|
46
|
+
const workerLogSuffix = workerCount < concurrency ? ` (requested ${concurrency})` : '';
|
|
47
|
+
log.info(`Initializing ${adapter.name} worker pool with ${workerCount} workers...${workerLogSuffix}`);
|
|
48
|
+
const poolStart = Date.now();
|
|
49
|
+
// Ink spinner on stderr while workers start up
|
|
50
|
+
let spinnerInstance = null;
|
|
51
|
+
if (process.stderr.isTTY) {
|
|
52
|
+
spinnerInstance = render(createElement(PoolSpinner, { message: 'Starting pool...' }), { stdout: process.stderr, stderr: process.stderr });
|
|
53
|
+
}
|
|
54
|
+
try {
|
|
55
|
+
await adapter.init(workerCount);
|
|
56
|
+
}
|
|
57
|
+
finally {
|
|
58
|
+
if (spinnerInstance) {
|
|
59
|
+
spinnerInstance.unmount();
|
|
60
|
+
spinnerInstance = null;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
const poolDurationMs = Date.now() - poolStart;
|
|
64
|
+
log.info(`\u2713 Worker pool ready (${poolDurationMs}ms)`);
|
|
65
|
+
progress.start();
|
|
66
|
+
let nextIdx = 0;
|
|
67
|
+
async function processTask(task) {
|
|
68
|
+
const { v, tests, key } = task;
|
|
69
|
+
log.debug('Cache ' + JSON.stringify(cache));
|
|
70
|
+
const cached = cache[key];
|
|
71
|
+
if (cached) {
|
|
72
|
+
progress.update(cached.status);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
if (tests.length === 0) {
|
|
76
|
+
cache[key] = {
|
|
77
|
+
status: 'skipped',
|
|
78
|
+
file: v.file,
|
|
79
|
+
line: v.line,
|
|
80
|
+
col: v.col,
|
|
81
|
+
mutator: v.name,
|
|
82
|
+
};
|
|
83
|
+
progress.update('skipped');
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
log.debug(`Running tests for mutant ${v.name} ${JSON.stringify(tests)}`);
|
|
87
|
+
const result = await adapter.runMutant({
|
|
88
|
+
id: v.id,
|
|
89
|
+
name: v.name,
|
|
90
|
+
file: v.file,
|
|
91
|
+
code: v.code,
|
|
92
|
+
line: v.line,
|
|
93
|
+
col: v.col,
|
|
94
|
+
}, tests);
|
|
95
|
+
const status = result.status;
|
|
96
|
+
cache[key] = {
|
|
97
|
+
status,
|
|
98
|
+
file: v.file,
|
|
99
|
+
line: v.line,
|
|
100
|
+
col: v.col,
|
|
101
|
+
mutator: v.name,
|
|
102
|
+
};
|
|
103
|
+
progress.update(status);
|
|
104
|
+
}
|
|
105
|
+
async function worker() {
|
|
106
|
+
while (true) {
|
|
107
|
+
const i = nextIdx++;
|
|
108
|
+
if (i >= tasks.length)
|
|
109
|
+
break;
|
|
110
|
+
await processTask(tasks[i]);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
const workers = [];
|
|
114
|
+
// Register signal handlers so Ctrl+C / SIGTERM still triggers cleanup
|
|
115
|
+
let signalCleanedUp = false;
|
|
116
|
+
const signalHandler = async (signal) => {
|
|
117
|
+
if (signalCleanedUp)
|
|
118
|
+
return;
|
|
119
|
+
signalCleanedUp = true;
|
|
120
|
+
log.info(`\nReceived ${signal}, cleaning up...`);
|
|
121
|
+
finishOnce();
|
|
122
|
+
await adapter.shutdown();
|
|
123
|
+
await cleanupMutineerDirs(cwd);
|
|
124
|
+
process.exit(1);
|
|
125
|
+
};
|
|
126
|
+
process.on('SIGINT', () => void signalHandler('SIGINT'));
|
|
127
|
+
process.on('SIGTERM', () => void signalHandler('SIGTERM'));
|
|
128
|
+
try {
|
|
129
|
+
for (let i = 0; i < workerCount; i++)
|
|
130
|
+
workers.push(worker());
|
|
131
|
+
await Promise.all(workers);
|
|
132
|
+
await saveCacheAtomic(cwd, cache);
|
|
133
|
+
}
|
|
134
|
+
finally {
|
|
135
|
+
process.removeAllListeners('SIGINT');
|
|
136
|
+
process.removeAllListeners('SIGTERM');
|
|
137
|
+
if (!signalCleanedUp) {
|
|
138
|
+
finishOnce();
|
|
139
|
+
await adapter.shutdown();
|
|
140
|
+
await cleanupMutineerDirs(cwd);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { describe, it, expect, afterEach } from 'vitest';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import { getMutantFilePath } from '../mutant-paths.js';
|
|
6
|
+
let createdDirs = [];
|
|
7
|
+
afterEach(() => {
|
|
8
|
+
// Clean up any __mutineer__ directories we created
|
|
9
|
+
for (const dir of createdDirs) {
|
|
10
|
+
try {
|
|
11
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
// ignore
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
createdDirs = [];
|
|
18
|
+
});
|
|
19
|
+
describe('getMutantFilePath', () => {
|
|
20
|
+
it('creates a mutant file path with numeric ID suffix', () => {
|
|
21
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mutineer-paths-'));
|
|
22
|
+
const srcFile = path.join(tmpDir, 'foo.ts');
|
|
23
|
+
fs.writeFileSync(srcFile, '');
|
|
24
|
+
const result = getMutantFilePath(srcFile, 'foo.ts#42');
|
|
25
|
+
createdDirs.push(path.join(tmpDir, '__mutineer__'));
|
|
26
|
+
expect(result).toBe(path.join(tmpDir, '__mutineer__', 'foo_42.ts'));
|
|
27
|
+
});
|
|
28
|
+
it('creates __mutineer__ directory if it does not exist', () => {
|
|
29
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mutineer-paths-'));
|
|
30
|
+
const srcFile = path.join(tmpDir, 'bar.ts');
|
|
31
|
+
fs.writeFileSync(srcFile, '');
|
|
32
|
+
const mutineerDir = path.join(tmpDir, '__mutineer__');
|
|
33
|
+
createdDirs.push(mutineerDir);
|
|
34
|
+
getMutantFilePath(srcFile, 'bar.ts#1');
|
|
35
|
+
expect(fs.existsSync(mutineerDir)).toBe(true);
|
|
36
|
+
});
|
|
37
|
+
it('handles non-numeric mutant IDs', () => {
|
|
38
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mutineer-paths-'));
|
|
39
|
+
const srcFile = path.join(tmpDir, 'baz.ts');
|
|
40
|
+
fs.writeFileSync(srcFile, '');
|
|
41
|
+
createdDirs.push(path.join(tmpDir, '__mutineer__'));
|
|
42
|
+
const result = getMutantFilePath(srcFile, 'some-weird-id');
|
|
43
|
+
expect(path.basename(result)).toMatch(/^baz_/);
|
|
44
|
+
expect(result).toMatch(/\.ts$/);
|
|
45
|
+
});
|
|
46
|
+
it('preserves file extension', () => {
|
|
47
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mutineer-paths-'));
|
|
48
|
+
const srcFile = path.join(tmpDir, 'component.vue');
|
|
49
|
+
fs.writeFileSync(srcFile, '');
|
|
50
|
+
createdDirs.push(path.join(tmpDir, '__mutineer__'));
|
|
51
|
+
const result = getMutantFilePath(srcFile, 'component.vue#5');
|
|
52
|
+
expect(result).toMatch(/\.vue$/);
|
|
53
|
+
expect(path.basename(result)).toBe('component_5.vue');
|
|
54
|
+
});
|
|
55
|
+
it('reuses existing __mutineer__ directory', () => {
|
|
56
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mutineer-paths-'));
|
|
57
|
+
const srcFile = path.join(tmpDir, 'foo.ts');
|
|
58
|
+
fs.writeFileSync(srcFile, '');
|
|
59
|
+
const mutineerDir = path.join(tmpDir, '__mutineer__');
|
|
60
|
+
fs.mkdirSync(mutineerDir);
|
|
61
|
+
createdDirs.push(mutineerDir);
|
|
62
|
+
// Should not throw
|
|
63
|
+
const result = getMutantFilePath(srcFile, 'foo.ts#1');
|
|
64
|
+
expect(result).toBe(path.join(mutineerDir, 'foo_1.ts'));
|
|
65
|
+
});
|
|
66
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
+
import { initializeRedirectState, setRedirect, getRedirect, clearRedirect, } from '../redirect-state.js';
|
|
3
|
+
describe('redirect-state', () => {
|
|
4
|
+
beforeEach(() => {
|
|
5
|
+
initializeRedirectState();
|
|
6
|
+
});
|
|
7
|
+
describe('initializeRedirectState', () => {
|
|
8
|
+
it('sets global redirect to null/null', () => {
|
|
9
|
+
expect(globalThis.__mutineer_redirect__).toEqual({
|
|
10
|
+
from: null,
|
|
11
|
+
to: null,
|
|
12
|
+
});
|
|
13
|
+
});
|
|
14
|
+
});
|
|
15
|
+
describe('setRedirect', () => {
|
|
16
|
+
it('sets the redirect config', () => {
|
|
17
|
+
setRedirect({ from: '/src/foo.ts', to: '/tmp/mutant.ts' });
|
|
18
|
+
expect(globalThis.__mutineer_redirect__).toEqual({
|
|
19
|
+
from: '/src/foo.ts',
|
|
20
|
+
to: '/tmp/mutant.ts',
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
describe('getRedirect', () => {
|
|
25
|
+
it('returns null when no redirect is set', () => {
|
|
26
|
+
expect(getRedirect()).toBeNull();
|
|
27
|
+
});
|
|
28
|
+
it('returns the redirect config when set', () => {
|
|
29
|
+
setRedirect({ from: '/src/foo.ts', to: '/tmp/mutant.ts' });
|
|
30
|
+
const redirect = getRedirect();
|
|
31
|
+
expect(redirect).toEqual({
|
|
32
|
+
from: '/src/foo.ts',
|
|
33
|
+
to: '/tmp/mutant.ts',
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
it('returns null when from is null', () => {
|
|
37
|
+
globalThis.__mutineer_redirect__ = { from: null, to: '/tmp/mutant.ts' };
|
|
38
|
+
expect(getRedirect()).toBeNull();
|
|
39
|
+
});
|
|
40
|
+
it('returns null when to is null', () => {
|
|
41
|
+
globalThis.__mutineer_redirect__ = { from: '/src/foo.ts', to: null };
|
|
42
|
+
expect(getRedirect()).toBeNull();
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
describe('clearRedirect', () => {
|
|
46
|
+
it('resets the redirect to null/null', () => {
|
|
47
|
+
setRedirect({ from: '/src/foo.ts', to: '/tmp/mutant.ts' });
|
|
48
|
+
clearRedirect();
|
|
49
|
+
expect(getRedirect()).toBeNull();
|
|
50
|
+
expect(globalThis.__mutineer_redirect__).toEqual({
|
|
51
|
+
from: null,
|
|
52
|
+
to: null,
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Variant } from '../types/mutant.js';
|
|
2
|
+
import type { PerTestCoverageMap } from '../utils/coverage.js';
|
|
3
|
+
export interface MutantTask {
|
|
4
|
+
v: Variant;
|
|
5
|
+
tests: string[];
|
|
6
|
+
key: string;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Prepare mutant tasks from variants by pruning tests via per-test coverage,
|
|
10
|
+
* sorting tests deterministically, and computing cache keys.
|
|
11
|
+
*/
|
|
12
|
+
export declare function prepareTasks(variants: readonly Variant[], perTestCoverage: PerTestCoverageMap | null): MutantTask[];
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { filterTestsByCoverage } from './variants.js';
|
|
2
|
+
import { hash, keyForTests } from './cache.js';
|
|
3
|
+
import { createLogger } from '../utils/logger.js';
|
|
4
|
+
const log = createLogger('tasks');
|
|
5
|
+
/**
|
|
6
|
+
* Prepare mutant tasks from variants by pruning tests via per-test coverage,
|
|
7
|
+
* sorting tests deterministically, and computing cache keys.
|
|
8
|
+
*/
|
|
9
|
+
export function prepareTasks(variants, perTestCoverage) {
|
|
10
|
+
return variants.map((v) => {
|
|
11
|
+
let tests = Array.from(v.tests);
|
|
12
|
+
if (perTestCoverage && tests.length) {
|
|
13
|
+
const before = tests.length;
|
|
14
|
+
tests = filterTestsByCoverage(perTestCoverage, tests, v.file, v.line);
|
|
15
|
+
if (tests.length !== before) {
|
|
16
|
+
log.debug(`Pruned tests ${before} -> ${tests.length} for mutant ${v.name} via per-test coverage`);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
tests.sort();
|
|
20
|
+
const testSig = hash(keyForTests(tests));
|
|
21
|
+
const codeSig = hash(v.code);
|
|
22
|
+
const key = `${testSig}:${codeSig}`;
|
|
23
|
+
return { v, tests, key };
|
|
24
|
+
});
|
|
25
|
+
}
|
|
@@ -4,8 +4,10 @@
|
|
|
4
4
|
* Functions for enumerating mutation variants from source files.
|
|
5
5
|
* Handles both regular modules and Vue SFC files.
|
|
6
6
|
*/
|
|
7
|
-
import type { MutateTarget } from '../types/config.js';
|
|
8
|
-
import type { MutantPayload } from '../types/mutant.js';
|
|
7
|
+
import type { MutateTarget, MutineerConfig } from '../types/config.js';
|
|
8
|
+
import type { MutantPayload, Variant } from '../types/mutant.js';
|
|
9
|
+
import { type CoverageData } from '../utils/coverage.js';
|
|
10
|
+
import type { TestMap } from './discover.js';
|
|
9
11
|
/**
|
|
10
12
|
* Get file path from target (handles both string and object forms).
|
|
11
13
|
*/
|
|
@@ -18,4 +20,17 @@ export declare function enumerateVariantsForTarget(root: string, t: MutateTarget
|
|
|
18
20
|
* Filter tests to only those that cover a specific line in a file.
|
|
19
21
|
*/
|
|
20
22
|
export declare function filterTestsByCoverage(perTest: Map<string, Map<string, Set<number>>>, tests: readonly string[], filePath: string, line: number): string[];
|
|
23
|
+
export interface EnumerateAllParams {
|
|
24
|
+
cwd: string;
|
|
25
|
+
targets: readonly MutateTarget[];
|
|
26
|
+
testMap: TestMap;
|
|
27
|
+
changedFiles: Set<string> | null;
|
|
28
|
+
coverageData: CoverageData | null;
|
|
29
|
+
config: MutineerConfig;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Enumerate variants for all targets, filtering by changed files and coverage.
|
|
33
|
+
* Links each variant to its relevant test files via the testMap.
|
|
34
|
+
*/
|
|
35
|
+
export declare function enumerateAllVariants(params: EnumerateAllParams): Promise<Variant[]>;
|
|
21
36
|
export type { Variant } from '../types/mutant.js';
|
package/dist/runner/variants.js
CHANGED
|
@@ -8,6 +8,8 @@ import fs from 'node:fs/promises';
|
|
|
8
8
|
import path from 'node:path';
|
|
9
9
|
import { mutateVueSfcScriptSetup } from '../core/sfc.js';
|
|
10
10
|
import { mutateModuleSource } from '../core/module.js';
|
|
11
|
+
import { normalizePath } from '../utils/normalizePath.js';
|
|
12
|
+
import { isLineCovered } from '../utils/coverage.js';
|
|
11
13
|
import { createLogger } from '../utils/logger.js';
|
|
12
14
|
const log = createLogger('variants');
|
|
13
15
|
/**
|
|
@@ -64,3 +66,34 @@ export function filterTestsByCoverage(perTest, tests, filePath, line) {
|
|
|
64
66
|
return lines.has(line);
|
|
65
67
|
});
|
|
66
68
|
}
|
|
69
|
+
/**
|
|
70
|
+
* Enumerate variants for all targets, filtering by changed files and coverage.
|
|
71
|
+
* Links each variant to its relevant test files via the testMap.
|
|
72
|
+
*/
|
|
73
|
+
export async function enumerateAllVariants(params) {
|
|
74
|
+
const { cwd, targets, testMap, changedFiles, coverageData, config } = params;
|
|
75
|
+
const enumerated = await Promise.all(targets.map(async (target) => {
|
|
76
|
+
const file = getTargetFile(target);
|
|
77
|
+
const absFile = normalizePath(path.isAbsolute(file) ? file : path.join(cwd, file));
|
|
78
|
+
if (changedFiles && !changedFiles.has(absFile))
|
|
79
|
+
return [];
|
|
80
|
+
log.debug('Target file: ' + absFile);
|
|
81
|
+
const files = await enumerateVariantsForTarget(cwd, target, config.include, config.exclude, config.maxMutantsPerFile);
|
|
82
|
+
const testsAbs = testMap.get(normalizePath(absFile));
|
|
83
|
+
const tests = testsAbs ? Array.from(testsAbs) : [];
|
|
84
|
+
log.debug(` found ${files.length} variants, linked to ${tests.length} tests`);
|
|
85
|
+
// Filter by coverage if enabled
|
|
86
|
+
let filtered = files;
|
|
87
|
+
if (coverageData) {
|
|
88
|
+
filtered = files.filter((v) => isLineCovered(coverageData, absFile, v.line));
|
|
89
|
+
if (filtered.length !== files.length) {
|
|
90
|
+
log.debug(` filtered ${files.length} -> ${filtered.length} variants by coverage`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return filtered.map((v) => ({ ...v, tests }));
|
|
94
|
+
}));
|
|
95
|
+
const variants = [];
|
|
96
|
+
for (const list of enumerated)
|
|
97
|
+
variants.push(...list);
|
|
98
|
+
return variants;
|
|
99
|
+
}
|
|
@@ -3,6 +3,10 @@ import fs from 'node:fs/promises';
|
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import os from 'node:os';
|
|
5
5
|
import { pathToFileURL } from 'node:url';
|
|
6
|
+
vi.mock('node:module', async (importOriginal) => {
|
|
7
|
+
const actual = await importOriginal();
|
|
8
|
+
return { ...actual, register: vi.fn() };
|
|
9
|
+
});
|
|
6
10
|
import { resolve as poolResolve } from '../redirect-loader.js';
|
|
7
11
|
describe('pool-redirect-loader resolve', () => {
|
|
8
12
|
afterEach(() => {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
// We need to test with different DEBUG values, so we mock the module dynamically
|
|
3
|
+
describe('logger', () => {
|
|
4
|
+
let originalDebug;
|
|
5
|
+
beforeEach(() => {
|
|
6
|
+
originalDebug = process.env.MUTINEER_DEBUG;
|
|
7
|
+
vi.restoreAllMocks();
|
|
8
|
+
});
|
|
9
|
+
afterEach(() => {
|
|
10
|
+
if (originalDebug === undefined) {
|
|
11
|
+
delete process.env.MUTINEER_DEBUG;
|
|
12
|
+
}
|
|
13
|
+
else {
|
|
14
|
+
process.env.MUTINEER_DEBUG = originalDebug;
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
it('info logs to console.log', async () => {
|
|
18
|
+
const spy = vi.spyOn(console, 'log').mockImplementation(() => { });
|
|
19
|
+
const { createLogger } = await import('../logger.js');
|
|
20
|
+
const log = createLogger('test');
|
|
21
|
+
log.info('hello %s', 'world');
|
|
22
|
+
expect(spy).toHaveBeenCalledWith('hello %s', 'world');
|
|
23
|
+
});
|
|
24
|
+
it('warn logs to console.warn', async () => {
|
|
25
|
+
const spy = vi.spyOn(console, 'warn').mockImplementation(() => { });
|
|
26
|
+
const { createLogger } = await import('../logger.js');
|
|
27
|
+
const log = createLogger('test');
|
|
28
|
+
log.warn('warning message');
|
|
29
|
+
expect(spy).toHaveBeenCalledWith('warning message');
|
|
30
|
+
});
|
|
31
|
+
it('error logs to console.error', async () => {
|
|
32
|
+
const spy = vi.spyOn(console, 'error').mockImplementation(() => { });
|
|
33
|
+
const { createLogger } = await import('../logger.js');
|
|
34
|
+
const log = createLogger('test');
|
|
35
|
+
log.error('error message');
|
|
36
|
+
expect(spy).toHaveBeenCalledWith('error message');
|
|
37
|
+
});
|
|
38
|
+
it('debug logs to console.error when DEBUG is enabled', async () => {
|
|
39
|
+
const spy = vi.spyOn(console, 'error').mockImplementation(() => { });
|
|
40
|
+
// DEBUG is a module-level constant, so we check the tag prefix behavior
|
|
41
|
+
const { createLogger, DEBUG } = await import('../logger.js');
|
|
42
|
+
const log = createLogger('mytag');
|
|
43
|
+
log.debug('debug message');
|
|
44
|
+
if (DEBUG) {
|
|
45
|
+
expect(spy).toHaveBeenCalledWith('[mytag] debug message');
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
// When DEBUG is false, debug should be a no-op
|
|
49
|
+
expect(spy).not.toHaveBeenCalled();
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
it('debug includes the tag prefix', async () => {
|
|
53
|
+
const spy = vi.spyOn(console, 'error').mockImplementation(() => { });
|
|
54
|
+
const { createLogger, DEBUG } = await import('../logger.js');
|
|
55
|
+
const log = createLogger('custom-tag');
|
|
56
|
+
log.debug('test');
|
|
57
|
+
if (DEBUG) {
|
|
58
|
+
expect(spy).toHaveBeenCalledWith(expect.stringContaining('[custom-tag]'));
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { normalizePath } from '../normalizePath.js';
|
|
3
|
+
describe('normalizePath', () => {
|
|
4
|
+
it('replaces backslashes with forward slashes', () => {
|
|
5
|
+
expect(normalizePath('src\\utils\\file.ts')).toBe('src/utils/file.ts');
|
|
6
|
+
});
|
|
7
|
+
it('handles multiple consecutive backslashes', () => {
|
|
8
|
+
expect(normalizePath('src\\\\file.ts')).toBe('src//file.ts');
|
|
9
|
+
});
|
|
10
|
+
it('leaves forward slashes unchanged', () => {
|
|
11
|
+
expect(normalizePath('src/utils/file.ts')).toBe('src/utils/file.ts');
|
|
12
|
+
});
|
|
13
|
+
it('handles empty string', () => {
|
|
14
|
+
expect(normalizePath('')).toBe('');
|
|
15
|
+
});
|
|
16
|
+
it('handles mixed slashes', () => {
|
|
17
|
+
expect(normalizePath('src\\utils/file.ts')).toBe('src/utils/file.ts');
|
|
18
|
+
});
|
|
19
|
+
it('handles Windows-style absolute paths', () => {
|
|
20
|
+
expect(normalizePath('C:\\Users\\dev\\project')).toBe('C:/Users/dev/project');
|
|
21
|
+
});
|
|
22
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mutineerjs/mutineer",
|
|
3
|
-
"version": "v0.2.
|
|
3
|
+
"version": "v0.2.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"private": false,
|
|
6
6
|
"bin": {
|
|
@@ -62,6 +62,8 @@
|
|
|
62
62
|
"eslint": "^9.39.1",
|
|
63
63
|
"jsdom": "^27.0.0",
|
|
64
64
|
"typescript": "^5.5.4",
|
|
65
|
+
"vite": "^6.3.6",
|
|
66
|
+
"vitest": "^4.0.15",
|
|
65
67
|
"vue": "^3.5.12"
|
|
66
68
|
}
|
|
67
69
|
}
|