@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
package/README.md
CHANGED
|
@@ -26,25 +26,31 @@ Mutations are applied using Babel AST analysis, so operators inside strings and
|
|
|
26
26
|
|
|
27
27
|
## Supported Mutations (WIP)
|
|
28
28
|
|
|
29
|
-
| Category
|
|
30
|
-
|
|
|
31
|
-
| Equality
|
|
32
|
-
|
|
|
33
|
-
|
|
|
34
|
-
|
|
|
35
|
-
| Boundary
|
|
36
|
-
|
|
|
37
|
-
|
|
|
38
|
-
|
|
|
39
|
-
| Logical
|
|
40
|
-
|
|
|
41
|
-
|
|
|
42
|
-
| Arithmetic
|
|
43
|
-
|
|
|
44
|
-
|
|
|
45
|
-
|
|
|
46
|
-
|
|
|
47
|
-
|
|
|
29
|
+
| Category | Mutator | Transformation |
|
|
30
|
+
| ------------ | ------------------- | ------------------------------------ |
|
|
31
|
+
| Equality | `flipStrictEQ` | `===` → `!==` |
|
|
32
|
+
| | `flipStrictNEQ` | `!==` → `===` |
|
|
33
|
+
| | `flipEQ` | `==` → `!=` |
|
|
34
|
+
| | `flipNEQ` | `!=` → `==` |
|
|
35
|
+
| Boundary | `relaxLE` | `<=` → `<` |
|
|
36
|
+
| | `relaxGE` | `>=` → `>` |
|
|
37
|
+
| | `tightenLT` | `<` → `<=` |
|
|
38
|
+
| | `tightenGT` | `>` → `>=` |
|
|
39
|
+
| Logical | `andToOr` | `&&` → `\|\|` |
|
|
40
|
+
| | `orToAnd` | `\|\|` → `&&` |
|
|
41
|
+
| | `nullishToOr` | `??` → `\|\|` |
|
|
42
|
+
| Arithmetic | `addToSub` | `+` → `-` |
|
|
43
|
+
| | `subToAdd` | `-` → `+` |
|
|
44
|
+
| | `mulToDiv` | `*` → `/` |
|
|
45
|
+
| | `divToMul` | `/` → `*` |
|
|
46
|
+
| | `modToMul` | `%` → `*` |
|
|
47
|
+
| | `powerToMul` | `**` → `*` |
|
|
48
|
+
| Return value | `returnToNull` | `return x` → `return null` |
|
|
49
|
+
| | `returnToUndefined` | `return x` → `return undefined` |
|
|
50
|
+
| | `returnFlipBool` | `return true` ↔ `return false` |
|
|
51
|
+
| | `returnZero` | `return n` → `return 0` |
|
|
52
|
+
| | `returnEmptyStr` | `return s` → `return ''` |
|
|
53
|
+
| | `returnEmptyArr` | `return [...]` → `return []` |
|
|
48
54
|
|
|
49
55
|
## Installation
|
|
50
56
|
|
|
@@ -56,11 +62,11 @@ npm i @mutineerjs/mutineer
|
|
|
56
62
|
|
|
57
63
|
### Commands
|
|
58
64
|
|
|
59
|
-
| Command | Description
|
|
60
|
-
| ---------------- |
|
|
61
|
-
| `mutineer init` | Create a `mutineer.config.ts` with minimal defaults
|
|
62
|
-
| `mutineer run` | Run mutation testing
|
|
63
|
-
| `mutineer clean` | Remove leftover `__mutineer__` temp directories
|
|
65
|
+
| Command | Description |
|
|
66
|
+
| ---------------- | --------------------------------------------------- |
|
|
67
|
+
| `mutineer init` | Create a `mutineer.config.ts` with minimal defaults |
|
|
68
|
+
| `mutineer run` | Run mutation testing |
|
|
69
|
+
| `mutineer clean` | Remove leftover `__mutineer__` temp directories |
|
|
64
70
|
|
|
65
71
|
### Quick Start
|
|
66
72
|
|
|
@@ -93,7 +99,7 @@ npm run mutineer
|
|
|
93
99
|
| ------------------------ | ------------------------------------------ | ------------- |
|
|
94
100
|
| `--runner <type>` | Test runner: `vitest` or `jest` | `vitest` |
|
|
95
101
|
| `--config`, `-c` | Path to config file | auto-detected |
|
|
96
|
-
| `--concurrency <n>` | Parallel workers (1
|
|
102
|
+
| `--concurrency <n>` | Parallel workers (min 1) | CPUs - 1 |
|
|
97
103
|
| `--changed` | Only mutate files changed vs base branch | -- |
|
|
98
104
|
| `--changed-with-deps` | Include dependents of changed files | -- |
|
|
99
105
|
| `--only-covered-lines` | Skip mutations on uncovered lines | -- |
|
|
@@ -140,23 +146,23 @@ export default defineMutineerConfig({
|
|
|
140
146
|
|
|
141
147
|
### Config Options
|
|
142
148
|
|
|
143
|
-
| Option | Type
|
|
144
|
-
| ------------------- |
|
|
145
|
-
| `source` | `string \| string[]`
|
|
146
|
-
| `targets` | `MutateTarget[]`
|
|
147
|
-
| `runner` | `'vitest' \| 'jest'`
|
|
148
|
-
| `vitestConfig` | `string`
|
|
149
|
-
| `jestConfig` | `string`
|
|
150
|
-
| `include` | `string[]`
|
|
151
|
-
| `exclude` | `string[]`
|
|
152
|
-
| `excludePaths` | `string[]`
|
|
153
|
-
| `maxMutantsPerFile` | `number`
|
|
154
|
-
| `minKillPercent` | `number`
|
|
155
|
-
| `onlyCoveredLines` | `boolean`
|
|
156
|
-
| `perTestCoverage` | `boolean`
|
|
157
|
-
| `baseRef` | `string`
|
|
158
|
-
| `testPatterns` | `string[]`
|
|
159
|
-
| `extensions` | `string[]`
|
|
149
|
+
| Option | Type | Description |
|
|
150
|
+
| ------------------- | -------------------- | ------------------------------------------------ |
|
|
151
|
+
| `source` | `string \| string[]` | Glob patterns for source files to mutate |
|
|
152
|
+
| `targets` | `MutateTarget[]` | Explicit list of files to mutate |
|
|
153
|
+
| `runner` | `'vitest' \| 'jest'` | Test runner to use |
|
|
154
|
+
| `vitestConfig` | `string` | Path to vitest config |
|
|
155
|
+
| `jestConfig` | `string` | Path to jest config |
|
|
156
|
+
| `include` | `string[]` | Only run these mutators |
|
|
157
|
+
| `exclude` | `string[]` | Skip these mutators |
|
|
158
|
+
| `excludePaths` | `string[]` | Glob patterns for paths to skip |
|
|
159
|
+
| `maxMutantsPerFile` | `number` | Cap mutations per file |
|
|
160
|
+
| `minKillPercent` | `number` | Fail if kill rate is below this |
|
|
161
|
+
| `onlyCoveredLines` | `boolean` | Only mutate lines covered by tests |
|
|
162
|
+
| `perTestCoverage` | `boolean` | Use per-test coverage to select tests |
|
|
163
|
+
| `baseRef` | `string` | Git ref for `--changed` (default: `origin/main`) |
|
|
164
|
+
| `testPatterns` | `string[]` | Globs for test file discovery |
|
|
165
|
+
| `extensions` | `string[]` | File extensions to consider |
|
|
160
166
|
|
|
161
167
|
## File Support
|
|
162
168
|
|
package/dist/bin/mutineer.js
CHANGED
|
File without changes
|
|
@@ -1,6 +1,69 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
import { mutateModuleSource } from '../module.js';
|
|
3
|
+
describe('mutateModuleSource', () => {
|
|
4
|
+
it('returns empty array for code with no mutable patterns', () => {
|
|
5
|
+
const result = mutateModuleSource('const x = 1');
|
|
6
|
+
expect(result).toEqual([]);
|
|
7
|
+
});
|
|
8
|
+
it('generates mutations for equality operators', () => {
|
|
9
|
+
const code = 'if (a === b) {}';
|
|
10
|
+
const result = mutateModuleSource(code);
|
|
11
|
+
expect(result.length).toBeGreaterThan(0);
|
|
12
|
+
expect(result.some((v) => v.name === 'flipStrictEQ')).toBe(true);
|
|
13
|
+
});
|
|
14
|
+
it('generates mutations for logical operators', () => {
|
|
15
|
+
const code = 'const x = a && b';
|
|
16
|
+
const result = mutateModuleSource(code);
|
|
17
|
+
expect(result.some((v) => v.name === 'andToOr')).toBe(true);
|
|
18
|
+
});
|
|
19
|
+
it('generates mutations for arithmetic operators', () => {
|
|
20
|
+
const code = 'const x = a + b';
|
|
21
|
+
const result = mutateModuleSource(code);
|
|
22
|
+
expect(result.some((v) => v.name === 'addToSub')).toBe(true);
|
|
23
|
+
});
|
|
24
|
+
it('deduplicates identical mutations', () => {
|
|
25
|
+
const code = 'if (a === b) {}';
|
|
26
|
+
const result = mutateModuleSource(code);
|
|
27
|
+
const codes = result.map((v) => v.code);
|
|
28
|
+
expect(new Set(codes).size).toBe(codes.length);
|
|
29
|
+
});
|
|
30
|
+
it('respects include filter', () => {
|
|
31
|
+
const code = 'if (a === b && c || d) {}';
|
|
32
|
+
const result = mutateModuleSource(code, ['andToOr']);
|
|
33
|
+
expect(result.every((v) => v.name === 'andToOr')).toBe(true);
|
|
34
|
+
});
|
|
35
|
+
it('respects exclude filter', () => {
|
|
36
|
+
const code = 'if (a === b && c || d) {}';
|
|
37
|
+
const result = mutateModuleSource(code, undefined, ['flipStrictEQ']);
|
|
38
|
+
expect(result.every((v) => v.name !== 'flipStrictEQ')).toBe(true);
|
|
39
|
+
});
|
|
40
|
+
it('respects max limit', () => {
|
|
41
|
+
const code = 'if (a === b && c || d) {}';
|
|
42
|
+
const all = mutateModuleSource(code);
|
|
43
|
+
expect(all.length).toBeGreaterThan(1);
|
|
44
|
+
const limited = mutateModuleSource(code, undefined, undefined, 1);
|
|
45
|
+
expect(limited).toHaveLength(1);
|
|
46
|
+
});
|
|
47
|
+
it('throws when max is 0', () => {
|
|
48
|
+
expect(() => mutateModuleSource('code', undefined, undefined, 0)).toThrow('max must be a positive number');
|
|
49
|
+
});
|
|
50
|
+
it('throws when max is negative', () => {
|
|
51
|
+
expect(() => mutateModuleSource('code', undefined, undefined, -1)).toThrow('max must be a positive number');
|
|
52
|
+
});
|
|
53
|
+
it('includes line and col in variants', () => {
|
|
54
|
+
const code = 'const x = a + b';
|
|
55
|
+
const result = mutateModuleSource(code);
|
|
56
|
+
for (const v of result) {
|
|
57
|
+
expect(typeof v.line).toBe('number');
|
|
58
|
+
expect(typeof v.col).toBe('number');
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
it('produces valid mutated code strings', () => {
|
|
62
|
+
const code = 'const x = a <= b';
|
|
63
|
+
const result = mutateModuleSource(code);
|
|
64
|
+
for (const v of result) {
|
|
65
|
+
expect(typeof v.code).toBe('string');
|
|
66
|
+
expect(v.code).not.toBe(code);
|
|
67
|
+
}
|
|
5
68
|
});
|
|
6
69
|
});
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
import { mutateVueSfcScriptSetup } from '../sfc.js';
|
|
3
|
+
// Mock @vue/compiler-sfc
|
|
4
|
+
vi.mock('@vue/compiler-sfc', () => ({
|
|
5
|
+
parse: (code, _opts) => {
|
|
6
|
+
// Simple mock that extracts content between <script setup> tags
|
|
7
|
+
const startTag = '<script setup>';
|
|
8
|
+
const endTag = '</script>';
|
|
9
|
+
const startIdx = code.indexOf(startTag);
|
|
10
|
+
if (startIdx === -1) {
|
|
11
|
+
return { descriptor: { scriptSetup: null } };
|
|
12
|
+
}
|
|
13
|
+
const contentStart = startIdx + startTag.length;
|
|
14
|
+
const contentEnd = code.indexOf(endTag, contentStart);
|
|
15
|
+
return {
|
|
16
|
+
descriptor: {
|
|
17
|
+
scriptSetup: {
|
|
18
|
+
loc: {
|
|
19
|
+
start: { offset: contentStart },
|
|
20
|
+
end: { offset: contentEnd },
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
},
|
|
26
|
+
}));
|
|
27
|
+
describe('mutateVueSfcScriptSetup', () => {
|
|
28
|
+
it('returns empty array when there is no script setup block', async () => {
|
|
29
|
+
const code = '<template><div>hello</div></template>';
|
|
30
|
+
const result = await mutateVueSfcScriptSetup('test.vue', code);
|
|
31
|
+
expect(result).toEqual([]);
|
|
32
|
+
});
|
|
33
|
+
it('throws when max is 0', async () => {
|
|
34
|
+
await expect(mutateVueSfcScriptSetup('test.vue', '<script setup></script>', [], [], 0)).rejects.toThrow('max must be a positive number, got: 0');
|
|
35
|
+
});
|
|
36
|
+
it('throws when max is negative', async () => {
|
|
37
|
+
await expect(mutateVueSfcScriptSetup('test.vue', '<script setup></script>', [], [], -1)).rejects.toThrow('max must be a positive number, got: -1');
|
|
38
|
+
});
|
|
39
|
+
it('generates mutations for script setup content', async () => {
|
|
40
|
+
const code = '<script setup>\nconst x = a && b\n</script>';
|
|
41
|
+
const result = await mutateVueSfcScriptSetup('test.vue', code);
|
|
42
|
+
// Should find at least the andToOr mutation
|
|
43
|
+
expect(result.length).toBeGreaterThan(0);
|
|
44
|
+
// Every result should have the full SFC code (containing template tags)
|
|
45
|
+
for (const v of result) {
|
|
46
|
+
expect(v.code).toContain('<script setup>');
|
|
47
|
+
expect(v.code).toContain('</script>');
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
it('deduplicates identical mutations', async () => {
|
|
51
|
+
const code = '<script setup>\nconst x = a && b\n</script>';
|
|
52
|
+
const result = await mutateVueSfcScriptSetup('test.vue', code);
|
|
53
|
+
const outputs = result.map((v) => v.code);
|
|
54
|
+
const unique = new Set(outputs);
|
|
55
|
+
expect(outputs.length).toBe(unique.size);
|
|
56
|
+
});
|
|
57
|
+
it('respects max limit', async () => {
|
|
58
|
+
const code = '<script setup>\nconst x = a && b\nconst y = c || d\n</script>';
|
|
59
|
+
const result = await mutateVueSfcScriptSetup('test.vue', code, undefined, undefined, 1);
|
|
60
|
+
expect(result.length).toBeLessThanOrEqual(1);
|
|
61
|
+
});
|
|
62
|
+
it('filters mutators with include', async () => {
|
|
63
|
+
const code = '<script setup>\nconst x = a && b\n</script>';
|
|
64
|
+
const result = await mutateVueSfcScriptSetup('test.vue', code, ['andToOr']);
|
|
65
|
+
for (const v of result) {
|
|
66
|
+
expect(v.name).toBe('andToOr');
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
it('filters mutators with exclude', async () => {
|
|
70
|
+
const code = '<script setup>\nconst x = a && b\n</script>';
|
|
71
|
+
const result = await mutateVueSfcScriptSetup('test.vue', code, undefined, ['andToOr']);
|
|
72
|
+
for (const v of result) {
|
|
73
|
+
expect(v.name).not.toBe('andToOr');
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
});
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { generateMutationVariants, getFilteredRegistry } from '../variant-utils.js';
|
|
3
|
+
function makeMutator(name, mutations) {
|
|
4
|
+
return {
|
|
5
|
+
name,
|
|
6
|
+
description: name,
|
|
7
|
+
apply: () => mutations,
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
describe('generateMutationVariants', () => {
|
|
11
|
+
it('returns empty array for empty registry', () => {
|
|
12
|
+
expect(generateMutationVariants([], 'const x = 1')).toEqual([]);
|
|
13
|
+
});
|
|
14
|
+
it('generates variants from a single mutator', () => {
|
|
15
|
+
const mutator = makeMutator('flip', [
|
|
16
|
+
{ code: 'const x = 2', line: 1, col: 0 },
|
|
17
|
+
]);
|
|
18
|
+
const result = generateMutationVariants([mutator], 'const x = 1');
|
|
19
|
+
expect(result).toEqual([
|
|
20
|
+
{ name: 'flip', code: 'const x = 2', line: 1, col: 0 },
|
|
21
|
+
]);
|
|
22
|
+
});
|
|
23
|
+
it('generates variants from multiple mutators', () => {
|
|
24
|
+
const m1 = makeMutator('a', [{ code: 'code_a', line: 1, col: 0 }]);
|
|
25
|
+
const m2 = makeMutator('b', [{ code: 'code_b', line: 2, col: 5 }]);
|
|
26
|
+
const result = generateMutationVariants([m1, m2], 'original');
|
|
27
|
+
expect(result).toHaveLength(2);
|
|
28
|
+
expect(result[0].name).toBe('a');
|
|
29
|
+
expect(result[1].name).toBe('b');
|
|
30
|
+
});
|
|
31
|
+
it('deduplicates identical mutation outputs', () => {
|
|
32
|
+
const m1 = makeMutator('a', [{ code: 'same', line: 1, col: 0 }]);
|
|
33
|
+
const m2 = makeMutator('b', [{ code: 'same', line: 2, col: 0 }]);
|
|
34
|
+
const result = generateMutationVariants([m1, m2], 'original');
|
|
35
|
+
expect(result).toHaveLength(1);
|
|
36
|
+
expect(result[0].name).toBe('a');
|
|
37
|
+
});
|
|
38
|
+
it('throws when max is 0', () => {
|
|
39
|
+
expect(() => generateMutationVariants([], 'code', { max: 0 })).toThrow('max must be a positive number, got: 0');
|
|
40
|
+
});
|
|
41
|
+
it('throws when max is negative', () => {
|
|
42
|
+
expect(() => generateMutationVariants([], 'code', { max: -5 })).toThrow('max must be a positive number, got: -5');
|
|
43
|
+
});
|
|
44
|
+
it('respects max limit', () => {
|
|
45
|
+
const mutator = makeMutator('m', [
|
|
46
|
+
{ code: 'v1', line: 1, col: 0 },
|
|
47
|
+
{ code: 'v2', line: 2, col: 0 },
|
|
48
|
+
{ code: 'v3', line: 3, col: 0 },
|
|
49
|
+
]);
|
|
50
|
+
const result = generateMutationVariants([mutator], 'code', { max: 2 });
|
|
51
|
+
expect(result).toHaveLength(2);
|
|
52
|
+
});
|
|
53
|
+
it('stops iterating mutators once max is reached via inner return', () => {
|
|
54
|
+
const m1 = makeMutator('a', [
|
|
55
|
+
{ code: 'v1', line: 1, col: 0 },
|
|
56
|
+
{ code: 'v2', line: 2, col: 0 },
|
|
57
|
+
]);
|
|
58
|
+
const m2 = makeMutator('b', [{ code: 'v3', line: 3, col: 0 }]);
|
|
59
|
+
const result = generateMutationVariants([m1, m2], 'code', { max: 2 });
|
|
60
|
+
expect(result).toHaveLength(2);
|
|
61
|
+
expect(result.every((v) => v.name === 'a')).toBe(true);
|
|
62
|
+
});
|
|
63
|
+
// NOTE: The outer loop `break` (variant-utils.ts line 42) is dead code.
|
|
64
|
+
// The inner loop always `return`s immediately when max is reached after adding
|
|
65
|
+
// a new unique variant, so the outer loop check can never fire.
|
|
66
|
+
// Same applies to sfc.ts line 43.
|
|
67
|
+
it('works with undefined max', () => {
|
|
68
|
+
const mutator = makeMutator('m', [
|
|
69
|
+
{ code: 'v1', line: 1, col: 0 },
|
|
70
|
+
{ code: 'v2', line: 2, col: 0 },
|
|
71
|
+
]);
|
|
72
|
+
const result = generateMutationVariants([mutator], 'code', {});
|
|
73
|
+
expect(result).toHaveLength(2);
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
describe('getFilteredRegistry', () => {
|
|
77
|
+
it('returns the full registry when no filters', () => {
|
|
78
|
+
const result = getFilteredRegistry();
|
|
79
|
+
expect(result.length).toBeGreaterThan(0);
|
|
80
|
+
expect(result[0]).toHaveProperty('name');
|
|
81
|
+
expect(result[0]).toHaveProperty('apply');
|
|
82
|
+
});
|
|
83
|
+
it('filters by include', () => {
|
|
84
|
+
const result = getFilteredRegistry(['flipEQ', 'andToOr']);
|
|
85
|
+
expect(result.map((r) => r.name)).toEqual(['andToOr', 'flipEQ']);
|
|
86
|
+
});
|
|
87
|
+
it('filters by exclude', () => {
|
|
88
|
+
const all = getFilteredRegistry();
|
|
89
|
+
const result = getFilteredRegistry(undefined, ['flipEQ']);
|
|
90
|
+
expect(result.length).toBe(all.length - 1);
|
|
91
|
+
expect(result.find((r) => r.name === 'flipEQ')).toBeUndefined();
|
|
92
|
+
});
|
|
93
|
+
});
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { relaxLE, relaxGE, tightenLT, tightenGT, andToOr, orToAnd, nullishToOr, flipEQ, flipNEQ, flipStrictEQ, flipStrictNEQ, addToSub, subToAdd, mulToDiv, divToMul, modToMul, powerToMul, } from '../operator.js';
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// Shared behaviour (tested once; all mutators use the same factory)
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
describe('operator mutator shared behaviour', () => {
|
|
7
|
+
it('produces one mutation per matching operator occurrence', () => {
|
|
8
|
+
const src = `const ok = a && b && c`;
|
|
9
|
+
const results = andToOr.apply(src);
|
|
10
|
+
expect(results).toHaveLength(2);
|
|
11
|
+
});
|
|
12
|
+
it('reports the correct 1-based line number', () => {
|
|
13
|
+
const src = `const x = 1\nconst ok = a && b`;
|
|
14
|
+
const [result] = andToOr.apply(src);
|
|
15
|
+
expect(result.line).toBe(2);
|
|
16
|
+
});
|
|
17
|
+
it('reports the correct visual column', () => {
|
|
18
|
+
const src = `const ok = a && b`;
|
|
19
|
+
// ^ col 14 ('a' is at col 12, ' ' 13, '&' 14)
|
|
20
|
+
const [result] = andToOr.apply(src);
|
|
21
|
+
expect(result.col).toBe(14);
|
|
22
|
+
});
|
|
23
|
+
it('does not mutate operators inside string literals (AST-safe)', () => {
|
|
24
|
+
const src = `const s = 'a && b'`;
|
|
25
|
+
const results = andToOr.apply(src);
|
|
26
|
+
expect(results).toHaveLength(0);
|
|
27
|
+
});
|
|
28
|
+
it('does not mutate operators inside comments (AST-safe)', () => {
|
|
29
|
+
const src = `// a && b\nconst x = 1`;
|
|
30
|
+
const results = andToOr.apply(src);
|
|
31
|
+
expect(results).toHaveLength(0);
|
|
32
|
+
});
|
|
33
|
+
it('returns no results when the operator is absent', () => {
|
|
34
|
+
const src = `const x = a || b`;
|
|
35
|
+
const results = andToOr.apply(src);
|
|
36
|
+
expect(results).toHaveLength(0);
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
// Boundary mutators
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
describe('relaxLE', () => {
|
|
43
|
+
it("replaces '<=' with '<'", () => {
|
|
44
|
+
const src = `if (x <= 10) {}`;
|
|
45
|
+
const [result] = relaxLE.apply(src);
|
|
46
|
+
expect(result.code).toBe(`if (x < 10) {}`);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
describe('relaxGE', () => {
|
|
50
|
+
it("replaces '>=' with '>'", () => {
|
|
51
|
+
const src = `if (x >= 0) {}`;
|
|
52
|
+
const [result] = relaxGE.apply(src);
|
|
53
|
+
expect(result.code).toBe(`if (x > 0) {}`);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
describe('tightenLT', () => {
|
|
57
|
+
it("replaces '<' with '<='", () => {
|
|
58
|
+
const src = `if (x < 10) {}`;
|
|
59
|
+
const [result] = tightenLT.apply(src);
|
|
60
|
+
expect(result.code).toBe(`if (x <= 10) {}`);
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
describe('tightenGT', () => {
|
|
64
|
+
it("replaces '>' with '>='", () => {
|
|
65
|
+
const src = `if (x > 0) {}`;
|
|
66
|
+
const [result] = tightenGT.apply(src);
|
|
67
|
+
expect(result.code).toBe(`if (x >= 0) {}`);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
// Logical mutators
|
|
72
|
+
// ---------------------------------------------------------------------------
|
|
73
|
+
describe('andToOr', () => {
|
|
74
|
+
it("replaces '&&' with '||'", () => {
|
|
75
|
+
const src = `const ok = a && b`;
|
|
76
|
+
const [result] = andToOr.apply(src);
|
|
77
|
+
expect(result.code).toBe(`const ok = a || b`);
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
describe('orToAnd', () => {
|
|
81
|
+
it("replaces '||' with '&&'", () => {
|
|
82
|
+
const src = `const ok = a || b`;
|
|
83
|
+
const [result] = orToAnd.apply(src);
|
|
84
|
+
expect(result.code).toBe(`const ok = a && b`);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
describe('nullishToOr', () => {
|
|
88
|
+
it("replaces '??' with '||'", () => {
|
|
89
|
+
const src = `const v = x ?? defaultValue`;
|
|
90
|
+
const [result] = nullishToOr.apply(src);
|
|
91
|
+
expect(result.code).toBe(`const v = x || defaultValue`);
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
// ---------------------------------------------------------------------------
|
|
95
|
+
// Equality mutators
|
|
96
|
+
// ---------------------------------------------------------------------------
|
|
97
|
+
describe('flipEQ', () => {
|
|
98
|
+
it("replaces '==' with '!='", () => {
|
|
99
|
+
const src = `if (a == b) {}`;
|
|
100
|
+
const [result] = flipEQ.apply(src);
|
|
101
|
+
expect(result.code).toBe(`if (a != b) {}`);
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
describe('flipNEQ', () => {
|
|
105
|
+
it("replaces '!=' with '=='", () => {
|
|
106
|
+
const src = `if (a != b) {}`;
|
|
107
|
+
const [result] = flipNEQ.apply(src);
|
|
108
|
+
expect(result.code).toBe(`if (a == b) {}`);
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
describe('flipStrictEQ', () => {
|
|
112
|
+
it("replaces '===' with '!=='", () => {
|
|
113
|
+
const src = `if (a === b) {}`;
|
|
114
|
+
const [result] = flipStrictEQ.apply(src);
|
|
115
|
+
expect(result.code).toBe(`if (a !== b) {}`);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
describe('flipStrictNEQ', () => {
|
|
119
|
+
it("replaces '!==' with '==='", () => {
|
|
120
|
+
const src = `if (a !== b) {}`;
|
|
121
|
+
const [result] = flipStrictNEQ.apply(src);
|
|
122
|
+
expect(result.code).toBe(`if (a === b) {}`);
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
// ---------------------------------------------------------------------------
|
|
126
|
+
// Arithmetic mutators
|
|
127
|
+
// ---------------------------------------------------------------------------
|
|
128
|
+
describe('addToSub', () => {
|
|
129
|
+
it("replaces '+' with '-'", () => {
|
|
130
|
+
const src = `const n = a + b`;
|
|
131
|
+
const [result] = addToSub.apply(src);
|
|
132
|
+
expect(result.code).toBe(`const n = a - b`);
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
describe('subToAdd', () => {
|
|
136
|
+
it("replaces '-' with '+'", () => {
|
|
137
|
+
const src = `const n = a - b`;
|
|
138
|
+
const [result] = subToAdd.apply(src);
|
|
139
|
+
expect(result.code).toBe(`const n = a + b`);
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
describe('mulToDiv', () => {
|
|
143
|
+
it("replaces '*' with '/'", () => {
|
|
144
|
+
const src = `const n = a * b`;
|
|
145
|
+
const [result] = mulToDiv.apply(src);
|
|
146
|
+
expect(result.code).toBe(`const n = a / b`);
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
describe('divToMul', () => {
|
|
150
|
+
it("replaces '/' with '*'", () => {
|
|
151
|
+
const src = `const n = a / b`;
|
|
152
|
+
const [result] = divToMul.apply(src);
|
|
153
|
+
expect(result.code).toBe(`const n = a * b`);
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
describe('modToMul', () => {
|
|
157
|
+
it("replaces '%' with '*'", () => {
|
|
158
|
+
const src = `const n = a % b`;
|
|
159
|
+
const [result] = modToMul.apply(src);
|
|
160
|
+
expect(result.code).toBe(`const n = a * b`);
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
describe('powerToMul', () => {
|
|
164
|
+
it("replaces '**' with '*'", () => {
|
|
165
|
+
const src = `const n = a ** b`;
|
|
166
|
+
const [result] = powerToMul.apply(src);
|
|
167
|
+
expect(result.code).toBe(`const n = a * b`);
|
|
168
|
+
});
|
|
169
|
+
});
|
|
@@ -18,6 +18,12 @@ const ALL_NAMES = [
|
|
|
18
18
|
'divToMul',
|
|
19
19
|
'modToMul',
|
|
20
20
|
'powerToMul',
|
|
21
|
+
'returnToNull',
|
|
22
|
+
'returnToUndefined',
|
|
23
|
+
'returnFlipBool',
|
|
24
|
+
'returnZero',
|
|
25
|
+
'returnEmptyStr',
|
|
26
|
+
'returnEmptyArr',
|
|
21
27
|
];
|
|
22
28
|
describe('mutator registry', () => {
|
|
23
29
|
it('returns all mutators by default in declared order', () => {
|