api-tests-coverage 1.0.23 → 1.0.25
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +39 -0
- package/config.yaml.example +35 -0
- package/dist/dashboard/dist/assets/_basePickBy-BHjg34fk.js +1 -0
- package/dist/dashboard/dist/assets/_basePickBy-D4Hl8chy.js +1 -0
- package/dist/dashboard/dist/assets/_baseUniq-BSUUnV_V.js +1 -0
- package/dist/dashboard/dist/assets/_baseUniq-DxJYHd7T.js +1 -0
- package/dist/dashboard/dist/assets/arc-DcXkmNi0.js +1 -0
- package/dist/dashboard/dist/assets/arc-DhDluTY5.js +1 -0
- package/dist/dashboard/dist/assets/architectureDiagram-VXUJARFQ-ChMY32ql.js +36 -0
- package/dist/dashboard/dist/assets/architectureDiagram-VXUJARFQ-DGlUU7dC.js +36 -0
- package/dist/dashboard/dist/assets/blockDiagram-VD42YOAC-CgXi3kEZ.js +122 -0
- package/dist/dashboard/dist/assets/blockDiagram-VD42YOAC-DVhWtRxG.js +122 -0
- package/dist/dashboard/dist/assets/c4Diagram-YG6GDRKO-B6esYq70.js +10 -0
- package/dist/dashboard/dist/assets/c4Diagram-YG6GDRKO-Cr3xB15y.js +10 -0
- package/dist/dashboard/dist/assets/channel-DYAie-7m.js +1 -0
- package/dist/dashboard/dist/assets/channel-Df6s6dhy.js +1 -0
- package/dist/dashboard/dist/assets/chunk-4BX2VUAB-B7Pkx3C3.js +1 -0
- package/dist/dashboard/dist/assets/chunk-4BX2VUAB-BaW3__pI.js +1 -0
- package/dist/dashboard/dist/assets/chunk-55IACEB6-8ClDkPsD.js +1 -0
- package/dist/dashboard/dist/assets/chunk-55IACEB6-DyYevfEQ.js +1 -0
- package/dist/dashboard/dist/assets/chunk-B4BG7PRW--cjprmFF.js +165 -0
- package/dist/dashboard/dist/assets/chunk-B4BG7PRW-C2bwZFec.js +165 -0
- package/dist/dashboard/dist/assets/chunk-DI55MBZ5-D9bxNSnS.js +220 -0
- package/dist/dashboard/dist/assets/chunk-DI55MBZ5-DO0T2xne.js +220 -0
- package/dist/dashboard/dist/assets/chunk-FMBD7UC4-CCYA4j_f.js +15 -0
- package/dist/dashboard/dist/assets/chunk-FMBD7UC4-CSek7h3u.js +15 -0
- package/dist/dashboard/dist/assets/chunk-QN33PNHL-BRCzcTtl.js +1 -0
- package/dist/dashboard/dist/assets/chunk-QN33PNHL-Cdhqs7xo.js +1 -0
- package/dist/dashboard/dist/assets/chunk-QZHKN3VN-BzHw38Ki.js +1 -0
- package/dist/dashboard/dist/assets/chunk-QZHKN3VN-TFdw1-iS.js +1 -0
- package/dist/dashboard/dist/assets/chunk-TZMSLE5B-CWotsEVz.js +1 -0
- package/dist/dashboard/dist/assets/chunk-TZMSLE5B-dkJ0rsgF.js +1 -0
- package/dist/dashboard/dist/assets/classDiagram-2ON5EDUG-DiIv5Pho.js +1 -0
- package/dist/dashboard/dist/assets/classDiagram-2ON5EDUG-Dr8j2BkV.js +1 -0
- package/dist/dashboard/dist/assets/classDiagram-v2-WZHVMYZB-DiIv5Pho.js +1 -0
- package/dist/dashboard/dist/assets/classDiagram-v2-WZHVMYZB-Dr8j2BkV.js +1 -0
- package/dist/dashboard/dist/assets/clone-B4LorrSy.js +1 -0
- package/dist/dashboard/dist/assets/clone-D-A0zWrx.js +1 -0
- package/dist/dashboard/dist/assets/cose-bilkent-S5V4N54A-L06bC_vI.js +1 -0
- package/dist/dashboard/dist/assets/cose-bilkent-S5V4N54A-jzGbyPIS.js +1 -0
- package/dist/dashboard/dist/assets/dagre-6UL2VRFP-D7rgvBx1.js +4 -0
- package/dist/dashboard/dist/assets/dagre-6UL2VRFP-LQJxsDjp.js +4 -0
- package/dist/dashboard/dist/assets/diagram-PSM6KHXK-2rYklqon.js +24 -0
- package/dist/dashboard/dist/assets/diagram-PSM6KHXK-Bguvtjhb.js +24 -0
- package/dist/dashboard/dist/assets/diagram-QEK2KX5R-CDM-bAUc.js +43 -0
- package/dist/dashboard/dist/assets/diagram-QEK2KX5R-CGrvALqm.js +43 -0
- package/dist/dashboard/dist/assets/diagram-S2PKOQOG-DA3c-QP4.js +24 -0
- package/dist/dashboard/dist/assets/diagram-S2PKOQOG-DNQuKOCA.js +24 -0
- package/dist/dashboard/dist/assets/erDiagram-Q2GNP2WA-BsYH8cLH.js +60 -0
- package/dist/dashboard/dist/assets/erDiagram-Q2GNP2WA-CgAEujxC.js +60 -0
- package/dist/dashboard/dist/assets/flowDiagram-NV44I4VS-C8juupCT.js +162 -0
- package/dist/dashboard/dist/assets/flowDiagram-NV44I4VS-Da_JhBCy.js +162 -0
- package/dist/dashboard/dist/assets/ganttDiagram-JELNMOA3-BzXOAiOm.js +267 -0
- package/dist/dashboard/dist/assets/ganttDiagram-JELNMOA3-D8FTswNn.js +267 -0
- package/dist/dashboard/dist/assets/gitGraphDiagram-V2S2FVAM-6Rn0oWgA.js +65 -0
- package/dist/dashboard/dist/assets/gitGraphDiagram-V2S2FVAM-BFJR-ITH.js +65 -0
- package/dist/dashboard/dist/assets/graph-CIvnjOQQ.js +1 -0
- package/dist/dashboard/dist/assets/graph-VO6A5Zyb.js +1 -0
- package/dist/dashboard/dist/assets/index-BD_Ue7zI.js +777 -0
- package/dist/dashboard/dist/assets/index-BWX0sSZn.css +1 -0
- package/dist/dashboard/dist/assets/index-CbAFWEor.js +777 -0
- package/dist/dashboard/dist/assets/infoDiagram-HS3SLOUP-BEOgUULT.js +2 -0
- package/dist/dashboard/dist/assets/infoDiagram-HS3SLOUP-OcK0Lxgi.js +2 -0
- package/dist/dashboard/dist/assets/journeyDiagram-XKPGCS4Q-CBFUW_L2.js +139 -0
- package/dist/dashboard/dist/assets/journeyDiagram-XKPGCS4Q-DTJukVOY.js +139 -0
- package/dist/dashboard/dist/assets/kanban-definition-3W4ZIXB7-BXpodEnf.js +89 -0
- package/dist/dashboard/dist/assets/kanban-definition-3W4ZIXB7-Di65fNuD.js +89 -0
- package/dist/dashboard/dist/assets/layout-Cpj8l95P.js +1 -0
- package/dist/dashboard/dist/assets/layout-DAt24RVX.js +1 -0
- package/dist/dashboard/dist/assets/mindmap-definition-VGOIOE7T-DxI8MXCF.js +68 -0
- package/dist/dashboard/dist/assets/mindmap-definition-VGOIOE7T-_3DZbNEl.js +68 -0
- package/dist/dashboard/dist/assets/pieDiagram-ADFJNKIX-B--OM1Gs.js +30 -0
- package/dist/dashboard/dist/assets/pieDiagram-ADFJNKIX-BafKx3_Y.js +30 -0
- package/dist/dashboard/dist/assets/quadrantDiagram-AYHSOK5B-BcZsArkk.js +7 -0
- package/dist/dashboard/dist/assets/quadrantDiagram-AYHSOK5B-CDx0v76p.js +7 -0
- package/dist/dashboard/dist/assets/requirementDiagram-UZGBJVZJ-CbvZ1a-7.js +64 -0
- package/dist/dashboard/dist/assets/requirementDiagram-UZGBJVZJ-CqFAO2t6.js +64 -0
- package/dist/dashboard/dist/assets/sankeyDiagram-TZEHDZUN-CqSaCg-3.js +10 -0
- package/dist/dashboard/dist/assets/sankeyDiagram-TZEHDZUN-D-fji9s3.js +10 -0
- package/dist/dashboard/dist/assets/sequenceDiagram-WL72ISMW-6IXD1uqW.js +145 -0
- package/dist/dashboard/dist/assets/sequenceDiagram-WL72ISMW-CWB1Ub2x.js +145 -0
- package/dist/dashboard/dist/assets/stateDiagram-FKZM4ZOC-DvSVQAfp.js +1 -0
- package/dist/dashboard/dist/assets/stateDiagram-FKZM4ZOC-J-c1KNJ7.js +1 -0
- package/dist/dashboard/dist/assets/stateDiagram-v2-4FDKWEC3-BMFdt0QQ.js +1 -0
- package/dist/dashboard/dist/assets/stateDiagram-v2-4FDKWEC3-DRL2jF9p.js +1 -0
- package/dist/dashboard/dist/assets/timeline-definition-IT6M3QCI-Cll7Nvth.js +61 -0
- package/dist/dashboard/dist/assets/timeline-definition-IT6M3QCI-LOxOovzx.js +61 -0
- package/dist/dashboard/dist/assets/treemap-GDKQZRPO-C6DntuKu.js +162 -0
- package/dist/dashboard/dist/assets/treemap-GDKQZRPO-DtqX8zNC.js +162 -0
- package/dist/dashboard/dist/assets/xychartDiagram-PRI3JC2R-BKisDUaz.js +7 -0
- package/dist/dashboard/dist/assets/xychartDiagram-PRI3JC2R-zxwS9i0A.js +7 -0
- package/dist/dashboard/dist/index.html +2 -2
- package/dist/dashboard/dist/reports/coverage-summary.json +75 -1
- package/dist/dashboard/dist/reports/security-full.json +157 -0
- package/dist/src/compatibilityCoverage.d.ts +34 -15
- package/dist/src/compatibilityCoverage.d.ts.map +1 -1
- package/dist/src/compatibilityCoverage.js +387 -85
- package/dist/src/config/defaultConfig.d.ts.map +1 -1
- package/dist/src/config/defaultConfig.js +62 -0
- package/dist/src/config/schema.d.ts.map +1 -1
- package/dist/src/config/schema.js +1 -1
- package/dist/src/config/types.d.ts +81 -1
- package/dist/src/config/types.d.ts.map +1 -1
- package/dist/src/config/validateConfig.d.ts.map +1 -1
- package/dist/src/config/validateConfig.js +126 -0
- package/dist/src/contracts/compatibilityMatrix.d.ts +20 -0
- package/dist/src/contracts/compatibilityMatrix.d.ts.map +1 -0
- package/dist/src/contracts/compatibilityMatrix.js +198 -0
- package/dist/src/contracts/pactBrokerClient.d.ts +10 -0
- package/dist/src/contracts/pactBrokerClient.d.ts.map +1 -0
- package/dist/src/contracts/pactBrokerClient.js +117 -0
- package/dist/src/contracts/schemaEvolutionChecker.d.ts +17 -0
- package/dist/src/contracts/schemaEvolutionChecker.d.ts.map +1 -0
- package/dist/src/contracts/schemaEvolutionChecker.js +95 -0
- package/dist/src/contracts/springCloudContractParser.d.ts +10 -0
- package/dist/src/contracts/springCloudContractParser.d.ts.map +1 -0
- package/dist/src/contracts/springCloudContractParser.js +144 -0
- package/dist/src/discovery/fileClassifier.d.ts.map +1 -1
- package/dist/src/discovery/fileClassifier.js +25 -0
- package/dist/src/discovery/projectDiscovery.d.ts +2 -0
- package/dist/src/discovery/projectDiscovery.d.ts.map +1 -1
- package/dist/src/discovery/projectDiscovery.js +25 -25
- package/dist/src/index.js +233 -16
- package/dist/src/inference/routeInference.d.ts +10 -2
- package/dist/src/inference/routeInference.d.ts.map +1 -1
- package/dist/src/inference/routeInference.js +363 -62
- package/dist/src/languageDetection.d.ts.map +1 -1
- package/dist/src/languageDetection.js +21 -4
- package/dist/src/lib/index.d.ts +3 -0
- package/dist/src/lib/index.d.ts.map +1 -1
- package/dist/src/lib/index.js +3 -1
- package/dist/src/pipeline/stages/tia/parameterizedTestExpander.js +152 -79
- package/dist/src/pipeline/stages/tia/testEndpointMapper.d.ts +5 -1
- package/dist/src/pipeline/stages/tia/testEndpointMapper.d.ts.map +1 -1
- package/dist/src/pipeline/stages/tia/testEndpointMapper.js +356 -42
- package/dist/src/pipeline/stages/tia/testLayerClassifier.d.ts.map +1 -1
- package/dist/src/pipeline/stages/tia/testLayerClassifier.js +20 -5
- package/dist/src/pipeline/stages/tia/tiaStage.d.ts.map +1 -1
- package/dist/src/pipeline/stages/tia/tiaStage.js +3 -1
- package/dist/src/pipeline/stages/tia/types.d.ts +11 -2
- package/dist/src/pipeline/stages/tia/types.d.ts.map +1 -1
- package/dist/src/projectDefaults.d.ts +6 -0
- package/dist/src/projectDefaults.d.ts.map +1 -0
- package/dist/src/projectDefaults.js +43 -0
- package/dist/src/security/hub.d.ts +81 -0
- package/dist/src/security/hub.d.ts.map +1 -0
- package/dist/src/security/hub.js +420 -0
- package/dist/src/security/index.d.ts +1 -0
- package/dist/src/security/index.d.ts.map +1 -1
- package/dist/src/security/index.js +8 -2
- package/dist/src/security/normalizers/gitleaks.d.ts +7 -0
- package/dist/src/security/normalizers/gitleaks.d.ts.map +1 -0
- package/dist/src/security/normalizers/gitleaks.js +32 -0
- package/dist/src/security/scanners/gitleaks.d.ts +3 -0
- package/dist/src/security/scanners/gitleaks.d.ts.map +1 -0
- package/dist/src/security/scanners/gitleaks.js +105 -0
- package/dist/src/security/scanners/semgrep.d.ts.map +1 -1
- package/dist/src/security/scanners/semgrep.js +24 -2
- package/dist/src/security/scanners/trivy.d.ts.map +1 -1
- package/dist/src/security/scanners/trivy.js +24 -2
- package/dist/src/security/scanners/zap.d.ts.map +1 -1
- package/dist/src/security/scanners/zap.js +27 -2
- package/dist/src/security/types.d.ts +15 -1
- package/dist/src/security/types.d.ts.map +1 -1
- package/dist/src/streaming/schema/index.d.ts +23 -0
- package/dist/src/streaming/schema/index.d.ts.map +1 -0
- package/dist/src/streaming/schema/index.js +196 -0
- package/dist/src/summary/markdownRenderer.d.ts.map +1 -1
- package/dist/src/summary/markdownRenderer.js +15 -1
- package/dist/src/summary/summaryTypes.d.ts.map +1 -1
- package/dist/src/summary/summaryTypes.js +1 -0
- package/dist/src/unitAnalysis.d.ts +145 -0
- package/dist/src/unitAnalysis.d.ts.map +1 -0
- package/dist/src/unitAnalysis.js +1392 -0
- package/package.json +1 -1
|
@@ -0,0 +1,1392 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.analyzeUnitTests = analyzeUnitTests;
|
|
40
|
+
exports.parseJacocoXml = parseJacocoXml;
|
|
41
|
+
exports.parseIstanbulSummary = parseIstanbulSummary;
|
|
42
|
+
exports.parseLcovInfo = parseLcovInfo;
|
|
43
|
+
exports.parseCoverageXml = parseCoverageXml;
|
|
44
|
+
exports.parseGoCoverage = parseGoCoverage;
|
|
45
|
+
exports.parseStrykerReport = parseStrykerReport;
|
|
46
|
+
exports.parseSlowTests = parseSlowTests;
|
|
47
|
+
exports.detectTestSmells = detectTestSmells;
|
|
48
|
+
const fs = __importStar(require("fs"));
|
|
49
|
+
const path = __importStar(require("path"));
|
|
50
|
+
const child_process_1 = require("child_process");
|
|
51
|
+
const fast_glob_1 = __importDefault(require("fast-glob"));
|
|
52
|
+
const COVERAGE_REPORT_CANDIDATES = [
|
|
53
|
+
'target/site/jacoco/jacoco.xml',
|
|
54
|
+
'build/reports/jacoco/test/jacocoTestReport.xml',
|
|
55
|
+
'coverage/coverage-summary.json',
|
|
56
|
+
'coverage/lcov.info',
|
|
57
|
+
'coverage.xml',
|
|
58
|
+
'.coverage',
|
|
59
|
+
'coverage.out',
|
|
60
|
+
];
|
|
61
|
+
const TEST_RESULT_GLOBS = [
|
|
62
|
+
'target/surefire-reports/*.xml',
|
|
63
|
+
'build/test-results/**/*.xml',
|
|
64
|
+
'reports/junit*.xml',
|
|
65
|
+
'junit*.xml',
|
|
66
|
+
'results.xml',
|
|
67
|
+
'jest-results.json',
|
|
68
|
+
'reports/jest-results.json',
|
|
69
|
+
];
|
|
70
|
+
const PIT_REPORT_GLOBS = [
|
|
71
|
+
'target/pit-reports/**/mutations.xml',
|
|
72
|
+
'build/reports/pitest/**/mutations.xml',
|
|
73
|
+
];
|
|
74
|
+
const STRYKER_REPORT_GLOBS = [
|
|
75
|
+
'reports/mutation/mutation-report.json',
|
|
76
|
+
'reports/stryker/mutation-report.json',
|
|
77
|
+
'StrykerOutput/**/mutation-report.json',
|
|
78
|
+
];
|
|
79
|
+
async function analyzeUnitTests(options) {
|
|
80
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o;
|
|
81
|
+
const rootDir = path.resolve(options.rootDir);
|
|
82
|
+
const unitConfig = options.config;
|
|
83
|
+
const testAnalyses = options.testFiles.map((filePath) => analyzeTestFile(rootDir, filePath));
|
|
84
|
+
const sourceMethods = options.serviceFiles.flatMap((filePath) => discoverSourceMethods(rootDir, filePath));
|
|
85
|
+
const unitItems = buildUnitCoverageItems(sourceMethods, testAnalyses);
|
|
86
|
+
const assertionDensity = computeAssertionDensity(testAnalyses);
|
|
87
|
+
const smells = ((_a = unitConfig.smellDetection) === null || _a === void 0 ? void 0 : _a.enabled) !== false
|
|
88
|
+
? detectTestSmells(rootDir, testAnalyses)
|
|
89
|
+
: [];
|
|
90
|
+
const slowTests = parseSlowTests(rootDir, unitConfig);
|
|
91
|
+
const independence = ((_b = unitConfig.independenceCheck) === null || _b === void 0 ? void 0 : _b.enabled) !== false
|
|
92
|
+
? computeIndependence(rootDir, testAnalyses)
|
|
93
|
+
: null;
|
|
94
|
+
const coverageDiscovery = ((_c = unitConfig.codeCoverage) === null || _c === void 0 ? void 0 : _c.enabled) !== false
|
|
95
|
+
? discoverCoverage(rootDir)
|
|
96
|
+
: null;
|
|
97
|
+
const mutationTesting = ((_d = unitConfig.mutationTesting) === null || _d === void 0 ? void 0 : _d.enabled)
|
|
98
|
+
? runMutationTesting(rootDir, (_e = options.languages) !== null && _e !== void 0 ? _e : [], unitConfig)
|
|
99
|
+
: null;
|
|
100
|
+
const failures = [];
|
|
101
|
+
const warnings = [];
|
|
102
|
+
const messages = [];
|
|
103
|
+
if (coverageDiscovery) {
|
|
104
|
+
if (coverageDiscovery.summary) {
|
|
105
|
+
const coverageThresholds = (_f = unitConfig.codeCoverage) === null || _f === void 0 ? void 0 : _f.thresholds;
|
|
106
|
+
if ((coverageThresholds === null || coverageThresholds === void 0 ? void 0 : coverageThresholds.line) !== undefined && coverageDiscovery.summary.lineCoverage !== null && coverageDiscovery.summary.lineCoverage < coverageThresholds.line) {
|
|
107
|
+
failures.push(`Unit line coverage ${coverageDiscovery.summary.lineCoverage.toFixed(1)}% is below threshold ${coverageThresholds.line}%`);
|
|
108
|
+
}
|
|
109
|
+
if ((coverageThresholds === null || coverageThresholds === void 0 ? void 0 : coverageThresholds.branch) !== undefined && coverageDiscovery.summary.branchCoverage !== null && coverageDiscovery.summary.branchCoverage < coverageThresholds.branch) {
|
|
110
|
+
failures.push(`Unit branch coverage ${coverageDiscovery.summary.branchCoverage.toFixed(1)}% is below threshold ${coverageThresholds.branch}%`);
|
|
111
|
+
}
|
|
112
|
+
if ((coverageThresholds === null || coverageThresholds === void 0 ? void 0 : coverageThresholds.method) !== undefined && coverageDiscovery.summary.methodCoverage !== null && coverageDiscovery.summary.methodCoverage < coverageThresholds.method) {
|
|
113
|
+
failures.push(`Unit method coverage ${coverageDiscovery.summary.methodCoverage.toFixed(1)}% is below threshold ${coverageThresholds.method}%`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
else if (coverageDiscovery.missing) {
|
|
117
|
+
messages.push(coverageDiscovery.missing.message);
|
|
118
|
+
messages.push(coverageDiscovery.missing.hint);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
if (mutationTesting === null || mutationTesting === void 0 ? void 0 : mutationTesting.summary) {
|
|
122
|
+
const threshold = (_h = (_g = unitConfig.mutationTesting) === null || _g === void 0 ? void 0 : _g.threshold) !== null && _h !== void 0 ? _h : 70;
|
|
123
|
+
if (mutationTesting.summary.mutationScore < threshold) {
|
|
124
|
+
failures.push(`Mutation score ${mutationTesting.summary.mutationScore.toFixed(1)}% is below threshold ${threshold}%`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
if (mutationTesting === null || mutationTesting === void 0 ? void 0 : mutationTesting.warning) {
|
|
128
|
+
warnings.push(mutationTesting.warning);
|
|
129
|
+
}
|
|
130
|
+
const smellFailOn = (_k = (_j = unitConfig.smellDetection) === null || _j === void 0 ? void 0 : _j.failOn) !== null && _k !== void 0 ? _k : 'CRITICAL';
|
|
131
|
+
if (smellFailOn !== 'none') {
|
|
132
|
+
const hasBlockingSmell = smells.some((smell) => severityAtOrAbove(smell.severity, smellFailOn));
|
|
133
|
+
if (hasBlockingSmell) {
|
|
134
|
+
failures.push(`Detected test smells at or above ${smellFailOn} severity`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
if (independence && ((_l = unitConfig.independenceCheck) === null || _l === void 0 ? void 0 : _l.minScore) !== undefined && independence.score < unitConfig.independenceCheck.minScore) {
|
|
138
|
+
failures.push(`Test independence score ${independence.score} is below minimum ${unitConfig.independenceCheck.minScore}`);
|
|
139
|
+
}
|
|
140
|
+
const details = {
|
|
141
|
+
items: unitItems,
|
|
142
|
+
codeCoverage: (_m = coverageDiscovery === null || coverageDiscovery === void 0 ? void 0 : coverageDiscovery.summary) !== null && _m !== void 0 ? _m : null,
|
|
143
|
+
codeCoverageMissing: coverageDiscovery === null || coverageDiscovery === void 0 ? void 0 : coverageDiscovery.missing,
|
|
144
|
+
mutationTesting: (_o = mutationTesting === null || mutationTesting === void 0 ? void 0 : mutationTesting.summary) !== null && _o !== void 0 ? _o : null,
|
|
145
|
+
testSmells: {
|
|
146
|
+
bySeverity: summarizeSmells(smells),
|
|
147
|
+
items: smells,
|
|
148
|
+
},
|
|
149
|
+
slowTests,
|
|
150
|
+
independence,
|
|
151
|
+
assertionDensity,
|
|
152
|
+
messages,
|
|
153
|
+
};
|
|
154
|
+
return {
|
|
155
|
+
result: {
|
|
156
|
+
type: 'unit',
|
|
157
|
+
totalItems: unitItems.length,
|
|
158
|
+
coveredItems: unitItems.filter((item) => item.covered).length,
|
|
159
|
+
coveragePercent: unitItems.length > 0
|
|
160
|
+
? Number(((unitItems.filter((item) => item.covered).length / unitItems.length) * 100).toFixed(1))
|
|
161
|
+
: 0,
|
|
162
|
+
details,
|
|
163
|
+
},
|
|
164
|
+
failures,
|
|
165
|
+
warnings,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
function analyzeTestFile(rootDir, filePath) {
|
|
169
|
+
const content = safeRead(filePath);
|
|
170
|
+
const language = detectTestLanguage(filePath);
|
|
171
|
+
const blocks = extractTestBlocks(language, content);
|
|
172
|
+
const assertionCount = countAssertions(language, content);
|
|
173
|
+
return {
|
|
174
|
+
filePath,
|
|
175
|
+
relativePath: normalizePath(path.relative(rootDir, filePath)),
|
|
176
|
+
language,
|
|
177
|
+
content,
|
|
178
|
+
qualityScore: scoreTestQuality(content),
|
|
179
|
+
parameterizedExpansions: countParameterizedExpansions(content),
|
|
180
|
+
assertionCount,
|
|
181
|
+
mockLibrary: detectMockLibrary(content),
|
|
182
|
+
layer: classifyLayer(content, filePath),
|
|
183
|
+
coveredMethods: extractCoveredMethods(language, content),
|
|
184
|
+
instantiatedClasses: extractInstantiatedClasses(content),
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
function discoverSourceMethods(rootDir, filePath) {
|
|
188
|
+
const content = safeRead(filePath);
|
|
189
|
+
const language = detectTestLanguage(filePath);
|
|
190
|
+
const relative = normalizePath(path.relative(rootDir, filePath));
|
|
191
|
+
if (language === 'java' || language === 'kotlin') {
|
|
192
|
+
const className = path.basename(filePath, path.extname(filePath));
|
|
193
|
+
const regex = /(?:public|protected|private|internal)?\s*(?:suspend\s+)?(?:async\s+)?(?:static\s+)?(?:final\s+)?(?:[\w<>\[\],?]+\s+)+([a-zA-Z_]\w*)\s*\([^;{]*\)\s*\{/g;
|
|
194
|
+
const methods = [];
|
|
195
|
+
let match;
|
|
196
|
+
while ((match = regex.exec(content)) !== null) {
|
|
197
|
+
const methodName = match[1];
|
|
198
|
+
if (methodName === className)
|
|
199
|
+
continue;
|
|
200
|
+
methods.push({
|
|
201
|
+
id: `${className}.${methodName}`,
|
|
202
|
+
className,
|
|
203
|
+
methodName,
|
|
204
|
+
file: relative,
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
return dedupeBy(methods, (method) => method.id);
|
|
208
|
+
}
|
|
209
|
+
if (language === 'typescript' || language === 'javascript') {
|
|
210
|
+
const className = path.basename(filePath, path.extname(filePath));
|
|
211
|
+
const methods = [];
|
|
212
|
+
const classMethodRegex = /(?:public|protected|private|async|static|get|set|readonly|#)?\s*([a-zA-Z_]\w*)\s*\([^;{]*\)\s*\{/g;
|
|
213
|
+
let match;
|
|
214
|
+
while ((match = classMethodRegex.exec(content)) !== null) {
|
|
215
|
+
const methodName = match[1];
|
|
216
|
+
if (['if', 'for', 'while', 'switch', 'catch', 'constructor'].includes(methodName))
|
|
217
|
+
continue;
|
|
218
|
+
methods.push({ id: `${className}.${methodName}`, className, methodName, file: relative });
|
|
219
|
+
}
|
|
220
|
+
const exportedFunctionRegex = /export\s+(?:async\s+)?function\s+([a-zA-Z_]\w*)\s*\(/g;
|
|
221
|
+
while ((match = exportedFunctionRegex.exec(content)) !== null) {
|
|
222
|
+
const methodName = match[1];
|
|
223
|
+
methods.push({ id: `${className}.${methodName}`, className, methodName, file: relative });
|
|
224
|
+
}
|
|
225
|
+
return dedupeBy(methods, (method) => method.id);
|
|
226
|
+
}
|
|
227
|
+
if (language === 'python') {
|
|
228
|
+
const className = path.basename(filePath, path.extname(filePath));
|
|
229
|
+
const methods = [];
|
|
230
|
+
const regex = /^\s*def\s+([a-zA-Z_]\w*)\s*\(/gm;
|
|
231
|
+
let match;
|
|
232
|
+
while ((match = regex.exec(content)) !== null) {
|
|
233
|
+
const methodName = match[1];
|
|
234
|
+
if (methodName.startsWith('_'))
|
|
235
|
+
continue;
|
|
236
|
+
methods.push({ id: `${className}.${methodName}`, className, methodName, file: relative });
|
|
237
|
+
}
|
|
238
|
+
return dedupeBy(methods, (method) => method.id);
|
|
239
|
+
}
|
|
240
|
+
return [];
|
|
241
|
+
}
|
|
242
|
+
function buildUnitCoverageItems(sourceMethods, testAnalyses) {
|
|
243
|
+
var _a, _b, _c, _d;
|
|
244
|
+
const byMethod = new Map();
|
|
245
|
+
for (const method of sourceMethods) {
|
|
246
|
+
byMethod.set(method.id, {
|
|
247
|
+
id: method.id,
|
|
248
|
+
covered: false,
|
|
249
|
+
tests: [],
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
for (const test of testAnalyses) {
|
|
253
|
+
for (const methodId of test.coveredMethods) {
|
|
254
|
+
const existing = (_a = byMethod.get(methodId)) !== null && _a !== void 0 ? _a : {
|
|
255
|
+
id: methodId,
|
|
256
|
+
covered: false,
|
|
257
|
+
tests: [],
|
|
258
|
+
};
|
|
259
|
+
const next = {
|
|
260
|
+
...existing,
|
|
261
|
+
covered: true,
|
|
262
|
+
tests: dedupeStrings([...existing.tests, path.basename(test.relativePath)]),
|
|
263
|
+
layer: test.layer,
|
|
264
|
+
mockLibrary: test.mockLibrary,
|
|
265
|
+
qualityScore: Math.max((_b = existing.qualityScore) !== null && _b !== void 0 ? _b : 0, test.qualityScore),
|
|
266
|
+
parameterizedExpansions: ((_c = existing.parameterizedExpansions) !== null && _c !== void 0 ? _c : 0) + test.parameterizedExpansions,
|
|
267
|
+
assertionCount: Math.max((_d = existing.assertionCount) !== null && _d !== void 0 ? _d : 0, test.assertionCount),
|
|
268
|
+
};
|
|
269
|
+
byMethod.set(methodId, next);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
return Array.from(byMethod.values()).sort((a, b) => a.id.localeCompare(b.id));
|
|
273
|
+
}
|
|
274
|
+
function discoverCoverage(rootDir) {
|
|
275
|
+
const checkedPaths = COVERAGE_REPORT_CANDIDATES.map((candidate) => normalizePath(candidate));
|
|
276
|
+
for (const candidate of COVERAGE_REPORT_CANDIDATES) {
|
|
277
|
+
const fullPath = path.join(rootDir, candidate);
|
|
278
|
+
if (!fs.existsSync(fullPath))
|
|
279
|
+
continue;
|
|
280
|
+
if (candidate.endsWith('jacoco.xml') || candidate.endsWith('jacocoTestReport.xml')) {
|
|
281
|
+
return { summary: parseJacocoXml(fullPath) };
|
|
282
|
+
}
|
|
283
|
+
if (candidate.endsWith('coverage-summary.json')) {
|
|
284
|
+
return { summary: parseIstanbulSummary(fullPath) };
|
|
285
|
+
}
|
|
286
|
+
if (candidate.endsWith('lcov.info')) {
|
|
287
|
+
return { summary: parseLcovInfo(fullPath) };
|
|
288
|
+
}
|
|
289
|
+
if (candidate === 'coverage.xml') {
|
|
290
|
+
return { summary: parseCoverageXml(fullPath) };
|
|
291
|
+
}
|
|
292
|
+
if (candidate === 'coverage.out') {
|
|
293
|
+
return { summary: parseGoCoverage(fullPath) };
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
return {
|
|
297
|
+
summary: null,
|
|
298
|
+
missing: {
|
|
299
|
+
message: 'Run tests with coverage enabled to see report',
|
|
300
|
+
hint: 'Add jacoco plugin to build.gradle',
|
|
301
|
+
checkedPaths,
|
|
302
|
+
},
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
function parseJacocoXml(reportPath) {
|
|
306
|
+
var _a;
|
|
307
|
+
const xml = safeRead(reportPath);
|
|
308
|
+
const counters = extractCounters(xml);
|
|
309
|
+
const uncoveredMethods = [];
|
|
310
|
+
const classRegex = /<class\b([^>]*?)name="([^"]+)"([^>]*?)sourcefilename="([^"]+)"([^>]*)>([\s\S]*?)<\/class>/g;
|
|
311
|
+
let classMatch;
|
|
312
|
+
while ((classMatch = classRegex.exec(xml)) !== null) {
|
|
313
|
+
const className = (_a = classMatch[2].split('/').pop()) !== null && _a !== void 0 ? _a : classMatch[2];
|
|
314
|
+
const file = classMatch[4];
|
|
315
|
+
const classBody = classMatch[6];
|
|
316
|
+
const methodRegex = /<method\b[^>]*name="([^"]+)"[^>]*line="(\d+)"[^>]*>([\s\S]*?)<\/method>/g;
|
|
317
|
+
let methodMatch;
|
|
318
|
+
while ((methodMatch = methodRegex.exec(classBody)) !== null) {
|
|
319
|
+
const methodName = methodMatch[1];
|
|
320
|
+
const line = Number(methodMatch[2]);
|
|
321
|
+
const body = methodMatch[3];
|
|
322
|
+
const lineCounter = /<counter\b[^>]*type="LINE"[^>]*missed="(\d+)"[^>]*covered="(\d+)"/.exec(body);
|
|
323
|
+
if (lineCounter && Number(lineCounter[1]) > 0) {
|
|
324
|
+
uncoveredMethods.push({ class: className, method: methodName, file, line });
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
return {
|
|
329
|
+
tool: 'jacoco',
|
|
330
|
+
lineCoverage: coveragePercent(counters.LINE),
|
|
331
|
+
branchCoverage: coveragePercent(counters.BRANCH),
|
|
332
|
+
methodCoverage: coveragePercent(counters.METHOD),
|
|
333
|
+
classCoverage: coveragePercent(counters.CLASS),
|
|
334
|
+
uncoveredMethods,
|
|
335
|
+
reportPath: normalizePath(reportPath),
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
function parseIstanbulSummary(reportPath) {
|
|
339
|
+
var _a;
|
|
340
|
+
const json = JSON.parse(safeRead(reportPath));
|
|
341
|
+
const total = ((_a = json['total']) !== null && _a !== void 0 ? _a : {});
|
|
342
|
+
return {
|
|
343
|
+
tool: 'istanbul',
|
|
344
|
+
lineCoverage: readPct(total, 'lines'),
|
|
345
|
+
branchCoverage: readPct(total, 'branches'),
|
|
346
|
+
methodCoverage: readPct(total, 'functions'),
|
|
347
|
+
classCoverage: readPct(total, 'statements'),
|
|
348
|
+
uncoveredMethods: [],
|
|
349
|
+
reportPath: normalizePath(reportPath),
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
function parseLcovInfo(reportPath) {
|
|
353
|
+
var _a, _b;
|
|
354
|
+
const content = safeRead(reportPath);
|
|
355
|
+
const files = content.split('end_of_record').map((chunk) => chunk.trim()).filter(Boolean);
|
|
356
|
+
let totalLines = 0;
|
|
357
|
+
let hitLines = 0;
|
|
358
|
+
let totalBranches = 0;
|
|
359
|
+
let hitBranches = 0;
|
|
360
|
+
let totalFunctions = 0;
|
|
361
|
+
let hitFunctions = 0;
|
|
362
|
+
let coveredFiles = 0;
|
|
363
|
+
const uncoveredMethods = [];
|
|
364
|
+
for (const fileChunk of files) {
|
|
365
|
+
const lines = fileChunk.split('\n');
|
|
366
|
+
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
|
+
const fnMap = new Map();
|
|
368
|
+
let fileCovered = false;
|
|
369
|
+
for (const line of lines) {
|
|
370
|
+
if (line.startsWith('LF:'))
|
|
371
|
+
totalLines += Number(line.slice(3));
|
|
372
|
+
if (line.startsWith('LH:')) {
|
|
373
|
+
const hits = Number(line.slice(3));
|
|
374
|
+
hitLines += hits;
|
|
375
|
+
if (hits > 0)
|
|
376
|
+
fileCovered = true;
|
|
377
|
+
}
|
|
378
|
+
if (line.startsWith('BRF:'))
|
|
379
|
+
totalBranches += Number(line.slice(4));
|
|
380
|
+
if (line.startsWith('BRH:'))
|
|
381
|
+
hitBranches += Number(line.slice(4));
|
|
382
|
+
if (line.startsWith('FNF:'))
|
|
383
|
+
totalFunctions += Number(line.slice(4));
|
|
384
|
+
if (line.startsWith('FNH:'))
|
|
385
|
+
hitFunctions += Number(line.slice(4));
|
|
386
|
+
if (line.startsWith('FN:')) {
|
|
387
|
+
const [lineNo, name] = line.slice(3).split(',');
|
|
388
|
+
fnMap.set(name, { line: Number(lineNo), hits: 0 });
|
|
389
|
+
}
|
|
390
|
+
if (line.startsWith('FNDA:')) {
|
|
391
|
+
const [hits, name] = line.slice(5).split(',');
|
|
392
|
+
const entry = fnMap.get(name);
|
|
393
|
+
if (entry)
|
|
394
|
+
entry.hits = Number(hits);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
if (fileCovered)
|
|
398
|
+
coveredFiles += 1;
|
|
399
|
+
for (const [name, data] of fnMap.entries()) {
|
|
400
|
+
if (data.hits === 0) {
|
|
401
|
+
uncoveredMethods.push({
|
|
402
|
+
class: path.basename(file, path.extname(file)),
|
|
403
|
+
method: name,
|
|
404
|
+
file: normalizePath(file),
|
|
405
|
+
line: data.line,
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
return {
|
|
411
|
+
tool: 'lcov',
|
|
412
|
+
lineCoverage: safePercent(hitLines, totalLines),
|
|
413
|
+
branchCoverage: safePercent(hitBranches, totalBranches),
|
|
414
|
+
methodCoverage: safePercent(hitFunctions, totalFunctions),
|
|
415
|
+
classCoverage: files.length > 0 ? Number(((coveredFiles / files.length) * 100).toFixed(1)) : null,
|
|
416
|
+
uncoveredMethods,
|
|
417
|
+
reportPath: normalizePath(reportPath),
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
function parseCoverageXml(reportPath) {
|
|
421
|
+
var _a;
|
|
422
|
+
const xml = safeRead(reportPath);
|
|
423
|
+
const linesValid = readNumericAttribute(xml, 'lines-valid');
|
|
424
|
+
const linesCovered = readNumericAttribute(xml, 'lines-covered');
|
|
425
|
+
const branchesValid = readNumericAttribute(xml, 'branches-valid');
|
|
426
|
+
const branchesCovered = readNumericAttribute(xml, 'branches-covered');
|
|
427
|
+
const classes = [...xml.matchAll(/<class\b[^>]*name="([^"]+)"[^>]*filename="([^"]+)"[\s\S]*?<\/class>/g)];
|
|
428
|
+
const coveredClasses = classes.filter((match) => /line-rate="(?!0(?:\.0+)?)\d*\.?\d+"/.test(match[0])).length;
|
|
429
|
+
const uncoveredMethods = [];
|
|
430
|
+
const methodRegex = /<method\b[^>]*name="([^"]+)"[^>]*line-rate="([^"]+)"[^>]*>([\s\S]*?)<\/method>/g;
|
|
431
|
+
let methodMatch;
|
|
432
|
+
while ((methodMatch = methodRegex.exec(xml)) !== null) {
|
|
433
|
+
if (Number(methodMatch[2]) === 0) {
|
|
434
|
+
const line = (_a = readNumericAttribute(methodMatch[3], 'number')) !== null && _a !== void 0 ? _a : 0;
|
|
435
|
+
uncoveredMethods.push({
|
|
436
|
+
class: 'coverage.py',
|
|
437
|
+
method: methodMatch[1],
|
|
438
|
+
file: 'coverage.xml',
|
|
439
|
+
line,
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
const totalMethods = [...xml.matchAll(/<method\b/g)].length;
|
|
444
|
+
const coveredMethods = [...xml.matchAll(/<method\b[^>]*line-rate="(?!0(?:\.0+)?)\d*\.?\d+"/g)].length;
|
|
445
|
+
return {
|
|
446
|
+
tool: 'coverage.py',
|
|
447
|
+
lineCoverage: safePercent(linesCovered, linesValid),
|
|
448
|
+
branchCoverage: safePercent(branchesCovered, branchesValid),
|
|
449
|
+
methodCoverage: safePercent(coveredMethods, totalMethods),
|
|
450
|
+
classCoverage: classes.length > 0 ? Number(((coveredClasses / classes.length) * 100).toFixed(1)) : null,
|
|
451
|
+
uncoveredMethods,
|
|
452
|
+
reportPath: normalizePath(reportPath),
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
function parseGoCoverage(reportPath) {
|
|
456
|
+
const content = safeRead(reportPath);
|
|
457
|
+
let total = 0;
|
|
458
|
+
let covered = 0;
|
|
459
|
+
for (const line of content.split('\n')) {
|
|
460
|
+
if (!line || line.startsWith('mode:'))
|
|
461
|
+
continue;
|
|
462
|
+
const parts = line.trim().split(' ');
|
|
463
|
+
if (parts.length < 3)
|
|
464
|
+
continue;
|
|
465
|
+
const count = Number(parts[2]);
|
|
466
|
+
const range = parts[1];
|
|
467
|
+
const lineSpan = range.split(',').map((segment) => segment.split('.'));
|
|
468
|
+
const startLine = Number(lineSpan[0][0]);
|
|
469
|
+
const endLine = Number(lineSpan[1][0]);
|
|
470
|
+
const lines = Math.max(1, endLine - startLine + 1);
|
|
471
|
+
total += lines;
|
|
472
|
+
if (count > 0)
|
|
473
|
+
covered += lines;
|
|
474
|
+
}
|
|
475
|
+
return {
|
|
476
|
+
tool: 'go-cover',
|
|
477
|
+
lineCoverage: safePercent(covered, total),
|
|
478
|
+
branchCoverage: null,
|
|
479
|
+
methodCoverage: null,
|
|
480
|
+
classCoverage: null,
|
|
481
|
+
uncoveredMethods: [],
|
|
482
|
+
reportPath: normalizePath(reportPath),
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
function runMutationTesting(rootDir, languages, config) {
|
|
486
|
+
var _a, _b;
|
|
487
|
+
const selectedTool = resolveMutationTool((_b = (_a = config.mutationTesting) === null || _a === void 0 ? void 0 : _a.tool) !== null && _b !== void 0 ? _b : 'auto', languages, rootDir);
|
|
488
|
+
if (!selectedTool) {
|
|
489
|
+
return { summary: null, warning: 'Mutation testing is enabled but no supported mutation tool could be resolved for this project.' };
|
|
490
|
+
}
|
|
491
|
+
const command = mutationCommandFor(rootDir, selectedTool);
|
|
492
|
+
const result = (0, child_process_1.spawnSync)(command.command, command.args, {
|
|
493
|
+
cwd: rootDir,
|
|
494
|
+
encoding: 'utf-8',
|
|
495
|
+
shell: false,
|
|
496
|
+
});
|
|
497
|
+
const summary = parseMutationReport(rootDir, selectedTool, result.stdout, result.stderr);
|
|
498
|
+
if (!summary) {
|
|
499
|
+
const stderr = [result.stderr, result.stdout].filter(Boolean).join('\n').trim();
|
|
500
|
+
return {
|
|
501
|
+
summary: null,
|
|
502
|
+
warning: stderr || `Mutation testing tool ${selectedTool} did not produce a supported report.`,
|
|
503
|
+
};
|
|
504
|
+
}
|
|
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
|
+
}
|
|
576
|
+
function parsePitestXml(reportPath) {
|
|
577
|
+
var _a, _b, _c, _d, _e, _f, _g;
|
|
578
|
+
const xml = safeRead(reportPath);
|
|
579
|
+
const mutations = [...xml.matchAll(/<mutation\b([^>]*)>([\s\S]*?)<\/mutation>/g)];
|
|
580
|
+
const survivedByFile = new Map();
|
|
581
|
+
let total = 0;
|
|
582
|
+
let killed = 0;
|
|
583
|
+
let survived = 0;
|
|
584
|
+
for (const mutation of mutations) {
|
|
585
|
+
total += 1;
|
|
586
|
+
const attrs = mutation[1];
|
|
587
|
+
const body = mutation[2];
|
|
588
|
+
const statusMatch = /status="([^"]+)"/.exec(attrs);
|
|
589
|
+
const status = (_a = statusMatch === null || statusMatch === void 0 ? void 0 : statusMatch[1]) !== null && _a !== void 0 ? _a : 'UNKNOWN';
|
|
590
|
+
const file = (_b = extractXmlText(body, 'sourceFile')) !== null && _b !== void 0 ? _b : 'unknown';
|
|
591
|
+
const line = Number((_c = extractXmlText(body, 'lineNumber')) !== null && _c !== void 0 ? _c : '0');
|
|
592
|
+
const mutator = (_d = extractXmlText(body, 'mutator')) !== null && _d !== void 0 ? _d : 'mutation';
|
|
593
|
+
const description = (_e = extractXmlText(body, 'description')) !== null && _e !== void 0 ? _e : mutator;
|
|
594
|
+
const killingTest = extractXmlText(body, 'killingTest');
|
|
595
|
+
if (status === 'KILLED') {
|
|
596
|
+
killed += 1;
|
|
597
|
+
}
|
|
598
|
+
if (status === 'SURVIVED' || status === 'NO_COVERAGE' || status === 'TIMED_OUT') {
|
|
599
|
+
survived += 1;
|
|
600
|
+
const entries = (_f = survivedByFile.get(file)) !== null && _f !== void 0 ? _f : [];
|
|
601
|
+
entries.push({
|
|
602
|
+
line,
|
|
603
|
+
mutation: description,
|
|
604
|
+
testsThatShouldHaveCaught: killingTest ? [killingTest] : [],
|
|
605
|
+
});
|
|
606
|
+
survivedByFile.set(file, entries);
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
return {
|
|
610
|
+
tool: 'pitest',
|
|
611
|
+
mutationScore: (_g = safePercent(killed, total)) !== null && _g !== void 0 ? _g : 0,
|
|
612
|
+
totalMutants: total,
|
|
613
|
+
killedMutants: killed,
|
|
614
|
+
survivedMutants: survived,
|
|
615
|
+
survivedByFile: Array.from(survivedByFile.entries()).map(([file, items]) => ({ file, survived: items })),
|
|
616
|
+
reportPath: normalizePath(reportPath),
|
|
617
|
+
};
|
|
618
|
+
}
|
|
619
|
+
function parseStrykerReport(reportPath, toolName) {
|
|
620
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j;
|
|
621
|
+
const json = JSON.parse(safeRead(reportPath));
|
|
622
|
+
const files = ((_a = json['files']) !== null && _a !== void 0 ? _a : {});
|
|
623
|
+
let total = 0;
|
|
624
|
+
let killed = 0;
|
|
625
|
+
let survived = 0;
|
|
626
|
+
const survivedByFile = [];
|
|
627
|
+
for (const [file, fileValue] of Object.entries(files)) {
|
|
628
|
+
const fileRecord = fileValue;
|
|
629
|
+
const mutants = ((_b = fileRecord['mutants']) !== null && _b !== void 0 ? _b : []);
|
|
630
|
+
const survivors = [];
|
|
631
|
+
for (const mutant of mutants) {
|
|
632
|
+
total += 1;
|
|
633
|
+
const status = String((_c = mutant['status']) !== null && _c !== void 0 ? _c : 'Unknown');
|
|
634
|
+
if (/Killed/i.test(status))
|
|
635
|
+
killed += 1;
|
|
636
|
+
if (/Survived|NoCoverage|Timeout/i.test(status)) {
|
|
637
|
+
survived += 1;
|
|
638
|
+
const location = ((_d = mutant['location']) !== null && _d !== void 0 ? _d : {});
|
|
639
|
+
const start = ((_e = location['start']) !== null && _e !== void 0 ? _e : {});
|
|
640
|
+
survivors.push({
|
|
641
|
+
line: Number((_f = start['line']) !== null && _f !== void 0 ? _f : 0),
|
|
642
|
+
mutation: String((_h = (_g = mutant['mutatorName']) !== null && _g !== void 0 ? _g : mutant['id']) !== null && _h !== void 0 ? _h : 'mutation'),
|
|
643
|
+
originalCode: typeof mutant['replacement'] === 'string' ? String(mutant['replacement']) : undefined,
|
|
644
|
+
mutatedCode: typeof mutant['description'] === 'string' ? String(mutant['description']) : undefined,
|
|
645
|
+
testsThatShouldHaveCaught: [],
|
|
646
|
+
});
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
if (survivors.length > 0) {
|
|
650
|
+
survivedByFile.push({ file: normalizePath(file), survived: survivors });
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
return {
|
|
654
|
+
tool: toolName,
|
|
655
|
+
mutationScore: (_j = safePercent(killed, total)) !== null && _j !== void 0 ? _j : 0,
|
|
656
|
+
totalMutants: total,
|
|
657
|
+
killedMutants: killed,
|
|
658
|
+
survivedMutants: survived,
|
|
659
|
+
survivedByFile,
|
|
660
|
+
reportPath: normalizePath(reportPath),
|
|
661
|
+
};
|
|
662
|
+
}
|
|
663
|
+
function parseMutmutOutput(output) {
|
|
664
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j;
|
|
665
|
+
const total = Number((_b = (_a = /survived\s*:\s*(\d+)/i.exec(output)) === null || _a === void 0 ? void 0 : _a[1]) !== null && _b !== void 0 ? _b : 0)
|
|
666
|
+
+ Number((_d = (_c = /killed\s*:\s*(\d+)/i.exec(output)) === null || _c === void 0 ? void 0 : _c[1]) !== null && _d !== void 0 ? _d : 0);
|
|
667
|
+
const killed = Number((_f = (_e = /killed\s*:\s*(\d+)/i.exec(output)) === null || _e === void 0 ? void 0 : _e[1]) !== null && _f !== void 0 ? _f : 0);
|
|
668
|
+
const survived = Number((_h = (_g = /survived\s*:\s*(\d+)/i.exec(output)) === null || _g === void 0 ? void 0 : _g[1]) !== null && _h !== void 0 ? _h : 0);
|
|
669
|
+
if (total === 0)
|
|
670
|
+
return null;
|
|
671
|
+
return {
|
|
672
|
+
mutationScore: (_j = safePercent(killed, total)) !== null && _j !== void 0 ? _j : 0,
|
|
673
|
+
totalMutants: total,
|
|
674
|
+
killedMutants: killed,
|
|
675
|
+
survivedMutants: survived,
|
|
676
|
+
survivedByFile: [],
|
|
677
|
+
};
|
|
678
|
+
}
|
|
679
|
+
function parseGoMutestingOutput(output) {
|
|
680
|
+
var _a, _b, _c, _d, _e, _f, _g;
|
|
681
|
+
const total = Number((_b = (_a = /total mutants:\s*(\d+)/i.exec(output)) === null || _a === void 0 ? void 0 : _a[1]) !== null && _b !== void 0 ? _b : 0);
|
|
682
|
+
const killed = Number((_d = (_c = /killed mutants:\s*(\d+)/i.exec(output)) === null || _c === void 0 ? void 0 : _c[1]) !== null && _d !== void 0 ? _d : 0);
|
|
683
|
+
const survived = Number((_f = (_e = /survived mutants:\s*(\d+)/i.exec(output)) === null || _e === void 0 ? void 0 : _e[1]) !== null && _f !== void 0 ? _f : 0);
|
|
684
|
+
if (total === 0)
|
|
685
|
+
return null;
|
|
686
|
+
return {
|
|
687
|
+
mutationScore: (_g = safePercent(killed, total)) !== null && _g !== void 0 ? _g : 0,
|
|
688
|
+
totalMutants: total,
|
|
689
|
+
killedMutants: killed,
|
|
690
|
+
survivedMutants: survived,
|
|
691
|
+
survivedByFile: [],
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
function parseSlowTests(rootDir, config) {
|
|
695
|
+
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
|
+
const unitThreshold = (_b = (_a = config.slowTestThreshold) === null || _a === void 0 ? void 0 : _a.unit) !== null && _b !== void 0 ? _b : 200;
|
|
711
|
+
const slowTests = durations
|
|
712
|
+
.filter((item) => item.durationMs > unitThreshold)
|
|
713
|
+
.map((item) => ({
|
|
714
|
+
...item,
|
|
715
|
+
severity: item.durationMs > 1000 ? 'HIGH' : 'MEDIUM',
|
|
716
|
+
hint: buildSlowTestHint(item.durationMs),
|
|
717
|
+
}))
|
|
718
|
+
.sort((a, b) => b.durationMs - a.durationMs);
|
|
719
|
+
const totalDurationMs = durations.reduce((sum, item) => sum + item.durationMs, 0);
|
|
720
|
+
const fastCount = durations.filter((item) => item.durationMs < 50).length;
|
|
721
|
+
const slowCount = durations.filter((item) => item.durationMs >= unitThreshold).length;
|
|
722
|
+
return {
|
|
723
|
+
slowTests,
|
|
724
|
+
avgDurationMs: Math.round(totalDurationMs / durations.length),
|
|
725
|
+
totalDurationMs,
|
|
726
|
+
fastPercent: Math.round((fastCount / durations.length) * 100),
|
|
727
|
+
slowPercent: Math.round((slowCount / durations.length) * 100),
|
|
728
|
+
reportPaths: reportPaths.map((reportPath) => normalizePath(path.relative(rootDir, String(reportPath)))),
|
|
729
|
+
};
|
|
730
|
+
}
|
|
731
|
+
function parseJunitXmlDurations(reportPath) {
|
|
732
|
+
const xml = safeRead(reportPath);
|
|
733
|
+
const testcases = [...xml.matchAll(/<testcase\b([^>]*)\/?>(?:[\s\S]*?<\/testcase>)?/g)];
|
|
734
|
+
return testcases.map((match) => {
|
|
735
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j;
|
|
736
|
+
const attrs = match[1];
|
|
737
|
+
const name = (_b = (_a = /\bname="([^"]+)"/.exec(attrs)) === null || _a === void 0 ? void 0 : _a[1]) !== null && _b !== void 0 ? _b : 'unknown';
|
|
738
|
+
const className = (_d = (_c = /\bclassname="([^"]+)"/.exec(attrs)) === null || _c === void 0 ? void 0 : _c[1]) !== null && _d !== void 0 ? _d : '';
|
|
739
|
+
const file = (_g = (_f = (_e = /\bfile="([^"]+)"/.exec(attrs)) === null || _e === void 0 ? void 0 : _e[1]) !== null && _f !== void 0 ? _f : className) !== null && _g !== void 0 ? _g : 'unknown';
|
|
740
|
+
const timeSeconds = Number((_j = (_h = /\btime="([^"]+)"/.exec(attrs)) === null || _h === void 0 ? void 0 : _h[1]) !== null && _j !== void 0 ? _j : 0);
|
|
741
|
+
return {
|
|
742
|
+
file: normalizePath(file),
|
|
743
|
+
method: name,
|
|
744
|
+
durationMs: Math.round(timeSeconds * 1000),
|
|
745
|
+
};
|
|
746
|
+
});
|
|
747
|
+
}
|
|
748
|
+
function parseJestJsonDurations(reportPath) {
|
|
749
|
+
var _a, _b, _c, _d, _e, _f;
|
|
750
|
+
const json = JSON.parse(safeRead(reportPath));
|
|
751
|
+
const suites = ((_a = json['testResults']) !== null && _a !== void 0 ? _a : []);
|
|
752
|
+
const durations = [];
|
|
753
|
+
for (const suite of suites) {
|
|
754
|
+
const file = normalizePath(String((_b = suite['name']) !== null && _b !== void 0 ? _b : 'unknown'));
|
|
755
|
+
const assertions = ((_c = suite['assertionResults']) !== null && _c !== void 0 ? _c : []);
|
|
756
|
+
for (const assertion of assertions) {
|
|
757
|
+
durations.push({
|
|
758
|
+
file,
|
|
759
|
+
method: String((_e = (_d = assertion['fullName']) !== null && _d !== void 0 ? _d : assertion['title']) !== null && _e !== void 0 ? _e : 'unknown'),
|
|
760
|
+
durationMs: Number((_f = assertion['duration']) !== null && _f !== void 0 ? _f : 0),
|
|
761
|
+
});
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
return durations;
|
|
765
|
+
}
|
|
766
|
+
function buildSlowTestHint(durationMs) {
|
|
767
|
+
if (durationMs > 5000) {
|
|
768
|
+
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.`;
|
|
769
|
+
}
|
|
770
|
+
if (durationMs > 1000) {
|
|
771
|
+
return `Test takes ${(durationMs / 1000).toFixed(1)}s — likely loading Spring context or making real HTTP call. Use @MockBean instead.`;
|
|
772
|
+
}
|
|
773
|
+
return `Test takes ${durationMs}ms — likely doing avoidable IO. Prefer isolated unit doubles and in-memory fixtures.`;
|
|
774
|
+
}
|
|
775
|
+
function detectTestSmells(rootDir, tests) {
|
|
776
|
+
const smells = [];
|
|
777
|
+
for (const test of tests) {
|
|
778
|
+
smells.push(...detectCommentedOutTests(rootDir, test));
|
|
779
|
+
smells.push(...detectPerBlockSmells(rootDir, test));
|
|
780
|
+
smells.push(...detectSharedStateSmells(rootDir, test));
|
|
781
|
+
smells.push(...detectSleepAndFileSmells(rootDir, test));
|
|
782
|
+
smells.push(...detectDeadAssertions(rootDir, test));
|
|
783
|
+
smells.push(...detectProductionInstantiation(rootDir, test));
|
|
784
|
+
}
|
|
785
|
+
return smells.sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line || a.smellId.localeCompare(b.smellId));
|
|
786
|
+
}
|
|
787
|
+
function detectCommentedOutTests(rootDir, test) {
|
|
788
|
+
const smells = [];
|
|
789
|
+
const lines = test.content.split('\n');
|
|
790
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
791
|
+
const line = lines[index];
|
|
792
|
+
if (/^\s*\/\/\s*@Test\b/.test(line) || /^\s*\/\/\s*(?:test|it)\s*\(/.test(line) || /^\s*#\s*def\s+test_/.test(line)) {
|
|
793
|
+
smells.push({
|
|
794
|
+
smellId: 'SMELL-02',
|
|
795
|
+
severity: 'HIGH',
|
|
796
|
+
file: test.relativePath,
|
|
797
|
+
line: index + 1,
|
|
798
|
+
description: 'Commented-out test code detected',
|
|
799
|
+
fix: 'Delete dead test code or restore it as an active test case',
|
|
800
|
+
});
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
return smells;
|
|
804
|
+
}
|
|
805
|
+
function detectPerBlockSmells(rootDir, test) {
|
|
806
|
+
const smells = [];
|
|
807
|
+
const blocks = extractTestBlocks(test.language, test.content);
|
|
808
|
+
for (const block of blocks) {
|
|
809
|
+
const body = block.body;
|
|
810
|
+
const assertionCount = countAssertions(test.language, body);
|
|
811
|
+
if (assertionCount === 0 && !hasVerificationCall(test.language, body)) {
|
|
812
|
+
smells.push({
|
|
813
|
+
smellId: 'SMELL-01',
|
|
814
|
+
severity: 'CRITICAL',
|
|
815
|
+
file: test.relativePath,
|
|
816
|
+
line: block.line,
|
|
817
|
+
testMethod: block.name,
|
|
818
|
+
description: 'Test has no assertions — proves nothing',
|
|
819
|
+
fix: 'Add assertion on the return value or verify mock interaction',
|
|
820
|
+
});
|
|
821
|
+
}
|
|
822
|
+
if (isDisabledWithoutReason(test.language, block, test.content)) {
|
|
823
|
+
smells.push({
|
|
824
|
+
smellId: 'SMELL-03',
|
|
825
|
+
severity: 'HIGH',
|
|
826
|
+
file: test.relativePath,
|
|
827
|
+
line: block.line,
|
|
828
|
+
testMethod: block.name,
|
|
829
|
+
description: 'Disabled test has no documented reason',
|
|
830
|
+
fix: 'Add a reason to the skip/disable annotation or re-enable the test',
|
|
831
|
+
});
|
|
832
|
+
}
|
|
833
|
+
if ((test.language === 'java' || test.language === 'kotlin' || test.language === 'javascript' || test.language === 'typescript') && assertionCount > 7 && !hasAssertionMessages(test.language, body)) {
|
|
834
|
+
smells.push({
|
|
835
|
+
smellId: 'SMELL-04',
|
|
836
|
+
severity: 'MEDIUM',
|
|
837
|
+
file: test.relativePath,
|
|
838
|
+
line: block.line,
|
|
839
|
+
testMethod: block.name,
|
|
840
|
+
description: 'Assertion roulette detected — many assertions with no diagnostic messages',
|
|
841
|
+
fix: 'Split the test or add assertion messages to clarify failures',
|
|
842
|
+
});
|
|
843
|
+
}
|
|
844
|
+
if (isEagerTest(body)) {
|
|
845
|
+
smells.push({
|
|
846
|
+
smellId: 'SMELL-05',
|
|
847
|
+
severity: 'MEDIUM',
|
|
848
|
+
file: test.relativePath,
|
|
849
|
+
line: block.line,
|
|
850
|
+
testMethod: block.name,
|
|
851
|
+
description: 'Test appears to exercise too many behaviours',
|
|
852
|
+
fix: 'Split the test into smaller behaviour-focused test cases',
|
|
853
|
+
});
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
return smells;
|
|
857
|
+
}
|
|
858
|
+
function detectSharedStateSmells(rootDir, test) {
|
|
859
|
+
var _a, _b, _c;
|
|
860
|
+
const smells = [];
|
|
861
|
+
if (test.language === 'java' || test.language === 'kotlin') {
|
|
862
|
+
const staticFields = [...test.content.matchAll(/(?:private|protected|public)?\s*static\s+(?!final)(?:[\w<>\[\]]+\s+)+([a-zA-Z_]\w*)\s*[;=]/g)];
|
|
863
|
+
const blocks = extractTestBlocks(test.language, test.content);
|
|
864
|
+
for (const field of staticFields) {
|
|
865
|
+
const fieldName = field[1];
|
|
866
|
+
const assignmentRegex = new RegExp(`\\b${fieldName}\\s*=`, 'm');
|
|
867
|
+
if (blocks.some((block) => assignmentRegex.test(block.body))) {
|
|
868
|
+
smells.push({
|
|
869
|
+
smellId: 'SMELL-07',
|
|
870
|
+
severity: 'HIGH',
|
|
871
|
+
file: test.relativePath,
|
|
872
|
+
line: lineNumberForIndex(test.content, (_a = field.index) !== null && _a !== void 0 ? _a : 0),
|
|
873
|
+
description: `Shared mutable static state detected for ${fieldName}`,
|
|
874
|
+
fix: 'Replace shared static mutable state with per-test setup data',
|
|
875
|
+
});
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
if (test.language === 'javascript' || test.language === 'typescript') {
|
|
880
|
+
const letMatches = [...test.content.matchAll(/\blet\s+([a-zA-Z_]\w*)\s*[=;]/g)];
|
|
881
|
+
const blocks = extractTestBlocks(test.language, test.content);
|
|
882
|
+
for (const match of letMatches) {
|
|
883
|
+
const variable = match[1];
|
|
884
|
+
const assignmentRegex = new RegExp(`\\b${variable}\\s*=`, 'm');
|
|
885
|
+
if (blocks.some((block) => assignmentRegex.test(block.body))) {
|
|
886
|
+
smells.push({
|
|
887
|
+
smellId: 'SMELL-07',
|
|
888
|
+
severity: 'HIGH',
|
|
889
|
+
file: test.relativePath,
|
|
890
|
+
line: lineNumberForIndex(test.content, (_b = match.index) !== null && _b !== void 0 ? _b : 0),
|
|
891
|
+
description: `Shared mutable describe-scope variable detected: ${variable}`,
|
|
892
|
+
fix: 'Use fresh state per test in beforeEach or local variables',
|
|
893
|
+
});
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
if (test.language === 'python') {
|
|
898
|
+
const classAttrs = [...test.content.matchAll(/^\s{4}([a-zA-Z_]\w*)\s*=.+$/gm)];
|
|
899
|
+
for (const attr of classAttrs) {
|
|
900
|
+
const attrName = attr[1];
|
|
901
|
+
const assignmentRegex = new RegExp(`self\\.${attrName}\\s*=`, 'm');
|
|
902
|
+
if (assignmentRegex.test(test.content)) {
|
|
903
|
+
smells.push({
|
|
904
|
+
smellId: 'SMELL-07',
|
|
905
|
+
severity: 'HIGH',
|
|
906
|
+
file: test.relativePath,
|
|
907
|
+
line: lineNumberForIndex(test.content, (_c = attr.index) !== null && _c !== void 0 ? _c : 0),
|
|
908
|
+
description: `Shared mutable class-level attribute detected: ${attrName}`,
|
|
909
|
+
fix: 'Move mutable state into per-test fixtures or local variables',
|
|
910
|
+
});
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
return smells;
|
|
915
|
+
}
|
|
916
|
+
function detectSleepAndFileSmells(rootDir, test) {
|
|
917
|
+
const smells = [];
|
|
918
|
+
const lines = test.content.split('\n');
|
|
919
|
+
const filePattern = /new\s+File\(|Files\.(?:read|write)|readFile\(|fs\.(?:readFile|writeFile)|open\(|Path\.|Paths\.|FileReader\(/;
|
|
920
|
+
const sleepPattern = /Thread\.sleep\(|time\.sleep\(|setTimeout\(|sleep\(/;
|
|
921
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
922
|
+
const line = lines[index];
|
|
923
|
+
if (filePattern.test(line) && !/fixture|fixtures|helper/i.test(line)) {
|
|
924
|
+
smells.push({
|
|
925
|
+
smellId: 'SMELL-06',
|
|
926
|
+
severity: 'MEDIUM',
|
|
927
|
+
file: test.relativePath,
|
|
928
|
+
line: index + 1,
|
|
929
|
+
description: 'Mystery guest detected — test reaches into external files directly',
|
|
930
|
+
fix: 'Use an explicit fixture helper or inline test data',
|
|
931
|
+
});
|
|
932
|
+
}
|
|
933
|
+
if (sleepPattern.test(line) && !/pollUntil/.test(line)) {
|
|
934
|
+
if (!/setTimeout\([^,]+,\s*0\s*\)/.test(line)) {
|
|
935
|
+
smells.push({
|
|
936
|
+
smellId: 'SMELL-08',
|
|
937
|
+
severity: 'HIGH',
|
|
938
|
+
file: test.relativePath,
|
|
939
|
+
line: index + 1,
|
|
940
|
+
description: 'Sleeping test detected',
|
|
941
|
+
fix: 'Use polling helpers or await explicit conditions instead of sleeping',
|
|
942
|
+
});
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
return smells;
|
|
947
|
+
}
|
|
948
|
+
function detectDeadAssertions(rootDir, test) {
|
|
949
|
+
const smells = [];
|
|
950
|
+
const lines = test.content.split('\n');
|
|
951
|
+
const patterns = [
|
|
952
|
+
/assertTrue\(\s*true\s*\)/,
|
|
953
|
+
/assertEquals\(\s*([a-zA-Z_][\w.]*)\s*,\s*\1\s*\)/,
|
|
954
|
+
/assertNotNull\(\s*new\s+[A-Z][\w<>]*\s*\(/,
|
|
955
|
+
/expect\(([^)]+)\)\.toBe\(\s*\1\s*\)/,
|
|
956
|
+
/expect\(\s*true\s*\)\.toBe\(\s*true\s*\)/,
|
|
957
|
+
];
|
|
958
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
959
|
+
const line = lines[index];
|
|
960
|
+
if (patterns.some((pattern) => pattern.test(line))) {
|
|
961
|
+
smells.push({
|
|
962
|
+
smellId: 'SMELL-09',
|
|
963
|
+
severity: 'CRITICAL',
|
|
964
|
+
file: test.relativePath,
|
|
965
|
+
line: index + 1,
|
|
966
|
+
description: 'Dead assertion detected — assertion always passes',
|
|
967
|
+
fix: 'Assert on a real behavioural outcome instead of a tautology',
|
|
968
|
+
});
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
return smells;
|
|
972
|
+
}
|
|
973
|
+
function detectProductionInstantiation(rootDir, test) {
|
|
974
|
+
const smells = [];
|
|
975
|
+
const lines = test.content.split('\n');
|
|
976
|
+
if (test.mockLibrary)
|
|
977
|
+
return smells;
|
|
978
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
979
|
+
const line = lines[index];
|
|
980
|
+
if (/new\s+[A-Z]\w*(?:Service|Manager|Controller)\s*\(\s*new\s+[A-Z]\w+/.test(line)) {
|
|
981
|
+
smells.push({
|
|
982
|
+
smellId: 'SMELL-10',
|
|
983
|
+
severity: 'MEDIUM',
|
|
984
|
+
file: test.relativePath,
|
|
985
|
+
line: index + 1,
|
|
986
|
+
description: 'Production class is instantiated with real dependencies in a unit test',
|
|
987
|
+
fix: 'Inject mocks or fakes instead of concrete production collaborators',
|
|
988
|
+
});
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
return smells;
|
|
992
|
+
}
|
|
993
|
+
function computeIndependence(rootDir, tests) {
|
|
994
|
+
var _a;
|
|
995
|
+
const deductions = [];
|
|
996
|
+
for (const test of tests) {
|
|
997
|
+
const staticFields = [...test.content.matchAll(/(?:static\s+(?!final)|\blet\s+)([a-zA-Z_]\w*)/g)];
|
|
998
|
+
for (const field of staticFields) {
|
|
999
|
+
deductions.push({
|
|
1000
|
+
reason: `Shared mutable state: ${field[1]}`,
|
|
1001
|
+
points: 10,
|
|
1002
|
+
file: test.relativePath,
|
|
1003
|
+
line: lineNumberForIndex(test.content, (_a = field.index) !== null && _a !== void 0 ? _a : 0),
|
|
1004
|
+
});
|
|
1005
|
+
}
|
|
1006
|
+
if (/@TestMethodOrder\(|@Order\(/.test(test.content)) {
|
|
1007
|
+
deductions.push({
|
|
1008
|
+
reason: 'Ordered test chain detected',
|
|
1009
|
+
points: 20,
|
|
1010
|
+
file: test.relativePath,
|
|
1011
|
+
});
|
|
1012
|
+
}
|
|
1013
|
+
if (/(jdbc|EntityManager|Repository|save\(|insert into|select \*)/i.test(test.content) && !/@Transactional|@DirtiesContext/.test(test.content)) {
|
|
1014
|
+
deductions.push({
|
|
1015
|
+
reason: 'Database state dependency without isolation guard',
|
|
1016
|
+
points: 10,
|
|
1017
|
+
file: test.relativePath,
|
|
1018
|
+
});
|
|
1019
|
+
}
|
|
1020
|
+
if (/(writeFile|Files\.write|open\([^)]*,\s*['"]w|FileWriter)/.test(test.content) && /(readFile|Files\.read|open\([^)]*,\s*['"]r|FileReader)/.test(test.content)) {
|
|
1021
|
+
deductions.push({
|
|
1022
|
+
reason: 'File system state shared across tests',
|
|
1023
|
+
points: 10,
|
|
1024
|
+
file: test.relativePath,
|
|
1025
|
+
});
|
|
1026
|
+
}
|
|
1027
|
+
const portMatch = /\b(?:localhost:|server\.port\s*=\s*|bind\()\s*(8080|3000|5000|8000)\b/.exec(test.content);
|
|
1028
|
+
if (portMatch) {
|
|
1029
|
+
deductions.push({
|
|
1030
|
+
reason: `Fixed port binding detected: ${portMatch[1]}`,
|
|
1031
|
+
points: 30,
|
|
1032
|
+
file: test.relativePath,
|
|
1033
|
+
});
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
const score = Math.max(0, 100 - deductions.reduce((sum, deduction) => sum + deduction.points, 0));
|
|
1037
|
+
return { score, deductions };
|
|
1038
|
+
}
|
|
1039
|
+
function computeAssertionDensity(tests) {
|
|
1040
|
+
const files = tests.map((test) => {
|
|
1041
|
+
const blocks = extractTestBlocks(test.language, test.content);
|
|
1042
|
+
const totalLines = blocks.reduce((sum, block) => sum + block.body.split('\n').filter((line) => line.trim()).length, 0);
|
|
1043
|
+
const density = totalLines > 0 ? Number((test.assertionCount / totalLines).toFixed(3)) : 0;
|
|
1044
|
+
return {
|
|
1045
|
+
file: test.relativePath,
|
|
1046
|
+
density,
|
|
1047
|
+
band: density > 0.3 ? 'GREEN' : density >= 0.1 ? 'YELLOW' : 'RED',
|
|
1048
|
+
assertionCount: test.assertionCount,
|
|
1049
|
+
totalLines,
|
|
1050
|
+
};
|
|
1051
|
+
});
|
|
1052
|
+
const averageDensity = files.length > 0
|
|
1053
|
+
? Number((files.reduce((sum, file) => sum + file.density, 0) / files.length).toFixed(3))
|
|
1054
|
+
: 0;
|
|
1055
|
+
return { averageDensity, files };
|
|
1056
|
+
}
|
|
1057
|
+
function extractTestBlocks(language, content) {
|
|
1058
|
+
if (language === 'javascript' || language === 'typescript') {
|
|
1059
|
+
const regex = /(x?it|x?test|test\.skip|it\.skip)\s*\(\s*['"`]([^'"`]+)['"`][\s\S]{0,200}?=>\s*\{/g;
|
|
1060
|
+
const blocks = [];
|
|
1061
|
+
let match;
|
|
1062
|
+
while ((match = regex.exec(content)) !== null) {
|
|
1063
|
+
const start = match.index + match[0].length - 1;
|
|
1064
|
+
const end = findMatchingBrace(content, start);
|
|
1065
|
+
blocks.push({
|
|
1066
|
+
name: match[2],
|
|
1067
|
+
body: content.slice(start + 1, end),
|
|
1068
|
+
line: lineNumberForIndex(content, match.index),
|
|
1069
|
+
decorators: [match[1]],
|
|
1070
|
+
});
|
|
1071
|
+
regex.lastIndex = end + 1;
|
|
1072
|
+
}
|
|
1073
|
+
return blocks;
|
|
1074
|
+
}
|
|
1075
|
+
if (language === 'java' || language === 'kotlin') {
|
|
1076
|
+
const regex = /(?:public|protected|private|internal)?\s*(?:void|fun)\s+([a-zA-Z_]\w*)\s*\([^)]*\)\s*\{/g;
|
|
1077
|
+
const blocks = [];
|
|
1078
|
+
let match;
|
|
1079
|
+
while ((match = regex.exec(content)) !== null) {
|
|
1080
|
+
const annotations = extractLeadingAnnotations(content, match.index);
|
|
1081
|
+
if (!annotations.some((annotation) => /@(?:Test|ParameterizedTest|Disabled)/.test(annotation)))
|
|
1082
|
+
continue;
|
|
1083
|
+
const start = match.index + match[0].length - 1;
|
|
1084
|
+
const end = findMatchingBrace(content, start);
|
|
1085
|
+
blocks.push({
|
|
1086
|
+
name: match[1],
|
|
1087
|
+
body: content.slice(start + 1, end),
|
|
1088
|
+
line: lineNumberForIndex(content, match.index),
|
|
1089
|
+
decorators: annotations,
|
|
1090
|
+
});
|
|
1091
|
+
regex.lastIndex = end + 1;
|
|
1092
|
+
}
|
|
1093
|
+
return blocks;
|
|
1094
|
+
}
|
|
1095
|
+
if (language === 'python') {
|
|
1096
|
+
const regex = /((?:@[^\n]+\n)*)def\s+(test_[a-zA-Z_]\w*)\s*\([^)]*\):\n([\s\S]*?)(?=\n\S|$)/g;
|
|
1097
|
+
const blocks = [];
|
|
1098
|
+
let match;
|
|
1099
|
+
while ((match = regex.exec(content)) !== null) {
|
|
1100
|
+
blocks.push({
|
|
1101
|
+
name: match[2],
|
|
1102
|
+
body: match[3],
|
|
1103
|
+
line: lineNumberForIndex(content, match.index),
|
|
1104
|
+
decorators: match[1].split('\n').map((line) => line.trim()).filter(Boolean),
|
|
1105
|
+
});
|
|
1106
|
+
}
|
|
1107
|
+
return blocks;
|
|
1108
|
+
}
|
|
1109
|
+
return [];
|
|
1110
|
+
}
|
|
1111
|
+
function hasVerificationCall(language, body) {
|
|
1112
|
+
if (language === 'java' || language === 'kotlin')
|
|
1113
|
+
return /\b(?:verify|check|assert)\w*\s*\(/.test(body);
|
|
1114
|
+
if (language === 'javascript' || language === 'typescript')
|
|
1115
|
+
return /expect\s*\(|toHaveBeenCalled/.test(body);
|
|
1116
|
+
if (language === 'python')
|
|
1117
|
+
return /\bassert\b|assert_called/.test(body);
|
|
1118
|
+
return false;
|
|
1119
|
+
}
|
|
1120
|
+
function isDisabledWithoutReason(language, block, content) {
|
|
1121
|
+
if (language === 'java' || language === 'kotlin') {
|
|
1122
|
+
return block.decorators.some((decorator) => decorator.startsWith('@Disabled') && !/@Disabled\([^)]*[^\s)]/.test(decorator));
|
|
1123
|
+
}
|
|
1124
|
+
if (language === 'javascript' || language === 'typescript') {
|
|
1125
|
+
const disabled = block.decorators.some((decorator) => /x?it|x?test|test\.skip|it\.skip/.test(decorator));
|
|
1126
|
+
if (!disabled)
|
|
1127
|
+
return false;
|
|
1128
|
+
const lines = content.split('\n');
|
|
1129
|
+
const previousLines = lines.slice(Math.max(0, block.line - 3), block.line - 1);
|
|
1130
|
+
return !previousLines.some((line) => /^\s*\/\//.test(line) || /\/\*/.test(line));
|
|
1131
|
+
}
|
|
1132
|
+
if (language === 'python') {
|
|
1133
|
+
return block.decorators.some((decorator) => /@pytest\.mark\.skip(?!\s*\(|.*reason\s*=)/.test(decorator));
|
|
1134
|
+
}
|
|
1135
|
+
return false;
|
|
1136
|
+
}
|
|
1137
|
+
function hasAssertionMessages(language, body) {
|
|
1138
|
+
if (language === 'java' || language === 'kotlin')
|
|
1139
|
+
return /assert\w+\(\s*['"][^'"]+['"]\s*,/.test(body);
|
|
1140
|
+
if (language === 'javascript' || language === 'typescript')
|
|
1141
|
+
return /expect\([^)]*['"][^'"]+['"][^)]*\)/.test(body);
|
|
1142
|
+
return false;
|
|
1143
|
+
}
|
|
1144
|
+
function isEagerTest(body) {
|
|
1145
|
+
var _a, _b;
|
|
1146
|
+
const behaviourBlocks = ((_a = body.match(/\/\/\s*(?:given|when|then)/gi)) !== null && _a !== void 0 ? _a : []).length;
|
|
1147
|
+
const actionCalls = ((_b = body.match(/\.[a-zA-Z_]\w*\s*\(/g)) !== null && _b !== void 0 ? _b : [])
|
|
1148
|
+
.filter((call) => !/\.(?:then|catch|expect|assert|verify|toBe|toEqual)/.test(call))
|
|
1149
|
+
.length;
|
|
1150
|
+
return behaviourBlocks > 3 || actionCalls > 3;
|
|
1151
|
+
}
|
|
1152
|
+
function extractCoveredMethods(language, content) {
|
|
1153
|
+
if (language === 'java' || language === 'kotlin') {
|
|
1154
|
+
const targets = [];
|
|
1155
|
+
const javaRegex = /@InjectMocks[\s\S]{0,500}?(?:private|protected|public|internal)?\s*(?:lateinit\s+)?(?:var\s+)?([A-Z]\w*)\s+([a-zA-Z_]\w*)\s*(?:=|;)/g;
|
|
1156
|
+
let match;
|
|
1157
|
+
while ((match = javaRegex.exec(content)) !== null) {
|
|
1158
|
+
targets.push({ className: match[1], variableName: match[2] });
|
|
1159
|
+
}
|
|
1160
|
+
const kotlinRegex = /@InjectMocks[\s\S]{0,500}?(?:private|protected|public|internal)?\s*(?:lateinit\s+)?var\s+([a-zA-Z_]\w*)\s*:\s*([A-Z]\w*)/g;
|
|
1161
|
+
while ((match = kotlinRegex.exec(content)) !== null) {
|
|
1162
|
+
targets.push({ className: match[2], variableName: match[1] });
|
|
1163
|
+
}
|
|
1164
|
+
const methods = [];
|
|
1165
|
+
for (const target of targets) {
|
|
1166
|
+
const callRegex = new RegExp(`${target.variableName}\\.([a-zA-Z_]\\w*)\\s*\\(`, 'g');
|
|
1167
|
+
while ((match = callRegex.exec(content)) !== null) {
|
|
1168
|
+
methods.push(`${target.className}.${match[1]}`);
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
return dedupeStrings(methods);
|
|
1172
|
+
}
|
|
1173
|
+
if (language === 'javascript' || language === 'typescript') {
|
|
1174
|
+
const targets = [...content.matchAll(/(?:const|let|var)\s+([a-zA-Z_]\w*)\s*=\s*new\s+([A-Z]\w*)\s*\(/g)];
|
|
1175
|
+
const methods = [];
|
|
1176
|
+
for (const target of targets) {
|
|
1177
|
+
const variableName = target[1];
|
|
1178
|
+
const className = target[2];
|
|
1179
|
+
const callRegex = new RegExp(`${variableName}\\.([a-zA-Z_]\\w*)\\s*\\(`, 'g');
|
|
1180
|
+
let match;
|
|
1181
|
+
while ((match = callRegex.exec(content)) !== null) {
|
|
1182
|
+
methods.push(`${className}.${match[1]}`);
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
return dedupeStrings(methods);
|
|
1186
|
+
}
|
|
1187
|
+
if (language === 'python') {
|
|
1188
|
+
const targets = [...content.matchAll(/([a-zA-Z_]\w*)\s*=\s*([A-Z]\w*)\s*\(/g)];
|
|
1189
|
+
const methods = [];
|
|
1190
|
+
for (const target of targets) {
|
|
1191
|
+
const variableName = target[1];
|
|
1192
|
+
const className = target[2];
|
|
1193
|
+
const callRegex = new RegExp(`${variableName}\\.([a-zA-Z_]\\w*)\\s*\\(`, 'g');
|
|
1194
|
+
let match;
|
|
1195
|
+
while ((match = callRegex.exec(content)) !== null) {
|
|
1196
|
+
methods.push(`${className}.${match[1]}`);
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
return dedupeStrings(methods);
|
|
1200
|
+
}
|
|
1201
|
+
return [];
|
|
1202
|
+
}
|
|
1203
|
+
function extractInstantiatedClasses(content) {
|
|
1204
|
+
return dedupeStrings([
|
|
1205
|
+
...[...content.matchAll(/new\s+([A-Z]\w*)\s*\(/g)].map((match) => match[1]),
|
|
1206
|
+
...[...content.matchAll(/=\s*([A-Z]\w*)\s*\(/g)].map((match) => match[1]),
|
|
1207
|
+
]);
|
|
1208
|
+
}
|
|
1209
|
+
function scoreTestQuality(content) {
|
|
1210
|
+
let score = 0;
|
|
1211
|
+
if (/expect\(|assert\w*\(|\bassert\b/.test(content))
|
|
1212
|
+
score += 25;
|
|
1213
|
+
if (/verify\(|toHaveBeenCalled|assert_called/.test(content))
|
|
1214
|
+
score += 20;
|
|
1215
|
+
if (/invalid|error|exception|reject|fail/i.test(content))
|
|
1216
|
+
score += 20;
|
|
1217
|
+
if (!/toBeTruthy\(|toBeDefined\(/.test(content))
|
|
1218
|
+
score += 20;
|
|
1219
|
+
if (/test\(|it\(|@Test|def\s+test_/.test(content))
|
|
1220
|
+
score += 15;
|
|
1221
|
+
return score;
|
|
1222
|
+
}
|
|
1223
|
+
function countParameterizedExpansions(content) {
|
|
1224
|
+
var _a, _b;
|
|
1225
|
+
const matches = [
|
|
1226
|
+
...content.matchAll(/@CsvSource\(([^)]*)\)/g),
|
|
1227
|
+
...content.matchAll(/test\.each\(([^)]*)\)/g),
|
|
1228
|
+
...content.matchAll(/pytest\.mark\.parametrize\(([^)]*)\)/g),
|
|
1229
|
+
];
|
|
1230
|
+
let total = 0;
|
|
1231
|
+
for (const match of matches) {
|
|
1232
|
+
const args = match[1];
|
|
1233
|
+
total += Math.max(1, ((_a = args.match(/\[/g)) !== null && _a !== void 0 ? _a : []).length || ((_b = args.match(/,/g)) !== null && _b !== void 0 ? _b : []).length);
|
|
1234
|
+
}
|
|
1235
|
+
return total;
|
|
1236
|
+
}
|
|
1237
|
+
function detectMockLibrary(content) {
|
|
1238
|
+
if (/Mockito|@Mock\b|mockito/i.test(content))
|
|
1239
|
+
return 'mockito';
|
|
1240
|
+
if (/jest\.mock|vi\.mock|sinon/i.test(content))
|
|
1241
|
+
return 'jest';
|
|
1242
|
+
if (/unittest\.mock|MagicMock|pytest-mock|mocker\./i.test(content))
|
|
1243
|
+
return 'unittest.mock';
|
|
1244
|
+
return undefined;
|
|
1245
|
+
}
|
|
1246
|
+
function classifyLayer(content, filePath) {
|
|
1247
|
+
if (/supertest|axios|fetch\(|request\(|HttpClient/.test(content))
|
|
1248
|
+
return 'integration';
|
|
1249
|
+
if (/\.integration\.|IntegrationTest|E2E/.test(filePath))
|
|
1250
|
+
return 'integration';
|
|
1251
|
+
return 'unit';
|
|
1252
|
+
}
|
|
1253
|
+
function countAssertions(language, content) {
|
|
1254
|
+
var _a, _b, _c;
|
|
1255
|
+
if (language === 'java' || language === 'kotlin') {
|
|
1256
|
+
return ((_a = content.match(/\bassert\w*\s*\(|\bverify\s*\(/g)) !== null && _a !== void 0 ? _a : []).length;
|
|
1257
|
+
}
|
|
1258
|
+
if (language === 'javascript' || language === 'typescript') {
|
|
1259
|
+
return ((_b = content.match(/expect\s*\(|assert\./g)) !== null && _b !== void 0 ? _b : []).length;
|
|
1260
|
+
}
|
|
1261
|
+
if (language === 'python') {
|
|
1262
|
+
return ((_c = content.match(/\bassert\b|assert_called/g)) !== null && _c !== void 0 ? _c : []).length;
|
|
1263
|
+
}
|
|
1264
|
+
return 0;
|
|
1265
|
+
}
|
|
1266
|
+
function summarizeSmells(smells) {
|
|
1267
|
+
return {
|
|
1268
|
+
CRITICAL: smells.filter((smell) => smell.severity === 'CRITICAL').length,
|
|
1269
|
+
HIGH: smells.filter((smell) => smell.severity === 'HIGH').length,
|
|
1270
|
+
MEDIUM: smells.filter((smell) => smell.severity === 'MEDIUM').length,
|
|
1271
|
+
};
|
|
1272
|
+
}
|
|
1273
|
+
function severityAtOrAbove(actual, threshold) {
|
|
1274
|
+
if (threshold === 'none')
|
|
1275
|
+
return false;
|
|
1276
|
+
const order = {
|
|
1277
|
+
CRITICAL: 3,
|
|
1278
|
+
HIGH: 2,
|
|
1279
|
+
MEDIUM: 1,
|
|
1280
|
+
};
|
|
1281
|
+
return order[actual] >= order[threshold];
|
|
1282
|
+
}
|
|
1283
|
+
function detectTestLanguage(filePath) {
|
|
1284
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
1285
|
+
if (ext === '.java')
|
|
1286
|
+
return 'java';
|
|
1287
|
+
if (ext === '.kt' || ext === '.kts')
|
|
1288
|
+
return 'kotlin';
|
|
1289
|
+
if (ext === '.js' || ext === '.jsx')
|
|
1290
|
+
return 'javascript';
|
|
1291
|
+
if (ext === '.ts' || ext === '.tsx')
|
|
1292
|
+
return 'typescript';
|
|
1293
|
+
if (ext === '.py')
|
|
1294
|
+
return 'python';
|
|
1295
|
+
return 'other';
|
|
1296
|
+
}
|
|
1297
|
+
function findMatchingBrace(content, openBraceIndex) {
|
|
1298
|
+
let depth = 0;
|
|
1299
|
+
for (let index = openBraceIndex; index < content.length; index += 1) {
|
|
1300
|
+
const char = content[index];
|
|
1301
|
+
if (char === '{')
|
|
1302
|
+
depth += 1;
|
|
1303
|
+
if (char === '}') {
|
|
1304
|
+
depth -= 1;
|
|
1305
|
+
if (depth === 0)
|
|
1306
|
+
return index;
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
return content.length - 1;
|
|
1310
|
+
}
|
|
1311
|
+
function lineNumberForIndex(content, index) {
|
|
1312
|
+
return content.slice(0, index).split('\n').length;
|
|
1313
|
+
}
|
|
1314
|
+
function extractLeadingAnnotations(content, index) {
|
|
1315
|
+
const precedingLines = content
|
|
1316
|
+
.slice(0, index)
|
|
1317
|
+
.split('\n')
|
|
1318
|
+
.slice(-6)
|
|
1319
|
+
.map((line) => line.trim())
|
|
1320
|
+
.filter(Boolean);
|
|
1321
|
+
const annotations = [];
|
|
1322
|
+
for (let lineIndex = precedingLines.length - 1; lineIndex >= 0; lineIndex -= 1) {
|
|
1323
|
+
const line = precedingLines[lineIndex];
|
|
1324
|
+
if (line.startsWith('@')) {
|
|
1325
|
+
annotations.unshift(line);
|
|
1326
|
+
continue;
|
|
1327
|
+
}
|
|
1328
|
+
if (annotations.length > 0)
|
|
1329
|
+
break;
|
|
1330
|
+
}
|
|
1331
|
+
return annotations;
|
|
1332
|
+
}
|
|
1333
|
+
function extractCounters(xml) {
|
|
1334
|
+
const counters = {};
|
|
1335
|
+
for (const match of xml.matchAll(/<counter\b[^>]*type="([^"]+)"[^>]*missed="(\d+)"[^>]*covered="(\d+)"/g)) {
|
|
1336
|
+
counters[match[1]] = {
|
|
1337
|
+
missed: Number(match[2]),
|
|
1338
|
+
covered: Number(match[3]),
|
|
1339
|
+
};
|
|
1340
|
+
}
|
|
1341
|
+
return counters;
|
|
1342
|
+
}
|
|
1343
|
+
function coveragePercent(counter) {
|
|
1344
|
+
if (!counter)
|
|
1345
|
+
return null;
|
|
1346
|
+
return safePercent(counter.covered, counter.covered + counter.missed);
|
|
1347
|
+
}
|
|
1348
|
+
function safePercent(covered, total) {
|
|
1349
|
+
if (!Number.isFinite(total) || total <= 0)
|
|
1350
|
+
return null;
|
|
1351
|
+
return Number(((covered / total) * 100).toFixed(1));
|
|
1352
|
+
}
|
|
1353
|
+
function readPct(total, key) {
|
|
1354
|
+
const entry = total[key];
|
|
1355
|
+
if (!entry || typeof entry.pct !== 'number')
|
|
1356
|
+
return null;
|
|
1357
|
+
return Number(entry.pct.toFixed(1));
|
|
1358
|
+
}
|
|
1359
|
+
function readNumericAttribute(xml, name) {
|
|
1360
|
+
var _a, _b;
|
|
1361
|
+
return Number((_b = (_a = new RegExp(`${name}="([^\"]+)"`).exec(xml)) === null || _a === void 0 ? void 0 : _a[1]) !== null && _b !== void 0 ? _b : 0);
|
|
1362
|
+
}
|
|
1363
|
+
function extractXmlText(xml, tag) {
|
|
1364
|
+
var _a, _b;
|
|
1365
|
+
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
|
+
}
|
|
1367
|
+
function dedupeBy(items, keyFn) {
|
|
1368
|
+
const seen = new Set();
|
|
1369
|
+
const deduped = [];
|
|
1370
|
+
for (const item of items) {
|
|
1371
|
+
const key = keyFn(item);
|
|
1372
|
+
if (seen.has(key))
|
|
1373
|
+
continue;
|
|
1374
|
+
seen.add(key);
|
|
1375
|
+
deduped.push(item);
|
|
1376
|
+
}
|
|
1377
|
+
return deduped;
|
|
1378
|
+
}
|
|
1379
|
+
function dedupeStrings(items) {
|
|
1380
|
+
return Array.from(new Set(items));
|
|
1381
|
+
}
|
|
1382
|
+
function normalizePath(value) {
|
|
1383
|
+
return value.replace(/\\/g, '/');
|
|
1384
|
+
}
|
|
1385
|
+
function safeRead(filePath) {
|
|
1386
|
+
try {
|
|
1387
|
+
return fs.readFileSync(filePath, 'utf-8');
|
|
1388
|
+
}
|
|
1389
|
+
catch {
|
|
1390
|
+
return '';
|
|
1391
|
+
}
|
|
1392
|
+
}
|