UNPKG

apex-code-coverage-transformer

Version:

Transform Salesforce Apex code coverage JSONs into other formats accepted by SonarQube, GitHub, GitLab, Azure, Bitbucket, etc.

169 lines 6.47 kB
'use strict'; import { BaseHandler } from './BaseHandler.js'; import { HandlerRegistry } from './HandlerRegistry.js'; /** * Handler for generating OpenCover XML coverage reports. * * OpenCover is a code coverage tool for .NET, but its XML format * is also accepted by Azure DevOps, Visual Studio, and other tools. * * **Format Origin**: OpenCover (.NET coverage tool) * * @see https://github.com/OpenCover/opencover * @see https://github.com/OpenCover/opencover/wiki/Reports * * **Apex-Specific Adaptations**: * - Salesforce Apex only provides line-level coverage data * - Each Apex class is represented as an OpenCover "Module" * - Line coverage is mapped to "SequencePoints" (executable code locations) * - Branch coverage is always 0 (Apex doesn't provide branch/decision coverage) * - Column information (`@sc`, `@ec`) defaults to 0 (not available in Apex) * * **Limitations**: * - No branch/decision coverage - OpenCover supports this, Apex does not * - No method-level coverage granularity - treating entire class as one method * - No cyclomatic complexity metrics * - No column-level positioning data * * **Structure Mapping**: * - Apex Class → OpenCover Module/Class * - Apex Class → OpenCover Method (single method per class) * - Apex Lines → OpenCover SequencePoints * * Compatible with: * - Azure DevOps * - Visual Studio * - Codecov * - JetBrains tools (ReSharper, Rider) * * @example * ```xml * <CoverageSession> * <Summary numSequencePoints="100" visitedSequencePoints="75" /> * <Modules> * <Module> * <Files> * <File uid="1" fullPath="path/to/file.cls" /> * </Files> * <Classes> * <Class fullName="ClassName"> * <Methods> * <Method name="MethodName"> * <SequencePoints> * <SequencePoint vc="1" sl="1" /> * </SequencePoints> * </Method> * </Methods> * </Class> * </Classes> * </Module> * </Modules> * </CoverageSession> * ``` */ export class OpenCoverCoverageHandler extends BaseHandler { coverageObj; module; fileIdCounter = 1; filePathToId = new Map(); constructor() { super(); this.module = { '@hash': 'apex-module', Files: { File: [] }, Classes: { Class: [] }, }; this.coverageObj = { CoverageSession: { Summary: { '@numSequencePoints': 0, '@visitedSequencePoints': 0, '@numBranchPoints': 0, '@visitedBranchPoints': 0, '@sequenceCoverage': 0, '@branchCoverage': 0, }, Modules: { Module: [this.module], }, }, }; } processFile(filePath, fileName, lines) { // Register file if not already registered if (!this.filePathToId.has(filePath)) { const fileId = this.fileIdCounter++; this.filePathToId.set(filePath, fileId); const fileObj = { '@uid': fileId, '@fullPath': filePath, }; this.module.Files.File.push(fileObj); } const { totalLines, coveredLines } = this.calculateCoverage(lines); // Create sequence points for each line // In OpenCover, a SequencePoint represents an executable statement location // We map each Apex line to a SequencePoint const sequencePoints = []; for (const [lineNumber, hits] of Object.entries(lines)) { sequencePoints.push({ '@vc': hits, // visit count (number of times this line was executed) '@sl': Number(lineNumber), // start line '@sc': 0, // start column (not available in Apex coverage data) '@el': Number(lineNumber), // end line (same as start for line-level coverage) '@ec': 0, // end column (not available in Apex coverage data) }); } // Create a method for this file // NOTE: Apex classes are treated as a single method since we don't have // method-level coverage granularity from Salesforce const method = { '@name': fileName, '@isConstructor': false, '@isStatic': false, SequencePoints: { SequencePoint: sequencePoints, }, }; // Create a class for this file const classObj = { '@fullName': fileName, Methods: { Method: [method], }, }; this.module.Classes.Class.push(classObj); // Update summary statistics this.coverageObj.CoverageSession.Summary['@numSequencePoints'] += totalLines; this.coverageObj.CoverageSession.Summary['@visitedSequencePoints'] += coveredLines; } finalize() { const summary = this.coverageObj.CoverageSession.Summary; // Calculate sequence coverage percentage if (summary['@numSequencePoints'] > 0) { const coverage = (summary['@visitedSequencePoints'] / summary['@numSequencePoints']) * 100; summary['@sequenceCoverage'] = Number(coverage.toFixed(2)); } // Branch coverage is always 0 for Apex (no branch/decision coverage data available) // In .NET environments, this would track if/else branches, switch cases, etc. summary['@branchCoverage'] = 0; // Sort classes by name for consistent output this.module.Classes.Class.sort((a, b) => a['@fullName'].localeCompare(b['@fullName'])); // Sort files by path and reassign UIDs sequentially for deterministic output this.module.Files.File.sort((a, b) => a['@fullPath'].localeCompare(b['@fullPath'])); // Reassign UIDs based on sorted order for (let i = 0; i < this.module.Files.File.length; i++) { this.module.Files.File[i]['@uid'] = i + 1; } return this.coverageObj; } } // Self-register this handler HandlerRegistry.register({ name: 'opencover', description: 'OpenCover XML format for .NET and Azure DevOps', fileExtension: '.xml', handler: () => new OpenCoverCoverageHandler(), compatibleWith: ['Azure DevOps', 'Visual Studio', 'Codecov', 'JetBrains Tools'], }); //# sourceMappingURL=opencover.js.map