Skip to content

Commit

Permalink
feat: remove command flag by adding type guard functions
Browse files Browse the repository at this point in the history
  • Loading branch information
mcarvin8 committed Oct 22, 2024
1 parent d2de3fe commit be380bf
Show file tree
Hide file tree
Showing 11 changed files with 250 additions and 74 deletions.
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -47,4 +47,7 @@ node_modules

oclif.manifest.json

oclif.lock
oclif.lock
*.log
stderr*.txt
stdout*.txt
11 changes: 8 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,11 @@ This command needs to be ran somewhere inside your Salesforce DX git repository,

```
USAGE
$ sf acc-transformer transform -j <value> -x <value> -c <value> [--json]
$ sf acc-transformer transform -j <value> -x <value> [--json]
FLAGS
-j, --coverage-json=<value> Path to the code coverage JSON file created by the Salesforce CLI deployment or test command.
-x, --xml=<value> [default: "coverage.xml"] Path to code coverage XML file that will be created by this plugin.
-c, --command=<value> [default: "deploy"] The type of Salesforce CLI command you are running. Valid options: "deploy" or "test".
GLOBAL FLAGS
--json Format output as json.
Expand All @@ -57,7 +56,7 @@ DESCRIPTION
This plugin will convert the code coverage JSON file created by the Salesforce CLI during Apex deployments and test runs into an XML accepted by tools like SonarQube.
EXAMPLES
$ sf acc-transformer transform -j "coverage.json" -x "coverage.xml" -c "deploy"
$ sf acc-transformer transform -j "coverage.json" -x "coverage.xml"
```

## Hook
Expand Down Expand Up @@ -102,6 +101,12 @@ Warning: The file name AccountProfile was not found in any package directory.
Warning: None of the files listed in the coverage JSON were processed. The coverage XML will be empty.
```

The code coverage JSON files created by the Salesforce CLI deployment commands follow a different format than the format created by the test commands. If the code coverage JSON file does not match one of the 2 expected coverage data types, the plugin will fail with:

```
Error (1): The provided JSON does not match a known coverage data format from the Salesforce deploy or test command.
```

If the `sfdx-project.json` file was not found in your repository's root folder, the plugin will fail with:

```
Expand Down
6 changes: 1 addition & 5 deletions messages/transformer.transform.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ This plugin will convert the code coverage JSON file created by the Salesforce C

# examples

- `sf acc-transformer transform -j "coverage.json" -x "coverage.xml" -c "deploy"`
- `sf acc-transformer transform -j "coverage.json" -x "coverage.xml"`

# flags.coverage-json.summary

Expand All @@ -17,7 +17,3 @@ Path to the code coverage JSON file created by the Salesforce CLI deployment or
# flags.xml.summary

Path to code coverage XML file that will be created by this plugin.

# flags.command.summary

The type of Salesforce CLI command you are running.
27 changes: 13 additions & 14 deletions src/commands/acc-transformer/transform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { Messages } from '@salesforce/core';
import { DeployCoverageData, TestCoverageData, TransformerTransformResult } from '../../helpers/types.js';
import { transformDeployCoverageReport } from '../../helpers/transformDeployCoverageReport.js';
import { transformTestCoverageReport } from '../../helpers/transformTestCoverageReport.js';
import { checkCoverageDataType } from '../../helpers/setCoverageDataType.js';

Messages.importMessagesDirectoryFromMetaUrl(import.meta.url);
const messages = Messages.loadMessages('apex-code-coverage-transformer', 'transformer.transform');
Expand All @@ -31,37 +32,35 @@ export default class TransformerTransform extends SfCommand<TransformerTransform
exists: false,
default: 'coverage.xml',
}),
command: Flags.string({
summary: messages.getMessage('flags.command.summary'),
char: 'c',
required: true,
default: 'deploy',
options: ['deploy', 'test'],
}),
};

