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: render test failure counter #1215

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
11 changes: 8 additions & 3 deletions src/formatters/testResultsFormatter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { ensureArray } from '@salesforce/kit';
import { TestLevel, Verbosity } from '../utils/types.js';
import { tableHeader, error, success, check } from '../utils/output.js';
import { coverageOutput } from '../utils/coverage.js';
import { isCI } from '../utils/deployStages.js';

const ux = new Ux();

Expand All @@ -45,10 +46,14 @@ export class TestResultsFormatter {
return;
}

displayVerboseTestFailures(this.result.response);
if (!isCI()) {
displayVerboseTestFailures(this.result.response);
}

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

Expand Down Expand Up @@ -122,7 +127,7 @@ const displayVerboseTestCoverage = (coverage?: CodeCoverage | CodeCoverage[]): v
}
};

const testResultSort = <T extends Successes | Failures>(a: T, b: T): number =>
export 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 =>
Expand Down
87 changes: 85 additions & 2 deletions src/utils/deployStages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,21 @@
* 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 os from 'node:os';
import { MultiStageOutput } from '@oclif/multi-stage-output';
import { Lifecycle, Messages } from '@salesforce/core';
import { MetadataApiDeploy, MetadataApiDeployStatus, RequestStatus } from '@salesforce/source-deploy-retrieve';
import {
Failures,
MetadataApiDeploy,
MetadataApiDeployStatus,
RequestStatus,
} from '@salesforce/source-deploy-retrieve';
import { SourceMemberPollingEvent } from '@salesforce/source-tracking';
import terminalLink from 'terminal-link';
import ansis from 'ansis';
import { testResultSort } from '../formatters/testResultsFormatter.js';
import { getZipFileSize } from './output.js';
import { isTruthy } from './types.js';

Messages.importMessagesDirectoryFromMetaUrl(import.meta.url);
const mdTransferMessages = Messages.loadMessages('@salesforce/plugin-deploy-retrieve', 'metadata.transfer');
Expand Down Expand Up @@ -47,8 +56,14 @@ function formatProgress(current: number, total: number): string {

export class DeployStages {
private mso: MultiStageOutput<Data>;
/**
* Set of Apex test failures that were already rendered in the `Running Tests` block.
* This is used in the `Failed` stage block for CI output to ensure test failures aren't duplicated when rendering new failures on polling.
*/
private printedApexTestFailures: Set<string>;

public constructor({ title, jsonEnabled }: Options) {
this.printedApexTestFailures = new Set();
this.mso = new MultiStageOutput<Data>({
title,
stages: [
Expand Down Expand Up @@ -129,14 +144,51 @@ export class DeployStages {
type: 'dynamic-key-value',
},
{
label: 'Tests',
label: 'Successful',
get: (data): string | undefined =>
data?.mdapiDeploy?.numberTestsTotal && data?.mdapiDeploy?.numberTestsCompleted
? formatProgress(data?.mdapiDeploy?.numberTestsCompleted, data?.mdapiDeploy?.numberTestsTotal)
: undefined,
stage: 'Running Tests',
type: 'dynamic-key-value',
},
{
label: 'Failed',
alwaysPrintInCI: true,
get: (data): string | undefined => {
let testFailures: Failures[] = [];

// only render new test failures
if (isCI() && Array.isArray(data?.mdapiDeploy.details.runTestResult?.failures)) {
// skip failure counter/progress info if there's no new failures to render.
if (
this.printedApexTestFailures.size > 0 &&
data.mdapiDeploy.numberTestErrors === this.printedApexTestFailures.size
) {
return undefined;
}

testFailures = data.mdapiDeploy.details.runTestResult?.failures.filter(
(f) => !this.printedApexTestFailures.has(`${f.name}.${f.methodName}`)
);

data?.mdapiDeploy.details.runTestResult?.failures.forEach((f) =>
this.printedApexTestFailures.add(`${f.name}.${f.methodName}`)
);

return data?.mdapiDeploy?.numberTestsTotal && data?.mdapiDeploy?.numberTestErrors
? formatProgress(data?.mdapiDeploy?.numberTestErrors, data?.mdapiDeploy?.numberTestsTotal) +
(isCI() ? os.EOL + formatTestFailures(testFailures) : '')
: undefined;
}

return data?.mdapiDeploy?.numberTestsTotal && data?.mdapiDeploy?.numberTestErrors
? formatProgress(data?.mdapiDeploy?.numberTestErrors, data?.mdapiDeploy?.numberTestsTotal)
: undefined;
},
stage: 'Running Tests',
type: 'dynamic-key-value',
},
{
label: 'Members',
get: (data): string | undefined =>
Expand Down Expand Up @@ -232,3 +284,34 @@ export class DeployStages {
this.mso.skipTo('Done', data);
}
}

function formatTestFailures(failuresData: Failures[]): string {
const failures = failuresData.sort(testResultSort);

let output = '';

for (const test of failures) {
const testName = ansis.underline(`${test.name}.${test.methodName}`);
output += ` • ${testName}${os.EOL}`;
output += ` message: ${test.message}${os.EOL}`;
if (test.stackTrace) {
const stackTrace = test.stackTrace.replace(/\n/g, `${os.EOL} `);
output += ` stacktrace:${os.EOL} ${stackTrace}${os.EOL}${os.EOL}`;
}
}

// remove last EOL char
return output.slice(0, -1);
}

export function isCI(): boolean {
if (
isTruthy(process.env.CI) &&
('CI' in process.env ||
'CONTINUOUS_INTEGRATION' in process.env ||
Object.keys(process.env).some((key) => key.startsWith('CI_')))
)
return true;

return false;
}
4 changes: 4 additions & 0 deletions src/utils/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,3 +124,7 @@ export const isFileResponseDeleted = (fileResponse: FileResponseSuccess): boolea
fileResponse.state === ComponentStatus.Deleted;

export const isDefined = <T>(value?: T): value is T => value !== undefined;

export function isTruthy(value: string | undefined): boolean {
return value !== '0' && value !== 'false';
}
Loading