@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,57 @@
|
|
|
1
|
+
export interface CoverageData {
|
|
2
|
+
/** Set of covered lines per file (absolute path -> Set of line numbers) */
|
|
3
|
+
coveredLines: Map<string, Set<number>>;
|
|
4
|
+
}
|
|
5
|
+
/** Per-test coverage map: test file -> file path -> covered lines */
|
|
6
|
+
export type PerTestCoverageMap = Map<string, Map<string, Set<number>>>;
|
|
7
|
+
/**
|
|
8
|
+
* Load and parse Istanbul-format coverage JSON file.
|
|
9
|
+
* Supports both coverage-final.json (from Istanbul) and Vitest's coverage output.
|
|
10
|
+
*
|
|
11
|
+
* @param coverageFile - Path to the coverage JSON file
|
|
12
|
+
* @param cwd - Current working directory for resolving relative paths
|
|
13
|
+
* @returns Parsed coverage data with covered lines per file
|
|
14
|
+
*/
|
|
15
|
+
export declare function loadCoverageData(coverageFile: string, cwd: string): Promise<CoverageData>;
|
|
16
|
+
/**
|
|
17
|
+
* Check if a specific line in a file is covered by tests.
|
|
18
|
+
*
|
|
19
|
+
* @param coverage - Coverage data from loadCoverageData
|
|
20
|
+
* @param filePath - Absolute path to the file
|
|
21
|
+
* @param line - Line number to check (1-indexed)
|
|
22
|
+
* @returns true if the line is covered, false otherwise
|
|
23
|
+
*/
|
|
24
|
+
export declare function isLineCovered(coverage: CoverageData, filePath: string, line: number): boolean;
|
|
25
|
+
/**
|
|
26
|
+
* Get coverage statistics for a file.
|
|
27
|
+
*
|
|
28
|
+
* @param coverage - Coverage data from loadCoverageData
|
|
29
|
+
* @param filePath - Absolute path to the file
|
|
30
|
+
* @returns Object with covered line count and set of covered lines, or null if file not in coverage
|
|
31
|
+
*/
|
|
32
|
+
export declare function getFileCoverageStats(coverage: CoverageData, filePath: string): {
|
|
33
|
+
count: number;
|
|
34
|
+
lines: Set<number>;
|
|
35
|
+
} | null;
|
|
36
|
+
/**
|
|
37
|
+
* Best-effort loader for per-test coverage data.
|
|
38
|
+
* Expects a JSON file with a shape like:
|
|
39
|
+
* {
|
|
40
|
+
* "tests": {
|
|
41
|
+
* "/abs/path/to/test.spec.ts": {
|
|
42
|
+
* "files": {
|
|
43
|
+
* "/abs/path/to/src/file.ts": { "lines": [1,2,3] }
|
|
44
|
+
* }
|
|
45
|
+
* }
|
|
46
|
+
* }
|
|
47
|
+
* }
|
|
48
|
+
* or a simplified shape:
|
|
49
|
+
* {
|
|
50
|
+
* "/abs/path/to/test.spec.ts": {
|
|
51
|
+
* "/abs/path/to/src/file.ts": [1,2,3]
|
|
52
|
+
* }
|
|
53
|
+
* }
|
|
54
|
+
*
|
|
55
|
+
* Returns a map: testFile -> (filePath -> covered lines).
|
|
56
|
+
*/
|
|
57
|
+
export declare function loadPerTestCoverageData(reportsDir: string, cwd: string): Promise<PerTestCoverageMap | null>;
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
/**
|
|
4
|
+
* Load and parse Istanbul-format coverage JSON file.
|
|
5
|
+
* Supports both coverage-final.json (from Istanbul) and Vitest's coverage output.
|
|
6
|
+
*
|
|
7
|
+
* @param coverageFile - Path to the coverage JSON file
|
|
8
|
+
* @param cwd - Current working directory for resolving relative paths
|
|
9
|
+
* @returns Parsed coverage data with covered lines per file
|
|
10
|
+
*/
|
|
11
|
+
export async function loadCoverageData(coverageFile, cwd) {
|
|
12
|
+
const absPath = path.isAbsolute(coverageFile)
|
|
13
|
+
? coverageFile
|
|
14
|
+
: path.join(cwd, coverageFile);
|
|
15
|
+
let raw;
|
|
16
|
+
try {
|
|
17
|
+
raw = await fs.readFile(absPath, 'utf8');
|
|
18
|
+
}
|
|
19
|
+
catch (err) {
|
|
20
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
21
|
+
throw new Error(`Failed to read coverage file "${absPath}": ${msg}`);
|
|
22
|
+
}
|
|
23
|
+
let data;
|
|
24
|
+
try {
|
|
25
|
+
data = JSON.parse(raw);
|
|
26
|
+
}
|
|
27
|
+
catch (err) {
|
|
28
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
29
|
+
throw new Error(`Failed to parse coverage file "${absPath}" as JSON: ${msg}`);
|
|
30
|
+
}
|
|
31
|
+
const coveredLines = new Map();
|
|
32
|
+
for (const [filePath, fileCoverage] of Object.entries(data)) {
|
|
33
|
+
if (!fileCoverage.statementMap || !fileCoverage.s) {
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
const lines = new Set();
|
|
37
|
+
// Extract lines that have been executed (hit count > 0)
|
|
38
|
+
for (const [stmtId, hitCount] of Object.entries(fileCoverage.s)) {
|
|
39
|
+
if (hitCount > 0) {
|
|
40
|
+
const stmt = fileCoverage.statementMap[stmtId];
|
|
41
|
+
if (stmt) {
|
|
42
|
+
// Add all lines covered by this statement
|
|
43
|
+
for (let line = stmt.start.line; line <= stmt.end.line; line++) {
|
|
44
|
+
lines.add(line);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (lines.size > 0) {
|
|
50
|
+
// Normalize the file path to absolute
|
|
51
|
+
const absFilePath = path.isAbsolute(filePath)
|
|
52
|
+
? filePath
|
|
53
|
+
: path.join(cwd, filePath);
|
|
54
|
+
coveredLines.set(absFilePath, lines);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return { coveredLines };
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Check if a specific line in a file is covered by tests.
|
|
61
|
+
*
|
|
62
|
+
* @param coverage - Coverage data from loadCoverageData
|
|
63
|
+
* @param filePath - Absolute path to the file
|
|
64
|
+
* @param line - Line number to check (1-indexed)
|
|
65
|
+
* @returns true if the line is covered, false otherwise
|
|
66
|
+
*/
|
|
67
|
+
export function isLineCovered(coverage, filePath, line) {
|
|
68
|
+
const fileLines = coverage.coveredLines.get(filePath);
|
|
69
|
+
if (!fileLines) {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
return fileLines.has(line);
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Get coverage statistics for a file.
|
|
76
|
+
*
|
|
77
|
+
* @param coverage - Coverage data from loadCoverageData
|
|
78
|
+
* @param filePath - Absolute path to the file
|
|
79
|
+
* @returns Object with covered line count and set of covered lines, or null if file not in coverage
|
|
80
|
+
*/
|
|
81
|
+
export function getFileCoverageStats(coverage, filePath) {
|
|
82
|
+
const fileLines = coverage.coveredLines.get(filePath);
|
|
83
|
+
if (!fileLines) {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
return { count: fileLines.size, lines: fileLines };
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Best-effort loader for per-test coverage data.
|
|
90
|
+
* Expects a JSON file with a shape like:
|
|
91
|
+
* {
|
|
92
|
+
* "tests": {
|
|
93
|
+
* "/abs/path/to/test.spec.ts": {
|
|
94
|
+
* "files": {
|
|
95
|
+
* "/abs/path/to/src/file.ts": { "lines": [1,2,3] }
|
|
96
|
+
* }
|
|
97
|
+
* }
|
|
98
|
+
* }
|
|
99
|
+
* }
|
|
100
|
+
* or a simplified shape:
|
|
101
|
+
* {
|
|
102
|
+
* "/abs/path/to/test.spec.ts": {
|
|
103
|
+
* "/abs/path/to/src/file.ts": [1,2,3]
|
|
104
|
+
* }
|
|
105
|
+
* }
|
|
106
|
+
*
|
|
107
|
+
* Returns a map: testFile -> (filePath -> covered lines).
|
|
108
|
+
*/
|
|
109
|
+
export async function loadPerTestCoverageData(reportsDir, cwd) {
|
|
110
|
+
const base = path.isAbsolute(reportsDir)
|
|
111
|
+
? reportsDir
|
|
112
|
+
: path.join(cwd, reportsDir);
|
|
113
|
+
const candidates = [
|
|
114
|
+
path.join(base, 'per-test-coverage.json'),
|
|
115
|
+
path.join(base, 'coverage-per-test.json'),
|
|
116
|
+
path.join(base, 'coverage-final.json'), // fallback if someone writes per-test into the main file
|
|
117
|
+
];
|
|
118
|
+
const map = new Map();
|
|
119
|
+
let loaded = false;
|
|
120
|
+
for (const file of candidates) {
|
|
121
|
+
try {
|
|
122
|
+
const raw = await fs.readFile(file, 'utf8');
|
|
123
|
+
const data = JSON.parse(raw);
|
|
124
|
+
ingestPerTestJson(data, map);
|
|
125
|
+
loaded = map.size > 0;
|
|
126
|
+
if (loaded)
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
// ignore missing/unreadable candidates
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if (!loaded) {
|
|
134
|
+
// Try scanning coverage/tmp for per-test artifacts
|
|
135
|
+
try {
|
|
136
|
+
const tmpDir = path.join(base, 'tmp');
|
|
137
|
+
const entries = await fs.readdir(tmpDir, { withFileTypes: true });
|
|
138
|
+
for (const entry of entries) {
|
|
139
|
+
if (!entry.isFile() || !entry.name.endsWith('.json'))
|
|
140
|
+
continue;
|
|
141
|
+
const p = path.join(tmpDir, entry.name);
|
|
142
|
+
try {
|
|
143
|
+
const raw = await fs.readFile(p, 'utf8');
|
|
144
|
+
const data = JSON.parse(raw);
|
|
145
|
+
ingestPerTestJson(data, map);
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
loaded = map.size > 0;
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
// ignore
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return loaded ? map : null;
|
|
158
|
+
}
|
|
159
|
+
function ingestPerTestJson(json, out) {
|
|
160
|
+
if (!json || typeof json !== 'object')
|
|
161
|
+
return;
|
|
162
|
+
// If wrapped under "tests"
|
|
163
|
+
if ('tests' in json && json.tests && typeof json.tests === 'object') {
|
|
164
|
+
ingestTestsObject(json.tests, out);
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
// Otherwise treat top-level as tests map
|
|
168
|
+
ingestTestsObject(json, out);
|
|
169
|
+
}
|
|
170
|
+
function ingestTestsObject(testsObj, out) {
|
|
171
|
+
if (!testsObj || typeof testsObj !== 'object')
|
|
172
|
+
return;
|
|
173
|
+
for (const [testPath, value] of Object.entries(testsObj)) {
|
|
174
|
+
if (!value || typeof value !== 'object')
|
|
175
|
+
continue;
|
|
176
|
+
// Format A: { files: { "/file": { lines: [] } } }
|
|
177
|
+
if ('files' in value && value.files && typeof value.files === 'object') {
|
|
178
|
+
const files = value.files;
|
|
179
|
+
const fileMap = ensureTestEntry(out, testPath);
|
|
180
|
+
for (const [filePath, detail] of Object.entries(files)) {
|
|
181
|
+
if (detail &&
|
|
182
|
+
typeof detail === 'object' &&
|
|
183
|
+
'lines' in detail &&
|
|
184
|
+
Array.isArray(detail.lines)) {
|
|
185
|
+
fileMap.set(filePath, new Set(detail.lines));
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
// Format B: { "/file": [lines] }
|
|
191
|
+
const fileMap = ensureTestEntry(out, testPath);
|
|
192
|
+
for (const [filePath, lines] of Object.entries(value)) {
|
|
193
|
+
if (Array.isArray(lines)) {
|
|
194
|
+
fileMap.set(filePath, new Set(lines.filter((n) => Number.isFinite(n))));
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
function ensureTestEntry(map, testPath) {
|
|
200
|
+
if (!map.has(testPath)) {
|
|
201
|
+
map.set(testPath, new Map());
|
|
202
|
+
}
|
|
203
|
+
return map.get(testPath);
|
|
204
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export declare const DEBUG: boolean;
|
|
2
|
+
export interface Logger {
|
|
3
|
+
debug(msg: string, ...args: unknown[]): void;
|
|
4
|
+
info(msg: string, ...args: unknown[]): void;
|
|
5
|
+
warn(msg: string, ...args: unknown[]): void;
|
|
6
|
+
error(msg: string, ...args: unknown[]): void;
|
|
7
|
+
}
|
|
8
|
+
export declare function createLogger(tag: string): Logger;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export const DEBUG = process.env.MUTINEER_DEBUG === '1';
|
|
2
|
+
export function createLogger(tag) {
|
|
3
|
+
return {
|
|
4
|
+
debug(msg, ...args) {
|
|
5
|
+
if (DEBUG)
|
|
6
|
+
console.error(`[${tag}] ${msg}`, ...args);
|
|
7
|
+
},
|
|
8
|
+
info(msg, ...args) {
|
|
9
|
+
console.log(msg, ...args);
|
|
10
|
+
},
|
|
11
|
+
warn(msg, ...args) {
|
|
12
|
+
console.warn(msg, ...args);
|
|
13
|
+
},
|
|
14
|
+
error(msg, ...args) {
|
|
15
|
+
console.error(msg, ...args);
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { MutantStatus } from '../types/mutant.js';
|
|
2
|
+
export type ProgressMode = 'bar' | 'list';
|
|
3
|
+
export interface ProgressOptions {
|
|
4
|
+
readonly mode?: ProgressMode;
|
|
5
|
+
readonly stream?: 'stdout' | 'stderr';
|
|
6
|
+
}
|
|
7
|
+
export declare class Progress {
|
|
8
|
+
private total;
|
|
9
|
+
private done;
|
|
10
|
+
private killed;
|
|
11
|
+
private escaped;
|
|
12
|
+
private skipped;
|
|
13
|
+
private errors;
|
|
14
|
+
private timeouts;
|
|
15
|
+
private readonly mode;
|
|
16
|
+
private readonly useTTY;
|
|
17
|
+
private readonly stream;
|
|
18
|
+
private started;
|
|
19
|
+
private finished;
|
|
20
|
+
constructor(total: number, opts?: ProgressOptions);
|
|
21
|
+
start(): void;
|
|
22
|
+
update(status: MutantStatus): void;
|
|
23
|
+
finish(): void;
|
|
24
|
+
private writeBar;
|
|
25
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
export class Progress {
|
|
3
|
+
constructor(total, opts = {}) {
|
|
4
|
+
this.done = 0;
|
|
5
|
+
this.killed = 0;
|
|
6
|
+
this.escaped = 0;
|
|
7
|
+
this.skipped = 0;
|
|
8
|
+
this.errors = 0;
|
|
9
|
+
this.timeouts = 0;
|
|
10
|
+
this.started = false;
|
|
11
|
+
this.finished = false;
|
|
12
|
+
this.total = Math.max(0, total);
|
|
13
|
+
this.mode = opts.mode ?? 'bar';
|
|
14
|
+
this.stream = opts.stream === 'stdout' ? process.stdout : process.stderr;
|
|
15
|
+
this.useTTY = Boolean(this.stream.isTTY) && this.mode === 'bar';
|
|
16
|
+
}
|
|
17
|
+
start() {
|
|
18
|
+
if (this.started || this.finished)
|
|
19
|
+
return;
|
|
20
|
+
this.started = true;
|
|
21
|
+
if (this.useTTY) {
|
|
22
|
+
this.stream.write('\x1b[?25l'); // hide cursor
|
|
23
|
+
this.writeBar();
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
console.log(`mutineer: running ${this.total} mutants`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
update(status) {
|
|
30
|
+
if (!this.started || this.finished)
|
|
31
|
+
return;
|
|
32
|
+
this.done++;
|
|
33
|
+
if (status === 'killed')
|
|
34
|
+
this.killed++;
|
|
35
|
+
else if (status === 'escaped')
|
|
36
|
+
this.escaped++;
|
|
37
|
+
else if (status === 'error')
|
|
38
|
+
this.errors++;
|
|
39
|
+
else if (status === 'timeout')
|
|
40
|
+
this.timeouts++;
|
|
41
|
+
else
|
|
42
|
+
this.skipped++;
|
|
43
|
+
if (this.useTTY) {
|
|
44
|
+
this.writeBar();
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
console.log(`mutant ${this.done}/${this.total} ${status}`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
finish() {
|
|
51
|
+
if (!this.started || this.finished)
|
|
52
|
+
return;
|
|
53
|
+
this.finished = true;
|
|
54
|
+
if (this.useTTY) {
|
|
55
|
+
this.stream.write('\r\x1b[2K'); // clear the bar line
|
|
56
|
+
this.stream.write('\x1b[?25h'); // show cursor
|
|
57
|
+
}
|
|
58
|
+
console.log(`mutineer: killed=${this.killed} escaped=${this.escaped} ` +
|
|
59
|
+
`errors=${this.errors} timeouts=${this.timeouts} skipped=${this.skipped}`);
|
|
60
|
+
}
|
|
61
|
+
writeBar() {
|
|
62
|
+
const cols = this.stream.columns || 80;
|
|
63
|
+
const ratio = this.total === 0 ? 1 : Math.min(this.done / this.total, 1);
|
|
64
|
+
const pct = Math.round(ratio * 100);
|
|
65
|
+
// Calculate visible widths to ensure the line fits in one terminal row
|
|
66
|
+
const prefix = `mutants ${this.done}/${this.total} [`;
|
|
67
|
+
const suffix = `] ${pct}% `;
|
|
68
|
+
const stats = `killed=${this.killed} ` +
|
|
69
|
+
`escaped=${this.escaped} ` +
|
|
70
|
+
`errors=${this.errors} ` +
|
|
71
|
+
`timeouts=${this.timeouts} ` +
|
|
72
|
+
`skipped=${this.skipped}`;
|
|
73
|
+
const barWidth = Math.max(10, cols - prefix.length - suffix.length - stats.length - 1);
|
|
74
|
+
const filled = Math.round(ratio * barWidth);
|
|
75
|
+
const bar = '\u2588'.repeat(filled) + '\u2591'.repeat(barWidth - filled);
|
|
76
|
+
const line = prefix +
|
|
77
|
+
bar +
|
|
78
|
+
suffix +
|
|
79
|
+
chalk.green(`killed=${this.killed}`) +
|
|
80
|
+
' ' +
|
|
81
|
+
chalk.red(`escaped=${this.escaped}`) +
|
|
82
|
+
' ' +
|
|
83
|
+
chalk.yellow(`errors=${this.errors}`) +
|
|
84
|
+
' ' +
|
|
85
|
+
chalk.yellow(`timeouts=${this.timeouts}`) +
|
|
86
|
+
' ' +
|
|
87
|
+
chalk.dim(`skipped=${this.skipped}`);
|
|
88
|
+
this.stream.write('\r\x1b[2K' + line);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { MutantCacheEntry } from '../types/mutant.js';
|
|
2
|
+
export interface Summary {
|
|
3
|
+
readonly total: number;
|
|
4
|
+
readonly killed: number;
|
|
5
|
+
readonly escaped: number;
|
|
6
|
+
readonly skipped: number;
|
|
7
|
+
readonly evaluated: number;
|
|
8
|
+
readonly killRate: number;
|
|
9
|
+
}
|
|
10
|
+
export declare function computeSummary(cache: Readonly<Record<string, MutantCacheEntry>>): Summary;
|
|
11
|
+
export declare function printSummary(summary: Summary, cache?: Readonly<Record<string, MutantCacheEntry>>, durationMs?: number): void;
|
|
12
|
+
export declare function summarise(cache: Readonly<Record<string, MutantCacheEntry>>): Summary;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
const SEPARATOR = '\u2500'.repeat(45);
|
|
4
|
+
export function computeSummary(cache) {
|
|
5
|
+
const allEntries = Object.values(cache);
|
|
6
|
+
let killed = 0;
|
|
7
|
+
let escaped = 0;
|
|
8
|
+
let skipped = 0;
|
|
9
|
+
for (const entry of allEntries) {
|
|
10
|
+
if (entry.status === 'killed')
|
|
11
|
+
killed++;
|
|
12
|
+
else if (entry.status === 'escaped')
|
|
13
|
+
escaped++;
|
|
14
|
+
else
|
|
15
|
+
skipped++;
|
|
16
|
+
}
|
|
17
|
+
const evaluated = killed + escaped;
|
|
18
|
+
const total = allEntries.length;
|
|
19
|
+
const killRate = evaluated === 0 ? 0 : (killed / evaluated) * 100;
|
|
20
|
+
return { total, killed, escaped, skipped, evaluated, killRate };
|
|
21
|
+
}
|
|
22
|
+
function formatDuration(ms) {
|
|
23
|
+
if (ms < 1000)
|
|
24
|
+
return `${ms}ms`;
|
|
25
|
+
const seconds = ms / 1000;
|
|
26
|
+
if (seconds < 60)
|
|
27
|
+
return `${seconds.toFixed(2)}s`;
|
|
28
|
+
const minutes = Math.floor(seconds / 60);
|
|
29
|
+
const remainingSeconds = seconds % 60;
|
|
30
|
+
return `${minutes}m ${remainingSeconds.toFixed(1)}s`;
|
|
31
|
+
}
|
|
32
|
+
export function printSummary(summary, cache, durationMs) {
|
|
33
|
+
console.log('\n' + chalk.dim(SEPARATOR));
|
|
34
|
+
console.log(chalk.bold(' Mutineer Test Suite Summary'));
|
|
35
|
+
console.log(chalk.dim(SEPARATOR));
|
|
36
|
+
if (summary.total === 0) {
|
|
37
|
+
console.log('\nNo mutants found');
|
|
38
|
+
console.log('\n' + chalk.dim(SEPARATOR) + '\n');
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const cwd = process.cwd();
|
|
42
|
+
const allEntries = cache ? Object.values(cache) : [];
|
|
43
|
+
const relativePaths = allEntries.map((e) => path.relative(cwd, e.file));
|
|
44
|
+
const maxPathLen = Math.min(Math.max(...relativePaths.map((p) => p.length), 0), 40) || 25;
|
|
45
|
+
const maxMutatorLen = Math.min(Math.max(...allEntries.map((e) => e.mutator.length), 0), 20) || 10;
|
|
46
|
+
function formatRow(entry) {
|
|
47
|
+
const relativePath = path.relative(cwd, entry.file);
|
|
48
|
+
const location = `${relativePath}@${entry.line},${entry.col}`;
|
|
49
|
+
if (entry.status === 'killed') {
|
|
50
|
+
return `${chalk.green('\u2713')} ${location.padEnd(maxPathLen)} ${chalk.dim(entry.mutator.padEnd(maxMutatorLen))}`;
|
|
51
|
+
}
|
|
52
|
+
if (entry.status === 'escaped') {
|
|
53
|
+
return `${chalk.red('\u2A2F')} ${location.padEnd(maxPathLen)} ${chalk.dim(entry.mutator.padEnd(maxMutatorLen))}`;
|
|
54
|
+
}
|
|
55
|
+
return `${chalk.dim('\u2022')} ${chalk.dim(location.padEnd(maxPathLen))} ${chalk.dim(entry.mutator.padEnd(maxMutatorLen))}`;
|
|
56
|
+
}
|
|
57
|
+
const entriesByStatus = {
|
|
58
|
+
killed: [],
|
|
59
|
+
escaped: [],
|
|
60
|
+
skipped: [],
|
|
61
|
+
};
|
|
62
|
+
for (const entry of allEntries) {
|
|
63
|
+
if (entry.status === 'killed')
|
|
64
|
+
entriesByStatus.killed.push(entry);
|
|
65
|
+
else if (entry.status === 'escaped')
|
|
66
|
+
entriesByStatus.escaped.push(entry);
|
|
67
|
+
else
|
|
68
|
+
entriesByStatus.skipped.push(entry);
|
|
69
|
+
}
|
|
70
|
+
if (entriesByStatus.killed.length) {
|
|
71
|
+
console.log('\n' + chalk.green.bold('Killed Mutants:'));
|
|
72
|
+
for (const entry of entriesByStatus.killed)
|
|
73
|
+
console.log(' ' + formatRow(entry));
|
|
74
|
+
}
|
|
75
|
+
if (entriesByStatus.escaped.length) {
|
|
76
|
+
console.log('\n' + chalk.red.bold('Escaped Mutants:'));
|
|
77
|
+
for (const entry of entriesByStatus.escaped)
|
|
78
|
+
console.log(' ' + formatRow(entry));
|
|
79
|
+
}
|
|
80
|
+
if (entriesByStatus.skipped.length) {
|
|
81
|
+
console.log('\n' + chalk.dim('Skipped Mutants:'));
|
|
82
|
+
for (const entry of entriesByStatus.skipped)
|
|
83
|
+
console.log(' ' + formatRow(entry));
|
|
84
|
+
}
|
|
85
|
+
console.log('\n' + chalk.dim(SEPARATOR));
|
|
86
|
+
console.log(`Total: ${summary.total} \u2014 ${chalk.green(`Killed: ${summary.killed}`)}, ${chalk.red(`Escaped: ${summary.escaped}`)}, ${chalk.dim(`Skipped: ${summary.skipped}`)}`);
|
|
87
|
+
if (summary.evaluated === 0) {
|
|
88
|
+
console.log(`Kill rate: ${chalk.dim('0.00% (no mutants executed)')}`);
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
const rateColor = summary.killRate >= 80
|
|
92
|
+
? chalk.green
|
|
93
|
+
: summary.killRate >= 50
|
|
94
|
+
? chalk.yellow
|
|
95
|
+
: chalk.red;
|
|
96
|
+
console.log(`Kill rate: ${rateColor(summary.killRate.toFixed(2) + '%')} (${summary.killed}/${summary.evaluated})`);
|
|
97
|
+
}
|
|
98
|
+
if (durationMs !== undefined) {
|
|
99
|
+
console.log(`Duration: ${chalk.cyan(formatDuration(durationMs))}`);
|
|
100
|
+
}
|
|
101
|
+
console.log(chalk.dim(SEPARATOR) + '\n');
|
|
102
|
+
}
|
|
103
|
+
export function summarise(cache) {
|
|
104
|
+
const s = computeSummary(cache);
|
|
105
|
+
printSummary(s, cache);
|
|
106
|
+
return s;
|
|
107
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mutineerjs/mutineer",
|
|
3
|
+
"version": "v0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"private": false,
|
|
6
|
+
"bin": {
|
|
7
|
+
"mutineer": "./dist/bin/mutineer.js"
|
|
8
|
+
},
|
|
9
|
+
"main": "dist/index.js",
|
|
10
|
+
"types": "dist/index.d.ts",
|
|
11
|
+
"files": [
|
|
12
|
+
"dist"
|
|
13
|
+
],
|
|
14
|
+
"exports": {
|
|
15
|
+
".": {
|
|
16
|
+
"types": "./dist/index.d.ts",
|
|
17
|
+
"import": "./dist/index.js"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"scripts": {
|
|
21
|
+
"build": "tsc -p tsconfig.json",
|
|
22
|
+
"dev": "tsc -w",
|
|
23
|
+
"test": "vitest --run",
|
|
24
|
+
"test:watch": "vitest",
|
|
25
|
+
"test:coverage": "vitest --run --coverage",
|
|
26
|
+
"lint": "eslint --config ./eslint.config.cjs --ext .ts,.js src/",
|
|
27
|
+
"mutate": "MUTINEER_DEBUG=0 tsx src/bin/mutineer.ts --config mutineer.config.ts"
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"chalk": "^5.6.2",
|
|
31
|
+
"ink": "^5.2.1",
|
|
32
|
+
"ink-spinner": "^5.0.0",
|
|
33
|
+
"magic-string": "^0.30.9",
|
|
34
|
+
"react": "^18.3.1",
|
|
35
|
+
"tsx": "^4.20.6"
|
|
36
|
+
},
|
|
37
|
+
"peerDependencies": {
|
|
38
|
+
"@vue/compiler-sfc": ">=3.4.0",
|
|
39
|
+
"esbuild": "^0.25.10"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@babel/parser": "^7.28.4",
|
|
43
|
+
"@babel/traverse": "^7.28.4",
|
|
44
|
+
"@babel/types": "^7.28.4",
|
|
45
|
+
"@types/babel__traverse": "^7.28.0",
|
|
46
|
+
"@types/node": "^24.7.0",
|
|
47
|
+
"@types/react": "^19.2.14",
|
|
48
|
+
"@typescript-eslint/eslint-plugin": "^8.47.0",
|
|
49
|
+
"@typescript-eslint/parser": "^8.47.0",
|
|
50
|
+
"@vitejs/plugin-vue": "^5.1.4",
|
|
51
|
+
"@vitest/coverage-v8": "^4.0.15",
|
|
52
|
+
"eslint": "^9.39.1",
|
|
53
|
+
"fast-glob": "^3.3.3",
|
|
54
|
+
"jsdom": "^27.0.0",
|
|
55
|
+
"typescript": "^5.5.4",
|
|
56
|
+
"vite": "^6.3.6",
|
|
57
|
+
"vue": "^3.5.12"
|
|
58
|
+
}
|
|
59
|
+
}
|