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.
- package/README.md +34 -3
- package/lib/adapter/apexClassRepository.d.ts +14 -1
- package/lib/adapter/apexClassRepository.js +93 -31
- package/lib/adapter/apexClassRepository.js.map +1 -1
- package/lib/commands/apex/mutation/test/run.d.ts +1 -0
- package/lib/commands/apex/mutation/test/run.js +4 -0
- package/lib/commands/apex/mutation/test/run.js.map +1 -1
- package/lib/mutator/experimentalSwitchMutator.d.ts +7 -0
- package/lib/mutator/experimentalSwitchMutator.js +18 -1
- package/lib/mutator/experimentalSwitchMutator.js.map +1 -1
- package/lib/mutator/mutationListener.d.ts +2 -0
- package/lib/mutator/mutationListener.js +20 -8
- package/lib/mutator/mutationListener.js.map +1 -1
- package/lib/reporter/HTMLReporter.js +68 -12
- package/lib/reporter/HTMLReporter.js.map +1 -1
- package/lib/service/configReader.js +9 -1
- package/lib/service/configReader.js.map +1 -1
- package/lib/service/exactColoring.d.ts +25 -0
- package/lib/service/exactColoring.js +143 -0
- package/lib/service/exactColoring.js.map +1 -0
- package/lib/service/groupExecutor.d.ts +29 -0
- package/lib/service/groupExecutor.js +194 -0
- package/lib/service/groupExecutor.js.map +1 -0
- package/lib/service/mutantGenerator.d.ts +14 -4
- package/lib/service/mutantGenerator.js +42 -13
- package/lib/service/mutantGenerator.js.map +1 -1
- package/lib/service/mutationGrouper.d.ts +22 -0
- package/lib/service/mutationGrouper.js +130 -0
- package/lib/service/mutationGrouper.js.map +1 -0
- package/lib/service/mutationLocation.d.ts +12 -0
- package/lib/service/mutationLocation.js +54 -0
- package/lib/service/mutationLocation.js.map +1 -0
- package/lib/service/mutationTestingService.d.ts +3 -7
- package/lib/service/mutationTestingService.js +83 -158
- package/lib/service/mutationTestingService.js.map +1 -1
- package/lib/service/timeUtils.d.ts +1 -0
- package/lib/service/timeUtils.js +8 -0
- package/lib/service/timeUtils.js.map +1 -1
- package/lib/service/typeDiscoverer.d.ts +12 -0
- package/lib/service/typeDiscoverer.js +46 -2
- package/lib/service/typeDiscoverer.js.map +1 -1
- package/lib/type/ApexMutationParameter.d.ts +1 -0
- package/messages/apex.mutation.test.run.md +13 -1
- package/npm-shrinkwrap.json +1528 -1353
- package/oclif.manifest.json +7 -1
- 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":"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"}
|
|
@@ -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
|
|
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,
|
|
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
|
|
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
|
|
164
|
+
const analysis = await typeDiscoverer.analyzeFull(apexClass.Body);
|
|
174
165
|
this.spinner.stop('Done');
|
|
175
|
-
return
|
|
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,
|
|
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) *
|
|
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:
|
|
274
|
+
location: calculateMutationPosition(mutation),
|
|
283
275
|
replacement: mutation.replacement,
|
|
284
|
-
original:
|
|
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
|
-
|
|
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
|
|
302
|
-
const
|
|
303
|
-
|
|
304
|
-
|
|
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
|
-
|
|
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
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
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('
|
|
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
|