Skip to content

Commit

Permalink
feat: add support for clover format
Browse files Browse the repository at this point in the history
  • Loading branch information
mcarvin8 committed Dec 20, 2024
1 parent 89d562a commit 6d8d09e
Show file tree
Hide file tree
Showing 13 changed files with 499 additions and 94 deletions.
91 changes: 86 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
- [License](#license)
</details>

A Salesforce CLI plugin to transform the Apex code coverage JSON files created during deployments and test runs into SonarQube format or Cobertura format.
A Salesforce CLI plugin to transform the Apex code coverage JSON files created during deployments and test runs into SonarQube, Cobertura, or Clover format.

## Install

Expand All @@ -36,7 +36,7 @@ When the plugin is unable to find the Apex file from the Salesforce CLI coverage

## Creating Code Coverage Files with the Salesforce CLI

**This tool will only support the "json" coverage format from the Salesforce CLI. Do not use the "json-summary" or "cobertura" format from the Salesforce CLI.**
**This tool will only support the "json" coverage format from the Salesforce CLI. Do not use the "json-summary", "clover", or "cobertura" format from the Salesforce CLI.**

To create the code coverage JSON when deploying or validating, append `--coverage-formatters json --results-dir "coverage"` to the `sf project deploy` command. This will create a coverage JSON in this relative path - `coverage/coverage/coverage.json`.

Expand Down Expand Up @@ -72,19 +72,21 @@ FLAGS
-x, --xml=<value> Path to the code coverage XML file that will be created by this plugin.
[default: "coverage.xml"]
-f, --format=<value> Output format for the code coverage format.
Valid options are "sonar" or "cobertura".
Valid options are "sonar", "clover", or "cobertura".
[default: "sonar"]
GLOBAL FLAGS
--json Format output as json.
DESCRIPTION
Transform the Apex code coverage JSON file created by the Salesforce CLI deploy and test command into SonarQube or Cobertura format.
Transform the Apex code coverage JSON file created by the Salesforce CLI deploy and test command into SonarQube, Clover, or Cobertura format.
EXAMPLES
$ sf acc-transformer transform -j "coverage.json" -x "coverage.xml" -f "sonar"
$ sf acc-transformer transform -j "coverage.json" -x "coverage.xml" -f "cobertura"
$ sf acc-transformer transform -j "coverage.json" -x "coverage.xml" -f "clover"
```

## Hook
Expand All @@ -109,7 +111,7 @@ The `.apexcodecovtransformer.config.json` should look like this:
- `deployCoverageJsonPath` is required to use the hook after deployments and should be the path to the code coverage JSON created by the Salesforce CLI deployment command. Recommend using a relative path.
- `testCoverageJsonPath` is required to use the hook after test runs and should be the path to the code coverage JSON created by the Salesforce CLI test command. Recommend using a relative path.
- `coverageXmlPath` is optional and should be the path to the code coverage XML created by this plugin. Recommend using a relative path. If this isn't provided, it will default to `coverage.xml` in the working directory.
- `format` is optional and should be the intended output format for the code coverage XML created by this plugin. Options are "sonar" or "cobertura". If this isn't provided, it will default to "sonar".
- `format` is optional and should be the intended output format for the code coverage XML created by this plugin. Options are "sonar", "clover", or "cobertura". If this isn't provided, it will default to "sonar".

If the `.apexcodecovtransformer.config.json` file isn't found, the hook will be skipped.

Expand Down Expand Up @@ -315,6 +317,85 @@ and this format for Cobertura:
</coverage>
```

and this format for Clover:

```xml
<?xml version="1.0" encoding="UTF-8"?>
<coverage generated="1734733618708" clover="3.2.0">
<project timestamp="1734733618708" name="All files">
<metrics statements="62" coveredstatements="54" conditionals="0" coveredconditionals="0" methods="0" coveredmethods="0" elements="62" coveredelements="54" complexity="0" loc="62" ncloc="62" packages="1" files="2" classes="2"/>
<file name="AccountTrigger" path="packaged/triggers/AccountTrigger.trigger">
<metrics statements="62" coveredstatements="54" conditionals="0" coveredconditionals="0" methods="0" coveredmethods="0"/>
<line num="52" count="0" type="stmt"/>
<line num="53" count="0" type="stmt"/>
<line num="59" count="0" type="stmt"/>
<line num="60" count="0" type="stmt"/>
<line num="1" count="1" type="stmt"/>
<line num="2" count="1" type="stmt"/>
<line num="3" count="1" type="stmt"/>
<line num="4" count="1" type="stmt"/>
<line num="5" count="1" type="stmt"/>
<line num="6" count="1" type="stmt"/>
<line num="7" count="1" type="stmt"/>
<line num="8" count="1" type="stmt"/>
<line num="9" count="1" type="stmt"/>
<line num="10" count="1" type="stmt"/>
<line num="11" count="1" type="stmt"/>
<line num="12" count="1" type="stmt"/>
<line num="13" count="1" type="stmt"/>
<line num="14" count="1" type="stmt"/>
<line num="15" count="1" type="stmt"/>
<line num="16" count="1" type="stmt"/>
<line num="17" count="1" type="stmt"/>
<line num="18" count="1" type="stmt"/>
<line num="19" count="1" type="stmt"/>
<line num="20" count="1" type="stmt"/>
<line num="21" count="1" type="stmt"/>
<line num="22" count="1" type="stmt"/>
<line num="23" count="1" type="stmt"/>
<line num="24" count="1" type="stmt"/>
<line num="25" count="1" type="stmt"/>
<line num="26" count="1" type="stmt"/>
<line num="27" count="1" type="stmt"/>
</file>
<file name="AccountProfile" path="force-app/main/default/classes/AccountProfile.cls">
<metrics statements="62" coveredstatements="54" conditionals="0" coveredconditionals="0" methods="0" coveredmethods="0"/>
<line num="52" count="0" type="stmt"/>
<line num="53" count="0" type="stmt"/>
<line num="59" count="0" type="stmt"/>
<line num="60" count="0" type="stmt"/>
<line num="54" count="1" type="stmt"/>
<line num="55" count="1" type="stmt"/>
<line num="56" count="1" type="stmt"/>
<line num="57" count="1" type="stmt"/>
<line num="58" count="1" type="stmt"/>
<line num="61" count="1" type="stmt"/>
<line num="62" count="1" type="stmt"/>
<line num="63" count="1" type="stmt"/>
<line num="64" count="1" type="stmt"/>
<line num="65" count="1" type="stmt"/>
<line num="66" count="1" type="stmt"/>
<line num="67" count="1" type="stmt"/>
<line num="68" count="1" type="stmt"/>
<line num="69" count="1" type="stmt"/>
<line num="70" count="1" type="stmt"/>
<line num="71" count="1" type="stmt"/>
<line num="72" count="1" type="stmt"/>
<line num="1" count="1" type="stmt"/>
<line num="2" count="1" type="stmt"/>
<line num="3" count="1" type="stmt"/>
<line num="4" count="1" type="stmt"/>
<line num="5" count="1" type="stmt"/>
<line num="6" count="1" type="stmt"/>
<line num="7" count="1" type="stmt"/>
<line num="8" count="1" type="stmt"/>
<line num="9" count="1" type="stmt"/>
<line num="10" count="1" type="stmt"/>
</file>
</project>
</coverage>
```

## Issues

If you encounter any issues, please create an issue in the repository's [issue tracker](https://github.com/mcarvin8/apex-code-coverage-transformer/issues). Please also create issues to suggest any new features.
Expand Down
5 changes: 3 additions & 2 deletions messages/transformer.transform.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
# summary

Transforms the Code Coverage JSON into SonarQube or Cobertura format.
Transforms the Code Coverage JSON into SonarQube, Clover, or Cobertura format.

# description

Transform the Apex code coverage JSON file created by the Salesforce CLI deploy and test command into SonarQube or Cobertura format.
Transform the Apex code coverage JSON file created by the Salesforce CLI deploy and test command into SonarQube, Clover, or Cobertura format.

# examples

- `sf acc-transformer transform -j "coverage.json" -x "coverage.xml" -f "sonar"`
- `sf acc-transformer transform -j "coverage.json" -x "coverage.xml" -f "cobertura"`
- `sf acc-transformer transform -j "coverage.json" -x "coverage.xml" -f "clover"`

# flags.coverage-json.summary

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "apex-code-coverage-transformer",
"description": "Transforms the Apex code coverage JSON created during Salesforce deployments and test runs into SonarQube or Cobertura format.",
"description": "Transforms the Apex code coverage JSON created during Salesforce deployments and test runs into SonarQube, Clover, or Cobertura format.",
"version": "2.3.0",
"dependencies": {
"@oclif/core": "^4.0.37",
Expand Down
3 changes: 2 additions & 1 deletion src/commands/acc-transformer/transform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { DeployCoverageData, TestCoverageData, TransformerTransformResult } from
import { transformDeployCoverageReport } from '../../helpers/transformDeployCoverageReport.js';
import { transformTestCoverageReport } from '../../helpers/transformTestCoverageReport.js';
import { checkCoverageDataType } from '../../helpers/setCoverageDataType.js';
import { formatOptions } from '../../helpers/constants.js';

Messages.importMessagesDirectoryFromMetaUrl(import.meta.url);
const messages = Messages.loadMessages('apex-code-coverage-transformer', 'transformer.transform');
Expand Down Expand Up @@ -38,7 +39,7 @@ export default class TransformerTransform extends SfCommand<TransformerTransform
required: true,
multiple: false,
default: 'sonar',
options: ['sonar', 'cobertura'],
options: formatOptions,
}),
};

