Skip to content

Commit e05b983

Browse files
feat: render test failure counter (#1215)
* 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]>
1 parent 03bcb6a commit e05b983

File tree

5 files changed

+152
-40
lines changed

5 files changed

+152
-40
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
"bugs": "https://github.com/forcedotcom/cli/issues",
77
"dependencies": {
88
"@oclif/core": "^4.0.37",
9-
"@oclif/multi-stage-output": "^0.7.15",
9+
"@oclif/multi-stage-output": "^0.8.0",
1010
"@salesforce/apex-node": "^8.1.18",
1111
"@salesforce/core": "^8.6.4",
1212
"@salesforce/kit": "^3.2.3",

src/formatters/testResultsFormatter.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { ensureArray } from '@salesforce/kit';
2121
import { TestLevel, Verbosity } from '../utils/types.js';
2222
import { tableHeader, error, success, check } from '../utils/output.js';
2323
import { coverageOutput } from '../utils/coverage.js';
24+
import { isCI } from '../utils/deployStages.js';
2425

2526
const ux = new Ux();
2627

@@ -45,10 +46,14 @@ export class TestResultsFormatter {
4546
return;
4647
}
4748

48-
displayVerboseTestFailures(this.result.response);
49+
if (!isCI()) {
50+
displayVerboseTestFailures(this.result.response);
51+
}
4952

5053
if (this.verbosity === 'verbose') {
51-
displayVerboseTestSuccesses(this.result.response.details.runTestResult?.successes);
54+
if (!isCI()) {
55+
displayVerboseTestSuccesses(this.result.response.details.runTestResult?.successes);
56+
}
5257
displayVerboseTestCoverage(this.result.response.details.runTestResult?.codeCoverage);
5358
}
5459

@@ -122,7 +127,7 @@ const displayVerboseTestCoverage = (coverage?: CodeCoverage | CodeCoverage[]): v
122127
}
123128
};
124129

125-
const testResultSort = <T extends Successes | Failures>(a: T, b: T): number =>
130+
export const testResultSort = <T extends Successes | Failures>(a: T, b: T): number =>
126131
a.methodName === b.methodName ? a.name.localeCompare(b.name) : a.methodName.localeCompare(b.methodName);
127132

128133
const coverageSort = (a: CodeCoverage, b: CodeCoverage): number =>

src/utils/deployStages.ts

Lines changed: 85 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,21 @@
44
* Licensed under the BSD 3-Clause license.
55
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
66
*/
7+
import os from 'node:os';
78
import { MultiStageOutput } from '@oclif/multi-stage-output';
89
import { Lifecycle, Messages } from '@salesforce/core';
9-
import { MetadataApiDeploy, MetadataApiDeployStatus, RequestStatus } from '@salesforce/source-deploy-retrieve';
10+
import {
11+
Failures,
12+
MetadataApiDeploy,
13+
MetadataApiDeployStatus,
14+
RequestStatus,
15+
} from '@salesforce/source-deploy-retrieve';
1016
import { SourceMemberPollingEvent } from '@salesforce/source-tracking';
1117
import terminalLink from 'terminal-link';
18+
import ansis from 'ansis';
19+
import { testResultSort } from '../formatters/testResultsFormatter.js';
1220
import { getZipFileSize } from './output.js';
21+
import { isTruthy } from './types.js';
1322

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

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

