Skip to content

Commit

Permalink
feat: support multiple package directories via the sfdx-project.json
Browse files Browse the repository at this point in the history
  • Loading branch information
Matt Carvin authored and mcarvin8 committed Mar 20, 2024
1 parent ad45bf0 commit 52c1a12
Show file tree
Hide file tree
Showing 9 changed files with 147 additions and 293 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ tmp/
# use yarn by default, so ignore npm
package-lock.json

.sfdx/

# never checkin npm config
.npmrc

Expand Down
244 changes: 10 additions & 234 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

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.

This plugin supports code coverage metrics created for Apex Classes, Apex Triggers, and Flows (if flows are deployed as active in your org).
This plugin supports code coverage metrics created for Apex Classes, Apex Triggers, and Flows (if flows are deployed as active in your org). 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:

Expand All @@ -30,16 +30,18 @@ The `apex-code-coverage-transformer` has 1 command:

- `sf apex-code-coverage transformer transform`

Recommend running this command in the same directory that your `sfdx-project.json` file is located in. This command will use the `packageDirectories` in the JSON file to set the file-paths in the coverage file.

## `sf apex-code-coverage transformer transform`

```
USAGE
$ sf apex-code-coverage transformer transform -j <value> -x <value> -d <value> [--json]
$ 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
-d, --dx-directory=<value> [default: force-app/main/default] The root directory containing your Salesforce metadata (should be the relative path in your repository).
-c, --sfdx-configuration=<value> [default: 'sfdx-project.json' in the current working directory] The path to your Salesforce DX configuration file, 'sfdx-project.json'.
GLOBAL FLAGS
--json Format output as json.
Expand All @@ -48,255 +50,29 @@ DESCRIPTION
This plugin will convert the JSON file created by the Salesforce CLI during Apex deployments into the Generic Test Coverage Format.
EXAMPLES
$ apex-code-coverage transformer transform -j "test.json" -x "coverage.xml" -d "force-app/main/default"
$ apex-code-coverage transformer transform -j "test.json" -x "coverage.xml" -c "sfdx-project.json"
```

## Example

A JSON created by the Salesforce CLI :

