Skip to content

Commit

Permalink
feat: color nuances for test coverage % (#756)
Browse files Browse the repository at this point in the history
  • Loading branch information
mshanemc authored Sep 19, 2023
1 parent f74f3e7 commit b7af57a
Show file tree
Hide file tree
Showing 6 changed files with 432 additions and 105 deletions.
4 changes: 2 additions & 2 deletions src/commands/project/reset/tracking.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@
*/

import { Messages } from '@salesforce/core';
import * as chalk from 'chalk';
import { SourceTracking } from '@salesforce/source-tracking';
import {
Flags,
loglevel,
orgApiVersionFlagWithDeprecations,
requiredOrgFlagWithDeprecations,
SfCommand,
StandardColors,
} from '@salesforce/sf-plugins-core';

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

if (flags['no-prompt'] || (await this.confirm(chalk.dim(messages.getMessage('promptMessage'))))) {
if (flags['no-prompt'] || (await this.confirm(StandardColors.info(messages.getMessage('promptMessage'))))) {
const sourceTracking = await SourceTracking.create({
project: this.project,
org: flags['target-org'],
Expand Down
5 changes: 2 additions & 3 deletions src/formatters/deleteResultFormatter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,9 @@
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/
import { ux } from '@oclif/core';
import * as chalk from 'chalk';
import { DeployResult, FileResponse, RequestStatus } from '@salesforce/source-deploy-retrieve';
import { ensureArray } from '@salesforce/kit';
import { bold } from 'chalk';
import { bold, blue } from 'chalk';
import { StandardColors } from '@salesforce/sf-plugins-core';
import { DeleteSourceJson, Formatter, TestLevel } from '../utils/types';
import { sortFileResponses, asRelativePaths } from '../utils/output';
Expand Down Expand Up @@ -76,7 +75,7 @@ export class DeleteResultFormatter extends TestResultsFormatter implements Forma
}

ux.log('');
ux.styledHeader(chalk.blue('Deleted Source'));
ux.styledHeader(blue('Deleted Source'));
ux.table(
successes.map((entry) => ({
fullName: entry.fullName,
Expand Down
29 changes: 13 additions & 16 deletions src/formatters/deployResultFormatter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { DeployResultJson, isSdrFailure, isSdrSuccess, TestLevel, Verbosity, For
import {
generateCoveredLines,
getCoverageFormattersOptions,
getCoverageNumbers,
mapTestResults,
transformCoverageToApexCoverage,
} from '../utils/coverage';
Expand Down Expand Up @@ -63,20 +64,17 @@ export class DeployResultFormatter extends TestResultsFormatter implements Forma
(!this.result.response?.numberTestsTotal && !this.flags['test-level']) ||
this.flags['test-level'] === 'NoTestRun'
) {
let testsWarn = '';

if (this.coverageOptions.reportFormats?.length) {
testsWarn += `\`--coverage-formatters\` was specified but no tests ran.${EOL}`;
}
if (this.junit) {
testsWarn += `\`--junit\` was specified but no tests ran.${EOL}`;
}
const testsWarn = (
this.coverageOptions.reportFormats?.length ? ['`--coverage-formatters` was specified but no tests ran.'] : []
)
.concat(this.junit ? ['`--junit` was specified but no tests ran.'] : [])
.concat([
'You can ensure tests run by specifying `--test-level` and setting it to `RunSpecifiedTests`, `RunLocalTests` or `RunAllTestsInOrg`.',
]);

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

Expand Down Expand Up @@ -185,10 +183,9 @@ export class DeployResultFormatter extends TestResultsFormatter implements Forma
...mapTestResults(ensureArray(runTestResult.failures)),
],
codecoverage: ensureArray(runTestResult?.codeCoverage).map((cov): CodeCoverageResult => {
const numLinesUncovered = parseInt(cov.numLocationsNotCovered, 10);
const [uncoveredLines, coveredLines] = generateCoveredLines(cov);
const numLocationsNum = parseInt(cov.numLocations, 10);
const numLocationsNotCovered: number = parseInt(cov.numLocationsNotCovered, 10);
const [numLocationsNum, numLinesUncovered] = getCoverageNumbers(cov);

return {
// TODO: fix this type in SDR?
type: cov.type as 'ApexClass' | 'ApexTrigger',
Expand All @@ -200,7 +197,7 @@ export class DeployResultFormatter extends TestResultsFormatter implements Forma
uncoveredLines,
percentage:
numLocationsNum > 0
? (((numLocationsNum - numLocationsNotCovered) / numLocationsNum) * 100).toFixed() + '%'
? (((numLocationsNum - numLinesUncovered) / numLocationsNum) * 100).toFixed() + '%'
: '',
};
}),
Expand Down
108 changes: 57 additions & 51 deletions src/formatters/testResultsFormatter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,15 @@
import * as os from 'os';
import { ux } from '@oclif/core';
import { dim, underline } from 'chalk';
import { CodeCoverageWarnings, DeployResult, Failures, Successes } from '@salesforce/source-deploy-retrieve';
import {
CodeCoverage,
CodeCoverageWarnings,
DeployResult,
Failures,
MetadataApiDeployStatus,
RunTestResult,
Successes,
} from '@salesforce/source-deploy-retrieve';
import { ensureArray } from '@salesforce/kit';
import { TestLevel, Verbosity } from '../utils/types';
import { tableHeader, error, success, check } from '../utils/output';
Expand All @@ -34,11 +42,11 @@ export class TestResultsFormatter {
return;
}

this.displayVerboseTestFailures();
displayVerboseTestFailures(this.result.response);

if (this.verbosity === 'verbose') {
this.displayVerboseTestSuccesses();
this.displayVerboseTestCoverage();
displayVerboseTestSuccesses(this.result.response.details.runTestResult?.successes);
displayVerboseTestCoverage(this.result.response.details.runTestResult?.codeCoverage);
}

ux.log();
Expand All @@ -59,60 +67,58 @@ export class TestResultsFormatter {
if (this.flags.verbose) return 'verbose';
return 'normal';
}
}

private displayVerboseTestCoverage(): void {
const codeCoverage = ensureArray(this.result.response.details.runTestResult?.codeCoverage);
if (codeCoverage.length) {
const coverage = codeCoverage.sort((a, b) => (a.name.toUpperCase() > b.name.toUpperCase() ? 1 : -1));
ux.log();
ux.log(tableHeader('Apex Code Coverage'));

ux.table(coverage.map(coverageOutput), {
name: { header: 'Name' },
numLocations: { header: '% Covered' },
lineNotCovered: { header: 'Uncovered Lines' },
});
const displayVerboseTestSuccesses = (resultSuccesses: RunTestResult['successes']): void => {
const successes = ensureArray(resultSuccesses).sort(testResultSort);
if (successes.length > 0) {
ux.log();
ux.log(success(`Test Success [${successes.length}]`));
for (const test of successes) {
const testName = underline(`${test.name}.${test.methodName}`);
ux.log(`${check} ${testName}`);
}
}
};

private displayVerboseTestSuccesses(): void {
const successes = ensureArray(this.result.response.details.runTestResult?.successes);
if (successes.length > 0) {
const testSuccesses = sortTestResults(successes);
ux.log();
ux.log(success(`Test Success [${successes.length}]`));
for (const test of testSuccesses) {
const testName = underline(`${test.name}.${test.methodName}`);
ux.log(`${check} ${testName}`);
}
/** display the Test failures if there are any testErrors in the mdapi deploy response */
const displayVerboseTestFailures = (response: MetadataApiDeployStatus): void => {
if (!response.numberTestErrors) return;
const failures = ensureArray(response.details.runTestResult?.failures).sort(testResultSort);
const failureCount = response.details.runTestResult?.numFailures;
ux.log();
ux.log(error(`Test Failures [${failureCount}]`));
for (const test of failures) {
const testName = underline(`${test.name}.${test.methodName}`);
ux.log(`• ${testName}`);
ux.log(` ${dim('message')}: ${test.message}`);
if (test.stackTrace) {
const stackTrace = test.stackTrace.replace(/\n/g, `${os.EOL} `);
ux.log(` ${dim('stacktrace')}: ${os.EOL} ${stackTrace}`);
}
ux.log();
}
};

private displayVerboseTestFailures(): void {
if (!this.result.response.numberTestErrors) return;
const failures = ensureArray(this.result.response.details.runTestResult?.failures);
const failureCount = this.result.response.details.runTestResult?.numFailures;
const testFailures = sortTestResults(failures);
/**
* Display the table if there is at least one coverage item in the result
*/
const displayVerboseTestCoverage = (coverage?: CodeCoverage | CodeCoverage[]): void => {
const codeCoverage = ensureArray(coverage);
if (codeCoverage.length) {
ux.log();
ux.log(error(`Test Failures [${failureCount}]`));
for (const test of testFailures) {
const testName = underline(`${test.name}.${test.methodName}`);
ux.log(`• ${testName}`);
ux.log(` ${dim('message')}: ${test.message}`);
if (test.stackTrace) {
const stackTrace = test.stackTrace.replace(/\n/g, `${os.EOL} `);
ux.log(` ${dim('stacktrace')}: ${os.EOL} ${stackTrace}`);
}
ux.log();
}
ux.log(tableHeader('Apex Code Coverage'));

ux.table(codeCoverage.sort(coverageSort).map(coverageOutput), {
name: { header: 'Name' },
coveragePercent: { header: '% Covered' },
linesNotCovered: { header: 'Uncovered Lines' },
});
}
}
};

function sortTestResults<T extends Failures | Successes>(results: T[]): T[] {
return results.sort((a, b) => {
if (a.methodName === b.methodName) {
return a.name.localeCompare(b.name);
}
return a.methodName.localeCompare(b.methodName);
});
}
const testResultSort = <T extends Successes | Failures>(a: T, b: T): number =>
a.methodName === b.methodName ? a.name.localeCompare(b.name) : a.methodName.localeCompare(b.methodName);

const coverageSort = (a: CodeCoverage, b: CodeCoverage): number =>
a.name.toUpperCase() > b.name.toUpperCase() ? 1 : -1;
63 changes: 31 additions & 32 deletions src/utils/coverage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
import { Successes, Failures, CodeCoverage } from '@salesforce/source-deploy-retrieve';
import { ensureArray } from '@salesforce/kit';
import { StandardColors } from '@salesforce/sf-plugins-core';
import { Chalk } from 'chalk';

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

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

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

const ac: ApexCodeCoverageAggregateRecord = {
return {
ApexClassOrTrigger: {
Id: cov.id,
Name: cov.name,
},
NumLinesCovered: numCovered,
NumLinesUncovered: numUncovered,
NumLinesCovered,
NumLinesUncovered,
Coverage: {
coveredLines,
uncoveredLines,
},
};
return ac;
});
return { done: true, totalSize: apexCoverage.length, records: apexCoverage };
};

export const coverageOutput = (
cov: CodeCoverage
): Pick<CodeCoverage, 'name' | 'numLocations'> & { lineNotCovered: string } => {
const numLocationsNum = parseInt(cov.numLocations, 10);
const numLocationsNotCovered: number = parseInt(cov.numLocationsNotCovered, 10);
const color = numLocationsNotCovered > 0 ? StandardColors.error : StandardColors.success;
): Pick<CodeCoverage, 'name'> & { coveragePercent: string; linesNotCovered: string } => ({
name: cov.name,
coveragePercent: formatPercent(getCoveragePct(cov)),
linesNotCovered: cov.locationsNotCovered
? ensureArray(cov.locationsNotCovered)
.map((location) => location.line)
.join(',')
: '',
});

let pctCovered = 100;
const coverageDecimal: number = parseFloat(((numLocationsNum - numLocationsNotCovered) / numLocationsNum).toFixed(2));
if (numLocationsNum > 0) {
pctCovered = coverageDecimal * 100;
}
// cov.numLocations = color(`${pctCovered}%`);
const base = {
name: cov.name,
numLocations: color(`${pctCovered}%`),
};
const color = (percent: number): Chalk =>
percent >= 90 ? StandardColors.success : percent >= 75 ? StandardColors.warning : StandardColors.error;

if (!cov.locationsNotCovered) {
return { ...base, lineNotCovered: '' };
}
const locations = ensureArray(cov.locationsNotCovered);
const formatPercent = (percent: number): string => color(percent)(`${percent}%`);

return {
...base,
lineNotCovered: locations.map((location) => location.line).join(','),
};
export const getCoveragePct = (cov: CodeCoverage): number => {
const [lineCount, uncoveredLineCount] = getCoverageNumbers(cov);
const coverageDecimal = parseFloat(((lineCount - uncoveredLineCount) / lineCount).toFixed(2));

return lineCount > 0 ? coverageDecimal * 100 : 100;
};

/** returns the number of total line for which coverage should apply, and the total uncovered line */
export const getCoverageNumbers = (cov: CodeCoverage): [lineCount: number, uncoveredLineCount: number] => [
parseInt(cov.numLocations, 10),
parseInt(cov.numLocationsNotCovered, 10),
];
Loading

0 comments on commit b7af57a

Please sign in to comment.