Skip to content

Commit 00ffa74

Browse files
committed
feat: add support for clover format
1 parent 89d562a commit 00ffa74

File tree

13 files changed

+507
-95
lines changed

13 files changed

+507
-95
lines changed

README.md

Lines changed: 86 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
- [License](#license)
1919
</details>
2020

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

2323
## Install
2424

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

3737
## Creating Code Coverage Files with the Salesforce CLI
3838

39-
**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.**
39+
**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.**
4040

4141
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`.
4242

@@ -72,19 +72,21 @@ FLAGS
7272
-x, --xml=<value> Path to the code coverage XML file that will be created by this plugin.
7373
[default: "coverage.xml"]
7474
-f, --format=<value> Output format for the code coverage format.
75-
Valid options are "sonar" or "cobertura".
75+
Valid options are "sonar", "clover", or "cobertura".
7676
[default: "sonar"]
7777
7878
GLOBAL FLAGS
7979
--json Format output as json.
8080
8181
DESCRIPTION
82-
Transform the Apex code coverage JSON file created by the Salesforce CLI deploy and test command into SonarQube or Cobertura format.
82+
Transform the Apex code coverage JSON file created by the Salesforce CLI deploy and test command into SonarQube, Clover, or Cobertura format.
8383
8484
EXAMPLES
8585
$ sf acc-transformer transform -j "coverage.json" -x "coverage.xml" -f "sonar"
8686
8787
$ sf acc-transformer transform -j "coverage.json" -x "coverage.xml" -f "cobertura"
88+
89+
$ sf acc-transformer transform -j "coverage.json" -x "coverage.xml" -f "clover"
8890
```
8991

9092
## Hook
@@ -109,7 +111,7 @@ The `.apexcodecovtransformer.config.json` should look like this:
109111
- `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.
110112
- `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.
111113
- `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.
112-
- `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".
114+
- `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".
113115

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

@@ -315,6 +317,85 @@ and this format for Cobertura:
315317
</coverage>
316318
```
317319

320+
and this format for Clover:
321+
322+
```xml
323+
<?xml version="1.0" encoding="UTF-8"?>
324+
<coverage generated="1734733618708" clover="3.2.0">
325+
<project timestamp="1734733618708" name="All files">
326+
<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"/>
327+
<file name="AccountTrigger" path="packaged/triggers/AccountTrigger.trigger">
328+
<metrics statements="62" coveredstatements="54" conditionals="0" coveredconditionals="0" methods="0" coveredmethods="0"/>
329+
<line num="52" count="0" type="stmt"/>
330+
<line num="53" count="0" type="stmt"/>
331+
<line num="59" count="0" type="stmt"/>
332+
<line num="60" count="0" type="stmt"/>
333+
<line num="1" count="1" type="stmt"/>
334+
<line num="2" count="1" type="stmt"/>
335+
<line num="3" count="1" type="stmt"/>
336+
<line num="4" count="1" type="stmt"/>
337+
<line num="5" count="1" type="stmt"/>
338+
<line num="6" count="1" type="stmt"/>
339+
<line num="7" count="1" type="stmt"/>
340+
<line num="8" count="1" type="stmt"/>
341+
<line num="9" count="1" type="stmt"/>
342+
<line num="10" count="1" type="stmt"/>
343+
<line num="11" count="1" type="stmt"/>
344+
<line num="12" count="1" type="stmt"/>
345+
<line num="13" count="1" type="stmt"/>
346+
<line num="14" count="1" type="stmt"/>
347+
<line num="15" count="1" type="stmt"/>
348+
<line num="16" count="1" type="stmt"/>
349+
<line num="17" count="1" type="stmt"/>
350+
<line num="18" count="1" type="stmt"/>
351+
<line num="19" count="1" type="stmt"/>
352+
<line num="20" count="1" type="stmt"/>
353+
<line num="21" count="1" type="stmt"/>
354+
<line num="22" count="1" type="stmt"/>
355+
<line num="23" count="1" type="stmt"/>
356+
<line num="24" count="1" type="stmt"/>
357+
<line num="25" count="1" type="stmt"/>
358+
<line num="26" count="1" type="stmt"/>
359+
<line num="27" count="1" type="stmt"/>
360+
</file>
361+
<file name="AccountProfile" path="force-app/main/default/classes/AccountProfile.cls">
362+
<metrics statements="62" coveredstatements="54" conditionals="0" coveredconditionals="0" methods="0" coveredmethods="0"/>
363+
<line num="52" count="0" type="stmt"/>
364+
<line num="53" count="0" type="stmt"/>
365+
<line num="59" count="0" type="stmt"/>
366+
<line num="60" count="0" type="stmt"/>
367+
<line num="54" count="1" type="stmt"/>
368+
<line num="55" count="1" type="stmt"/>
369+
<line num="56" count="1" type="stmt"/>
370+
<line num="57" count="1" type="stmt"/>
371+
<line num="58" count="1" type="stmt"/>
372+
<line num="61" count="1" type="stmt"/>
373+
<line num="62" count="1" type="stmt"/>
374+
<line num="63" count="1" type="stmt"/>
375+
<line num="64" count="1" type="stmt"/>
376+
<line num="65" count="1" type="stmt"/>
377+
<line num="66" count="1" type="stmt"/>
378+
<line num="67" count="1" type="stmt"/>
379+
<line num="68" count="1" type="stmt"/>
380+
<line num="69" count="1" type="stmt"/>
381+
<line num="70" count="1" type="stmt"/>
382+
<line num="71" count="1" type="stmt"/>
383+
<line num="72" count="1" type="stmt"/>
384+
<line num="1" count="1" type="stmt"/>
385+
<line num="2" count="1" type="stmt"/>
386+
<line num="3" count="1" type="stmt"/>
387+
<line num="4" count="1" type="stmt"/>
388+
<line num="5" count="1" type="stmt"/>
389+
<line num="6" count="1" type="stmt"/>
390+
<line num="7" count="1" type="stmt"/>
391+
<line num="8" count="1" type="stmt"/>
392+
<line num="9" count="1" type="stmt"/>
393+
<line num="10" count="1" type="stmt"/>
394+
</file>
395+
</project>
396+
</coverage>
397+
```
398+
318399
## Issues
319400

320401
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.

messages/transformer.transform.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
11
# summary
22

3-
Transforms the Code Coverage JSON into SonarQube or Cobertura format.
3+
Transforms the Code Coverage JSON into SonarQube, Clover, or Cobertura format.
44

55
# description
66

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

99
# examples
1010

1111
- `sf acc-transformer transform -j "coverage.json" -x "coverage.xml" -f "sonar"`
1212
- `sf acc-transformer transform -j "coverage.json" -x "coverage.xml" -f "cobertura"`
13+
- `sf acc-transformer transform -j "coverage.json" -x "coverage.xml" -f "clover"`
1314

1415
# flags.coverage-json.summary
1516

package.json

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "apex-code-coverage-transformer",
3-
"description": "Transforms the Apex code coverage JSON created during Salesforce deployments and test runs into SonarQube or Cobertura format.",
3+
"description": "Transforms the Apex code coverage JSON created during Salesforce deployments and test runs into SonarQube, Clover, or Cobertura format.",
44
"version": "2.3.0",
55
"dependencies": {
66
"@oclif/core": "^4.0.37",
@@ -46,7 +46,14 @@
4646
"apex",
4747
"coverage",
4848
"git",
49-
"cobertura"
49+
"cobertura",
50+
"clover",
51+
"converter",
52+
"transformer",
53+
"code",
54+
"quality",
55+
"validation",
56+
"deployment"
5057
],
5158
"license": "MIT",
5259
"oclif": {

src/commands/acc-transformer/transform.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { DeployCoverageData, TestCoverageData, TransformerTransformResult } from
99
import { transformDeployCoverageReport } from '../../helpers/transformDeployCoverageReport.js';
1010
import { transformTestCoverageReport } from '../../helpers/transformTestCoverageReport.js';
1111
import { checkCoverageDataType } from '../../helpers/setCoverageDataType.js';
12+
import { formatOptions } from '../../helpers/constants.js';
1213

1314
Messages.importMessagesDirectoryFromMetaUrl(import.meta.url);
1415
const messages = Messages.loadMessages('apex-code-coverage-transformer', 'transformer.transform');
@@ -38,7 +39,7 @@ export default class TransformerTransform extends SfCommand<TransformerTransform
3839
required: true,
3940
multiple: false,
4041
default: 'sonar',
41-
options: ['sonar', 'cobertura'],
42+
options: formatOptions,
4243
}),
4344
};
4445