```json
{
"no-map/AccountTrigger": {
"fnMap": {},
"branchMap": {},
"path": "no-map/AccountTrigger",
"f": {},
"b": {},
"s": {
"52": 0,
"53": 0,
"54": 1,
"55": 1,
"56": 1,
"57": 1,
"58": 1,
"59": 0,
"60": 0,
"61": 1,
"62": 1,
"63": 1,
"64": 1,
"65": 1,
"66": 1,
"67": 1,
"68": 1,
"69": 1,
"70": 1,
"71": 1,
"72": 1,
"73": 1,
"74": 1,
"75": 1,
"76": 1,
"77": 1,
"78": 1,
"79": 1,
"80": 1,
"81": 1,
"82": 1
},
"statementMap": {
"52": { "start": { "line": 52, "column": 0 }, "end": { "line": 52, "column": 0 } },
"53": { "start": { "line": 53, "column": 0 }, "end": { "line": 53, "column": 0 } },
"54": { "start": { "line": 54, "column": 0 }, "end": { "line": 54, "column": 0 } },
"55": { "start": { "line": 55, "column": 0 }, "end": { "line": 55, "column": 0 } },
"56": { "start": { "line": 56, "column": 0 }, "end": { "line": 56, "column": 0 } },
"57": { "start": { "line": 57, "column": 0 }, "end": { "line": 57, "column": 0 } },
"58": { "start": { "line": 58, "column": 0 }, "end": { "line": 58, "column": 0 } },
"59": { "start": { "line": 59, "column": 0 }, "end": { "line": 59, "column": 0 } },
"60": { "start": { "line": 60, "column": 0 }, "end": { "line": 60, "column": 0 } },
"61": { "start": { "line": 61, "column": 0 }, "end": { "line": 61, "column": 0 } },
"62": { "start": { "line": 62, "column": 0 }, "end": { "line": 62, "column": 0 } },
"63": { "start": { "line": 63, "column": 0 }, "end": { "line": 63, "column": 0 } },
"64": { "start": { "line": 64, "column": 0 }, "end": { "line": 64, "column": 0 } },
"65": { "start": { "line": 65, "column": 0 }, "end": { "line": 65, "column": 0 } },
"66": { "start": { "line": 66, "column": 0 }, "end": { "line": 66, "column": 0 } },
"67": { "start": { "line": 67, "column": 0 }, "end": { "line": 67, "column": 0 } },
"68": { "start": { "line": 68, "column": 0 }, "end": { "line": 68, "column": 0 } },
"69": { "start": { "line": 69, "column": 0 }, "end": { "line": 69, "column": 0 } },
"70": { "start": { "line": 70, "column": 0 }, "end": { "line": 70, "column": 0 } },
"71": { "start": { "line": 71, "column": 0 }, "end": { "line": 71, "column": 0 } },
"72": { "start": { "line": 72, "column": 0 }, "end": { "line": 72, "column": 0 } },
"73": { "start": { "line": 73, "column": 0 }, "end": { "line": 73, "column": 0 } },
"74": { "start": { "line": 74, "column": 0 }, "end": { "line": 74, "column": 0 } },
"75": { "start": { "line": 75, "column": 0 }, "end": { "line": 75, "column": 0 } },
"76": { "start": { "line": 76, "column": 0 }, "end": { "line": 76, "column": 0 } },
"77": { "start": { "line": 77, "column": 0 }, "end": { "line": 77, "column": 0 } },
"78": { "start": { "line": 78, "column": 0 }, "end": { "line": 78, "column": 0 } },
"79": { "start": { "line": 79, "column": 0 }, "end": { "line": 79, "column": 0 } },
"80": { "start": { "line": 80, "column": 0 }, "end": { "line": 80, "column": 0 } },
"81": { "start": { "line": 81, "column": 0 }, "end": { "line": 81, "column": 0 } },
"82": { "start": { "line": 82, "column": 0 }, "end": { "line": 82, "column": 0 } }
}
},
"no-map/AccountProfile": {
"fnMap": {},
"branchMap": {},
"path": "no-map/AccountProfile",
"f": {},
"b": {},
"s": {
"52": 0,
"53": 0,
"54": 1,
"55": 1,
"56": 1,
"57": 1,
"58": 1,
"59": 0,
"60": 0,
"61": 1,
"62": 1,
"63": 1,
"64": 1,
"65": 1,
"66": 1,
"67": 1,
"68": 1,
"69": 1,
"70": 1,
"71": 1,
"72": 1,
"73": 1,
"74": 1,
"75": 1,
"76": 1,
"77": 1,
"78": 1,
"79": 1,
"80": 1,
"81": 1,
"82": 1
},
"statementMap": {
"52": { "start": { "line": 52, "column": 0 }, "end": { "line": 52, "column": 0 } },
"53": { "start": { "line": 53, "column": 0 }, "end": { "line": 53, "column": 0 } },
"54": { "start": { "line": 54, "column": 0 }, "end": { "line": 54, "column": 0 } },
"55": { "start": { "line": 55, "column": 0 }, "end": { "line": 55, "column": 0 } },
"56": { "start": { "line": 56, "column": 0 }, "end": { "line": 56, "column": 0 } },
"57": { "start": { "line": 57, "column": 0 }, "end": { "line": 57, "column": 0 } },
"58": { "start": { "line": 58, "column": 0 }, "end": { "line": 58, "column": 0 } },
"59": { "start": { "line": 59, "column": 0 }, "end": { "line": 59, "column": 0 } },
"60": { "start": { "line": 60, "column": 0 }, "end": { "line": 60, "column": 0 } },
"61": { "start": { "line": 61, "column": 0 }, "end": { "line": 61, "column": 0 } },
"62": { "start": { "line": 62, "column": 0 }, "end": { "line": 62, "column": 0 } },
"63": { "start": { "line": 63, "column": 0 }, "end": { "line": 63, "column": 0 } },
"64": { "start": { "line": 64, "column": 0 }, "end": { "line": 64, "column": 0 } },
"65": { "start": { "line": 65, "column": 0 }, "end": { "line": 65, "column": 0 } },
"66": { "start": { "line": 66, "column": 0 }, "end": { "line": 66, "column": 0 } },
"67": { "start": { "line": 67, "column": 0 }, "end": { "line": 67, "column": 0 } },
"68": { "start": { "line": 68, "column": 0 }, "end": { "line": 68, "column": 0 } },
"69": { "start": { "line": 69, "column": 0 }, "end": { "line": 69, "column": 0 } },
"70": { "start": { "line": 70, "column": 0 }, "end": { "line": 70, "column": 0 } },
"71": { "start": { "line": 71, "column": 0 }, "end": { "line": 71, "column": 0 } },
"72": { "start": { "line": 72, "column": 0 }, "end": { "line": 72, "column": 0 } },
"73": { "start": { "line": 73, "column": 0 }, "end": { "line": 73, "column": 0 } },
"74": { "start": { "line": 74, "column": 0 }, "end": { "line": 74, "column": 0 } },
"75": { "start": { "line": 75, "column": 0 }, "end": { "line": 75, "column": 0 } },
"76": { "start": { "line": 76, "column": 0 }, "end": { "line": 76, "column": 0 } },
"77": { "start": { "line": 77, "column": 0 }, "end": { "line": 77, "column": 0 } },
"78": { "start": { "line": 78, "column": 0 }, "end": { "line": 78, "column": 0 } },
"79": { "start": { "line": 79, "column": 0 }, "end": { "line": 79, "column": 0 } },
"80": { "start": { "line": 80, "column": 0 }, "end": { "line": 80, "column": 0 } },
"81": { "start": { "line": 81, "column": 0 }, "end": { "line": 81, "column": 0 } },
"82": { "start": { "line": 82, "column": 0 }, "end": { "line": 82, "column": 0 } }
}
},
"no-map/Get_Info": {
"fnMap": {},
"branchMap": {},
"path": "no-map/Get_Info",
"f": {},
"b": {},
"s": {
"52": 0,
"53": 0,
"54": 1,
"55": 1,
"56": 1,
"57": 1,
"58": 1,
"59": 0,
"60": 0,
"61": 1,
"62": 1,
"63": 1,
"64": 1,
"65": 1,
"66": 1,
"67": 1,
"68": 1,
"69": 1,
"70": 1,
"71": 1,
"72": 1,
"73": 1,
"74": 1,
"75": 1,
"76": 1,
"77": 1,
"78": 1,
"79": 1,
"80": 1,
"81": 1,
"82": 1
},
"statementMap": {
"52": { "start": { "line": 52, "column": 0 }, "end": { "line": 52, "column": 0 } },
"53": { "start": { "line": 53, "column": 0 }, "end": { "line": 53, "column": 0 } },
"54": { "start": { "line": 54, "column": 0 }, "end": { "line": 54, "column": 0 } },
"55": { "start": { "line": 55, "column": 0 }, "end": { "line": 55, "column": 0 } },
"56": { "start": { "line": 56, "column": 0 }, "end": { "line": 56, "column": 0 } },
"57": { "start": { "line": 57, "column": 0 }, "end": { "line": 57, "column": 0 } },
"58": { "start": { "line": 58, "column": 0 }, "end": { "line": 58, "column": 0 } },
"59": { "start": { "line": 59, "column": 0 }, "end": { "line": 59, "column": 0 } },
"60": { "start": { "line": 60, "column": 0 }, "end": { "line": 60, "column": 0 } },
"61": { "start": { "line": 61, "column": 0 }, "end": { "line": 61, "column": 0 } },
"62": { "start": { "line": 62, "column": 0 }, "end": { "line": 62, "column": 0 } },
"63": { "start": { "line": 63, "column": 0 }, "end": { "line": 63, "column": 0 } },
"64": { "start": { "line": 64, "column": 0 }, "end": { "line": 64, "column": 0 } },
"65": { "start": { "line": 65, "column": 0 }, "end": { "line": 65, "column": 0 } },
"66": { "start": { "line": 66, "column": 0 }, "end": { "line": 66, "column": 0 } },
"67": { "start": { "line": 67, "column": 0 }, "end": { "line": 67, "column": 0 } },
"68": { "start": { "line": 68, "column": 0 }, "end": { "line": 68, "column": 0 } },
"69": { "start": { "line": 69, "column": 0 }, "end": { "line": 69, "column": 0 } },
"70": { "start": { "line": 70, "column": 0 }, "end": { "line": 70, "column": 0 } },
"71": { "start": { "line": 71, "column": 0 }, "end": { "line": 71, "column": 0 } },
"72": { "start": { "line": 72, "column": 0 }, "end": { "line": 72, "column": 0 } },
"73": { "start": { "line": 73, "column": 0 }, "end": { "line": 73, "column": 0 } },
"74": { "start": { "line": 74, "column": 0 }, "end": { "line": 74, "column": 0 } },
"75": { "start": { "line": 75, "column": 0 }, "end": { "line": 75, "column": 0 } },
"76": { "start": { "line": 76, "column": 0 }, "end": { "line": 76, "column": 0 } },
"77": { "start": { "line": 77, "column": 0 }, "end": { "line": 77, "column": 0 } },
"78": { "start": { "line": 78, "column": 0 }, "end": { "line": 78, "column": 0 } },
"79": { "start": { "line": 79, "column": 0 }, "end": { "line": 79, "column": 0 } },
"80": { "start": { "line": 80, "column": 0 }, "end": { "line": 80, "column": 0 } },
"81": { "start": { "line": 81, "column": 0 }, "end": { "line": 81, "column": 0 } },
"82": { "start": { "line": 82, "column": 0 }, "end": { "line": 82, "column": 0 } }
}
}
}
```

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

