Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: remove command flag by adding type guard functions #48

Merged
merged 2 commits into from
Oct 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ on:
push:
branches-ignore:
- main
paths-ignore:
- '**.md'

jobs:
unit-tests:
Expand Down
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 code coverage files created by the test commands. If the code coverage JSON file provided 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