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

fix: deploy errors are reported properly #146

Merged
merged 3 commits into from
Jul 19, 2021
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
31 changes: 10 additions & 21 deletions src/formatters/deployResultFormatter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ export class DeployResultFormatter extends ResultFormatter {
}

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

// Returns true if the components returned in the server response
// were mapped to local source in the ComponentSet.
protected hasMappedComponents(): boolean {
return getNumber(this.result, 'components.size', 0) > 0;
}

// Returns true if the server response contained components.
protected hasComponents(): boolean {
const successes = getNumber(this.result, 'response.details.componentSuccesses.length', 0) > 0;
const failures = getNumber(this.result, 'response.details.componentFailures.length', 0) > 0;
return successes || failures;
}

protected isRunTestsEnabled(): boolean {
return getBoolean(this.result, 'response.runTestsEnabled', false);
}
Expand All @@ -115,13 +102,17 @@ export class DeployResultFormatter extends ResultFormatter {
}

protected displaySuccesses(): void {
if (this.isSuccess() && this.hasComponents()) {
this.sortFileResponses(this.fileResponses);
this.asRelativePaths(this.fileResponses);
if (this.isSuccess() && this.fileResponses?.length) {
const successes = this.fileResponses.filter((f) => f.state !== 'Failed');
if (!successes.length) {
return;
}
this.sortFileResponses(successes);
this.asRelativePaths(successes);

this.ux.log('');
this.ux.styledHeader(chalk.blue('Deployed Source'));
this.ux.table(this.fileResponses, {
this.ux.table(successes, {
columns: [
{ key: 'fullName', label: 'FULL NAME' },
{ key: 'type', label: 'TYPE' },
Expand All @@ -132,15 +123,13 @@ export class DeployResultFormatter extends ResultFormatter {
}

protected displayFailures(): void {
if (this.hasStatus(RequestStatus.Failed) && this.hasComponents()) {
if (this.hasStatus(RequestStatus.Failed) && this.fileResponses?.length) {
const failures = this.fileResponses.filter((f) => f.state === 'Failed');
this.sortFileResponses(failures);
this.asRelativePaths(failures);

this.ux.log('');
this.ux.styledHeader(chalk.red(`Component Failures [${failures.length}]`));
// TODO: do we really need the project path or file path in the table?
// Seems like we can just provide the full name and devs will know.
this.ux.table(failures, {
columns: [
{ key: 'problemType', label: 'Type' },
Expand Down
54 changes: 43 additions & 11 deletions test/commands/source/deployResponses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,12 @@
*/

import { DeployResult } from '@salesforce/source-deploy-retrieve';
import { MetadataApiDeployStatus, RequestStatus } from '@salesforce/source-deploy-retrieve/lib/src/client/types';
import {
DeployMessage,
MetadataApiDeployStatus,
RequestStatus,
} from '@salesforce/source-deploy-retrieve/lib/src/client/types';
import { cloneJson } from '@salesforce/kit';

const baseDeployResponse = {
checkOnly: false,
Expand Down Expand Up @@ -73,15 +78,26 @@ export const getDeployResponse = (
type: DeployResponseType,
overrides?: Partial<MetadataApiDeployStatus>
): MetadataApiDeployStatus => {
const response = { ...baseDeployResponse, ...overrides };
const response = JSON.parse(JSON.stringify({ ...baseDeployResponse, ...overrides })) as MetadataApiDeployStatus;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

probably worth a comment as to why the parse/stringify is required


if (type === 'canceled') {
response.canceledBy = '0051h000006BHOq';
response.canceledByName = 'Canceling User';
response.status = RequestStatus.Canceled;
}

return response as MetadataApiDeployStatus;
if (type === 'failed') {
response.status = RequestStatus.Failed;
response.success = false;
response.details.componentFailures = cloneJson(baseDeployResponse.details.componentSuccesses[1]) as DeployMessage;
response.details.componentSuccesses = cloneJson(baseDeployResponse.details.componentSuccesses[0]) as DeployMessage;
response.details.componentFailures.success = 'false';
delete response.details.componentFailures.id;
response.details.componentFailures.problemType = 'Error';
response.details.componentFailures.problem = 'This component has some problems';
}

return response;
};

export const getDeployResult = (
Expand All @@ -93,14 +109,30 @@ export const getDeployResult = (
return {
response,
getFileResponses() {
let successes = response.details.componentSuccesses;
successes = Array.isArray(successes) ? successes : [successes];
return successes.map((comp) => ({
fullName: comp.fullName,
filePath: comp.fileName,
state: 'Changed',
type: comp.componentType,
}));
let fileProps: DeployMessage[] = [];
if (type === 'failed') {
const failures = response.details.componentFailures || [];
fileProps = Array.isArray(failures) ? failures : [failures];
return fileProps.map((comp) => ({
fullName: comp.fullName,
filePath: comp.fileName,
state: 'Failed',
type: comp.componentType,
error: comp.problem,
problemType: comp.problemType,
}));
} else {
const successes = response.details.componentSuccesses;
fileProps = Array.isArray(successes) ? successes : [successes];
return fileProps
.filter((p) => p.fileName !== 'package.xml')
.map((comp) => ({
fullName: comp.fullName,
filePath: comp.fileName,
state: 'Changed',
type: comp.componentType,
}));
}
},
} as DeployResult;
};
90 changes: 90 additions & 0 deletions test/formatters/deployResultFormatter.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/*
* Copyright (c) 2020, salesforce.com, inc.
* All rights reserved.
* 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 * as sinon from 'sinon';
import { expect } from 'chai';
import { Logger } from '@salesforce/core';
import { UX } from '@salesforce/command';
import { stubInterface } from '@salesforce/ts-sinon';
import { getDeployResult } from '../commands/source/deployResponses';
import { DeployCommandResult, DeployResultFormatter } from '../../src/formatters/deployResultFormatter';

describe('DeployResultFormatter', () => {
const sandbox = sinon.createSandbox();

const deployResultSuccess = getDeployResult('successSync');
const deployResultFailure = getDeployResult('failed');

const logger = Logger.childFromRoot('deployTestLogger').useMemoryLogging();
let ux;
let logStub: sinon.SinonStub;
let styledHeaderStub: sinon.SinonStub;
let tableStub: sinon.SinonStub;

beforeEach(() => {
logStub = sandbox.stub();
styledHeaderStub = sandbox.stub();
tableStub = sandbox.stub();
ux = stubInterface<UX>(sandbox, {
log: logStub,
styledHeader: styledHeaderStub,
table: tableStub,
});
});

afterEach(() => {
sandbox.restore();
});

describe('getJson', () => {
it('should return expected json for a success', async () => {
const deployResponse = JSON.parse(JSON.stringify(deployResultSuccess.response)) as DeployCommandResult;
const expectedSuccessResults = deployResultSuccess.response as DeployCommandResult;
const formatter = new DeployResultFormatter(logger, ux, {}, deployResultSuccess);
const json = formatter.getJson();

expectedSuccessResults.deployedSource = deployResultSuccess.getFileResponses();
expectedSuccessResults.outboundFiles = [];
expectedSuccessResults.deploys = [deployResponse];
expect(json).to.deep.equal(expectedSuccessResults);
});

it('should return expected json for a failure', async () => {
const deployResponse = JSON.parse(JSON.stringify(deployResultFailure.response)) as DeployCommandResult;
const expectedFailureResults = deployResultFailure.response as DeployCommandResult;
expectedFailureResults.deployedSource = deployResultFailure.getFileResponses();
expectedFailureResults.outboundFiles = [];
expectedFailureResults.deploys = [deployResponse];
const formatter = new DeployResultFormatter(logger, ux, {}, deployResultFailure);
expect(formatter.getJson()).to.deep.equal(expectedFailureResults);
});
});

describe('display', () => {
it('should output as expected for a success', async () => {
const formatter = new DeployResultFormatter(logger, ux, {}, deployResultSuccess);
formatter.display();
expect(styledHeaderStub.calledOnce).to.equal(true);
expect(logStub.calledOnce).to.equal(true);
expect(tableStub.called).to.equal(true);
expect(styledHeaderStub.firstCall.args[0]).to.contain('Deployed Source');
const fileResponses = deployResultSuccess.getFileResponses();
expect(tableStub.firstCall.args[0]).to.deep.equal(fileResponses);
});

it('should output as expected for a failure', async () => {
const formatter = new DeployResultFormatter(logger, ux, {}, deployResultFailure);
formatter.display();
expect(styledHeaderStub.calledOnce).to.equal(true);
expect(logStub.calledTwice).to.equal(true);
expect(tableStub.called).to.equal(true);
expect(styledHeaderStub.firstCall.args[0]).to.contain('Component Failures [1]');
const fileResponses = deployResultFailure.getFileResponses();
expect(tableStub.firstCall.args[0]).to.deep.equal(fileResponses);
});
});
});