src/helpers/constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export const formatOptions: string[] = ['sonar', 'cobertura', 'clover'];

src/helpers/generateXml.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,19 @@
11
'use strict';
22

33
import { create } from 'xmlbuilder2';
4-
import { SonarCoverageObject, CoberturaCoverageObject } from './types.js';
4+
import { SonarCoverageObject, CoberturaCoverageObject, CloverCoverageObject } from './types.js';
55

6-
export function generateXml(coverageObj: SonarCoverageObject | CoberturaCoverageObject, format: string): string {
7-
let xml = create(coverageObj).end({ prettyPrint: true, indent: ' ', headless: format === 'cobertura' });
6+
export function generateXml(
7+
coverageObj: SonarCoverageObject | CoberturaCoverageObject | CloverCoverageObject,
8+
format: string
9+
): string {
10+
const isHeadless = format === 'cobertura' || format === 'clover';
11+
let xml = create(coverageObj).end({ prettyPrint: true, indent: ' ', headless: isHeadless });
812

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

1319
return xml;
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
'use strict';
2+
3+
import {
4+
SonarCoverageObject,
5+
CoberturaCoverageObject,
6+
CloverCoverageObject,
7+
CoberturaClass,
8+
CoberturaPackage,
9+
} from './types.js';
10+
11+
export function initializeCoverageObject(format: string): {
12+
coverageObj: SonarCoverageObject | CoberturaCoverageObject | CloverCoverageObject;
13+
packageObj: CoberturaPackage | null;
14+
} {
15+
let coverageObj: SonarCoverageObject | CoberturaCoverageObject | CloverCoverageObject;
16+
17+
if (format === 'sonar') {
18+
coverageObj = {
19+
coverage: { '@version': '1', file: [] },
20+
} as SonarCoverageObject;
21+
} else if (format === 'cobertura') {
22+
coverageObj = {
23+
coverage: {
24+
'@lines-valid': 0,
25+
'@lines-covered': 0,
26+
'@line-rate': 0,
27+
'@branches-valid': 0,
28+
'@branches-covered': 0,
29+
'@branch-rate': 1,
30+
'@timestamp': Date.now(),
31+
'@complexity': 0,
32+
'@version': '0.1',
33+
sources: { source: ['.'] },
34+
packages: { package: [] },
35+
},
36+
} as CoberturaCoverageObject;
37+
} else {
38+
coverageObj = {
39+
coverage: {
40+
'@generated': Date.now(),
41+
'@clover': '3.2.0',
42+
project: {
43+
'@timestamp': Date.now(),
44+
'@name': 'All files',
45+
metrics: {
46+
'@statements': 0,
47+
'@coveredstatements': 0,
48+
'@conditionals': 0,
49+
'@coveredconditionals': 0,
50+
'@methods': 0,
51+
'@coveredmethods': 0,
52+
'@elements': 0,
53+
'@coveredelements': 0,
54+
'@complexity': 0,
55+
'@loc': 0,
56+
'@ncloc': 0,
57+
'@packages': 1,
58+
'@files': 0,
59+
'@classes': 0,
60+
},
61+
file: [],
62+
},
63+
},
64+
} as CloverCoverageObject;
65+
}
66+
67+
const packageObj =
68+
format === 'cobertura'
69+
? ({
70+
'@name': 'main',
71+
'@line-rate': 0,
72+
'@branch-rate': 1,
73+
classes: { class: [] as CoberturaClass[] },
74+
} as CoberturaPackage)
75+
: null;
76+
77+
if (packageObj) {
78+
(coverageObj as CoberturaCoverageObject).coverage.packages.package.push(packageObj);
79+
}
80+
81+
return { coverageObj, packageObj };
82+
}

src/helpers/setCoveredLinesClover.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
'use strict';
2+
/* eslint-disable no-param-reassign */
3+
4+
import { join } from 'node:path';
5+
import { getTotalLines } from './getTotalLines.js';
6+
import { CloverFile, CloverLine } from './types.js';
7+
8+
export async function setCoveredLinesClover(
9+
coveredLines: number[],
10+
uncoveredLines: number[],
11+
repoRoot: string,
12+
filePath: string,
13+
fileObj: CloverFile
14+
): Promise<void> {
15+
const randomLines: number[] = [];
16+
const totalLines = await getTotalLines(join(repoRoot, filePath));
17+
18+
for (const coveredLine of coveredLines) {
19+
if (coveredLine > totalLines) {
20+
for (let randomLineNumber = 1; randomLineNumber <= totalLines; randomLineNumber++) {
21+
if (
22+
!uncoveredLines.includes(randomLineNumber) &&
23+
!coveredLines.includes(randomLineNumber) &&
24+
!randomLines.includes(randomLineNumber)
25+
) {
26+
const randomLine: CloverLine = {
27+
'@num': randomLineNumber,
28+
'@count': 1,
29+
'@type': 'stmt',
30+
};
31+
fileObj.line.push(randomLine);
32+
randomLines.push(randomLineNumber);
33+
break;
34+
}
35+
}
36+
} else {
37+
const coveredLineObj: CloverLine = {
38+
'@num': coveredLine,
39+
'@count': 1,
40+
'@type': 'stmt',
41+
};
42+
fileObj.line.push(coveredLineObj);
43+
}
44+
}
45+
46+
// Update Clover file-level metrics
47+
fileObj.metrics['@statements'] += coveredLines.length + uncoveredLines.length;
48+
fileObj.metrics['@coveredstatements'] += coveredLines.length;
49+
50+
// Optionally calculate derived metrics
51+
fileObj.metrics['@conditionals'] ??= 0; // Add default if missing
52+
fileObj.metrics['@coveredconditionals'] ??= 0;
53+
fileObj.metrics['@methods'] ??= 0;
54+
fileObj.metrics['@coveredmethods'] ??= 0;
55+
}

0 commit comments

Comments
 (0)