Skip to content

Commit

Permalink
fix: add remote components to report response
Browse files Browse the repository at this point in the history
  • Loading branch information
WillieRuemmele committed Apr 3, 2024
1 parent 7dbfd03 commit c7b2eb0
Show file tree
Hide file tree
Showing 2 changed files with 290 additions and 4 deletions.
29 changes: 25 additions & 4 deletions src/formatters/deployResultFormatter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
JUnitReporter,
TestResult,
} from '@salesforce/apex-node';
import { getState } from '@salesforce/source-deploy-retrieve/lib/src/client/deployMessages.js';
import {
DeployResultJson,
isSdrFailure,
Expand All @@ -51,10 +52,10 @@ import {
import { TestResultsFormatter } from '../formatters/testResultsFormatter.js';

export class DeployResultFormatter extends TestResultsFormatter implements Formatter<DeployResultJson> {
private relativeFiles: FileResponse[];
private absoluteFiles: FileResponse[];
private coverageOptions: CoverageReporterOptions;
private resultsDir: string;
private readonly relativeFiles: FileResponse[];
private readonly absoluteFiles: FileResponse[];
private readonly coverageOptions: CoverageReporterOptions;
private readonly resultsDir: string;
private readonly junit: boolean | undefined;

public constructor(
Expand Down Expand Up @@ -285,6 +286,26 @@ export class DeployResultFormatter extends TestResultsFormatter implements Forma
private displaySuccesses(): void {
const successes = this.relativeFiles.filter(isSdrSuccess);

ensureArray(this.result.response.details.componentSuccesses)
.filter(
(fromServer) =>
// removes package.xml, other manifests
fromServer.componentType !== '' &&
// if we don't find the file in the response, it's because it doesn't exist locally, yet
!successes.find(
(fromLocal) => fromServer.fullName === fromLocal.fullName && fromServer.componentType === fromLocal.type
)
)
.map((s) =>
successes.push({
fullName: s.fullName,
// @ts-expect-error getState can return 'failed' which isn't applicable to FileSuccess
state: getState(s),
type: s.componentType ?? '',
filePath: 'Not found in project',
})
);

if (!successes.length || this.result.response.status === RequestStatus.Failed) return;

const columns = {
Expand Down
265 changes: 265 additions & 0 deletions test/utils/deployResultFormatter.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,265 @@
/*
* 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 path from 'node:path';
import { assert, expect, config } from 'chai';
import sinon from 'sinon';
import { DeployMessage, DeployResult, FileResponse } from '@salesforce/source-deploy-retrieve';
import { ux } from '@oclif/core';
import { ensureArray } from '@salesforce/kit';
import { getCoverageFormattersOptions } from '../../src/utils/coverage.js';
import { DeployResultFormatter } from '../../src/formatters/deployResultFormatter.js';
import { getDeployResult } from './deployResponses.js';

config.truncateThreshold = 0;

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

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

describe('displaySuccesses', () => {
const deployResultSuccess = getDeployResult('successSync');
let tableStub: sinon.SinonStub;

beforeEach(() => {
tableStub = sandbox.stub(ux, 'table');
});

it('finds components in server response, and adds them if not in fileResponses', () => {
ensureArray(deployResultSuccess.response.details.componentSuccesses).push({
changed: 'false',
created: 'true',
createdDate: '123',
fullName: 'remoteClass',
componentType: 'ApexClass',
success: 'true',
deleted: 'false',
fileName: '',
});
const formatter = new DeployResultFormatter(deployResultSuccess, { verbose: true });
formatter.display();
expect(tableStub.callCount).to.equal(1);
expect(tableStub.firstCall.args[0]).to.deep.equal([
{
filePath: 'classes/ProductController.cls',
fullName: 'ProductController',
state: 'Changed',
type: 'ApexClass',
},
{
filePath: 'Not found in project',
fullName: 'remoteClass',
state: 'Created',
type: 'ApexClass',
},
]);
});
});

describe('displayFailures', () => {
const deployResultFailure = getDeployResult('failed');
let tableStub: sinon.SinonStub;

beforeEach(() => {
tableStub = sandbox.stub(ux, 'table');
});

it('prints file responses, and messages from server', () => {
const formatter = new DeployResultFormatter(deployResultFailure, { verbose: true });
formatter.display();
expect(tableStub.callCount).to.equal(1);
expect(tableStub.firstCall.args[0]).to.deep.equal([
{
error: 'This component has some problems',
fullName: 'ProductController',
loc: '27:18',
problemType: 'Error',
},
]);
});

it('displays errors from the server not in file responses', () => {
const deployFailure = getDeployResult('failed');
const error1 = {
changed: false,
componentType: 'ApexClass',
created: false,
createdDate: '2021-04-27T22:18:07.000Z',
deleted: false,
fileName: 'classes/ProductController.cls',
fullName: 'ProductController',
success: false,
problemType: 'Error',
problem: 'This component has some problems',
lineNumber: '27',
columnNumber: '18',
} as DeployMessage;

// add package.xml error, which is different from a FileResponse error
const error2 = {
changed: false,
componentType: '',
created: false,
createdDate: '2023-11-17T21:18:36.000Z',
deleted: false,
fileName: 'package.xml',
fullName: 'Create_property',
problem:
"An object 'Create_property' of type Flow was named in package.xml, but was not found in zipped directory",
problemType: 'Error',
success: false,
} as DeployMessage;

deployFailure.response.details.componentFailures = [error1, error2];
sandbox.stub(deployFailure, 'getFileResponses').returns([
{
fullName: error1.fullName,
filePath: error1.fileName,
type: error1.componentType,
state: 'Failed',
lineNumber: error1.lineNumber,
columnNumber: error1.columnNumber,
error: error1.problem,
problemType: error1.problemType,
},
] as FileResponse[]);
const formatter = new DeployResultFormatter(deployFailure, { verbose: true });
formatter.display();
expect(tableStub.callCount).to.equal(1);
expect(tableStub.firstCall.args[0]).to.deep.equal([
{
error: error2.problem,
fullName: error2.fullName,
loc: '',
problemType: error2.problemType,
},
{
error: 'This component has some problems',
fullName: 'ProductController',
loc: '27:18',
problemType: 'Error',
},
]);
});
});

describe('coverage functions', () => {
describe('getCoverageFormattersOptions', () => {
it('clover, json', () => {
const result = getCoverageFormattersOptions(['clover', 'json']);
expect(result).to.deep.equal({
reportFormats: ['clover', 'json'],
reportOptions: {
clover: { file: path.join('coverage', 'clover.xml'), projectRoot: '.' },
json: { file: path.join('coverage', 'coverage.json') },
},
});
});

it('will warn when code coverage warning present from server', () => {
const deployResult = getDeployResult('codeCoverageWarning');
const formatter = new DeployResultFormatter(deployResult, {});
const warnStub = sandbox.stub(ux, 'warn');
formatter.display();
expect(warnStub.callCount).to.equal(1);
expect(warnStub.firstCall.args[0]).to.equal(
'Average test coverage across all Apex Classes and Triggers is 25%, at least 75% test coverage is required.'
);
});

it('will write test output when in json mode', async () => {
const deployResult = getDeployResult('passedTest');
const formatter = new DeployResultFormatter(deployResult, {
junit: true,
'coverage-formatters': ['text', 'cobertura'],
});
// private method stub
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const coverageReportStub = sandbox.stub(formatter, 'createCoverageReport');
// private method stub
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const junitStub = sandbox.stub(formatter, 'createJunitResults');
await formatter.getJson();
expect(coverageReportStub.calledOnce).to.equal(true);
expect(junitStub.calledOnce).to.equal(true);
});

it('teamcity', () => {
const result = getCoverageFormattersOptions(['teamcity']);
expect(result).to.deep.equal({
reportFormats: ['teamcity'],
reportOptions: {
teamcity: { file: path.join('coverage', 'teamcity.txt'), blockName: 'coverage' },
},
});
});
});
});

describe('replacements', () => {
const deployResultSuccess = getDeployResult('successSync');
const deployResultSuccessWithReplacements = {
...getDeployResult('successSync'),
replacements: new Map<string, string[]>([['foo', ['bar', 'baz']]]),
} as DeployResult;

describe('json', () => {
it('shows replacements when not concise', async () => {
const formatter = new DeployResultFormatter(deployResultSuccessWithReplacements, { verbose: true });
const json = await formatter.getJson();
assert('replacements' in json && json.replacements);
expect(json.replacements).to.deep.equal({ foo: ['bar', 'baz'] });
});
it('no replacements when concise', () => {
const formatter = new DeployResultFormatter(deployResultSuccessWithReplacements, { concise: true });
const json = formatter.getJson();
expect(json).to.not.have.property('replacements');
});
});
describe('human', () => {
let uxStub: sinon.SinonStub;
beforeEach(() => {
uxStub = sandbox.stub(process.stdout, 'write');
});

const getStdout = () =>
uxStub
.getCalls()
// args are typed as any[]
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
.flatMap((call) => call.args)
.join('\n');

it('shows replacements when verbose and replacements exist', () => {
const formatter = new DeployResultFormatter(deployResultSuccessWithReplacements, { verbose: true });
formatter.display();
expect(getStdout()).to.include('Metadata Replacements');
expect(getStdout()).to.include('TEXT REPLACED');
});

it('no replacements when verbose but there are none', () => {
const formatter = new DeployResultFormatter(deployResultSuccess, { verbose: true });
formatter.display();
expect(getStdout()).to.not.include('Metadata Replacements');
});
it('no replacements when not verbose', () => {
const formatter = new DeployResultFormatter(deployResultSuccessWithReplacements, { verbose: false });
formatter.display();
expect(getStdout()).to.not.include('Metadata Replacements');
});
it('no replacements when concise', () => {
const formatter = new DeployResultFormatter(deployResultSuccessWithReplacements, { concise: true });
formatter.display();
expect(getStdout()).to.not.include('Metadata Replacements');
});
});
});
});

0 comments on commit c7b2eb0

Please sign in to comment.