public async run(): Promise<TransformerTransformResult> {
const { flags } = await this.parse(TransformerTransform);
const jsonFilePath = resolve(flags['coverage-json']);
const xmlFilePath = resolve(flags['xml']);
const commandType = flags['command'];
const jsonData = await readFile(jsonFilePath, 'utf-8');

let xmlData: string;
let warnings: string[] = [];
let filesProcessed: number = 0;
if (commandType === 'test') {
const coverageData = JSON.parse(jsonData) as TestCoverageData[];
const result = await transformTestCoverageReport(coverageData);
const parsedData = JSON.parse(jsonData) as DeployCoverageData | TestCoverageData[];
const commandType = checkCoverageDataType(parsedData);

// Determine the type of coverage data using type guards
if (commandType === 'TestCoverageData') {
const result = await transformTestCoverageReport(parsedData as TestCoverageData[]);
xmlData = result.xml;
warnings = result.warnings;
filesProcessed = result.filesProcessed;
} else {
const coverageData = JSON.parse(jsonData) as DeployCoverageData;
const result = await transformDeployCoverageReport(coverageData);
} else if (commandType === 'DeployCoverageData') {
const result = await transformDeployCoverageReport(parsedData as DeployCoverageData);
xmlData = result.xml;
warnings = result.warnings;
filesProcessed = result.filesProcessed;
} else {
this.error(
'The provided JSON does not match a known coverage data format from the Salesforce deploy or test command.'
);
}

// Print warnings if any
Expand Down
82 changes: 82 additions & 0 deletions src/helpers/setCoverageDataType.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
'use strict';

import { DeployCoverageData, TestCoverageData } from './types.js';

// Type guard for DeployCoverageData
export function isDeployCoverageData(data: unknown): data is DeployCoverageData {
if (typeof data !== 'object' || data === null) return false;

return Object.entries(data).every(([, item]) => {
if (typeof item !== 'object' || item === null) return false;

const { path, fnMap, branchMap, f, b, s, statementMap } = item as {
path: unknown;
fnMap: unknown;
branchMap: unknown;
f: unknown;
b: unknown;
s: unknown;
statementMap: unknown;
};

if (
typeof path !== 'string' ||
typeof fnMap !== 'object' ||
typeof branchMap !== 'object' ||
typeof f !== 'object' ||
typeof b !== 'object' ||
typeof s !== 'object' ||
typeof statementMap !== 'object' ||
statementMap === null
) {
return false;
}

return Object.values(statementMap).every((statement) => {
if (typeof statement !== 'object' || statement === null) return false;
const { start, end } = statement as { start: unknown; end: unknown };

return (
typeof start === 'object' &&
start !== null &&
typeof (start as { line: unknown }).line === 'number' &&
typeof (start as { column: unknown }).column === 'number' &&
typeof end === 'object' &&
end !== null &&
typeof (end as { line: unknown }).line === 'number' &&
typeof (end as { column: unknown }).column === 'number'
);
});
});
}

// Type guard for a single TestCoverageData
export function isSingleTestCoverageData(data: unknown): data is TestCoverageData {
return (
typeof data === 'object' &&
data !== null &&
typeof (data as TestCoverageData).id === 'string' &&
typeof (data as TestCoverageData).name === 'string' &&
typeof (data as TestCoverageData).totalLines === 'number' &&
typeof (data as TestCoverageData).lines === 'object' &&
typeof (data as TestCoverageData).totalCovered === 'number' &&
typeof (data as TestCoverageData).coveredPercent === 'number' &&
Object.values((data as TestCoverageData).lines).every((line: unknown) => typeof line === 'number')
);
}

// Type guard for TestCoverageData array
export function isTestCoverageDataArray(data: unknown): data is TestCoverageData[] {
return Array.isArray(data) && data.every(isSingleTestCoverageData);
}

export function checkCoverageDataType(
data: DeployCoverageData | TestCoverageData[]
): 'DeployCoverageData' | 'TestCoverageData' | 'Unknown' {
if (isDeployCoverageData(data)) {
return 'DeployCoverageData';
} else if (isTestCoverageDataArray(data)) {
return 'TestCoverageData';
}
return 'Unknown';
}
2 changes: 0 additions & 2 deletions src/hooks/postrun.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,5 @@ export const postrun: Hook<'postrun'> = async function (options) {
commandArgs.push(coverageJsonPath);
commandArgs.push('--xml');
commandArgs.push(coverageXmlPath);
commandArgs.push('--command');
commandArgs.push(commandType);
await TransformerTransform.run(commandArgs);
};
69 changes: 44 additions & 25 deletions test/commands/acc-transformer/transform.nut.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,13 @@ describe('acc-transformer transform NUTs', () => {
const deployCoverageNoExts = resolve('test/deploy_coverage_no_file_exts.json');
const deployCoverageWithExts = resolve('test/deploy_coverage_with_file_exts.json');
const testCoverage = resolve('test/test_coverage.json');
const baselineXmlPath = resolve('test/coverage_baseline.xml');
const testXmlPath1 = resolve('coverage1.xml');
const testXmlPath2 = resolve('coverage2.xml');
const testXmlPath3 = resolve('coverage3.xml');
const invalidJson = resolve('test/invalid.json');
const deployBaselineXmlPath = resolve('test/deploy_coverage_baseline.xml');
const testBaselineXmlPath = resolve('test/test_coverage_baseline.xml');
const coverageXmlPath1 = resolve('coverage1.xml');
const coverageXmlPath2 = resolve('coverage2.xml');
const coverageXmlPath3 = resolve('coverage3.xml');
const sfdxConfigFile = resolve('sfdx-project.json');

const configFile = {
packageDirectories: [{ path: 'force-app', default: true }, { path: 'packaged' }],
Expand All @@ -29,7 +32,7 @@ describe('acc-transformer transform NUTs', () => {

before(async () => {
session = await TestSession.create({ devhubAuthStrategy: 'NONE' });
await writeFile('sfdx-project.json', configJsonString);
await writeFile(sfdxConfigFile, configJsonString);
await mkdir('force-app/main/default/classes', { recursive: true });
await mkdir('packaged/triggers', { recursive: true });
await copyFile(baselineClassPath, 'force-app/main/default/classes/AccountProfile.cls');
Expand All @@ -38,49 +41,65 @@ describe('acc-transformer transform NUTs', () => {

after(async () => {
await session?.clean();
await rm('sfdx-project.json');
await rm(sfdxConfigFile);
await rm('force-app/main/default/classes/AccountProfile.cls');
await rm('packaged/triggers/AccountTrigger.trigger');
await rm('force-app', { recursive: true });
await rm('packaged', { recursive: true });
await rm(testXmlPath1);
await rm(testXmlPath2);
await rm(testXmlPath3);
await rm(coverageXmlPath1);
await rm(coverageXmlPath2);
await rm(coverageXmlPath3);
});

it('runs transform on the deploy coverage file without file extensions.', async () => {
const command = `acc-transformer transform --coverage-json "${deployCoverageNoExts}" --xml "${testXmlPath1}"`;
const command = `acc-transformer transform --coverage-json "${deployCoverageNoExts}" --xml "${coverageXmlPath1}"`;
const output = execCmd(command, { ensureExitCode: 0 }).shellOutput.stdout;

expect(output.replace('\n', '')).to.equal(`The coverage XML has been written to ${testXmlPath1}`);
expect(output.replace('\n', '')).to.equal(`The coverage XML has been written to ${coverageXmlPath1}`);
});

it('runs transform on the deploy coverage file with file extensions.', async () => {
const command = `acc-transformer transform --coverage-json "${deployCoverageWithExts}" --xml "${testXmlPath2}"`;
const command = `acc-transformer transform --coverage-json "${deployCoverageWithExts}" --xml "${coverageXmlPath2}"`;
const output = execCmd(command, { ensureExitCode: 0 }).shellOutput.stdout;

expect(output.replace('\n', '')).to.equal(`The coverage XML has been written to ${testXmlPath2}`);
expect(output.replace('\n', '')).to.equal(`The coverage XML has been written to ${coverageXmlPath2}`);
});

it('runs transform on the test coverage file.', async () => {
const command = `acc-transformer transform --coverage-json "${testCoverage}" --xml "${testXmlPath3}"`;
const command = `acc-transformer transform --coverage-json "${testCoverage}" --xml "${coverageXmlPath3}"`;
const output = execCmd(command, { ensureExitCode: 0 }).shellOutput.stdout;

expect(output.replace('\n', '')).to.equal(`The coverage XML has been written to ${testXmlPath3}`);
expect(output.replace('\n', '')).to.equal(`The coverage XML has been written to ${coverageXmlPath3}`);
});
it('confirm the 2 XML files created in the previous tests are the same as the baseline.', async () => {
const xmlContent1 = await readFile(testXmlPath1, 'utf-8');
const xmlContent2 = await readFile(testXmlPath2, 'utf-8');
const baselineXmlContent = await readFile(baselineXmlPath, 'utf-8');
it('confirms a failure on an invalid JSON file.', async () => {
const command = `acc-transformer transform --coverage-json "${invalidJson}"`;
const error = execCmd(command, { ensureExitCode: 2 }).shellOutput.stderr;

expect(error.replace('\n', '')).to.contain(
'The provided JSON does not match a known coverage data format from the Salesforce deploy or test command.'
);
});

it('confirm the XML files created are the same as the baselines.', async () => {
const deployXml1 = await readFile(coverageXmlPath1, 'utf-8');
const deployXml2 = await readFile(coverageXmlPath2, 'utf-8');
const testXml = await readFile(coverageXmlPath3, 'utf-8');
const deployBaselineXmlContent = await readFile(deployBaselineXmlPath, 'utf-8');
const testBaselineXmlContent = await readFile(testBaselineXmlPath, 'utf-8');
strictEqual(
deployXml1,
deployBaselineXmlContent,
`File content is different between ${coverageXmlPath1} and ${deployBaselineXmlPath}`
);
strictEqual(
xmlContent1,
baselineXmlContent,
`File content is different between ${testXmlPath1} and ${baselineXmlPath}`
deployXml2,
deployBaselineXmlContent,
`File content is different between ${coverageXmlPath2} and ${deployBaselineXmlPath}`
);
strictEqual(
xmlContent2,
baselineXmlContent,
`File content is different between ${testXmlPath2} and ${baselineXmlPath}`
testXml,
testBaselineXmlContent,
`File content is different between ${coverageXmlPath2} and ${testBaselineXmlPath}`
);
});
});
Loading

0 comments on commit be380bf

Please sign in to comment.