api-tests-coverage 1.0.25 → 1.0.26
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/dist/dashboard/dist/assets/_basePickBy-DIphIltc.js +1 -0
- package/dist/dashboard/dist/assets/_baseUniq-D57u2_9m.js +1 -0
- package/dist/dashboard/dist/assets/arc-DQosMxPM.js +1 -0
- package/dist/dashboard/dist/assets/architectureDiagram-VXUJARFQ-CNbtIqHR.js +36 -0
- package/dist/dashboard/dist/assets/blockDiagram-VD42YOAC-NgdJaQvK.js +122 -0
- package/dist/dashboard/dist/assets/c4Diagram-YG6GDRKO-ChTe70Dn.js +10 -0
- package/dist/dashboard/dist/assets/channel-B3Mj1BTw.js +1 -0
- package/dist/dashboard/dist/assets/chunk-4BX2VUAB-BS3-4dfL.js +1 -0
- package/dist/dashboard/dist/assets/chunk-55IACEB6-BCczdImM.js +1 -0
- package/dist/dashboard/dist/assets/chunk-B4BG7PRW-D6Mi4ccz.js +165 -0
- package/dist/dashboard/dist/assets/chunk-DI55MBZ5-B0tOisd5.js +220 -0
- package/dist/dashboard/dist/assets/chunk-FMBD7UC4-RSShKwSG.js +15 -0
- package/dist/dashboard/dist/assets/chunk-QN33PNHL-DFyjAoyD.js +1 -0
- package/dist/dashboard/dist/assets/chunk-QZHKN3VN-ARq4habW.js +1 -0
- package/dist/dashboard/dist/assets/chunk-TZMSLE5B-DrmzpdLp.js +1 -0
- package/dist/dashboard/dist/assets/classDiagram-2ON5EDUG-cvlgQ4cC.js +1 -0
- package/dist/dashboard/dist/assets/classDiagram-v2-WZHVMYZB-cvlgQ4cC.js +1 -0
- package/dist/dashboard/dist/assets/clone-DRiM0_7k.js +1 -0
- package/dist/dashboard/dist/assets/cose-bilkent-S5V4N54A-_dXvVagP.js +1 -0
- package/dist/dashboard/dist/assets/dagre-6UL2VRFP-BfhkcdcZ.js +4 -0
- package/dist/dashboard/dist/assets/diagram-PSM6KHXK-C8bgfsC2.js +24 -0
- package/dist/dashboard/dist/assets/diagram-QEK2KX5R-SPnyk4NX.js +43 -0
- package/dist/dashboard/dist/assets/diagram-S2PKOQOG-Cv8CAseP.js +24 -0
- package/dist/dashboard/dist/assets/erDiagram-Q2GNP2WA-DHMIYnca.js +60 -0
- package/dist/dashboard/dist/assets/flowDiagram-NV44I4VS-B-9A_TD6.js +162 -0
- package/dist/dashboard/dist/assets/ganttDiagram-JELNMOA3-DDmcIEO0.js +267 -0
- package/dist/dashboard/dist/assets/gitGraphDiagram-V2S2FVAM-D4YFQ0Qf.js +65 -0
- package/dist/dashboard/dist/assets/graph-DI2MOSai.js +1 -0
- package/dist/dashboard/dist/assets/index-BQfUzgMV.js +778 -0
- package/dist/dashboard/dist/assets/index-Bpho1Ov5.css +1 -0
- package/dist/dashboard/dist/assets/infoDiagram-HS3SLOUP-BULYGXV8.js +2 -0
- package/dist/dashboard/dist/assets/journeyDiagram-XKPGCS4Q-CpGd67rs.js +139 -0
- package/dist/dashboard/dist/assets/kanban-definition-3W4ZIXB7-CXjvcKGc.js +89 -0
- package/dist/dashboard/dist/assets/layout-D5qgY_UX.js +1 -0
- package/dist/dashboard/dist/assets/mindmap-definition-VGOIOE7T-CKZrc1IF.js +68 -0
- package/dist/dashboard/dist/assets/pieDiagram-ADFJNKIX-uIOJq-u0.js +30 -0
- package/dist/dashboard/dist/assets/quadrantDiagram-AYHSOK5B-D5g_wTRC.js +7 -0
- package/dist/dashboard/dist/assets/requirementDiagram-UZGBJVZJ-BLpGY-Om.js +64 -0
- package/dist/dashboard/dist/assets/sankeyDiagram-TZEHDZUN-E0klRQfk.js +10 -0
- package/dist/dashboard/dist/assets/sequenceDiagram-WL72ISMW-Byy1IdkL.js +145 -0
- package/dist/dashboard/dist/assets/stateDiagram-FKZM4ZOC-x3lHxmNY.js +1 -0
- package/dist/dashboard/dist/assets/stateDiagram-v2-4FDKWEC3-D0gUM6SR.js +1 -0
- package/dist/dashboard/dist/assets/timeline-definition-IT6M3QCI-D490JqJU.js +61 -0
- package/dist/dashboard/dist/assets/treemap-GDKQZRPO-C5Nk6dQh.js +162 -0
- package/dist/dashboard/dist/assets/xychartDiagram-PRI3JC2R-CYsIKi3H.js +7 -0
- package/dist/dashboard/dist/index.html +2 -2
- package/dist/src/analyzeHelpers.d.ts +60 -0
- package/dist/src/analyzeHelpers.d.ts.map +1 -0
- package/dist/src/analyzeHelpers.js +670 -0
- package/dist/src/config/defaultConfig.js +1 -1
- package/dist/src/index.js +553 -615
- package/dist/src/reporting.d.ts +10 -0
- package/dist/src/reporting.d.ts.map +1 -1
- package/dist/src/reporting.js +3 -0
- package/dist/src/summary/evaluateMetrics.js +3 -2
- package/dist/src/unitAnalysis.d.ts +132 -1
- package/dist/src/unitAnalysis.d.ts.map +1 -1
- package/dist/src/unitAnalysis.js +1139 -185
- package/package.json +1 -1
package/dist/src/unitAnalysis.js
CHANGED
|
@@ -44,40 +44,76 @@ exports.parseCoverageXml = parseCoverageXml;
|
|
|
44
44
|
exports.parseGoCoverage = parseGoCoverage;
|
|
45
45
|
exports.parseStrykerReport = parseStrykerReport;
|
|
46
46
|
exports.parseSlowTests = parseSlowTests;
|
|
47
|
+
exports.calculateDynamicPassRate = calculateDynamicPassRate;
|
|
48
|
+
exports.calculateUnitScore = calculateUnitScore;
|
|
47
49
|
exports.detectTestSmells = detectTestSmells;
|
|
50
|
+
exports.calculateWeightedCoveredItems = calculateWeightedCoveredItems;
|
|
48
51
|
const fs = __importStar(require("fs"));
|
|
49
52
|
const path = __importStar(require("path"));
|
|
50
|
-
const child_process_1 = require("child_process");
|
|
51
53
|
const fast_glob_1 = __importDefault(require("fast-glob"));
|
|
52
54
|
const COVERAGE_REPORT_CANDIDATES = [
|
|
53
|
-
'target/site/jacoco/jacoco.xml',
|
|
54
55
|
'build/reports/jacoco/test/jacocoTestReport.xml',
|
|
55
|
-
'
|
|
56
|
+
'build/reports/jacoco/jacocoTestReport.xml',
|
|
57
|
+
'build/reports/jacoco/**/*.xml',
|
|
58
|
+
'target/site/jacoco/*.xml',
|
|
56
59
|
'coverage/lcov.info',
|
|
60
|
+
'coverage/coverage-summary.json',
|
|
61
|
+
'.nyc_output/**/*.json',
|
|
57
62
|
'coverage.xml',
|
|
58
63
|
'.coverage',
|
|
64
|
+
'coverage/.resultset.json',
|
|
59
65
|
'coverage.out',
|
|
66
|
+
'build/reports/kover/**/*.xml',
|
|
67
|
+
'build/reports/clover.xml',
|
|
60
68
|
];
|
|
61
69
|
const TEST_RESULT_GLOBS = [
|
|
62
|
-
'target/surefire-reports/*.xml',
|
|
63
70
|
'build/test-results/**/*.xml',
|
|
64
|
-
'reports
|
|
71
|
+
'target/surefire-reports/**/*.xml',
|
|
72
|
+
'build/reports/tests/**/*.xml',
|
|
73
|
+
'test-results/**/*.xml',
|
|
65
74
|
'junit*.xml',
|
|
66
|
-
'results
|
|
67
|
-
'jest-results.json',
|
|
68
|
-
'reports/jest-results.json',
|
|
75
|
+
'.test-results/**/*.xml',
|
|
69
76
|
];
|
|
70
|
-
const
|
|
71
|
-
'target/pit-reports/**/mutations.xml',
|
|
77
|
+
const MUTATION_REPORT_GLOBS = [
|
|
72
78
|
'build/reports/pitest/**/mutations.xml',
|
|
79
|
+
'target/pit-reports/**/mutations.xml',
|
|
80
|
+
'build/reports/pitest/**/*.xml',
|
|
81
|
+
'reports/mutation/**/*.json',
|
|
82
|
+
'reports/stryker/**/*.json',
|
|
83
|
+
'stryker-tmp/**/*.json',
|
|
84
|
+
'.mutmut-cache',
|
|
85
|
+
'reports/infection.json',
|
|
73
86
|
];
|
|
74
|
-
const
|
|
75
|
-
'
|
|
76
|
-
'
|
|
77
|
-
'
|
|
87
|
+
const CONTRACT_REPORT_GLOBS = [
|
|
88
|
+
'target/pacts/**/*.json',
|
|
89
|
+
'pacts/**/*.json',
|
|
90
|
+
'build/pacts/**/*.json',
|
|
91
|
+
'.pact/pacts/**/*.json',
|
|
78
92
|
];
|
|
93
|
+
const AST_ASSERTION_FULL_SCORE = 20;
|
|
94
|
+
const AST_ASSERTION_PARTIAL_SCORE = 10;
|
|
95
|
+
const AST_ASSERTION_PARTIAL_THRESHOLD = 0.1;
|
|
96
|
+
const AST_ISOLATION_FULL_SCORE = 20;
|
|
97
|
+
const AST_ISOLATION_PARTIAL_SCORE = 10;
|
|
98
|
+
const AST_ISOLATION_FULL_THRESHOLD = 80;
|
|
99
|
+
const AST_ISOLATION_PARTIAL_THRESHOLD = 50;
|
|
100
|
+
const AST_NAMING_FULL_SCORE = 20;
|
|
101
|
+
const AST_NAMING_PARTIAL_SCORE = 10;
|
|
102
|
+
const AST_NAMING_FULL_THRESHOLD = 80;
|
|
103
|
+
const AST_NAMING_PARTIAL_THRESHOLD = 50;
|
|
104
|
+
const AST_DISABLED_FULL_SCORE = 20;
|
|
105
|
+
const AST_DISABLED_PARTIAL_SCORE = 10;
|
|
106
|
+
const AST_DISABLED_PARTIAL_THRESHOLD = 2;
|
|
107
|
+
const AST_CRITICAL_SMELL_BASE_SCORE = 20;
|
|
108
|
+
const AST_CRITICAL_SMELL_DEDUCTION = 5;
|
|
109
|
+
const SCORE_WEIGHTS = {
|
|
110
|
+
astOnly: { ast: 1 },
|
|
111
|
+
astMutation: { ast: 0.6, mutation: 0.4 },
|
|
112
|
+
astRuntime: { ast: 0.4, line: 0.4, branch: 0.2 },
|
|
113
|
+
astRuntimeMutation: { ast: 0.3, line: 0.3, branch: 0.2, mutation: 0.2 },
|
|
114
|
+
};
|
|
79
115
|
async function analyzeUnitTests(options) {
|
|
80
|
-
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o;
|
|
116
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y, _z, _0, _1, _2, _3, _4, _5, _6, _7, _8;
|
|
81
117
|
const rootDir = path.resolve(options.rootDir);
|
|
82
118
|
const unitConfig = options.config;
|
|
83
119
|
const testAnalyses = options.testFiles.map((filePath) => analyzeTestFile(rootDir, filePath));
|
|
@@ -87,7 +123,8 @@ async function analyzeUnitTests(options) {
|
|
|
87
123
|
const smells = ((_a = unitConfig.smellDetection) === null || _a === void 0 ? void 0 : _a.enabled) !== false
|
|
88
124
|
? detectTestSmells(rootDir, testAnalyses)
|
|
89
125
|
: [];
|
|
90
|
-
const
|
|
126
|
+
const ast = buildAstSummary(testAnalyses, smells);
|
|
127
|
+
const dynamicDiscovery = discoverDynamicResults(rootDir, unitConfig);
|
|
91
128
|
const independence = ((_b = unitConfig.independenceCheck) === null || _b === void 0 ? void 0 : _b.enabled) !== false
|
|
92
129
|
? computeIndependence(rootDir, testAnalyses)
|
|
93
130
|
: null;
|
|
@@ -95,14 +132,23 @@ async function analyzeUnitTests(options) {
|
|
|
95
132
|
? discoverCoverage(rootDir)
|
|
96
133
|
: null;
|
|
97
134
|
const mutationTesting = ((_d = unitConfig.mutationTesting) === null || _d === void 0 ? void 0 : _d.enabled)
|
|
98
|
-
?
|
|
99
|
-
: null;
|
|
135
|
+
? discoverMutationTesting(rootDir)
|
|
136
|
+
: { summary: null };
|
|
137
|
+
const contractTesting = discoverContractTesting(rootDir);
|
|
100
138
|
const failures = [];
|
|
101
139
|
const warnings = [];
|
|
102
140
|
const messages = [];
|
|
141
|
+
const consoleLines = buildUnitAnalysisConsoleLines({
|
|
142
|
+
ast,
|
|
143
|
+
dynamic: dynamicDiscovery.summary,
|
|
144
|
+
coverage: (_e = coverageDiscovery === null || coverageDiscovery === void 0 ? void 0 : coverageDiscovery.summary) !== null && _e !== void 0 ? _e : null,
|
|
145
|
+
mutation: mutationTesting.summary,
|
|
146
|
+
contract: contractTesting.summary,
|
|
147
|
+
languages: (_f = options.languages) !== null && _f !== void 0 ? _f : [],
|
|
148
|
+
});
|
|
103
149
|
if (coverageDiscovery) {
|
|
104
150
|
if (coverageDiscovery.summary) {
|
|
105
|
-
const coverageThresholds = (
|
|
151
|
+
const coverageThresholds = (_g = unitConfig.codeCoverage) === null || _g === void 0 ? void 0 : _g.thresholds;
|
|
106
152
|
if ((coverageThresholds === null || coverageThresholds === void 0 ? void 0 : coverageThresholds.line) !== undefined && coverageDiscovery.summary.lineCoverage !== null && coverageDiscovery.summary.lineCoverage < coverageThresholds.line) {
|
|
107
153
|
failures.push(`Unit line coverage ${coverageDiscovery.summary.lineCoverage.toFixed(1)}% is below threshold ${coverageThresholds.line}%`);
|
|
108
154
|
}
|
|
@@ -118,51 +164,102 @@ async function analyzeUnitTests(options) {
|
|
|
118
164
|
messages.push(coverageDiscovery.missing.hint);
|
|
119
165
|
}
|
|
120
166
|
}
|
|
121
|
-
if (mutationTesting
|
|
122
|
-
const threshold = (
|
|
167
|
+
if (mutationTesting.summary) {
|
|
168
|
+
const threshold = (_j = (_h = unitConfig.mutationTesting) === null || _h === void 0 ? void 0 : _h.threshold) !== null && _j !== void 0 ? _j : 70;
|
|
123
169
|
if (mutationTesting.summary.mutationScore < threshold) {
|
|
124
170
|
failures.push(`Mutation score ${mutationTesting.summary.mutationScore.toFixed(1)}% is below threshold ${threshold}%`);
|
|
125
171
|
}
|
|
126
172
|
}
|
|
127
|
-
if (mutationTesting
|
|
173
|
+
if (mutationTesting.warning) {
|
|
128
174
|
warnings.push(mutationTesting.warning);
|
|
129
175
|
}
|
|
130
|
-
const smellFailOn = (
|
|
176
|
+
const smellFailOn = (_l = (_k = unitConfig.smellDetection) === null || _k === void 0 ? void 0 : _k.failOn) !== null && _l !== void 0 ? _l : 'CRITICAL';
|
|
131
177
|
if (smellFailOn !== 'none') {
|
|
132
178
|
const hasBlockingSmell = smells.some((smell) => severityAtOrAbove(smell.severity, smellFailOn));
|
|
133
179
|
if (hasBlockingSmell) {
|
|
134
180
|
failures.push(`Detected test smells at or above ${smellFailOn} severity`);
|
|
135
181
|
}
|
|
136
182
|
}
|
|
137
|
-
if (independence && ((
|
|
183
|
+
if (independence && ((_m = unitConfig.independenceCheck) === null || _m === void 0 ? void 0 : _m.minScore) !== undefined && independence.score < unitConfig.independenceCheck.minScore) {
|
|
138
184
|
failures.push(`Test independence score ${independence.score} is below minimum ${unitConfig.independenceCheck.minScore}`);
|
|
139
185
|
}
|
|
186
|
+
const layersApplied = ['ast'];
|
|
187
|
+
if (dynamicDiscovery.summary)
|
|
188
|
+
layersApplied.push('dynamic');
|
|
189
|
+
if (coverageDiscovery === null || coverageDiscovery === void 0 ? void 0 : coverageDiscovery.summary)
|
|
190
|
+
layersApplied.push('coverage');
|
|
191
|
+
if (mutationTesting.summary)
|
|
192
|
+
layersApplied.push('mutation');
|
|
193
|
+
if (contractTesting.summary)
|
|
194
|
+
layersApplied.push('contract');
|
|
195
|
+
const scoreInput = {
|
|
196
|
+
astQuality: ast.qualityScore,
|
|
197
|
+
dynamicPassRate: dynamicDiscovery.summary ? calculateDynamicPassRate(dynamicDiscovery.summary) : null,
|
|
198
|
+
lineCoverage: (_p = (_o = coverageDiscovery === null || coverageDiscovery === void 0 ? void 0 : coverageDiscovery.summary) === null || _o === void 0 ? void 0 : _o.lineCoverage) !== null && _p !== void 0 ? _p : null,
|
|
199
|
+
branchCoverage: (_r = (_q = coverageDiscovery === null || coverageDiscovery === void 0 ? void 0 : coverageDiscovery.summary) === null || _q === void 0 ? void 0 : _q.branchCoverage) !== null && _r !== void 0 ? _r : null,
|
|
200
|
+
mutationScore: (_t = (_s = mutationTesting.summary) === null || _s === void 0 ? void 0 : _s.mutationScore) !== null && _t !== void 0 ? _t : null,
|
|
201
|
+
layersApplied,
|
|
202
|
+
};
|
|
203
|
+
const weightedScore = calculateUnitScore(scoreInput);
|
|
204
|
+
const scoreWeighting = describeAppliedWeighting(scoreInput);
|
|
205
|
+
const appliedScoreWeights = getAppliedUnitScoreWeights(scoreInput);
|
|
206
|
+
consoleLines.push('');
|
|
207
|
+
consoleLines.push(` Final score: ${weightedScore}% (${scoreWeighting})`);
|
|
208
|
+
appendDetectedSmells(consoleLines, ast);
|
|
209
|
+
appendLowCoverageFiles(consoleLines, (_u = coverageDiscovery === null || coverageDiscovery === void 0 ? void 0 : coverageDiscovery.summary) !== null && _u !== void 0 ? _u : null);
|
|
210
|
+
const totalItems = ast.testCount;
|
|
211
|
+
const qualityPassingTests = countPassingQualityTests(testAnalyses);
|
|
212
|
+
const coveredItems = calculateWeightedCoveredItems(totalItems, weightedScore);
|
|
140
213
|
const details = {
|
|
141
214
|
items: unitItems,
|
|
142
|
-
|
|
215
|
+
layersApplied,
|
|
216
|
+
ast,
|
|
217
|
+
dynamic: dynamicDiscovery.summary,
|
|
218
|
+
coverage: (_v = coverageDiscovery === null || coverageDiscovery === void 0 ? void 0 : coverageDiscovery.summary) !== null && _v !== void 0 ? _v : null,
|
|
219
|
+
mutation: mutationTesting.summary,
|
|
220
|
+
contract: contractTesting.summary,
|
|
221
|
+
codeCoverage: (_w = coverageDiscovery === null || coverageDiscovery === void 0 ? void 0 : coverageDiscovery.summary) !== null && _w !== void 0 ? _w : null,
|
|
143
222
|
codeCoverageMissing: coverageDiscovery === null || coverageDiscovery === void 0 ? void 0 : coverageDiscovery.missing,
|
|
144
|
-
mutationTesting:
|
|
223
|
+
mutationTesting: mutationTesting.summary,
|
|
224
|
+
filesScanned: ast.filesScanned,
|
|
225
|
+
smells,
|
|
226
|
+
enrichedByRuntime: Boolean(appliedScoreWeights.line || appliedScoreWeights.branch),
|
|
227
|
+
enrichedByMutation: Boolean(appliedScoreWeights.mutation),
|
|
228
|
+
lineCoverage: (_x = coverageDiscovery === null || coverageDiscovery === void 0 ? void 0 : coverageDiscovery.summary) === null || _x === void 0 ? void 0 : _x.lineCoverage,
|
|
229
|
+
branchCoverage: (_y = coverageDiscovery === null || coverageDiscovery === void 0 ? void 0 : coverageDiscovery.summary) === null || _y === void 0 ? void 0 : _y.branchCoverage,
|
|
230
|
+
classCoverage: (_z = coverageDiscovery === null || coverageDiscovery === void 0 ? void 0 : coverageDiscovery.summary) === null || _z === void 0 ? void 0 : _z.classCoverage,
|
|
231
|
+
methodCoverage: (_0 = coverageDiscovery === null || coverageDiscovery === void 0 ? void 0 : coverageDiscovery.summary) === null || _0 === void 0 ? void 0 : _0.methodCoverage,
|
|
232
|
+
uncoveredFiles: (_1 = coverageDiscovery === null || coverageDiscovery === void 0 ? void 0 : coverageDiscovery.summary) === null || _1 === void 0 ? void 0 : _1.uncoveredFiles,
|
|
233
|
+
lowCoverageFiles: (_2 = coverageDiscovery === null || coverageDiscovery === void 0 ? void 0 : coverageDiscovery.summary) === null || _2 === void 0 ? void 0 : _2.lowCoverageFiles,
|
|
234
|
+
runtimeReportPath: (_3 = coverageDiscovery === null || coverageDiscovery === void 0 ? void 0 : coverageDiscovery.summary) === null || _3 === void 0 ? void 0 : _3.reportPath,
|
|
235
|
+
mutationScore: (_4 = mutationTesting.summary) === null || _4 === void 0 ? void 0 : _4.mutationScore,
|
|
236
|
+
mutationSurvivedCount: (_5 = mutationTesting.summary) === null || _5 === void 0 ? void 0 : _5.survivedMutants,
|
|
237
|
+
mutationKilledCount: (_6 = mutationTesting.summary) === null || _6 === void 0 ? void 0 : _6.killedMutants,
|
|
238
|
+
worstMutationFiles: (_7 = mutationTesting.summary) === null || _7 === void 0 ? void 0 : _7.worstMutationFiles,
|
|
239
|
+
mutationReportPath: (_8 = mutationTesting.summary) === null || _8 === void 0 ? void 0 : _8.reportPath,
|
|
145
240
|
testSmells: {
|
|
146
241
|
bySeverity: summarizeSmells(smells),
|
|
147
242
|
items: smells,
|
|
148
243
|
},
|
|
149
|
-
slowTests,
|
|
244
|
+
slowTests: dynamicDiscovery.summary,
|
|
150
245
|
independence,
|
|
151
246
|
assertionDensity,
|
|
152
247
|
messages,
|
|
248
|
+
weightedScore,
|
|
249
|
+
scoreWeighting,
|
|
250
|
+
qualityPassingTests,
|
|
153
251
|
};
|
|
154
252
|
return {
|
|
155
253
|
result: {
|
|
156
254
|
type: 'unit',
|
|
157
|
-
totalItems
|
|
158
|
-
coveredItems
|
|
159
|
-
coveragePercent:
|
|
160
|
-
? Number(((unitItems.filter((item) => item.covered).length / unitItems.length) * 100).toFixed(1))
|
|
161
|
-
: 0,
|
|
255
|
+
totalItems,
|
|
256
|
+
coveredItems,
|
|
257
|
+
coveragePercent: clampScore(weightedScore),
|
|
162
258
|
details,
|
|
163
259
|
},
|
|
164
260
|
failures,
|
|
165
261
|
warnings,
|
|
262
|
+
consoleLines,
|
|
166
263
|
};
|
|
167
264
|
}
|
|
168
265
|
function analyzeTestFile(rootDir, filePath) {
|
|
@@ -170,14 +267,35 @@ function analyzeTestFile(rootDir, filePath) {
|
|
|
170
267
|
const language = detectTestLanguage(filePath);
|
|
171
268
|
const blocks = extractTestBlocks(language, content);
|
|
172
269
|
const assertionCount = countAssertions(language, content);
|
|
270
|
+
const isolationUsage = blocks.filter((block) => testUsesIsolation(language, block.body)).length;
|
|
173
271
|
return {
|
|
174
272
|
filePath,
|
|
175
273
|
relativePath: normalizePath(path.relative(rootDir, filePath)),
|
|
176
274
|
language,
|
|
177
275
|
content,
|
|
276
|
+
testBlocks: blocks,
|
|
277
|
+
testCount: blocks.length,
|
|
178
278
|
qualityScore: scoreTestQuality(content),
|
|
179
279
|
parameterizedExpansions: countParameterizedExpansions(content),
|
|
180
280
|
assertionCount,
|
|
281
|
+
testClassCount: countTestClasses(language, content),
|
|
282
|
+
mockCount: countMockSignals(content),
|
|
283
|
+
stubCount: countStubSignals(content),
|
|
284
|
+
spyCount: countSpySignals(content),
|
|
285
|
+
testsUsingIsolation: isolationUsage,
|
|
286
|
+
descriptiveTestNameCount: blocks.filter((block) => isDescriptiveTestName(block.name)).length,
|
|
287
|
+
genericNameExamples: blocks
|
|
288
|
+
.map((block) => block.name)
|
|
289
|
+
.filter((name) => isGenericTestName(name))
|
|
290
|
+
.slice(0, 5),
|
|
291
|
+
emptyTestBodies: blocks
|
|
292
|
+
.filter((block) => isEmptyTestBody(block.body))
|
|
293
|
+
.map((block) => `${normalizePath(path.relative(rootDir, filePath))}#${block.name}`),
|
|
294
|
+
threadSleepUsage: collectLineMatches(rootDir, filePath, content, /Thread\.sleep\(|time\.sleep\(|setTimeout\(|\bSleep\(/),
|
|
295
|
+
systemOutUsage: collectLineMatches(rootDir, filePath, content, /System\.out\.println\(|console\.log\(|print\(/),
|
|
296
|
+
hardcodedCredentials: collectLineMatches(rootDir, filePath, content, /\b(?:password|secret|token|api[_-]?key)\b\s*[:=]\s*['"][^'"]+['"]/i),
|
|
297
|
+
staticMutableStateModified: hasStaticMutableStateMutation(language, content),
|
|
298
|
+
sharedMutableFields: collectSharedMutableFields(language, content),
|
|
181
299
|
mockLibrary: detectMockLibrary(content),
|
|
182
300
|
layer: classifyLayer(content, filePath),
|
|
183
301
|
coveredMethods: extractCoveredMethods(language, content),
|
|
@@ -271,34 +389,155 @@ function buildUnitCoverageItems(sourceMethods, testAnalyses) {
|
|
|
271
389
|
}
|
|
272
390
|
return Array.from(byMethod.values()).sort((a, b) => a.id.localeCompare(b.id));
|
|
273
391
|
}
|
|
392
|
+
function buildAstSummary(testAnalyses, smells) {
|
|
393
|
+
const totalTests = testAnalyses.reduce((sum, test) => sum + test.testCount, 0);
|
|
394
|
+
const parameterizedTestCount = testAnalyses.reduce((sum, test) => sum + countParameterizedTests(test.language, test.content), 0);
|
|
395
|
+
const testClassCount = testAnalyses.reduce((sum, test) => sum + test.testClassCount, 0);
|
|
396
|
+
const mockCount = testAnalyses.reduce((sum, test) => sum + test.mockCount, 0);
|
|
397
|
+
const stubCount = testAnalyses.reduce((sum, test) => sum + test.stubCount, 0);
|
|
398
|
+
const spyCount = testAnalyses.reduce((sum, test) => sum + test.spyCount, 0);
|
|
399
|
+
const isolatedTests = testAnalyses.reduce((sum, test) => sum + test.testsUsingIsolation, 0);
|
|
400
|
+
const assertionCount = testAnalyses.reduce((sum, test) => sum + test.assertionCount, 0);
|
|
401
|
+
const testsWithZeroAssertions = testAnalyses.flatMap((test) => test.testBlocks
|
|
402
|
+
.filter((block) => countAssertions(test.language, block.body) === 0 && !hasVerificationCall(test.language, block.body))
|
|
403
|
+
.map((block) => `${test.relativePath}#${block.name}`));
|
|
404
|
+
const testsWithTooManyAssertions = testAnalyses.flatMap((test) => test.testBlocks
|
|
405
|
+
.filter((block) => countAssertions(test.language, block.body) > 10)
|
|
406
|
+
.map((block) => `${test.relativePath}#${block.name}`));
|
|
407
|
+
const descriptiveNameCount = testAnalyses.reduce((sum, test) => sum + test.descriptiveTestNameCount, 0);
|
|
408
|
+
const genericNameExamples = dedupeStrings(testAnalyses.flatMap((test) => test.genericNameExamples)).slice(0, 5);
|
|
409
|
+
const disabledTests = testAnalyses.flatMap((test) => test.testBlocks
|
|
410
|
+
.filter((block) => isDisabledBlock(test.language, block))
|
|
411
|
+
.map((block) => `${test.relativePath}#${block.name}`));
|
|
412
|
+
const emptyTestBodies = testAnalyses.flatMap((test) => test.emptyTestBodies);
|
|
413
|
+
const threadSleepUsage = testAnalyses.flatMap((test) => test.threadSleepUsage);
|
|
414
|
+
const systemOutUsage = testAnalyses.flatMap((test) => test.systemOutUsage);
|
|
415
|
+
const hardcodedCredentials = testAnalyses.flatMap((test) => test.hardcodedCredentials);
|
|
416
|
+
const sharedMutableFields = dedupeStrings(testAnalyses.flatMap((test) => test.sharedMutableFields));
|
|
417
|
+
const staticMutableStateModified = testAnalyses.some((test) => test.staticMutableStateModified);
|
|
418
|
+
// Scoring mirrors the cascaded unit-analysis specification:
|
|
419
|
+
// 20 points when every test asserts, 10 points when fewer than 10% miss assertions,
|
|
420
|
+
// otherwise 0 so AST-only mode still surfaces weak test suites clearly.
|
|
421
|
+
const assertionCompleteness = testsWithZeroAssertions.length === 0
|
|
422
|
+
? AST_ASSERTION_FULL_SCORE
|
|
423
|
+
: totalTests > 0 && (testsWithZeroAssertions.length / totalTests) < AST_ASSERTION_PARTIAL_THRESHOLD
|
|
424
|
+
? AST_ASSERTION_PARTIAL_SCORE
|
|
425
|
+
: 0;
|
|
426
|
+
const isolationPercent = totalTests > 0 ? Number(((isolatedTests / totalTests) * 100).toFixed(1)) : 0;
|
|
427
|
+
const testIsolation = isolationPercent >= AST_ISOLATION_FULL_THRESHOLD
|
|
428
|
+
? AST_ISOLATION_FULL_SCORE
|
|
429
|
+
: isolationPercent >= AST_ISOLATION_PARTIAL_THRESHOLD
|
|
430
|
+
? AST_ISOLATION_PARTIAL_SCORE
|
|
431
|
+
: 0;
|
|
432
|
+
const descriptiveNamePercent = totalTests > 0 ? Number(((descriptiveNameCount / totalTests) * 100).toFixed(1)) : 0;
|
|
433
|
+
const namingQuality = descriptiveNamePercent >= AST_NAMING_FULL_THRESHOLD
|
|
434
|
+
? AST_NAMING_FULL_SCORE
|
|
435
|
+
: descriptiveNamePercent >= AST_NAMING_PARTIAL_THRESHOLD
|
|
436
|
+
? AST_NAMING_PARTIAL_SCORE
|
|
437
|
+
: 0;
|
|
438
|
+
const disabledTestsScore = disabledTests.length === 0
|
|
439
|
+
? AST_DISABLED_FULL_SCORE
|
|
440
|
+
: disabledTests.length <= AST_DISABLED_PARTIAL_THRESHOLD
|
|
441
|
+
? AST_DISABLED_PARTIAL_SCORE
|
|
442
|
+
: 0;
|
|
443
|
+
// Critical smells share equal deductions because each one represents a strong signal
|
|
444
|
+
// that the test is timing-dependent, empty, or leaking credentials.
|
|
445
|
+
const criticalSmells = Math.max(0, AST_CRITICAL_SMELL_BASE_SCORE - ((threadSleepUsage.length + emptyTestBodies.length + hardcodedCredentials.length) * AST_CRITICAL_SMELL_DEDUCTION));
|
|
446
|
+
const qualityScore = clampScore(assertionCompleteness + testIsolation + namingQuality + disabledTestsScore + criticalSmells);
|
|
447
|
+
return {
|
|
448
|
+
filesScanned: testAnalyses.length,
|
|
449
|
+
testCount: totalTests,
|
|
450
|
+
parameterizedTestCount,
|
|
451
|
+
testClassCount,
|
|
452
|
+
mockCount,
|
|
453
|
+
stubCount,
|
|
454
|
+
spyCount,
|
|
455
|
+
isolationPercent,
|
|
456
|
+
assertionCount,
|
|
457
|
+
assertionsPerTest: totalTests > 0 ? Number((assertionCount / totalTests).toFixed(1)) : 0,
|
|
458
|
+
testsWithZeroAssertions,
|
|
459
|
+
testsWithTooManyAssertions,
|
|
460
|
+
descriptiveNamePercent,
|
|
461
|
+
genericNameExamples,
|
|
462
|
+
qualityScore,
|
|
463
|
+
dimensionScores: {
|
|
464
|
+
assertionCompleteness,
|
|
465
|
+
testIsolation,
|
|
466
|
+
namingQuality,
|
|
467
|
+
disabledTests: disabledTestsScore,
|
|
468
|
+
criticalSmells,
|
|
469
|
+
},
|
|
470
|
+
smells,
|
|
471
|
+
disabledTests,
|
|
472
|
+
emptyTestBodies,
|
|
473
|
+
threadSleepUsage,
|
|
474
|
+
systemOutUsage,
|
|
475
|
+
hardcodedCredentials,
|
|
476
|
+
staticMutableStateModified,
|
|
477
|
+
sharedMutableFields,
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
function discoverFirstMatchingPaths(rootDir, patterns) {
|
|
481
|
+
for (const pattern of patterns) {
|
|
482
|
+
const matchedPaths = fast_glob_1.default.sync(pattern, {
|
|
483
|
+
cwd: rootDir,
|
|
484
|
+
absolute: true,
|
|
485
|
+
onlyFiles: true,
|
|
486
|
+
unique: true,
|
|
487
|
+
}).sort((a, b) => a.length - b.length || a.localeCompare(b));
|
|
488
|
+
if (matchedPaths.length > 0) {
|
|
489
|
+
return {
|
|
490
|
+
summary: null,
|
|
491
|
+
matchedPaths,
|
|
492
|
+
checkedPaths: patterns.map((candidate) => normalizePath(candidate)),
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
return {
|
|
497
|
+
summary: null,
|
|
498
|
+
matchedPaths: [],
|
|
499
|
+
checkedPaths: patterns.map((candidate) => normalizePath(candidate)),
|
|
500
|
+
};
|
|
501
|
+
}
|
|
274
502
|
function discoverCoverage(rootDir) {
|
|
275
|
-
const
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
503
|
+
const discovery = discoverFirstMatchingPaths(rootDir, COVERAGE_REPORT_CANDIDATES);
|
|
504
|
+
const [reportPath] = discovery.matchedPaths;
|
|
505
|
+
if (reportPath) {
|
|
506
|
+
const relativePath = normalizePath(path.relative(rootDir, reportPath));
|
|
507
|
+
if (/jacoco|kover/i.test(relativePath)) {
|
|
508
|
+
return { summary: { ...parseJacocoXml(reportPath), reportPath: relativePath } };
|
|
509
|
+
}
|
|
510
|
+
if (relativePath.endsWith('coverage-summary.json')) {
|
|
511
|
+
return { summary: { ...parseIstanbulSummary(reportPath), reportPath: relativePath } };
|
|
512
|
+
}
|
|
513
|
+
if (relativePath.endsWith('lcov.info')) {
|
|
514
|
+
return { summary: { ...parseLcovInfo(reportPath), reportPath: relativePath } };
|
|
515
|
+
}
|
|
516
|
+
if (relativePath.includes('.nyc_output/') && relativePath.endsWith('.json')) {
|
|
517
|
+
return { summary: { ...parseNycCoverageJson(reportPath), reportPath: relativePath } };
|
|
282
518
|
}
|
|
283
|
-
if (
|
|
284
|
-
return { summary:
|
|
519
|
+
if (relativePath.endsWith('coverage.xml')) {
|
|
520
|
+
return { summary: { ...parseCoverageXml(reportPath), reportPath: relativePath } };
|
|
285
521
|
}
|
|
286
|
-
if (
|
|
287
|
-
return { summary:
|
|
522
|
+
if (relativePath.endsWith('.coverage')) {
|
|
523
|
+
return { summary: { ...parseCoveragePyBinary(reportPath), reportPath: relativePath } };
|
|
288
524
|
}
|
|
289
|
-
if (
|
|
290
|
-
return { summary:
|
|
525
|
+
if (relativePath.endsWith('coverage/.resultset.json')) {
|
|
526
|
+
return { summary: { ...parseSimpleCovJson(reportPath), reportPath: relativePath } };
|
|
291
527
|
}
|
|
292
|
-
if (
|
|
293
|
-
return { summary: parseGoCoverage(
|
|
528
|
+
if (relativePath.endsWith('coverage.out')) {
|
|
529
|
+
return { summary: { ...parseGoCoverage(reportPath), reportPath: relativePath } };
|
|
530
|
+
}
|
|
531
|
+
if (relativePath.endsWith('clover.xml')) {
|
|
532
|
+
return { summary: { ...parseCloverXml(reportPath), reportPath: relativePath } };
|
|
294
533
|
}
|
|
295
534
|
}
|
|
296
535
|
return {
|
|
297
536
|
summary: null,
|
|
298
537
|
missing: {
|
|
299
538
|
message: 'Run tests with coverage enabled to see report',
|
|
300
|
-
hint: '
|
|
301
|
-
checkedPaths,
|
|
539
|
+
hint: 'Write JaCoCo, Kover, LCOV, coverage.py, or Clover reports to one of the supported paths',
|
|
540
|
+
checkedPaths: discovery.checkedPaths,
|
|
302
541
|
},
|
|
303
542
|
};
|
|
304
543
|
}
|
|
@@ -307,12 +546,25 @@ function parseJacocoXml(reportPath) {
|
|
|
307
546
|
const xml = safeRead(reportPath);
|
|
308
547
|
const counters = extractCounters(xml);
|
|
309
548
|
const uncoveredMethods = [];
|
|
549
|
+
const uncoveredClasses = [];
|
|
550
|
+
const fileCoverage = [];
|
|
310
551
|
const classRegex = /<class\b([^>]*?)name="([^"]+)"([^>]*?)sourcefilename="([^"]+)"([^>]*)>([\s\S]*?)<\/class>/g;
|
|
311
552
|
let classMatch;
|
|
312
553
|
while ((classMatch = classRegex.exec(xml)) !== null) {
|
|
313
554
|
const className = (_a = classMatch[2].split('/').pop()) !== null && _a !== void 0 ? _a : classMatch[2];
|
|
314
555
|
const file = classMatch[4];
|
|
315
556
|
const classBody = classMatch[6];
|
|
557
|
+
const classBodyWithoutMethods = classBody.replace(/<method\b[\s\S]*?<\/method>/g, '');
|
|
558
|
+
const classLineCounter = /<counter\b[^>]*type="LINE"[^>]*missed="(\d+)"[^>]*covered="(\d+)"/.exec(classBodyWithoutMethods);
|
|
559
|
+
const classLineCoverage = classLineCounter
|
|
560
|
+
? safePercent(Number(classLineCounter[2]), Number(classLineCounter[1]) + Number(classLineCounter[2]))
|
|
561
|
+
: null;
|
|
562
|
+
if (classLineCoverage !== null) {
|
|
563
|
+
fileCoverage.push({ file: normalizePath(file), lineCoverage: classLineCoverage });
|
|
564
|
+
}
|
|
565
|
+
if (classLineCounter && Number(classLineCounter[2]) === 0 && Number(classLineCounter[1]) > 0) {
|
|
566
|
+
uncoveredClasses.push(className);
|
|
567
|
+
}
|
|
316
568
|
const methodRegex = /<method\b[^>]*name="([^"]+)"[^>]*line="(\d+)"[^>]*>([\s\S]*?)<\/method>/g;
|
|
317
569
|
let methodMatch;
|
|
318
570
|
while ((methodMatch = methodRegex.exec(classBody)) !== null) {
|
|
@@ -331,6 +583,9 @@ function parseJacocoXml(reportPath) {
|
|
|
331
583
|
branchCoverage: coveragePercent(counters.BRANCH),
|
|
332
584
|
methodCoverage: coveragePercent(counters.METHOD),
|
|
333
585
|
classCoverage: coveragePercent(counters.CLASS),
|
|
586
|
+
...buildCoverageFileFields(fileCoverage),
|
|
587
|
+
fileCoverage: sortCoverageFiles(fileCoverage),
|
|
588
|
+
uncoveredClasses: dedupeStrings(uncoveredClasses),
|
|
334
589
|
uncoveredMethods,
|
|
335
590
|
reportPath: normalizePath(reportPath),
|
|
336
591
|
};
|
|
@@ -339,18 +594,34 @@ function parseIstanbulSummary(reportPath) {
|
|
|
339
594
|
var _a;
|
|
340
595
|
const json = JSON.parse(safeRead(reportPath));
|
|
341
596
|
const total = ((_a = json['total']) !== null && _a !== void 0 ? _a : {});
|
|
597
|
+
const fileCoverage = Object.entries(json)
|
|
598
|
+
.filter(([key, value]) => key !== 'total' && typeof value === 'object' && value !== null)
|
|
599
|
+
.map(([key, value]) => {
|
|
600
|
+
var _a;
|
|
601
|
+
return ({
|
|
602
|
+
file: normalizePath(key),
|
|
603
|
+
lineCoverage: (_a = readPct(value, 'lines')) !== null && _a !== void 0 ? _a : 0,
|
|
604
|
+
});
|
|
605
|
+
});
|
|
606
|
+
const uncoveredClasses = Object.entries(json)
|
|
607
|
+
.filter(([key, value]) => key !== 'total' && typeof value === 'object' && value !== null)
|
|
608
|
+
.filter(([, value]) => readPct(value, 'lines') === 0)
|
|
609
|
+
.map(([key]) => path.basename(key, path.extname(key)));
|
|
342
610
|
return {
|
|
343
611
|
tool: 'istanbul',
|
|
344
612
|
lineCoverage: readPct(total, 'lines'),
|
|
345
613
|
branchCoverage: readPct(total, 'branches'),
|
|
346
614
|
methodCoverage: readPct(total, 'functions'),
|
|
347
615
|
classCoverage: readPct(total, 'statements'),
|
|
616
|
+
...buildCoverageFileFields(fileCoverage),
|
|
617
|
+
fileCoverage: sortCoverageFiles(fileCoverage),
|
|
618
|
+
uncoveredClasses: dedupeStrings(uncoveredClasses),
|
|
348
619
|
uncoveredMethods: [],
|
|
349
620
|
reportPath: normalizePath(reportPath),
|
|
350
621
|
};
|
|
351
622
|
}
|
|
352
623
|
function parseLcovInfo(reportPath) {
|
|
353
|
-
var _a, _b;
|
|
624
|
+
var _a, _b, _c;
|
|
354
625
|
const content = safeRead(reportPath);
|
|
355
626
|
const files = content.split('end_of_record').map((chunk) => chunk.trim()).filter(Boolean);
|
|
356
627
|
let totalLines = 0;
|
|
@@ -360,20 +631,25 @@ function parseLcovInfo(reportPath) {
|
|
|
360
631
|
let totalFunctions = 0;
|
|
361
632
|
let hitFunctions = 0;
|
|
362
633
|
let coveredFiles = 0;
|
|
634
|
+
const uncoveredClasses = [];
|
|
363
635
|
const uncoveredMethods = [];
|
|
636
|
+
const fileCoverage = [];
|
|
364
637
|
for (const fileChunk of files) {
|
|
365
638
|
const lines = fileChunk.split('\n');
|
|
366
639
|
const file = (_b = (_a = lines.find((line) => line.startsWith('SF:'))) === null || _a === void 0 ? void 0 : _a.slice(3)) !== null && _b !== void 0 ? _b : 'unknown';
|
|
367
640
|
const fnMap = new Map();
|
|
368
|
-
let
|
|
641
|
+
let fileTotalLines = 0;
|
|
642
|
+
let fileHitLines = 0;
|
|
369
643
|
for (const line of lines) {
|
|
370
|
-
if (line.startsWith('LF:'))
|
|
371
|
-
|
|
644
|
+
if (line.startsWith('LF:')) {
|
|
645
|
+
const count = Number(line.slice(3));
|
|
646
|
+
totalLines += count;
|
|
647
|
+
fileTotalLines += count;
|
|
648
|
+
}
|
|
372
649
|
if (line.startsWith('LH:')) {
|
|
373
650
|
const hits = Number(line.slice(3));
|
|
374
651
|
hitLines += hits;
|
|
375
|
-
|
|
376
|
-
fileCovered = true;
|
|
652
|
+
fileHitLines += hits;
|
|
377
653
|
}
|
|
378
654
|
if (line.startsWith('BRF:'))
|
|
379
655
|
totalBranches += Number(line.slice(4));
|
|
@@ -394,8 +670,13 @@ function parseLcovInfo(reportPath) {
|
|
|
394
670
|
entry.hits = Number(hits);
|
|
395
671
|
}
|
|
396
672
|
}
|
|
397
|
-
|
|
673
|
+
const fileLineCoverage = (_c = safePercent(fileHitLines, fileTotalLines)) !== null && _c !== void 0 ? _c : 0;
|
|
674
|
+
fileCoverage.push({ file: normalizePath(file), lineCoverage: fileLineCoverage });
|
|
675
|
+
if (fileLineCoverage > 0)
|
|
398
676
|
coveredFiles += 1;
|
|
677
|
+
if (fileLineCoverage === 0) {
|
|
678
|
+
uncoveredClasses.push(path.basename(file, path.extname(file)));
|
|
679
|
+
}
|
|
399
680
|
for (const [name, data] of fnMap.entries()) {
|
|
400
681
|
if (data.hits === 0) {
|
|
401
682
|
uncoveredMethods.push({
|
|
@@ -413,10 +694,71 @@ function parseLcovInfo(reportPath) {
|
|
|
413
694
|
branchCoverage: safePercent(hitBranches, totalBranches),
|
|
414
695
|
methodCoverage: safePercent(hitFunctions, totalFunctions),
|
|
415
696
|
classCoverage: files.length > 0 ? Number(((coveredFiles / files.length) * 100).toFixed(1)) : null,
|
|
697
|
+
...buildCoverageFileFields(fileCoverage),
|
|
698
|
+
fileCoverage: sortCoverageFiles(fileCoverage),
|
|
699
|
+
uncoveredClasses: dedupeStrings(uncoveredClasses),
|
|
416
700
|
uncoveredMethods,
|
|
417
701
|
reportPath: normalizePath(reportPath),
|
|
418
702
|
};
|
|
419
703
|
}
|
|
704
|
+
function parseNycCoverageJson(reportPath) {
|
|
705
|
+
const json = JSON.parse(safeRead(reportPath));
|
|
706
|
+
const fileCoverage = Object.entries(json).map(([file, value]) => {
|
|
707
|
+
var _a, _b;
|
|
708
|
+
const statements = Object.values((_a = value.s) !== null && _a !== void 0 ? _a : {});
|
|
709
|
+
const coveredStatements = statements.filter((count) => count > 0).length;
|
|
710
|
+
return {
|
|
711
|
+
file: normalizePath(file),
|
|
712
|
+
lineCoverage: (_b = safePercent(coveredStatements, statements.length)) !== null && _b !== void 0 ? _b : 0,
|
|
713
|
+
};
|
|
714
|
+
});
|
|
715
|
+
const statementCounts = Object.values(json).flatMap((value) => { var _a; return Object.values((_a = value.s) !== null && _a !== void 0 ? _a : {}); });
|
|
716
|
+
const branchCounts = Object.values(json).flatMap((value) => { var _a; return Object.values((_a = value.b) !== null && _a !== void 0 ? _a : {}).flat(); });
|
|
717
|
+
const functionCounts = Object.values(json).flatMap((value) => { var _a; return Object.values((_a = value.f) !== null && _a !== void 0 ? _a : {}); });
|
|
718
|
+
return {
|
|
719
|
+
tool: 'nyc',
|
|
720
|
+
lineCoverage: safePercent(statementCounts.filter((count) => count > 0).length, statementCounts.length),
|
|
721
|
+
branchCoverage: safePercent(branchCounts.filter((count) => count > 0).length, branchCounts.length),
|
|
722
|
+
methodCoverage: safePercent(functionCounts.filter((count) => count > 0).length, functionCounts.length),
|
|
723
|
+
classCoverage: null,
|
|
724
|
+
...buildCoverageFileFields(fileCoverage),
|
|
725
|
+
fileCoverage: sortCoverageFiles(fileCoverage),
|
|
726
|
+
uncoveredClasses: [],
|
|
727
|
+
uncoveredMethods: [],
|
|
728
|
+
reportPath: normalizePath(reportPath),
|
|
729
|
+
};
|
|
730
|
+
}
|
|
731
|
+
function parseSimpleCovJson(reportPath) {
|
|
732
|
+
var _a, _b;
|
|
733
|
+
const json = JSON.parse(safeRead(reportPath));
|
|
734
|
+
const fileCoverage = [];
|
|
735
|
+
let totalExecutable = 0;
|
|
736
|
+
let totalCovered = 0;
|
|
737
|
+
for (const run of Object.values(json)) {
|
|
738
|
+
for (const [file, hits] of Object.entries((_a = run.coverage) !== null && _a !== void 0 ? _a : {})) {
|
|
739
|
+
const executable = hits.filter((hit) => hit !== null).length;
|
|
740
|
+
const covered = hits.filter((hit) => typeof hit === 'number' && hit > 0).length;
|
|
741
|
+
totalExecutable += executable;
|
|
742
|
+
totalCovered += covered;
|
|
743
|
+
fileCoverage.push({
|
|
744
|
+
file: normalizePath(file),
|
|
745
|
+
lineCoverage: (_b = safePercent(covered, executable)) !== null && _b !== void 0 ? _b : 0,
|
|
746
|
+
});
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
return {
|
|
750
|
+
tool: 'simplecov',
|
|
751
|
+
lineCoverage: safePercent(totalCovered, totalExecutable),
|
|
752
|
+
branchCoverage: null,
|
|
753
|
+
methodCoverage: null,
|
|
754
|
+
classCoverage: null,
|
|
755
|
+
...buildCoverageFileFields(fileCoverage),
|
|
756
|
+
fileCoverage: sortCoverageFiles(fileCoverage),
|
|
757
|
+
uncoveredClasses: [],
|
|
758
|
+
uncoveredMethods: [],
|
|
759
|
+
reportPath: normalizePath(reportPath),
|
|
760
|
+
};
|
|
761
|
+
}
|
|
420
762
|
function parseCoverageXml(reportPath) {
|
|
421
763
|
var _a;
|
|
422
764
|
const xml = safeRead(reportPath);
|
|
@@ -426,6 +768,16 @@ function parseCoverageXml(reportPath) {
|
|
|
426
768
|
const branchesCovered = readNumericAttribute(xml, 'branches-covered');
|
|
427
769
|
const classes = [...xml.matchAll(/<class\b[^>]*name="([^"]+)"[^>]*filename="([^"]+)"[\s\S]*?<\/class>/g)];
|
|
428
770
|
const coveredClasses = classes.filter((match) => /line-rate="(?!0(?:\.0+)?)\d*\.?\d+"/.test(match[0])).length;
|
|
771
|
+
const uncoveredClasses = classes
|
|
772
|
+
.filter((match) => /line-rate="0(?:\.0+)?"/.test(match[0]))
|
|
773
|
+
.map((match) => { var _a; return (_a = String(match[1]).split('.').pop()) !== null && _a !== void 0 ? _a : String(match[1]); });
|
|
774
|
+
const fileCoverage = classes.map((match) => {
|
|
775
|
+
var _a, _b;
|
|
776
|
+
return ({
|
|
777
|
+
file: normalizePath(String(match[2])),
|
|
778
|
+
lineCoverage: Number((Number((_b = (_a = /line-rate="([^"]+)"/.exec(match[0])) === null || _a === void 0 ? void 0 : _a[1]) !== null && _b !== void 0 ? _b : 0) * 100).toFixed(1)),
|
|
779
|
+
});
|
|
780
|
+
});
|
|
429
781
|
const uncoveredMethods = [];
|
|
430
782
|
const methodRegex = /<method\b[^>]*name="([^"]+)"[^>]*line-rate="([^"]+)"[^>]*>([\s\S]*?)<\/method>/g;
|
|
431
783
|
let methodMatch;
|
|
@@ -448,14 +800,19 @@ function parseCoverageXml(reportPath) {
|
|
|
448
800
|
branchCoverage: safePercent(branchesCovered, branchesValid),
|
|
449
801
|
methodCoverage: safePercent(coveredMethods, totalMethods),
|
|
450
802
|
classCoverage: classes.length > 0 ? Number(((coveredClasses / classes.length) * 100).toFixed(1)) : null,
|
|
803
|
+
...buildCoverageFileFields(fileCoverage),
|
|
804
|
+
fileCoverage: sortCoverageFiles(fileCoverage),
|
|
805
|
+
uncoveredClasses: dedupeStrings(uncoveredClasses),
|
|
451
806
|
uncoveredMethods,
|
|
452
807
|
reportPath: normalizePath(reportPath),
|
|
453
808
|
};
|
|
454
809
|
}
|
|
455
810
|
function parseGoCoverage(reportPath) {
|
|
811
|
+
var _a;
|
|
456
812
|
const content = safeRead(reportPath);
|
|
457
813
|
let total = 0;
|
|
458
814
|
let covered = 0;
|
|
815
|
+
const fileStats = new Map();
|
|
459
816
|
for (const line of content.split('\n')) {
|
|
460
817
|
if (!line || line.startsWith('mode:'))
|
|
461
818
|
continue;
|
|
@@ -463,121 +820,102 @@ function parseGoCoverage(reportPath) {
|
|
|
463
820
|
if (parts.length < 3)
|
|
464
821
|
continue;
|
|
465
822
|
const count = Number(parts[2]);
|
|
466
|
-
const
|
|
823
|
+
const file = normalizePath(parts[0].split(':')[0]);
|
|
824
|
+
const range = parts[0].includes(':') ? parts[0].slice(parts[0].indexOf(':') + 1) : parts[1];
|
|
467
825
|
const lineSpan = range.split(',').map((segment) => segment.split('.'));
|
|
468
826
|
const startLine = Number(lineSpan[0][0]);
|
|
469
827
|
const endLine = Number(lineSpan[1][0]);
|
|
470
828
|
const lines = Math.max(1, endLine - startLine + 1);
|
|
471
829
|
total += lines;
|
|
472
|
-
|
|
830
|
+
const stat = (_a = fileStats.get(file)) !== null && _a !== void 0 ? _a : { total: 0, covered: 0 };
|
|
831
|
+
stat.total += lines;
|
|
832
|
+
if (count > 0) {
|
|
473
833
|
covered += lines;
|
|
834
|
+
stat.covered += lines;
|
|
835
|
+
}
|
|
836
|
+
fileStats.set(file, stat);
|
|
474
837
|
}
|
|
838
|
+
const fileCoverage = Array.from(fileStats.entries()).map(([file, stat]) => {
|
|
839
|
+
var _a;
|
|
840
|
+
return ({
|
|
841
|
+
file,
|
|
842
|
+
lineCoverage: (_a = safePercent(stat.covered, stat.total)) !== null && _a !== void 0 ? _a : 0,
|
|
843
|
+
});
|
|
844
|
+
});
|
|
475
845
|
return {
|
|
476
846
|
tool: 'go-cover',
|
|
477
847
|
lineCoverage: safePercent(covered, total),
|
|
478
848
|
branchCoverage: null,
|
|
479
849
|
methodCoverage: null,
|
|
480
850
|
classCoverage: null,
|
|
851
|
+
...buildCoverageFileFields(fileCoverage),
|
|
852
|
+
fileCoverage: sortCoverageFiles(fileCoverage),
|
|
853
|
+
uncoveredClasses: [],
|
|
481
854
|
uncoveredMethods: [],
|
|
482
855
|
reportPath: normalizePath(reportPath),
|
|
483
856
|
};
|
|
484
857
|
}
|
|
485
|
-
function
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
858
|
+
function parseCoveragePyBinary(reportPath) {
|
|
859
|
+
return {
|
|
860
|
+
tool: 'coverage.py',
|
|
861
|
+
lineCoverage: null,
|
|
862
|
+
branchCoverage: null,
|
|
863
|
+
methodCoverage: null,
|
|
864
|
+
classCoverage: null,
|
|
865
|
+
uncoveredFiles: [],
|
|
866
|
+
lowCoverageFiles: [],
|
|
867
|
+
fileCoverage: [],
|
|
868
|
+
uncoveredClasses: [],
|
|
869
|
+
uncoveredMethods: [],
|
|
870
|
+
reportPath: normalizePath(reportPath),
|
|
871
|
+
};
|
|
872
|
+
}
|
|
873
|
+
function parseCloverXml(reportPath) {
|
|
874
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m;
|
|
875
|
+
const xml = safeRead(reportPath);
|
|
876
|
+
const lineRate = safePercent(Number((_b = (_a = /coveredstatements="(\d+)"/.exec(xml)) === null || _a === void 0 ? void 0 : _a[1]) !== null && _b !== void 0 ? _b : 0), Number((_d = (_c = /statements="(\d+)"/.exec(xml)) === null || _c === void 0 ? void 0 : _c[1]) !== null && _d !== void 0 ? _d : 0));
|
|
877
|
+
const branchRate = safePercent(Number((_f = (_e = /coveredconditionals="(\d+)"/.exec(xml)) === null || _e === void 0 ? void 0 : _e[1]) !== null && _f !== void 0 ? _f : 0), Number((_h = (_g = /conditionals="(\d+)"/.exec(xml)) === null || _g === void 0 ? void 0 : _g[1]) !== null && _h !== void 0 ? _h : 0));
|
|
878
|
+
const methodRate = safePercent(Number((_k = (_j = /coveredmethods="(\d+)"/.exec(xml)) === null || _j === void 0 ? void 0 : _j[1]) !== null && _k !== void 0 ? _k : 0), Number((_m = (_l = /methods="(\d+)"/.exec(xml)) === null || _l === void 0 ? void 0 : _l[1]) !== null && _m !== void 0 ? _m : 0));
|
|
879
|
+
return {
|
|
880
|
+
tool: 'clover',
|
|
881
|
+
lineCoverage: lineRate,
|
|
882
|
+
branchCoverage: branchRate,
|
|
883
|
+
methodCoverage: methodRate,
|
|
884
|
+
classCoverage: null,
|
|
885
|
+
uncoveredFiles: [],
|
|
886
|
+
lowCoverageFiles: [],
|
|
887
|
+
fileCoverage: [],
|
|
888
|
+
uncoveredClasses: [],
|
|
889
|
+
uncoveredMethods: [],
|
|
890
|
+
reportPath: normalizePath(reportPath),
|
|
891
|
+
};
|
|
892
|
+
}
|
|
893
|
+
function discoverMutationTesting(rootDir) {
|
|
894
|
+
const discovery = discoverFirstMatchingPaths(rootDir, MUTATION_REPORT_GLOBS);
|
|
895
|
+
const [reportPath] = discovery.matchedPaths;
|
|
896
|
+
if (!reportPath) {
|
|
897
|
+
return { summary: null };
|
|
898
|
+
}
|
|
899
|
+
try {
|
|
900
|
+
const relativePath = normalizePath(path.relative(rootDir, reportPath));
|
|
901
|
+
if (reportPath.endsWith('.xml')) {
|
|
902
|
+
return { summary: { ...parsePitestXml(reportPath), reportPath: relativePath } };
|
|
903
|
+
}
|
|
904
|
+
return { summary: { ...parseStrykerReport(reportPath, /stryker/i.test(reportPath) ? 'stryker' : 'mutation'), reportPath: relativePath } };
|
|
905
|
+
}
|
|
906
|
+
catch (error) {
|
|
500
907
|
return {
|
|
501
908
|
summary: null,
|
|
502
|
-
warning:
|
|
909
|
+
warning: `Mutation report ${normalizePath(path.relative(rootDir, reportPath))} could not be parsed: ${error instanceof Error ? error.message : String(error)}`,
|
|
503
910
|
};
|
|
504
911
|
}
|
|
505
|
-
return { summary };
|
|
506
|
-
}
|
|
507
|
-
function resolveMutationTool(tool, languages, rootDir) {
|
|
508
|
-
if (tool !== 'auto')
|
|
509
|
-
return tool;
|
|
510
|
-
const lowerLanguages = languages.map((language) => language.toLowerCase());
|
|
511
|
-
if (lowerLanguages.includes('java') || lowerLanguages.includes('kotlin'))
|
|
512
|
-
return 'pitest';
|
|
513
|
-
if (lowerLanguages.includes('typescript') || lowerLanguages.includes('javascript'))
|
|
514
|
-
return 'stryker';
|
|
515
|
-
if (lowerLanguages.includes('python'))
|
|
516
|
-
return 'mutmut';
|
|
517
|
-
if (lowerLanguages.includes('go'))
|
|
518
|
-
return 'go-mutesting';
|
|
519
|
-
if (lowerLanguages.includes('csharp') || lowerLanguages.includes('dotnet'))
|
|
520
|
-
return 'dotnet-stryker';
|
|
521
|
-
if (fs.existsSync(path.join(rootDir, 'package.json')))
|
|
522
|
-
return 'stryker';
|
|
523
|
-
if (fs.existsSync(path.join(rootDir, 'pom.xml')))
|
|
524
|
-
return 'pitest';
|
|
525
|
-
if (fs.existsSync(path.join(rootDir, 'build.gradle')) || fs.existsSync(path.join(rootDir, 'build.gradle.kts')))
|
|
526
|
-
return 'pitest';
|
|
527
|
-
if (fs.existsSync(path.join(rootDir, 'pyproject.toml')) || fs.existsSync(path.join(rootDir, 'setup.py')))
|
|
528
|
-
return 'mutmut';
|
|
529
|
-
if (fs.existsSync(path.join(rootDir, 'go.mod')))
|
|
530
|
-
return 'go-mutesting';
|
|
531
|
-
if (fast_glob_1.default.sync(['*.sln', '*.csproj'], { cwd: rootDir }).length > 0)
|
|
532
|
-
return 'dotnet-stryker';
|
|
533
|
-
return null;
|
|
534
|
-
}
|
|
535
|
-
function mutationCommandFor(rootDir, tool) {
|
|
536
|
-
switch (tool) {
|
|
537
|
-
case 'pitest':
|
|
538
|
-
return fs.existsSync(path.join(rootDir, 'gradlew'))
|
|
539
|
-
? { command: path.join(rootDir, 'gradlew'), args: ['pitest'] }
|
|
540
|
-
: fs.existsSync(path.join(rootDir, 'build.gradle'))
|
|
541
|
-
? { command: 'gradle', args: ['pitest'] }
|
|
542
|
-
: { command: 'mvn', args: ['pitest:mutationCoverage'] };
|
|
543
|
-
case 'stryker':
|
|
544
|
-
return { command: 'npx', args: ['stryker', 'run'] };
|
|
545
|
-
case 'mutmut':
|
|
546
|
-
return { command: 'mutmut', args: ['run'] };
|
|
547
|
-
case 'go-mutesting':
|
|
548
|
-
return { command: 'go-mutesting', args: ['./...'] };
|
|
549
|
-
case 'dotnet-stryker':
|
|
550
|
-
return { command: 'dotnet', args: ['stryker'] };
|
|
551
|
-
default:
|
|
552
|
-
return { command: 'true', args: [] };
|
|
553
|
-
}
|
|
554
|
-
}
|
|
555
|
-
function parseMutationReport(rootDir, tool, stdout, stderr) {
|
|
556
|
-
if (tool === 'pitest') {
|
|
557
|
-
for (const reportPath of fast_glob_1.default.sync([...PIT_REPORT_GLOBS], { cwd: rootDir, absolute: true })) {
|
|
558
|
-
return parsePitestXml(reportPath);
|
|
559
|
-
}
|
|
560
|
-
}
|
|
561
|
-
if (tool === 'stryker' || tool === 'dotnet-stryker') {
|
|
562
|
-
for (const reportPath of fast_glob_1.default.sync([...STRYKER_REPORT_GLOBS], { cwd: rootDir, absolute: true })) {
|
|
563
|
-
return parseStrykerReport(reportPath, tool === 'dotnet-stryker' ? 'dotnet stryker' : 'stryker');
|
|
564
|
-
}
|
|
565
|
-
}
|
|
566
|
-
if (tool === 'mutmut') {
|
|
567
|
-
const summary = parseMutmutOutput(stdout || stderr);
|
|
568
|
-
return summary ? { ...summary, tool: 'mutmut' } : null;
|
|
569
|
-
}
|
|
570
|
-
if (tool === 'go-mutesting') {
|
|
571
|
-
const summary = parseGoMutestingOutput(stdout || stderr);
|
|
572
|
-
return summary ? { ...summary, tool: 'go-mutesting' } : null;
|
|
573
|
-
}
|
|
574
|
-
return null;
|
|
575
912
|
}
|
|
576
913
|
function parsePitestXml(reportPath) {
|
|
577
|
-
var _a, _b, _c, _d, _e, _f, _g;
|
|
914
|
+
var _a, _b, _c, _d, _e, _f, _g, _h;
|
|
578
915
|
const xml = safeRead(reportPath);
|
|
579
916
|
const mutations = [...xml.matchAll(/<mutation\b([^>]*)>([\s\S]*?)<\/mutation>/g)];
|
|
580
917
|
const survivedByFile = new Map();
|
|
918
|
+
const fileStats = new Map();
|
|
581
919
|
let total = 0;
|
|
582
920
|
let killed = 0;
|
|
583
921
|
let survived = 0;
|
|
@@ -588,16 +926,20 @@ function parsePitestXml(reportPath) {
|
|
|
588
926
|
const statusMatch = /status="([^"]+)"/.exec(attrs);
|
|
589
927
|
const status = (_a = statusMatch === null || statusMatch === void 0 ? void 0 : statusMatch[1]) !== null && _a !== void 0 ? _a : 'UNKNOWN';
|
|
590
928
|
const file = (_b = extractXmlText(body, 'sourceFile')) !== null && _b !== void 0 ? _b : 'unknown';
|
|
591
|
-
const
|
|
592
|
-
|
|
593
|
-
|
|
929
|
+
const stats = (_c = fileStats.get(file)) !== null && _c !== void 0 ? _c : { total: 0, killed: 0 };
|
|
930
|
+
stats.total += 1;
|
|
931
|
+
fileStats.set(file, stats);
|
|
932
|
+
const line = Number((_d = extractXmlText(body, 'lineNumber')) !== null && _d !== void 0 ? _d : '0');
|
|
933
|
+
const mutator = (_e = extractXmlText(body, 'mutator')) !== null && _e !== void 0 ? _e : 'mutation';
|
|
934
|
+
const description = (_f = extractXmlText(body, 'description')) !== null && _f !== void 0 ? _f : mutator;
|
|
594
935
|
const killingTest = extractXmlText(body, 'killingTest');
|
|
595
936
|
if (status === 'KILLED') {
|
|
596
937
|
killed += 1;
|
|
938
|
+
stats.killed += 1;
|
|
597
939
|
}
|
|
598
940
|
if (status === 'SURVIVED' || status === 'NO_COVERAGE' || status === 'TIMED_OUT') {
|
|
599
941
|
survived += 1;
|
|
600
|
-
const entries = (
|
|
942
|
+
const entries = (_g = survivedByFile.get(file)) !== null && _g !== void 0 ? _g : [];
|
|
601
943
|
entries.push({
|
|
602
944
|
line,
|
|
603
945
|
mutation: description,
|
|
@@ -608,38 +950,45 @@ function parsePitestXml(reportPath) {
|
|
|
608
950
|
}
|
|
609
951
|
return {
|
|
610
952
|
tool: 'pitest',
|
|
611
|
-
mutationScore: (
|
|
953
|
+
mutationScore: (_h = safePercent(killed, total)) !== null && _h !== void 0 ? _h : 0,
|
|
612
954
|
totalMutants: total,
|
|
613
955
|
killedMutants: killed,
|
|
614
956
|
survivedMutants: survived,
|
|
957
|
+
worstMutationFiles: rankWorstMutationFiles(fileStats),
|
|
958
|
+
weakestFiles: rankWeakestMutationFiles(fileStats),
|
|
615
959
|
survivedByFile: Array.from(survivedByFile.entries()).map(([file, items]) => ({ file, survived: items })),
|
|
616
960
|
reportPath: normalizePath(reportPath),
|
|
617
961
|
};
|
|
618
962
|
}
|
|
619
963
|
function parseStrykerReport(reportPath, toolName) {
|
|
620
|
-
var _a, _b, _c, _d, _e, _f, _g, _h, _j;
|
|
964
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
|
|
621
965
|
const json = JSON.parse(safeRead(reportPath));
|
|
622
966
|
const files = ((_a = json['files']) !== null && _a !== void 0 ? _a : {});
|
|
623
967
|
let total = 0;
|
|
624
968
|
let killed = 0;
|
|
625
969
|
let survived = 0;
|
|
970
|
+
const fileStats = new Map();
|
|
626
971
|
const survivedByFile = [];
|
|
627
972
|
for (const [file, fileValue] of Object.entries(files)) {
|
|
628
973
|
const fileRecord = fileValue;
|
|
629
974
|
const mutants = ((_b = fileRecord['mutants']) !== null && _b !== void 0 ? _b : []);
|
|
630
975
|
const survivors = [];
|
|
976
|
+
const stats = (_c = fileStats.get(file)) !== null && _c !== void 0 ? _c : { total: 0, killed: 0 };
|
|
631
977
|
for (const mutant of mutants) {
|
|
632
978
|
total += 1;
|
|
633
|
-
|
|
634
|
-
|
|
979
|
+
stats.total += 1;
|
|
980
|
+
const status = String((_d = mutant['status']) !== null && _d !== void 0 ? _d : 'Unknown');
|
|
981
|
+
if (/Killed/i.test(status)) {
|
|
635
982
|
killed += 1;
|
|
983
|
+
stats.killed += 1;
|
|
984
|
+
}
|
|
636
985
|
if (/Survived|NoCoverage|Timeout/i.test(status)) {
|
|
637
986
|
survived += 1;
|
|
638
|
-
const location = ((
|
|
639
|
-
const start = ((
|
|
987
|
+
const location = ((_e = mutant['location']) !== null && _e !== void 0 ? _e : {});
|
|
988
|
+
const start = ((_f = location['start']) !== null && _f !== void 0 ? _f : {});
|
|
640
989
|
survivors.push({
|
|
641
|
-
line: Number((
|
|
642
|
-
mutation: String((
|
|
990
|
+
line: Number((_g = start['line']) !== null && _g !== void 0 ? _g : 0),
|
|
991
|
+
mutation: String((_j = (_h = mutant['mutatorName']) !== null && _h !== void 0 ? _h : mutant['id']) !== null && _j !== void 0 ? _j : 'mutation'),
|
|
643
992
|
originalCode: typeof mutant['replacement'] === 'string' ? String(mutant['replacement']) : undefined,
|
|
644
993
|
mutatedCode: typeof mutant['description'] === 'string' ? String(mutant['description']) : undefined,
|
|
645
994
|
testsThatShouldHaveCaught: [],
|
|
@@ -649,13 +998,16 @@ function parseStrykerReport(reportPath, toolName) {
|
|
|
649
998
|
if (survivors.length > 0) {
|
|
650
999
|
survivedByFile.push({ file: normalizePath(file), survived: survivors });
|
|
651
1000
|
}
|
|
1001
|
+
fileStats.set(file, stats);
|
|
652
1002
|
}
|
|
653
1003
|
return {
|
|
654
1004
|
tool: toolName,
|
|
655
|
-
mutationScore: (
|
|
1005
|
+
mutationScore: (_k = safePercent(killed, total)) !== null && _k !== void 0 ? _k : 0,
|
|
656
1006
|
totalMutants: total,
|
|
657
1007
|
killedMutants: killed,
|
|
658
1008
|
survivedMutants: survived,
|
|
1009
|
+
worstMutationFiles: rankWorstMutationFiles(fileStats),
|
|
1010
|
+
weakestFiles: rankWeakestMutationFiles(fileStats),
|
|
659
1011
|
survivedByFile,
|
|
660
1012
|
reportPath: normalizePath(reportPath),
|
|
661
1013
|
};
|
|
@@ -673,6 +1025,8 @@ function parseMutmutOutput(output) {
|
|
|
673
1025
|
totalMutants: total,
|
|
674
1026
|
killedMutants: killed,
|
|
675
1027
|
survivedMutants: survived,
|
|
1028
|
+
worstMutationFiles: [],
|
|
1029
|
+
weakestFiles: [],
|
|
676
1030
|
survivedByFile: [],
|
|
677
1031
|
};
|
|
678
1032
|
}
|
|
@@ -688,25 +1042,78 @@ function parseGoMutestingOutput(output) {
|
|
|
688
1042
|
totalMutants: total,
|
|
689
1043
|
killedMutants: killed,
|
|
690
1044
|
survivedMutants: survived,
|
|
1045
|
+
worstMutationFiles: [],
|
|
1046
|
+
weakestFiles: [],
|
|
691
1047
|
survivedByFile: [],
|
|
692
1048
|
};
|
|
693
1049
|
}
|
|
1050
|
+
function discoverDynamicResults(rootDir, config) {
|
|
1051
|
+
var _a;
|
|
1052
|
+
const discovery = discoverFirstMatchingPaths(rootDir, TEST_RESULT_GLOBS);
|
|
1053
|
+
if (discovery.matchedPaths.length === 0) {
|
|
1054
|
+
return discovery;
|
|
1055
|
+
}
|
|
1056
|
+
const junitReportPaths = discovery.matchedPaths.filter((reportPath) => isLikelyJunitXml(reportPath));
|
|
1057
|
+
if (junitReportPaths.length === 0) {
|
|
1058
|
+
return {
|
|
1059
|
+
matchedPaths: [],
|
|
1060
|
+
checkedPaths: discovery.checkedPaths,
|
|
1061
|
+
summary: null,
|
|
1062
|
+
};
|
|
1063
|
+
}
|
|
1064
|
+
const tests = junitReportPaths.flatMap((reportPath) => parseJunitXmlResults(reportPath));
|
|
1065
|
+
const durations = tests.map((test) => ({
|
|
1066
|
+
file: test.file,
|
|
1067
|
+
method: test.method,
|
|
1068
|
+
durationMs: test.durationMs,
|
|
1069
|
+
}));
|
|
1070
|
+
const slowSummary = buildSlowTestSummary(durations, junitReportPaths.map((reportPath) => normalizePath(path.relative(rootDir, reportPath))), config);
|
|
1071
|
+
const failures = tests
|
|
1072
|
+
.filter((test) => test.status === 'failed' && test.failureMessage)
|
|
1073
|
+
.map((test) => {
|
|
1074
|
+
var _a;
|
|
1075
|
+
return ({
|
|
1076
|
+
file: test.file,
|
|
1077
|
+
method: test.method,
|
|
1078
|
+
message: (_a = test.failureMessage) !== null && _a !== void 0 ? _a : 'Test failed',
|
|
1079
|
+
stackTrace: test.stackTrace,
|
|
1080
|
+
});
|
|
1081
|
+
});
|
|
1082
|
+
const byTest = new Map();
|
|
1083
|
+
for (const test of tests) {
|
|
1084
|
+
const key = `${test.file}#${test.method}`;
|
|
1085
|
+
const statuses = (_a = byTest.get(key)) !== null && _a !== void 0 ? _a : new Set();
|
|
1086
|
+
statuses.add(test.status);
|
|
1087
|
+
byTest.set(key, statuses);
|
|
1088
|
+
}
|
|
1089
|
+
return {
|
|
1090
|
+
matchedPaths: junitReportPaths,
|
|
1091
|
+
checkedPaths: discovery.checkedPaths,
|
|
1092
|
+
summary: {
|
|
1093
|
+
passed: tests.filter((test) => test.status === 'passed').length,
|
|
1094
|
+
failed: tests.filter((test) => test.status === 'failed').length,
|
|
1095
|
+
skipped: tests.filter((test) => test.status === 'skipped').length,
|
|
1096
|
+
flakyTests: Array.from(byTest.entries())
|
|
1097
|
+
.filter(([, statuses]) => statuses.has('passed') && statuses.has('failed'))
|
|
1098
|
+
.map(([key]) => key),
|
|
1099
|
+
failures,
|
|
1100
|
+
...slowSummary,
|
|
1101
|
+
},
|
|
1102
|
+
};
|
|
1103
|
+
}
|
|
1104
|
+
function isLikelyJunitXml(reportPath) {
|
|
1105
|
+
if (path.extname(reportPath).toLowerCase() !== '.xml') {
|
|
1106
|
+
return false;
|
|
1107
|
+
}
|
|
1108
|
+
const xml = safeReadHead(reportPath);
|
|
1109
|
+
return /<testsuites?\b|<testcase\b/.test(xml);
|
|
1110
|
+
}
|
|
694
1111
|
function parseSlowTests(rootDir, config) {
|
|
1112
|
+
const discovery = discoverDynamicResults(rootDir, config);
|
|
1113
|
+
return discovery.summary;
|
|
1114
|
+
}
|
|
1115
|
+
function buildSlowTestSummary(durations, reportPaths, config) {
|
|
695
1116
|
var _a, _b;
|
|
696
|
-
const reportPaths = fast_glob_1.default.sync([...TEST_RESULT_GLOBS], { cwd: rootDir, absolute: true });
|
|
697
|
-
if (reportPaths.length === 0)
|
|
698
|
-
return null;
|
|
699
|
-
const durations = [];
|
|
700
|
-
for (const reportPath of reportPaths) {
|
|
701
|
-
if (reportPath.endsWith('.json')) {
|
|
702
|
-
durations.push(...parseJestJsonDurations(reportPath));
|
|
703
|
-
}
|
|
704
|
-
else {
|
|
705
|
-
durations.push(...parseJunitXmlDurations(reportPath));
|
|
706
|
-
}
|
|
707
|
-
}
|
|
708
|
-
if (durations.length === 0)
|
|
709
|
-
return null;
|
|
710
1117
|
const unitThreshold = (_b = (_a = config.slowTestThreshold) === null || _a === void 0 ? void 0 : _a.unit) !== null && _b !== void 0 ? _b : 200;
|
|
711
1118
|
const slowTests = durations
|
|
712
1119
|
.filter((item) => item.durationMs > unitThreshold)
|
|
@@ -716,32 +1123,43 @@ function parseSlowTests(rootDir, config) {
|
|
|
716
1123
|
hint: buildSlowTestHint(item.durationMs),
|
|
717
1124
|
}))
|
|
718
1125
|
.sort((a, b) => b.durationMs - a.durationMs);
|
|
719
|
-
const totalDurationMs = durations.
|
|
1126
|
+
const totalDurationMs = durations.length > 0
|
|
1127
|
+
? durations.reduce((sum, item) => sum + item.durationMs, 0)
|
|
1128
|
+
: 0;
|
|
720
1129
|
const fastCount = durations.filter((item) => item.durationMs < 50).length;
|
|
721
1130
|
const slowCount = durations.filter((item) => item.durationMs >= unitThreshold).length;
|
|
722
1131
|
return {
|
|
723
1132
|
slowTests,
|
|
724
|
-
avgDurationMs: Math.round(totalDurationMs / durations.length),
|
|
1133
|
+
avgDurationMs: durations.length > 0 ? Math.round(totalDurationMs / durations.length) : 0,
|
|
725
1134
|
totalDurationMs,
|
|
726
|
-
fastPercent: Math.round((fastCount / durations.length) * 100),
|
|
727
|
-
slowPercent: Math.round((slowCount / durations.length) * 100),
|
|
728
|
-
reportPaths
|
|
1135
|
+
fastPercent: durations.length > 0 ? Math.round((fastCount / durations.length) * 100) : 0,
|
|
1136
|
+
slowPercent: durations.length > 0 ? Math.round((slowCount / durations.length) * 100) : 0,
|
|
1137
|
+
reportPaths,
|
|
729
1138
|
};
|
|
730
1139
|
}
|
|
731
|
-
function
|
|
1140
|
+
function parseJunitXmlResults(reportPath) {
|
|
732
1141
|
const xml = safeRead(reportPath);
|
|
733
|
-
const testcases = [...xml.matchAll(/<testcase\b([^>]
|
|
1142
|
+
const testcases = [...xml.matchAll(/<testcase\b([^>]*?)(?:\/>|>([\s\S]*?)<\/testcase>)/g)];
|
|
734
1143
|
return testcases.map((match) => {
|
|
735
|
-
var _a, _b, _c, _d, _e, _f, _g, _h, _j;
|
|
1144
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
|
|
736
1145
|
const attrs = match[1];
|
|
737
|
-
const
|
|
738
|
-
const
|
|
739
|
-
const
|
|
740
|
-
const
|
|
1146
|
+
const body = (_a = match[2]) !== null && _a !== void 0 ? _a : '';
|
|
1147
|
+
const name = (_c = (_b = /\bname="([^"]+)"/.exec(attrs)) === null || _b === void 0 ? void 0 : _b[1]) !== null && _c !== void 0 ? _c : 'unknown';
|
|
1148
|
+
const className = ((_d = /\bclassname="([^"]+)"/.exec(attrs)) === null || _d === void 0 ? void 0 : _d[1]) || 'unknown';
|
|
1149
|
+
const file = ((_e = /\bfile="([^"]+)"/.exec(attrs)) === null || _e === void 0 ? void 0 : _e[1]) || className;
|
|
1150
|
+
const timeSeconds = Number((_g = (_f = /\btime="([^"]+)"/.exec(attrs)) === null || _f === void 0 ? void 0 : _f[1]) !== null && _g !== void 0 ? _g : 0);
|
|
1151
|
+
const failureMatch = /<(failure|error)\b([^>]*)>([\s\S]*?)<\/\1>/.exec(body);
|
|
1152
|
+
const skippedMatch = /<skipped\b/.exec(body);
|
|
1153
|
+
const failureAttrs = (_h = failureMatch === null || failureMatch === void 0 ? void 0 : failureMatch[2]) !== null && _h !== void 0 ? _h : '';
|
|
1154
|
+
const failureBody = sanitizeXmlText((_j = failureMatch === null || failureMatch === void 0 ? void 0 : failureMatch[3]) !== null && _j !== void 0 ? _j : '');
|
|
1155
|
+
const failureMessage = (_l = (_k = /\bmessage="([^"]+)"/.exec(failureAttrs)) === null || _k === void 0 ? void 0 : _k[1]) !== null && _l !== void 0 ? _l : failureBody.split('\n').map((line) => line.trim()).find(Boolean);
|
|
741
1156
|
return {
|
|
742
1157
|
file: normalizePath(file),
|
|
743
1158
|
method: name,
|
|
1159
|
+
status: failureMatch ? 'failed' : skippedMatch ? 'skipped' : 'passed',
|
|
744
1160
|
durationMs: Math.round(timeSeconds * 1000),
|
|
1161
|
+
failureMessage,
|
|
1162
|
+
stackTrace: failureBody || undefined,
|
|
745
1163
|
};
|
|
746
1164
|
});
|
|
747
1165
|
}
|
|
@@ -763,6 +1181,40 @@ function parseJestJsonDurations(reportPath) {
|
|
|
763
1181
|
}
|
|
764
1182
|
return durations;
|
|
765
1183
|
}
|
|
1184
|
+
function discoverContractTesting(rootDir) {
|
|
1185
|
+
const discovery = discoverFirstMatchingPaths(rootDir, CONTRACT_REPORT_GLOBS);
|
|
1186
|
+
if (discovery.matchedPaths.length === 0) {
|
|
1187
|
+
return discovery;
|
|
1188
|
+
}
|
|
1189
|
+
const failedVerifications = [];
|
|
1190
|
+
let verifiedContracts = 0;
|
|
1191
|
+
for (const contractPath of discovery.matchedPaths) {
|
|
1192
|
+
try {
|
|
1193
|
+
const json = JSON.parse(safeRead(contractPath));
|
|
1194
|
+
const contractName = formatContractName(json, contractPath);
|
|
1195
|
+
const verification = extractContractVerificationStatus(json);
|
|
1196
|
+
if (verification === true) {
|
|
1197
|
+
verifiedContracts += 1;
|
|
1198
|
+
}
|
|
1199
|
+
if (verification === false) {
|
|
1200
|
+
failedVerifications.push(`${contractName}: ${extractContractFailureReason(json)}`);
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
catch {
|
|
1204
|
+
failedVerifications.push(`${normalizePath(path.relative(rootDir, contractPath))}: invalid JSON`);
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
return {
|
|
1208
|
+
matchedPaths: discovery.matchedPaths,
|
|
1209
|
+
checkedPaths: discovery.checkedPaths,
|
|
1210
|
+
summary: {
|
|
1211
|
+
totalContracts: discovery.matchedPaths.length,
|
|
1212
|
+
verifiedContracts,
|
|
1213
|
+
failedVerifications,
|
|
1214
|
+
contractPaths: discovery.matchedPaths.map((contractPath) => normalizePath(path.relative(rootDir, contractPath))),
|
|
1215
|
+
},
|
|
1216
|
+
};
|
|
1217
|
+
}
|
|
766
1218
|
function buildSlowTestHint(durationMs) {
|
|
767
1219
|
if (durationMs > 5000) {
|
|
768
1220
|
return `Test takes ${(durationMs / 1000).toFixed(1)}s — likely performing real IO or loading a full application context. Mock external calls or use a lighter fixture.`;
|
|
@@ -772,6 +1224,143 @@ function buildSlowTestHint(durationMs) {
|
|
|
772
1224
|
}
|
|
773
1225
|
return `Test takes ${durationMs}ms — likely doing avoidable IO. Prefer isolated unit doubles and in-memory fixtures.`;
|
|
774
1226
|
}
|
|
1227
|
+
function calculateDynamicPassRate(summary) {
|
|
1228
|
+
const total = summary.passed + summary.failed + summary.skipped;
|
|
1229
|
+
if (total === 0)
|
|
1230
|
+
return 0;
|
|
1231
|
+
return Number(((summary.passed / total) * 100).toFixed(1));
|
|
1232
|
+
}
|
|
1233
|
+
function calculateUnitScore(input) {
|
|
1234
|
+
var _a, _b, _c, _d, _e, _f;
|
|
1235
|
+
const weights = getAppliedUnitScoreWeights(input);
|
|
1236
|
+
return Number(((input.astQuality * weights.ast)
|
|
1237
|
+
+ (((_a = input.lineCoverage) !== null && _a !== void 0 ? _a : 0) * ((_b = weights.line) !== null && _b !== void 0 ? _b : 0))
|
|
1238
|
+
+ (((_c = input.branchCoverage) !== null && _c !== void 0 ? _c : 0) * ((_d = weights.branch) !== null && _d !== void 0 ? _d : 0))
|
|
1239
|
+
+ (((_e = input.mutationScore) !== null && _e !== void 0 ? _e : 0) * ((_f = weights.mutation) !== null && _f !== void 0 ? _f : 0))).toFixed(1));
|
|
1240
|
+
}
|
|
1241
|
+
function describeAppliedWeighting(input) {
|
|
1242
|
+
const weights = getAppliedUnitScoreWeights({
|
|
1243
|
+
astQuality: 0,
|
|
1244
|
+
dynamicPassRate: null,
|
|
1245
|
+
lineCoverage: input.lineCoverage,
|
|
1246
|
+
branchCoverage: input.branchCoverage,
|
|
1247
|
+
mutationScore: input.mutationScore,
|
|
1248
|
+
layersApplied: input.layersApplied,
|
|
1249
|
+
});
|
|
1250
|
+
const parts = [`AST ${formatWeightPercent(weights.ast)}`];
|
|
1251
|
+
if (weights.line) {
|
|
1252
|
+
parts.push(`Line ${formatWeightPercent(weights.line)}`);
|
|
1253
|
+
}
|
|
1254
|
+
if (weights.branch) {
|
|
1255
|
+
parts.push(`Branch ${formatWeightPercent(weights.branch)}`);
|
|
1256
|
+
}
|
|
1257
|
+
if (weights.mutation) {
|
|
1258
|
+
parts.push(`Mutation ${formatWeightPercent(weights.mutation)}`);
|
|
1259
|
+
}
|
|
1260
|
+
return parts.join(' + ');
|
|
1261
|
+
}
|
|
1262
|
+
function getAppliedUnitScoreWeights(input) {
|
|
1263
|
+
const hasRuntime = input.layersApplied.includes('coverage') && input.lineCoverage !== null;
|
|
1264
|
+
const hasBranch = hasRuntime && input.branchCoverage !== null;
|
|
1265
|
+
const hasMutation = input.layersApplied.includes('mutation') && input.mutationScore !== null;
|
|
1266
|
+
if (hasRuntime && hasMutation) {
|
|
1267
|
+
return hasBranch
|
|
1268
|
+
? SCORE_WEIGHTS.astRuntimeMutation
|
|
1269
|
+
: normalizeUnitScoreWeights({
|
|
1270
|
+
ast: SCORE_WEIGHTS.astRuntimeMutation.ast,
|
|
1271
|
+
line: SCORE_WEIGHTS.astRuntimeMutation.line,
|
|
1272
|
+
mutation: SCORE_WEIGHTS.astRuntimeMutation.mutation,
|
|
1273
|
+
});
|
|
1274
|
+
}
|
|
1275
|
+
if (hasRuntime) {
|
|
1276
|
+
return hasBranch
|
|
1277
|
+
? SCORE_WEIGHTS.astRuntime
|
|
1278
|
+
: normalizeUnitScoreWeights({
|
|
1279
|
+
ast: SCORE_WEIGHTS.astRuntime.ast,
|
|
1280
|
+
line: SCORE_WEIGHTS.astRuntime.line,
|
|
1281
|
+
});
|
|
1282
|
+
}
|
|
1283
|
+
if (hasMutation) {
|
|
1284
|
+
return SCORE_WEIGHTS.astMutation;
|
|
1285
|
+
}
|
|
1286
|
+
return SCORE_WEIGHTS.astOnly;
|
|
1287
|
+
}
|
|
1288
|
+
function normalizeUnitScoreWeights(weights) {
|
|
1289
|
+
if (Object.values(weights).some((weight) => weight !== undefined && weight < 0)) {
|
|
1290
|
+
return { ast: 1 };
|
|
1291
|
+
}
|
|
1292
|
+
const total = Object.values(weights).reduce((sum, value) => sum + value, 0);
|
|
1293
|
+
if (total <= 0) {
|
|
1294
|
+
return { ast: 1 };
|
|
1295
|
+
}
|
|
1296
|
+
const normalized = {
|
|
1297
|
+
ast: weights.ast / total,
|
|
1298
|
+
};
|
|
1299
|
+
if (weights.line) {
|
|
1300
|
+
normalized.line = weights.line / total;
|
|
1301
|
+
}
|
|
1302
|
+
if (weights.branch) {
|
|
1303
|
+
normalized.branch = weights.branch / total;
|
|
1304
|
+
}
|
|
1305
|
+
if (weights.mutation) {
|
|
1306
|
+
normalized.mutation = weights.mutation / total;
|
|
1307
|
+
}
|
|
1308
|
+
return normalized;
|
|
1309
|
+
}
|
|
1310
|
+
function formatWeightPercent(weight) {
|
|
1311
|
+
const percent = weight * 100;
|
|
1312
|
+
const rounded = percent.toFixed(1);
|
|
1313
|
+
return rounded.endsWith('.0') ? `${rounded.slice(0, -2)}%` : `${rounded}%`;
|
|
1314
|
+
}
|
|
1315
|
+
function buildUnitAnalysisConsoleLines(input) {
|
|
1316
|
+
var _a;
|
|
1317
|
+
const lines = [
|
|
1318
|
+
'=== Unit Analysis ===',
|
|
1319
|
+
`Files scanned: ${input.ast.filesScanned} test files, ${input.ast.testCount} test methods`,
|
|
1320
|
+
'',
|
|
1321
|
+
`AST Quality Score: ${input.ast.qualityScore}/100`,
|
|
1322
|
+
` ${scoreIcon(input.ast.dimensionScores.assertionCompleteness)} Assertion completeness: ${input.ast.dimensionScores.assertionCompleteness}/20 (${describeAssertionCompleteness(input.ast)})`,
|
|
1323
|
+
` ${scoreIcon(input.ast.dimensionScores.testIsolation)} Test isolation: ${input.ast.dimensionScores.testIsolation}/20 (${input.ast.isolationPercent}% use mocks/stubs/spies)`,
|
|
1324
|
+
` ${scoreIcon(input.ast.dimensionScores.namingQuality)} Naming quality: ${input.ast.dimensionScores.namingQuality}/20 (${input.ast.descriptiveNamePercent}% descriptive names)`,
|
|
1325
|
+
` ${scoreIcon(input.ast.dimensionScores.disabledTests)} No disabled tests: ${input.ast.dimensionScores.disabledTests}/20 (${input.ast.disabledTests.length} disabled tests)`,
|
|
1326
|
+
` ${scoreIcon(input.ast.dimensionScores.criticalSmells)} Smell-free: ${input.ast.dimensionScores.criticalSmells}/20 (${describeCriticalSmells(input.ast)})`,
|
|
1327
|
+
];
|
|
1328
|
+
if (input.coverage) {
|
|
1329
|
+
lines.push('');
|
|
1330
|
+
lines.push(`Runtime enrichment: ✓ ${input.coverage.tool} found at ${input.coverage.reportPath}`);
|
|
1331
|
+
lines.push(` Line coverage: ${formatPct(input.coverage.lineCoverage)}`);
|
|
1332
|
+
lines.push(` Branch coverage: ${formatPct(input.coverage.branchCoverage)}`);
|
|
1333
|
+
if (input.coverage.methodCoverage !== null) {
|
|
1334
|
+
lines.push(` Method coverage: ${formatPct(input.coverage.methodCoverage)}`);
|
|
1335
|
+
}
|
|
1336
|
+
if (input.coverage.classCoverage !== null) {
|
|
1337
|
+
lines.push(` Class coverage: ${formatPct(input.coverage.classCoverage)}`);
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
else {
|
|
1341
|
+
lines.push('');
|
|
1342
|
+
lines.push('Runtime enrichment: · Not found (reads existing coverage artifacts only)');
|
|
1343
|
+
}
|
|
1344
|
+
if (input.mutation) {
|
|
1345
|
+
lines.push('');
|
|
1346
|
+
lines.push(`Mutation enrichment: ✓ ${input.mutation.tool} found at ${(_a = input.mutation.reportPath) !== null && _a !== void 0 ? _a : 'mutation report'}`);
|
|
1347
|
+
lines.push(` Mutation score: ${input.mutation.mutationScore}%`);
|
|
1348
|
+
lines.push(` Survived: ${input.mutation.survivedMutants}`);
|
|
1349
|
+
lines.push(` Killed: ${input.mutation.killedMutants}`);
|
|
1350
|
+
}
|
|
1351
|
+
else {
|
|
1352
|
+
lines.push('');
|
|
1353
|
+
lines.push(`Mutation enrichment: · Not found (run: ${suggestMutationCommand(input.languages)})`);
|
|
1354
|
+
}
|
|
1355
|
+
if (input.dynamic) {
|
|
1356
|
+
lines.push('');
|
|
1357
|
+
lines.push(`Dynamic evidence: ${input.dynamic.reportPaths[0]} (${input.dynamic.passed} passed, ${input.dynamic.failed} failed, ${input.dynamic.skipped} skipped)`);
|
|
1358
|
+
}
|
|
1359
|
+
if (input.contract) {
|
|
1360
|
+
lines.push(`Contract evidence: ${input.contract.totalContracts} contracts, ${input.contract.verifiedContracts} verified`);
|
|
1361
|
+
}
|
|
1362
|
+
return lines;
|
|
1363
|
+
}
|
|
775
1364
|
function detectTestSmells(rootDir, tests) {
|
|
776
1365
|
const smells = [];
|
|
777
1366
|
for (const test of tests) {
|
|
@@ -819,6 +1408,17 @@ function detectPerBlockSmells(rootDir, test) {
|
|
|
819
1408
|
fix: 'Add assertion on the return value or verify mock interaction',
|
|
820
1409
|
});
|
|
821
1410
|
}
|
|
1411
|
+
if (isEmptyTestBody(body)) {
|
|
1412
|
+
smells.push({
|
|
1413
|
+
smellId: 'SMELL-11',
|
|
1414
|
+
severity: 'HIGH',
|
|
1415
|
+
file: test.relativePath,
|
|
1416
|
+
line: block.line,
|
|
1417
|
+
testMethod: block.name,
|
|
1418
|
+
description: 'Empty test body detected',
|
|
1419
|
+
fix: 'Add test steps and assertions or remove the placeholder test',
|
|
1420
|
+
});
|
|
1421
|
+
}
|
|
822
1422
|
if (isDisabledWithoutReason(test.language, block, test.content)) {
|
|
823
1423
|
smells.push({
|
|
824
1424
|
smellId: 'SMELL-03',
|
|
@@ -1106,6 +1706,37 @@ function extractTestBlocks(language, content) {
|
|
|
1106
1706
|
}
|
|
1107
1707
|
return blocks;
|
|
1108
1708
|
}
|
|
1709
|
+
if (language === 'ruby') {
|
|
1710
|
+
const regex = /(xit|xspecify|it|specify|test)\s*(?:\(\s*)?['"]([^'"]+)['"](?:\s*\))?\s*do\s*\n([\s\S]*?)(?=\n\s*end\b)/g;
|
|
1711
|
+
const blocks = [];
|
|
1712
|
+
let match;
|
|
1713
|
+
while ((match = regex.exec(content)) !== null) {
|
|
1714
|
+
blocks.push({
|
|
1715
|
+
name: match[2],
|
|
1716
|
+
body: match[3],
|
|
1717
|
+
line: lineNumberForIndex(content, match.index),
|
|
1718
|
+
decorators: [match[1]],
|
|
1719
|
+
});
|
|
1720
|
+
}
|
|
1721
|
+
return blocks;
|
|
1722
|
+
}
|
|
1723
|
+
if (language === 'go') {
|
|
1724
|
+
const regex = /func\s+(Test[a-zA-Z_]\w*)\s*\([^)]*\)\s*\{/g;
|
|
1725
|
+
const blocks = [];
|
|
1726
|
+
let match;
|
|
1727
|
+
while ((match = regex.exec(content)) !== null) {
|
|
1728
|
+
const start = match.index + match[0].length - 1;
|
|
1729
|
+
const end = findMatchingBrace(content, start);
|
|
1730
|
+
blocks.push({
|
|
1731
|
+
name: match[1],
|
|
1732
|
+
body: content.slice(start + 1, end),
|
|
1733
|
+
line: lineNumberForIndex(content, match.index),
|
|
1734
|
+
decorators: [],
|
|
1735
|
+
});
|
|
1736
|
+
regex.lastIndex = end + 1;
|
|
1737
|
+
}
|
|
1738
|
+
return blocks;
|
|
1739
|
+
}
|
|
1109
1740
|
return [];
|
|
1110
1741
|
}
|
|
1111
1742
|
function hasVerificationCall(language, body) {
|
|
@@ -1115,6 +1746,10 @@ function hasVerificationCall(language, body) {
|
|
|
1115
1746
|
return /expect\s*\(|toHaveBeenCalled/.test(body);
|
|
1116
1747
|
if (language === 'python')
|
|
1117
1748
|
return /\bassert\b|assert_called/.test(body);
|
|
1749
|
+
if (language === 'ruby')
|
|
1750
|
+
return /\bexpect\(|assert_|refute_|have_received/.test(body);
|
|
1751
|
+
if (language === 'go')
|
|
1752
|
+
return /\b(?:assert|require)\.\w+\(|t\.Fatalf?\(|t\.Errorf?\(/.test(body);
|
|
1118
1753
|
return false;
|
|
1119
1754
|
}
|
|
1120
1755
|
function isDisabledWithoutReason(language, block, content) {
|
|
@@ -1132,6 +1767,24 @@ function isDisabledWithoutReason(language, block, content) {
|
|
|
1132
1767
|
if (language === 'python') {
|
|
1133
1768
|
return block.decorators.some((decorator) => /@pytest\.mark\.skip(?!\s*\(|.*reason\s*=)/.test(decorator));
|
|
1134
1769
|
}
|
|
1770
|
+
if (language === 'ruby') {
|
|
1771
|
+
return block.decorators.some((decorator) => /^(xit|xspecify)$/.test(decorator));
|
|
1772
|
+
}
|
|
1773
|
+
return false;
|
|
1774
|
+
}
|
|
1775
|
+
function isDisabledBlock(language, block) {
|
|
1776
|
+
if (language === 'java' || language === 'kotlin') {
|
|
1777
|
+
return block.decorators.some((decorator) => decorator.startsWith('@Disabled'));
|
|
1778
|
+
}
|
|
1779
|
+
if (language === 'javascript' || language === 'typescript') {
|
|
1780
|
+
return block.decorators.some((decorator) => /^(xit|xtest|test\.skip|it\.skip)$/.test(decorator));
|
|
1781
|
+
}
|
|
1782
|
+
if (language === 'python') {
|
|
1783
|
+
return block.decorators.some((decorator) => /@pytest\.mark\.skip/.test(decorator));
|
|
1784
|
+
}
|
|
1785
|
+
if (language === 'ruby') {
|
|
1786
|
+
return block.decorators.some((decorator) => /^(xit|xspecify)$/.test(decorator));
|
|
1787
|
+
}
|
|
1135
1788
|
return false;
|
|
1136
1789
|
}
|
|
1137
1790
|
function hasAssertionMessages(language, body) {
|
|
@@ -1224,6 +1877,7 @@ function countParameterizedExpansions(content) {
|
|
|
1224
1877
|
var _a, _b;
|
|
1225
1878
|
const matches = [
|
|
1226
1879
|
...content.matchAll(/@CsvSource\(([^)]*)\)/g),
|
|
1880
|
+
...content.matchAll(/@ParameterizedTest/g),
|
|
1227
1881
|
...content.matchAll(/test\.each\(([^)]*)\)/g),
|
|
1228
1882
|
...content.matchAll(/pytest\.mark\.parametrize\(([^)]*)\)/g),
|
|
1229
1883
|
];
|
|
@@ -1241,6 +1895,10 @@ function detectMockLibrary(content) {
|
|
|
1241
1895
|
return 'jest';
|
|
1242
1896
|
if (/unittest\.mock|MagicMock|pytest-mock|mocker\./i.test(content))
|
|
1243
1897
|
return 'unittest.mock';
|
|
1898
|
+
if (/\bdouble\(|instance_double\(|allow\(|stub_const\(/.test(content))
|
|
1899
|
+
return 'rspec';
|
|
1900
|
+
if (/gomock|testify\/mock/.test(content))
|
|
1901
|
+
return 'gomock';
|
|
1244
1902
|
return undefined;
|
|
1245
1903
|
}
|
|
1246
1904
|
function classifyLayer(content, filePath) {
|
|
@@ -1251,7 +1909,7 @@ function classifyLayer(content, filePath) {
|
|
|
1251
1909
|
return 'unit';
|
|
1252
1910
|
}
|
|
1253
1911
|
function countAssertions(language, content) {
|
|
1254
|
-
var _a, _b, _c;
|
|
1912
|
+
var _a, _b, _c, _d, _e;
|
|
1255
1913
|
if (language === 'java' || language === 'kotlin') {
|
|
1256
1914
|
return ((_a = content.match(/\bassert\w*\s*\(|\bverify\s*\(/g)) !== null && _a !== void 0 ? _a : []).length;
|
|
1257
1915
|
}
|
|
@@ -1261,8 +1919,151 @@ function countAssertions(language, content) {
|
|
|
1261
1919
|
if (language === 'python') {
|
|
1262
1920
|
return ((_c = content.match(/\bassert\b|assert_called/g)) !== null && _c !== void 0 ? _c : []).length;
|
|
1263
1921
|
}
|
|
1922
|
+
if (language === 'ruby') {
|
|
1923
|
+
return ((_d = content.match(/\bexpect\(|\bassert_\w+|\brefute_\w+|have_received/g)) !== null && _d !== void 0 ? _d : []).length;
|
|
1924
|
+
}
|
|
1925
|
+
if (language === 'go') {
|
|
1926
|
+
return ((_e = content.match(/\b(?:assert|require)\.\w+\(|t\.Fatalf?\(|t\.Errorf?\(/g)) !== null && _e !== void 0 ? _e : []).length;
|
|
1927
|
+
}
|
|
1264
1928
|
return 0;
|
|
1265
1929
|
}
|
|
1930
|
+
function countTestClasses(language, content) {
|
|
1931
|
+
var _a, _b, _c, _d;
|
|
1932
|
+
if (language === 'java' || language === 'kotlin') {
|
|
1933
|
+
return ((_a = content.match(/\bclass\s+[A-Z]\w*Test\b/g)) !== null && _a !== void 0 ? _a : []).length || 1;
|
|
1934
|
+
}
|
|
1935
|
+
if (language === 'python') {
|
|
1936
|
+
return ((_b = content.match(/^\s*class\s+\w+\(.*TestCase.*\):/gm)) !== null && _b !== void 0 ? _b : []).length || 1;
|
|
1937
|
+
}
|
|
1938
|
+
if (language === 'ruby') {
|
|
1939
|
+
return ((_c = content.match(/\bRSpec\.describe\b|\bdescribe\s+['"]/g)) !== null && _c !== void 0 ? _c : []).length || 1;
|
|
1940
|
+
}
|
|
1941
|
+
if (language === 'go') {
|
|
1942
|
+
return 1;
|
|
1943
|
+
}
|
|
1944
|
+
return ((_d = content.match(/\bdescribe\s*\(/g)) !== null && _d !== void 0 ? _d : []).length || 1;
|
|
1945
|
+
}
|
|
1946
|
+
function countParameterizedTests(language, content) {
|
|
1947
|
+
var _a, _b, _c, _d;
|
|
1948
|
+
if (language === 'java' || language === 'kotlin')
|
|
1949
|
+
return ((_a = content.match(/@ParameterizedTest/g)) !== null && _a !== void 0 ? _a : []).length;
|
|
1950
|
+
if (language === 'javascript' || language === 'typescript')
|
|
1951
|
+
return ((_b = content.match(/\b(?:test|it)\.each\s*\(/g)) !== null && _b !== void 0 ? _b : []).length;
|
|
1952
|
+
if (language === 'python')
|
|
1953
|
+
return ((_c = content.match(/pytest\.mark\.parametrize/g)) !== null && _c !== void 0 ? _c : []).length;
|
|
1954
|
+
if (language === 'ruby')
|
|
1955
|
+
return ((_d = content.match(/\bshared_examples\b|\beach do \|/g)) !== null && _d !== void 0 ? _d : []).length;
|
|
1956
|
+
return 0;
|
|
1957
|
+
}
|
|
1958
|
+
function countMockSignals(content) {
|
|
1959
|
+
var _a;
|
|
1960
|
+
return ((_a = content.match(/@MockBean\b|@Mock\b|\bmock\(|jest\.mock\(|vi\.mock\(|patch\(|MagicMock|mocker\.|double\(|gomock|testify\/mock/g)) !== null && _a !== void 0 ? _a : []).length;
|
|
1961
|
+
}
|
|
1962
|
+
function countStubSignals(content) {
|
|
1963
|
+
var _a;
|
|
1964
|
+
return ((_a = content.match(/thenReturn\(|mockReturnValue\(|returns\(|allow\(.+\)\.to receive|stub\(/g)) !== null && _a !== void 0 ? _a : []).length;
|
|
1965
|
+
}
|
|
1966
|
+
function countSpySignals(content) {
|
|
1967
|
+
var _a;
|
|
1968
|
+
return ((_a = content.match(/@Spy\b|jest\.spyOn\(|vi\.spyOn\(|spyOn\(/g)) !== null && _a !== void 0 ? _a : []).length;
|
|
1969
|
+
}
|
|
1970
|
+
function testUsesIsolation(language, body) {
|
|
1971
|
+
if (language === 'java' || language === 'kotlin')
|
|
1972
|
+
return /@Mock\b|@MockBean\b|verify\(|mock\(|when\(.+\)\.thenReturn|spy\(/.test(body);
|
|
1973
|
+
if (language === 'javascript' || language === 'typescript')
|
|
1974
|
+
return /jest\.mock\(|vi\.mock\(|jest\.spyOn\(|vi\.spyOn\(|mockReturnValue|toHaveBeenCalled/.test(body);
|
|
1975
|
+
if (language === 'python')
|
|
1976
|
+
return /patch\(|MagicMock|mocker\.|assert_called/.test(body);
|
|
1977
|
+
if (language === 'ruby')
|
|
1978
|
+
return /double\(|instance_double\(|allow\(|have_received/.test(body);
|
|
1979
|
+
if (language === 'go')
|
|
1980
|
+
return /gomock|testify\/mock|mock\./.test(body);
|
|
1981
|
+
return false;
|
|
1982
|
+
}
|
|
1983
|
+
function isDescriptiveTestName(name) {
|
|
1984
|
+
const normalized = name
|
|
1985
|
+
.replace(/([a-z])([A-Z])/g, '$1 $2')
|
|
1986
|
+
.replace(/[_\-]+/g, ' ')
|
|
1987
|
+
.trim();
|
|
1988
|
+
return normalized.split(/\s+/).filter(Boolean).length > 3;
|
|
1989
|
+
}
|
|
1990
|
+
function isGenericTestName(name) {
|
|
1991
|
+
return /^(test\d+|testMethod\d+|testMethod|shouldWork|works|test)$/i.test(name.replace(/\s+/g, ''));
|
|
1992
|
+
}
|
|
1993
|
+
function isEmptyTestBody(body) {
|
|
1994
|
+
const meaningful = body
|
|
1995
|
+
.split('\n')
|
|
1996
|
+
.map((line) => line.replace(/\/\/.*$/, '').trim())
|
|
1997
|
+
.filter(Boolean);
|
|
1998
|
+
return meaningful.length === 0 || meaningful.every((line) => /^(pass|todo)$/i.test(line) || /^(\/\/|#)/.test(line));
|
|
1999
|
+
}
|
|
2000
|
+
function collectLineMatches(rootDir, filePath, content, pattern) {
|
|
2001
|
+
const relativePath = normalizePath(path.relative(rootDir, filePath));
|
|
2002
|
+
return content
|
|
2003
|
+
.split('\n')
|
|
2004
|
+
.flatMap((line, index) => (pattern.test(line) ? [`${relativePath}:${index + 1}`] : []));
|
|
2005
|
+
}
|
|
2006
|
+
function hasStaticMutableStateMutation(language, content) {
|
|
2007
|
+
var _a, _b, _c, _d;
|
|
2008
|
+
if (language === 'java' || language === 'kotlin') {
|
|
2009
|
+
const beforeAllBlock = (_b = (_a = /@BeforeAll[\s\S]*?\{([\s\S]*?)\}/.exec(content)) === null || _a === void 0 ? void 0 : _a[1]) !== null && _b !== void 0 ? _b : '';
|
|
2010
|
+
const staticFields = [...content.matchAll(/static\s+(?!final)[^{;=]+([a-zA-Z_]\w*)/g)].map((match) => match[1]);
|
|
2011
|
+
if (staticFields.length === 0)
|
|
2012
|
+
return false;
|
|
2013
|
+
const mutatedFields = staticFields.filter((field) => new RegExp(`\\b${field}\\s*=`).test(beforeAllBlock));
|
|
2014
|
+
if (mutatedFields.length === 0)
|
|
2015
|
+
return false;
|
|
2016
|
+
const afterAllBlock = (_d = (_c = /@AfterAll[\s\S]*?\{([\s\S]*?)\}/.exec(content)) === null || _c === void 0 ? void 0 : _c[1]) !== null && _d !== void 0 ? _d : '';
|
|
2017
|
+
return mutatedFields.some((field) => !new RegExp(`\\b${field}\\s*=`).test(afterAllBlock));
|
|
2018
|
+
}
|
|
2019
|
+
if (language === 'javascript' || language === 'typescript') {
|
|
2020
|
+
return /\blet\s+[a-zA-Z_]\w*/.test(content) && /beforeAll\s*\([\s\S]*?\b[a-zA-Z_]\w*\s*=/.test(content);
|
|
2021
|
+
}
|
|
2022
|
+
if (language === 'python') {
|
|
2023
|
+
return /@classmethod\s+def\s+setUpClass/.test(content) && /cls\.[a-zA-Z_]\w*\s*=/.test(content) && !/@classmethod\s+def\s+tearDownClass/.test(content);
|
|
2024
|
+
}
|
|
2025
|
+
return false;
|
|
2026
|
+
}
|
|
2027
|
+
function collectSharedMutableFields(language, content) {
|
|
2028
|
+
if (language === 'java' || language === 'kotlin') {
|
|
2029
|
+
return dedupeStrings([...content.matchAll(/(?:private|protected|public)?\s*(?!final)(?:[\w<>\[\]]+\s+)+([a-zA-Z_]\w*)\s*[;=]/g)].map((match) => match[1]));
|
|
2030
|
+
}
|
|
2031
|
+
if (language === 'javascript' || language === 'typescript') {
|
|
2032
|
+
return dedupeStrings([...content.matchAll(/\blet\s+([a-zA-Z_]\w*)\s*[=;]/g)].map((match) => match[1]));
|
|
2033
|
+
}
|
|
2034
|
+
if (language === 'python') {
|
|
2035
|
+
return dedupeStrings([...content.matchAll(/self\.([a-zA-Z_]\w*)\s*=/g)].map((match) => match[1]));
|
|
2036
|
+
}
|
|
2037
|
+
return [];
|
|
2038
|
+
}
|
|
2039
|
+
function clampScore(score) {
|
|
2040
|
+
return Math.max(0, Math.min(100, Number(score.toFixed(1))));
|
|
2041
|
+
}
|
|
2042
|
+
function calculateWeightedCoveredItems(totalItems, weightedScore) {
|
|
2043
|
+
if (totalItems <= 0) {
|
|
2044
|
+
return 0;
|
|
2045
|
+
}
|
|
2046
|
+
return Math.max(0, Math.min(totalItems, Math.round((totalItems * clampScore(weightedScore)) / 100)));
|
|
2047
|
+
}
|
|
2048
|
+
function countPassingQualityTests(tests) {
|
|
2049
|
+
let passing = 0;
|
|
2050
|
+
for (const test of tests) {
|
|
2051
|
+
for (const block of test.testBlocks) {
|
|
2052
|
+
const assertionCount = countAssertions(test.language, block.body);
|
|
2053
|
+
const hasCriticalSmell = isEmptyTestBody(block.body)
|
|
2054
|
+
|| /Thread\.sleep\(|time\.sleep\(|setTimeout\(/.test(block.body)
|
|
2055
|
+
|| /\b(?:password|secret|token|api[_-]?key)\b\s*[:=]\s*['"][^'"]+['"]/i.test(block.body);
|
|
2056
|
+
const passes = assertionCount > 0
|
|
2057
|
+
&& !isDisabledBlock(test.language, block)
|
|
2058
|
+
&& isDescriptiveTestName(block.name)
|
|
2059
|
+
&& !hasCriticalSmell;
|
|
2060
|
+
if (passes) {
|
|
2061
|
+
passing += 1;
|
|
2062
|
+
}
|
|
2063
|
+
}
|
|
2064
|
+
}
|
|
2065
|
+
return passing;
|
|
2066
|
+
}
|
|
1266
2067
|
function summarizeSmells(smells) {
|
|
1267
2068
|
return {
|
|
1268
2069
|
CRITICAL: smells.filter((smell) => smell.severity === 'CRITICAL').length,
|
|
@@ -1292,6 +2093,10 @@ function detectTestLanguage(filePath) {
|
|
|
1292
2093
|
return 'typescript';
|
|
1293
2094
|
if (ext === '.py')
|
|
1294
2095
|
return 'python';
|
|
2096
|
+
if (ext === '.rb')
|
|
2097
|
+
return 'ruby';
|
|
2098
|
+
if (ext === '.go')
|
|
2099
|
+
return 'go';
|
|
1295
2100
|
return 'other';
|
|
1296
2101
|
}
|
|
1297
2102
|
function findMatchingBrace(content, openBraceIndex) {
|
|
@@ -1345,6 +2150,19 @@ function coveragePercent(counter) {
|
|
|
1345
2150
|
return null;
|
|
1346
2151
|
return safePercent(counter.covered, counter.covered + counter.missed);
|
|
1347
2152
|
}
|
|
2153
|
+
function sortCoverageFiles(fileCoverage) {
|
|
2154
|
+
return dedupeBy(fileCoverage, (entry) => entry.file)
|
|
2155
|
+
.sort((a, b) => a.lineCoverage - b.lineCoverage || a.file.localeCompare(b.file));
|
|
2156
|
+
}
|
|
2157
|
+
function buildCoverageFileFields(fileCoverage) {
|
|
2158
|
+
const sorted = sortCoverageFiles(fileCoverage);
|
|
2159
|
+
return {
|
|
2160
|
+
uncoveredFiles: sorted.filter((entry) => entry.lineCoverage === 0).map((entry) => entry.file),
|
|
2161
|
+
lowCoverageFiles: sorted
|
|
2162
|
+
.filter((entry) => entry.lineCoverage < 50)
|
|
2163
|
+
.map((entry) => ({ file: entry.file, lineCoverage: entry.lineCoverage })),
|
|
2164
|
+
};
|
|
2165
|
+
}
|
|
1348
2166
|
function safePercent(covered, total) {
|
|
1349
2167
|
if (!Number.isFinite(total) || total <= 0)
|
|
1350
2168
|
return null;
|
|
@@ -1364,6 +2182,126 @@ function extractXmlText(xml, tag) {
|
|
|
1364
2182
|
var _a, _b;
|
|
1365
2183
|
return (_b = (_a = new RegExp(`<${tag}>([\\s\\S]*?)<\\/${tag}>`).exec(xml)) === null || _a === void 0 ? void 0 : _a[1]) === null || _b === void 0 ? void 0 : _b.trim();
|
|
1366
2184
|
}
|
|
2185
|
+
function sanitizeXmlText(value) {
|
|
2186
|
+
return value
|
|
2187
|
+
.replace(/<!\[CDATA\[([\s\S]*?)\]\]>/g, '$1')
|
|
2188
|
+
.replace(/</g, '<')
|
|
2189
|
+
.replace(/>/g, '>')
|
|
2190
|
+
.trim();
|
|
2191
|
+
}
|
|
2192
|
+
function rankWorstMutationFiles(fileStats) {
|
|
2193
|
+
return Array.from(fileStats.entries())
|
|
2194
|
+
.filter(([, stats]) => stats.total > 0)
|
|
2195
|
+
.map(([file, stats]) => ({
|
|
2196
|
+
file: normalizePath(file),
|
|
2197
|
+
survived: stats.total - stats.killed,
|
|
2198
|
+
killed: stats.killed,
|
|
2199
|
+
}))
|
|
2200
|
+
.sort((a, b) => b.survived - a.survived || a.file.localeCompare(b.file))
|
|
2201
|
+
.slice(0, 5);
|
|
2202
|
+
}
|
|
2203
|
+
function rankWeakestMutationFiles(fileStats) {
|
|
2204
|
+
return Array.from(fileStats.entries())
|
|
2205
|
+
.filter(([, stats]) => stats.total > 0)
|
|
2206
|
+
.sort((a, b) => {
|
|
2207
|
+
const aScore = (a[1].killed / a[1].total) * 100;
|
|
2208
|
+
const bScore = (b[1].killed / b[1].total) * 100;
|
|
2209
|
+
return aScore - bScore || a[0].localeCompare(b[0]);
|
|
2210
|
+
})
|
|
2211
|
+
.slice(0, 5)
|
|
2212
|
+
.map(([file]) => normalizePath(file));
|
|
2213
|
+
}
|
|
2214
|
+
function formatPct(value) {
|
|
2215
|
+
return value === null ? 'n/a' : `${value}%`;
|
|
2216
|
+
}
|
|
2217
|
+
function scoreIcon(score) {
|
|
2218
|
+
return score >= 20 ? '✓' : score > 0 ? '⚠' : '✗';
|
|
2219
|
+
}
|
|
2220
|
+
function describeAssertionCompleteness(ast) {
|
|
2221
|
+
if (ast.testsWithZeroAssertions.length === 0) {
|
|
2222
|
+
return 'all tests have assertions';
|
|
2223
|
+
}
|
|
2224
|
+
return `${ast.testsWithZeroAssertions.length} tests without assertions`;
|
|
2225
|
+
}
|
|
2226
|
+
function describeCriticalSmells(ast) {
|
|
2227
|
+
const parts = [
|
|
2228
|
+
ast.threadSleepUsage.length > 0 ? `${ast.threadSleepUsage.length} sleep usages` : undefined,
|
|
2229
|
+
ast.emptyTestBodies.length > 0 ? `${ast.emptyTestBodies.length} empty bodies` : undefined,
|
|
2230
|
+
ast.hardcodedCredentials.length > 0 ? `${ast.hardcodedCredentials.length} hardcoded credentials` : undefined,
|
|
2231
|
+
].filter((value) => Boolean(value));
|
|
2232
|
+
return parts.length > 0 ? parts.join(', ') : 'no critical smells';
|
|
2233
|
+
}
|
|
2234
|
+
function appendDetectedSmells(lines, ast) {
|
|
2235
|
+
const smellEntries = [
|
|
2236
|
+
...ast.threadSleepUsage.map((entry) => ` ⚠ Thread.sleep()/sleep usage in ${entry}`),
|
|
2237
|
+
...ast.emptyTestBodies.map((entry) => ` ⚠ Empty test body: ${entry}`),
|
|
2238
|
+
...ast.disabledTests.map((entry) => ` ⚠ Disabled test: ${entry}`),
|
|
2239
|
+
];
|
|
2240
|
+
if (smellEntries.length === 0)
|
|
2241
|
+
return;
|
|
2242
|
+
lines.push('');
|
|
2243
|
+
lines.push(`Smells detected (${smellEntries.length}):`);
|
|
2244
|
+
for (const entry of smellEntries.slice(0, 10)) {
|
|
2245
|
+
lines.push(entry);
|
|
2246
|
+
}
|
|
2247
|
+
}
|
|
2248
|
+
function appendLowCoverageFiles(lines, coverage) {
|
|
2249
|
+
if (!coverage || coverage.lowCoverageFiles.length === 0)
|
|
2250
|
+
return;
|
|
2251
|
+
lines.push('');
|
|
2252
|
+
lines.push('Low coverage files (< 50%):');
|
|
2253
|
+
for (const entry of coverage.lowCoverageFiles.slice(0, 10)) {
|
|
2254
|
+
lines.push(` ⚠ ${entry.file}: ${entry.lineCoverage}% line coverage`);
|
|
2255
|
+
}
|
|
2256
|
+
}
|
|
2257
|
+
function suggestMutationCommand(languages) {
|
|
2258
|
+
const lowerLanguages = languages.map((language) => language.toLowerCase());
|
|
2259
|
+
if (lowerLanguages.some((language) => language === 'java' || language === 'kotlin'))
|
|
2260
|
+
return 'gradle pitest';
|
|
2261
|
+
if (lowerLanguages.some((language) => language === 'typescript' || language === 'javascript'))
|
|
2262
|
+
return 'npx stryker run';
|
|
2263
|
+
if (lowerLanguages.includes('python'))
|
|
2264
|
+
return 'mutmut run';
|
|
2265
|
+
if (lowerLanguages.includes('ruby'))
|
|
2266
|
+
return 'configure a mutation report under reports/mutation/';
|
|
2267
|
+
return 'write a supported mutation report under build/reports/pitest or reports/mutation';
|
|
2268
|
+
}
|
|
2269
|
+
function formatContractName(contract, contractPath) {
|
|
2270
|
+
var _a, _b;
|
|
2271
|
+
const consumer = typeof contract['consumer'] === 'object' && contract['consumer'] !== null
|
|
2272
|
+
? String((_a = contract['consumer']['name']) !== null && _a !== void 0 ? _a : 'unknown')
|
|
2273
|
+
: 'unknown';
|
|
2274
|
+
const provider = typeof contract['provider'] === 'object' && contract['provider'] !== null
|
|
2275
|
+
? String((_b = contract['provider']['name']) !== null && _b !== void 0 ? _b : 'unknown')
|
|
2276
|
+
: 'unknown';
|
|
2277
|
+
return consumer !== 'unknown' || provider !== 'unknown'
|
|
2278
|
+
? `${consumer}→${provider}`
|
|
2279
|
+
: normalizePath(contractPath);
|
|
2280
|
+
}
|
|
2281
|
+
function extractContractVerificationStatus(contract) {
|
|
2282
|
+
var _a, _b, _c, _d;
|
|
2283
|
+
const metadata = ((_a = contract['metadata']) !== null && _a !== void 0 ? _a : {});
|
|
2284
|
+
const verification = ((_d = (_c = (_b = contract['verificationResult']) !== null && _b !== void 0 ? _b : contract['verification']) !== null && _c !== void 0 ? _c : metadata['verificationResult']) !== null && _d !== void 0 ? _d : metadata['verification']);
|
|
2285
|
+
if (typeof verification === 'boolean')
|
|
2286
|
+
return verification;
|
|
2287
|
+
if (verification && typeof verification === 'object') {
|
|
2288
|
+
if (typeof verification['success'] === 'boolean')
|
|
2289
|
+
return verification['success'];
|
|
2290
|
+
if (typeof verification['passed'] === 'boolean')
|
|
2291
|
+
return verification['passed'];
|
|
2292
|
+
if (typeof verification['verified'] === 'boolean')
|
|
2293
|
+
return verification['verified'];
|
|
2294
|
+
}
|
|
2295
|
+
if (typeof contract['verified'] === 'boolean')
|
|
2296
|
+
return contract['verified'];
|
|
2297
|
+
return null;
|
|
2298
|
+
}
|
|
2299
|
+
function extractContractFailureReason(contract) {
|
|
2300
|
+
var _a, _b, _c, _d, _e, _f, _g;
|
|
2301
|
+
const metadata = ((_a = contract['metadata']) !== null && _a !== void 0 ? _a : {});
|
|
2302
|
+
const verification = ((_d = (_c = (_b = contract['verificationResult']) !== null && _b !== void 0 ? _b : contract['verification']) !== null && _c !== void 0 ? _c : metadata['verificationResult']) !== null && _d !== void 0 ? _d : metadata['verification']);
|
|
2303
|
+
return String((_g = (_f = (_e = verification === null || verification === void 0 ? void 0 : verification['reason']) !== null && _e !== void 0 ? _e : verification === null || verification === void 0 ? void 0 : verification['message']) !== null && _f !== void 0 ? _f : contract['verificationError']) !== null && _g !== void 0 ? _g : 'verification failed');
|
|
2304
|
+
}
|
|
1367
2305
|
function dedupeBy(items, keyFn) {
|
|
1368
2306
|
const seen = new Set();
|
|
1369
2307
|
const deduped = [];
|
|
@@ -1390,3 +2328,19 @@ function safeRead(filePath) {
|
|
|
1390
2328
|
return '';
|
|
1391
2329
|
}
|
|
1392
2330
|
}
|
|
2331
|
+
function safeReadHead(filePath, maxBytes = 4096) {
|
|
2332
|
+
try {
|
|
2333
|
+
const fd = fs.openSync(filePath, 'r');
|
|
2334
|
+
try {
|
|
2335
|
+
const buffer = Buffer.alloc(maxBytes);
|
|
2336
|
+
const bytesRead = fs.readSync(fd, buffer, 0, maxBytes, 0);
|
|
2337
|
+
return buffer.toString('utf-8', 0, bytesRead);
|
|
2338
|
+
}
|
|
2339
|
+
finally {
|
|
2340
|
+
fs.closeSync(fd);
|
|
2341
|
+
}
|
|
2342
|
+
}
|
|
2343
|
+
catch {
|
|
2344
|
+
return '';
|
|
2345
|
+
}
|
|
2346
|
+
}
|