```xml
<?xml version="1.0"?>
<coverage version="1">
<file path="force-app/main/default/triggers/AccountTrigger.trigger">
<file path="force-app\main\default\triggers\AccountTrigger.trigger">
<lineToCover lineNumber="52" covered="false"/>
<lineToCover lineNumber="53" covered="false"/>
<lineToCover lineNumber="59" covered="false"/>
<lineToCover lineNumber="60" covered="false"/>
</file>
<file path="force-app/main/default/classes/AccountProfile.cls">
<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"/>
</file>
<file path="force-app/main/default/flows/Get_Info.flow-meta.xml">
<file path="force-app\main\default\flows\Get_Info.flow-meta.xml">
<lineToCover lineNumber="52" covered="false"/>
<lineToCover lineNumber="53" covered="false"/>
<lineToCover lineNumber="59" covered="false"/>
Expand Down
4 changes: 2 additions & 2 deletions messages/transformer.transform.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ This plugin will convert the JSON file created by the Salesforce CLI during Apex

- `sf apex-code-coverage transformer transform --coverage-json "path-to-cli-coverage.json"`

# flags.dx-directory.summary
# flags.sfdx-configuration.summary

Directory containing Salesforce metadata relative to your repository (default: `force-app/main/default`).
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.

# flags.coverage-json.summary

Expand Down
25 changes: 12 additions & 13 deletions src/commands/apex-code-coverage/transformer/transform.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
'use strict';

import * as fs from 'node:fs';
import * as path from 'node:path';

Expand All @@ -20,12 +21,12 @@ export default class TransformerTransform extends SfCommand<TransformerTransform
public static readonly examples = messages.getMessages('examples');

public static readonly flags = {
'dx-directory': Flags.directory({
summary: messages.getMessage('flags.dx-directory.summary'),
char: 'd',
'sfdx-configuration': Flags.file({
summary: messages.getMessage('flags.sfdx-configuration.summary'),
char: 'c',
required: true,
exists: true,
default: 'force-app/main/default',
default: 'sfdx-project.json',
}),
'coverage-json': Flags.file({
summary: messages.getMessage('flags.coverage-json.summary'),
Expand All @@ -46,25 +47,23 @@ export default class TransformerTransform extends SfCommand<TransformerTransform
const { flags } = await this.parse(TransformerTransform);
let jsonFilePath = flags['coverage-json'];
let xmlFilePath = flags['xml'];
const dxDirectory = flags['dx-directory'];
let sfdxConfigFile = flags['sfdx-configuration'];
jsonFilePath = path.resolve(jsonFilePath);
xmlFilePath = path.resolve(xmlFilePath);
// Check if the JSON file exists
if (!fs.existsSync(jsonFilePath)) {
this.error(`JSON file does not exist: ${jsonFilePath}`);
}
sfdxConfigFile = path.resolve(sfdxConfigFile);

const jsonData = fs.readFileSync(jsonFilePath, 'utf-8');
const coverageData = JSON.parse(jsonData) as CoverageData;
const xmlData = convertToGenericCoverageReport(coverageData, dxDirectory);
const xmlData = await convertToGenericCoverageReport(coverageData, sfdxConfigFile);

// Write the XML data to the XML file
try {
fs.writeFileSync(xmlFilePath, xmlData);
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
this.log(`The XML data has been written to ${xmlFilePath}`);
} catch (error) {
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
this.error(`Error writing XML data to file: ${error}`);
if (error instanceof Error) {
this.error(`Error writing XML data to file: ${error.message}`);
}
}

return { path: xmlFilePath };
Expand Down
Loading

0 comments on commit 52c1a12

Please sign in to comment.