apex-mutation-testing 1.6.0 → 1.7.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.
Files changed (46) hide show
  1. package/README.md +34 -3
  2. package/lib/adapter/apexClassRepository.d.ts +14 -1
  3. package/lib/adapter/apexClassRepository.js +93 -31
  4. package/lib/adapter/apexClassRepository.js.map +1 -1
  5. package/lib/commands/apex/mutation/test/run.d.ts +1 -0
  6. package/lib/commands/apex/mutation/test/run.js +4 -0
  7. package/lib/commands/apex/mutation/test/run.js.map +1 -1
  8. package/lib/mutator/experimentalSwitchMutator.d.ts +7 -0
  9. package/lib/mutator/experimentalSwitchMutator.js +18 -1
  10. package/lib/mutator/experimentalSwitchMutator.js.map +1 -1
  11. package/lib/mutator/mutationListener.d.ts +2 -0
  12. package/lib/mutator/mutationListener.js +20 -8
  13. package/lib/mutator/mutationListener.js.map +1 -1
  14. package/lib/reporter/HTMLReporter.js +68 -12
  15. package/lib/reporter/HTMLReporter.js.map +1 -1
  16. package/lib/service/configReader.js +9 -1
  17. package/lib/service/configReader.js.map +1 -1
  18. package/lib/service/exactColoring.d.ts +25 -0
  19. package/lib/service/exactColoring.js +143 -0
  20. package/lib/service/exactColoring.js.map +1 -0
  21. package/lib/service/groupExecutor.d.ts +29 -0
  22. package/lib/service/groupExecutor.js +194 -0
  23. package/lib/service/groupExecutor.js.map +1 -0
  24. package/lib/service/mutantGenerator.d.ts +14 -4
  25. package/lib/service/mutantGenerator.js +42 -13
  26. package/lib/service/mutantGenerator.js.map +1 -1
  27. package/lib/service/mutationGrouper.d.ts +22 -0
  28. package/lib/service/mutationGrouper.js +130 -0
  29. package/lib/service/mutationGrouper.js.map +1 -0
  30. package/lib/service/mutationLocation.d.ts +12 -0
  31. package/lib/service/mutationLocation.js +54 -0
  32. package/lib/service/mutationLocation.js.map +1 -0
  33. package/lib/service/mutationTestingService.d.ts +3 -7
  34. package/lib/service/mutationTestingService.js +83 -158
  35. package/lib/service/mutationTestingService.js.map +1 -1
  36. package/lib/service/timeUtils.d.ts +1 -0
  37. package/lib/service/timeUtils.js +8 -0
  38. package/lib/service/timeUtils.js.map +1 -1
  39. package/lib/service/typeDiscoverer.d.ts +12 -0
  40. package/lib/service/typeDiscoverer.js +46 -2
  41. package/lib/service/typeDiscoverer.js.map +1 -1
  42. package/lib/type/ApexMutationParameter.d.ts +1 -0
  43. package/messages/apex.mutation.test.run.md +13 -1
  44. package/npm-shrinkwrap.json +1528 -1353
  45. package/oclif.manifest.json +7 -1
  46. package/package.json +18 -17
