Skip to content

Commit b7af57a

Browse files
authored
feat: color nuances for test coverage % (#756)
1 parent f74f3e7 commit b7af57a

File tree

6 files changed

+432
-105
lines changed

6 files changed

+432
-105
lines changed

src/commands/project/reset/tracking.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,14 @@
66
*/
77

88
import { Messages } from '@salesforce/core';
9-
import * as chalk from 'chalk';
109
import { SourceTracking } from '@salesforce/source-tracking';
1110
import {
1211
Flags,
1312
loglevel,
1413
orgApiVersionFlagWithDeprecations,
1514
requiredOrgFlagWithDeprecations,
1615
SfCommand,
16+
StandardColors,
1717
} from '@salesforce/sf-plugins-core';
1818

1919
Messages.importMessagesDirectory(__dirname);
@@ -53,7 +53,7 @@ export class ResetTracking extends SfCommand<ResetTrackingResult> {
5353
public async run(): Promise<ResetTrackingResult> {
5454
const { flags } = await this.parse(ResetTracking);
5555

56-
if (flags['no-prompt'] || (await this.confirm(chalk.dim(messages.getMessage('promptMessage'))))) {
56+
if (flags['no-prompt'] || (await this.confirm(StandardColors.info(messages.getMessage('promptMessage'))))) {
5757
const sourceTracking = await SourceTracking.create({
5858
project: this.project,
5959
org: flags['target-org'],

src/formatters/deleteResultFormatter.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,9 @@
55
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
66
*/
77
import { ux } from '@oclif/core';
8-
import * as chalk from 'chalk';
98
import { DeployResult, FileResponse, RequestStatus } from '@salesforce/source-deploy-retrieve';
109
import { ensureArray } from '@salesforce/kit';
11-
import { bold } from 'chalk';
10+
import { bold, blue } from 'chalk';
1211
import { StandardColors } from '@salesforce/sf-plugins-core';
1312
import { DeleteSourceJson, Formatter, TestLevel } from '../utils/types';
1413
import { sortFileResponses, asRelativePaths } from '../utils/output';
@@ -76,7 +75,7 @@ export class DeleteResultFormatter extends TestResultsFormatter implements Forma
7675
}
7776

7877
ux.log('');
79-
ux.styledHeader(chalk.blue('Deleted Source'));
78+
ux.styledHeader(blue('Deleted Source'));
8079
ux.table(
8180
successes.map((entry) => ({
8281
fullName: entry.fullName,

src/formatters/deployResultFormatter.ts

Lines changed: 13 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import { DeployResultJson, isSdrFailure, isSdrSuccess, TestLevel, Verbosity, For
2222
import {
2323
generateCoveredLines,
2424
getCoverageFormattersOptions,
25+
getCoverageNumbers,
2526
mapTestResults,
2627
transformCoverageToApexCoverage,
2728
} from '../utils/coverage';
@@ -63,20 +64,17 @@ export class DeployResultFormatter extends TestResultsFormatter implements Forma
6364
(!this.result.response?.numberTestsTotal && !this.flags['test-level']) ||
6465
this.flags['test-level'] === 'NoTestRun'
6566
) {
66-
let testsWarn = '';
67-
68-
if (this.coverageOptions.reportFormats?.length) {
69-
testsWarn += `\`--coverage-formatters\` was specified but no tests ran.${EOL}`;
70-
}
71-
if (this.junit) {
72-
testsWarn += `\`--junit\` was specified but no tests ran.${EOL}`;
73-
}
67+
const testsWarn = (
68+
this.coverageOptions.reportFormats?.length ? ['`--coverage-formatters` was specified but no tests ran.'] : []
69+
)
70+
.concat(this.junit ? ['`--junit` was specified but no tests ran.'] : [])
71+
.concat([
72+
'You can ensure tests run by specifying `--test-level` and setting it to `RunSpecifiedTests`, `RunLocalTests` or `RunAllTestsInOrg`.',
73+
]);
7474

7575
// only emit warning if --coverage-formatters or --junit flags were passed
76-
if (testsWarn.length > 0) {
77-
testsWarn +=
78-
'You can ensure tests run by specifying `--test-level` and setting it to `RunSpecifiedTests`, `RunLocalTests` or `RunAllTestsInOrg`.';
79-
await Lifecycle.getInstance().emitWarning(testsWarn);
76+
if (testsWarn.length > 1) {
77+
await Lifecycle.getInstance().emitWarning(testsWarn.join(EOL));
8078
}
8179
}
8280

@@ -185,10 +183,9 @@ export class DeployResultFormatter extends TestResultsFormatter implements Forma
185183
...mapTestResults(ensureArray(runTestResult.failures)),
186184
],
187185
codecoverage: ensureArray(runTestResult?.codeCoverage).map((cov): CodeCoverageResult => {
188-
const numLinesUncovered = parseInt(cov.numLocationsNotCovered, 10);
189186
const [uncoveredLines, coveredLines] = generateCoveredLines(cov);
190-
const numLocationsNum = parseInt(cov.numLocations, 10);
191-
const numLocationsNotCovered: number = parseInt(cov.numLocationsNotCovered, 10);
187+
const [numLocationsNum, numLinesUncovered] = getCoverageNumbers(cov);
188+
192189
return {
193190
// TODO: fix this type in SDR?
194191
type: cov.type as 'ApexClass' | 'ApexTrigger',
@@ -200,7 +197,7 @@ export class DeployResultFormatter extends TestResultsFormatter implements Forma
200197
uncoveredLines,
201198
percentage:
202199
numLocationsNum > 0
203-
? (((numLocationsNum - numLocationsNotCovered) / numLocationsNum) * 100).toFixed() + '%'
200+
? (((numLocationsNum - numLinesUncovered) / numLocationsNum) * 100).toFixed() + '%'
204201
: '',
205202
};
206203
}),

src/formatters/testResultsFormatter.ts

Lines changed: 57 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,15 @@
77
import * as os from 'os';
88
import { ux } from '@oclif/core';
99
import { dim, underline } from 'chalk';
10-
import { CodeCoverageWarnings, DeployResult, Failures, Successes } from '@salesforce/source-deploy-retrieve';
10+
import {
11+
CodeCoverage,
12+
CodeCoverageWarnings,
13+
DeployResult,
14+
Failures,
15+
MetadataApiDeployStatus,
16+
RunTestResult,
17+
Successes,
18+
} from '@salesforce/source-deploy-retrieve';
1119
import { ensureArray } from '@salesforce/kit';
1220
import { TestLevel, Verbosity } from '../utils/types';
1321
import { tableHeader, error, success, check } from '../utils/output';
@@ -34,11 +42,11 @@ export class TestResultsFormatter {
3442
return;
3543
}
3644

37-
this.displayVerboseTestFailures();
45+
displayVerboseTestFailures(this.result.response);
3846

3947
if (this.verbosity === 'verbose') {
40-
this.displayVerboseTestSuccesses();
41-
this.displayVerboseTestCoverage();
48+
displayVerboseTestSuccesses(this.result.response.details.runTestResult?.successes);
49+
displayVerboseTestCoverage(this.result.response.details.runTestResult?.codeCoverage);
4250
}
4351

4452
ux.log();
@@ -59,60 +67,58 @@ export class TestResultsFormatter {
5967
if (this.flags.verbose) return 'verbose';
6068
return 'normal';
6169
}
70+
}
6271

63-
private displayVerboseTestCoverage(): void {
64-
const codeCoverage = ensureArray(this.result.response.details.runTestResult?.codeCoverage);
65-
if (codeCoverage.length) {
66-
const coverage = codeCoverage.sort((a, b) => (a.name.toUpperCase() > b.name.toUpperCase() ? 1 : -1));
67-
ux.log();
68-
ux.log(tableHeader('Apex Code Coverage'));
69-
70-
ux.table(coverage.map(coverageOutput), {
71-
name: { header: 'Name' },
72-
numLocations: { header: '% Covered' },
73-
lineNotCovered: { header: 'Uncovered Lines' },
74-
});
72+
const displayVerboseTestSuccesses = (resultSuccesses: RunTestResult['successes']): void => {
73+
const successes = ensureArray(resultSuccesses).sort(testResultSort);
74+
if (successes.length > 0) {
75+
ux.log();
76+
ux.log(success(`Test Success [${successes.length}]`));
77+
for (const test of successes) {
78+
const testName = underline(`${test.name}.${test.methodName}`);
79+
ux.log(`${check} ${testName}`);
7580
}
7681
}
82+
};
7783

78-
private displayVerboseTestSuccesses(): void {
79-
const successes = ensureArray(this.result.response.details.runTestResult?.successes);
80-
if (successes.length > 0) {
81-
const testSuccesses = sortTestResults(successes);
82-
ux.log();
83-
ux.log(success(`Test Success [${successes.length}]`));
84-
for (const test of testSuccesses) {
85-
const testName = underline(`${test.name}.${test.methodName}`);
86-
ux.log(`${check} ${testName}`);
87-
}
84+
/** display the Test failures if there are any testErrors in the mdapi deploy response */
85+
const displayVerboseTestFailures = (response: MetadataApiDeployStatus): void => {
86+
if (!response.numberTestErrors) return;
87+
const failures = ensureArray(response.details.runTestResult?.failures).sort(testResultSort);
88+
const failureCount = response.details.runTestResult?.numFailures;
89+
ux.log();
90+
ux.log(error(`Test Failures [${failureCount}]`));
91+
for (const test of failures) {
92+
const testName = underline(`${test.name}.${test.methodName}`);
93+
ux.log(`• ${testName}`);
94+
ux.log(` ${dim('message')}: ${test.message}`);
95+
if (test.stackTrace) {
96+
const stackTrace = test.stackTrace.replace(/\n/g, `${os.EOL} `);
97+
ux.log(` ${dim('stacktrace')}: ${os.EOL} ${stackTrace}`);
8898
}
99+
ux.log();
89100
}
101+
};
90102

91-
private displayVerboseTestFailures(): void {
92-
if (!this.result.response.numberTestErrors) return;
93-
const failures = ensureArray(this.result.response.details.runTestResult?.failures);
94-
const failureCount = this.result.response.details.runTestResult?.numFailures;
95-
const testFailures = sortTestResults(failures);
103+
/**
104+
* Display the table if there is at least one coverage item in the result
105+
*/
106+
const displayVerboseTestCoverage = (coverage?: CodeCoverage | CodeCoverage[]): void => {
107+
const codeCoverage = ensureArray(coverage);
108+
if (codeCoverage.length) {
96109
ux.log();
97-
ux.log(error(`Test Failures [${failureCount}]`));
98-
for (const test of testFailures) {
99-
const testName = underline(`${test.name}.${test.methodName}`);
100-
ux.log(`• ${testName}`);
101-
ux.log(` ${dim('message')}: ${test.message}`);
102-
if (test.stackTrace) {
103-
const stackTrace = test.stackTrace.replace(/\n/g, `${os.EOL} `);
104-
ux.log(` ${dim('stacktrace')}: ${os.EOL} ${stackTrace}`);
105-
}
106-
ux.log();
107-
}
110+
ux.log(tableHeader('Apex Code Coverage'));
111+
112+
ux.table(codeCoverage.sort(coverageSort).map(coverageOutput), {
113+
name: { header: 'Name' },
114+
coveragePercent: { header: '% Covered' },
115+
linesNotCovered: { header: 'Uncovered Lines' },
116+
});
108117
}
109-
}
118+
};
110119

111-
function sortTestResults<T extends Failures | Successes>(results: T[]): T[] {
112-
return results.sort((a, b) => {
113-
if (a.methodName === b.methodName) {
114-
return a.name.localeCompare(b.name);
115-
}
116-
return a.methodName.localeCompare(b.methodName);
117-
});
118-
}
120+
const testResultSort = <T extends Successes | Failures>(a: T, b: T): number =>
121+
a.methodName === b.methodName ? a.name.localeCompare(b.name) : a.methodName.localeCompare(b.methodName);
122+
123+
const coverageSort = (a: CodeCoverage, b: CodeCoverage): number =>
124+
a.name.toUpperCase() > b.name.toUpperCase() ? 1 : -1;

src/utils/coverage.ts

Lines changed: 31 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
import { Successes, Failures, CodeCoverage } from '@salesforce/source-deploy-retrieve';
1919
import { ensureArray } from '@salesforce/kit';
2020
import { StandardColors } from '@salesforce/sf-plugins-core';
21+
import { Chalk } from 'chalk';
2122

2223
export const mapTestResults = <T extends Failures | Successes>(testResults: T[]): ApexTestResultData[] =>
2324
testResults.map((testResult) => ({
@@ -37,11 +38,10 @@ export const mapTestResults = <T extends Failures | Successes>(testResults: T[])
3738
}));
3839

3940
export const generateCoveredLines = (cov: CodeCoverage): [number[], number[]] => {
40-
const numCovered = parseInt(cov.numLocations, 10);
41-
const numUncovered = parseInt(cov.numLocationsNotCovered, 10);
41+
const [lineCount, uncoveredLineCount] = getCoverageNumbers(cov);
4242
const uncoveredLines = ensureArray(cov.locationsNotCovered).map((location) => parseInt(location.line, 10));
4343
const minLineNumber = uncoveredLines.length ? Math.min(...uncoveredLines) : 1;
44-
const lines = [...Array(numCovered + numUncovered).keys()].map((i) => i + minLineNumber);
44+
const lines = [...Array(lineCount + uncoveredLineCount).keys()].map((i) => i + minLineNumber);
4545
const coveredLines = lines.filter((line) => !uncoveredLines.includes(line));
4646
return [uncoveredLines, coveredLines];
4747
};
@@ -72,53 +72,52 @@ export const getCoverageFormattersOptions = (formatters: string[] = []): Coverag
7272
};
7373

7474
export const transformCoverageToApexCoverage = (mdCoverage: CodeCoverage[]): ApexCodeCoverageAggregate => {
75-
const apexCoverage = mdCoverage.map((cov) => {
76-
const numCovered = parseInt(cov.numLocations, 10);
77-
const numUncovered = parseInt(cov.numLocationsNotCovered, 10);
75+
const apexCoverage = mdCoverage.map((cov): ApexCodeCoverageAggregateRecord => {
76+
const [NumLinesCovered, NumLinesUncovered] = getCoverageNumbers(cov);
7877
const [uncoveredLines, coveredLines] = generateCoveredLines(cov);
7978

80-
const ac: ApexCodeCoverageAggregateRecord = {
79+
return {
8180
ApexClassOrTrigger: {
8281
Id: cov.id,
8382
Name: cov.name,
8483
},
85-
NumLinesCovered: numCovered,
86-
NumLinesUncovered: numUncovered,
84+
NumLinesCovered,
85+
NumLinesUncovered,
8786
Coverage: {
8887
coveredLines,
8988
uncoveredLines,
9089
},
9190
};
92-
return ac;
9391
});
9492
return { done: true, totalSize: apexCoverage.length, records: apexCoverage };
9593
};
9694

9795
export const coverageOutput = (
9896
cov: CodeCoverage
99-
): Pick<CodeCoverage, 'name' | 'numLocations'> & { lineNotCovered: string } => {
100-
const numLocationsNum = parseInt(cov.numLocations, 10);
101-
const numLocationsNotCovered: number = parseInt(cov.numLocationsNotCovered, 10);
102-
const color = numLocationsNotCovered > 0 ? StandardColors.error : StandardColors.success;
97+
): Pick<CodeCoverage, 'name'> & { coveragePercent: string; linesNotCovered: string } => ({
98+
name: cov.name,
99+
coveragePercent: formatPercent(getCoveragePct(cov)),
100+
linesNotCovered: cov.locationsNotCovered
101+
? ensureArray(cov.locationsNotCovered)
102+
.map((location) => location.line)
103+
.join(',')
104+
: '',
105+
});
103106

104-
let pctCovered = 100;
105-
const coverageDecimal: number = parseFloat(((numLocationsNum - numLocationsNotCovered) / numLocationsNum).toFixed(2));
106-
if (numLocationsNum > 0) {
107-
pctCovered = coverageDecimal * 100;
108-
}
109-
// cov.numLocations = color(`${pctCovered}%`);
110-
const base = {
111-
name: cov.name,
112-
numLocations: color(`${pctCovered}%`),
113-
};
107+
const color = (percent: number): Chalk =>
108+
percent >= 90 ? StandardColors.success : percent >= 75 ? StandardColors.warning : StandardColors.error;
114109

115-
if (!cov.locationsNotCovered) {
116-
return { ...base, lineNotCovered: '' };
117-
}
118-
const locations = ensureArray(cov.locationsNotCovered);
110+
const formatPercent = (percent: number): string => color(percent)(`${percent}%`);
119111

120-
return {
121-
...base,
122-
lineNotCovered: locations.map((location) => location.line).join(','),
123-
};
112+
export const getCoveragePct = (cov: CodeCoverage): number => {
113+
const [lineCount, uncoveredLineCount] = getCoverageNumbers(cov);
114+
const coverageDecimal = parseFloat(((lineCount - uncoveredLineCount) / lineCount).toFixed(2));
115+
116+
return lineCount > 0 ? coverageDecimal * 100 : 100;
124117
};
118+
119+
/** returns the number of total line for which coverage should apply, and the total uncovered line */
120+
export const getCoverageNumbers = (cov: CodeCoverage): [lineCount: number, uncoveredLineCount: number] => [
121+
parseInt(cov.numLocations, 10),
122+
parseInt(cov.numLocationsNotCovered, 10),
123+
];

0 commit comments

Comments
 (0)