diff --git a/messages/run-command.md b/messages/run-command.md index d001a3cc7..bb2ee25d9 100644 --- a/messages/run-command.md +++ b/messages/run-command.md @@ -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. diff --git a/src/commands/code-analyzer/run.ts b/src/commands/code-analyzer/run.ts index 247cab9ff..16fa2b2e5 100644 --- a/src/commands/code-analyzer/run.ts +++ b/src/commands/code-analyzer/run.ts @@ -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'; @@ -59,7 +59,6 @@ export default class RunCommand extends SfCommand 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({ @@ -83,7 +82,7 @@ export default class RunCommand extends SfCommand 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'], @@ -97,17 +96,16 @@ export default class RunCommand extends SfCommand 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) }; } @@ -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(); + } +} diff --git a/src/lib/viewers/ResultsViewer.ts b/src/lib/viewers/ResultsViewer.ts index 1c06b3950..d4ed113a5 100644 --- a/src/lib/viewers/ResultsViewer.ts +++ b/src/lib/viewers/ResultsViewer.ts @@ -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; diff --git a/test/commands/code-analyzer/run.test.ts b/test/commands/code-analyzer/run.test.ts index 094ece083..74bb6c6ad 100644 --- a/test/commands/code-analyzer/run.test.ts +++ b/test/commands/code-analyzer/run.test.ts @@ -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) => { @@ -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(() => { @@ -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]); @@ -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'; @@ -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'); + }); + }); + }); });