From 1f132d6bd33f0f199509d9b89e9392708f210015 Mon Sep 17 00:00:00 2001 From: Matt Carvin <90224411+mcarvin8@users.noreply.github.com> Date: Thu, 19 Dec 2024 10:18:24 -0500 Subject: [PATCH] refactor: break into smaller functions --- README.md | 6 +- src/helpers/generateXml.ts | 14 ++ src/helpers/transformDeployCoverageReport.ts | 225 +++++++++-------- src/helpers/transformTestCoverageReport.ts | 245 ++++++++++--------- src/helpers/types.ts | 2 +- 5 files changed, 281 insertions(+), 211 deletions(-) create mode 100644 src/helpers/generateXml.ts diff --git a/README.md b/README.md index 089c612..f33d87e 100644 --- a/README.md +++ b/README.md @@ -230,14 +230,14 @@ and this format for Cobertura: ```xml - + . - + @@ -273,7 +273,7 @@ and this format for Cobertura: - + diff --git a/src/helpers/generateXml.ts b/src/helpers/generateXml.ts new file mode 100644 index 0000000..774a827 --- /dev/null +++ b/src/helpers/generateXml.ts @@ -0,0 +1,14 @@ +'use strict'; + +import { create } from 'xmlbuilder2'; +import { SonarCoverageObject, CoberturaCoverageObject } from './types.js'; + +export function generateXml(coverageObj: SonarCoverageObject | CoberturaCoverageObject, format: string): string { + let xml = create(coverageObj).end({ prettyPrint: true, indent: ' ', headless: format === 'cobertura' }); + + if (format === 'cobertura') { + xml = `\n\n${xml}`; + } + + return xml; +} diff --git a/src/helpers/transformDeployCoverageReport.ts b/src/helpers/transformDeployCoverageReport.ts index f081f35..1c27df0 100644 --- a/src/helpers/transformDeployCoverageReport.ts +++ b/src/helpers/transformDeployCoverageReport.ts @@ -1,63 +1,42 @@ 'use strict'; /* eslint-disable no-await-in-loop */ -import { create } from 'xmlbuilder2'; import { DeployCoverageData, SonarCoverageObject, CoberturaCoverageObject, SonarClass, CoberturaClass, + CoberturaPackage, } from './types.js'; import { getPackageDirectories } from './getPackageDirectories.js'; import { findFilePath } from './findFilePath.js'; import { setCoveredLinesSonar } from './setCoveredLinesSonar.js'; import { setCoveredLinesCobertura } from './setCoveredLinesCobertura.js'; import { normalizePathToUnix } from './normalizePathToUnix.js'; +import { generateXml } from './generateXml.js'; export async function transformDeployCoverageReport( data: DeployCoverageData, format: string ): Promise<{ xml: string; warnings: string[]; filesProcessed: number }> { + if (!['sonar', 'cobertura'].includes(format)) { + throw new Error(`Unsupported format: ${format}`); + } + const warnings: string[] = []; let filesProcessed: number = 0; const { repoRoot, packageDirectories } = await getPackageDirectories(); - if (format === 'sonar') { - const coverageObj: SonarCoverageObject = { coverage: { '@version': '1', file: [] } }; - - for (const fileName in data) { - if (!Object.hasOwn(data, fileName)) continue; - const fileInfo = data[fileName]; - const formattedFileName = fileName.replace(/no-map[\\/]+/, ''); - const relativeFilePath = await findFilePath(formattedFileName, packageDirectories, repoRoot); - if (relativeFilePath === undefined) { - warnings.push(`The file name ${formattedFileName} was not found in any package directory.`); - continue; - } - const uncoveredLines = Object.keys(fileInfo.s) - .filter((lineNumber) => fileInfo.s[lineNumber] === 0) - .map(Number); - const coveredLines = Object.keys(fileInfo.s) - .filter((lineNumber) => fileInfo.s[lineNumber] === 1) - .map(Number); - - const fileObj: SonarClass = { - '@path': normalizePathToUnix(relativeFilePath), - lineToCover: uncoveredLines.map((lineNumber: number) => ({ - '@lineNumber': lineNumber, - '@covered': 'false', - })), - }; + // Initialize format-specific coverage objects + let coverageObj: SonarCoverageObject | CoberturaCoverageObject; - await setCoveredLinesSonar(coveredLines, uncoveredLines, repoRoot, relativeFilePath, fileObj); - filesProcessed++; - coverageObj.coverage.file.push(fileObj); - } - const xml = create(coverageObj).end({ prettyPrint: true, indent: ' ' }); - return { xml, warnings, filesProcessed }; - } else if (format === 'cobertura') { - const coberturaObj: CoberturaCoverageObject = { + if (format === 'sonar') { + coverageObj = { + coverage: { '@version': '1', file: [] }, + } as SonarCoverageObject; + } else { + coverageObj = { coverage: { '@lines-valid': 0, '@lines-covered': 0, @@ -71,72 +50,122 @@ export async function transformDeployCoverageReport( sources: { source: ['.'] }, packages: { package: [] }, }, - }; - - // Single package for all classes - const packageObj = { - '@name': 'main', - '@line-rate': 0, - '@branch-rate': 1, - classes: { class: [] as CoberturaClass[] }, - }; - coberturaObj.coverage.packages.package.push(packageObj); - - for (const fileName in data) { - if (!Object.hasOwn(data, fileName)) continue; - const fileInfo = data[fileName]; - const formattedFileName = fileName.replace(/no-map[\\/]+/, ''); - const relativeFilePath = await findFilePath(formattedFileName, packageDirectories, repoRoot); - if (relativeFilePath === undefined) { - warnings.push(`The file name ${formattedFileName} was not found in any package directory.`); - continue; - } - const uncoveredLines = Object.keys(fileInfo.s) - .filter((lineNumber) => fileInfo.s[lineNumber] === 0) - .map(Number); - const coveredLines = Object.keys(fileInfo.s) - .filter((lineNumber) => fileInfo.s[lineNumber] === 1) - .map(Number); - - const classObj: CoberturaClass = { - '@name': formattedFileName, - '@filename': normalizePathToUnix(relativeFilePath), - '@line-rate': (coveredLines.length / (coveredLines.length + uncoveredLines.length)).toFixed(4), - '@branch-rate': '1', - methods: {}, - lines: { - line: [ - ...uncoveredLines.map((lineNumber) => ({ - '@number': lineNumber, - '@hits': 0, - '@branch': 'false', - })), - ], - }, - }; - - await setCoveredLinesCobertura(coveredLines, uncoveredLines, repoRoot, relativeFilePath, classObj); - - // Update package and overall coverage metrics - coberturaObj.coverage['@lines-valid'] += uncoveredLines.length + coveredLines.length; - coberturaObj.coverage['@lines-covered'] += coveredLines.length; - - packageObj.classes.class.push(classObj); - filesProcessed++; - } + } as CoberturaCoverageObject; + } + + const packageObj = + format === 'cobertura' + ? { + '@name': 'main', + '@line-rate': 0, + '@branch-rate': 1, + classes: { class: [] as CoberturaClass[] }, + } + : null; + + if (packageObj) { + (coverageObj as CoberturaCoverageObject).coverage.packages.package.push(packageObj); + } + + for (const fileName in data) { + if (!Object.hasOwn(data, fileName)) continue; - // Update overall line-rate for the package - packageObj['@line-rate'] = Number( - (coberturaObj.coverage['@lines-covered'] / coberturaObj.coverage['@lines-valid']).toFixed(4) - ); - coberturaObj.coverage['@line-rate'] = packageObj['@line-rate']; + const fileInfo = data[fileName]; + const formattedFileName = fileName.replace(/no-map[\\/]+/, ''); + const relativeFilePath = await findFilePath(formattedFileName, packageDirectories, repoRoot); - let xml = create(coberturaObj).end({ prettyPrint: true, indent: ' ', headless: true }); + if (relativeFilePath === undefined) { + warnings.push(`The file name ${formattedFileName} was not found in any package directory.`); + continue; + } + + const uncoveredLines = Object.keys(fileInfo.s) + .filter((lineNumber) => fileInfo.s[lineNumber] === 0) + .map(Number); + const coveredLines = Object.keys(fileInfo.s) + .filter((lineNumber) => fileInfo.s[lineNumber] === 1) + .map(Number); + + if (format === 'sonar') { + await handleSonarFormat( + relativeFilePath, + uncoveredLines, + coveredLines, + repoRoot, + coverageObj as SonarCoverageObject + ); + } else { + await handleCoberturaFormat( + relativeFilePath, + formattedFileName, + uncoveredLines, + coveredLines, + repoRoot, + coverageObj as CoberturaCoverageObject, + packageObj! + ); + } - // Add DOCTYPE declaration at the beginning of the XML - xml = `\n\n${xml}`; - return { xml, warnings, filesProcessed }; + filesProcessed++; } - throw new Error(`Unsupported format: ${format}`); + const xml = generateXml(coverageObj, format); + return { xml, warnings, filesProcessed }; +} + +async function handleSonarFormat( + filePath: string, + uncoveredLines: number[], + coveredLines: number[], + repoRoot: string, + coverageObj: SonarCoverageObject +): Promise { + const fileObj: SonarClass = { + '@path': normalizePathToUnix(filePath), + lineToCover: uncoveredLines.map((lineNumber) => ({ + '@lineNumber': lineNumber, + '@covered': 'false', + })), + }; + + await setCoveredLinesSonar(coveredLines, uncoveredLines, repoRoot, filePath, fileObj); + coverageObj.coverage.file.push(fileObj); +} + +async function handleCoberturaFormat( + filePath: string, + fileName: string, + uncoveredLines: number[], + coveredLines: number[], + repoRoot: string, + coverageObj: CoberturaCoverageObject, + packageObj: CoberturaPackage +): Promise { + const classObj: CoberturaClass = { + '@name': fileName, + '@filename': normalizePathToUnix(filePath), + '@line-rate': (coveredLines.length / (coveredLines.length + uncoveredLines.length)).toFixed(4), + '@branch-rate': '1', + methods: {}, + lines: { + line: [ + ...uncoveredLines.map((lineNumber) => ({ + '@number': lineNumber, + '@hits': 0, + '@branch': 'false', + })), + ], + }, + }; + + await setCoveredLinesCobertura(coveredLines, uncoveredLines, repoRoot, filePath, classObj); + + coverageObj.coverage['@lines-valid'] += uncoveredLines.length + coveredLines.length; + coverageObj.coverage['@lines-covered'] += coveredLines.length; + packageObj.classes.class.push(classObj); + + packageObj['@line-rate'] = Number( + (coverageObj.coverage['@lines-covered'] / coverageObj.coverage['@lines-valid']).toFixed(4) + ); + coverageObj.coverage['@line-rate'] = packageObj['@line-rate']; } diff --git a/src/helpers/transformTestCoverageReport.ts b/src/helpers/transformTestCoverageReport.ts index 5966e21..87e61ba 100644 --- a/src/helpers/transformTestCoverageReport.ts +++ b/src/helpers/transformTestCoverageReport.ts @@ -1,59 +1,40 @@ 'use strict'; /* eslint-disable no-await-in-loop */ -import { create } from 'xmlbuilder2'; -import { TestCoverageData, SonarCoverageObject, SonarClass, CoberturaCoverageObject, CoberturaClass } from './types.js'; +import { + TestCoverageData, + SonarCoverageObject, + SonarClass, + CoberturaPackage, + CoberturaCoverageObject, + CoberturaClass, +} from './types.js'; import { getPackageDirectories } from './getPackageDirectories.js'; import { findFilePath } from './findFilePath.js'; import { normalizePathToUnix } from './normalizePathToUnix.js'; +import { generateXml } from './generateXml.js'; export async function transformTestCoverageReport( testCoverageData: TestCoverageData[], format: string ): Promise<{ xml: string; warnings: string[]; filesProcessed: number }> { + if (!['sonar', 'cobertura'].includes(format)) { + throw new Error(`Unsupported format: ${format}`); + } + const warnings: string[] = []; let filesProcessed: number = 0; const { repoRoot, packageDirectories } = await getPackageDirectories(); - let coverageData = testCoverageData; - if (!Array.isArray(coverageData)) { - coverageData = [coverageData]; - } - if (format === 'sonar') { - const coverageObj: SonarCoverageObject = { coverage: { '@version': '1', file: [] } }; - - for (const data of coverageData) { - const name = data?.name; - const lines = data?.lines; - - if (!name || !lines) continue; - - const formattedFileName = name.replace(/no-map[\\/]+/, ''); - const relativeFilePath = await findFilePath(formattedFileName, packageDirectories, repoRoot); - if (relativeFilePath === undefined) { - warnings.push(`The file name ${formattedFileName} was not found in any package directory.`); - continue; - } - - const fileObj: SonarClass = { - '@path': normalizePathToUnix(relativeFilePath), - lineToCover: [], - }; - - for (const [lineNumber, isCovered] of Object.entries(lines)) { - fileObj.lineToCover.push({ - '@lineNumber': Number(lineNumber), - '@covered': `${isCovered === 1}`, - }); - } - filesProcessed++; - coverageObj.coverage.file.push(fileObj); - } + // Initialize format-specific coverage objects + let coverageObj: SonarCoverageObject | CoberturaCoverageObject; - const xml = create(coverageObj).end({ prettyPrint: true, indent: ' ' }); - return { xml, warnings, filesProcessed }; - } else if (format === 'cobertura') { - const coberturaObj: CoberturaCoverageObject = { + if (format === 'sonar') { + coverageObj = { + coverage: { '@version': '1', file: [] }, + } as SonarCoverageObject; + } else { + coverageObj = { coverage: { '@lines-valid': 0, '@lines-covered': 0, @@ -67,79 +48,125 @@ export async function transformTestCoverageReport( sources: { source: ['.'] }, packages: { package: [] }, }, - }; - - // Single package for all classes - const packageObj = { - '@name': 'main', - '@line-rate': 0, - '@branch-rate': 1, - classes: { class: [] as CoberturaClass[] }, - }; - coberturaObj.coverage.packages.package.push(packageObj); - - for (const data of coverageData) { - const name = data?.name; - const lines = data?.lines; - - if (!name || !lines) continue; - - const formattedFileName = name.replace(/no-map[\\/]+/, ''); - const relativeFilePath = await findFilePath(formattedFileName, packageDirectories, repoRoot); - if (relativeFilePath === undefined) { - warnings.push(`The file name ${formattedFileName} was not found in any package directory.`); - continue; - } - - const uncoveredLines = Object.entries(lines) - .filter(([, isCovered]) => isCovered === 0) - .map(([lineNumber]) => Number(lineNumber)); - const coveredLines = Object.entries(lines) - .filter(([, isCovered]) => isCovered === 1) - .map(([lineNumber]) => Number(lineNumber)); - - const classObj: CoberturaClass = { - '@name': formattedFileName, - '@filename': normalizePathToUnix(relativeFilePath), - '@line-rate': (coveredLines.length / (coveredLines.length + uncoveredLines.length)).toFixed(4), - '@branch-rate': '1', - methods: {}, - lines: { - line: [ - ...uncoveredLines.map((lineNumber) => ({ - '@number': lineNumber, - '@hits': 0, - '@branch': 'false', - })), - ...coveredLines.map((lineNumber) => ({ - '@number': lineNumber, - '@hits': 1, - '@branch': 'false', - })), - ], - }, - }; - - // Update package and overall coverage metrics - coberturaObj.coverage['@lines-valid'] += uncoveredLines.length + coveredLines.length; - coberturaObj.coverage['@lines-covered'] += coveredLines.length; - - packageObj.classes.class.push(classObj); - filesProcessed++; + } as CoberturaCoverageObject; + } + + const packageObj = + format === 'cobertura' + ? { + '@name': 'main', + '@line-rate': 0, + '@branch-rate': 1, + classes: { class: [] as CoberturaClass[] }, + } + : null; + + if (packageObj) { + (coverageObj as CoberturaCoverageObject).coverage.packages.package.push(packageObj); + } + + let coverageData = testCoverageData; + if (!Array.isArray(coverageData)) { + coverageData = [coverageData]; + } + + for (const data of coverageData) { + const name = data?.name; + const lines = data?.lines; + + if (!name || !lines) continue; + + const formattedFileName = name.replace(/no-map[\\/]+/, ''); + const relativeFilePath = await findFilePath(formattedFileName, packageDirectories, repoRoot); + if (relativeFilePath === undefined) { + warnings.push(`The file name ${formattedFileName} was not found in any package directory.`); + continue; + } + + const uncoveredLines = Object.entries(lines) + .filter(([, isCovered]) => isCovered === 0) + .map(([lineNumber]) => Number(lineNumber)); + const coveredLines = Object.entries(lines) + .filter(([, isCovered]) => isCovered === 1) + .map(([lineNumber]) => Number(lineNumber)); + + if (format === 'sonar') { + handleSonarFormat(relativeFilePath, lines, repoRoot, coverageObj as SonarCoverageObject); + } else { + handleCoberturaFormat( + relativeFilePath, + formattedFileName, + lines, + uncoveredLines, + coveredLines, + repoRoot, + coverageObj as CoberturaCoverageObject, + packageObj! + ); } - // Update overall line-rate for the package - packageObj['@line-rate'] = parseFloat( - (coberturaObj.coverage['@lines-covered'] / coberturaObj.coverage['@lines-valid']).toFixed(4) - ); - coberturaObj.coverage['@line-rate'] = packageObj['@line-rate']; + filesProcessed++; + } + const xml = generateXml(coverageObj, format); + return { xml, warnings, filesProcessed }; +} - let xml = create(coberturaObj).end({ prettyPrint: true, indent: ' ', headless: true }); +function handleSonarFormat( + filePath: string, + lines: Record, + repoRoot: string, + coverageObj: SonarCoverageObject +): void { + const fileObj: SonarClass = { + '@path': normalizePathToUnix(filePath), + lineToCover: [], + }; + + for (const [lineNumber, isCovered] of Object.entries(lines)) { + fileObj.lineToCover.push({ + '@lineNumber': Number(lineNumber), + '@covered': `${isCovered === 1}`, + }); + } - // Add DOCTYPE declaration at the beginning of the XML - xml = `\n\n${xml}`; - return { xml, warnings, filesProcessed }; + coverageObj.coverage.file.push(fileObj); +} + +function handleCoberturaFormat( + filePath: string, + fileName: string, + lines: Record, + uncoveredLines: number[], + coveredLines: number[], + repoRoot: string, + coverageObj: CoberturaCoverageObject, + packageObj: CoberturaPackage +): void { + const classObj: CoberturaClass = { + '@name': fileName, + '@filename': normalizePathToUnix(filePath), + '@line-rate': (coveredLines.length / (coveredLines.length + uncoveredLines.length)).toFixed(4), + '@branch-rate': '1', + methods: {}, + lines: { + line: [], + }, + }; + + for (const [lineNumber, isCovered] of Object.entries(lines)) { + classObj.lines.line.push({ + '@number': Number(lineNumber), + '@hits': isCovered === 1 ? 1 : 0, + '@branch': 'false', + }); } - throw new Error(`Unsupported format: ${format}`); + coverageObj.coverage['@lines-valid'] += uncoveredLines.length + coveredLines.length; + coverageObj.coverage['@lines-covered'] += coveredLines.length; + packageObj.classes.class.push(classObj); + + packageObj['@line-rate'] = Number( + (coverageObj.coverage['@lines-covered'] / coverageObj.coverage['@lines-valid']).toFixed(4) + ); + coverageObj.coverage['@line-rate'] = packageObj['@line-rate']; } diff --git a/src/helpers/types.ts b/src/helpers/types.ts index 6d0b90c..a27fa34 100644 --- a/src/helpers/types.ts +++ b/src/helpers/types.ts @@ -76,7 +76,7 @@ export type CoberturaClass = { }; }; -type CoberturaPackage = { +export type CoberturaPackage = { '@name': string; '@line-rate': number; '@branch-rate': number;