Skip to content

Commit

Permalink
feat: render test failure counter (#1215)
Browse files Browse the repository at this point in the history
* feat: render test failure counter

* feat: show test failures while polling for status

* fix: improve CI output, handle verbose output

* chore: dedup failures on mso

* chore: refactor + skip dups

* chore: remove success on CI output for deploy

* fix: use `alwaysPrintInCI` for test failures block

* chore: bump mso

---------

Co-authored-by: Mike Donnalley <[email protected]>
  • Loading branch information
cristiand391 and mdonnalley authored Dec 16, 2024
1 parent 03bcb6a commit e05b983
Show file tree
Hide file tree
Showing 5 changed files with 152 additions and 40 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"bugs": "https://github.com/forcedotcom/cli/issues",
"dependencies": {
"@oclif/core": "^4.0.37",
"@oclif/multi-stage-output": "^0.7.15",
"@oclif/multi-stage-output": "^0.8.0",
"@salesforce/apex-node": "^8.1.18",
"@salesforce/core": "^8.6.4",
"@salesforce/kit": "^3.2.3",
Expand Down
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';
}
88 changes: 54 additions & 34 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1396,16 +1396,16 @@
wordwrap "^1.0.0"
wrap-ansi "^7.0.0"

"@oclif/multi-stage-output@^0.7.15":
version "0.7.15"
resolved "https://registry.yarnpkg.com/@oclif/multi-stage-output/-/multi-stage-output-0.7.15.tgz#fe568e4db01d8d406bb735a120f1a6b4bb31213e"
integrity sha512-6qDhWbbUbdSFTEtwzWvVcEKvWY5E+ioECqS0UPOmwt2Nx1TdW1HZkHAmJ6P+4FheJhAJseFaD/hV6gdaYVwsaA==
"@oclif/multi-stage-output@^0.8.0":
version "0.8.0"
resolved "https://registry.yarnpkg.com/@oclif/multi-stage-output/-/multi-stage-output-0.8.0.tgz#868fcce009981afbb614b71246b80cd02c483782"
integrity sha512-B858dgCQPZWHRnzcU42t/cHq3u858UWMQk635qjn/lNlUpFDZKpoVgjVqLlGkQ9iEnqpwTXtyOXSYrLnPjVUeg==
dependencies:
"@oclif/core" "^4"
"@types/react" "^18.3.12"
cli-spinners "^2"
figures "^6.1.0"
ink "^5.0.1"
ink "^5.1.0"
react "^18.3.1"
wrap-ansi "^9.0.0"

Expand Down Expand Up @@ -4072,6 +4072,11 @@ es-to-primitive@^1.2.1:
is-date-object "^1.0.1"
is-symbol "^1.0.2"

es-toolkit@^1.22.0:
version "1.30.1"
resolved "https://registry.yarnpkg.com/es-toolkit/-/es-toolkit-1.30.1.tgz#311be8eec88f53b0b1a9d40117f3f3c1e763e274"
integrity sha512-ZXflqanzH8BpHkDhFa10bBf6ONDCe84EPUm7SSICGzuuROSluT2ynTPtwn9PcRelMtorCRozSknI/U0MNYp0Uw==

es6-error@^4.0.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/es6-error/-/es6-error-4.1.1.tgz#9e3af407459deed47e9a91f9b885a84eb05c561d"
Expand Down Expand Up @@ -5239,6 +5244,36 @@ ink@^5.0.1:
ws "^8.15.0"
yoga-wasm-web "~0.3.3"

ink@^5.1.0:
version "5.1.0"
resolved "https://registry.yarnpkg.com/ink/-/ink-5.1.0.tgz#8ed050bf7a468489f231c99031f8bb1393c44079"
integrity sha512-3vIO+CU4uSg167/dZrg4wHy75llUINYXxN4OsdaCkE40q4zyOTPwNc2VEpLnnWsIvIQeo6x6lilAhuaSt+rIsA==
dependencies:
"@alcalzone/ansi-tokenize" "^0.1.3"
ansi-escapes "^7.0.0"
ansi-styles "^6.2.1"
auto-bind "^5.0.1"
chalk "^5.3.0"
cli-boxes "^3.0.0"
cli-cursor "^4.0.0"
cli-truncate "^4.0.0"
code-excerpt "^4.0.0"
es-toolkit "^1.22.0"
indent-string "^5.0.0"
is-in-ci "^1.0.0"
patch-console "^2.0.0"
react-reconciler "^0.29.0"
scheduler "^0.23.0"
signal-exit "^3.0.7"
slice-ansi "^7.1.0"
stack-utils "^2.0.6"
string-width "^7.2.0"
type-fest "^4.27.0"
widest-line "^5.0.0"
wrap-ansi "^9.0.0"
ws "^8.18.0"
yoga-wasm-web "~0.3.3"

internal-slot@^1.0.7:
version "1.0.7"
resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.7.tgz#c06dcca3ed874249881007b0a5523b172a190802"
Expand Down Expand Up @@ -5373,6 +5408,11 @@ is-in-ci@^0.1.0:
resolved "https://registry.yarnpkg.com/is-in-ci/-/is-in-ci-0.1.0.tgz#5e07d6a02ec3a8292d3f590973357efa3fceb0d3"
integrity sha512-d9PXLEY0v1iJ64xLiQMJ51J128EYHAaOR4yZqQi8aHGfw6KgifM3/Viw1oZZ1GCVmb3gBuyhLyHj0HgR2DhSXQ==

is-in-ci@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/is-in-ci/-/is-in-ci-1.0.0.tgz#9a86bbda7e42c6129902e0574c54b018fbb6ab88"
integrity sha512-eUuAjybVTHMYWm/U+vBO1sY/JOCgoPCXRxzdju0K+K0BiGW0SChEL1MLC0PoCIR1OlPo5YAp8HuQoUlsWEICwg==

is-inside-container@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/is-inside-container/-/is-inside-container-1.0.0.tgz#e81fba699662eb31dbdaf26766a61d4814717ea4"
Expand Down Expand Up @@ -7786,16 +7826,7 @@ [email protected]:
dependencies:
escodegen "^1.8.1"

"string-width-cjs@npm:string-width@^4.2.0":
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
dependencies:
emoji-regex "^8.0.0"
is-fullwidth-code-point "^3.0.0"
strip-ansi "^6.0.1"

string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
Expand Down Expand Up @@ -7864,14 +7895,7 @@ string_decoder@~1.1.1:
dependencies:
safe-buffer "~5.1.0"

"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
dependencies:
ansi-regex "^5.0.1"

[email protected], strip-ansi@^6.0.0, strip-ansi@^6.0.1:
"strip-ansi-cjs@npm:strip-ansi@^6.0.1", [email protected], strip-ansi@^6.0.0, strip-ansi@^6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
Expand Down Expand Up @@ -8164,6 +8188,11 @@ type-fest@^1.0.2:
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-1.4.0.tgz#e9fb813fe3bf1744ec359d55d1affefa76f14be1"
integrity sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==

type-fest@^4.27.0:
version "4.30.1"
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-4.30.1.tgz#120b9e15177310ec4e9d5d6f187d86c0f4b55e0e"
integrity sha512-ojFL7eDMX2NF0xMbDwPZJ8sb7ckqtlAi1GsmgsFXvErT9kFTk1r0DuQKvrCh73M6D4nngeHJmvogF9OluXs7Hw==

type-fest@^4.8.3:
version "4.26.1"
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-4.26.1.tgz#a4a17fa314f976dd3e6d6675ef6c775c16d7955e"
Expand Down Expand Up @@ -8472,7 +8501,7 @@ workerpool@^6.5.1:
resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.5.1.tgz#060f73b39d0caf97c6db64da004cd01b4c099544"
integrity sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA==

"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
Expand All @@ -8490,15 +8519,6 @@ wrap-ansi@^6.2.0:
string-width "^4.1.0"
strip-ansi "^6.0.0"

wrap-ansi@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
dependencies:
ansi-styles "^4.0.0"
string-width "^4.1.0"
strip-ansi "^6.0.0"

wrap-ansi@^8.1.0:
version "8.1.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"
Expand Down Expand Up @@ -8532,7 +8552,7 @@ write-file-atomic@^3.0.0:
signal-exit "^3.0.2"
typedarray-to-buffer "^3.1.5"

ws@^8.15.0:
ws@^8.15.0, ws@^8.18.0:
version "8.18.0"
resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.0.tgz#0d7505a6eafe2b0e712d232b42279f53bc289bbc"
integrity sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==
Expand Down

0 comments on commit e05b983

Please sign in to comment.