Expand Down
1 change: 1 addition & 0 deletions src/helpers/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const formatOptions: string[] = ['sonar', 'cobertura', 'clover'];
12 changes: 9 additions & 3 deletions src/helpers/generateXml.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
'use strict';

import { create } from 'xmlbuilder2';
import { SonarCoverageObject, CoberturaCoverageObject } from './types.js';
import { SonarCoverageObject, CoberturaCoverageObject, CloverCoverageObject } from './types.js';

export function generateXml(coverageObj: SonarCoverageObject | CoberturaCoverageObject, format: string): string {
let xml = create(coverageObj).end({ prettyPrint: true, indent: ' ', headless: format === 'cobertura' });
export function generateXml(
coverageObj: SonarCoverageObject | CoberturaCoverageObject | CloverCoverageObject,
format: string
): string {
const isHeadless = format === 'cobertura' || format === 'clover';
let xml = create(coverageObj).end({ prettyPrint: true, indent: ' ', headless: isHeadless });

if (format === 'cobertura') {
xml = `<?xml version="1.0" ?>\n<!DOCTYPE coverage SYSTEM "http://cobertura.sourceforge.net/xml/coverage-04.dtd">\n${xml}`;
} else if (format === 'clover') {
xml = `<?xml version="1.0" encoding="UTF-8"?>\n${xml}`;
}

return xml;
Expand Down
82 changes: 82 additions & 0 deletions src/helpers/initializeCoverageObject.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
'use strict';

import {
SonarCoverageObject,
CoberturaCoverageObject,
CloverCoverageObject,
CoberturaClass,
CoberturaPackage,
} from './types.js';

export function initializeCoverageObject(format: string): {
coverageObj: SonarCoverageObject | CoberturaCoverageObject | CloverCoverageObject;
packageObj: CoberturaPackage | null;
} {
let coverageObj: SonarCoverageObject | CoberturaCoverageObject | CloverCoverageObject;

if (format === 'sonar') {
coverageObj = {
coverage: { '@version': '1', file: [] },
} as SonarCoverageObject;
} else if (format === 'cobertura') {
coverageObj = {
coverage: {
'@lines-valid': 0,
'@lines-covered': 0,
'@line-rate': 0,
'@branches-valid': 0,
'@branches-covered': 0,
'@branch-rate': 1,
'@timestamp': Date.now(),
'@complexity': 0,
'@version': '0.1',
sources: { source: ['.'] },
packages: { package: [] },
},
} as CoberturaCoverageObject;
} else {
coverageObj = {
coverage: {
'@generated': Date.now(),
'@clover': '3.2.0',
project: {
'@timestamp': Date.now(),
'@name': 'All files',
metrics: {
'@statements': 0,
'@coveredstatements': 0,
'@conditionals': 0,
'@coveredconditionals': 0,
'@methods': 0,
'@coveredmethods': 0,
'@elements': 0,
'@coveredelements': 0,
'@complexity': 0,
'@loc': 0,
'@ncloc': 0,
'@packages': 1,
'@files': 0,
'@classes': 0,
},
file: [],
},
},
} as CloverCoverageObject;
}

const packageObj =
format === 'cobertura'
? ({
'@name': 'main',
'@line-rate': 0,
'@branch-rate': 1,
classes: { class: [] as CoberturaClass[] },
} as CoberturaPackage)
: null;

if (packageObj) {
(coverageObj as CoberturaCoverageObject).coverage.packages.package.push(packageObj);
}

return { coverageObj, packageObj };
}
55 changes: 55 additions & 0 deletions src/helpers/setCoveredLinesClover.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
'use strict';
/* eslint-disable no-param-reassign */

import { join } from 'node:path';
import { getTotalLines } from './getTotalLines.js';
import { CloverFile, CloverLine } from './types.js';

export async function setCoveredLinesClover(
coveredLines: number[],
uncoveredLines: number[],
repoRoot: string,
filePath: string,
fileObj: CloverFile
): Promise<void> {
const randomLines: number[] = [];
const totalLines = await getTotalLines(join(repoRoot, filePath));

for (const coveredLine of coveredLines) {
if (coveredLine > totalLines) {
for (let randomLineNumber = 1; randomLineNumber <= totalLines; randomLineNumber++) {
if (
!uncoveredLines.includes(randomLineNumber) &&
!coveredLines.includes(randomLineNumber) &&
!randomLines.includes(randomLineNumber)
) {
const randomLine: CloverLine = {
'@num': randomLineNumber,
'@count': 1,
'@type': 'stmt',
};
fileObj.line.push(randomLine);
randomLines.push(randomLineNumber);
break;
}
}
} else {
const coveredLineObj: CloverLine = {
'@num': coveredLine,
'@count': 1,
'@type': 'stmt',
};
fileObj.line.push(coveredLineObj);
}
}

// Update Clover file-level metrics
fileObj.metrics['@statements'] += coveredLines.length + uncoveredLines.length;
fileObj.metrics['@coveredstatements'] += coveredLines.length;

// Optionally calculate derived metrics
fileObj.metrics['@conditionals'] ??= 0; // Add default if missing
fileObj.metrics['@coveredconditionals'] ??= 0;
fileObj.metrics['@methods'] ??= 0;
fileObj.metrics['@coveredmethods'] ??= 0;
}
Loading

0 comments on commit 6d8d09e

Please sign in to comment.