@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
|
@@ -38,6 +38,173 @@ describe('coverage utilities', () => {
|
|
|
38
38
|
const coverage = { coveredLines: new Map() };
|
|
39
39
|
expect(getFileCoverageStats(coverage, '/nope.ts')).toBeNull();
|
|
40
40
|
});
|
|
41
|
+
it('loads coverage from a relative path', async () => {
|
|
42
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'mutineer-coverage-'));
|
|
43
|
+
const filePath = '/src/file.ts';
|
|
44
|
+
const data = {
|
|
45
|
+
[filePath]: {
|
|
46
|
+
path: filePath,
|
|
47
|
+
statementMap: {
|
|
48
|
+
'0': { start: { line: 1, column: 0 }, end: { line: 1, column: 10 } },
|
|
49
|
+
},
|
|
50
|
+
s: { '0': 1 },
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
await fs.writeFile(path.join(tmpDir, 'cov.json'), JSON.stringify(data), 'utf8');
|
|
54
|
+
try {
|
|
55
|
+
const coverage = await loadCoverageData('cov.json', tmpDir);
|
|
56
|
+
expect(coverage.coveredLines.size).toBeGreaterThan(0);
|
|
57
|
+
}
|
|
58
|
+
finally {
|
|
59
|
+
await fs.rm(tmpDir, { recursive: true, force: true });
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
it('throws on missing coverage file', async () => {
|
|
63
|
+
await expect(loadCoverageData('/nonexistent/path.json', '/tmp')).rejects.toThrow('Failed to read coverage file');
|
|
64
|
+
});
|
|
65
|
+
it('throws on invalid JSON in coverage file', async () => {
|
|
66
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'mutineer-coverage-'));
|
|
67
|
+
await fs.writeFile(path.join(tmpDir, 'bad.json'), 'not json', 'utf8');
|
|
68
|
+
try {
|
|
69
|
+
await expect(loadCoverageData(path.join(tmpDir, 'bad.json'), tmpDir)).rejects.toThrow('Failed to parse coverage file');
|
|
70
|
+
}
|
|
71
|
+
finally {
|
|
72
|
+
await fs.rm(tmpDir, { recursive: true, force: true });
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
it('skips files with missing statementMap or s', async () => {
|
|
76
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'mutineer-coverage-'));
|
|
77
|
+
const data = {
|
|
78
|
+
'/src/nomap.ts': { path: '/src/nomap.ts' },
|
|
79
|
+
'/src/nos.ts': {
|
|
80
|
+
path: '/src/nos.ts',
|
|
81
|
+
statementMap: {
|
|
82
|
+
'0': { start: { line: 1, column: 0 }, end: { line: 1, column: 10 } },
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
await fs.writeFile(path.join(tmpDir, 'cov.json'), JSON.stringify(data), 'utf8');
|
|
87
|
+
try {
|
|
88
|
+
const coverage = await loadCoverageData(path.join(tmpDir, 'cov.json'), tmpDir);
|
|
89
|
+
expect(coverage.coveredLines.size).toBe(0);
|
|
90
|
+
}
|
|
91
|
+
finally {
|
|
92
|
+
await fs.rm(tmpDir, { recursive: true, force: true });
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
it('normalizes relative file paths in coverage data', async () => {
|
|
96
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'mutineer-coverage-'));
|
|
97
|
+
const data = {
|
|
98
|
+
'src/relative.ts': {
|
|
99
|
+
path: 'src/relative.ts',
|
|
100
|
+
statementMap: {
|
|
101
|
+
'0': { start: { line: 1, column: 0 }, end: { line: 1, column: 10 } },
|
|
102
|
+
},
|
|
103
|
+
s: { '0': 1 },
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
await fs.writeFile(path.join(tmpDir, 'cov.json'), JSON.stringify(data), 'utf8');
|
|
107
|
+
try {
|
|
108
|
+
const coverage = await loadCoverageData(path.join(tmpDir, 'cov.json'), tmpDir);
|
|
109
|
+
// Should have resolved relative path
|
|
110
|
+
const absPath = path.join(tmpDir, 'src/relative.ts');
|
|
111
|
+
expect(coverage.coveredLines.has(absPath)).toBe(true);
|
|
112
|
+
}
|
|
113
|
+
finally {
|
|
114
|
+
await fs.rm(tmpDir, { recursive: true, force: true });
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
it('skips files where all statements have zero hits', async () => {
|
|
118
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'mutineer-coverage-'));
|
|
119
|
+
const data = {
|
|
120
|
+
'/src/uncovered.ts': {
|
|
121
|
+
path: '/src/uncovered.ts',
|
|
122
|
+
statementMap: {
|
|
123
|
+
'0': { start: { line: 1, column: 0 }, end: { line: 1, column: 10 } },
|
|
124
|
+
},
|
|
125
|
+
s: { '0': 0 },
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
await fs.writeFile(path.join(tmpDir, 'cov.json'), JSON.stringify(data), 'utf8');
|
|
129
|
+
try {
|
|
130
|
+
const coverage = await loadCoverageData(path.join(tmpDir, 'cov.json'), tmpDir);
|
|
131
|
+
expect(coverage.coveredLines.size).toBe(0);
|
|
132
|
+
}
|
|
133
|
+
finally {
|
|
134
|
+
await fs.rm(tmpDir, { recursive: true, force: true });
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
it('isLineCovered returns false for uncovered file', () => {
|
|
138
|
+
const coverage = { coveredLines: new Map() };
|
|
139
|
+
expect(isLineCovered(coverage, '/nonexistent.ts', 1)).toBe(false);
|
|
140
|
+
});
|
|
141
|
+
it('returns null for per-test coverage when no files found', async () => {
|
|
142
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'mutineer-coverage-'));
|
|
143
|
+
await fs.mkdir(path.join(tmpDir, 'empty-reports'), { recursive: true });
|
|
144
|
+
try {
|
|
145
|
+
const map = await loadPerTestCoverageData(path.join(tmpDir, 'empty-reports'), tmpDir);
|
|
146
|
+
expect(map).toBeNull();
|
|
147
|
+
}
|
|
148
|
+
finally {
|
|
149
|
+
await fs.rm(tmpDir, { recursive: true, force: true });
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
it('loads per-test coverage in format B (direct arrays)', async () => {
|
|
153
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'mutineer-coverage-'));
|
|
154
|
+
const reportsDir = path.join(tmpDir, 'coverage');
|
|
155
|
+
await fs.mkdir(reportsDir, { recursive: true });
|
|
156
|
+
const formatB = {
|
|
157
|
+
'/test/a.spec.ts': {
|
|
158
|
+
'/src/a.ts': [1, 2, 3],
|
|
159
|
+
},
|
|
160
|
+
};
|
|
161
|
+
await fs.writeFile(path.join(reportsDir, 'per-test-coverage.json'), JSON.stringify(formatB), 'utf8');
|
|
162
|
+
try {
|
|
163
|
+
const map = await loadPerTestCoverageData(reportsDir, tmpDir);
|
|
164
|
+
expect(map).not.toBeNull();
|
|
165
|
+
const lines = map.get('/test/a.spec.ts').get('/src/a.ts');
|
|
166
|
+
expect(lines.has(1)).toBe(true);
|
|
167
|
+
expect(lines.has(3)).toBe(true);
|
|
168
|
+
}
|
|
169
|
+
finally {
|
|
170
|
+
await fs.rm(tmpDir, { recursive: true, force: true });
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
it('handles per-test coverage with non-object/null values gracefully', async () => {
|
|
174
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'mutineer-coverage-'));
|
|
175
|
+
const reportsDir = path.join(tmpDir, 'coverage');
|
|
176
|
+
await fs.mkdir(reportsDir, { recursive: true });
|
|
177
|
+
const data = {
|
|
178
|
+
'/test/a.spec.ts': null,
|
|
179
|
+
'/test/b.spec.ts': 'not-an-object',
|
|
180
|
+
};
|
|
181
|
+
await fs.writeFile(path.join(reportsDir, 'per-test-coverage.json'), JSON.stringify(data), 'utf8');
|
|
182
|
+
try {
|
|
183
|
+
const map = await loadPerTestCoverageData(reportsDir, tmpDir);
|
|
184
|
+
// No valid entries, should return null
|
|
185
|
+
expect(map).toBeNull();
|
|
186
|
+
}
|
|
187
|
+
finally {
|
|
188
|
+
await fs.rm(tmpDir, { recursive: true, force: true });
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
it('uses relative reportsDir when not absolute', async () => {
|
|
192
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'mutineer-coverage-'));
|
|
193
|
+
const reportsDir = path.join(tmpDir, 'reports');
|
|
194
|
+
await fs.mkdir(reportsDir, { recursive: true });
|
|
195
|
+
const data = {
|
|
196
|
+
'/test/a.spec.ts': { '/src/a.ts': [1] },
|
|
197
|
+
};
|
|
198
|
+
await fs.writeFile(path.join(reportsDir, 'per-test-coverage.json'), JSON.stringify(data), 'utf8');
|
|
199
|
+
try {
|
|
200
|
+
// Pass relative path
|
|
201
|
+
const map = await loadPerTestCoverageData('reports', tmpDir);
|
|
202
|
+
expect(map).not.toBeNull();
|
|
203
|
+
}
|
|
204
|
+
finally {
|
|
205
|
+
await fs.rm(tmpDir, { recursive: true, force: true });
|
|
206
|
+
}
|
|
207
|
+
});
|
|
41
208
|
it('loads per-test coverage data from various shapes', async () => {
|
|
42
209
|
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'mutineer-coverage-'));
|
|
43
210
|
const reportsDir = path.join(tmpDir, 'coverage');
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
// We need to test with different DEBUG values, so we mock the module dynamically
|
|
3
|
+
describe('logger', () => {
|
|
4
|
+
let originalDebug;
|
|
5
|
+
beforeEach(() => {
|
|
6
|
+
originalDebug = process.env.MUTINEER_DEBUG;
|
|
7
|
+
vi.restoreAllMocks();
|
|
8
|
+
});
|
|
9
|
+
afterEach(() => {
|
|
10
|
+
if (originalDebug === undefined) {
|
|
11
|
+
delete process.env.MUTINEER_DEBUG;
|
|
12
|
+
}
|
|
13
|
+
else {
|
|
14
|
+
process.env.MUTINEER_DEBUG = originalDebug;
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
it('info logs to console.log', async () => {
|
|
18
|
+
const spy = vi.spyOn(console, 'log').mockImplementation(() => { });
|
|
19
|
+
const { createLogger } = await import('../logger.js');
|
|
20
|
+
const log = createLogger('test');
|
|
21
|
+
log.info('hello %s', 'world');
|
|
22
|
+
expect(spy).toHaveBeenCalledWith('hello %s', 'world');
|
|
23
|
+
});
|
|
24
|
+
it('warn logs to console.warn', async () => {
|
|
25
|
+
const spy = vi.spyOn(console, 'warn').mockImplementation(() => { });
|
|
26
|
+
const { createLogger } = await import('../logger.js');
|
|
27
|
+
const log = createLogger('test');
|
|
28
|
+
log.warn('warning message');
|
|
29
|
+
expect(spy).toHaveBeenCalledWith('warning message');
|
|
30
|
+
});
|
|
31
|
+
it('error logs to console.error', async () => {
|
|
32
|
+
const spy = vi.spyOn(console, 'error').mockImplementation(() => { });
|
|
33
|
+
const { createLogger } = await import('../logger.js');
|
|
34
|
+
const log = createLogger('test');
|
|
35
|
+
log.error('error message');
|
|
36
|
+
expect(spy).toHaveBeenCalledWith('error message');
|
|
37
|
+
});
|
|
38
|
+
it('debug logs to console.error when DEBUG is enabled', async () => {
|
|
39
|
+
const spy = vi.spyOn(console, 'error').mockImplementation(() => { });
|
|
40
|
+
// DEBUG is a module-level constant, so we check the tag prefix behavior
|
|
41
|
+
const { createLogger, DEBUG } = await import('../logger.js');
|
|
42
|
+
const log = createLogger('mytag');
|
|
43
|
+
log.debug('debug message');
|
|
44
|
+
if (DEBUG) {
|
|
45
|
+
expect(spy).toHaveBeenCalledWith('[mytag] debug message');
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
// When DEBUG is false, debug should be a no-op
|
|
49
|
+
expect(spy).not.toHaveBeenCalled();
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
it('debug includes the tag prefix', async () => {
|
|
53
|
+
const spy = vi.spyOn(console, 'error').mockImplementation(() => { });
|
|
54
|
+
const { createLogger, DEBUG } = await import('../logger.js');
|
|
55
|
+
const log = createLogger('custom-tag');
|
|
56
|
+
log.debug('test');
|
|
57
|
+
if (DEBUG) {
|
|
58
|
+
expect(spy).toHaveBeenCalledWith(expect.stringContaining('[custom-tag]'));
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { normalizePath } from '../normalizePath.js';
|
|
3
|
+
describe('normalizePath', () => {
|
|
4
|
+
it('replaces backslashes with forward slashes', () => {
|
|
5
|
+
expect(normalizePath('src\\utils\\file.ts')).toBe('src/utils/file.ts');
|
|
6
|
+
});
|
|
7
|
+
it('handles multiple consecutive backslashes', () => {
|
|
8
|
+
expect(normalizePath('src\\\\file.ts')).toBe('src//file.ts');
|
|
9
|
+
});
|
|
10
|
+
it('leaves forward slashes unchanged', () => {
|
|
11
|
+
expect(normalizePath('src/utils/file.ts')).toBe('src/utils/file.ts');
|
|
12
|
+
});
|
|
13
|
+
it('handles empty string', () => {
|
|
14
|
+
expect(normalizePath('')).toBe('');
|
|
15
|
+
});
|
|
16
|
+
it('handles mixed slashes', () => {
|
|
17
|
+
expect(normalizePath('src\\utils/file.ts')).toBe('src/utils/file.ts');
|
|
18
|
+
});
|
|
19
|
+
it('handles Windows-style absolute paths', () => {
|
|
20
|
+
expect(normalizePath('C:\\Users\\dev\\project')).toBe('C:/Users/dev/project');
|
|
21
|
+
});
|
|
22
|
+
});
|
|
@@ -47,4 +47,100 @@ describe('Progress', () => {
|
|
|
47
47
|
progress.finish();
|
|
48
48
|
expect(logSpy).toHaveBeenCalled();
|
|
49
49
|
});
|
|
50
|
+
it('ignores update and finish calls when not started', () => {
|
|
51
|
+
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
|
|
52
|
+
const progress = new Progress(3, { mode: 'list' });
|
|
53
|
+
progress.update('killed'); // no-op: not started
|
|
54
|
+
progress.finish(); // no-op: not started
|
|
55
|
+
expect(logSpy).not.toHaveBeenCalled();
|
|
56
|
+
});
|
|
57
|
+
it('ignores duplicate start and calls after finish', () => {
|
|
58
|
+
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
|
|
59
|
+
const progress = new Progress(2, { mode: 'list' });
|
|
60
|
+
progress.start();
|
|
61
|
+
progress.start(); // no-op: already started
|
|
62
|
+
progress.finish();
|
|
63
|
+
progress.update('killed'); // no-op: already finished
|
|
64
|
+
progress.finish(); // no-op: already finished
|
|
65
|
+
// Only one start message and one finish message
|
|
66
|
+
const logs = logSpy.mock.calls.map((args) => args.join(' '));
|
|
67
|
+
const startLogs = logs.filter((l) => l.includes('running 2 mutants'));
|
|
68
|
+
expect(startLogs).toHaveLength(1);
|
|
69
|
+
});
|
|
70
|
+
it('uses stdout stream when configured', () => {
|
|
71
|
+
vi.spyOn(console, 'log').mockImplementation(() => { });
|
|
72
|
+
const progress = new Progress(1, { mode: 'list', stream: 'stdout' });
|
|
73
|
+
progress.start();
|
|
74
|
+
progress.update('killed');
|
|
75
|
+
progress.finish();
|
|
76
|
+
// Should still log to console (non-TTY path)
|
|
77
|
+
expect(console.log).toHaveBeenCalled();
|
|
78
|
+
});
|
|
79
|
+
it('writes progress bar in TTY mode', () => {
|
|
80
|
+
const writeSpy = vi.fn();
|
|
81
|
+
const fakeStream = {
|
|
82
|
+
isTTY: true,
|
|
83
|
+
write: writeSpy,
|
|
84
|
+
columns: 120,
|
|
85
|
+
};
|
|
86
|
+
// Patch process.stderr to simulate TTY
|
|
87
|
+
const origStderr = process.stderr;
|
|
88
|
+
Object.defineProperty(process, 'stderr', {
|
|
89
|
+
value: fakeStream,
|
|
90
|
+
writable: true,
|
|
91
|
+
configurable: true,
|
|
92
|
+
});
|
|
93
|
+
try {
|
|
94
|
+
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
|
|
95
|
+
const progress = new Progress(3, { mode: 'bar' });
|
|
96
|
+
progress.start();
|
|
97
|
+
expect(writeSpy).toHaveBeenCalled(); // hide cursor + bar
|
|
98
|
+
const hideCursor = writeSpy.mock.calls.find((c) => c[0].includes('\x1b[?25l'));
|
|
99
|
+
expect(hideCursor).toBeDefined();
|
|
100
|
+
progress.update('killed');
|
|
101
|
+
progress.update('escaped');
|
|
102
|
+
progress.finish();
|
|
103
|
+
// Show cursor on finish
|
|
104
|
+
const showCursor = writeSpy.mock.calls.find((c) => c[0].includes('\x1b[?25h'));
|
|
105
|
+
expect(showCursor).toBeDefined();
|
|
106
|
+
// Final summary still logged via console.log
|
|
107
|
+
expect(logSpy).toHaveBeenCalled();
|
|
108
|
+
}
|
|
109
|
+
finally {
|
|
110
|
+
Object.defineProperty(process, 'stderr', {
|
|
111
|
+
value: origStderr,
|
|
112
|
+
writable: true,
|
|
113
|
+
configurable: true,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
it('handles zero total in bar mode', () => {
|
|
118
|
+
const writeSpy = vi.fn();
|
|
119
|
+
const fakeStream = {
|
|
120
|
+
isTTY: true,
|
|
121
|
+
write: writeSpy,
|
|
122
|
+
columns: 80,
|
|
123
|
+
};
|
|
124
|
+
const origStderr = process.stderr;
|
|
125
|
+
Object.defineProperty(process, 'stderr', {
|
|
126
|
+
value: fakeStream,
|
|
127
|
+
writable: true,
|
|
128
|
+
configurable: true,
|
|
129
|
+
});
|
|
130
|
+
try {
|
|
131
|
+
vi.spyOn(console, 'log').mockImplementation(() => { });
|
|
132
|
+
const progress = new Progress(0, { mode: 'bar' });
|
|
133
|
+
progress.start();
|
|
134
|
+
progress.finish();
|
|
135
|
+
// Should not throw; ratio should be 1 (100%)
|
|
136
|
+
expect(writeSpy).toHaveBeenCalled();
|
|
137
|
+
}
|
|
138
|
+
finally {
|
|
139
|
+
Object.defineProperty(process, 'stderr', {
|
|
140
|
+
value: origStderr,
|
|
141
|
+
writable: true,
|
|
142
|
+
configurable: true,
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
});
|
|
50
146
|
});
|
package/package.json
CHANGED
|
@@ -1,10 +1,33 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mutineerjs/mutineer",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "0.3.2",
|
|
4
|
+
"description": "A fast, targeted mutation testing framework for JavaScript and TypeScript",
|
|
4
5
|
"type": "module",
|
|
5
6
|
"private": false,
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"author": "Billy Jones",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/mutineerjs/mutineer.git"
|
|
12
|
+
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/mutineerjs/mutineer/issues"
|
|
15
|
+
},
|
|
16
|
+
"homepage": "https://github.com/mutineerjs/mutineer#readme",
|
|
17
|
+
"keywords": [
|
|
18
|
+
"mutation-testing",
|
|
19
|
+
"testing",
|
|
20
|
+
"vitest",
|
|
21
|
+
"jest",
|
|
22
|
+
"typescript",
|
|
23
|
+
"javascript",
|
|
24
|
+
"code-quality"
|
|
25
|
+
],
|
|
26
|
+
"engines": {
|
|
27
|
+
"node": ">=20"
|
|
28
|
+
},
|
|
6
29
|
"bin": {
|
|
7
|
-
"mutineer": "
|
|
30
|
+
"mutineer": "dist/bin/mutineer.js"
|
|
8
31
|
},
|
|
9
32
|
"main": "dist/index.js",
|
|
10
33
|
"types": "dist/index.d.ts",
|
|
@@ -24,46 +47,72 @@
|
|
|
24
47
|
"test:watch": "vitest",
|
|
25
48
|
"test:coverage": "vitest --run --coverage",
|
|
26
49
|
"lint": "eslint --config ./eslint.config.cjs --ext .ts,.js src/",
|
|
27
|
-
"mutate": "MUTINEER_DEBUG=0 tsx src/bin/mutineer.ts --config mutineer.config.ts"
|
|
50
|
+
"mutate": "MUTINEER_DEBUG=0 tsx src/bin/mutineer.ts --config mutineer.config.ts",
|
|
51
|
+
"prepare": "husky",
|
|
52
|
+
"prepublishOnly": "npm run build"
|
|
53
|
+
},
|
|
54
|
+
"lint-staged": {
|
|
55
|
+
"src/**/*.ts": [
|
|
56
|
+
"eslint --config ./eslint.config.cjs --fix",
|
|
57
|
+
"prettier --write"
|
|
58
|
+
],
|
|
59
|
+
"*.{json,md,yml,yaml}": [
|
|
60
|
+
"prettier --write"
|
|
61
|
+
]
|
|
28
62
|
},
|
|
29
63
|
"dependencies": {
|
|
30
|
-
"@babel/parser": "^7.
|
|
31
|
-
"@babel/traverse": "^7.
|
|
64
|
+
"@babel/parser": "^7.29.0",
|
|
65
|
+
"@babel/traverse": "^7.29.0",
|
|
32
66
|
"@babel/types": "^7.28.4",
|
|
33
67
|
"chalk": "^5.6.2",
|
|
34
68
|
"fast-glob": "^3.3.3",
|
|
35
|
-
"ink": "^
|
|
69
|
+
"ink": "^6.8.0",
|
|
36
70
|
"ink-spinner": "^5.0.0",
|
|
37
71
|
"magic-string": "^0.30.9",
|
|
38
|
-
"react": "^
|
|
39
|
-
"tsx": "^4.
|
|
72
|
+
"react": "^19.2.4",
|
|
73
|
+
"tsx": "^4.21.0"
|
|
40
74
|
},
|
|
41
75
|
"peerDependencies": {
|
|
42
76
|
"@vitejs/plugin-vue": "^5.1.4",
|
|
43
77
|
"@vitest/coverage-v8": "^4.0.15",
|
|
44
78
|
"@vue/compiler-sfc": ">=3.4.0",
|
|
45
79
|
"esbuild": "^0.25.10",
|
|
46
|
-
"vite": "^
|
|
47
|
-
"vitest": "^4.0.
|
|
80
|
+
"vite": "^7.3.1",
|
|
81
|
+
"vitest": "^4.0.18"
|
|
48
82
|
},
|
|
49
83
|
"peerDependenciesMeta": {
|
|
50
|
-
"@vitejs/plugin-vue": {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
"vitest": {
|
|
54
|
-
|
|
84
|
+
"@vitejs/plugin-vue": {
|
|
85
|
+
"optional": true
|
|
86
|
+
},
|
|
87
|
+
"@vitest/coverage-v8": {
|
|
88
|
+
"optional": true
|
|
89
|
+
},
|
|
90
|
+
"@vue/compiler-sfc": {
|
|
91
|
+
"optional": true
|
|
92
|
+
},
|
|
93
|
+
"vitest": {
|
|
94
|
+
"optional": true
|
|
95
|
+
},
|
|
96
|
+
"vite": {
|
|
97
|
+
"optional": true
|
|
98
|
+
}
|
|
55
99
|
},
|
|
56
100
|
"devDependencies": {
|
|
101
|
+
"@commitlint/cli": "^20.4.3",
|
|
102
|
+
"@commitlint/config-conventional": "^20.4.3",
|
|
57
103
|
"@types/babel__traverse": "^7.28.0",
|
|
58
|
-
"@types/node": "^
|
|
104
|
+
"@types/node": "^25.3.5",
|
|
59
105
|
"@types/react": "^19.2.14",
|
|
60
|
-
"@typescript-eslint/eslint-plugin": "^8.
|
|
106
|
+
"@typescript-eslint/eslint-plugin": "^8.56.1",
|
|
61
107
|
"@typescript-eslint/parser": "^8.47.0",
|
|
62
|
-
"eslint": "^
|
|
63
|
-
"
|
|
108
|
+
"eslint": "^10.0.3",
|
|
109
|
+
"husky": "^9.1.7",
|
|
110
|
+
"jsdom": "^28.1.0",
|
|
111
|
+
"lint-staged": "^16.1.0",
|
|
112
|
+
"prettier": "^3.8.1",
|
|
64
113
|
"typescript": "^5.5.4",
|
|
65
|
-
"vite": "^
|
|
66
|
-
"vitest": "^4.0.
|
|
67
|
-
"vue": "^3.5.
|
|
114
|
+
"vite": "^7.3.1",
|
|
115
|
+
"vitest": "^4.0.18",
|
|
116
|
+
"vue": "^3.5.30"
|
|
68
117
|
}
|
|
69
118
|
}
|