Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
2 changes: 2 additions & 0 deletions messages/run-command.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,8 @@ Format to display the command results in the terminal.

The format `table` is concise and shows minimal output, the format `detail` shows all available information.

If you specify neither --view nor --output-file, then the default table view is shown. If you specify --output-file but not --view, only summary information is shown.

# flags.output-file.summary

Output file that contains the analysis results. The file format depends on the extension you specify, such as .csv, .html, .xml, and so on.
Expand Down
24 changes: 17 additions & 7 deletions src/commands/code-analyzer/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {View} from '../../Constants';
import {CodeAnalyzerConfigFactoryImpl} from '../../lib/factories/CodeAnalyzerConfigFactory';
import {EnginePluginsFactoryImpl} from '../../lib/factories/EnginePluginsFactory';
import {CompositeResultsWriter} from '../../lib/writers/ResultsWriter';
import {ResultsDetailDisplayer, ResultsTableDisplayer} from '../../lib/viewers/ResultsViewer';
import {ResultsDetailDisplayer, ResultsNoOpDisplayer, ResultsTableDisplayer, ResultsViewer} from '../../lib/viewers/ResultsViewer';
import {RunActionSummaryViewer} from '../../lib/viewers/ActionSummaryViewer';
import {BundleName, getMessage, getMessages} from '../../lib/messages';
import {LogEventDisplayer} from '../../lib/listeners/LogEventListener';
Expand Down Expand Up @@ -59,7 +59,6 @@ export default class RunCommand extends SfCommand<void> implements Displayable {
summary: getMessage(BundleName.RunCommand, 'flags.view.summary'),
description: getMessage(BundleName.RunCommand, 'flags.view.description'),
char: 'v',
default: View.TABLE,
options: Object.values(View)
}),
'output-file': Flags.string({
Expand All @@ -83,7 +82,7 @@ export default class RunCommand extends SfCommand<void> implements Displayable {
this.warn(getMessage(BundleName.Shared, "warning.command-state", [getMessage(BundleName.Shared, 'label.command-state')]));

const parsedFlags = (await this.parse(RunCommand)).flags;
const dependencies: RunDependencies = this.createDependencies(parsedFlags.view as View, parsedFlags['output-file']);
const dependencies: RunDependencies = this.createDependencies(parsedFlags.view as View|undefined, parsedFlags['output-file']);
const action: RunAction = RunAction.createAction(dependencies);
const runInput: RunInput = {
'config-file': parsedFlags['config-file'],
Expand All @@ -97,17 +96,16 @@ export default class RunCommand extends SfCommand<void> implements Displayable {
await action.execute(runInput);
}

protected createDependencies(view: View, outputFiles: string[] = []): RunDependencies {
protected createDependencies(view: View|undefined, outputFiles: string[] = []): RunDependencies {
const uxDisplay: UxDisplay = new UxDisplay(this, this.spinner);
const resultsViewer: ResultsViewer = createResultsViewer(view, outputFiles, uxDisplay);
return {
configFactory: new CodeAnalyzerConfigFactoryImpl(),
pluginsFactory: new EnginePluginsFactoryImpl(),
writer: CompositeResultsWriter.fromFiles(outputFiles),
logEventListeners: [new LogEventDisplayer(uxDisplay)],
progressListeners: [new EngineRunProgressSpinner(uxDisplay), new RuleSelectionProgressSpinner(uxDisplay)],
resultsViewer: view === View.TABLE
? new ResultsTableDisplayer(uxDisplay)
: new ResultsDetailDisplayer(uxDisplay),
resultsViewer,
actionSummaryViewer: new RunActionSummaryViewer(uxDisplay)
};
}
Expand Down Expand Up @@ -138,3 +136,15 @@ function convertThresholdToEnum(threshold: string): SeverityLevel {
}
}

function createResultsViewer(view: View|undefined, outputFiles: string[], uxDisplay: UxDisplay): ResultsViewer {
switch (view) {
case View.DETAIL:
return new ResultsDetailDisplayer(uxDisplay);
case View.TABLE:
return new ResultsTableDisplayer(uxDisplay);
default:
return outputFiles.length === 0
? new ResultsTableDisplayer(uxDisplay)
: new ResultsNoOpDisplayer();
}
}
7 changes: 7 additions & 0 deletions src/lib/viewers/ResultsViewer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,13 @@ export interface ResultsViewer {
view(results: RunResults): void;
}

export class ResultsNoOpDisplayer implements ResultsViewer {
public view(_results: RunResults): void {
// istanbul ignore next - No need to cover deliberate no-op
return;
}
}

abstract class AbstractResultsDisplayer implements ResultsViewer {
protected display: Display;

Expand Down
57 changes: 40 additions & 17 deletions test/commands/code-analyzer/run.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ describe('`code-analyzer run` tests', () => {
let createActionSpy: jest.SpyInstance;
let receivedActionInput: RunInput;
let receivedActionDependencies: RunDependencies;
let fromFilesSpy: jest.SpyInstance;
let receivedFiles: string[];
beforeEach(() => {
stubSfCommandUx($$.SANDBOX);
executeSpy = jest.spyOn(RunAction.prototype, 'execute').mockImplementation((input) => {
Expand All @@ -22,6 +24,11 @@ describe('`code-analyzer run` tests', () => {
receivedActionDependencies = dependencies;
return originalCreateAction(dependencies);
});
const originalFromFiles = CompositeResultsWriter.fromFiles;
fromFilesSpy = jest.spyOn(CompositeResultsWriter, 'fromFiles').mockImplementation(files => {
receivedFiles = files;
return originalFromFiles(files);
})
});

afterEach(() => {
Expand Down Expand Up @@ -231,17 +238,6 @@ describe('`code-analyzer run` tests', () => {
});

describe('--output-file', () => {
let fromFilesSpy: jest.SpyInstance;
let receivedFiles: string[];

beforeEach(() => {
const originalFromFiles = CompositeResultsWriter.fromFiles;
fromFilesSpy = jest.spyOn(CompositeResultsWriter, 'fromFiles').mockImplementation(files => {
receivedFiles = files;
return originalFromFiles(files);
})
});

it('Can be supplied once with a single value', async () => {
const inputValue = './somefile.json';
await RunCommand.run(['--output-file', inputValue]);
Expand Down Expand Up @@ -312,12 +308,6 @@ describe('`code-analyzer run` tests', () => {
expect(executeSpy).not.toHaveBeenCalled();
});

it('Defaults to value of "table"', async () => {
await RunCommand.run([]);
expect(createActionSpy).toHaveBeenCalled();
expect(receivedActionDependencies.resultsViewer.constructor.name).toEqual('ResultsTableDisplayer');
});

it('Can be supplied only once', async () => {
const inputValue1 = 'detail';
const inputValue2 = 'table';
Expand All @@ -334,5 +324,38 @@ describe('`code-analyzer run` tests', () => {
expect(receivedActionDependencies.resultsViewer.constructor.name).toEqual('ResultsDetailDisplayer');
});
});

describe('Flag interactions', () => {
describe('--output-file and --view', () => {
it('When --output-file and --view are both present, both are used', async () => {
const outfileInput = 'beep.json';
const viewInput = 'detail';
await RunCommand.run(['--output-file', outfileInput, '--view', viewInput]);
expect(executeSpy).toHaveBeenCalled();
expect(createActionSpy).toHaveBeenCalled();
expect(fromFilesSpy).toHaveBeenCalled();
expect(receivedFiles).toEqual([outfileInput]);
expect(receivedActionDependencies.resultsViewer.constructor.name).toEqual('ResultsDetailDisplayer');
});

it('When --output-file is present and --view is not, --view is a no-op', async () => {
const outfileInput= 'beep.json';
await RunCommand.run(['--output-file', outfileInput]);
expect(executeSpy).toHaveBeenCalled();
expect(createActionSpy).toHaveBeenCalled();
expect(fromFilesSpy).toHaveBeenCalled();
expect(receivedFiles).toEqual([outfileInput]);
expect(receivedActionDependencies.resultsViewer.constructor.name).toEqual('ResultsNoOpDisplayer');
});

it('When --output-file and --view are both absent, --view defaults to "table"', async () => {
await RunCommand.run([]);
expect(createActionSpy).toHaveBeenCalled();
expect(fromFilesSpy).toHaveBeenCalled();
expect(receivedFiles).toEqual([]);
expect(receivedActionDependencies.resultsViewer.constructor.name).toEqual('ResultsTableDisplayer');
});
});
});
});