@mutineerjs/mutineer 0.1.0
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/LICENSE +21 -0
- package/README.md +218 -0
- package/dist/admin/assets/index-B7nXq-e7.js +32 -0
- package/dist/admin/assets/index-B7nXq-e7.js.map +1 -0
- package/dist/admin/assets/index-BDQLkBUE.js +32 -0
- package/dist/admin/assets/index-BDQLkBUE.js.map +1 -0
- package/dist/admin/assets/index-DVkP-Tc7.css +1 -0
- package/dist/admin/index.html +13 -0
- package/dist/admin/server/admin.d.ts +6 -0
- package/dist/admin/server/admin.js +234 -0
- package/dist/bin/mutate-vitest.d.ts +2 -0
- package/dist/bin/mutate-vitest.js +90 -0
- package/dist/bin/mutineer.d.ts +2 -0
- package/dist/bin/mutineer.js +46 -0
- package/dist/core/__tests__/module.spec.d.ts +1 -0
- package/dist/core/__tests__/module.spec.js +6 -0
- package/dist/core/module.d.ts +11 -0
- package/dist/core/module.js +14 -0
- package/dist/core/sfc.d.ts +12 -0
- package/dist/core/sfc.js +54 -0
- package/dist/core/types.d.ts +6 -0
- package/dist/core/types.js +1 -0
- package/dist/core/variant-utils.d.ts +30 -0
- package/dist/core/variant-utils.js +54 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +3 -0
- package/dist/mutators/__tests__/registry.spec.d.ts +1 -0
- package/dist/mutators/__tests__/registry.spec.js +43 -0
- package/dist/mutators/__tests__/utils.spec.d.ts +1 -0
- package/dist/mutators/__tests__/utils.spec.js +15 -0
- package/dist/mutators/registry.d.ts +37 -0
- package/dist/mutators/registry.js +101 -0
- package/dist/mutators/types.d.ts +39 -0
- package/dist/mutators/types.js +7 -0
- package/dist/mutators/utils.d.ts +37 -0
- package/dist/mutators/utils.js +151 -0
- package/dist/plugin/viteMutate.d.ts +15 -0
- package/dist/plugin/viteMutate.js +52 -0
- package/dist/plugin/vitest.setup.d.ts +47 -0
- package/dist/plugin/vitest.setup.js +118 -0
- package/dist/plugin/withVitest.d.ts +13 -0
- package/dist/plugin/withVitest.js +30 -0
- package/dist/runner/__tests__/discover.spec.d.ts +1 -0
- package/dist/runner/__tests__/discover.spec.js +59 -0
- package/dist/runner/__tests__/orchestrator.spec.d.ts +1 -0
- package/dist/runner/__tests__/orchestrator.spec.js +55 -0
- package/dist/runner/adapters/__tests__/jest.spec.d.ts +1 -0
- package/dist/runner/adapters/__tests__/jest.spec.js +88 -0
- package/dist/runner/adapters/__tests__/vitest-worker-runtime.spec.d.ts +1 -0
- package/dist/runner/adapters/__tests__/vitest-worker-runtime.spec.js +59 -0
- package/dist/runner/adapters/__tests__/vitest.spec.d.ts +1 -0
- package/dist/runner/adapters/__tests__/vitest.spec.js +118 -0
- package/dist/runner/adapters/index.d.ts +10 -0
- package/dist/runner/adapters/index.js +9 -0
- package/dist/runner/adapters/jest/__tests__/index.spec.d.ts +1 -0
- package/dist/runner/adapters/jest/__tests__/index.spec.js +88 -0
- package/dist/runner/adapters/jest/index.d.ts +24 -0
- package/dist/runner/adapters/jest/index.js +216 -0
- package/dist/runner/adapters/jest/worker-runtime.d.ts +37 -0
- package/dist/runner/adapters/jest/worker-runtime.js +171 -0
- package/dist/runner/adapters/jest-worker-runtime.d.ts +37 -0
- package/dist/runner/adapters/jest-worker-runtime.js +171 -0
- package/dist/runner/adapters/jest.d.ts +24 -0
- package/dist/runner/adapters/jest.js +216 -0
- package/dist/runner/adapters/types.d.ts +89 -0
- package/dist/runner/adapters/types.js +8 -0
- package/dist/runner/adapters/vitest/__tests__/index.spec.d.ts +1 -0
- package/dist/runner/adapters/vitest/__tests__/index.spec.js +118 -0
- package/dist/runner/adapters/vitest/__tests__/worker-runtime.spec.d.ts +1 -0
- package/dist/runner/adapters/vitest/__tests__/worker-runtime.spec.js +59 -0
- package/dist/runner/adapters/vitest/index.d.ts +33 -0
- package/dist/runner/adapters/vitest/index.js +267 -0
- package/dist/runner/adapters/vitest/worker-runtime.d.ts +25 -0
- package/dist/runner/adapters/vitest/worker-runtime.js +118 -0
- package/dist/runner/adapters/vitest-worker-runtime.d.ts +25 -0
- package/dist/runner/adapters/vitest-worker-runtime.js +118 -0
- package/dist/runner/adapters/vitest.d.ts +33 -0
- package/dist/runner/adapters/vitest.js +267 -0
- package/dist/runner/args.d.ts +50 -0
- package/dist/runner/args.js +123 -0
- package/dist/runner/cache.d.ts +38 -0
- package/dist/runner/cache.js +118 -0
- package/dist/runner/changed.d.ts +22 -0
- package/dist/runner/changed.js +210 -0
- package/dist/runner/cleanup.d.ts +4 -0
- package/dist/runner/cleanup.js +21 -0
- package/dist/runner/config.d.ts +13 -0
- package/dist/runner/config.js +94 -0
- package/dist/runner/discover.d.ts +7 -0
- package/dist/runner/discover.js +258 -0
- package/dist/runner/jest/__tests__/adapter.spec.d.ts +1 -0
- package/dist/runner/jest/__tests__/adapter.spec.js +110 -0
- package/dist/runner/jest/adapter.d.ts +24 -0
- package/dist/runner/jest/adapter.js +191 -0
- package/dist/runner/jest/index.d.ts +8 -0
- package/dist/runner/jest/index.js +7 -0
- package/dist/runner/jest/pool.d.ts +47 -0
- package/dist/runner/jest/pool.js +307 -0
- package/dist/runner/jest/resolver.cjs +61 -0
- package/dist/runner/jest/resolver.d.cts +11 -0
- package/dist/runner/jest/worker-runtime.d.ts +30 -0
- package/dist/runner/jest/worker-runtime.js +98 -0
- package/dist/runner/jest/worker.d.mts +1 -0
- package/dist/runner/jest/worker.mjs +55 -0
- package/dist/runner/orchestrator.d.ts +13 -0
- package/dist/runner/orchestrator.js +387 -0
- package/dist/runner/pool/__tests__/index.spec.d.ts +1 -0
- package/dist/runner/pool/__tests__/index.spec.js +83 -0
- package/dist/runner/pool/__tests__/pool-plugin.spec.d.ts +1 -0
- package/dist/runner/pool/__tests__/pool-plugin.spec.js +59 -0
- package/dist/runner/pool/__tests__/pool-redirect-loader.spec.d.ts +1 -0
- package/dist/runner/pool/__tests__/pool-redirect-loader.spec.js +78 -0
- package/dist/runner/pool/index.d.ts +8 -0
- package/dist/runner/pool/index.js +9 -0
- package/dist/runner/pool/jest/pool.d.ts +52 -0
- package/dist/runner/pool/jest/pool.js +309 -0
- package/dist/runner/pool/jest/worker.d.mts +1 -0
- package/dist/runner/pool/jest/worker.mjs +60 -0
- package/dist/runner/pool/jest-pool.d.ts +52 -0
- package/dist/runner/pool/jest-pool.js +309 -0
- package/dist/runner/pool/jest-worker.d.mts +1 -0
- package/dist/runner/pool/jest-worker.mjs +60 -0
- package/dist/runner/pool/plugin.d.ts +18 -0
- package/dist/runner/pool/plugin.js +60 -0
- package/dist/runner/pool/pool-plugin.d.ts +18 -0
- package/dist/runner/pool/pool-plugin.js +60 -0
- package/dist/runner/pool/pool-redirect-loader.d.ts +19 -0
- package/dist/runner/pool/pool-redirect-loader.js +116 -0
- package/dist/runner/pool/pool-redirect-loader.mjs +146 -0
- package/dist/runner/pool/redirect-loader.d.ts +19 -0
- package/dist/runner/pool/redirect-loader.js +116 -0
- package/dist/runner/pool/vitest/pool.d.ts +70 -0
- package/dist/runner/pool/vitest/pool.js +376 -0
- package/dist/runner/pool/vitest/worker.d.mts +15 -0
- package/dist/runner/pool/vitest/worker.mjs +96 -0
- package/dist/runner/pool/vitest-worker.d.mts +15 -0
- package/dist/runner/pool/vitest-worker.mjs +96 -0
- package/dist/runner/shared/index.d.ts +9 -0
- package/dist/runner/shared/index.js +8 -0
- package/dist/runner/shared/mutant-paths.d.ts +15 -0
- package/dist/runner/shared/mutant-paths.js +30 -0
- package/dist/runner/shared/redirect-state.d.ts +45 -0
- package/dist/runner/shared/redirect-state.js +50 -0
- package/dist/runner/shared-module-redirect.d.ts +56 -0
- package/dist/runner/shared-module-redirect.js +84 -0
- package/dist/runner/types.d.ts +88 -0
- package/dist/runner/types.js +8 -0
- package/dist/runner/variants.d.ts +21 -0
- package/dist/runner/variants.js +66 -0
- package/dist/runner/vitest/__tests__/adapter.spec.d.ts +1 -0
- package/dist/runner/vitest/__tests__/adapter.spec.js +131 -0
- package/dist/runner/vitest/__tests__/plugin.spec.d.ts +1 -0
- package/dist/runner/vitest/__tests__/plugin.spec.js +65 -0
- package/dist/runner/vitest/__tests__/pool.spec.d.ts +1 -0
- package/dist/runner/vitest/__tests__/pool.spec.js +106 -0
- package/dist/runner/vitest/__tests__/redirect-loader.spec.d.ts +1 -0
- package/dist/runner/vitest/__tests__/redirect-loader.spec.js +87 -0
- package/dist/runner/vitest/__tests__/worker-runtime.spec.d.ts +1 -0
- package/dist/runner/vitest/__tests__/worker-runtime.spec.js +75 -0
- package/dist/runner/vitest/adapter.d.ts +33 -0
- package/dist/runner/vitest/adapter.js +277 -0
- package/dist/runner/vitest/index.d.ts +11 -0
- package/dist/runner/vitest/index.js +10 -0
- package/dist/runner/vitest/plugin.d.ts +12 -0
- package/dist/runner/vitest/plugin.js +49 -0
- package/dist/runner/vitest/pool.d.ts +65 -0
- package/dist/runner/vitest/pool.js +376 -0
- package/dist/runner/vitest/redirect-loader.d.ts +30 -0
- package/dist/runner/vitest/redirect-loader.js +123 -0
- package/dist/runner/vitest/worker-runtime.d.ts +16 -0
- package/dist/runner/vitest/worker-runtime.js +105 -0
- package/dist/runner/vitest/worker.d.mts +15 -0
- package/dist/runner/vitest/worker.mjs +92 -0
- package/dist/types/api.d.ts +20 -0
- package/dist/types/api.js +1 -0
- package/dist/types/config.d.ts +48 -0
- package/dist/types/config.js +1 -0
- package/dist/types/index.d.ts +13 -0
- package/dist/types/index.js +11 -0
- package/dist/types/mutant.d.ts +44 -0
- package/dist/types/mutant.js +7 -0
- package/dist/utils/PoolSpinner.d.ts +5 -0
- package/dist/utils/PoolSpinner.js +6 -0
- package/dist/utils/ProgressBar.d.ts +11 -0
- package/dist/utils/ProgressBar.js +9 -0
- package/dist/utils/__tests__/coverage.spec.d.ts +1 -0
- package/dist/utils/__tests__/coverage.spec.js +91 -0
- package/dist/utils/__tests__/progress.spec.d.ts +1 -0
- package/dist/utils/__tests__/progress.spec.js +50 -0
- package/dist/utils/__tests__/summary.spec.d.ts +1 -0
- package/dist/utils/__tests__/summary.spec.js +54 -0
- package/dist/utils/coverage.d.ts +57 -0
- package/dist/utils/coverage.js +204 -0
- package/dist/utils/logger.d.ts +8 -0
- package/dist/utils/logger.js +18 -0
- package/dist/utils/progress.d.ts +25 -0
- package/dist/utils/progress.js +90 -0
- package/dist/utils/summary.d.ts +12 -0
- package/dist/utils/summary.js +107 -0
- package/package.json +59 -0
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Jest custom resolver for mutineer.
|
|
4
|
+
*
|
|
5
|
+
* This resolver intercepts module resolution to redirect imports of the original
|
|
6
|
+
* source file to the mutated version during test execution.
|
|
7
|
+
*
|
|
8
|
+
* The redirect configuration is passed via environment variables:
|
|
9
|
+
* - MUTINEER_REDIRECT_FROM: Absolute path to the original file
|
|
10
|
+
* - MUTINEER_REDIRECT_TO: Absolute path to the mutant file
|
|
11
|
+
*/
|
|
12
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
|
+
const path = require('path');
|
|
14
|
+
/**
|
|
15
|
+
* Custom Jest resolver that redirects module resolution for mutant testing.
|
|
16
|
+
*/
|
|
17
|
+
module.exports = (request, options) => {
|
|
18
|
+
// Get redirect configuration from environment
|
|
19
|
+
const from = process.env.MUTINEER_REDIRECT_FROM;
|
|
20
|
+
const to = process.env.MUTINEER_REDIRECT_TO;
|
|
21
|
+
const normalizedFrom = from ? path.resolve(from) : null;
|
|
22
|
+
// Helper to resolve using Jest's default resolver
|
|
23
|
+
const resolveWith = (req, opts) => {
|
|
24
|
+
if (options?.defaultResolver) {
|
|
25
|
+
return options.defaultResolver(req, opts);
|
|
26
|
+
}
|
|
27
|
+
// Fallback to require.resolve if no default resolver provided
|
|
28
|
+
return require.resolve(req, { paths: opts?.paths || [process.cwd()] });
|
|
29
|
+
};
|
|
30
|
+
// Helper to check if a path matches the redirect source
|
|
31
|
+
const matchesFrom = (p) => {
|
|
32
|
+
if (!normalizedFrom)
|
|
33
|
+
return false;
|
|
34
|
+
const abs = path.resolve(p);
|
|
35
|
+
if (abs === normalizedFrom)
|
|
36
|
+
return true;
|
|
37
|
+
// Handle imports without extensions (e.g., './foo' resolving to './foo.ts')
|
|
38
|
+
const withExt = path.extname(abs) ? abs : abs + path.extname(normalizedFrom);
|
|
39
|
+
return path.resolve(withExt) === normalizedFrom;
|
|
40
|
+
};
|
|
41
|
+
// Try to resolve the request normally first
|
|
42
|
+
let resolved;
|
|
43
|
+
try {
|
|
44
|
+
resolved = resolveWith(request, options);
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
resolved = null;
|
|
48
|
+
}
|
|
49
|
+
// Check if the request itself (before resolution) matches
|
|
50
|
+
const baseDir = options?.basedir ?? process.cwd();
|
|
51
|
+
const candidate = path.resolve(baseDir, request);
|
|
52
|
+
// If either the candidate or the resolved path matches our redirect source,
|
|
53
|
+
// return the mutant file path
|
|
54
|
+
if (normalizedFrom &&
|
|
55
|
+
to &&
|
|
56
|
+
(matchesFrom(candidate) || (resolved && matchesFrom(resolved)))) {
|
|
57
|
+
return to;
|
|
58
|
+
}
|
|
59
|
+
// Otherwise, return the normally resolved path (or resolve again if it failed before)
|
|
60
|
+
return resolved ?? resolveWith(request, options);
|
|
61
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Jest custom resolver for mutineer.
|
|
3
|
+
*
|
|
4
|
+
* This resolver intercepts module resolution to redirect imports of the original
|
|
5
|
+
* source file to the mutated version during test execution.
|
|
6
|
+
*
|
|
7
|
+
* The redirect configuration is passed via environment variables:
|
|
8
|
+
* - MUTINEER_REDIRECT_FROM: Absolute path to the original file
|
|
9
|
+
* - MUTINEER_REDIRECT_TO: Absolute path to the mutant file
|
|
10
|
+
*/
|
|
11
|
+
export {};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { MutantPayload } from '../types.js';
|
|
2
|
+
import type { MutantRunSummary } from '../../types/mutant.js';
|
|
3
|
+
interface JestTestResult {
|
|
4
|
+
failureMessage?: string;
|
|
5
|
+
}
|
|
6
|
+
interface JestAggregatedResult {
|
|
7
|
+
success: boolean;
|
|
8
|
+
numTotalTests?: number;
|
|
9
|
+
testResults?: JestTestResult[];
|
|
10
|
+
}
|
|
11
|
+
export type JestRunCLI = (argv: Record<string, unknown>, projects: string[]) => Promise<{
|
|
12
|
+
results: JestAggregatedResult;
|
|
13
|
+
globalConfig: unknown;
|
|
14
|
+
}>;
|
|
15
|
+
export interface JestWorkerRuntimeOptions {
|
|
16
|
+
workerId: string;
|
|
17
|
+
cwd: string;
|
|
18
|
+
jestConfigPath?: string;
|
|
19
|
+
}
|
|
20
|
+
export declare class JestWorkerRuntime {
|
|
21
|
+
private readonly options;
|
|
22
|
+
private readonly resolverPath;
|
|
23
|
+
private readonly requireFromCwd;
|
|
24
|
+
constructor(options: JestWorkerRuntimeOptions);
|
|
25
|
+
init(): Promise<void>;
|
|
26
|
+
shutdown(): Promise<void>;
|
|
27
|
+
run(mutant: MutantPayload, tests: string[]): Promise<MutantRunSummary>;
|
|
28
|
+
}
|
|
29
|
+
export declare function createJestWorkerRuntime(options: JestWorkerRuntimeOptions): JestWorkerRuntime;
|
|
30
|
+
export {};
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import { createRequire } from 'node:module';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { getMutantFilePath, setRedirect, clearRedirect, } from '../shared/index.js';
|
|
6
|
+
import { createLogger } from '../../utils/logger.js';
|
|
7
|
+
const log = createLogger('jest-runtime');
|
|
8
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
9
|
+
const __dirname = path.dirname(__filename);
|
|
10
|
+
/**
|
|
11
|
+
* Get the path to the pre-built Jest resolver module.
|
|
12
|
+
*/
|
|
13
|
+
function getResolverPath() {
|
|
14
|
+
// The resolver.cjs is in the same directory as this file
|
|
15
|
+
return path.join(__dirname, 'resolver.cjs');
|
|
16
|
+
}
|
|
17
|
+
async function loadRunCLI(requireFromCwd) {
|
|
18
|
+
try {
|
|
19
|
+
return requireFromCwd('@jest/core');
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return import('@jest/core');
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
export class JestWorkerRuntime {
|
|
26
|
+
constructor(options) {
|
|
27
|
+
this.options = options;
|
|
28
|
+
this.requireFromCwd = createRequire(path.join(options.cwd, 'package.json'));
|
|
29
|
+
this.resolverPath = getResolverPath();
|
|
30
|
+
}
|
|
31
|
+
async init() {
|
|
32
|
+
// Resolver is pre-built, no initialization needed
|
|
33
|
+
}
|
|
34
|
+
async shutdown() {
|
|
35
|
+
// Resolver is pre-built, no cleanup needed
|
|
36
|
+
}
|
|
37
|
+
async run(mutant, tests) {
|
|
38
|
+
const start = Date.now();
|
|
39
|
+
try {
|
|
40
|
+
const mutantPath = getMutantFilePath(mutant.file, mutant.id);
|
|
41
|
+
fs.writeFileSync(mutantPath, mutant.code, 'utf8');
|
|
42
|
+
const redirectFrom = path.resolve(mutant.file);
|
|
43
|
+
setRedirect({ from: redirectFrom, to: mutantPath });
|
|
44
|
+
process.env.MUTINEER_REDIRECT_FROM = redirectFrom;
|
|
45
|
+
process.env.MUTINEER_REDIRECT_TO = mutantPath;
|
|
46
|
+
const cliOptions = {
|
|
47
|
+
_: [...tests],
|
|
48
|
+
$0: 'mutineer',
|
|
49
|
+
runInBand: true,
|
|
50
|
+
runTestsByPath: true,
|
|
51
|
+
testPathPattern: [...tests],
|
|
52
|
+
watch: false,
|
|
53
|
+
passWithNoTests: true,
|
|
54
|
+
resolver: this.resolverPath,
|
|
55
|
+
silent: true,
|
|
56
|
+
};
|
|
57
|
+
if (this.options.jestConfigPath) {
|
|
58
|
+
cliOptions.config = this.options.jestConfigPath;
|
|
59
|
+
}
|
|
60
|
+
const { runCLI } = await loadRunCLI(this.requireFromCwd);
|
|
61
|
+
const { results } = await runCLI(cliOptions, [this.options.cwd]);
|
|
62
|
+
const killed = !results.success;
|
|
63
|
+
const failureMessages = results.testResults
|
|
64
|
+
?.map((r) => r.failureMessage)
|
|
65
|
+
.filter(Boolean)
|
|
66
|
+
.join('\n');
|
|
67
|
+
log.debug(`runCLI success=${results.success} tests=${results.numTotalTests ?? 'n/a'}`);
|
|
68
|
+
return {
|
|
69
|
+
killed,
|
|
70
|
+
durationMs: Date.now() - start,
|
|
71
|
+
error: failureMessages || undefined,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
catch (err) {
|
|
75
|
+
log.debug(`runCLI error: ${err}`);
|
|
76
|
+
return {
|
|
77
|
+
killed: true,
|
|
78
|
+
durationMs: Date.now() - start,
|
|
79
|
+
error: err instanceof Error ? err.message : String(err),
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
finally {
|
|
83
|
+
const mutantPath = getMutantFilePath(mutant.file, mutant.id);
|
|
84
|
+
clearRedirect();
|
|
85
|
+
delete process.env.MUTINEER_REDIRECT_FROM;
|
|
86
|
+
delete process.env.MUTINEER_REDIRECT_TO;
|
|
87
|
+
try {
|
|
88
|
+
fs.rmSync(mutantPath, { force: true });
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
// ignore
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
export function createJestWorkerRuntime(options) {
|
|
97
|
+
return new JestWorkerRuntime(options);
|
|
98
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { createJestWorkerRuntime } from './worker-runtime.js';
|
|
2
|
+
import { createLogger } from '../../utils/logger.js';
|
|
3
|
+
const log = createLogger('jest-worker');
|
|
4
|
+
globalThis.__mutineer_redirect__ = { from: null, to: null };
|
|
5
|
+
async function main() {
|
|
6
|
+
const workerId = process.env.MUTINEER_WORKER_ID ?? 'unknown';
|
|
7
|
+
const cwd = process.env.MUTINEER_CWD ?? process.cwd();
|
|
8
|
+
const jestConfigPath = process.env.MUTINEER_JEST_CONFIG;
|
|
9
|
+
log.debug(`Starting worker ${workerId} in ${cwd}`);
|
|
10
|
+
const runtime = createJestWorkerRuntime({
|
|
11
|
+
workerId,
|
|
12
|
+
cwd,
|
|
13
|
+
jestConfigPath,
|
|
14
|
+
});
|
|
15
|
+
try {
|
|
16
|
+
await runtime.init();
|
|
17
|
+
}
|
|
18
|
+
catch (err) {
|
|
19
|
+
log.error(`Failed to initialize: ${err}`);
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
|
22
|
+
process.send?.({ type: 'ready', workerId });
|
|
23
|
+
process.on('message', async (raw) => {
|
|
24
|
+
if (raw.type === 'shutdown') {
|
|
25
|
+
log.debug('Shutting down');
|
|
26
|
+
await runtime.shutdown();
|
|
27
|
+
process.send?.({ type: 'shutdown', ok: true });
|
|
28
|
+
process.exit(0);
|
|
29
|
+
}
|
|
30
|
+
if (raw.type === 'run') {
|
|
31
|
+
try {
|
|
32
|
+
const { mutant, tests } = raw;
|
|
33
|
+
const result = await runtime.run(mutant, tests);
|
|
34
|
+
process.send?.({
|
|
35
|
+
type: 'result',
|
|
36
|
+
killed: result.killed,
|
|
37
|
+
durationMs: result.durationMs,
|
|
38
|
+
error: result.error,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
catch (err) {
|
|
42
|
+
process.send?.({
|
|
43
|
+
type: 'result',
|
|
44
|
+
killed: true,
|
|
45
|
+
durationMs: 0,
|
|
46
|
+
error: String(err),
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
main().catch((err) => {
|
|
53
|
+
log.error(`Fatal error: ${err}`);
|
|
54
|
+
process.exit(1);
|
|
55
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mutation Testing Orchestrator
|
|
3
|
+
*
|
|
4
|
+
* Coordinates the mutation testing process:
|
|
5
|
+
* 1. Parse CLI arguments and load configuration
|
|
6
|
+
* 2. Discover targets and tests
|
|
7
|
+
* 3. Run baseline tests
|
|
8
|
+
* 4. Enumerate mutation variants
|
|
9
|
+
* 5. Execute mutants via worker pool
|
|
10
|
+
* 6. Report results
|
|
11
|
+
*/
|
|
12
|
+
export { readMutantCache } from './cache.js';
|
|
13
|
+
export declare function runOrchestrator(cliArgs: string[], cwd: string): Promise<void>;
|
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mutation Testing Orchestrator
|
|
3
|
+
*
|
|
4
|
+
* Coordinates the mutation testing process:
|
|
5
|
+
* 1. Parse CLI arguments and load configuration
|
|
6
|
+
* 2. Discover targets and tests
|
|
7
|
+
* 3. Run baseline tests
|
|
8
|
+
* 4. Enumerate mutation variants
|
|
9
|
+
* 5. Execute mutants via worker pool
|
|
10
|
+
* 6. Report results
|
|
11
|
+
*/
|
|
12
|
+
import path from 'node:path';
|
|
13
|
+
import os from 'node:os';
|
|
14
|
+
import { normalizePath } from 'vite';
|
|
15
|
+
import { render } from 'ink';
|
|
16
|
+
import { createElement } from 'react';
|
|
17
|
+
import { autoDiscoverTargetsAndTests } from './discover.js';
|
|
18
|
+
import { listChangedFiles } from './changed.js';
|
|
19
|
+
import { loadMutineerConfig } from './config.js';
|
|
20
|
+
import { Progress } from '../utils/progress.js';
|
|
21
|
+
import { computeSummary, printSummary } from '../utils/summary.js';
|
|
22
|
+
import { loadCoverageData, loadPerTestCoverageData, isLineCovered, } from '../utils/coverage.js';
|
|
23
|
+
import { createVitestAdapter, isCoverageRequestedInArgs, } from './vitest/index.js';
|
|
24
|
+
import { createJestAdapter } from './jest/index.js';
|
|
25
|
+
import { createLogger } from '../utils/logger.js';
|
|
26
|
+
import { PoolSpinner } from '../utils/PoolSpinner.js';
|
|
27
|
+
// CLI argument parsing
|
|
28
|
+
import { parseCliOptions } from './args.js';
|
|
29
|
+
// Cache management
|
|
30
|
+
import { clearCacheOnStart, saveCacheAtomic, readMutantCache, keyForTests, hash, } from './cache.js';
|
|
31
|
+
// Variant enumeration
|
|
32
|
+
import { enumerateVariantsForTarget, filterTestsByCoverage, getTargetFile, } from './variants.js';
|
|
33
|
+
const log = createLogger('orchestrator');
|
|
34
|
+
let testMap;
|
|
35
|
+
// Per-mutant test timeout (ms). Can be overridden with env MUTINEER_MUTANT_TIMEOUT_MS
|
|
36
|
+
const MUTANT_TIMEOUT_MS = (() => {
|
|
37
|
+
const raw = process.env.MUTINEER_MUTANT_TIMEOUT_MS;
|
|
38
|
+
const n = raw ? Number(raw) : NaN;
|
|
39
|
+
return Number.isFinite(n) && n > 0 ? n : 30_000;
|
|
40
|
+
})();
|
|
41
|
+
import { cleanupMutineerDirs } from './cleanup.js';
|
|
42
|
+
// Re-export readMutantCache for external use
|
|
43
|
+
export { readMutantCache } from './cache.js';
|
|
44
|
+
export async function runOrchestrator(cliArgs, cwd) {
|
|
45
|
+
// Load configuration
|
|
46
|
+
const configPath = cliArgs.find((arg, i) => arg === '--config' || arg === '-c'
|
|
47
|
+
? cliArgs[i + 1]
|
|
48
|
+
: arg.startsWith('--config=') || arg.startsWith('-c='));
|
|
49
|
+
const cfgPath = configPath?.startsWith('--config=')
|
|
50
|
+
? configPath.slice(9)
|
|
51
|
+
: configPath?.startsWith('-c=')
|
|
52
|
+
? configPath.slice(3)
|
|
53
|
+
: cliArgs.includes('--config')
|
|
54
|
+
? cliArgs[cliArgs.indexOf('--config') + 1]
|
|
55
|
+
: cliArgs.includes('-c')
|
|
56
|
+
? cliArgs[cliArgs.indexOf('-c') + 1]
|
|
57
|
+
: undefined;
|
|
58
|
+
const cfg = await loadMutineerConfig(cwd, cfgPath);
|
|
59
|
+
// Parse CLI options
|
|
60
|
+
const opts = parseCliOptions(cliArgs, cfg);
|
|
61
|
+
await clearCacheOnStart(cwd);
|
|
62
|
+
// Create test runner adapter
|
|
63
|
+
const adapter = (opts.runner === 'jest' ? createJestAdapter : createVitestAdapter)({
|
|
64
|
+
cwd,
|
|
65
|
+
concurrency: opts.concurrency,
|
|
66
|
+
timeoutMs: MUTANT_TIMEOUT_MS,
|
|
67
|
+
config: cfg,
|
|
68
|
+
cliArgs,
|
|
69
|
+
});
|
|
70
|
+
// Detect coverage configuration from the adapter
|
|
71
|
+
const coverageConfig = await adapter.detectCoverageConfig();
|
|
72
|
+
const wantsPerTestCoverageFromConfig = coverageConfig.perTestEnabled;
|
|
73
|
+
const coveragePreference = cfg.coverage;
|
|
74
|
+
const wantsCoverageRun = coveragePreference === true
|
|
75
|
+
? true
|
|
76
|
+
: coveragePreference === false
|
|
77
|
+
? false
|
|
78
|
+
: isCoverageRequestedInArgs(cliArgs) || coverageConfig.coverageEnabled;
|
|
79
|
+
// Load pre-existing coverage data if provided
|
|
80
|
+
let coverageData = null;
|
|
81
|
+
let perTestCoverage = null;
|
|
82
|
+
if (opts.coverageFilePath) {
|
|
83
|
+
log.info(`Loading coverage data from ${opts.coverageFilePath}...`);
|
|
84
|
+
coverageData = await loadCoverageData(opts.coverageFilePath, cwd);
|
|
85
|
+
log.info(`Loaded coverage for ${coverageData.coveredLines.size} files`);
|
|
86
|
+
}
|
|
87
|
+
// If --only-covered-lines but no coverage file, we'll generate it during baseline
|
|
88
|
+
const needsCoverageFromBaseline = opts.wantsOnlyCoveredLines && !coverageData;
|
|
89
|
+
const hasCoverageProviderInstalled = adapter.hasCoverageProvider();
|
|
90
|
+
const rawPerTestCoverage = opts.wantsPerTestCoverage ||
|
|
91
|
+
wantsPerTestCoverageFromConfig ||
|
|
92
|
+
(opts.wantsOnlyCoveredLines && hasCoverageProviderInstalled);
|
|
93
|
+
const wantsPerTestCoverage = opts.runner === 'jest' ? false : rawPerTestCoverage;
|
|
94
|
+
if (opts.runner === 'jest' && rawPerTestCoverage) {
|
|
95
|
+
log.warn('Per-test coverage is not supported for Jest; continuing without per-test coverage.');
|
|
96
|
+
}
|
|
97
|
+
if (needsCoverageFromBaseline && !hasCoverageProviderInstalled) {
|
|
98
|
+
log.warn('The "onlyCoveredLines" option requires a coverage provider to generate coverage data.');
|
|
99
|
+
log.warn('Please install the appropriate coverage package (or disable onlyCoveredLines).');
|
|
100
|
+
process.exitCode = 1;
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
if (opts.wantsOnlyCoveredLines &&
|
|
104
|
+
coverageData &&
|
|
105
|
+
!hasCoverageProviderInstalled) {
|
|
106
|
+
log.warn('The "onlyCoveredLines" option is enabled, but no coverage provider is installed.');
|
|
107
|
+
log.warn('Running baseline tests without injecting per-test coverage; existing coverageFile will be used for filtering.');
|
|
108
|
+
}
|
|
109
|
+
log.info(`Mutineer starting in ${opts.wantsChangedWithDeps
|
|
110
|
+
? 'changed files with dependencies'
|
|
111
|
+
: opts.wantsChanged
|
|
112
|
+
? 'changed files only'
|
|
113
|
+
: 'full'} mode${opts.wantsOnlyCoveredLines ? ' (only covered lines)' : ''}...`);
|
|
114
|
+
log.info(`Using concurrency=${opts.concurrency} (cpus=${os.cpus().length})`);
|
|
115
|
+
const enableCoverageForBaseline = needsCoverageFromBaseline ||
|
|
116
|
+
wantsPerTestCoverage ||
|
|
117
|
+
wantsCoverageRun ||
|
|
118
|
+
(opts.wantsOnlyCoveredLines && hasCoverageProviderInstalled);
|
|
119
|
+
// Enumerate changed files if requested
|
|
120
|
+
const changedAbs = opts.wantsChanged || opts.wantsChangedWithDeps
|
|
121
|
+
? new Set(listChangedFiles(cwd, {
|
|
122
|
+
includeDeps: opts.wantsChangedWithDeps,
|
|
123
|
+
baseRef: cfg.baseRef,
|
|
124
|
+
maxDepth: cfg.dependencyDepth,
|
|
125
|
+
}))
|
|
126
|
+
: null;
|
|
127
|
+
const variants = [];
|
|
128
|
+
const cache = await readMutantCache(cwd);
|
|
129
|
+
// Always run discovery to build testMap (maps source files → test files)
|
|
130
|
+
const discovered = await autoDiscoverTargetsAndTests(cwd, cfg);
|
|
131
|
+
testMap = discovered.testMap;
|
|
132
|
+
// Use explicit targets if provided, otherwise use discovered targets
|
|
133
|
+
const targets = cfg.targets?.length
|
|
134
|
+
? [...cfg.targets]
|
|
135
|
+
: (cfg.autoDiscover ?? true)
|
|
136
|
+
? discovered.targets
|
|
137
|
+
: [];
|
|
138
|
+
// Collect all test files for baseline run
|
|
139
|
+
const allTestFiles = new Set();
|
|
140
|
+
for (const target of targets) {
|
|
141
|
+
const file = getTargetFile(target);
|
|
142
|
+
const absFile = normalizePath(path.isAbsolute(file) ? file : path.join(cwd, file));
|
|
143
|
+
if (changedAbs && !changedAbs.has(absFile))
|
|
144
|
+
continue;
|
|
145
|
+
const testsAbs = testMap?.get(normalizePath(absFile));
|
|
146
|
+
if (testsAbs) {
|
|
147
|
+
for (const t of testsAbs)
|
|
148
|
+
allTestFiles.add(t);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
const baselineTests = Array.from(allTestFiles);
|
|
152
|
+
if (!baselineTests.length) {
|
|
153
|
+
log.info('No tests found for targets. Exiting.');
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
// Run baseline tests first (with coverage if needed for filtering)
|
|
157
|
+
log.info(`Running ${baselineTests.length} baseline tests${enableCoverageForBaseline ? ' (collecting coverage)' : ''}\u2026`);
|
|
158
|
+
const baselineOk = await adapter.runBaseline(baselineTests, {
|
|
159
|
+
collectCoverage: enableCoverageForBaseline ?? false,
|
|
160
|
+
perTestCoverage: wantsPerTestCoverage ?? false,
|
|
161
|
+
});
|
|
162
|
+
if (!baselineOk) {
|
|
163
|
+
process.exitCode = 1;
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
log.info('\u2713 Baseline tests complete');
|
|
167
|
+
// Load coverage from baseline if we generated it
|
|
168
|
+
if (needsCoverageFromBaseline) {
|
|
169
|
+
const defaultCoveragePath = path.join(cwd, 'coverage', 'coverage-final.json');
|
|
170
|
+
log.info(`Loading coverage data from ${defaultCoveragePath}...`);
|
|
171
|
+
try {
|
|
172
|
+
coverageData = await loadCoverageData(defaultCoveragePath, cwd);
|
|
173
|
+
log.info(`Loaded coverage for ${coverageData.coveredLines.size} files`);
|
|
174
|
+
}
|
|
175
|
+
catch (err) {
|
|
176
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
177
|
+
log.warn(`Warning: Could not load coverage data: ${msg}`);
|
|
178
|
+
log.warn('Continuing without coverage filtering.');
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
// Load per-test coverage if requested
|
|
182
|
+
if (wantsPerTestCoverage) {
|
|
183
|
+
const reportsDir = path.join(cwd, 'coverage');
|
|
184
|
+
log.info('Loading per-test coverage data...');
|
|
185
|
+
perTestCoverage = await loadPerTestCoverageData(reportsDir, cwd);
|
|
186
|
+
if (!perTestCoverage) {
|
|
187
|
+
log.warn('Per-test coverage data not found. Continuing without per-test test pruning.');
|
|
188
|
+
}
|
|
189
|
+
else {
|
|
190
|
+
log.info(`Loaded per-test coverage for ${perTestCoverage.size} tests`);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
// Enumerate variants for targets in parallel. Keep order deterministic by mapping then flattening.
|
|
194
|
+
const enumerated = await Promise.all(targets.map(async (target) => {
|
|
195
|
+
const file = getTargetFile(target);
|
|
196
|
+
const absFile = normalizePath(path.isAbsolute(file) ? file : path.join(cwd, file));
|
|
197
|
+
if (changedAbs && !changedAbs.has(absFile))
|
|
198
|
+
return [];
|
|
199
|
+
log.debug('Target file: ' + absFile);
|
|
200
|
+
const files = await enumerateVariantsForTarget(cwd, target, cfg.include, cfg.exclude, cfg.maxMutantsPerFile);
|
|
201
|
+
const testsAbs = testMap?.get(normalizePath(absFile));
|
|
202
|
+
const tests = testsAbs ? Array.from(testsAbs) : [];
|
|
203
|
+
log.debug(` found ${files.length} variants, linked to ${tests.length} tests`);
|
|
204
|
+
// Filter by coverage if enabled
|
|
205
|
+
let filtered = files;
|
|
206
|
+
if (coverageData) {
|
|
207
|
+
filtered = files.filter((v) => isLineCovered(coverageData, absFile, v.line));
|
|
208
|
+
if (filtered.length !== files.length) {
|
|
209
|
+
log.debug(` filtered ${files.length} -> ${filtered.length} variants by coverage`);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return filtered.map((v) => ({ ...v, tests }));
|
|
213
|
+
}));
|
|
214
|
+
for (const list of enumerated)
|
|
215
|
+
variants.push(...list);
|
|
216
|
+
if (!variants.length) {
|
|
217
|
+
const msg = coverageData
|
|
218
|
+
? 'No mutants to test (all mutations are on uncovered lines). Exiting.'
|
|
219
|
+
: 'No mutants to test. Exiting.';
|
|
220
|
+
log.info(msg);
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
const progress = new Progress(variants.length, {
|
|
224
|
+
mode: opts.progressMode === 'bar' ? 'bar' : 'list',
|
|
225
|
+
stream: 'stderr',
|
|
226
|
+
});
|
|
227
|
+
// Track mutation testing duration
|
|
228
|
+
const mutationStartTime = Date.now();
|
|
229
|
+
// Precompute task metadata for faster worker loops (sort tests, compute keys once)
|
|
230
|
+
const tasks = variants.map((v) => {
|
|
231
|
+
let tests = Array.from(v.tests);
|
|
232
|
+
if (perTestCoverage && tests.length) {
|
|
233
|
+
const before = tests.length;
|
|
234
|
+
tests = filterTestsByCoverage(perTestCoverage, tests, v.file, v.line);
|
|
235
|
+
if (tests.length !== before) {
|
|
236
|
+
log.debug(`Pruned tests ${before} -> ${tests.length} for mutant ${v.name} via per-test coverage`);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
tests.sort();
|
|
240
|
+
const testSig = hash(keyForTests(tests));
|
|
241
|
+
const codeSig = hash(v.code);
|
|
242
|
+
const key = `${testSig}:${codeSig}`;
|
|
243
|
+
return { v, tests, key };
|
|
244
|
+
});
|
|
245
|
+
const workerCount = Math.max(1, Math.min(opts.concurrency, tasks.length));
|
|
246
|
+
// Ensure we only finish once
|
|
247
|
+
let finished = false;
|
|
248
|
+
const finishOnce = () => {
|
|
249
|
+
if (finished)
|
|
250
|
+
return;
|
|
251
|
+
finished = true;
|
|
252
|
+
const durationMs = Date.now() - mutationStartTime;
|
|
253
|
+
// Finish progress display first
|
|
254
|
+
progress.finish();
|
|
255
|
+
// Compute and print a human-friendly summary
|
|
256
|
+
const summary = computeSummary(cache);
|
|
257
|
+
printSummary(summary, cache, durationMs);
|
|
258
|
+
if (opts.minKillPercent !== undefined) {
|
|
259
|
+
const killRateString = summary.killRate.toFixed(2);
|
|
260
|
+
const thresholdString = opts.minKillPercent.toFixed(2);
|
|
261
|
+
if (summary.killRate < opts.minKillPercent) {
|
|
262
|
+
const note = summary.evaluated === 0 ? ' No mutants were executed.' : '';
|
|
263
|
+
log.error(`Mutation kill rate ${killRateString}% did not meet required ${thresholdString}% threshold.${note}`);
|
|
264
|
+
// Set exit code and let caller/CLI decide if it should terminate abruptly
|
|
265
|
+
process.exitCode = 1;
|
|
266
|
+
}
|
|
267
|
+
else {
|
|
268
|
+
log.info(`Mutation kill rate ${killRateString}% meets required ${thresholdString}% threshold`);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
};
|
|
272
|
+
// Initialize test runner adapter
|
|
273
|
+
const workerLogSuffix = workerCount < opts.concurrency ? ` (requested ${opts.concurrency})` : '';
|
|
274
|
+
log.info(`Initializing ${adapter.name} worker pool with ${workerCount} workers...${workerLogSuffix}`);
|
|
275
|
+
const poolStart = Date.now();
|
|
276
|
+
// Ink spinner on stderr while workers start up
|
|
277
|
+
let spinnerInstance = null;
|
|
278
|
+
if (process.stderr.isTTY) {
|
|
279
|
+
spinnerInstance = render(createElement(PoolSpinner, { message: 'Starting pool...' }), { stdout: process.stderr, stderr: process.stderr });
|
|
280
|
+
}
|
|
281
|
+
try {
|
|
282
|
+
await adapter.init(workerCount);
|
|
283
|
+
}
|
|
284
|
+
finally {
|
|
285
|
+
if (spinnerInstance) {
|
|
286
|
+
spinnerInstance.unmount();
|
|
287
|
+
spinnerInstance = null;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
const poolDurationMs = Date.now() - poolStart;
|
|
291
|
+
log.info(`\u2713 Worker pool ready (${poolDurationMs}ms)`);
|
|
292
|
+
progress.start();
|
|
293
|
+
let nextIdx = 0;
|
|
294
|
+
/**
|
|
295
|
+
* Process a single mutant task: check cache, run tests if needed, update cache and progress.
|
|
296
|
+
* This function is designed to be called by multiple workers concurrently.
|
|
297
|
+
*/
|
|
298
|
+
async function processTask(task) {
|
|
299
|
+
const { v, tests, key } = task;
|
|
300
|
+
log.debug('Cache ' + JSON.stringify(cache));
|
|
301
|
+
// Check if already cached
|
|
302
|
+
const cached = cache[key];
|
|
303
|
+
if (cached) {
|
|
304
|
+
progress.update(cached.status);
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
// Skip if no tests import this file
|
|
308
|
+
if (tests.length === 0) {
|
|
309
|
+
cache[key] = {
|
|
310
|
+
status: 'skipped',
|
|
311
|
+
file: v.file,
|
|
312
|
+
line: v.line,
|
|
313
|
+
col: v.col,
|
|
314
|
+
mutator: v.name,
|
|
315
|
+
};
|
|
316
|
+
progress.update('skipped');
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
log.debug(`Running tests for mutant ${v.name} ${JSON.stringify(tests)}`);
|
|
320
|
+
// Run mutant via test runner adapter
|
|
321
|
+
const result = await adapter.runMutant({
|
|
322
|
+
id: v.id,
|
|
323
|
+
name: v.name,
|
|
324
|
+
file: v.file,
|
|
325
|
+
code: v.code,
|
|
326
|
+
line: v.line,
|
|
327
|
+
col: v.col,
|
|
328
|
+
}, tests);
|
|
329
|
+
const status = result.status;
|
|
330
|
+
cache[key] = {
|
|
331
|
+
status,
|
|
332
|
+
file: v.file,
|
|
333
|
+
line: v.line,
|
|
334
|
+
col: v.col,
|
|
335
|
+
mutator: v.name,
|
|
336
|
+
};
|
|
337
|
+
progress.update(status);
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* Worker coroutine: process mutant tasks from the queue until exhausted.
|
|
341
|
+
* Multiple workers run concurrently, sharing the task queue.
|
|
342
|
+
*/
|
|
343
|
+
async function worker() {
|
|
344
|
+
while (true) {
|
|
345
|
+
const i = nextIdx++;
|
|
346
|
+
if (i >= tasks.length)
|
|
347
|
+
break;
|
|
348
|
+
await processTask(tasks[i]);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
const workers = [];
|
|
352
|
+
// Register signal handlers so Ctrl+C / SIGTERM still triggers cleanup
|
|
353
|
+
let signalCleanedUp = false;
|
|
354
|
+
const signalHandler = async (signal) => {
|
|
355
|
+
if (signalCleanedUp)
|
|
356
|
+
return;
|
|
357
|
+
signalCleanedUp = true;
|
|
358
|
+
log.info(`\nReceived ${signal}, cleaning up...`);
|
|
359
|
+
finishOnce();
|
|
360
|
+
await adapter.shutdown();
|
|
361
|
+
await cleanupMutineerDirs(cwd);
|
|
362
|
+
process.exit(1);
|
|
363
|
+
};
|
|
364
|
+
process.on('SIGINT', () => void signalHandler('SIGINT'));
|
|
365
|
+
process.on('SIGTERM', () => void signalHandler('SIGTERM'));
|
|
366
|
+
try {
|
|
367
|
+
// Spawn worker coroutines
|
|
368
|
+
for (let i = 0; i < workerCount; i++)
|
|
369
|
+
workers.push(worker());
|
|
370
|
+
// Wait for all workers to complete
|
|
371
|
+
await Promise.all(workers);
|
|
372
|
+
// Persist results to disk
|
|
373
|
+
await saveCacheAtomic(cwd, cache);
|
|
374
|
+
}
|
|
375
|
+
finally {
|
|
376
|
+
// Remove signal handlers to avoid double cleanup
|
|
377
|
+
process.removeAllListeners('SIGINT');
|
|
378
|
+
process.removeAllListeners('SIGTERM');
|
|
379
|
+
if (!signalCleanedUp) {
|
|
380
|
+
finishOnce();
|
|
381
|
+
// Shutdown adapter
|
|
382
|
+
await adapter.shutdown();
|
|
383
|
+
// Clean up all __mutineer__ temp directories
|
|
384
|
+
await cleanupMutineerDirs(cwd);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|