Skip to content

Commit

Permalink
fix: build XML using xmlbuilder2 and normalize path building using posix
Browse files Browse the repository at this point in the history
  • Loading branch information
mcarvin8 committed Apr 18, 2024
1 parent b90d649 commit c6d6d94
Show file tree
Hide file tree
Showing 10 changed files with 628 additions and 1,067 deletions.
152 changes: 76 additions & 76 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,29 @@

[![NPM](https://img.shields.io/npm/v/apex-code-coverage-transformer.svg?label=apex-code-coverage-transformer)](https://www.npmjs.com/package/apex-code-coverage-transformer) [![Downloads/week](https://img.shields.io/npm/dw/apex-code-coverage-transformer.svg)](https://npmjs.org/package/apex-code-coverage-transformer) [![License](https://img.shields.io/badge/License-MIT-yellow.svg)](https://raw.githubusercontent.com/mcarvin8/apex-code-coverage-transformer/main/LICENSE.md)

The `apex-code-coverage-transformer` is a simple Salesforce CLI plugin to transform the Apex Code Coverage JSON file into Generic Test Coverage Format (XML). This format is accepted by static code analysis tools like SonarQube.
The `apex-code-coverage-transformer` is a Salesforce CLI plugin to transform the Apex Code Coverage JSON files created during deployments into the Generic Test Coverage Format (XML). This format is accepted by static code analysis tools like SonarQube.

This plugin supports code coverage metrics created for Apex Classes and Apex Triggers. This also supports multiple package directories as listed in your project's `sfdx-project.json` configuration, assuming unique file-names are used in your package directories.

To create the code coverage JSON during a Salesforce CLI deployment/validation, append `--coverage-formatters json --results-dir coverage` to the `sf project deploy` command:

```
sf project deploy validate -x manifest/package.xml -l RunSpecifiedTests -t {testclasses} --verbose --coverage-formatters json --results-dir coverage
sf project deploy [start/validate] -x manifest/package.xml -l RunSpecifiedTests -t {testclasses} --verbose --coverage-formatters json --results-dir coverage
```

This will create a coverage JSON in this relative path - `coverage/coverage/coverage.json`

This JSON isn't accepted by SonarQube automatically and needs to be converted using this plugin.

**Disclaimer**: Due to existing bugs with how the Salesforce CLI reports `covered` lines (see [5511](https://github.com/forcedotcom/salesforcedx-vscode/issues/5511) and [1568](https://github.com/forcedotcom/cli/issues/1568)), to add support for `covered` lines in this plugin, I had to add a function to re-number out-of-range `covered` lines the CLI may report (ex: line 100 in a 98-line Apex Class is reported back as `covered` by the Salesforce CLI deploy command). Once Salesforce updates the API to correctly return `covered` lines in the deploy command, this function will be removed.
**Disclaimer**: Due to existing bugs with how the Salesforce CLI reports covered lines (see [5511](https://github.com/forcedotcom/salesforcedx-vscode/issues/5511) and [1568](https://github.com/forcedotcom/cli/issues/1568)), to add support for covered lines in this plugin, I had to add a function to re-number out-of-range covered lines the CLI may report (ex: line 100 in a 98-line Apex Class is reported back as covered by the Salesforce CLI deploy command). Salesforce's coverage result may also include extra lines as covered (ex: 120 lines are included in the coverage report for a 100 line file), so the coverage percentage may vary based on how many lines the API returns in the coverage report. Once Salesforce fixes the API to correctly return covered lines in the deploy command, this function will be removed.

## Install

```bash
sf plugins install [email protected]
```

## Commands
## Command

The `apex-code-coverage-transformer` has 1 command:

Expand All @@ -39,18 +39,18 @@ USAGE
$ sf apex-code-coverage transformer transform -j <value> -x <value> -c <value> [--json]
FLAGS
-j, --coverage-json=<value> The path to the JSON file created by the Salesforce CLI for code coverage.
-x, --xml=<value> [default: coverage.xml] Output path for the XML file created by this plugin
-c, --sfdx-configuration=<value> [default: 'sfdx-project.json' in the current working directory] The path to your Salesforce DX configuration file, 'sfdx-project.json'.
-j, --coverage-json=<value> Path to the code coverage JSON file created by the Salesforce CLI deployment command.
-x, --xml=<value> [default: coverage.xml] Path to code coverage XML file that will be created by this plugin.
-c, --sfdx-configuration=<value> [default: 'sfdx-project.json'] Path to your project's Salesforce DX configuration file.
GLOBAL FLAGS
--json Format output as json.
DESCRIPTION
This plugin will convert the JSON file created by the Salesforce CLI during Apex deployments into the Generic Test Coverage Format.
This plugin will convert the code coverage JSON file created by the Salesforce CLI during Apex deployments into an XML accepted by tools like SonarQube.
EXAMPLES
$ apex-code-coverage transformer transform -j "test.json" -x "coverage.xml" -c "sfdx-project.json"
$ sf apex-code-coverage transformer transform -j "coverage.json" -x "coverage.xml" -c "sfdx-project.json"
```

## Errors and Warnings
Expand Down Expand Up @@ -85,76 +85,76 @@ Error (1): ENOENT: no such file or directory: {packageDirPath}

## Example

This [code coverage JSON file](https://raw.githubusercontent.com/mcarvin8/apex-code-coverage-transformer/main/test/coverage_no_file_exts.json) created by the Salesforce CLI will be transformed into:
This [code coverage JSON file](https://raw.githubusercontent.com/mcarvin8/apex-code-coverage-transformer/main/test/coverage_no_file_exts.json) created during a Salesforce CLI deployment will be transformed into:

```xml
<?xml version="1.0"?>
<coverage version="1">
<file path="packaged\triggers\AccountTrigger.trigger">
<lineToCover lineNumber="52" covered="false"/>
<lineToCover lineNumber="53" covered="false"/>
<lineToCover lineNumber="59" covered="false"/>
<lineToCover lineNumber="60" covered="false"/>
<lineToCover lineNumber="1" covered="true"/>
<lineToCover lineNumber="2" covered="true"/>
<lineToCover lineNumber="3" covered="true"/>
<lineToCover lineNumber="4" covered="true"/>
<lineToCover lineNumber="5" covered="true"/>
<lineToCover lineNumber="6" covered="true"/>
<lineToCover lineNumber="7" covered="true"/>
<lineToCover lineNumber="8" covered="true"/>
<lineToCover lineNumber="9" covered="true"/>
<lineToCover lineNumber="10" covered="true"/>
<lineToCover lineNumber="11" covered="true"/>
<lineToCover lineNumber="12" covered="true"/>
<lineToCover lineNumber="13" covered="true"/>
<lineToCover lineNumber="14" covered="true"/>
<lineToCover lineNumber="15" covered="true"/>
<lineToCover lineNumber="16" covered="true"/>
<lineToCover lineNumber="17" covered="true"/>
<lineToCover lineNumber="18" covered="true"/>
<lineToCover lineNumber="19" covered="true"/>
<lineToCover lineNumber="20" covered="true"/>
<lineToCover lineNumber="21" covered="true"/>
<lineToCover lineNumber="22" covered="true"/>
<lineToCover lineNumber="23" covered="true"/>
<lineToCover lineNumber="24" covered="true"/>
<lineToCover lineNumber="25" covered="true"/>
<lineToCover lineNumber="26" covered="true"/>
<lineToCover lineNumber="27" covered="true"/>
</file>
<file path="force-app\main\default\classes\AccountProfile.cls">
<lineToCover lineNumber="52" covered="false"/>
<lineToCover lineNumber="53" covered="false"/>
<lineToCover lineNumber="59" covered="false"/>
<lineToCover lineNumber="60" covered="false"/>
<lineToCover lineNumber="54" covered="true"/>
<lineToCover lineNumber="55" covered="true"/>
<lineToCover lineNumber="56" covered="true"/>
<lineToCover lineNumber="57" covered="true"/>
<lineToCover lineNumber="58" covered="true"/>
<lineToCover lineNumber="61" covered="true"/>
<lineToCover lineNumber="62" covered="true"/>
<lineToCover lineNumber="63" covered="true"/>
<lineToCover lineNumber="64" covered="true"/>
<lineToCover lineNumber="65" covered="true"/>
<lineToCover lineNumber="66" covered="true"/>
<lineToCover lineNumber="67" covered="true"/>
<lineToCover lineNumber="68" covered="true"/>
<lineToCover lineNumber="69" covered="true"/>
<lineToCover lineNumber="70" covered="true"/>
<lineToCover lineNumber="71" covered="true"/>
<lineToCover lineNumber="72" covered="true"/>
<lineToCover lineNumber="1" covered="true"/>
<lineToCover lineNumber="2" covered="true"/>
<lineToCover lineNumber="3" covered="true"/>
<lineToCover lineNumber="4" covered="true"/>
<lineToCover lineNumber="5" covered="true"/>
<lineToCover lineNumber="6" covered="true"/>
<lineToCover lineNumber="7" covered="true"/>
<lineToCover lineNumber="8" covered="true"/>
<lineToCover lineNumber="9" covered="true"/>
<lineToCover lineNumber="10" covered="true"/>
</file>
<file path="packaged/triggers/AccountTrigger.trigger">
<lineToCover lineNumber="52" covered="false"/>
<lineToCover lineNumber="53" covered="false"/>
<lineToCover lineNumber="59" covered="false"/>
<lineToCover lineNumber="60" covered="false"/>
<lineToCover lineNumber="1" covered="true"/>
<lineToCover lineNumber="2" covered="true"/>
<lineToCover lineNumber="3" covered="true"/>
<lineToCover lineNumber="4" covered="true"/>
<lineToCover lineNumber="5" covered="true"/>
<lineToCover lineNumber="6" covered="true"/>
<lineToCover lineNumber="7" covered="true"/>
<lineToCover lineNumber="8" covered="true"/>
<lineToCover lineNumber="9" covered="true"/>
<lineToCover lineNumber="10" covered="true"/>
<lineToCover lineNumber="11" covered="true"/>
<lineToCover lineNumber="12" covered="true"/>
<lineToCover lineNumber="13" covered="true"/>
<lineToCover lineNumber="14" covered="true"/>
<lineToCover lineNumber="15" covered="true"/>
<lineToCover lineNumber="16" covered="true"/>
<lineToCover lineNumber="17" covered="true"/>
<lineToCover lineNumber="18" covered="true"/>
<lineToCover lineNumber="19" covered="true"/>
<lineToCover lineNumber="20" covered="true"/>
<lineToCover lineNumber="21" covered="true"/>
<lineToCover lineNumber="22" covered="true"/>
<lineToCover lineNumber="23" covered="true"/>
<lineToCover lineNumber="24" covered="true"/>
<lineToCover lineNumber="25" covered="true"/>
<lineToCover lineNumber="26" covered="true"/>
<lineToCover lineNumber="27" covered="true"/>
</file>
<file path="force-app/main/default/classes/AccountProfile.cls">
<lineToCover lineNumber="52" covered="false"/>
<lineToCover lineNumber="53" covered="false"/>
<lineToCover lineNumber="59" covered="false"/>
<lineToCover lineNumber="60" covered="false"/>
<lineToCover lineNumber="54" covered="true"/>
<lineToCover lineNumber="55" covered="true"/>
<lineToCover lineNumber="56" covered="true"/>
<lineToCover lineNumber="57" covered="true"/>
<lineToCover lineNumber="58" covered="true"/>
<lineToCover lineNumber="61" covered="true"/>
<lineToCover lineNumber="62" covered="true"/>
<lineToCover lineNumber="63" covered="true"/>
<lineToCover lineNumber="64" covered="true"/>
<lineToCover lineNumber="65" covered="true"/>
<lineToCover lineNumber="66" covered="true"/>
<lineToCover lineNumber="67" covered="true"/>
<lineToCover lineNumber="68" covered="true"/>
<lineToCover lineNumber="69" covered="true"/>
<lineToCover lineNumber="70" covered="true"/>
<lineToCover lineNumber="71" covered="true"/>
<lineToCover lineNumber="72" covered="true"/>
<lineToCover lineNumber="1" covered="true"/>
<lineToCover lineNumber="2" covered="true"/>
<lineToCover lineNumber="3" covered="true"/>
<lineToCover lineNumber="4" covered="true"/>
<lineToCover lineNumber="5" covered="true"/>
<lineToCover lineNumber="6" covered="true"/>
<lineToCover lineNumber="7" covered="true"/>
<lineToCover lineNumber="8" covered="true"/>
<lineToCover lineNumber="9" covered="true"/>
<lineToCover lineNumber="10" covered="true"/>
</file>
</coverage>
```
12 changes: 6 additions & 6 deletions messages/transformer.transform.md
Original file line number Diff line number Diff line change
@@ -1,23 +1,23 @@
# summary

Transforms the Code Coverage JSON into the Generic Test Data Format (XML).
Transforms the Code Coverage JSON into the Generic Test Coverage Format (XML).

# description

This plugin will convert the JSON file created by the Salesforce CLI during Apex deployments
This plugin will convert the code coverage JSON file created by the Salesforce CLI during Apex deployments into an XML accepted by tools like SonarQube.

# examples

- `sf apex-code-coverage transformer transform --coverage-json "path-to-cli-coverage.json"`
- `sf apex-code-coverage transformer transform -j "coverage.json" -x "coverage.xml" -c "sfdx-project.json"`

# flags.sfdx-configuration.summary

Path to your project's Salesforce DX configuration file (`sfdx-project.json`). By default, it will look for `sfdx-project.json` in the same directory you're running this plugin in.
Path to your project's Salesforce DX configuration file.

# flags.coverage-json.summary

Path to the JSON file created by the Salesforce CLI deployment command.
Path to the code coverage JSON file created by the Salesforce CLI deployment command.

# flags.xml.summary

XML file created by this plugin (default: `coverage.xml`).
Path to code coverage XML file that will be created by this plugin.
9 changes: 6 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
{
"name": "apex-code-coverage-transformer",
"description": "Transforms the Apex Code Coverage JSON into the Generic Test Data Report.",
"description": "Transforms the Apex code coverage JSON created during Salesforce deployments into the Generic Test Coverage Format (XML).",
"version": "1.6.4",
"dependencies": {
"@oclif/core": "^3.18.1",
"@salesforce/core": "^6.4.7",
"@salesforce/sf-plugins-core": "^7.1.3"
"@salesforce/sf-plugins-core": "^7.1.3",
"xmlbuilder2": "^3.1.1"
},
"devDependencies": {
"@commitlint/cli": "^18.6.0",
Expand Down Expand Up @@ -46,7 +47,9 @@
"sfdx-plugin",
"xml",
"json",
"sonarqube"
"sonarqube",
"apex",
"coverage"
],
"license": "MIT",
"oclif": {
Expand Down
25 changes: 14 additions & 11 deletions src/helpers/convertToGenericCoverageReport.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
'use strict';
/* eslint-disable no-await-in-loop */

import { CoverageData } from './types.js';
import { create } from 'xmlbuilder2';

import { CoverageData, CoverageObject, FileObject } from './types.js';
import { findFilePath } from './findFilePath.js';
import { setCoveredLines } from './setCoveredLines.js';

export async function convertToGenericCoverageReport(
data: CoverageData,
dxConfigFile: string
): Promise<{ xml: string; warnings: string[]; filesProcessed: number }> {
let xml = '<?xml version="1.0"?>\n<coverage version="1">\n';
const coverageObj: CoverageObject = { coverage: { '@version': '1', file: [] } };
const warnings: string[] = [];
let filesProcessed: number = 0;

Expand All @@ -22,25 +24,26 @@ export async function convertToGenericCoverageReport(
warnings.push(`The file name ${formattedFileName} was not found in any package directory.`);
continue;
}
// Extract the "uncovered lines" from the JSON data
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);

xml += `\t<file path="${filePath}">\n`;

for (const uncoveredLine of uncoveredLines) {
xml += `\t\t<lineToCover lineNumber="${uncoveredLine}" covered="false"/>\n`;
}
const fileObj: FileObject = {
'@path': filePath,
lineToCover: uncoveredLines.map((lineNumber: number) => ({
'@lineNumber': lineNumber,
'@covered': 'false',
})),
};

// this function is only needed until Salesforce fixes the API to correctly return covered lines
xml += await setCoveredLines(coveredLines, uncoveredLines, filePath);
await setCoveredLines(coveredLines, uncoveredLines, filePath, fileObj);
filesProcessed++;
xml += '\t</file>\n';
coverageObj.coverage.file.push(fileObj);
}
xml += '</coverage>';
const xml = create(coverageObj).end({ prettyPrint: true, indent: ' ' });
return { xml, warnings, filesProcessed };
}
2 changes: 1 addition & 1 deletion src/helpers/findFilePath.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
/* eslint-disable no-await-in-loop */

import { readdir, stat } from 'node:fs/promises';
import { join } from 'node:path';
import { join } from 'node:path/posix';

import { getPackageDirectories } from './getPackageDirectories.js';

Expand Down
18 changes: 12 additions & 6 deletions src/helpers/setCoveredLines.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
'use strict';

import { getTotalLines } from './getTotalLines.js';
import { FileObject } from './types.js';

export async function setCoveredLines(
coveredLines: number[],
uncoveredLines: number[],
filePath: string
): Promise<string> {
let formattedCoveredLines: string = '';
filePath: string,
fileObj: FileObject
): Promise<void> {
const randomLines: number[] = [];
const totalLines = await getTotalLines(filePath);
for (const coveredLine of coveredLines) {
Expand All @@ -18,14 +19,19 @@ export async function setCoveredLines(
!coveredLines.includes(randomLineNumber) &&
!randomLines.includes(randomLineNumber)
) {
formattedCoveredLines += `\t\t<lineToCover lineNumber="${randomLineNumber}" covered="true"/>\n`;
fileObj.lineToCover.push({
'@lineNumber': randomLineNumber,
'@covered': 'true',
});
randomLines.push(randomLineNumber);
break;
}
}
} else {
formattedCoveredLines += `\t\t<lineToCover lineNumber="${coveredLine}" covered="true"/>\n`;
fileObj.lineToCover.push({
'@lineNumber': coveredLine,
'@covered': 'true',
});
}
}
return formattedCoveredLines;
}
17 changes: 17 additions & 0 deletions src/helpers/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,20 @@ export interface CoverageData {
export interface SfdxProject {
packageDirectories: Array<{ path: string }>;
}

interface LineToCover {
'@lineNumber': number;
'@covered': string;
}

export interface FileObject {
'@path': string;
lineToCover: LineToCover[];
}

export interface CoverageObject {
coverage: {
file: FileObject[];
'@version': string;
};
}
Loading

0 comments on commit c6d6d94

Please sign in to comment.