diff --git a/src/coverage-system/renderer.ts b/src/coverage-system/renderer.ts index 6edf767..0ed037e 100644 --- a/src/coverage-system/renderer.ts +++ b/src/coverage-system/renderer.ts @@ -12,6 +12,12 @@ export interface ICoverageLines { none: Range[]; } +interface CoverageSets { + full: Set; + partial: Set; + none: Set; +} + export class Renderer { private configStore: Config; private sectionFinder: SectionFinder; @@ -33,29 +39,31 @@ export class Renderer { sections: Map, textEditors: readonly TextEditor[], ) { - const coverageLines: ICoverageLines = { - full: [], - none: [], - partial: [], - }; - + // Single-pass iteration for better performance textEditors.forEach((textEditor) => { // Remove all decorations first to prevent graphical issues this.removeDecorationsForEditor(textEditor); - }); - textEditors.forEach((textEditor) => { // Reset lines for new editor - coverageLines.full = []; - coverageLines.none = []; - coverageLines.partial = []; + const coverageSets: CoverageSets = { + full: new Set(), + none: new Set(), + partial: new Set(), + }; // find the section(s) (or undefined) by looking relatively at each workspace // users can also optional use absolute instead of relative for this const foundSections = this.sectionFinder.findSectionsForEditor(textEditor, sections); if (!foundSections.length) { return; } - this.filterCoverage(foundSections, coverageLines); + this.filterCoverage(foundSections, coverageSets); + + const coverageLines: ICoverageLines = { + full: this.setsToRanges(coverageSets.full), + none: this.setsToRanges(coverageSets.none), + partial: this.setsToRanges(coverageSets.partial), + }; + this.setDecorationsForEditor(textEditor, coverageLines); }); } @@ -97,60 +105,66 @@ export class Renderer { /** * Takes an array of sections and computes the coverage lines * @param sections sections to filter the coverage for - * @param coverageLines the current coverage lines as this point in time + * @param coverageSets the current coverage sets as this point in time */ private filterCoverage( sections: Section[], - coverageLines: ICoverageLines, + coverageSets: CoverageSets, ) { sections.forEach((section) => { - this.filterLineCoverage(section, coverageLines); - this.filterBranchCoverage(section, coverageLines); + this.filterLineCoverage(section, coverageSets); + this.filterBranchCoverage(section, coverageSets); }); } private filterLineCoverage( section: Section, - coverageLines: ICoverageLines, + coverageSets: CoverageSets, ) { if (!section || !section.lines) { return; } section.lines.details - .filter((detail) => detail.line > 0) - .forEach((detail) => { - const lineRange = new Range(detail.line - 1, 0, detail.line - 1, 0); - if (detail.hit > 0) { - // Evaluates to true if at least one element in range is equal to LineRange - if (coverageLines.none.some((range) => range.isEqual(lineRange))) { - coverageLines.none = coverageLines.none.filter((range) => !range.isEqual(lineRange)) - } - coverageLines.full.push(lineRange); - } else { - if (!coverageLines.full.some((range) => range.isEqual(lineRange))) { - // only add a none coverage if no full ones exist - coverageLines.none.push(lineRange); + .filter((detail) => detail.line > 0) + .forEach((detail) => { + const line = detail.line - 1; + if (detail.hit > 0) { + if (coverageSets.none.has(line)) { + coverageSets.none.delete(line); + } + coverageSets.full.add(line); + } else { + if (!coverageSets.full.has(line)) { + // only add a none coverage if no full ones exist + coverageSets.none.add(line); + } } - } - }); + }); } private filterBranchCoverage( section: Section, - coverageLines: ICoverageLines, + coverageSets: CoverageSets, ) { if (!section || !section.branches) { return; } section.branches.details - .filter((detail) => detail.taken === 0 && detail.line > 0) - .forEach((detail) => { - const partialRange = new Range(detail.line - 1, 0, detail.line - 1, 0); - // Evaluates to true if at least one element in range is equal to partialRange - if (coverageLines.full.some((range) => range.isEqual(partialRange))){ - coverageLines.full = coverageLines.full.filter((range) => !range.isEqual(partialRange)); - coverageLines.partial.push(partialRange); - } + .filter((detail) => detail.taken === 0 && detail.line > 0) + .forEach((detail) => { + const line = detail.line - 1; + if (coverageSets.full.has(line)) { + coverageSets.full.delete(line); + coverageSets.partial.add(line); + } + }); + } + + private setsToRanges(lines: Set): Range[] { + const ranges: Range[] = []; + lines.forEach((line) => { + ranges.push(new Range(line, 0, line, 0)); }); + return ranges; } } diff --git a/src/files/coverageparser.ts b/src/files/coverageparser.ts index b9d5c36..bf613a4 100644 --- a/src/files/coverageparser.ts +++ b/src/files/coverageparser.ts @@ -21,39 +21,42 @@ export class CoverageParser { files: Map ): Promise> { const coverages = new Map(); + const parsePromises: Promise[] = []; for (const [fileName, fileContent] of files) { // get coverage file type const coverageFile = new CoverageFile(fileContent); switch (coverageFile.type) { case CoverageType.CLOVER: - await this.xmlExtractClover( - coverages, + parsePromises.push(this.xmlExtractClover( fileName, fileContent - ); + )); break; case CoverageType.JACOCO: - await this.xmlExtractJacoco( - coverages, + parsePromises.push(this.xmlExtractJacoco( fileName, fileContent - ); + )); break; case CoverageType.COBERTURA: - await this.xmlExtractCobertura( - coverages, + parsePromises.push(this.xmlExtractCobertura( fileName, fileContent - ); + )); break; case CoverageType.LCOV: - this.lcovExtract(coverages, fileName, fileContent); + parsePromises.push(this.lcovExtract(fileName, fileContent)); break; default: break; } } + + const results = await Promise.all(parsePromises); + const flattenedSections = results.reduce((acc, val) => acc.concat(val), []); + await this.addSections(coverages, flattenedSections); + return coverages; } @@ -80,16 +83,15 @@ export class CoverageParser { } private xmlExtractCobertura( - coverages: Map, coverageFilename: string, xmlFile: string ) { - return new Promise((resolve) => { + return new Promise((resolve) => { const checkError = (err: Error) => { if (err) { err.message = `filename: ${coverageFilename} ${err.message}`; this.handleError("cobertura-parse", err); - return resolve(); + return resolve([]); } }; @@ -98,8 +100,7 @@ export class CoverageParser { xmlFile, async (err, data) => { checkError(err); - await this.addSections(coverages, data); - return resolve(); + return resolve(data); }, true ); @@ -111,24 +112,22 @@ export class CoverageParser { } private xmlExtractJacoco( - coverages: Map, coverageFilename: string, xmlFile: string ) { - return new Promise((resolve) => { + return new Promise((resolve) => { const checkError = (err: Error) => { if (err) { err.message = `filename: ${coverageFilename} ${err.message}`; this.handleError("jacoco-parse", err); - return resolve(); + return resolve([]); } }; try { parseContentJacoco(xmlFile, async (err, data) => { checkError(err); - await this.addSections(coverages, data); - return resolve(); + return resolve(data); }); // eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (error: any) { @@ -138,39 +137,37 @@ export class CoverageParser { } private async xmlExtractClover( - coverages: Map, coverageFilename: string, xmlFile: string - ) { + ): Promise { try { const data = await parseContentClover(xmlFile); - await this.addSections(coverages, data); + return data; // eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (error: any) { error.message = `filename: ${coverageFilename} ${error.message}`; this.handleError("clover-parse", error); + return []; } } private lcovExtract( - coverages: Map, coverageFilename: string, lcovFile: string ) { - return new Promise((resolve) => { + return new Promise((resolve) => { const checkError = (err: Error) => { if (err) { err.message = `filename: ${coverageFilename} ${err.message}`; this.handleError("lcov-parse", err); - return resolve(); + return resolve([]); } }; try { source(lcovFile, async (err, data) => { checkError(err); - await this.addSections(coverages, data); - return resolve(); + return resolve(data); }); // eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (error: any) { diff --git a/src/files/filesloader.ts b/src/files/filesloader.ts index 628fe92..243547e 100644 --- a/src/files/filesloader.ts +++ b/src/files/filesloader.ts @@ -1,8 +1,12 @@ -import { existsSync, readFile } from "fs"; +import { existsSync } from "fs"; +import { readFile } from "fs/promises"; import glob from "glob"; +import { promisify } from "util"; import { window, workspace, WorkspaceFolder } from "vscode"; import { Config } from "../extension/config"; +const globAsync = promisify(glob); + export class FilesLoader { private configStore: Config; @@ -41,28 +45,27 @@ export class FilesLoader { * @param files files that are to turned into data strings */ public async loadDataFiles(files: Set): Promise> { - // Load the files and convert into data strings - const dataFiles = new Map(); - for (const file of files) { - dataFiles.set(file, await this.load(file)); - } - return dataFiles; + // Load all files in parallel for better performance + const loadPromises = Array.from(files).map(async (file) => { + const data = await this.load(file); + return [file, data] as const; + }); + + const results = await Promise.all(loadPromises); + return new Map(results); } - private load(path: string) { - return new Promise((resolve, reject) => { - readFile(path, (err, data) => { - if (err) { return reject(err); } - return resolve(data.toString()); - }); - }); + private async load(path: string): Promise { + const data = await readFile(path, 'utf-8'); + return data; } private async findCoverageInWorkspace(fileNames: string[]) { - let files = new Set(); + const files = new Set(); for (const fileName of fileNames) { const coverage = await this.findCoverageForFileName(fileName); - files = new Set([...files, ...coverage]); + // Efficiently add all items without creating new Set + coverage.forEach(file => files.add(file)); } return files; } @@ -88,32 +91,27 @@ export class FilesLoader { }); } - private globFind( + private async globFind( workspaceFolder: WorkspaceFolder, fileName: string, - ) { - return new Promise>((resolve) => { - glob(`${this.configStore.coverageBaseDir}/${fileName}`, - { - cwd: workspaceFolder.uri.fsPath, - dot: true, - ignore: this.configStore.ignoredPathGlobs, - realpath: true, - strict: false, - }, - (err, files) => { - if (!files || !files.length) { - // Show any errors if no file was found. - if (err) { - window.showWarningMessage(`An error occured while looking for the coverage file ${err}`); - } - return resolve(new Set()); - } - const setFiles = new Set(); - files.forEach((file) => setFiles.add(file)); - return resolve(setFiles); - }, - ); - }); + ): Promise> { + try { + const files = await globAsync(`${this.configStore.coverageBaseDir}/${fileName}`, { + cwd: workspaceFolder.uri.fsPath, + dot: true, + ignore: this.configStore.ignoredPathGlobs, + realpath: true, + strict: false, + }); + + if (!files || !files.length) { + return new Set(); + } + + return new Set(files); + } catch (err) { + window.showWarningMessage(`An error occured while looking for the coverage file ${err}`); + return new Set(); + } } }