Skip to content

Commit

Permalink
feat: add junit result format to test:run command (#96)
Browse files Browse the repository at this point in the history
@W-7561364@
  • Loading branch information
AnanyaJha authored and sfsholden committed Nov 20, 2020
1 parent 09b3b12 commit d2b645d
Show file tree
Hide file tree
Showing 11 changed files with 480 additions and 35 deletions.
2 changes: 1 addition & 1 deletion packages/apex-node/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,5 @@ export {
ApexExecuteOptions
} from './execute';
export { LogService, ApexLogGetOptions } from './logs';
export { TapReporter } from './reporters';
export { JUnitReporter, TapReporter } from './reporters';
export { TestService } from './tests';
1 change: 1 addition & 0 deletions packages/apex-node/src/reporters/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@
*/

export { TapReporter } from './tapReporter';
export { JUnitReporter } from './junitReporter';
92 changes: 92 additions & 0 deletions packages/apex-node/src/reporters/junitReporter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/*
* Copyright (c) 2020, salesforce.com, inc.
* All rights reserved.
* Licensed under the BSD 3-Clause license.
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/
import {
ApexTestResultData,
ApexTestResultOutcome,
TestResult
} from '../tests/types';

// cli currently has spaces in multiples of four for junit format
const tab = ' ';

export class JUnitReporter {
public format(testResult: TestResult): string {
const { summary, tests } = testResult;

let output = `<?xml version="1.0" encoding="UTF-8"?>\n`;
output += `<testsuites>\n`;
output += `${tab}<testsuite name="force.apex" `;
output += `timestamp="${summary.testStartTime}" `;
output += `hostname="${summary.hostname}" `;
output += `tests="${summary.numTestsRan}" `;
output += `failures="${summary.failing}" `;
output += `time="${this.msToSecond(summary.testExecutionTime)} s">\n`;

output += this.buildProperties(testResult);
output += this.buildTestCases(tests);

output += `${tab}</testsuite>\n`;
output += `</testsuites>\n`;
return output;
}

private buildProperties(testResult: TestResult): string {
let junitProperties = `${tab}${tab}<properties>\n`;

Object.entries(testResult.summary).forEach(([key, value]) => {
if (
value === null ||
value === undefined ||
(typeof value === 'string' && value.length === 0)
) {
return;
}
if (key === 'testExecutionTime') {
value = `${this.msToSecond(value as number)} s`;
}
if (key === 'testStartTime') {
const date = new Date(value);
value = `${date.toDateString()} ${date.toLocaleTimeString()}`;
}

junitProperties += `${tab}${tab}${tab}<property name="${key}" value="${value}"/>\n`;
});

junitProperties += `${tab}${tab}</properties>\n`;
return junitProperties;
}

private buildTestCases(tests: ApexTestResultData[]): string {
let junitTests = '';

for (const testCase of tests) {
junitTests += `${tab}${tab}<testcase name="${
testCase.methodName
}" classname="${testCase.apexClass.fullName}" time="${this.msToSecond(
testCase.runTime
)}">\n`;

if (
testCase.outcome === ApexTestResultOutcome.Fail ||
testCase.outcome === ApexTestResultOutcome.CompileFail
) {
junitTests += `${tab}${tab}${tab}<failure message="${testCase.message}">`;
if (testCase.stackTrace) {
junitTests += `<![CDATA[${testCase.stackTrace}]]>`;
}
junitTests += `</failure>\n`;
}

junitTests += `${tab}${tab}</testcase>\n`;
}
return junitTests;
}

private msToSecond(timestamp: number): string {
return (timestamp / 1000).toFixed(2);
}
}
40 changes: 24 additions & 16 deletions packages/apex-node/src/tests/testService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,26 +110,30 @@ export class TestService {
const globalTestPassed = apiTestResult.successes.length;
const result: TestResult = {
summary: {
failRate: this.calculatePercentage(
globalTestFailed,
apiTestResult.numTestsRun
),
numTestsRan: apiTestResult.numTestsRun,
orgId: this.connection.getAuthInfoFields().orgId,
outcome:
globalTestFailed === 0
? ApexTestRunResultStatus.Completed
: ApexTestRunResultStatus.Failed,
numTestsRan: apiTestResult.numTestsRun,
passing: globalTestPassed,
failing: globalTestFailed,
skipped: 0,
passRate: this.calculatePercentage(
globalTestPassed,
apiTestResult.numTestsRun
),
failRate: this.calculatePercentage(
globalTestFailed,
apiTestResult.numTestsRun
),
skipRate: this.calculatePercentage(0, apiTestResult.numTestsRun),
testStartTime: `${startTime}`,
testExecutionTime: apiTestResult.totalTime,
hostname: this.connection.instanceUrl,
orgId: this.connection.getAuthInfoFields().orgId,
username: this.connection.getUsername(),
testRunId: '',
userId: this.connection.getConnectionOptions().userId,
username: this.connection.getUsername()
userId: this.connection.getConnectionOptions().userId
},
tests: testResults
};
Expand Down Expand Up @@ -295,26 +299,30 @@ export class TestService {

const result: TestResult = {
summary: {
failRate: this.calculatePercentage(
globalTestFailed,
testResults.length
),
numTestsRan: testResults.length,
orgId: this.connection.getAuthInfoFields().orgId,
outcome: summaryRecord.Status,
numTestsRan: testResults.length,
passing: globalTestPassed,
failing: globalTestFailed,
skipped: globalTestSkipped,
passRate: this.calculatePercentage(
globalTestPassed,
testResults.length
),
failRate: this.calculatePercentage(
globalTestFailed,
testResults.length
),
skipRate: this.calculatePercentage(
globalTestSkipped,
testResults.length
),
testStartTime: summaryRecord.StartTime,
testExecutionTime: summaryRecord.TestTime,
hostname: this.connection.instanceUrl,
orgId: this.connection.getAuthInfoFields().orgId,
username: this.connection.getUsername(),
testRunId,
userId: summaryRecord.UserId,
username: this.connection.getUsername()
userId: summaryRecord.UserId
},
tests: testResults
};
Expand Down
6 changes: 5 additions & 1 deletion packages/apex-node/src/tests/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -354,13 +354,17 @@ export type TestResult = {
orgId: string;
orgWideCoverage?: string;
outcome: string;
passing: number;
failing: number;
skipped: number;
passRate: string;
skipRate: string;
testStartTime: string;
testExecutionTime: number;
hostname: string;
username: string;
testRunId: string;
userId: string;
username: string;
};
tests: ApexTestResultData[];
codecoverage?: CodeCoverageResult[];
Expand Down
65 changes: 65 additions & 0 deletions packages/apex-node/test/reporters/junitReporter.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/*
* Copyright (c) 2020, salesforce.com, inc.
* All rights reserved.
* Licensed under the BSD 3-Clause license.
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/
import { expect } from 'chai';
import { JUnitReporter } from '../../src';
import {
testResults,
junitResult,
junitSuccess,
junitCodeCov,
junitMissingVal,
successResult
} from './testResults';

describe('JUnit Reporter Tests', () => {
const reporter = new JUnitReporter();

it('should format test results with failures', () => {
const result = reporter.format(testResults);
expect(result).to.not.be.empty;
expect(result).to.eql(junitResult);
expect(result).to.contain('</failure>');
});

it('should format tests with 0 failures', async () => {
const result = reporter.format(successResult);
expect(result).to.not.be.empty;
expect(result).to.eql(junitSuccess);
expect(result).to.not.contain('</failure>');
});

it('should format test results with undefined or empty values', () => {
successResult.summary.testRunId = '';
successResult.summary.userId = undefined;

const result = reporter.format(successResult);
expect(result).to.not.be.empty;
expect(result).to.eql(junitMissingVal);
expect(result).to.not.contain('testRunId');
expect(result).to.not.contain('userId');
});

it('should format test results with code coverage', () => {
successResult.codecoverage = [
{
apexId: '001917xACG',
name: 'ApexTestClass',
type: 'ApexClass',
numLinesCovered: 8,
numLinesUncovered: 2,
percentage: '12.5%',
coveredLines: [1, 2, 3, 4, 5, 6, 7, 8],
uncoveredLines: [9, 10]
}
];
successResult.summary.orgWideCoverage = '85%';
const result = reporter.format(successResult);
expect(result).to.not.be.empty;
expect(result).to.eql(junitCodeCov);
expect(result).to.contain('orgWideCoverage');
});
});
Loading

0 comments on commit d2b645d

Please sign in to comment.