@mutineerjs/mutineer 0.2.3 → 0.3.2
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 +48 -42
- package/dist/bin/mutineer.js +0 -0
- package/dist/core/__tests__/module.spec.js +66 -3
- package/dist/core/__tests__/sfc.spec.js +76 -0
- package/dist/core/__tests__/variant-utils.spec.js +93 -0
- package/dist/mutators/__tests__/operator.spec.js +169 -0
- package/dist/mutators/__tests__/registry.spec.js +6 -0
- package/dist/mutators/__tests__/return-value.spec.js +239 -0
- package/dist/mutators/__tests__/utils.spec.js +68 -1
- package/dist/mutators/operator.d.ts +25 -0
- package/dist/mutators/operator.js +50 -0
- package/dist/mutators/registry.d.ts +6 -28
- package/dist/mutators/registry.js +14 -66
- package/dist/mutators/return-value.d.ts +39 -0
- package/dist/mutators/return-value.js +104 -0
- package/dist/mutators/utils.d.ts +21 -0
- package/dist/mutators/utils.js +44 -27
- package/dist/runner/__tests__/args.spec.js +225 -0
- package/dist/runner/__tests__/cache.spec.js +180 -0
- package/dist/runner/__tests__/changed.spec.js +227 -0
- package/dist/runner/__tests__/cleanup.spec.js +41 -0
- package/dist/runner/__tests__/config.spec.js +71 -0
- package/dist/runner/__tests__/coverage-resolver.spec.js +171 -0
- package/dist/runner/__tests__/pool-executor.spec.js +211 -0
- package/dist/runner/__tests__/tasks.spec.js +95 -0
- package/dist/runner/__tests__/variants.spec.js +261 -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/discover.js +2 -1
- package/dist/runner/jest/__tests__/adapter.spec.js +1 -1
- package/dist/runner/jest/__tests__/pool.spec.d.ts +1 -0
- package/dist/runner/jest/__tests__/pool.spec.js +211 -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/jest/adapter.js +1 -1
- package/dist/runner/jest/pool.d.ts +1 -1
- package/dist/runner/jest/pool.js +6 -6
- package/dist/runner/jest/worker.mjs +1 -1
- 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/shared/index.d.ts +1 -1
- package/dist/runner/shared/index.js +1 -1
- package/dist/runner/shared/redirect-state.d.ts +2 -2
- package/dist/runner/shared/redirect-state.js +4 -4
- package/dist/runner/tasks.d.ts +12 -0
- package/dist/runner/tasks.js +25 -0
- package/dist/runner/types.d.ts +1 -1
- package/dist/runner/variants.d.ts +17 -2
- package/dist/runner/variants.js +33 -0
- package/dist/runner/vitest/__tests__/adapter.spec.js +1 -1
- package/dist/runner/vitest/__tests__/pool.spec.js +1 -1
- package/dist/runner/vitest/__tests__/redirect-loader.spec.js +87 -1
- package/dist/runner/vitest/__tests__/worker-runtime.spec.js +84 -0
- package/dist/runner/vitest/adapter.js +1 -1
- package/dist/runner/vitest/index.d.ts +0 -1
- package/dist/runner/vitest/index.js +0 -1
- package/dist/runner/vitest/pool.d.ts +1 -1
- package/dist/runner/vitest/pool.js +7 -7
- package/dist/runner/vitest/redirect-loader.d.ts +1 -1
- package/dist/runner/vitest/redirect-loader.js +1 -1
- package/dist/runner/vitest/worker-runtime.js +3 -3
- package/dist/runner/vitest/worker.mjs +1 -1
- package/dist/utils/__tests__/coverage.spec.js +167 -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/dist/utils/__tests__/progress.spec.js +96 -0
- package/package.json +71 -22
- package/dist/admin/assets/index-B7nXq-e7.js +0 -32
- package/dist/admin/assets/index-B7nXq-e7.js.map +0 -1
- package/dist/admin/assets/index-BDQLkBUE.js +0 -32
- package/dist/admin/assets/index-BDQLkBUE.js.map +0 -1
- package/dist/admin/assets/index-DVkP-Tc7.css +0 -1
- package/dist/admin/index.html +0 -13
- package/dist/admin/server/admin.d.ts +0 -6
- package/dist/admin/server/admin.js +0 -234
- package/dist/bin/mutate-vitest.d.ts +0 -2
- package/dist/bin/mutate-vitest.js +0 -90
- package/dist/plugin/viteMutate.d.ts +0 -15
- package/dist/plugin/viteMutate.js +0 -52
- package/dist/plugin/vitest.setup.d.ts +0 -47
- package/dist/plugin/vitest.setup.js +0 -118
- package/dist/plugin/withVitest.d.ts +0 -13
- package/dist/plugin/withVitest.js +0 -30
- package/dist/runner/__tests__/orchestrator.spec.js +0 -55
- package/dist/runner/adapters/__tests__/jest.spec.js +0 -88
- package/dist/runner/adapters/__tests__/vitest-worker-runtime.spec.js +0 -59
- package/dist/runner/adapters/__tests__/vitest.spec.js +0 -118
- package/dist/runner/adapters/index.d.ts +0 -10
- package/dist/runner/adapters/index.js +0 -9
- package/dist/runner/adapters/jest/__tests__/index.spec.js +0 -88
- package/dist/runner/adapters/jest/index.d.ts +0 -24
- package/dist/runner/adapters/jest/index.js +0 -216
- package/dist/runner/adapters/jest/worker-runtime.d.ts +0 -37
- package/dist/runner/adapters/jest/worker-runtime.js +0 -171
- package/dist/runner/adapters/jest-worker-runtime.d.ts +0 -37
- package/dist/runner/adapters/jest-worker-runtime.js +0 -171
- package/dist/runner/adapters/jest.d.ts +0 -24
- package/dist/runner/adapters/jest.js +0 -216
- package/dist/runner/adapters/types.d.ts +0 -89
- package/dist/runner/adapters/types.js +0 -8
- package/dist/runner/adapters/vitest/__tests__/index.spec.js +0 -118
- package/dist/runner/adapters/vitest/__tests__/worker-runtime.spec.js +0 -59
- package/dist/runner/adapters/vitest/index.d.ts +0 -33
- package/dist/runner/adapters/vitest/index.js +0 -267
- package/dist/runner/adapters/vitest/worker-runtime.d.ts +0 -25
- package/dist/runner/adapters/vitest/worker-runtime.js +0 -118
- package/dist/runner/adapters/vitest-worker-runtime.d.ts +0 -25
- package/dist/runner/adapters/vitest-worker-runtime.js +0 -118
- package/dist/runner/adapters/vitest.d.ts +0 -33
- package/dist/runner/adapters/vitest.js +0 -267
- package/dist/runner/pool/__tests__/index.spec.js +0 -83
- package/dist/runner/pool/__tests__/pool-plugin.spec.js +0 -59
- package/dist/runner/pool/__tests__/pool-redirect-loader.spec.js +0 -78
- package/dist/runner/pool/index.d.ts +0 -8
- package/dist/runner/pool/index.js +0 -9
- package/dist/runner/pool/jest/pool.d.ts +0 -52
- package/dist/runner/pool/jest/pool.js +0 -309
- package/dist/runner/pool/jest/worker.mjs +0 -60
- package/dist/runner/pool/jest-pool.d.ts +0 -52
- package/dist/runner/pool/jest-pool.js +0 -309
- package/dist/runner/pool/jest-worker.mjs +0 -60
- package/dist/runner/pool/plugin.d.ts +0 -18
- package/dist/runner/pool/plugin.js +0 -60
- package/dist/runner/pool/pool-plugin.d.ts +0 -18
- package/dist/runner/pool/pool-plugin.js +0 -60
- package/dist/runner/pool/pool-redirect-loader.d.ts +0 -19
- package/dist/runner/pool/pool-redirect-loader.js +0 -116
- package/dist/runner/pool/pool-redirect-loader.mjs +0 -146
- package/dist/runner/pool/redirect-loader.d.ts +0 -19
- package/dist/runner/pool/redirect-loader.js +0 -116
- package/dist/runner/pool/vitest/pool.d.ts +0 -70
- package/dist/runner/pool/vitest/pool.js +0 -376
- package/dist/runner/pool/vitest/worker.d.mts +0 -15
- package/dist/runner/pool/vitest/worker.mjs +0 -96
- package/dist/runner/pool/vitest-worker.d.mts +0 -15
- package/dist/runner/pool/vitest-worker.mjs +0 -96
- package/dist/runner/shared-module-redirect.d.ts +0 -56
- package/dist/runner/shared-module-redirect.js +0 -84
- package/dist/types/api.d.ts +0 -20
- /package/dist/{runner/__tests__/orchestrator.spec.d.ts → core/__tests__/sfc.spec.d.ts} +0 -0
- /package/dist/{runner/adapters/__tests__/jest.spec.d.ts → core/__tests__/variant-utils.spec.d.ts} +0 -0
- /package/dist/{runner/adapters/__tests__/vitest-worker-runtime.spec.d.ts → mutators/__tests__/operator.spec.d.ts} +0 -0
- /package/dist/{runner/adapters/__tests__/vitest.spec.d.ts → mutators/__tests__/return-value.spec.d.ts} +0 -0
- /package/dist/runner/{adapters/jest/__tests__/index.spec.d.ts → __tests__/args.spec.d.ts} +0 -0
- /package/dist/runner/{adapters/vitest/__tests__/index.spec.d.ts → __tests__/cache.spec.d.ts} +0 -0
- /package/dist/runner/{adapters/vitest/__tests__/worker-runtime.spec.d.ts → __tests__/changed.spec.d.ts} +0 -0
- /package/dist/runner/{pool/__tests__/index.spec.d.ts → __tests__/cleanup.spec.d.ts} +0 -0
- /package/dist/runner/{pool/__tests__/pool-plugin.spec.d.ts → __tests__/config.spec.d.ts} +0 -0
- /package/dist/runner/{pool/__tests__/pool-redirect-loader.spec.d.ts → __tests__/coverage-resolver.spec.d.ts} +0 -0
- /package/dist/runner/{pool/jest-worker.d.mts → __tests__/pool-executor.spec.d.ts} +0 -0
- /package/dist/runner/{pool/jest/worker.d.mts → __tests__/tasks.spec.d.ts} +0 -0
- /package/dist/{types/api.js → runner/__tests__/variants.spec.d.ts} +0 -0
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { loadCoverageData, loadPerTestCoverageData, } from '../utils/coverage.js';
|
|
3
|
+
import { isCoverageRequestedInArgs } from './vitest/index.js';
|
|
4
|
+
import { createLogger } from '../utils/logger.js';
|
|
5
|
+
const log = createLogger('coverage-resolver');
|
|
6
|
+
/**
|
|
7
|
+
* Resolve all coverage-related configuration from CLI options, config, and adapter detection.
|
|
8
|
+
* Returns a unified resolution object used by the orchestrator.
|
|
9
|
+
*/
|
|
10
|
+
export async function resolveCoverageConfig(opts, cfg, adapter, cliArgs) {
|
|
11
|
+
const coverageConfig = await adapter.detectCoverageConfig();
|
|
12
|
+
const wantsPerTestCoverageFromConfig = coverageConfig.perTestEnabled;
|
|
13
|
+
const coveragePreference = cfg.coverage;
|
|
14
|
+
const wantsCoverageRun = coveragePreference === true
|
|
15
|
+
? true
|
|
16
|
+
: coveragePreference === false
|
|
17
|
+
? false
|
|
18
|
+
: isCoverageRequestedInArgs([...cliArgs]) || coverageConfig.coverageEnabled;
|
|
19
|
+
// Load pre-existing coverage data if provided
|
|
20
|
+
let coverageData = null;
|
|
21
|
+
if (opts.coverageFilePath) {
|
|
22
|
+
log.info(`Loading coverage data from ${opts.coverageFilePath}...`);
|
|
23
|
+
coverageData = await loadCoverageData(opts.coverageFilePath, process.cwd());
|
|
24
|
+
log.info(`Loaded coverage for ${coverageData.coveredLines.size} files`);
|
|
25
|
+
}
|
|
26
|
+
const needsCoverageFromBaseline = opts.wantsOnlyCoveredLines && !coverageData;
|
|
27
|
+
const hasCoverageProviderInstalled = adapter.hasCoverageProvider();
|
|
28
|
+
const rawPerTestCoverage = opts.wantsPerTestCoverage ||
|
|
29
|
+
wantsPerTestCoverageFromConfig ||
|
|
30
|
+
(opts.wantsOnlyCoveredLines && hasCoverageProviderInstalled);
|
|
31
|
+
const wantsPerTestCoverage = opts.runner === 'jest' ? false : rawPerTestCoverage;
|
|
32
|
+
if (opts.runner === 'jest' && rawPerTestCoverage) {
|
|
33
|
+
log.warn('Per-test coverage is not supported for Jest; continuing without per-test coverage.');
|
|
34
|
+
}
|
|
35
|
+
if (needsCoverageFromBaseline && !hasCoverageProviderInstalled) {
|
|
36
|
+
log.warn('The "onlyCoveredLines" option requires a coverage provider to generate coverage data.');
|
|
37
|
+
log.warn('Please install the appropriate coverage package (or disable onlyCoveredLines).');
|
|
38
|
+
process.exitCode = 1;
|
|
39
|
+
return {
|
|
40
|
+
coverageData: null,
|
|
41
|
+
perTestCoverage: null,
|
|
42
|
+
enableCoverageForBaseline: false,
|
|
43
|
+
wantsPerTestCoverage: false,
|
|
44
|
+
needsCoverageFromBaseline,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
if (opts.wantsOnlyCoveredLines &&
|
|
48
|
+
coverageData &&
|
|
49
|
+
!hasCoverageProviderInstalled) {
|
|
50
|
+
log.warn('The "onlyCoveredLines" option is enabled, but no coverage provider is installed.');
|
|
51
|
+
log.warn('Running baseline tests without injecting per-test coverage; existing coverageFile will be used for filtering.');
|
|
52
|
+
}
|
|
53
|
+
const enableCoverageForBaseline = needsCoverageFromBaseline ||
|
|
54
|
+
wantsPerTestCoverage ||
|
|
55
|
+
wantsCoverageRun ||
|
|
56
|
+
(opts.wantsOnlyCoveredLines && hasCoverageProviderInstalled);
|
|
57
|
+
return {
|
|
58
|
+
coverageData,
|
|
59
|
+
perTestCoverage: null,
|
|
60
|
+
enableCoverageForBaseline,
|
|
61
|
+
wantsPerTestCoverage,
|
|
62
|
+
needsCoverageFromBaseline,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Load coverage data produced during the baseline run.
|
|
67
|
+
* Mutates and returns an updated CoverageResolution.
|
|
68
|
+
*/
|
|
69
|
+
export async function loadCoverageAfterBaseline(resolution, cwd) {
|
|
70
|
+
let { coverageData, perTestCoverage } = resolution;
|
|
71
|
+
if (resolution.needsCoverageFromBaseline) {
|
|
72
|
+
const defaultCoveragePath = path.join(cwd, 'coverage', 'coverage-final.json');
|
|
73
|
+
log.info(`Loading coverage data from ${defaultCoveragePath}...`);
|
|
74
|
+
try {
|
|
75
|
+
coverageData = await loadCoverageData(defaultCoveragePath, cwd);
|
|
76
|
+
log.info(`Loaded coverage for ${coverageData.coveredLines.size} files`);
|
|
77
|
+
}
|
|
78
|
+
catch (err) {
|
|
79
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
80
|
+
log.warn(`Warning: Could not load coverage data: ${msg}`);
|
|
81
|
+
log.warn('Continuing without coverage filtering.');
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (resolution.wantsPerTestCoverage) {
|
|
85
|
+
const reportsDir = path.join(cwd, 'coverage');
|
|
86
|
+
log.info('Loading per-test coverage data...');
|
|
87
|
+
perTestCoverage = await loadPerTestCoverageData(reportsDir, cwd);
|
|
88
|
+
if (!perTestCoverage) {
|
|
89
|
+
log.warn('Per-test coverage data not found. Continuing without per-test test pruning.');
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
log.info(`Loaded per-test coverage for ${perTestCoverage.size} tests`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return { ...resolution, coverageData, perTestCoverage };
|
|
96
|
+
}
|
package/dist/runner/discover.js
CHANGED
|
@@ -85,7 +85,8 @@ async function createViteResolver(rootAbs, exts) {
|
|
|
85
85
|
let plugins = [];
|
|
86
86
|
if (exts.has('.vue')) {
|
|
87
87
|
try {
|
|
88
|
-
const mod = await import(
|
|
88
|
+
const mod = await import(
|
|
89
|
+
/* @vite-ignore */ '@vitejs/plugin-vue');
|
|
89
90
|
const vue = mod.default ?? mod;
|
|
90
91
|
plugins = typeof vue === 'function' ? [vue()] : [];
|
|
91
92
|
}
|
|
@@ -39,7 +39,7 @@ describe('Jest adapter', () => {
|
|
|
39
39
|
afterEach(() => {
|
|
40
40
|
vi.useRealTimers();
|
|
41
41
|
});
|
|
42
|
-
it('
|
|
42
|
+
it('initialises pool with override concurrency', async () => {
|
|
43
43
|
const adapter = makeAdapter();
|
|
44
44
|
await adapter.init(4);
|
|
45
45
|
expect(poolInstance?.init).toHaveBeenCalledTimes(1);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
import { JestPool, runWithJestPool } from '../pool.js';
|
|
3
|
+
// We'll use the createWorker option to inject mock workers instead of forking processes
|
|
4
|
+
function makeMockWorker(id) {
|
|
5
|
+
const worker = {
|
|
6
|
+
id,
|
|
7
|
+
_ready: true,
|
|
8
|
+
_busy: false,
|
|
9
|
+
_mockResult: { killed: true, durationMs: 10 },
|
|
10
|
+
on: vi.fn(),
|
|
11
|
+
once: vi.fn(),
|
|
12
|
+
isReady: vi.fn(() => worker._ready),
|
|
13
|
+
isBusy: vi.fn(() => worker._busy),
|
|
14
|
+
start: vi.fn(async () => {
|
|
15
|
+
// Simulate worker startup
|
|
16
|
+
}),
|
|
17
|
+
run: vi.fn(async () => worker._mockResult),
|
|
18
|
+
shutdown: vi.fn(async () => { }),
|
|
19
|
+
kill: vi.fn(),
|
|
20
|
+
emit: vi.fn(),
|
|
21
|
+
};
|
|
22
|
+
return worker;
|
|
23
|
+
}
|
|
24
|
+
const dummyMutant = {
|
|
25
|
+
id: 'test#1',
|
|
26
|
+
name: 'flipEQ',
|
|
27
|
+
file: '/src/foo.ts',
|
|
28
|
+
code: 'mutated',
|
|
29
|
+
line: 1,
|
|
30
|
+
col: 0,
|
|
31
|
+
};
|
|
32
|
+
describe('JestPool', () => {
|
|
33
|
+
it('throws if run is called before init', async () => {
|
|
34
|
+
const pool = new JestPool({ cwd: '/tmp', concurrency: 1 });
|
|
35
|
+
await expect(pool.run(dummyMutant, ['test.ts'])).rejects.toThrow('Pool not initialised');
|
|
36
|
+
});
|
|
37
|
+
it('initialises with the specified concurrency', async () => {
|
|
38
|
+
const workers = [];
|
|
39
|
+
const pool = new JestPool({
|
|
40
|
+
cwd: '/tmp',
|
|
41
|
+
concurrency: 2,
|
|
42
|
+
createWorker: (id) => {
|
|
43
|
+
const w = makeMockWorker(id);
|
|
44
|
+
workers.push(w);
|
|
45
|
+
return w;
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
await pool.init();
|
|
49
|
+
expect(workers).toHaveLength(2);
|
|
50
|
+
expect(workers[0].start).toHaveBeenCalled();
|
|
51
|
+
expect(workers[1].start).toHaveBeenCalled();
|
|
52
|
+
});
|
|
53
|
+
it('does not re-initialise if already initialised', async () => {
|
|
54
|
+
const workers = [];
|
|
55
|
+
const pool = new JestPool({
|
|
56
|
+
cwd: '/tmp',
|
|
57
|
+
concurrency: 1,
|
|
58
|
+
createWorker: (id) => {
|
|
59
|
+
const w = makeMockWorker(id);
|
|
60
|
+
workers.push(w);
|
|
61
|
+
return w;
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
await pool.init();
|
|
65
|
+
await pool.init(); // second call should be no-op
|
|
66
|
+
expect(workers).toHaveLength(1);
|
|
67
|
+
});
|
|
68
|
+
it('runs a mutant via a worker', async () => {
|
|
69
|
+
const workers = [];
|
|
70
|
+
const pool = new JestPool({
|
|
71
|
+
cwd: '/tmp',
|
|
72
|
+
concurrency: 1,
|
|
73
|
+
createWorker: (id) => {
|
|
74
|
+
const w = makeMockWorker(id);
|
|
75
|
+
w._mockResult = { killed: true, durationMs: 50 };
|
|
76
|
+
workers.push(w);
|
|
77
|
+
return w;
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
await pool.init();
|
|
81
|
+
const result = await pool.run(dummyMutant, ['test.ts']);
|
|
82
|
+
expect(result.killed).toBe(true);
|
|
83
|
+
expect(result.durationMs).toBe(50);
|
|
84
|
+
});
|
|
85
|
+
it('shuts down all workers', async () => {
|
|
86
|
+
const workers = [];
|
|
87
|
+
const pool = new JestPool({
|
|
88
|
+
cwd: '/tmp',
|
|
89
|
+
concurrency: 2,
|
|
90
|
+
createWorker: (id) => {
|
|
91
|
+
const w = makeMockWorker(id);
|
|
92
|
+
workers.push(w);
|
|
93
|
+
return w;
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
await pool.init();
|
|
97
|
+
await pool.shutdown();
|
|
98
|
+
for (const w of workers) {
|
|
99
|
+
expect(w.shutdown).toHaveBeenCalled();
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
it('throws if run is called after shutdown', async () => {
|
|
103
|
+
const pool = new JestPool({
|
|
104
|
+
cwd: '/tmp',
|
|
105
|
+
concurrency: 1,
|
|
106
|
+
createWorker: (id) => makeMockWorker(id),
|
|
107
|
+
});
|
|
108
|
+
await pool.init();
|
|
109
|
+
await pool.shutdown();
|
|
110
|
+
// After shutdown, initialised is set to false, so "not initialised" check fires first
|
|
111
|
+
await expect(pool.run(dummyMutant, ['test.ts'])).rejects.toThrow('Pool not initialised');
|
|
112
|
+
});
|
|
113
|
+
it('does not double-shutdown', async () => {
|
|
114
|
+
const workers = [];
|
|
115
|
+
const pool = new JestPool({
|
|
116
|
+
cwd: '/tmp',
|
|
117
|
+
concurrency: 1,
|
|
118
|
+
createWorker: (id) => {
|
|
119
|
+
const w = makeMockWorker(id);
|
|
120
|
+
workers.push(w);
|
|
121
|
+
return w;
|
|
122
|
+
},
|
|
123
|
+
});
|
|
124
|
+
await pool.init();
|
|
125
|
+
await pool.shutdown();
|
|
126
|
+
await pool.shutdown(); // should not throw
|
|
127
|
+
expect(workers[0].shutdown).toHaveBeenCalledTimes(1);
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
describe('runWithJestPool', () => {
|
|
131
|
+
it('maps killed result correctly', async () => {
|
|
132
|
+
const pool = new JestPool({
|
|
133
|
+
cwd: '/tmp',
|
|
134
|
+
concurrency: 1,
|
|
135
|
+
createWorker: (id) => {
|
|
136
|
+
const w = makeMockWorker(id);
|
|
137
|
+
w._mockResult = { killed: true, durationMs: 10 };
|
|
138
|
+
return w;
|
|
139
|
+
},
|
|
140
|
+
});
|
|
141
|
+
await pool.init();
|
|
142
|
+
const result = await runWithJestPool(pool, dummyMutant, ['test.ts']);
|
|
143
|
+
expect(result.status).toBe('killed');
|
|
144
|
+
expect(result.durationMs).toBe(10);
|
|
145
|
+
await pool.shutdown();
|
|
146
|
+
});
|
|
147
|
+
it('maps escaped result correctly', async () => {
|
|
148
|
+
const pool = new JestPool({
|
|
149
|
+
cwd: '/tmp',
|
|
150
|
+
concurrency: 1,
|
|
151
|
+
createWorker: (id) => {
|
|
152
|
+
const w = makeMockWorker(id);
|
|
153
|
+
w._mockResult = { killed: false, durationMs: 20 };
|
|
154
|
+
return w;
|
|
155
|
+
},
|
|
156
|
+
});
|
|
157
|
+
await pool.init();
|
|
158
|
+
const result = await runWithJestPool(pool, dummyMutant, ['test.ts']);
|
|
159
|
+
expect(result.status).toBe('escaped');
|
|
160
|
+
await pool.shutdown();
|
|
161
|
+
});
|
|
162
|
+
it('maps timeout error correctly', async () => {
|
|
163
|
+
const pool = new JestPool({
|
|
164
|
+
cwd: '/tmp',
|
|
165
|
+
concurrency: 1,
|
|
166
|
+
createWorker: (id) => {
|
|
167
|
+
const w = makeMockWorker(id);
|
|
168
|
+
w._mockResult = { killed: true, durationMs: 5000, error: 'timeout' };
|
|
169
|
+
return w;
|
|
170
|
+
},
|
|
171
|
+
});
|
|
172
|
+
await pool.init();
|
|
173
|
+
const result = await runWithJestPool(pool, dummyMutant, ['test.ts']);
|
|
174
|
+
expect(result.status).toBe('timeout');
|
|
175
|
+
expect(result.error).toBe('timeout');
|
|
176
|
+
await pool.shutdown();
|
|
177
|
+
});
|
|
178
|
+
it('maps non-timeout error with !killed to error status', async () => {
|
|
179
|
+
const pool = new JestPool({
|
|
180
|
+
cwd: '/tmp',
|
|
181
|
+
concurrency: 1,
|
|
182
|
+
createWorker: (id) => {
|
|
183
|
+
const w = makeMockWorker(id);
|
|
184
|
+
w._mockResult = { killed: false, durationMs: 10, error: 'crash' };
|
|
185
|
+
return w;
|
|
186
|
+
},
|
|
187
|
+
});
|
|
188
|
+
await pool.init();
|
|
189
|
+
const result = await runWithJestPool(pool, dummyMutant, ['test.ts']);
|
|
190
|
+
expect(result.status).toBe('error');
|
|
191
|
+
expect(result.error).toBe('crash');
|
|
192
|
+
await pool.shutdown();
|
|
193
|
+
});
|
|
194
|
+
it('handles pool.run throwing an error', async () => {
|
|
195
|
+
const pool = new JestPool({
|
|
196
|
+
cwd: '/tmp',
|
|
197
|
+
concurrency: 1,
|
|
198
|
+
createWorker: (id) => {
|
|
199
|
+
const w = makeMockWorker(id);
|
|
200
|
+
w.run = vi.fn().mockRejectedValue(new Error('pool exploded'));
|
|
201
|
+
return w;
|
|
202
|
+
},
|
|
203
|
+
});
|
|
204
|
+
await pool.init();
|
|
205
|
+
const result = await runWithJestPool(pool, dummyMutant, ['test.ts']);
|
|
206
|
+
expect(result.status).toBe('error');
|
|
207
|
+
expect(result.error).toBe('pool exploded');
|
|
208
|
+
expect(result.durationMs).toBe(0);
|
|
209
|
+
await pool.shutdown();
|
|
210
|
+
});
|
|
211
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { JestWorkerRuntime, createJestWorkerRuntime, } from '../worker-runtime.js';
|
|
3
|
+
// Mock the shared utilities
|
|
4
|
+
vi.mock('../../shared/index.js', () => ({
|
|
5
|
+
getMutantFilePath: vi.fn((id) => `/tmp/__mutineer__/mutant_${id}.ts`),
|
|
6
|
+
setRedirect: vi.fn(),
|
|
7
|
+
clearRedirect: vi.fn(),
|
|
8
|
+
}));
|
|
9
|
+
// Mock fs sync operations
|
|
10
|
+
const writeFileSyncMock = vi.fn();
|
|
11
|
+
const rmSyncMock = vi.fn();
|
|
12
|
+
vi.mock('node:fs', () => ({
|
|
13
|
+
default: {
|
|
14
|
+
existsSync: vi.fn(() => true),
|
|
15
|
+
writeFileSync: (...args) => writeFileSyncMock(...args),
|
|
16
|
+
rmSync: (...args) => rmSyncMock(...args),
|
|
17
|
+
},
|
|
18
|
+
existsSync: vi.fn(() => true),
|
|
19
|
+
writeFileSync: (...args) => writeFileSyncMock(...args),
|
|
20
|
+
rmSync: (...args) => rmSyncMock(...args),
|
|
21
|
+
}));
|
|
22
|
+
// Mock @jest/core
|
|
23
|
+
const mockRunCLI = vi.fn();
|
|
24
|
+
vi.mock('@jest/core', () => ({
|
|
25
|
+
runCLI: (...args) => mockRunCLI(...args),
|
|
26
|
+
}));
|
|
27
|
+
describe('JestWorkerRuntime', () => {
|
|
28
|
+
let runtime;
|
|
29
|
+
beforeEach(() => {
|
|
30
|
+
vi.clearAllMocks();
|
|
31
|
+
runtime = new JestWorkerRuntime({
|
|
32
|
+
workerId: 'w0',
|
|
33
|
+
cwd: '/project',
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
afterEach(() => {
|
|
37
|
+
// Clean up env vars
|
|
38
|
+
delete process.env.MUTINEER_REDIRECT_FROM;
|
|
39
|
+
delete process.env.MUTINEER_REDIRECT_TO;
|
|
40
|
+
});
|
|
41
|
+
it('init and shutdown are no-ops', async () => {
|
|
42
|
+
await expect(runtime.init()).resolves.toBeUndefined();
|
|
43
|
+
await expect(runtime.shutdown()).resolves.toBeUndefined();
|
|
44
|
+
});
|
|
45
|
+
it('runs a mutant and returns killed when tests fail', async () => {
|
|
46
|
+
mockRunCLI.mockResolvedValueOnce({
|
|
47
|
+
results: {
|
|
48
|
+
success: false,
|
|
49
|
+
numTotalTests: 3,
|
|
50
|
+
testResults: [{ failureMessage: 'Expected true to be false' }],
|
|
51
|
+
},
|
|
52
|
+
globalConfig: {},
|
|
53
|
+
});
|
|
54
|
+
const result = await runtime.run({
|
|
55
|
+
id: 'foo.ts#1',
|
|
56
|
+
name: 'flipEQ',
|
|
57
|
+
file: '/project/src/foo.ts',
|
|
58
|
+
code: 'mutated code',
|
|
59
|
+
line: 1,
|
|
60
|
+
col: 0,
|
|
61
|
+
}, ['/project/tests/foo.test.ts']);
|
|
62
|
+
expect(result.killed).toBe(true);
|
|
63
|
+
expect(result.durationMs).toBeGreaterThanOrEqual(0);
|
|
64
|
+
expect(result.error).toBe('Expected true to be false');
|
|
65
|
+
});
|
|
66
|
+
it('returns not killed when tests pass', async () => {
|
|
67
|
+
mockRunCLI.mockResolvedValueOnce({
|
|
68
|
+
results: {
|
|
69
|
+
success: true,
|
|
70
|
+
numTotalTests: 3,
|
|
71
|
+
testResults: [],
|
|
72
|
+
},
|
|
73
|
+
globalConfig: {},
|
|
74
|
+
});
|
|
75
|
+
const result = await runtime.run({
|
|
76
|
+
id: 'foo.ts#1',
|
|
77
|
+
name: 'flipEQ',
|
|
78
|
+
file: '/project/src/foo.ts',
|
|
79
|
+
code: 'mutated code',
|
|
80
|
+
line: 1,
|
|
81
|
+
col: 0,
|
|
82
|
+
}, ['/project/tests/foo.test.ts']);
|
|
83
|
+
expect(result.killed).toBe(false);
|
|
84
|
+
});
|
|
85
|
+
it('returns killed on runCLI error', async () => {
|
|
86
|
+
mockRunCLI.mockRejectedValueOnce(new Error('Jest crashed'));
|
|
87
|
+
const result = await runtime.run({
|
|
88
|
+
id: 'foo.ts#1',
|
|
89
|
+
name: 'flipEQ',
|
|
90
|
+
file: '/project/src/foo.ts',
|
|
91
|
+
code: 'mutated code',
|
|
92
|
+
line: 1,
|
|
93
|
+
col: 0,
|
|
94
|
+
}, ['/project/tests/foo.test.ts']);
|
|
95
|
+
expect(result.killed).toBe(true);
|
|
96
|
+
expect(result.error).toBe('Jest crashed');
|
|
97
|
+
});
|
|
98
|
+
it('writes the mutant file and cleans up after run', async () => {
|
|
99
|
+
mockRunCLI.mockResolvedValueOnce({
|
|
100
|
+
results: { success: true, testResults: [] },
|
|
101
|
+
globalConfig: {},
|
|
102
|
+
});
|
|
103
|
+
const { getMutantFilePath, setRedirect, clearRedirect } = await import('../../shared/index.js');
|
|
104
|
+
await runtime.run({
|
|
105
|
+
id: 'foo.ts#1',
|
|
106
|
+
name: 'flipEQ',
|
|
107
|
+
file: '/project/src/foo.ts',
|
|
108
|
+
code: 'mutated code',
|
|
109
|
+
line: 1,
|
|
110
|
+
col: 0,
|
|
111
|
+
}, ['/project/tests/foo.test.ts']);
|
|
112
|
+
expect(getMutantFilePath).toHaveBeenCalled();
|
|
113
|
+
expect(writeFileSyncMock).toHaveBeenCalled();
|
|
114
|
+
expect(setRedirect).toHaveBeenCalled();
|
|
115
|
+
expect(clearRedirect).toHaveBeenCalled();
|
|
116
|
+
expect(rmSyncMock).toHaveBeenCalled();
|
|
117
|
+
});
|
|
118
|
+
it('uses jest config when provided', async () => {
|
|
119
|
+
const runtimeWithConfig = new JestWorkerRuntime({
|
|
120
|
+
workerId: 'w0',
|
|
121
|
+
cwd: '/project',
|
|
122
|
+
jestConfigPath: 'jest.config.ts',
|
|
123
|
+
});
|
|
124
|
+
mockRunCLI.mockResolvedValueOnce({
|
|
125
|
+
results: { success: true, testResults: [] },
|
|
126
|
+
globalConfig: {},
|
|
127
|
+
});
|
|
128
|
+
await runtimeWithConfig.run({
|
|
129
|
+
id: 'foo.ts#1',
|
|
130
|
+
name: 'flipEQ',
|
|
131
|
+
file: '/project/src/foo.ts',
|
|
132
|
+
code: 'mutated code',
|
|
133
|
+
line: 1,
|
|
134
|
+
col: 0,
|
|
135
|
+
}, ['/project/tests/foo.test.ts']);
|
|
136
|
+
const callArgs = mockRunCLI.mock.calls[0][0];
|
|
137
|
+
expect(callArgs.config).toBe('jest.config.ts');
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
describe('createJestWorkerRuntime', () => {
|
|
141
|
+
it('returns a JestWorkerRuntime instance', () => {
|
|
142
|
+
const runtime = createJestWorkerRuntime({
|
|
143
|
+
workerId: 'w1',
|
|
144
|
+
cwd: '/project',
|
|
145
|
+
});
|
|
146
|
+
expect(runtime).toBeInstanceOf(JestWorkerRuntime);
|
|
147
|
+
});
|
|
148
|
+
});
|
|
@@ -121,7 +121,7 @@ export class JestAdapter {
|
|
|
121
121
|
}
|
|
122
122
|
async runMutant(mutant, tests) {
|
|
123
123
|
if (!this.pool) {
|
|
124
|
-
throw new Error('JestAdapter not
|
|
124
|
+
throw new Error('JestAdapter not initialised. Call init() first.');
|
|
125
125
|
}
|
|
126
126
|
try {
|
|
127
127
|
const result = await this.pool.run(mutant, [...tests]);
|
package/dist/runner/jest/pool.js
CHANGED
|
@@ -170,7 +170,7 @@ export class JestPool {
|
|
|
170
170
|
this.workers = [];
|
|
171
171
|
this.availableWorkers = [];
|
|
172
172
|
this.waitingTasks = [];
|
|
173
|
-
this.
|
|
173
|
+
this.initialised = false;
|
|
174
174
|
this.shuttingDown = false;
|
|
175
175
|
this.options = {
|
|
176
176
|
cwd: options.cwd,
|
|
@@ -181,7 +181,7 @@ export class JestPool {
|
|
|
181
181
|
};
|
|
182
182
|
}
|
|
183
183
|
async init() {
|
|
184
|
-
if (this.
|
|
184
|
+
if (this.initialised)
|
|
185
185
|
return;
|
|
186
186
|
const startPromises = [];
|
|
187
187
|
for (let i = 0; i < this.options.concurrency; i++) {
|
|
@@ -200,7 +200,7 @@ export class JestPool {
|
|
|
200
200
|
}));
|
|
201
201
|
}
|
|
202
202
|
await Promise.all(startPromises);
|
|
203
|
-
this.
|
|
203
|
+
this.initialised = true;
|
|
204
204
|
}
|
|
205
205
|
handleWorkerExit(worker) {
|
|
206
206
|
const availIdx = this.availableWorkers.indexOf(worker);
|
|
@@ -249,8 +249,8 @@ export class JestPool {
|
|
|
249
249
|
}
|
|
250
250
|
}
|
|
251
251
|
async run(mutant, tests) {
|
|
252
|
-
if (!this.
|
|
253
|
-
throw new Error('Pool not
|
|
252
|
+
if (!this.initialised) {
|
|
253
|
+
throw new Error('Pool not initialised. Call init() first.');
|
|
254
254
|
}
|
|
255
255
|
if (this.shuttingDown) {
|
|
256
256
|
throw new Error('Pool is shutting down');
|
|
@@ -272,7 +272,7 @@ export class JestPool {
|
|
|
272
272
|
await Promise.all(this.workers.map((w) => w.shutdown()));
|
|
273
273
|
this.workers = [];
|
|
274
274
|
this.availableWorkers = [];
|
|
275
|
-
this.
|
|
275
|
+
this.initialised = false;
|
|
276
276
|
}
|
|
277
277
|
}
|
|
278
278
|
export async function runWithJestPool(pool, mutant, tests) {
|