| | "use strict"; |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { |
| | if (k2 === undefined) k2 = k; |
| | var desc = Object.getOwnPropertyDescriptor(m, k); |
| | if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { |
| | desc = { enumerable: true, get: function() { return m[k]; } }; |
| | } |
| | Object.defineProperty(o, k2, desc); |
| | }) : (function(o, m, k, k2) { |
| | if (k2 === undefined) k2 = k; |
| | o[k2] = m[k]; |
| | })); |
| | var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { |
| | Object.defineProperty(o, "default", { enumerable: true, value: v }); |
| | }) : function(o, v) { |
| | o["default"] = v; |
| | }); |
| | var __importStar = (this && this.__importStar) || (function () { |
| | var ownKeys = function(o) { |
| | ownKeys = Object.getOwnPropertyNames || function (o) { |
| | var ar = []; |
| | for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; |
| | return ar; |
| | }; |
| | return ownKeys(o); |
| | }; |
| | return function (mod) { |
| | if (mod && mod.__esModule) return mod; |
| | var result = {}; |
| | if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); |
| | __setModuleDefault(result, mod); |
| | return result; |
| | }; |
| | })(); |
| | Object.defineProperty(exports, "__esModule", { value: true }); |
| | exports.parseIstanbulCoverage = parseIstanbulCoverage; |
| | exports.findCoverageReport = findCoverageReport; |
| | exports.getFileCoverage = getFileCoverage; |
| | exports.suggestTests = suggestTests; |
| | exports.shouldRouteToTester = shouldRouteToTester; |
| | exports.getCoverageRoutingWeight = getCoverageRoutingWeight; |
| | const fs = __importStar(require("fs")); |
| | const path = __importStar(require("path")); |
| | |
| | |
| | |
| | function parseIstanbulCoverage(coveragePath) { |
| | const files = new Map(); |
| | const lowCoverageFiles = []; |
| | const uncoveredFiles = []; |
| | let totalLines = 0, coveredLines = 0; |
| | let totalFunctions = 0, coveredFunctions = 0; |
| | let totalBranches = 0, coveredBranches = 0; |
| | try { |
| | const coverage = JSON.parse(fs.readFileSync(coveragePath, 'utf8')); |
| | for (const [file, data] of Object.entries(coverage)) { |
| | |
| | if (file.includes('.test.') || file.includes('.spec.') || file.includes('__tests__')) { |
| | continue; |
| | } |
| | |
| | const statements = Object.values(data.s || {}); |
| | const linesCovered = statements.filter(n => n > 0).length; |
| | const linesTotal = statements.length; |
| | |
| | const functions = Object.values(data.f || {}); |
| | const fnCovered = functions.filter(n => n > 0).length; |
| | const fnTotal = functions.length; |
| | |
| | const branches = Object.values(data.b || {}).flat(); |
| | const brCovered = branches.filter(n => n > 0).length; |
| | const brTotal = branches.length; |
| | |
| | const uncoveredLines = []; |
| | for (const [line, count] of Object.entries(data.s || {})) { |
| | if (count === 0) { |
| | uncoveredLines.push(parseInt(line)); |
| | } |
| | } |
| | |
| | const uncoveredFunctions = []; |
| | const fnMap = data.fnMap || {}; |
| | for (const [fnId, count] of Object.entries(data.f || {})) { |
| | if (count === 0 && fnMap[fnId]) { |
| | uncoveredFunctions.push(fnMap[fnId].name || `function_${fnId}`); |
| | } |
| | } |
| | const linePercentage = linesTotal > 0 ? (linesCovered / linesTotal) * 100 : 100; |
| | const fnPercentage = fnTotal > 0 ? (fnCovered / fnTotal) * 100 : 100; |
| | const brPercentage = brTotal > 0 ? (brCovered / brTotal) * 100 : 100; |
| | files.set(file, { |
| | file, |
| | lines: { total: linesTotal, covered: linesCovered, percentage: linePercentage }, |
| | functions: { total: fnTotal, covered: fnCovered, percentage: fnPercentage }, |
| | branches: { total: brTotal, covered: brCovered, percentage: brPercentage }, |
| | uncoveredLines, |
| | uncoveredFunctions, |
| | }); |
| | totalLines += linesTotal; |
| | coveredLines += linesCovered; |
| | totalFunctions += fnTotal; |
| | coveredFunctions += fnCovered; |
| | totalBranches += brTotal; |
| | coveredBranches += brCovered; |
| | if (linePercentage < 50) { |
| | lowCoverageFiles.push(file); |
| | } |
| | if (linePercentage === 0 && linesTotal > 0) { |
| | uncoveredFiles.push(file); |
| | } |
| | } |
| | } |
| | catch (e) { |
| | |
| | } |
| | return { |
| | files, |
| | overall: { |
| | lines: totalLines > 0 ? (coveredLines / totalLines) * 100 : 0, |
| | functions: totalFunctions > 0 ? (coveredFunctions / totalFunctions) * 100 : 0, |
| | branches: totalBranches > 0 ? (coveredBranches / totalBranches) * 100 : 0, |
| | }, |
| | lowCoverageFiles, |
| | uncoveredFiles, |
| | }; |
| | } |
| | |
| | |
| | |
| | function findCoverageReport(projectRoot = process.cwd()) { |
| | const possiblePaths = [ |
| | 'coverage/coverage-final.json', |
| | 'coverage/coverage-summary.json', |
| | '.nyc_output/coverage.json', |
| | 'coverage.json', |
| | 'coverage/lcov.info', |
| | ]; |
| | for (const p of possiblePaths) { |
| | const fullPath = path.join(projectRoot, p); |
| | if (fs.existsSync(fullPath)) { |
| | return fullPath; |
| | } |
| | } |
| | return null; |
| | } |
| | |
| | |
| | |
| | function getFileCoverage(file, summary) { |
| | if (!summary) { |
| | const reportPath = findCoverageReport(); |
| | if (!reportPath) |
| | return null; |
| | summary = parseIstanbulCoverage(reportPath); |
| | } |
| | |
| | if (summary.files.has(file)) { |
| | return summary.files.get(file); |
| | } |
| | |
| | const basename = path.basename(file); |
| | for (const [key, data] of summary.files) { |
| | if (key.endsWith(file) || key.endsWith(basename)) { |
| | return data; |
| | } |
| | } |
| | return null; |
| | } |
| | |
| | |
| | |
| | function suggestTests(files, summary) { |
| | if (!summary) { |
| | const reportPath = findCoverageReport(); |
| | if (reportPath) { |
| | summary = parseIstanbulCoverage(reportPath); |
| | } |
| | } |
| | const suggestions = []; |
| | for (const file of files) { |
| | const coverage = summary ? getFileCoverage(file, summary) : null; |
| | |
| | const ext = path.extname(file); |
| | const base = path.basename(file, ext); |
| | const dir = path.dirname(file); |
| | const possibleTestFiles = [ |
| | path.join(dir, `${base}.test${ext}`), |
| | path.join(dir, `${base}.spec${ext}`), |
| | path.join(dir, '__tests__', `${base}.test${ext}`), |
| | path.join('test', `${base}.test${ext}`), |
| | path.join('tests', `${base}.test${ext}`), |
| | ]; |
| | const existingTestFile = possibleTestFiles.find(t => fs.existsSync(t)); |
| | const testFile = existingTestFile || possibleTestFiles[0]; |
| | if (!coverage) { |
| | suggestions.push({ |
| | file, |
| | testFile, |
| | reason: 'No coverage data - needs test file', |
| | priority: 'high', |
| | coverage: 0, |
| | uncoveredFunctions: [], |
| | }); |
| | } |
| | else if (coverage.lines.percentage < 30) { |
| | suggestions.push({ |
| | file, |
| | testFile, |
| | reason: `Very low coverage (${coverage.lines.percentage.toFixed(1)}%)`, |
| | priority: 'high', |
| | coverage: coverage.lines.percentage, |
| | uncoveredFunctions: coverage.uncoveredFunctions, |
| | }); |
| | } |
| | else if (coverage.lines.percentage < 70) { |
| | suggestions.push({ |
| | file, |
| | testFile, |
| | reason: `Low coverage (${coverage.lines.percentage.toFixed(1)}%)`, |
| | priority: 'medium', |
| | coverage: coverage.lines.percentage, |
| | uncoveredFunctions: coverage.uncoveredFunctions, |
| | }); |
| | } |
| | else if (coverage.uncoveredFunctions.length > 0) { |
| | suggestions.push({ |
| | file, |
| | testFile, |
| | reason: `${coverage.uncoveredFunctions.length} untested functions`, |
| | priority: 'low', |
| | coverage: coverage.lines.percentage, |
| | uncoveredFunctions: coverage.uncoveredFunctions, |
| | }); |
| | } |
| | } |
| | return suggestions.sort((a, b) => { |
| | const priorityOrder = { high: 0, medium: 1, low: 2 }; |
| | return priorityOrder[a.priority] - priorityOrder[b.priority]; |
| | }); |
| | } |
| | |
| | |
| | |
| | function shouldRouteToTester(file, summary) { |
| | const coverage = getFileCoverage(file, summary); |
| | if (!coverage) { |
| | return { |
| | route: true, |
| | reason: 'No test coverage data available', |
| | coverage: 0, |
| | }; |
| | } |
| | if (coverage.lines.percentage < 50) { |
| | return { |
| | route: true, |
| | reason: `Low coverage: ${coverage.lines.percentage.toFixed(1)}%`, |
| | coverage: coverage.lines.percentage, |
| | }; |
| | } |
| | if (coverage.uncoveredFunctions.length > 3) { |
| | return { |
| | route: true, |
| | reason: `${coverage.uncoveredFunctions.length} untested functions`, |
| | coverage: coverage.lines.percentage, |
| | }; |
| | } |
| | return { |
| | route: false, |
| | reason: `Adequate coverage: ${coverage.lines.percentage.toFixed(1)}%`, |
| | coverage: coverage.lines.percentage, |
| | }; |
| | } |
| | |
| | |
| | |
| | function getCoverageRoutingWeight(file, summary) { |
| | const coverage = getFileCoverage(file, summary); |
| | if (!coverage) { |
| | |
| | return { coder: 0.3, tester: 0.5, reviewer: 0.2 }; |
| | } |
| | const pct = coverage.lines.percentage; |
| | if (pct < 30) { |
| | |
| | return { coder: 0.2, tester: 0.6, reviewer: 0.2 }; |
| | } |
| | else if (pct < 60) { |
| | |
| | return { coder: 0.4, tester: 0.4, reviewer: 0.2 }; |
| | } |
| | else if (pct < 80) { |
| | |
| | return { coder: 0.5, tester: 0.3, reviewer: 0.2 }; |
| | } |
| | else { |
| | |
| | return { coder: 0.5, tester: 0.2, reviewer: 0.3 }; |
| | } |
| | } |
| | exports.default = { |
| | parseIstanbulCoverage, |
| | findCoverageReport, |
| | getFileCoverage, |
| | suggestTests, |
| | shouldRouteToTester, |
| | getCoverageRoutingWeight, |
| | }; |
| |
|