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

Sm/test coverage colors #756

Merged
merged 7 commits into from
Sep 19, 2023
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
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
Loading