Skip to content

Commit ecd74f6

Browse files
committed
feat: add jacoco format
1 parent be66e1c commit ecd74f6

24 files changed

+667
-790
lines changed

README.md

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -69,13 +69,13 @@ Both hardis commands will create the code coverage JSON to transform here: `hard
6969
- The coverage reports created by this plugin will add correct file-paths per your Salesforce DX repository. Salesforce CLI coverage reports have the `no-map/` prefix hard-coded into their coverage reports. The coverage report created in this plugin will only contain Apex coverage results against files found in your Salesforce DX repository, allowing you to use these reports in external code quality tools like SonarQube.
7070
- Normalizes the coverage reports created by the Salesforce CLI deploy and test command. The coverage reports created by both CLI commands follow different formats and have different coverage format options. These differences cause issues when trying to have external tools like SonarQube parse the coverage reports. This plugin handles parsing both command coverage reports and converting them into common formats accepted by external tools like SonarQube and GitLab.
7171
- The coverage reports created by this plugin "fixes" an issue with Salesforce CLI deploy command coverage reports. The coverage reports created by the deploy command contains several inaccuracies in their covered lines.
72-
- Salesforce's deploy covered report may report out-of-range lines as "covered", i.e. line 100 in a 98-line apex class is reported as "covered".
73-
- Salesforce's deploy covered report may report extra lines than the total lines in the apex class, i.e. 120 lines are included in the deploy coverage report for a 100-line apex class.
74-
- The coverage percentage may vary based on how many lines the API returns in the original deploy coverage report.
75-
- I had to add a re-numbering function to this plugin to work-around these inaccuracies and ensure the transformed coverage reports are accepted by external tools like SonarQube.
76-
- Once the Salesforce server team fixes the API to correctly return coverage in deploy command reports, I will remove this re-numbering function in this plugin.
77-
- See issues [5511](https://github.com/forcedotcom/salesforcedx-vscode/issues/5511) and [1568](https://github.com/forcedotcom/cli/issues/1568).
78-
- **NOTE**: This does not affect coverage reports created by the Salesforce CLI test commands.
72+
- Salesforce's deploy covered report may report out-of-range lines as "covered", i.e. line 100 in a 98-line apex class is reported as "covered".
73+
- Salesforce's deploy covered report may report extra lines than the total lines in the apex class, i.e. 120 lines are included in the deploy coverage report for a 100-line apex class.
74+
- The coverage percentage may vary based on how many lines the API returns in the original deploy coverage report.
75+
- I had to add a re-numbering function to this plugin to work-around these inaccuracies and ensure the transformed coverage reports are accepted by external tools like SonarQube.
76+
- Once the Salesforce server team fixes the API to correctly return coverage in deploy command reports, I will remove this re-numbering function in this plugin.
77+
- See issues [5511](https://github.com/forcedotcom/salesforcedx-vscode/issues/5511) and [1568](https://github.com/forcedotcom/cli/issues/1568).
78+
- **NOTE**: This does not affect coverage reports created by the Salesforce CLI test commands.
7979

8080
## Command
8181

@@ -116,12 +116,13 @@ EXAMPLES
116116

117117
The `-f`/`--format` flag allows you to specify the format of the coverage report.
118118

119-
| Flag Option | Description | Example |
120-
|-------------|-------------| -------------|
121-
| `sonar` | Generates a SonarQube-compatible coverage report. This is the default option. | [sonar example](https://raw.githubusercontent.com/mcarvin8/apex-code-coverage-transformer/main/test/deploy_coverage_baseline_sonar.xml) |
122-
| `clover` | Produces a Clover XML report format, commonly used with Atlassian tools. | [Clover example](https://raw.githubusercontent.com/mcarvin8/apex-code-coverage-transformer/main/test/deploy_coverage_baseline_clover.xml) |
123-
| `lcovonly` | Outputs coverage data in LCOV format, useful for integrating with LCOV-based tools. | [LCovOnly example](https://raw.githubusercontent.com/mcarvin8/apex-code-coverage-transformer/main/test/deploy_coverage_baseline_lcov.info) |
124-
| `cobertura` | Creates a Cobertura XML report, a widely used format for coverage reporting. | [Cobertura example](https://raw.githubusercontent.com/mcarvin8/apex-code-coverage-transformer/main/test/deploy_coverage_baseline_cobertura.xml) |
119+
| Flag Option | Description | Example |
120+
| ----------- | ----------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
121+
| `sonar` | Generates a SonarQube-compatible coverage report. This is the default option. | [sonar example](https://raw.githubusercontent.com/mcarvin8/apex-code-coverage-transformer/main/test/sonar_baseline.xml) |
122+
| `clover` | Produces a Clover XML report format, commonly used with Atlassian tools. | [Clover example](https://raw.githubusercontent.com/mcarvin8/apex-code-coverage-transformer/main/test/clover_baseline.xml) |
123+
| `lcovonly` | Outputs coverage data in LCOV format, useful for integrating with LCOV-based tools. | [LCovOnly example](https://raw.githubusercontent.com/mcarvin8/apex-code-coverage-transformer/main/test/lcov_baseline.info) |
124+
| `cobertura` | Creates a Cobertura XML report, a widely used format for coverage reporting. | [Cobertura example](https://raw.githubusercontent.com/mcarvin8/apex-code-coverage-transformer/main/test/cobertura_baseline.xml) |
125+
| `jacoco` | Creates a JaCoCo XML report, a format for Java. | [JaCoCo example](https://raw.githubusercontent.com/mcarvin8/apex-code-coverage-transformer/main/test/jacoco_baseline.xml) |
125126

126127
## Hook
127128

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,8 @@
5959
"gitlab",
6060
"github",
6161
"azure",
62-
"bitbucket"
62+
"bitbucket",
63+
"jacoco"
6364
],
6465
"license": "MIT",
6566
"oclif": {

src/handlers/getCoverageHandler.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,15 @@ import { CloverCoverageHandler } from './cloverCoverageHandler.js';
55
import { CoberturaCoverageHandler } from './coberturaCoverageHandler.js';
66
import { SonarCoverageHandler } from './sonarCoverageHandler.js';
77
import { LcovCoverageHandler } from './lcovCoverageHandler.js';
8+
import { JaCoCoCoverageHandler } from './jacocoCoverageHandler.js';
89

910
export function getCoverageHandler(format: string): CoverageHandler {
1011
const handlers: Record<string, CoverageHandler> = {
1112
sonar: new SonarCoverageHandler(),
1213
cobertura: new CoberturaCoverageHandler(),
1314
clover: new CloverCoverageHandler(),
1415
lcovonly: new LcovCoverageHandler(),
16+
jacoco: new JaCoCoCoverageHandler(),
1517
};
1618

1719
const handler = handlers[format];

src/handlers/jacocoCoverageHandler.ts

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
'use strict';
2+
3+
import { JaCoCoCoverageObject, JaCoCoPackage, JaCoCoClass, JaCoCoLine, CoverageHandler } from '../helpers/types.js';
4+
5+
export class JaCoCoCoverageHandler implements CoverageHandler {
6+
private readonly coverageObj: JaCoCoCoverageObject;
7+
private packageObj: JaCoCoPackage;
8+
9+
public constructor() {
10+
this.coverageObj = {
11+
report: {
12+
'@name': 'JaCoCo Coverage Report',
13+
sessionInfo: [],
14+
packages: { package: [] },
15+
},
16+
};
17+
this.packageObj = {
18+
'@name': 'main',
19+
classes: { class: [] },
20+
};
21+
this.coverageObj.report.packages.package.push(this.packageObj);
22+
}
23+
24+
public processFile(filePath: string, fileName: string, lines: Record<string, number>): void {
25+
const classObj: JaCoCoClass = {
26+
'@name': fileName,
27+
'@sourcefile': filePath,
28+
lines: { line: [] },
29+
counters: { counter: [] },
30+
};
31+
32+
let coveredLines = 0;
33+
let totalLines = 0;
34+
35+
for (const [lineNumber, isCovered] of Object.entries(lines)) {
36+
totalLines++;
37+
if (isCovered === 1) coveredLines++;
38+
39+
const lineObj: JaCoCoLine = {
40+
'@nr': Number(lineNumber),
41+
'@mi': isCovered === 0 ? 1 : 0,
42+
'@ci': isCovered === 1 ? 1 : 0,
43+
};
44+
classObj.lines.line.push(lineObj);
45+
}
46+
47+
classObj.counters.counter.push({
48+
'@type': 'LINE',
49+
'@missed': totalLines - coveredLines,
50+
'@covered': coveredLines,
51+
});
52+
53+
this.packageObj.classes.class.push(classObj);
54+
}
55+
56+
public finalize(): JaCoCoCoverageObject {
57+
if (this.coverageObj.report?.packages?.package) {
58+
this.coverageObj.report.packages.package.sort((a, b) => a['@name'].localeCompare(b['@name']));
59+
for (const pkg of this.coverageObj.report.packages.package) {
60+
if (pkg.classes?.class) {
61+
pkg.classes.class.sort((a, b) => a['@sourcefile'].localeCompare(b['@sourcefile']));
62+
}
63+
}
64+
}
65+
return this.coverageObj;
66+
}
67+
}

src/helpers/constants.ts

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

src/helpers/generateReport.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,21 @@
11
'use strict';
22

33
import { create } from 'xmlbuilder2';
4-
import { SonarCoverageObject, CoberturaCoverageObject, CloverCoverageObject, LcovCoverageObject } from './types.js';
4+
import {
5+
SonarCoverageObject,
6+
CoberturaCoverageObject,
7+
CloverCoverageObject,
8+
LcovCoverageObject,
9+
JaCoCoCoverageObject,
10+
} from './types.js';
511

612
export function generateReport(
7-
coverageObj: SonarCoverageObject | CoberturaCoverageObject | CloverCoverageObject | LcovCoverageObject,
13+
coverageObj:
14+
| SonarCoverageObject
15+
| CoberturaCoverageObject
16+
| CloverCoverageObject
17+
| LcovCoverageObject
18+
| JaCoCoCoverageObject,
819
format: string
920
): string {
1021
if (format === 'lcovonly') {

src/helpers/types.ts

Lines changed: 53 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -158,12 +158,13 @@ export type CloverCoverageObject = {
158158
};
159159

160160
export type CoverageHandler = {
161-
processFile(
162-
filePath: string,
163-
fileName: string,
164-
lines: Record<string, number>,
165-
): void;
166-
finalize(): SonarCoverageObject | CoberturaCoverageObject | CloverCoverageObject | LcovCoverageObject;
161+
processFile(filePath: string, fileName: string, lines: Record<string, number>): void;
162+
finalize():
163+
| SonarCoverageObject
164+
| CoberturaCoverageObject
165+
| CloverCoverageObject
166+
| LcovCoverageObject
167+
| JaCoCoCoverageObject;
167168
};
168169

169170
type LcovLine = {
@@ -181,3 +182,49 @@ export type LcovFile = {
181182
export type LcovCoverageObject = {
182183
files: LcovFile[];
183184
};
185+
186+
export type JaCoCoCounter = {
187+
'@type': 'INSTRUCTION' | 'BRANCH' | 'LINE' | 'METHOD' | 'CLASS';
188+
'@missed': number;
189+
'@covered': number;
190+
};
191+
192+
export type JaCoCoLine = {
193+
'@nr': number;
194+
'@mi': number;
195+
'@ci': number;
196+
};
197+
198+
export type JaCoCoClass = {
199+
'@name': string;
200+
'@sourcefile': string;
201+
lines: {
202+
line: JaCoCoLine[];
203+
};
204+
counters: {
205+
counter: JaCoCoCounter[];
206+
};
207+
};
208+
209+
export type JaCoCoPackage = {
210+
'@name': string;
211+
classes: {
212+
class: JaCoCoClass[];
213+
};
214+
};
215+
216+
export type JaCoCoSessionInfo = {
217+
'@id': string;
218+
'@start': number;
219+
'@dump': number;
220+
};
221+
222+
export type JaCoCoCoverageObject = {
223+
report: {
224+
'@name': string;
225+
sessionInfo: JaCoCoSessionInfo[];
226+
packages: {
227+
package: JaCoCoPackage[];
228+
};
229+
};
230+
};

test/baselines/classes/AccountProfile.cls

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,4 +69,22 @@ global class PrepareMySandbox implements SandboxPostCopy {
6969
}
7070
System.debug('Passwords reset for ' + users.size() + ' users.');
7171
}
72+
private void resetPasswords(List<User> users) {
73+
for (User u : users) {
74+
System.resetPassword(u.Id, true); // The second parameter generates a new password and sends an email
75+
}
76+
System.debug('Passwords reset for ' + users.size() + ' users.');
77+
}
78+
private void resetPasswords(List<User> users) {
79+
for (User u : users) {
80+
System.resetPassword(u.Id, true); // The second parameter generates a new password and sends an email
81+
}
82+
System.debug('Passwords reset for ' + users.size() + ' users.');
83+
}
84+
private void resetPasswords(List<User> users) {
85+
for (User u : users) {
86+
System.resetPassword(u.Id, true); // The second parameter generates a new password and sends an email
87+
}
88+
System.debug('Passwords reset for ' + users.size() + ' users.');
89+
}
7290
}

test/baselines/triggers/AccountTrigger.trigger

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,4 +37,66 @@ trigger helloWorldAccountTrigger on Account (before insert) {
3737
MyHelloWorld.addHelloWorld(accs);
3838
MyHelloWorld.addHelloWorld(accs);
3939
MyHelloWorld.addHelloWorld(accs);
40+
MyHelloWorld.addHelloWorld(accs);
41+
MyHelloWorld.addHelloWorld(accs);
42+
MyHelloWorld.addHelloWorld(accs);
43+
MyHelloWorld.addHelloWorld(accs);
44+
MyHelloWorld.addHelloWorld(accs);
45+
MyHelloWorld.addHelloWorld(accs);
46+
MyHelloWorld.addHelloWorld(accs);
47+
MyHelloWorld.addHelloWorld(accs);
48+
MyHelloWorld.addHelloWorld(accs);
49+
MyHelloWorld.addHelloWorld(accs);
50+
MyHelloWorld.addHelloWorld(accs);
51+
MyHelloWorld.addHelloWorld(accs);
52+
MyHelloWorld.addHelloWorld(accs);
53+
MyHelloWorld.addHelloWorld(accs);
54+
MyHelloWorld.addHelloWorld(accs);
55+
MyHelloWorld.addHelloWorld(accs);
56+
MyHelloWorld.addHelloWorld(accs);
57+
MyHelloWorld.addHelloWorld(accs);
58+
MyHelloWorld.addHelloWorld(accs);
59+
MyHelloWorld.addHelloWorld(accs);
60+
MyHelloWorld.addHelloWorld(accs);
61+
MyHelloWorld.addHelloWorld(accs);
62+
MyHelloWorld.addHelloWorld(accs);
63+
MyHelloWorld.addHelloWorld(accs);
64+
MyHelloWorld.addHelloWorld(accs);
65+
MyHelloWorld.addHelloWorld(accs);
66+
MyHelloWorld.addHelloWorld(accs);
67+
MyHelloWorld.addHelloWorld(accs);
68+
MyHelloWorld.addHelloWorld(accs);
69+
MyHelloWorld.addHelloWorld(accs);
70+
MyHelloWorld.addHelloWorld(accs);
71+
MyHelloWorld.addHelloWorld(accs);
72+
MyHelloWorld.addHelloWorld(accs);
73+
MyHelloWorld.addHelloWorld(accs);
74+
MyHelloWorld.addHelloWorld(accs);
75+
MyHelloWorld.addHelloWorld(accs);
76+
MyHelloWorld.addHelloWorld(accs);
77+
MyHelloWorld.addHelloWorld(accs);
78+
MyHelloWorld.addHelloWorld(accs);
79+
MyHelloWorld.addHelloWorld(accs);
80+
MyHelloWorld.addHelloWorld(accs);
81+
MyHelloWorld.addHelloWorld(accs);
82+
MyHelloWorld.addHelloWorld(accs);
83+
MyHelloWorld.addHelloWorld(accs);
84+
MyHelloWorld.addHelloWorld(accs);
85+
MyHelloWorld.addHelloWorld(accs);
86+
MyHelloWorld.addHelloWorld(accs);
87+
MyHelloWorld.addHelloWorld(accs);
88+
MyHelloWorld.addHelloWorld(accs);
89+
MyHelloWorld.addHelloWorld(accs);
90+
MyHelloWorld.addHelloWorld(accs);
91+
MyHelloWorld.addHelloWorld(accs);
92+
MyHelloWorld.addHelloWorld(accs);
93+
MyHelloWorld.addHelloWorld(accs);
94+
MyHelloWorld.addHelloWorld(accs);
95+
MyHelloWorld.addHelloWorld(accs);
96+
MyHelloWorld.addHelloWorld(accs);
97+
MyHelloWorld.addHelloWorld(accs);
98+
MyHelloWorld.addHelloWorld(accs);
99+
MyHelloWorld.addHelloWorld(accs);
100+
MyHelloWorld.addHelloWorld(accs);
101+
MyHelloWorld.addHelloWorld(accs);
40102
}

0 commit comments

Comments
 (0)