@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
|
@@ -12,51 +12,31 @@
|
|
|
12
12
|
import path from 'node:path';
|
|
13
13
|
import os from 'node:os';
|
|
14
14
|
import { normalizePath } from '../utils/normalizePath.js';
|
|
15
|
-
import { render } from 'ink';
|
|
16
|
-
import { createElement } from 'react';
|
|
17
15
|
import { autoDiscoverTargetsAndTests } from './discover.js';
|
|
18
16
|
import { listChangedFiles } from './changed.js';
|
|
19
17
|
import { loadMutineerConfig } from './config.js';
|
|
20
|
-
import {
|
|
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';
|
|
18
|
+
import { createVitestAdapter } from './vitest/index.js';
|
|
24
19
|
import { createJestAdapter } from './jest/index.js';
|
|
25
20
|
import { createLogger } from '../utils/logger.js';
|
|
26
|
-
import {
|
|
27
|
-
|
|
28
|
-
import {
|
|
29
|
-
|
|
30
|
-
import {
|
|
31
|
-
|
|
32
|
-
import { enumerateVariantsForTarget, filterTestsByCoverage, getTargetFile, } from './variants.js';
|
|
21
|
+
import { extractConfigPath, parseCliOptions } from './args.js';
|
|
22
|
+
import { clearCacheOnStart, readMutantCache } from './cache.js';
|
|
23
|
+
import { getTargetFile, enumerateAllVariants } from './variants.js';
|
|
24
|
+
import { resolveCoverageConfig, loadCoverageAfterBaseline, } from './coverage-resolver.js';
|
|
25
|
+
import { prepareTasks } from './tasks.js';
|
|
26
|
+
import { executePool } from './pool-executor.js';
|
|
33
27
|
const log = createLogger('orchestrator');
|
|
34
|
-
let testMap;
|
|
35
28
|
// Per-mutant test timeout (ms). Can be overridden with env MUTINEER_MUTANT_TIMEOUT_MS
|
|
36
29
|
const MUTANT_TIMEOUT_MS = (() => {
|
|
37
30
|
const raw = process.env.MUTINEER_MUTANT_TIMEOUT_MS;
|
|
38
31
|
const n = raw ? Number(raw) : NaN;
|
|
39
32
|
return Number.isFinite(n) && n > 0 ? n : 30_000;
|
|
40
33
|
})();
|
|
41
|
-
import { cleanupMutineerDirs } from './cleanup.js';
|
|
42
34
|
// Re-export readMutantCache for external use
|
|
43
35
|
export { readMutantCache } from './cache.js';
|
|
44
36
|
export async function runOrchestrator(cliArgs, cwd) {
|
|
45
|
-
//
|
|
46
|
-
const
|
|
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;
|
|
37
|
+
// 1. Parse CLI arguments and load configuration
|
|
38
|
+
const cfgPath = extractConfigPath(cliArgs);
|
|
58
39
|
const cfg = await loadMutineerConfig(cwd, cfgPath);
|
|
59
|
-
// Parse CLI options
|
|
60
40
|
const opts = parseCliOptions(cliArgs, cfg);
|
|
61
41
|
await clearCacheOnStart(cwd);
|
|
62
42
|
// Create test runner adapter
|
|
@@ -67,56 +47,17 @@ export async function runOrchestrator(cliArgs, cwd) {
|
|
|
67
47
|
config: cfg,
|
|
68
48
|
cliArgs,
|
|
69
49
|
});
|
|
70
|
-
//
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
}
|
|
50
|
+
// 2. Resolve coverage configuration
|
|
51
|
+
const coverage = await resolveCoverageConfig(opts, cfg, adapter, cliArgs);
|
|
52
|
+
if (process.exitCode)
|
|
53
|
+
return; // resolveCoverageConfig sets exitCode on fatal errors
|
|
109
54
|
log.info(`Mutineer starting in ${opts.wantsChangedWithDeps
|
|
110
55
|
? 'changed files with dependencies'
|
|
111
56
|
: opts.wantsChanged
|
|
112
57
|
? 'changed files only'
|
|
113
58
|
: 'full'} mode${opts.wantsOnlyCoveredLines ? ' (only covered lines)' : ''}...`);
|
|
114
59
|
log.info(`Using concurrency=${opts.concurrency} (cpus=${os.cpus().length})`);
|
|
115
|
-
|
|
116
|
-
wantsPerTestCoverage ||
|
|
117
|
-
wantsCoverageRun ||
|
|
118
|
-
(opts.wantsOnlyCoveredLines && hasCoverageProviderInstalled);
|
|
119
|
-
// Enumerate changed files if requested
|
|
60
|
+
// 3. Enumerate changed files if requested
|
|
120
61
|
const changedAbs = opts.wantsChanged || opts.wantsChangedWithDeps
|
|
121
62
|
? new Set(listChangedFiles(cwd, {
|
|
122
63
|
includeDeps: opts.wantsChangedWithDeps,
|
|
@@ -124,12 +65,10 @@ export async function runOrchestrator(cliArgs, cwd) {
|
|
|
124
65
|
maxDepth: cfg.dependencyDepth,
|
|
125
66
|
}))
|
|
126
67
|
: null;
|
|
127
|
-
|
|
68
|
+
// 4. Discover targets and tests
|
|
128
69
|
const cache = await readMutantCache(cwd);
|
|
129
|
-
// Always run discovery to build testMap (maps source files → test files)
|
|
130
70
|
const discovered = await autoDiscoverTargetsAndTests(cwd, cfg);
|
|
131
|
-
testMap = discovered.testMap;
|
|
132
|
-
// Use explicit targets if provided, otherwise use discovered targets
|
|
71
|
+
const testMap = discovered.testMap;
|
|
133
72
|
const targets = cfg.targets?.length
|
|
134
73
|
? [...cfg.targets]
|
|
135
74
|
: (cfg.autoDiscover ?? true)
|
|
@@ -142,7 +81,7 @@ export async function runOrchestrator(cliArgs, cwd) {
|
|
|
142
81
|
const absFile = normalizePath(path.isAbsolute(file) ? file : path.join(cwd, file));
|
|
143
82
|
if (changedAbs && !changedAbs.has(absFile))
|
|
144
83
|
continue;
|
|
145
|
-
const testsAbs = testMap
|
|
84
|
+
const testsAbs = testMap.get(normalizePath(absFile));
|
|
146
85
|
if (testsAbs) {
|
|
147
86
|
for (const t of testsAbs)
|
|
148
87
|
allTestFiles.add(t);
|
|
@@ -153,235 +92,44 @@ export async function runOrchestrator(cliArgs, cwd) {
|
|
|
153
92
|
log.info('No tests found for targets. Exiting.');
|
|
154
93
|
return;
|
|
155
94
|
}
|
|
156
|
-
// Run baseline tests
|
|
157
|
-
log.info(`Running ${baselineTests.length} baseline tests${enableCoverageForBaseline ? ' (collecting coverage)' : ''}\u2026`);
|
|
95
|
+
// 5. Run baseline tests (with coverage if needed for filtering)
|
|
96
|
+
log.info(`Running ${baselineTests.length} baseline tests${coverage.enableCoverageForBaseline ? ' (collecting coverage)' : ''}\u2026`);
|
|
158
97
|
const baselineOk = await adapter.runBaseline(baselineTests, {
|
|
159
|
-
collectCoverage: enableCoverageForBaseline
|
|
160
|
-
perTestCoverage: wantsPerTestCoverage
|
|
98
|
+
collectCoverage: coverage.enableCoverageForBaseline,
|
|
99
|
+
perTestCoverage: coverage.wantsPerTestCoverage,
|
|
161
100
|
});
|
|
162
101
|
if (!baselineOk) {
|
|
163
102
|
process.exitCode = 1;
|
|
164
103
|
return;
|
|
165
104
|
}
|
|
166
105
|
log.info('\u2713 Baseline tests complete');
|
|
167
|
-
// Load coverage from baseline if we generated it
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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);
|
|
106
|
+
// 6. Load coverage from baseline if we generated it
|
|
107
|
+
const updatedCoverage = await loadCoverageAfterBaseline(coverage, cwd);
|
|
108
|
+
// 7. Enumerate mutation variants
|
|
109
|
+
const variants = await enumerateAllVariants({
|
|
110
|
+
cwd,
|
|
111
|
+
targets,
|
|
112
|
+
testMap,
|
|
113
|
+
changedFiles: changedAbs,
|
|
114
|
+
coverageData: updatedCoverage.coverageData,
|
|
115
|
+
config: cfg,
|
|
116
|
+
});
|
|
216
117
|
if (!variants.length) {
|
|
217
|
-
const msg = coverageData
|
|
118
|
+
const msg = updatedCoverage.coverageData
|
|
218
119
|
? 'No mutants to test (all mutations are on uncovered lines). Exiting.'
|
|
219
120
|
: 'No mutants to test. Exiting.';
|
|
220
121
|
log.info(msg);
|
|
221
122
|
return;
|
|
222
123
|
}
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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 };
|
|
124
|
+
// 8. Prepare tasks and execute via worker pool
|
|
125
|
+
const tasks = prepareTasks(variants, updatedCoverage.perTestCoverage);
|
|
126
|
+
await executePool({
|
|
127
|
+
tasks,
|
|
128
|
+
adapter,
|
|
129
|
+
cache,
|
|
130
|
+
concurrency: opts.concurrency,
|
|
131
|
+
progressMode: opts.progressMode,
|
|
132
|
+
minKillPercent: opts.minKillPercent,
|
|
133
|
+
cwd,
|
|
244
134
|
});
|
|
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
135
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { MutantCacheEntry } from '../types/mutant.js';
|
|
2
|
+
import type { TestRunnerAdapter } from './types.js';
|
|
3
|
+
import type { MutantTask } from './tasks.js';
|
|
4
|
+
export interface PoolExecutionOptions {
|
|
5
|
+
tasks: MutantTask[];
|
|
6
|
+
adapter: TestRunnerAdapter;
|
|
7
|
+
cache: Record<string, MutantCacheEntry>;
|
|
8
|
+
concurrency: number;
|
|
9
|
+
progressMode: 'bar' | 'list' | 'quiet';
|
|
10
|
+
minKillPercent?: number;
|
|
11
|
+
cwd: string;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Execute all mutant tasks through the worker pool.
|
|
15
|
+
* Handles worker init, progress display, signal handling, and cleanup.
|
|
16
|
+
*/
|
|
17
|
+
export declare function executePool(opts: PoolExecutionOptions): Promise<void>;
|
|
@@ -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
|
+
// Initialise 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 {};
|