Skip to content

Commit a748368

Browse files
committed
NEW @W-16891765@ Config action mentions outfile
1 parent 1b6b60d commit a748368

File tree

12 files changed

+179
-10
lines changed

12 files changed

+179
-10
lines changed

messages/action-summary-viewer.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# common.summary-header
2+
3+
Summary
4+
5+
# common.logfile-location
6+
7+
Additional log information written to:
8+
9+
# config-action.no-outfiles
10+
11+
No output file was specified.
12+
13+
# config-action.outfile-location
14+
15+
Config written to:

src/commands/code-analyzer/config.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import {Flags, SfCommand} from '@salesforce/sf-plugins-core';
22
import {ConfigAction, ConfigDependencies} from '../../lib/actions/ConfigAction';
33
import {ConfigFileWriter} from '../../lib/writers/ConfigWriter';
44
import {ConfigStyledYamlViewer} from '../../lib/viewers/ConfigViewer';
5+
import {ConfigActionSummaryViewer} from '../../lib/viewers/ActionSummaryViewer';
56
import {CodeAnalyzerConfigFactoryImpl} from '../../lib/factories/CodeAnalyzerConfigFactory';
67
import {EnginePluginsFactoryImpl} from '../../lib/factories/EnginePluginsFactory';
78
import {BundleName, getMessage, getMessages} from '../../lib/messages';
@@ -70,6 +71,7 @@ export default class ConfigCommand extends SfCommand<void> implements Displayabl
7071
logEventListeners: [new LogEventDisplayer(uxDisplay)],
7172
progressEventListeners: [new RuleSelectionProgressSpinner(uxDisplay)],
7273
modelGenerator: modelGeneratorFunction,
74+
actionSummaryViewer: new ConfigActionSummaryViewer(uxDisplay),
7375
viewer: new ConfigStyledYamlViewer(uxDisplay)
7476
};
7577
if (outputFile) {

src/lib/actions/ConfigAction.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {ConfigViewer} from '../viewers/ConfigViewer';
77
import {createWorkspace} from '../utils/WorkspaceUtil';
88
import {LogEventListener, LogEventLogger} from '../listeners/LogEventListener';
99
import {ProgressEventListener} from '../listeners/ProgressEventListener';
10+
import {ConfigActionSummaryViewer} from '../viewers/ActionSummaryViewer';
1011
import {ConfigModel, ConfigModelGeneratorFunction, ConfigContext} from '../models/ConfigModel';
1112

1213
export type ConfigDependencies = {
@@ -16,11 +17,13 @@ export type ConfigDependencies = {
1617
logEventListeners: LogEventListener[];
1718
progressEventListeners: ProgressEventListener[];
1819
writer?: ConfigWriter;
20+
actionSummaryViewer: ConfigActionSummaryViewer;
1921
viewer: ConfigViewer;
2022
};
2123

2224
export type ConfigInput = {
2325
'config-file'?: string;
26+
'output-file'?: string;
2427
'rule-selector': string[];
2528
workspace?: string[];
2629
};
@@ -38,7 +41,8 @@ export class ConfigAction {
3841
const defaultConfig: CodeAnalyzerConfig = CodeAnalyzerConfig.withDefaults();
3942

4043
// We always add a Logger Listener to the appropriate listeners list, because we should always be logging.
41-
const logEventLogger: LogEventLogger = new LogEventLogger(await LogFileWriter.fromConfig(userConfig));
44+
const logFileWriter: LogFileWriter = await LogFileWriter.fromConfig(userConfig);
45+
const logEventLogger: LogEventLogger = new LogEventLogger(logFileWriter);
4246
this.dependencies.logEventListeners.push(logEventLogger);
4347

4448
// The User's config produces one Core.
@@ -118,7 +122,10 @@ export class ConfigAction {
118122
const configModel: ConfigModel = this.dependencies.modelGenerator(relevantEngines, userConfigContext, defaultConfigContext);
119123

120124
this.dependencies.viewer.view(configModel);
121-
await this.dependencies.writer?.write(configModel);
125+
const fileWritten: boolean = this.dependencies.writer
126+
? await this.dependencies.writer.write(configModel)
127+
: false;
128+
this.dependencies.actionSummaryViewer.view(logFileWriter.getLogDestination(), fileWritten ? input['output-file'] : undefined);
122129
return Promise.resolve();
123130
}
124131

src/lib/messages.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {Tokens} from '@salesforce/core/lib/messages';
55
Messages.importMessagesDirectory(__dirname);
66

77
export enum BundleName {
8+
ActionSummaryViewer = 'action-summary-viewer',
89
ConfigCommand = 'config-command',
910
ConfigModel = 'config-model',
1011
ConfigWriter = 'config-writer',
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import {Display} from '../Display';
2+
import {toStyledHeader, indent} from '../utils/StylingUtil';
3+
import {BundleName, getMessage} from '../messages';
4+
5+
abstract class AbstractActionSummaryViewer {
6+
protected readonly display: Display;
7+
8+
protected constructor(display: Display) {
9+
this.display = display;
10+
}
11+
12+
protected displaySummaryHeader(): void {
13+
this.display.displayLog(toStyledHeader(getMessage(BundleName.ActionSummaryViewer, 'common.summary-header')));
14+
}
15+
16+
protected displayLineSeparator(): void {
17+
this.display.displayLog("");
18+
}
19+
20+
protected displayLogFileInfo(logFile: string): void {
21+
this.display.displayLog(getMessage(BundleName.ActionSummaryViewer, 'common.logfile-location'));
22+
this.display.displayLog(indent(logFile));
23+
}
24+
}
25+
26+
export class ConfigActionSummaryViewer extends AbstractActionSummaryViewer {
27+
public constructor(display: Display) {
28+
super(display);
29+
}
30+
31+
public view(logFile: string, outfile?: string): void {
32+
this.displaySummaryHeader();
33+
this.displayLineSeparator();
34+
35+
if (outfile) {
36+
this.displayOutfile(outfile);
37+
} else {
38+
this.display.displayLog(getMessage(BundleName.ActionSummaryViewer, 'config-action.no-outfiles'));
39+
}
40+
this.displayLineSeparator();
41+
42+
this.displayLogFileInfo(logFile);
43+
}
44+
45+
private displayOutfile(outfile: string): void {
46+
this.display.displayLog(getMessage(BundleName.ActionSummaryViewer, 'config-action.outfile-location'));
47+
this.display.displayLog(indent(outfile));
48+
}
49+
}

src/lib/writers/ConfigWriter.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import {Display} from '../Display';
66
import {exists} from '../utils/FileUtil';
77

88
export interface ConfigWriter {
9-
write(model: ConfigModel): Promise<void>;
9+
write(model: ConfigModel): Promise<boolean>;
1010
}
1111

1212
export class ConfigFileWriter implements ConfigWriter {
@@ -20,10 +20,13 @@ export class ConfigFileWriter implements ConfigWriter {
2020
this.display = display;
2121
}
2222

23-
public async write(model: ConfigModel): Promise<void> {
23+
public async write(model: ConfigModel): Promise<boolean> {
2424
// Only write to the file if it doesn't already exist, or if the user confirms that they want to overwrite it.
2525
if (!(await exists(this.file)) || await this.display.confirm(getMessage(BundleName.ConfigWriter, 'prompt.overwrite-existing-file', [this.file]))) {
2626
fs.writeFileSync(this.file, model.toFormattedOutput(this.format));
27+
return true;
28+
} else {
29+
return false;
2730
}
2831
}
2932

test/commands/code-analyzer/config.test.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,7 @@ describe('`code-analyzer config` tests', () => {
177177
expect(createActionSpy).toHaveBeenCalled();
178178
expect(fromFileSpy).toHaveBeenCalled();
179179
expect(receivedFile).toEqual(inputValue);
180+
expect(receivedActionInput).toHaveProperty('output-file', inputValue);
180181
});
181182

182183
it('Can be referenced by its shortname, -f', async () => {
@@ -186,6 +187,7 @@ describe('`code-analyzer config` tests', () => {
186187
expect(createActionSpy).toHaveBeenCalled();
187188
expect(fromFileSpy).toHaveBeenCalled();
188189
expect(receivedFile).toEqual(inputValue);
190+
expect(receivedActionInput).toHaveProperty('output-file', inputValue);
189191
});
190192

191193
it('Cannot be supplied multiple times', async () => {
@@ -201,6 +203,7 @@ describe('`code-analyzer config` tests', () => {
201203
expect(executeSpy).toHaveBeenCalled();
202204
expect(createActionSpy).toHaveBeenCalled();
203205
expect(fromFileSpy).not.toHaveBeenCalled();
206+
expect(receivedActionInput['output-file']).toBeUndefined();
204207
});
205208

206209
});
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
=== Summary
2+
3+
No output file was specified.
4+
5+
Additional log information written to:
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
=== Summary
2+
3+
Config written to:
4+
out-config.yml
5+
6+
Additional log information written to:

test/lib/actions/ConfigAction.test.ts

Lines changed: 73 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ import {CodeAnalyzerConfigFactory} from "../../../src/lib/factories/CodeAnalyzer
99
import {EnginePluginsFactory} from '../../../src/lib/factories/EnginePluginsFactory';
1010
import {ConfigAction, ConfigDependencies, ConfigInput} from '../../../src/lib/actions/ConfigAction';
1111
import {AnnotatedConfigModel} from '../../../src/lib/models/ConfigModel';
12-
import {ConfigStyledYamlViewer} from '../../../lib/lib/viewers/ConfigViewer';
12+
import {ConfigStyledYamlViewer} from '../../../src/lib/viewers/ConfigViewer';
13+
import {ConfigActionSummaryViewer} from '../../../src/lib/viewers/ActionSummaryViewer';
1314

1415
import {SpyConfigWriter} from '../../stubs/SpyConfigWriter';
1516
import {DisplayEventType, SpyDisplay} from '../../stubs/SpyDisplay';
@@ -36,6 +37,7 @@ describe('ConfigAction tests', () => {
3637
viewer: new ConfigStyledYamlViewer(spyDisplay),
3738
configFactory: new DefaultStubCodeAnalyzerConfigFactory(),
3839
modelGenerator: AnnotatedConfigModel.fromSelection,
40+
actionSummaryViewer: new ConfigActionSummaryViewer(spyDisplay),
3941
pluginsFactory: new StubEnginePluginFactory()
4042
};
4143
});
@@ -159,6 +161,7 @@ describe('ConfigAction tests', () => {
159161
viewer: new ConfigStyledYamlViewer(spyDisplay),
160162
configFactory: stubConfigFactory,
161163
modelGenerator: AnnotatedConfigModel.fromSelection,
164+
actionSummaryViewer: new ConfigActionSummaryViewer(spyDisplay),
162165
pluginsFactory: new StubEnginePluginFactory()
163166
};
164167
});
@@ -389,6 +392,7 @@ describe('ConfigAction tests', () => {
389392
viewer: new ConfigStyledYamlViewer(spyDisplay),
390393
configFactory: new DefaultStubCodeAnalyzerConfigFactory(),
391394
modelGenerator: AnnotatedConfigModel.fromSelection,
395+
actionSummaryViewer: new ConfigActionSummaryViewer(spyDisplay),
392396
pluginsFactory: new StubEnginePluginFactory()
393397
};
394398
});
@@ -406,6 +410,74 @@ describe('ConfigAction tests', () => {
406410
expect(spyWriter.getCallHistory()).toHaveLength(1);
407411
});
408412
});
413+
414+
describe('Summary generation', () => {
415+
beforeEach(() => {
416+
spyDisplay = new SpyDisplay();
417+
dependencies = {
418+
logEventListeners: [],
419+
progressEventListeners: [],
420+
viewer: new ConfigStyledYamlViewer(spyDisplay),
421+
configFactory: new DefaultStubCodeAnalyzerConfigFactory(),
422+
modelGenerator: AnnotatedConfigModel.fromSelection,
423+
actionSummaryViewer: new ConfigActionSummaryViewer(spyDisplay),
424+
pluginsFactory: new StubEnginePluginFactory()
425+
}
426+
});
427+
428+
it('When an Outfile is created, it is mentioned by the Summarizer', async () => {
429+
// ==== SETUP ====
430+
// Assign a Writer to the dependencies.
431+
dependencies.writer = new SpyConfigWriter(true);
432+
433+
// ==== TESTED BEHAVIOR ====
434+
// Invoke the action, specifying an outfile.
435+
const action = ConfigAction.createAction(dependencies);
436+
const input: ConfigInput = {
437+
'rule-selector': ['all'],
438+
'output-file': 'out-config.yml'
439+
};
440+
await action.execute(input);
441+
442+
// ==== ASSERTIONS ====
443+
const displayEvents = spyDisplay.getDisplayEvents();
444+
const displayedLogEvents = ansis.strip(displayEvents
445+
.filter(e => e.type === DisplayEventType.LOG)
446+
.map(e => e.data)
447+
.join('\n'));
448+
449+
const goldfileContents: string = await readGoldFile(path.join(PATH_TO_COMPARISON_DIR, 'action-summaries', 'outfile-created.txt.goldfile'));
450+
expect(displayedLogEvents).toContain(goldfileContents);
451+
});
452+
453+
it.each([
454+
{case: 'an Outfile is specified but not written', writer: new SpyConfigWriter(false), outfile: 'out-config.yml'},
455+
{case: 'an Outfile is not specified at all', writer: undefined, outfile: undefined}
456+
])('When $case, the Summarizer mentions no outfile', async ({writer, outfile}) => {
457+
// ==== SETUP ====
458+
// Add the specified Writer (or lack-of-Writer) to the dependencies.
459+
dependencies.writer = writer;
460+
461+
// ==== TESTED BEHAVIOR ====
462+
// Invoke the action, specifying an outfile (or lack of one).
463+
const action = ConfigAction.createAction(dependencies);
464+
const input: ConfigInput = {
465+
'rule-selector': ['all'],
466+
'output-file': outfile
467+
};
468+
await action.execute(input);
469+
470+
// ==== ASSERTIONS ====
471+
const displayEvents = spyDisplay.getDisplayEvents();
472+
const displayedLogEvents = ansis.strip(displayEvents
473+
.filter(e => e.type === DisplayEventType.LOG)
474+
.map(e => e.data)
475+
.join('\n'));
476+
477+
const goldfileContents: string = await readGoldFile(path.join(PATH_TO_COMPARISON_DIR, 'action-summaries', 'no-outfile-created.txt.goldfile'));
478+
expect(displayedLogEvents).toContain(goldfileContents);
479+
});
480+
})
409481
// ====== HELPER FUNCTIONS ======
410482

411483
async function readGoldFile(goldFilePath: string): Promise<string> {
@@ -425,7 +497,6 @@ describe('ConfigAction tests', () => {
425497

426498
// ==== OUTPUT PROCESSING ====
427499
const displayEvents = spyDisplay.getDisplayEvents();
428-
expect(displayEvents).toHaveLength(1);
429500
expect(displayEvents[0].type).toEqual(DisplayEventType.LOG);
430501
return ansis.strip(displayEvents[0].data);
431502
}

0 commit comments

Comments
 (0)