src/coverage-utils/coverage-tracker.ts (79 lines of code) (raw):

import * as vscode from 'vscode' import lcovParser, {SectionSummary} from '@friedemannsommer/lcov-parser' import {createReadStream} from 'node:fs' import {Utils} from '../utils/utils' import path from 'path' import {Injectable} from '@nestjs/common' import {TestRun} from 'vscode' type fileToLineMapping = Map<string, (number | undefined)[]> @Injectable() export class CoverageTracker { // Cumulative line hit count data for a given run, including results from all LCOV files in that run. private resultsByRun = new WeakMap<TestRun, fileToLineMapping>() // Processed coverage data for a given file, in required format for VS Code coverage API. private coverageStore = new WeakMap< vscode.FileCoverage, vscode.FileCoverageDetail[] >() // Process reports in the order received, one by one due to shared data access. private queue: Promise<void> = Promise.resolve() /** * Receive and queue an incoming coverage report for processing. * @param run The test run for which coverage is being processed. * @param lcovReportPath The path to the lcov report file to be processed. * @returns Promise that resolves when the given coverage report has been processed. */ public async handleCoverageReport( run: TestRun, lcovReportPath: string ): Promise<void> { this.queue = this.queue .then(() => this.processCoverage(run, lcovReportPath)) .catch(err => { run.appendOutput(`Error processing coverage: ${err}`) }) await this.queue } /** * Implements required loadDetailedCoverage function from VS Code's coverage API. * Per VS Code coverage API, the FileCoverage object serves as a key to return full FileCoverageDetail for that file. * @param testRun The test run for which coverage is being loaded. * @param fileCoverage The file coverage for which detailed coverage is being loaded. * @param token Cancellation token. * @returns FileCoverageDetail[] representing the detailed coverage data that corresponds to the given FileCoverage summary object. */ public async loadDetailedCoverage( testRun: vscode.TestRun, fileCoverage: vscode.FileCoverage, token: vscode.CancellationToken ): Promise<vscode.FileCoverageDetail[]> { const details = this.coverageStore.get(fileCoverage) if (!details) { return [] } return details } /** * Process the given coverage report. * This updates cumulative line totals and then reports results by adding them to the run. * @param run Run for which coverage reports will be processed. */ private async processCoverage(run: TestRun, lcovReportPath: string) { let lineDetail: fileToLineMapping = this.resultsByRun.get(run) ?? new Map() this.resultsByRun.set(run, lineDetail) const workspaceRoot = (await Utils.getWorkspaceGitRoot()) ?? '' const reportPath = vscode.Uri.parse(lcovReportPath).fsPath const reportText = createReadStream(reportPath) const parsedReport = await lcovParser({from: reportText}) for (const fileData of parsedReport) { // hitsPerLine data is cumulative across all lcov reports in a run. let hitsPerLine = lineDetail.get(fileData.path) ?? [] lineDetail.set(fileData.path, hitsPerLine) const fileStatementCoverage = this.getStatementCoverage( fileData, hitsPerLine ) const fileCoverage = vscode.FileCoverage.fromDetails( vscode.Uri.parse(path.join(workspaceRoot, fileData.path)), fileStatementCoverage ) // Add or replace prior coverage data for this file. this.coverageStore.set(fileCoverage, fileStatementCoverage) run.addCoverage(fileCoverage) } } /** * Maps line coverage data for a given source file into VS Code's required StatementCoverage format. * Updates lineData array with latest cumulative line totals. * @param fileData SectionData representing the parsed coverage data for an individual source file. * @param lineData Array containing hit counts per line in the given source file, used to keep a cumulative total. * @returns Array of StatementCoverage objects representing the coverage data for the given source file. */ private getStatementCoverage( fileData: SectionSummary, lineData: (number | undefined)[] ): vscode.StatementCoverage[] { const result: vscode.StatementCoverage[] = [] for (const line of fileData.lines.details) { // Store the hit count or increment exist value. const lineNum = line.line - 1 // VS Code uses 0 index. lineData[lineNum] = (lineData[lineNum] ?? 0) + line.hit // Range represents up to 500 characters of a single line. const range = new vscode.Range( new vscode.Position(lineNum, 0), new vscode.Position(lineNum, 500) ) result.push( new vscode.StatementCoverage(lineData[lineNum] ?? false, range) ) } return result } }