5165
public constructor({ title, jsonEnabled }: Options) {
66+
this.printedApexTestFailures = new Set();
5267
this.mso = new MultiStageOutput<Data>({
5368
title,
5469
stages: [
@@ -129,14 +144,51 @@ export class DeployStages {
129144
type: 'dynamic-key-value',
130145
},
131146
{
132-
label: 'Tests',
147+
label: 'Successful',
133148
get: (data): string | undefined =>
134149
data?.mdapiDeploy?.numberTestsTotal && data?.mdapiDeploy?.numberTestsCompleted
135150
? formatProgress(data?.mdapiDeploy?.numberTestsCompleted, data?.mdapiDeploy?.numberTestsTotal)
136151
: undefined,
137152
stage: 'Running Tests',
138153
type: 'dynamic-key-value',
139154
},
155+
{
156+
label: 'Failed',
157+
alwaysPrintInCI: true,
158+
get: (data): string | undefined => {
159+
let testFailures: Failures[] = [];
160+
161+
// only render new test failures
162+
if (isCI() && Array.isArray(data?.mdapiDeploy.details.runTestResult?.failures)) {
163+
// skip failure counter/progress info if there's no new failures to render.
164+
if (
165+
this.printedApexTestFailures.size > 0 &&
166+
data.mdapiDeploy.numberTestErrors === this.printedApexTestFailures.size
167+
) {
168+
return undefined;
169+
}
170+
171+
testFailures = data.mdapiDeploy.details.runTestResult?.failures.filter(
172+
(f) => !this.printedApexTestFailures.has(`${f.name}.${f.methodName}`)
173+
);
174+
175+
data?.mdapiDeploy.details.runTestResult?.failures.forEach((f) =>
176+
this.printedApexTestFailures.add(`${f.name}.${f.methodName}`)
177+
);
178+
179+
return data?.mdapiDeploy?.numberTestsTotal && data?.mdapiDeploy?.numberTestErrors
180+
? formatProgress(data?.mdapiDeploy?.numberTestErrors, data?.mdapiDeploy?.numberTestsTotal) +
181+
(isCI() ? os.EOL + formatTestFailures(testFailures) : '')
182+
: undefined;
183+
}
184+
185+
return data?.mdapiDeploy?.numberTestsTotal && data?.mdapiDeploy?.numberTestErrors
186+
? formatProgress(data?.mdapiDeploy?.numberTestErrors, data?.mdapiDeploy?.numberTestsTotal)
187+
: undefined;
188+
},
189+
stage: 'Running Tests',
190+
type: 'dynamic-key-value',
191+
},
140192
{
141193
label: 'Members',
142194
get: (data): string | undefined =>
@@ -232,3 +284,34 @@ export class DeployStages {
232284
this.mso.skipTo('Done', data);
233285
}
234286
}
287+
288+
function formatTestFailures(failuresData: Failures[]): string {
289+
const failures = failuresData.sort(testResultSort);
290+
291+
let output = '';
292+
293+
for (const test of failures) {
294+
const testName = ansis.underline(`${test.name}.${test.methodName}`);
295+
output += ` • ${testName}${os.EOL}`;
296+
output += ` message: ${test.message}${os.EOL}`;
297+
if (test.stackTrace) {
298+
const stackTrace = test.stackTrace.replace(/\n/g, `${os.EOL} `);
299+
output += ` stacktrace:${os.EOL} ${stackTrace}${os.EOL}${os.EOL}`;
300+
}
301+
}
302+
303+
// remove last EOL char
304+
return output.slice(0, -1);
305+
}
306+
307+
export function isCI(): boolean {
308+
if (
309+
isTruthy(process.env.CI) &&
310+
('CI' in process.env ||
311+
'CONTINUOUS_INTEGRATION' in process.env ||
312+
Object.keys(process.env).some((key) => key.startsWith('CI_')))
313+
)
314+
return true;
315+
316+
return false;
317+
}

src/utils/types.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,3 +124,7 @@ export const isFileResponseDeleted = (fileResponse: FileResponseSuccess): boolea
124124
fileResponse.state === ComponentStatus.Deleted;
125125

126126
export const isDefined = <T>(value?: T): value is T => value !== undefined;
127+
128+
export function isTruthy(value: string | undefined): boolean {
129+
return value !== '0' && value !== 'false';
130+
}

yarn.lock

Lines changed: 54 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1396,16 +1396,16 @@
13961396
wordwrap "^1.0.0"
13971397
wrap-ansi "^7.0.0"
13981398

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

@@ -4072,6 +4072,11 @@ es-to-primitive@^1.2.1:
40724072
is-date-object "^1.0.1"
40734073
is-symbol "^1.0.2"
40744074

4075+
es-toolkit@^1.22.0:
4076+
version "1.30.1"
4077+
resolved "https://registry.yarnpkg.com/es-toolkit/-/es-toolkit-1.30.1.tgz#311be8eec88f53b0b1a9d40117f3f3c1e763e274"
4078+
integrity sha512-ZXflqanzH8BpHkDhFa10bBf6ONDCe84EPUm7SSICGzuuROSluT2ynTPtwn9PcRelMtorCRozSknI/U0MNYp0Uw==
4079+
40754080
es6-error@^4.0.1:
40764081
version "4.1.1"
40774082
resolved "https://registry.yarnpkg.com/es6-error/-/es6-error-4.1.1.tgz#9e3af407459deed47e9a91f9b885a84eb05c561d"
@@ -5239,6 +5244,36 @@ ink@^5.0.1:
52395244
ws "^8.15.0"
52405245
yoga-wasm-web "~0.3.3"
52415246

5247+
ink@^5.1.0:
5248+
version "5.1.0"
5249+
resolved "https://registry.yarnpkg.com/ink/-/ink-5.1.0.tgz#8ed050bf7a468489f231c99031f8bb1393c44079"
5250+
integrity sha512-3vIO+CU4uSg167/dZrg4wHy75llUINYXxN4OsdaCkE40q4zyOTPwNc2VEpLnnWsIvIQeo6x6lilAhuaSt+rIsA==
5251+
dependencies:
5252+
"@alcalzone/ansi-tokenize" "^0.1.3"
5253+
ansi-escapes "^7.0.0"
5254+
ansi-styles "^6.2.1"
5255+
auto-bind "^5.0.1"
5256+
chalk "^5.3.0"
5257+
cli-boxes "^3.0.0"
5258+
cli-cursor "^4.0.0"
5259+
cli-truncate "^4.0.0"
5260+
code-excerpt "^4.0.0"
5261+
es-toolkit "^1.22.0"
5262+
indent-string "^5.0.0"
5263+
is-in-ci "^1.0.0"
5264+
patch-console "^2.0.0"
5265+
react-reconciler "^0.29.0"
5266+
scheduler "^0.23.0"
5267+
signal-exit "^3.0.7"
5268+
slice-ansi "^7.1.0"
5269+
stack-utils "^2.0.6"
5270+
string-width "^7.2.0"
5271+
type-fest "^4.27.0"
5272+
widest-line "^5.0.0"
5273+
wrap-ansi "^9.0.0"
5274+
ws "^8.18.0"
5275+
yoga-wasm-web "~0.3.3"
5276+
52425277
internal-slot@^1.0.7:
52435278
version "1.0.7"
52445279
resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.7.tgz#c06dcca3ed874249881007b0a5523b172a190802"
@@ -5373,6 +5408,11 @@ is-in-ci@^0.1.0:
53735408
resolved "https://registry.yarnpkg.com/is-in-ci/-/is-in-ci-0.1.0.tgz#5e07d6a02ec3a8292d3f590973357efa3fceb0d3"
53745409
integrity sha512-d9PXLEY0v1iJ64xLiQMJ51J128EYHAaOR4yZqQi8aHGfw6KgifM3/Viw1oZZ1GCVmb3gBuyhLyHj0HgR2DhSXQ==
53755410

5411+
is-in-ci@^1.0.0:
5412+
version "1.0.0"
5413+
resolved "https://registry.yarnpkg.com/is-in-ci/-/is-in-ci-1.0.0.tgz#9a86bbda7e42c6129902e0574c54b018fbb6ab88"
5414+
integrity sha512-eUuAjybVTHMYWm/U+vBO1sY/JOCgoPCXRxzdju0K+K0BiGW0SChEL1MLC0PoCIR1OlPo5YAp8HuQoUlsWEICwg==
5415+
53765416
is-inside-container@^1.0.0:
53775417
version "1.0.0"
53785418
resolved "https://registry.yarnpkg.com/is-inside-container/-/is-inside-container-1.0.0.tgz#e81fba699662eb31dbdaf26766a61d4814717ea4"
@@ -7786,16 +7826,7 @@ [email protected]:
77867826
dependencies:
77877827
escodegen "^1.8.1"
77887828

7789-
"string-width-cjs@npm:string-width@^4.2.0":
7790-
version "4.2.3"
7791-
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
7792-
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
7793-
dependencies:
7794-
emoji-regex "^8.0.0"
7795-
is-fullwidth-code-point "^3.0.0"
7796-
strip-ansi "^6.0.1"
7797-
7798-
string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
7829+
"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:
77997830
version "4.2.3"
78007831
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
78017832
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
@@ -7864,14 +7895,7 @@ string_decoder@~1.1.1:
78647895
dependencies:
78657896
safe-buffer "~5.1.0"
78667897

7867-
"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
7868-
version "6.0.1"
7869-
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
7870-
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
7871-
dependencies:
7872-
ansi-regex "^5.0.1"
7873-
7874-
[email protected], strip-ansi@^6.0.0, strip-ansi@^6.0.1:
7898+
"strip-ansi-cjs@npm:strip-ansi@^6.0.1", [email protected], strip-ansi@^6.0.0, strip-ansi@^6.0.1:
78757899
version "6.0.1"
78767900
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
78777901
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
@@ -8164,6 +8188,11 @@ type-fest@^1.0.2:
81648188
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-1.4.0.tgz#e9fb813fe3bf1744ec359d55d1affefa76f14be1"
81658189
integrity sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==
81668190

8191+
type-fest@^4.27.0:
8192+
version "4.30.1"
8193+
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-4.30.1.tgz#120b9e15177310ec4e9d5d6f187d86c0f4b55e0e"
8194+
integrity sha512-ojFL7eDMX2NF0xMbDwPZJ8sb7ckqtlAi1GsmgsFXvErT9kFTk1r0DuQKvrCh73M6D4nngeHJmvogF9OluXs7Hw==
8195+
81678196
type-fest@^4.8.3:
81688197
version "4.26.1"
81698198
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-4.26.1.tgz#a4a17fa314f976dd3e6d6675ef6c775c16d7955e"
@@ -8472,7 +8501,7 @@ workerpool@^6.5.1:
84728501
resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.5.1.tgz#060f73b39d0caf97c6db64da004cd01b4c099544"
84738502
integrity sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA==
84748503

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

8493-
wrap-ansi@^7.0.0:
8494-
version "7.0.0"
8495-
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
8496-
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
8497-
dependencies:
8498-
ansi-styles "^4.0.0"
8499-
string-width "^4.1.0"
8500-
strip-ansi "^6.0.0"
8501-
85028522
wrap-ansi@^8.1.0:
85038523
version "8.1.0"
85048524
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"
@@ -8532,7 +8552,7 @@ write-file-atomic@^3.0.0:
85328552
signal-exit "^3.0.2"
85338553
typedarray-to-buffer "^3.1.5"
85348554

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

0 commit comments

Comments
 (0)