@@ -0,0 +1,130 @@
1
+ import { buildAdjacency } from './exactColoring.js';
2
+ // Partition mutations into the smallest number of conflict-free groups using
3
+ // DSATUR (Brélaz, 1979): the strongest polynomial-time graph-coloring
4
+ // heuristic. At each step, color the uncolored vertex whose **saturation**
5
+ // (count of distinct colors already used by its colored neighbors) is highest;
6
+ // tiebreak on raw degree, then input order. Provably optimal on bipartite
7
+ // graphs and several other structured classes; near-optimal on the rest.
8
+ //
9
+ // Each color = one batched deployment + one batched test run. Two mutations
10
+ // share a color iff their covering tests are pairwise disjoint, so test
11
+ // outcomes can be reverse-mapped per mutation without ambiguity.
12
+ //
13
+ // The conflict graph is the intersection graph of per-mutation test sets:
14
+ // every test method T induces a clique among the mutations it covers. The
15
+ // largest such test-induced set is a free lower bound on the chromatic
16
+ // number; pre-coloring it before DSATUR runs both gives the heuristic a
17
+ // maximally-constrained start and surfaces an optimality certificate
18
+ // (groups.length === lowerBound implies provably optimal).
19
+ export const groupMutations = (mutations, testMethodsPerLine) => {
20
+ const { groups, lowerBound } = groupMutationsWithInternals(mutations, testMethodsPerLine);
21
+ return { groups, lowerBound };
22
+ };
23
+ // Like `groupMutations` but additionally exposes the conflict graph, witness
24
+ // clique, DSATUR coloring, and per-mutation test sets. Used by the optional
25
+ // SAT-based exact-coloring path so it can re-use the work without rebuilding
26
+ // the graph.
27
+ export const groupMutationsWithInternals = (mutations, testMethodsPerLine) => {
28
+ const n = mutations.length;
29
+ if (n === 0) {
30
+ return {
31
+ groups: [],
32
+ lowerBound: 0,
33
+ internals: { adjacency: [], witness: [], coloring: [], tests: [] },
34
+ };
35
+ }
36
+ const tests = mutations.map(m => testMethodsPerLine.get(m.target.startToken.line) ?? new Set());
37
+ const adjacency = buildAdjacency(tests);
38
+ const degree = adjacency.map(neighbors => neighbors.length);
39
+ const witness = computeLowerBoundClique(tests);
40
+ const color = new Array(n).fill(-1);
41
+ const saturation = new Array(n).fill(0);
42
+ const neighborColors = Array.from({ length: n }, () => new Set());
43
+ for (let k = 0; k < witness.length; ++k) {
44
+ const v = witness[k];
45
+ color[v] = k;
46
+ propagate(v, k, adjacency, color, neighborColors, saturation);
47
+ }
48
+ for (let step = 0; step < n - witness.length; ++step) {
49
+ const pick = pickNextVertex(color, saturation, degree);
50
+ const chosenColor = pickSmallestAvailableColor(neighborColors[pick]);
51
+ color[pick] = chosenColor;
52
+ propagate(pick, chosenColor, adjacency, color, neighborColors, saturation);
53
+ }
54
+ return {
55
+ groups: assembleGroups(mutations, tests, color),
56
+ lowerBound: witness.length,
57
+ internals: { adjacency, witness, coloring: color, tests },
58
+ };
59
+ };
60
+ // Returns the indices of the largest test-induced clique. Each test method T
61
+ // induces a clique among {m : T ∈ tests(m)}; we pick the largest such bucket.
62
+ // O(n · k) where k = avg tests per mutation. Indexes by mutation (not by
63
+ // line) so two mutations sharing the same Set reference still produce two
64
+ // distinct witness entries.
65
+ const computeLowerBoundClique = (tests) => {
66
+ const testToMutations = new Map();
67
+ for (let i = 0; i < tests.length; ++i) {
68
+ for (const t of tests[i]) {
69
+ const bucket = testToMutations.get(t);
70
+ if (bucket === undefined)
71
+ testToMutations.set(t, [i]);
72
+ else
73
+ bucket.push(i);
74
+ }
75
+ }
76
+ let best = [];
77
+ for (const indices of testToMutations.values()) {
78
+ if (indices.length > best.length)
79
+ best = indices;
80
+ }
81
+ // Stable canonical order: ascending by mutation index. Combined with the
82
+ // strict-`>` tiebreak in pickNextVertex, makes the entire pipeline
83
+ // deterministic for fixed input.
84
+ return best.slice().sort((a, b) => a - b);
85
+ };
86
+ const propagate = (v, c, adjacency, color, neighborColors, saturation) => {
87
+ for (const neighbor of adjacency[v]) {
88
+ if (color[neighbor] === -1 && !neighborColors[neighbor].has(c)) {
89
+ neighborColors[neighbor].add(c);
90
+ ++saturation[neighbor];
91
+ }
92
+ }
93
+ };
94
+ // Strict `>` comparisons mean equal-key candidates retain the first-encountered
95
+ // pick — which is the lowest unprocessed index. Switching to `>=` would
96
+ // silently invert determinism on every tied pair; keep the contract.
97
+ const pickNextVertex = (color, saturation, degree) => {
98
+ let pick = -1;
99
+ for (let i = 0; i < color.length; ++i) {
100
+ if (color[i] !== -1)
101
+ continue;
102
+ if (pick === -1 ||
103
+ saturation[i] > saturation[pick] ||
104
+ (saturation[i] === saturation[pick] && degree[i] > degree[pick])) {
105
+ pick = i;
106
+ }
107
+ }
108
+ return pick;
109
+ };
110
+ const pickSmallestAvailableColor = (neighborColors) => {
111
+ let candidate = 0;
112
+ while (neighborColors.has(candidate)) {
113
+ ++candidate;
114
+ }
115
+ return candidate;
116
+ };
117
+ export const assembleGroups = (mutations, tests, color) => {
118
+ const groups = [];
119
+ for (let i = 0; i < mutations.length; ++i) {
120
+ const c = color[i];
121
+ while (groups.length <= c) {
122
+ groups.push({ mutations: [], testMethods: new Set() });
123
+ }
124
+ groups[c].mutations.push(mutations[i]);
125
+ for (const t of tests[i])
126
+ groups[c].testMethods.add(t);
127
+ }
128
+ return groups;
129
+ };
130
+ //# sourceMappingURL=mutationGrouper.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mutationGrouper.js","sourceRoot":"","sources":["../../src/service/mutationGrouper.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAA;AAuBnD,6EAA6E;AAC7E,sEAAsE;AACtE,2EAA2E;AAC3E,+EAA+E;AAC/E,0EAA0E;AAC1E,yEAAyE;AACzE,EAAE;AACF,4EAA4E;AAC5E,wEAAwE;AACxE,iEAAiE;AACjE,EAAE;AACF,0EAA0E;AAC1E,0EAA0E;AAC1E,uEAAuE;AACvE,wEAAwE;AACxE,qEAAqE;AACrE,2DAA2D;AAC3D,MAAM,CAAC,MAAM,cAAc,GAAG,CAC5B,SAAsC,EACtC,kBAA4C,EAC5B,EAAE;IAClB,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,GAAG,2BAA2B,CACxD,SAAS,EACT,kBAAkB,CACnB,CAAA;IACD,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,CAAA;AAC/B,CAAC,CAAA;AAED,6EAA6E;AAC7E,4EAA4E;AAC5E,6EAA6E;AAC7E,aAAa;AACb,MAAM,CAAC,MAAM,2BAA2B,GAAG,CACzC,SAAsC,EACtC,kBAA4C,EACf,EAAE;IAC/B,MAAM,CAAC,GAAG,SAAS,CAAC,MAAM,CAAA;IAC1B,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;QACZ,OAAO;YACL,MAAM,EAAE,EAAE;YACV,UAAU,EAAE,CAAC;YACb,SAAS,EAAE,EAAE,SAAS,EAAE,EAAE,EAAE,OAAO,EAAE,EAAE,EAAE,QAAQ,EAAE,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE;SACnE,CAAA;IACH,CAAC;IAED,MAAM,KAAK,GAAG,SAAS,CAAC,GAAG,CACzB,CAAC,CAAC,EAAE,CAAC,kBAAkB,CAAC,GAAG,CAAC,CAAC,CAAC,MAAM,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,IAAI,GAAG,EAAU,CAC3E,CAAA;IACD,MAAM,SAAS,GAAG,cAAc,CAAC,KAAK,CAAC,CAAA;IACvC,MAAM,MAAM,GAAG,SAAS,CAAC,GAAG,CAAC,SAAS,CAAC,EAAE,CAAC,SAAS,CAAC,MAAM,CAAC,CAAA;IAC3D,MAAM,OAAO,GAAG,uBAAuB,CAAC,KAAK,CAAC,CAAA;IAE9C,MAAM,KAAK,GAAG,IAAI,KAAK,CAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAA;IAC3C,MAAM,UAAU,GAAG,IAAI,KAAK,CAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;IAC/C,MAAM,cAAc,GAAuB,KAAK,CAAC,IAAI,CACnD,EAAE,MAAM,EAAE,CAAC,EAAE,EACb,GAAG,EAAE,CAAC,IAAI,GAAG,EAAE,CAChB,CAAA;IAED,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,EAAE,CAAC;QACxC,MAAM,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAA;QACpB,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,CAAA;QACZ,SAAS,CAAC,CAAC,EAAE,CAAC,EAAE,SAAS,EAAE,KAAK,EAAE,cAAc,EAAE,UAAU,CAAC,CAAA;IAC/D,CAAC;IAED,KAAK,IAAI,IAAI,GAAG,CAAC,EAAE,IAAI,GAAG,CAAC,GAAG,OAAO,CAAC,MAAM,EAAE,EAAE,IAAI,EAAE,CAAC;QACrD,MAAM,IAAI,GAAG,cAAc,CAAC,KAAK,EAAE,UAAU,EAAE,MAAM,CAAC,CAAA;QACtD,MAAM,WAAW,GAAG,0BAA0B,CAAC,cAAc,CAAC,IAAI,CAAC,CAAC,CAAA;QACpE,KAAK,CAAC,IAAI,CAAC,GAAG,WAAW,CAAA;QACzB,SAAS,CAAC,IAAI,EAAE,WAAW,EAAE,SAAS,EAAE,KAAK,EAAE,cAAc,EAAE,UAAU,CAAC,CAAA;IAC5E,CAAC;IAED,OAAO;QACL,MAAM,EAAE,cAAc,CAAC,SAAS,EAAE,KAAK,EAAE,KAAK,CAAC;QAC/C,UAAU,EAAE,OAAO,CAAC,MAAM;QAC1B,SAAS,EAAE,EAAE,SAAS,EAAE,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,KAAK,EAAE;KAC1D,CAAA;AACH,CAAC,CAAA;AAED,6EAA6E;AAC7E,8EAA8E;AAC9E,yEAAyE;AACzE,0EAA0E;AAC1E,4BAA4B;AAC5B,MAAM,uBAAuB,GAAG,CAC9B,KAAiC,EACvB,EAAE;IACZ,MAAM,eAAe,GAAG,IAAI,GAAG,EAAoB,CAAA;IACnD,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,EAAE,CAAC,EAAE,CAAC;QACtC,KAAK,MAAM,CAAC,IAAI,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC;YACzB,MAAM,MAAM,GAAG,eAAe,CAAC,GAAG,CAAC,CAAC,CAAC,CAAA;YACrC,IAAI,MAAM,KAAK,SAAS;gBAAE,eAAe,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAA;;gBAChD,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;QACrB,CAAC;IACH,CAAC;IACD,IAAI,IAAI,GAAa,EAAE,CAAA;IACvB,KAAK,MAAM,OAAO,IAAI,eAAe,CAAC,MAAM,EAAE,EAAE,CAAC;QAC/C,IAAI,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,MAAM;YAAE,IAAI,GAAG,OAAO,CAAA;IAClD,CAAC;IACD,yEAAyE;IACzE,mEAAmE;IACnE,iCAAiC;IACjC,OAAO,IAAI,CAAC,KAAK,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAA;AAC3C,CAAC,CAAA;AAED,MAAM,SAAS,GAAG,CAChB,CAAS,EACT,CAAS,EACT,SAA+C,EAC/C,KAA4B,EAC5B,cAA0C,EAC1C,UAAoB,EACd,EAAE;IACR,KAAK,MAAM,QAAQ,IAAI,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC;QACpC,IAAI,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,cAAc,CAAC,QAAQ,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;YAC/D,cAAc,CAAC,QAAQ,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAA;YAC/B,EAAE,UAAU,CAAC,QAAQ,CAAC,CAAA;QACxB,CAAC;IACH,CAAC;AACH,CAAC,CAAA;AAED,gFAAgF;AAChF,wEAAwE;AACxE,qEAAqE;AACrE,MAAM,cAAc,GAAG,CACrB,KAA4B,EAC5B,UAAiC,EACjC,MAA6B,EACrB,EAAE;IACV,IAAI,IAAI,GAAG,CAAC,CAAC,CAAA;IACb,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,EAAE,CAAC,EAAE,CAAC;QACtC,IAAI,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;YAAE,SAAQ;QAC7B,IACE,IAAI,KAAK,CAAC,CAAC;YACX,UAAU,CAAC,CAAC,CAAC,GAAG,UAAU,CAAC,IAAI,CAAC;YAChC,CAAC,UAAU,CAAC,CAAC,CAAC,KAAK,UAAU,CAAC,IAAI,CAAC,IAAI,MAAM,CAAC,CAAC,CAAC,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC,EAChE,CAAC;YACD,IAAI,GAAG,CAAC,CAAA;QACV,CAAC;IACH,CAAC;IACD,OAAO,IAAI,CAAA;AACb,CAAC,CAAA;AAED,MAAM,0BAA0B,GAAG,CACjC,cAAmC,EAC3B,EAAE;IACV,IAAI,SAAS,GAAG,CAAC,CAAA;IACjB,OAAO,cAAc,CAAC,GAAG,CAAC,SAAS,CAAC,EAAE,CAAC;QACrC,EAAE,SAAS,CAAA;IACb,CAAC;IACD,OAAO,SAAS,CAAA;AAClB,CAAC,CAAA;AAED,MAAM,CAAC,MAAM,cAAc,GAAG,CAC5B,SAAsC,EACtC,KAAiC,EACjC,KAA4B,EACX,EAAE;IACnB,MAAM,MAAM,GAAoB,EAAE,CAAA;IAClC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,SAAS,CAAC,MAAM,EAAE,EAAE,CAAC,EAAE,CAAC;QAC1C,MAAM,CAAC,GAAG,KAAK,CAAC,CAAC,CAAC,CAAA;QAClB,OAAO,MAAM,CAAC,MAAM,IAAI,CAAC,EAAE,CAAC;YAC1B,MAAM,CAAC,IAAI,CAAC,EAAE,SAAS,EAAE,EAAE,EAAE,WAAW,EAAE,IAAI,GAAG,EAAE,EAAE,CAAC,CAAA;QACxD,CAAC;QACD,MAAM,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAA;QACtC,KAAK,MAAM,CAAC,IAAI,KAAK,CAAC,CAAC,CAAC;YAAE,MAAM,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,CAAC,CAAA;IACxD,CAAC;IACD,OAAO,MAAM,CAAA;AACf,CAAC,CAAA"}
@@ -0,0 +1,12 @@
1
+ import { ApexMutation } from '../type/ApexMutation.js';
2
+ export declare const calculateMutationPosition: (mutation: ApexMutation) => {
3
+ start: {
4
+ line: number;
5
+ column: number;
6
+ };
7
+ end: {
8
+ line: number;
9
+ column: number;
10
+ };
11
+ };
12
+ export declare const extractMutationOriginalText: (mutation: ApexMutation, sourceContent: string) => string;
@@ -0,0 +1,54 @@
1
+ // Advance a 1-indexed (line, column) cursor through `text`, returning the
2
+ // position immediately AFTER the last character. Handles tokens whose text
3
+ // spans newlines (multi-line string literals, block comments).
4
+ //
5
+ // Used to compute the Stryker `end` position for a mutation: ANTLR tokens
6
+ // expose `line` and `charPositionInLine` for the START of the token but not
7
+ // past the end; walking `endToken.text` closes that gap without needing a
8
+ // separate line-offset index over the whole source.
9
+ const advancePosition = (text, startLine, startColumn) => {
10
+ let line = startLine;
11
+ let column = startColumn;
12
+ for (let i = 0; i < text.length; i++) {
13
+ if (text.charCodeAt(i) === 10 /* \n */) {
14
+ line++;
15
+ column = 1;
16
+ }
17
+ else {
18
+ column++;
19
+ }
20
+ }
21
+ return { line, column };
22
+ };
23
+ export const calculateMutationPosition = (mutation) => {
24
+ const start = mutation.target.startToken;
25
+ const end = mutation.target.endToken;
26
+ if (start.startIndex === undefined ||
27
+ end.stopIndex === undefined ||
28
+ end.text === undefined) {
29
+ throw new Error(`Failed to calculate position for mutation: ${mutation.mutationName}`);
30
+ }
31
+ // ANTLR tokens expose the position of the FIRST character directly.
32
+ // The Stryker `end` position is exclusive (one past the last char), so
33
+ // we walk endToken.text to advance from the end token's own start.
34
+ // This correctly handles tokens that span newlines (multi-line string
35
+ // literals, block comments).
36
+ return {
37
+ start: {
38
+ line: start.line,
39
+ column: start.charPositionInLine + 1,
40
+ },
41
+ end: advancePosition(end.text, end.line, end.charPositionInLine + 1),
42
+ };
43
+ };
44
+ export const extractMutationOriginalText = (mutation, sourceContent) => {
45
+ const start = mutation.target.startToken;
46
+ const end = mutation.target.endToken;
47
+ if (start.startIndex !== undefined &&
48
+ end.stopIndex !== undefined &&
49
+ sourceContent) {
50
+ return sourceContent.substring(start.startIndex, end.stopIndex + 1);
51
+ }
52
+ throw new Error(`Failed to extract original text for mutation: ${mutation.mutationName}`);
53
+ };
54
+ //# sourceMappingURL=mutationLocation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mutationLocation.js","sourceRoot":"","sources":["../../src/service/mutationLocation.ts"],"names":[],"mappings":"AAEA,0EAA0E;AAC1E,2EAA2E;AAC3E,+DAA+D;AAC/D,EAAE;AACF,0EAA0E;AAC1E,4EAA4E;AAC5E,0EAA0E;AAC1E,oDAAoD;AACpD,MAAM,eAAe,GAAG,CACtB,IAAY,EACZ,SAAiB,EACjB,WAAmB,EACe,EAAE;IACpC,IAAI,IAAI,GAAG,SAAS,CAAA;IACpB,IAAI,MAAM,GAAG,WAAW,CAAA;IACxB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACrC,IAAI,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,KAAK,EAAE,CAAC,QAAQ,EAAE,CAAC;YACvC,IAAI,EAAE,CAAA;YACN,MAAM,GAAG,CAAC,CAAA;QACZ,CAAC;aAAM,CAAC;YACN,MAAM,EAAE,CAAA;QACV,CAAC;IACH,CAAC;IACD,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,CAAA;AACzB,CAAC,CAAA;AAED,MAAM,CAAC,MAAM,yBAAyB,GAAG,CACvC,QAAsB,EAItB,EAAE;IACF,MAAM,KAAK,GAAG,QAAQ,CAAC,MAAM,CAAC,UAAU,CAAA;IACxC,MAAM,GAAG,GAAG,QAAQ,CAAC,MAAM,CAAC,QAAQ,CAAA;IAEpC,IACE,KAAK,CAAC,UAAU,KAAK,SAAS;QAC9B,GAAG,CAAC,SAAS,KAAK,SAAS;QAC3B,GAAG,CAAC,IAAI,KAAK,SAAS,EACtB,CAAC;QACD,MAAM,IAAI,KAAK,CACb,8CAA8C,QAAQ,CAAC,YAAY,EAAE,CACtE,CAAA;IACH,CAAC;IAED,oEAAoE;IACpE,uEAAuE;IACvE,mEAAmE;IACnE,sEAAsE;IACtE,6BAA6B;IAC7B,OAAO;QACL,KAAK,EAAE;YACL,IAAI,EAAE,KAAK,CAAC,IAAI;YAChB,MAAM,EAAE,KAAK,CAAC,kBAAkB,GAAG,CAAC;SACrC;QACD,GAAG,EAAE,eAAe,CAAC,GAAG,CAAC,IAAI,EAAE,GAAG,CAAC,IAAI,EAAE,GAAG,CAAC,kBAAkB,GAAG,CAAC,CAAC;KACrE,CAAA;AACH,CAAC,CAAA;AAED,MAAM,CAAC,MAAM,2BAA2B,GAAG,CACzC,QAAsB,EACtB,aAAqB,EACb,EAAE;IACV,MAAM,KAAK,GAAG,QAAQ,CAAC,MAAM,CAAC,UAAU,CAAA;IACxC,MAAM,GAAG,GAAG,QAAQ,CAAC,MAAM,CAAC,QAAQ,CAAA;IAEpC,IACE,KAAK,CAAC,UAAU,KAAK,SAAS;QAC9B,GAAG,CAAC,SAAS,KAAK,SAAS;QAC3B,aAAa,EACb,CAAC;QACD,OAAO,aAAa,CAAC,SAAS,CAAC,KAAK,CAAC,UAAU,EAAE,GAAG,CAAC,SAAS,GAAG,CAAC,CAAC,CAAA;IACrE,CAAC;IAED,MAAM,IAAI,KAAK,CACb,iDAAiD,QAAQ,CAAC,YAAY,EAAE,CACzE,CAAA;AACH,CAAC,CAAA"}
@@ -16,13 +16,12 @@ export declare class MutationTestingService {
16
16
  protected readonly excludeTestMethods: string[] | undefined;
17
17
  private readonly skipPatterns;
18
18
  private readonly allowedLines;
19
+ private readonly mutationGroupingEnabled;
19
20
  private apexClassContent;
20
- constructor(progress: Progress, spinner: Spinner, connection: Connection, { apexClassName, apexTestClassName, dryRun, includeMutators, excludeMutators, includeTestMethods, excludeTestMethods, skipPatterns, lines, }: ApexMutationParameter, messages: Messages<string>);
21
+ constructor(progress: Progress, spinner: Spinner, connection: Connection, { apexClassName, apexTestClassName, dryRun, includeMutators, excludeMutators, includeTestMethods, excludeTestMethods, skipPatterns, lines, mutationGrouping, }: ApexMutationParameter, messages: Messages<string>);
21
22
  process(): Promise<ApexMutationTestResult>;
23
+ private planGroups;
22
24
  calculateScore(mutationResult: ApexMutationTestResult): number;
23
- private buildMutantResult;
24
- private calculateMutationPosition;
25
- private convertAbsoluteIndexToLineColumn;
26
25
  private filterTestMethods;
27
26
  private createAdapters;
28
27
  private fetchApexClass;
@@ -36,8 +35,5 @@ export declare class MutationTestingService {
36
35
  private displayTimeEstimate;
37
36
  private buildDryRunResult;
38
37
  private executeMutationLoop;
39
- private evaluateMutation;
40
- private formatRemainingTime;
41
38
  private rollback;
42
- private extractMutationOriginalText;
43
39
  }
@@ -2,35 +2,14 @@ import { ApexClassRepository } from '../adapter/apexClassRepository.js';
2
2
  import { ApexTestRunner } from '../adapter/apexTestRunner.js';
3
3
  import { SObjectDescribeRepository } from '../adapter/sObjectDescribeRepository.js';
4
4
  import { ConfigReader } from './configReader.js';
5
+ import { decideExactOutcome, solveColoring } from './exactColoring.js';
6
+ import { GroupExecutor } from './groupExecutor.js';
5
7
  import { MutantGenerator } from './mutantGenerator.js';
8
+ import { assembleGroups, groupMutationsWithInternals, } from './mutationGrouper.js';
9
+ import { calculateMutationPosition, extractMutationOriginalText, } from './mutationLocation.js';
6
10
  import { formatDuration, timeExecution } from './timeUtils.js';
7
11
  import { TypeDiscoverer } from './typeDiscoverer.js';
8
12
  import { ApexClassTypeMatcher, SObjectTypeMatcher } from './typeMatcher.js';
9
- const errorStrategies = [
10
- {
11
- matches: msg => msg.startsWith('Deployment failed:'),
12
- classify: (msg, targetInfo) => ({
13
- status: 'CompileError',
14
- statusReason: msg,
15
- progressMessage: `Mutation result: compile error at line ${targetInfo.line}`,
16
- }),
17
- },
18
- {
19
- matches: msg => msg.includes('LIMIT_USAGE_FOR_NS'),
20
- classify: msg => ({
21
- status: 'Killed',
22
- progressMessage: `Mutation result: mutant killed (${msg})`,
23
- }),
24
- },
25
- {
26
- matches: () => true,
27
- classify: msg => ({
28
- status: 'RuntimeError',
29
- statusReason: msg,
30
- progressMessage: `Mutation result: runtime error (${msg})`,
31
- }),
32
- },
33
- ];
34
13
  export class MutationTestingService {
35
14
  progress;
36
15
  spinner;
@@ -45,8 +24,9 @@ export class MutationTestingService {
45
24
  excludeTestMethods;
46
25
  skipPatterns;
47
26
  allowedLines;
27
+ mutationGroupingEnabled;
48
28
  apexClassContent = '';
49
- constructor(progress, spinner, connection, { apexClassName, apexTestClassName, dryRun, includeMutators, excludeMutators, includeTestMethods, excludeTestMethods, skipPatterns, lines, }, messages) {
29
+ constructor(progress, spinner, connection, { apexClassName, apexTestClassName, dryRun, includeMutators, excludeMutators, includeTestMethods, excludeTestMethods, skipPatterns, lines, mutationGrouping, }, messages) {
50
30
  this.progress = progress;
51
31
  this.spinner = spinner;
52
32
  this.connection = connection;
@@ -60,24 +40,66 @@ export class MutationTestingService {
60
40
  this.excludeTestMethods = excludeTestMethods;
61
41
  this.skipPatterns = ConfigReader.compileSkipPatterns(skipPatterns);
62
42
  this.allowedLines = ConfigReader.parseLineRanges(lines);
43
+ this.mutationGroupingEnabled = mutationGrouping ?? false;
63
44
  }
64
45
  async process() {
65
46
  const { apexClassRepository, apexTestRunner } = this.createAdapters();
66
47
  const apexClass = await this.fetchApexClass(apexClassRepository);
67
- const typeRegistry = await this.discoverTypes(apexClass, apexClassRepository);
48
+ const typeAnalysis = await this.discoverTypes(apexClass, apexClassRepository);
68
49
  const deployTime = await this.verifyCompilation(apexClass, apexClassRepository);
69
50
  await this.verifyTestClassCompilation(apexClassRepository);
70
51
  const { testMethodsPerLine, testTime } = await this.runBaselineTests(apexTestRunner);
71
52
  const coveredLines = this.extractCoveredLines(testMethodsPerLine);
72
- const { mutations, mutantGenerator } = this.generateMutations(apexClass, coveredLines, typeRegistry);
73
- this.displayTimeEstimate(deployTime, testTime, mutations.length);
53
+ const { mutations, mutantGenerator, tokenStream } = this.generateMutations(apexClass, coveredLines, typeAnalysis);
74
54
  if (this.dryRun) {
55
+ this.displayTimeEstimate(deployTime, testTime, mutations.length, mutations.length);
75
56
  return this.buildDryRunResult(apexClass, mutations);
76
57
  }
77
- const result = await this.executeMutationLoop(apexClass, mutations, mutantGenerator, testMethodsPerLine, apexTestRunner, apexClassRepository);
58
+ const groups = await this.planGroups(mutations, testMethodsPerLine);
59
+ this.displayTimeEstimate(deployTime, testTime, mutations.length, groups.length);
60
+ const result = await this.executeMutationLoop(apexClass, mutations, groups, mutantGenerator, tokenStream, testMethodsPerLine, apexTestRunner, apexClassRepository);
78
61
  await this.rollback(apexClass, apexClassRepository);
79
62
  return result;
80
63
  }
64
+ async planGroups(mutations, testMethodsPerLine) {
65
+ if (!this.mutationGroupingEnabled) {
66
+ // No grouping: one mutation per group. Inlined here rather than going
67
+ // through groupMutations to avoid building the conflict graph for the
68
+ // common (default-off) case.
69
+ return mutations.map(m => ({
70
+ mutations: [m],
71
+ // extractCoveredLines guarantees the line is in the map.
72
+ testMethods: testMethodsPerLine.get(m.target.startToken.line),
73
+ }));
74
+ }
75
+ this.spinner.start(`Grouping ${mutations.length} mutations to minimize deployments`, undefined, { stdout: true });
76
+ const { groups: dsaturGroups, lowerBound, internals, } = groupMutationsWithInternals(mutations, testMethodsPerLine);
77
+ let groups = dsaturGroups;
78
+ const exact = solveColoring({
79
+ adjacency: internals.adjacency,
80
+ n: mutations.length,
81
+ lowerBound,
82
+ dsaturColors: dsaturGroups.length,
83
+ witness: internals.witness,
84
+ dsaturColoring: internals.coloring,
85
+ });
86
+ const decision = decideExactOutcome(exact, dsaturGroups.length);
87
+ const exactSuffix = decision.suffix;
88
+ if (decision.useGroups === 'exact') {
89
+ groups = assembleGroups(mutations, internals.tests, exact.coloring);
90
+ }
91
+ // Division is safe: generateMutations throws when mutations is empty,
92
+ // so planGroups is never reached with mutations.length === 0.
93
+ const savingsPct = Math.round((1 - groups.length / mutations.length) * 100);
94
+ this.spinner.stop(this.messages.getMessage('info.groupingPlan', [
95
+ String(mutations.length),
96
+ String(groups.length),
97
+ String(savingsPct),
98
+ String(lowerBound),
99
+ exactSuffix,
100
+ ]));
101
+ return groups;
102
+ }
81
103
  calculateScore(mutationResult) {
82
104
  const validMutants = mutationResult.mutants.filter(mutant => mutant.status !== 'CompileError');
83
105
  if (validMutants.length === 0) {
@@ -88,37 +110,6 @@ export class MutationTestingService {
88
110
  validMutants.length) *
89
111
  100);
90
112
  }
91
- buildMutantResult(mutation, testResult, targetInfo) {
92
- const mutationStatus = testResult.summary.outcome === 'Passed' ? 'Survived' : 'Killed';
93
- const location = this.calculateMutationPosition(mutation, this.apexClassContent);
94
- const originalText = this.extractMutationOriginalText(mutation);
95
- return {
96
- id: `${this.apexClassName}-${targetInfo.line}-${targetInfo.column}-${targetInfo.tokenIndex}-${Date.now()}`,
97
- mutatorName: mutation.mutationName,
98
- status: mutationStatus,
99
- location,
100
- replacement: mutation.replacement,
101
- original: originalText,
102
- };
103
- }
104
- calculateMutationPosition(mutation, sourceContent) {
105
- const start = mutation.target.startToken;
106
- const end = mutation.target.endToken;
107
- if (start.startIndex !== undefined && end.stopIndex !== undefined) {
108
- const startPos = this.convertAbsoluteIndexToLineColumn(sourceContent, start.startIndex);
109
- const endPos = this.convertAbsoluteIndexToLineColumn(sourceContent, end.stopIndex + 1);
110
- return { start: startPos, end: endPos };
111
- }
112
- throw new Error(`Failed to calculate position for mutation: ${mutation.mutationName}`);
113
- }
114
- convertAbsoluteIndexToLineColumn(sourceContent, absoluteIndex) {
115
- const textBeforeIndex = sourceContent.substring(0, absoluteIndex);
116
- const lines = textBeforeIndex.split('\n');
117
- return {
118
- line: lines.length,
119
- column: lines[lines.length - 1].length + 1,
120
- };
121
- }
122
113
  filterTestMethods(testMethodsPerLine) {
123
114
  const filterSet = this.includeTestMethods
124
115
  ? new Set(this.includeTestMethods)
@@ -170,9 +161,9 @@ export class MutationTestingService {
170
161
  const typeDiscoverer = new TypeDiscoverer()
171
162
  .withMatcher(apexClassMatcher)
172
163
  .withMatcher(sObjectMatcher);
173
- const typeRegistry = await typeDiscoverer.analyze(apexClass.Body);
164
+ const analysis = await typeDiscoverer.analyzeFull(apexClass.Body);
174
165
  this.spinner.stop('Done');
175
- return typeRegistry;
166
+ return analysis;
176
167
  }
177
168
  async verifyCompilation(apexClass, apexClassRepository) {
178
169
  this.spinner.start(`Verifying "${this.apexClassName}" apex class compilation`, undefined, { stdout: true });
@@ -237,11 +228,11 @@ export class MutationTestingService {
237
228
  }
238
229
  return coveredLines;
239
230
  }
240
- generateMutations(apexClass, coveredLines, typeRegistry) {
231
+ generateMutations(apexClass, coveredLines, typeAnalysis) {
241
232
  this.spinner.start(`Generating mutants for "${this.apexClassName}" ApexClass`, undefined, { stdout: true });
242
233
  const mutantGenerator = new MutantGenerator();
243
234
  const mutatorFilter = this.buildMutatorFilter();
244
- const mutations = mutantGenerator.compute(apexClass.Body, coveredLines, typeRegistry, mutatorFilter, this.skipPatterns, this.allowedLines);
235
+ const { mutations, tokenStream } = mutantGenerator.compute(apexClass.Body, coveredLines, typeAnalysis.typeRegistry, mutatorFilter, this.skipPatterns, this.allowedLines, { tree: typeAnalysis.tree, tokenStream: typeAnalysis.tokenStream });
245
236
  if (mutations.length === 0) {
246
237
  this.spinner.stop('0 mutations generated');
247
238
  throw new Error(this.messages.getMessage('error.noMutations', [
@@ -250,7 +241,7 @@ export class MutationTestingService {
250
241
  ]));
251
242
  }
252
243
  this.spinner.stop(`${mutations.length} mutations generated`);
253
- return { mutations, mutantGenerator };
244
+ return { mutations, mutantGenerator, tokenStream };
254
245
  }
255
246
  buildMutatorFilter() {
256
247
  if (this.includeMutators)
@@ -259,8 +250,8 @@ export class MutationTestingService {
259
250
  return { exclude: this.excludeMutators };
260
251
  return undefined;
261
252
  }
262
- displayTimeEstimate(deployTime, testTime, mutationCount) {
263
- const totalEstimateMs = (deployTime + testTime) * mutationCount;
253
+ displayTimeEstimate(deployTime, testTime, mutationCount, groupCount) {
254
+ const totalEstimateMs = (deployTime + testTime) * groupCount;
264
255
  this.spinner.start(this.messages.getMessage('info.timeEstimate', [
265
256
  formatDuration(totalEstimateMs),
266
257
  ]), undefined, { stdout: true });
@@ -268,6 +259,7 @@ export class MutationTestingService {
268
259
  formatDuration(deployTime),
269
260
  formatDuration(testTime),
270
261
  String(mutationCount),
262
+ String(groupCount),
271
263
  ]));
272
264
  }
273
265
  buildDryRunResult(apexClass, mutations) {
@@ -279,116 +271,49 @@ export class MutationTestingService {
279
271
  id: `${this.apexClassName}-${mutation.target.startToken.line}-${mutation.target.startToken.charPositionInLine}-${mutation.target.startToken.tokenIndex}-${Date.now()}`,
280
272
  mutatorName: mutation.mutationName,
281
273
  status: 'Pending',
282
- location: this.calculateMutationPosition(mutation, apexClass.Body),
274
+ location: calculateMutationPosition(mutation),
283
275
  replacement: mutation.replacement,
284
- original: this.extractMutationOriginalText(mutation),
276
+ original: extractMutationOriginalText(mutation, this.apexClassContent),
285
277
  })),
286
278
  };
287
279
  }
288
- async executeMutationLoop(apexClass, mutations, mutantGenerator, testMethodsPerLine, apexTestRunner, apexClassRepository) {
289
- const mutationResults = {
290
- sourceFile: this.apexClassName,
291
- sourceFileContent: apexClass.Body,
292
- testFile: this.apexTestClassName,
293
- mutants: [],
294
- };
280
+ async executeMutationLoop(apexClass, mutations, groups, mutantGenerator, tokenStream, testMethodsPerLine, apexTestRunner, apexClassRepository) {
295
281
  this.progress.start(mutations.length, { info: 'Starting mutation testing' }, {
296
282
  title: 'MUTATION TESTING PROGRESS',
297
283
  format: '%s | {bar} | {value}/{total} {info}',
298
284
  });
299
- let mutationCount = 0;
285
+ const executor = new GroupExecutor(apexClass, this.apexClassName, this.apexTestClassName, this.apexClassContent, tokenStream, testMethodsPerLine, mutantGenerator, apexTestRunner, apexClassRepository, this.progress, this.messages);
286
+ const indexByMutation = new Map(mutations.map((m, i) => [m, i]));
287
+ const orderedResults = new Array(mutations.length).fill(null);
288
+ let completed = 0;
300
289
  const loopStartTime = performance.now();
301
- for (const mutation of mutations) {
302
- const remainingText = this.formatRemainingTime(loopStartTime, mutationCount, mutations.length);
303
- this.progress.update(mutationCount, {
304
- info: `${remainingText}Deploying "${mutation.replacement}" mutation at line ${mutation.target.startToken.line}`,
305
- });
306
- const testMethods = testMethodsPerLine.get(mutation.target.startToken.line);
307
- if (testMethods) {
308
- this.progress.update(mutationCount, {
309
- info: `${remainingText}Running ${testMethods.size} tests methods for "${mutation.replacement}" mutation at line ${mutation.target.startToken.line}`,
310
- });
290
+ for (const group of groups) {
291
+ const mutantResults = await executor.evaluate(group, completed, loopStartTime, mutations.length);
292
+ for (let i = 0; i < group.mutations.length; ++i) {
293
+ const idx = indexByMutation.get(group.mutations[i]);
294
+ orderedResults[idx] = mutantResults[i];
311
295
  }
312
- const { mutantResult, progressMessage } = await this.evaluateMutation(mutation, mutantGenerator, apexClass, testMethodsPerLine, apexTestRunner, apexClassRepository);
313
- mutationResults.mutants.push(mutantResult);
314
- ++mutationCount;
315
- const updatedRemainingText = this.formatRemainingTime(loopStartTime, mutationCount, mutations.length);
316
- this.progress.update(mutationCount, {
317
- info: `${updatedRemainingText}${progressMessage}`,
318
- });
296
+ completed += group.mutations.length;
319
297
  }
320
298
  this.progress.finish({ info: 'All mutations evaluated' });
321
- return mutationResults;
322
- }
323
- async evaluateMutation(mutation, mutantGenerator, apexClass, testMethodsPerLine, apexTestRunner, apexClassRepository) {
324
- const mutatedVersion = mutantGenerator.mutate(mutation);
325
- const targetInfo = {
326
- line: mutation.target.startToken.line,
327
- column: mutation.target.startToken.charPositionInLine,
328
- tokenIndex: mutation.target.startToken.tokenIndex,
329
- text: mutation.target.text,
299
+ return {
300
+ sourceFile: this.apexClassName,
301
+ sourceFileContent: apexClass.Body,
302
+ testFile: this.apexTestClassName,
303
+ mutants: orderedResults.filter((r) => r !== null),
330
304
  };
331
- try {
332
- await apexClassRepository.update({
333
- Id: apexClass.Id,
334
- Body: mutatedVersion,
335
- });
336
- const testMethods = testMethodsPerLine.get(targetInfo.line);
337
- const testResult = await apexTestRunner.runTestMethods(this.apexTestClassName, testMethods);
338
- return {
339
- mutantResult: this.buildMutantResult(mutation, testResult, targetInfo),
340
- progressMessage: `Mutation result: ${testResult.summary.outcome === 'Passed' ? 'zombie' : 'mutant killed'}`,
341
- };
342
- }
343
- catch (error) {
344
- const errorMessage = error instanceof Error ? error.message : String(error);
345
- const location = this.calculateMutationPosition(mutation, this.apexClassContent);
346
- const originalText = this.extractMutationOriginalText(mutation);
347
- const strategy = errorStrategies.find(s => s.matches(errorMessage));
348
- const classification = strategy.classify(errorMessage, targetInfo);
349
- return {
350
- mutantResult: {
351
- id: `${this.apexClassName}-${targetInfo.line}-${targetInfo.column}-${targetInfo.tokenIndex}-${Date.now()}`,
352
- mutatorName: mutation.mutationName,
353
- status: classification.status,
354
- ...(classification.statusReason && {
355
- statusReason: classification.statusReason,
356
- }),
357
- location,
358
- replacement: mutation.replacement,
359
- original: originalText,
360
- },
361
- progressMessage: classification.progressMessage,
362
- };
363
- }
364
- }
365
- formatRemainingTime(loopStartTime, completedCount, totalCount) {
366
- if (completedCount === 0)
367
- return '';
368
- const elapsed = performance.now() - loopStartTime;
369
- const avgPerMutant = elapsed / completedCount;
370
- const remainingMs = avgPerMutant * (totalCount - completedCount);
371
- return `Remaining: ${formatDuration(remainingMs)} | `;
372
305
  }
373
306
  async rollback(apexClass, apexClassRepository) {
307
+ this.spinner.start(`Rolling back "${this.apexClassName}" ApexClass to its original state`, undefined, { stdout: true });
374
308
  try {
375
- this.spinner.start(`Rolling back "${this.apexClassName}" ApexClass to its original state`, undefined, { stdout: true });
376
309
  await apexClassRepository.update(apexClass);
377
310
  this.spinner.stop('Done');
378
311
  }
379
- catch {
380
- this.spinner.stop('Class not rolled back, please do it manually');
381
- }
382
- }
383
- extractMutationOriginalText(mutation) {
384
- const start = mutation.target.startToken;
385
- const end = mutation.target.endToken;
386
- if (start.startIndex !== undefined &&
387
- end.stopIndex !== undefined &&
388
- this.apexClassContent) {
389
- return this.apexClassContent.substring(start.startIndex, end.stopIndex + 1);
312
+ catch (error) {
313
+ this.spinner.stop(`Rollback FAILED — '${this.apexClassName}' remains in a mutated state on the target org. Redeploy the original class manually.`);
314
+ const cause = error instanceof Error ? error.message : String(error);
315
+ throw new Error(`Rollback of '${this.apexClassName}' failed. The class on the target org is still in a mutated state. Redeploy manually. Underlying cause: ${cause}`);
390
316
  }
391
- throw new Error(`Failed to extract original text for mutation: ${mutation.mutationName}`);
392
317
  }
393
318
  }
394
319
  //# sourceMappingURL=mutationTestingService.js.map