Skip to content

Commit 08fbbdd

Browse files
authored
fix: deploy errors are reported properly (#146)
* fix: deploy errors are reported properly * fix: add a comment for the clone
1 parent 0b82baf commit 08fbbdd

File tree

3 files changed

+144
-32
lines changed

3 files changed

+144
-32
lines changed

src/formatters/deployResultFormatter.ts

Lines changed: 10 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ export class DeployResultFormatter extends ResultFormatter {
5151
}
5252

5353
/**
54-
* Displays deploy results in human format. Output can vary based on:
54+
* Displays deploy results in human readable format. Output can vary based on:
5555
*
5656
* 1. Verbose option
5757
* 3. Checkonly deploy (checkonly=true)
@@ -85,19 +85,6 @@ export class DeployResultFormatter extends ResultFormatter {
8585
return getString(this.result, 'response.status') === status;
8686
}
8787

88-
// Returns true if the components returned in the server response
89-
// were mapped to local source in the ComponentSet.
90-
protected hasMappedComponents(): boolean {
91-
return getNumber(this.result, 'components.size', 0) > 0;
92-
}
93-
94-
// Returns true if the server response contained components.
95-
protected hasComponents(): boolean {
96-
const successes = getNumber(this.result, 'response.details.componentSuccesses.length', 0) > 0;
97-
const failures = getNumber(this.result, 'response.details.componentFailures.length', 0) > 0;
98-
return successes || failures;
99-
}
100-
10188
protected isRunTestsEnabled(): boolean {
10289
return getBoolean(this.result, 'response.runTestsEnabled', false);
10390
}
@@ -115,13 +102,17 @@ export class DeployResultFormatter extends ResultFormatter {
115102
}
116103

117104
protected displaySuccesses(): void {
118-
if (this.isSuccess() && this.hasComponents()) {
119-
this.sortFileResponses(this.fileResponses);
120-
this.asRelativePaths(this.fileResponses);
105+
if (this.isSuccess() && this.fileResponses?.length) {
106+
const successes = this.fileResponses.filter((f) => f.state !== 'Failed');
107+
if (!successes.length) {
108+
return;
109+
}
110+
this.sortFileResponses(successes);
111+
this.asRelativePaths(successes);
121112

122113
this.ux.log('');
123114
this.ux.styledHeader(chalk.blue('Deployed Source'));
124-
this.ux.table(this.fileResponses, {
115+
this.ux.table(successes, {
125116
columns: [
126117
{ key: 'fullName', label: 'FULL NAME' },
127118
{ key: 'type', label: 'TYPE' },
@@ -132,15 +123,13 @@ export class DeployResultFormatter extends ResultFormatter {
132123
}
133124

134125
protected displayFailures(): void {
135-
if (this.hasStatus(RequestStatus.Failed) && this.hasComponents()) {
126+
if (this.hasStatus(RequestStatus.Failed) && this.fileResponses?.length) {
136127
const failures = this.fileResponses.filter((f) => f.state === 'Failed');
137128
this.sortFileResponses(failures);
138129
this.asRelativePaths(failures);
139130

140131
this.ux.log('');
141132
this.ux.styledHeader(chalk.red(`Component Failures [${failures.length}]`));
142-
// TODO: do we really need the project path or file path in the table?
143-
// Seems like we can just provide the full name and devs will know.
144133
this.ux.table(failures, {
145134
columns: [
146135
{ key: 'problemType', label: 'Type' },

test/commands/source/deployResponses.ts

Lines changed: 44 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,12 @@
66
*/
77

88
import { DeployResult } from '@salesforce/source-deploy-retrieve';
9-
import { MetadataApiDeployStatus, RequestStatus } from '@salesforce/source-deploy-retrieve/lib/src/client/types';
9+
import {
10+
DeployMessage,
11+
MetadataApiDeployStatus,
12+
RequestStatus,
13+
} from '@salesforce/source-deploy-retrieve/lib/src/client/types';
14+
import { cloneJson } from '@salesforce/kit';
1015

1116
const baseDeployResponse = {
1217
checkOnly: false,
@@ -73,15 +78,27 @@ export const getDeployResponse = (
7378
type: DeployResponseType,
7479
overrides?: Partial<MetadataApiDeployStatus>
7580
): MetadataApiDeployStatus => {
76-
const response = { ...baseDeployResponse, ...overrides };
81+
// stringify --> parse to get a clone that doesn't affedt the base deploy response
82+
const response = JSON.parse(JSON.stringify({ ...baseDeployResponse, ...overrides })) as MetadataApiDeployStatus;
7783

7884
if (type === 'canceled') {
7985
response.canceledBy = '0051h000006BHOq';
8086
response.canceledByName = 'Canceling User';
8187
response.status = RequestStatus.Canceled;
8288
}
8389

84-
return response as MetadataApiDeployStatus;
90+
if (type === 'failed') {
91+
response.status = RequestStatus.Failed;
92+
response.success = false;
93+
response.details.componentFailures = cloneJson(baseDeployResponse.details.componentSuccesses[1]) as DeployMessage;
94+
response.details.componentSuccesses = cloneJson(baseDeployResponse.details.componentSuccesses[0]) as DeployMessage;
95+
response.details.componentFailures.success = 'false';
96+
delete response.details.componentFailures.id;
97+
response.details.componentFailures.problemType = 'Error';
98+
response.details.componentFailures.problem = 'This component has some problems';
99+
}
100+
101+
return response;
85102
};
86103

87104
export const getDeployResult = (
@@ -93,14 +110,30 @@ export const getDeployResult = (
93110
return {
94111
response,
95112
getFileResponses() {
96-
let successes = response.details.componentSuccesses;
97-
successes = Array.isArray(successes) ? successes : [successes];
98-
return successes.map((comp) => ({
99-
fullName: comp.fullName,
100-
filePath: comp.fileName,
101-
state: 'Changed',
102-
type: comp.componentType,
103-
}));
113+
let fileProps: DeployMessage[] = [];
114+
if (type === 'failed') {
115+
const failures = response.details.componentFailures || [];
116+
fileProps = Array.isArray(failures) ? failures : [failures];
117+
return fileProps.map((comp) => ({
118+
fullName: comp.fullName,
119+
filePath: comp.fileName,
120+
state: 'Failed',
121+
type: comp.componentType,
122+
error: comp.problem,
123+
problemType: comp.problemType,
124+
}));
125+
} else {
126+
const successes = response.details.componentSuccesses;
127+
fileProps = Array.isArray(successes) ? successes : [successes];
128+
return fileProps
129+
.filter((p) => p.fileName !== 'package.xml')
130+
.map((comp) => ({
131+
fullName: comp.fullName,
132+
filePath: comp.fileName,
133+
state: 'Changed',
134+
type: comp.componentType,
135+
}));
136+
}
104137
},
105138
} as DeployResult;
106139
};
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
/*
2+
* Copyright (c) 2020, salesforce.com, inc.
3+
* All rights reserved.
4+
* Licensed under the BSD 3-Clause license.
5+
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
6+
*/
7+
8+
import * as sinon from 'sinon';
9+
import { expect } from 'chai';
10+
import { Logger } from '@salesforce/core';
11+
import { UX } from '@salesforce/command';
12+
import { stubInterface } from '@salesforce/ts-sinon';
13+
import { getDeployResult } from '../commands/source/deployResponses';
14+
import { DeployCommandResult, DeployResultFormatter } from '../../src/formatters/deployResultFormatter';
15+
16+
describe('DeployResultFormatter', () => {
17+
const sandbox = sinon.createSandbox();
18+
19+
const deployResultSuccess = getDeployResult('successSync');
20+
const deployResultFailure = getDeployResult('failed');
21+
22+
const logger = Logger.childFromRoot('deployTestLogger').useMemoryLogging();
23+
let ux;
24+
let logStub: sinon.SinonStub;
25+
let styledHeaderStub: sinon.SinonStub;
26+
let tableStub: sinon.SinonStub;
27+
28+
beforeEach(() => {
29+
logStub = sandbox.stub();
30+
styledHeaderStub = sandbox.stub();
31+
tableStub = sandbox.stub();
32+
ux = stubInterface<UX>(sandbox, {
33+
log: logStub,
34+
styledHeader: styledHeaderStub,
35+
table: tableStub,
36+
});
37+
});
38+
39+
afterEach(() => {
40+
sandbox.restore();
41+
});
42+
43+
describe('getJson', () => {
44+
it('should return expected json for a success', async () => {
45+
const deployResponse = JSON.parse(JSON.stringify(deployResultSuccess.response)) as DeployCommandResult;
46+
const expectedSuccessResults = deployResultSuccess.response as DeployCommandResult;
47+
const formatter = new DeployResultFormatter(logger, ux, {}, deployResultSuccess);
48+
const json = formatter.getJson();
49+
50+
expectedSuccessResults.deployedSource = deployResultSuccess.getFileResponses();
51+
expectedSuccessResults.outboundFiles = [];
52+
expectedSuccessResults.deploys = [deployResponse];
53+
expect(json).to.deep.equal(expectedSuccessResults);
54+
});
55+
56+
it('should return expected json for a failure', async () => {
57+
const deployResponse = JSON.parse(JSON.stringify(deployResultFailure.response)) as DeployCommandResult;
58+
const expectedFailureResults = deployResultFailure.response as DeployCommandResult;
59+
expectedFailureResults.deployedSource = deployResultFailure.getFileResponses();
60+
expectedFailureResults.outboundFiles = [];
61+
expectedFailureResults.deploys = [deployResponse];
62+
const formatter = new DeployResultFormatter(logger, ux, {}, deployResultFailure);
63+
expect(formatter.getJson()).to.deep.equal(expectedFailureResults);
64+
});
65+
});
66+
67+
describe('display', () => {
68+
it('should output as expected for a success', async () => {
69+
const formatter = new DeployResultFormatter(logger, ux, {}, deployResultSuccess);
70+
formatter.display();
71+
expect(styledHeaderStub.calledOnce).to.equal(true);
72+
expect(logStub.calledOnce).to.equal(true);
73+
expect(tableStub.called).to.equal(true);
74+
expect(styledHeaderStub.firstCall.args[0]).to.contain('Deployed Source');
75+
const fileResponses = deployResultSuccess.getFileResponses();
76+
expect(tableStub.firstCall.args[0]).to.deep.equal(fileResponses);
77+
});
78+
79+
it('should output as expected for a failure', async () => {
80+
const formatter = new DeployResultFormatter(logger, ux, {}, deployResultFailure);
81+
formatter.display();
82+
expect(styledHeaderStub.calledOnce).to.equal(true);
83+
expect(logStub.calledTwice).to.equal(true);
84+
expect(tableStub.called).to.equal(true);
85+
expect(styledHeaderStub.firstCall.args[0]).to.contain('Component Failures [1]');
86+
const fileResponses = deployResultFailure.getFileResponses();
87+
expect(tableStub.firstCall.args[0]).to.deep.equal(fileResponses);
88+
});
89+
});
90+
});

0 commit comments

Comments
 (0)