Skip to content

Commit cebd1e9

Browse files
authored
Merge pull request #1666 from forcedotcom/d/W-17052953
CHANGE @W-17052953@ Polished output for rules command.
2 parents 69a623b + f755f67 commit cebd1e9

File tree

10 files changed

+124
-6
lines changed

10 files changed

+124
-6
lines changed

messages/action-summary-viewer.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,15 @@ Additional log information written to:
99
# config-action.outfile-location
1010

1111
Configuration written to:
12+
13+
# rules-action.found-no-rules
14+
15+
Found 0 rules.
16+
17+
# rules-action.rules-total
18+
19+
Found %d rule(s) from %d engine(s):
20+
21+
# rules-action.rules-item
22+
23+
%d %s rule(s) found.

messages/progress-event-listener.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ Selecting rules
55
Eligible engines: %s; Completion: %d%; Elapsed time: %ds
66

77
# selection-spinner.finished-status
8-
done. Selected rules from %s.
8+
done.
99

1010
# execution-spinner.action
1111
Executing rules

src/commands/code-analyzer/rules.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {View} from '../../Constants';
33
import {CodeAnalyzerConfigFactoryImpl} from '../../lib/factories/CodeAnalyzerConfigFactory';
44
import {EnginePluginsFactoryImpl} from '../../lib/factories/EnginePluginsFactory';
55
import {RuleDetailDisplayer, RuleTableDisplayer} from '../../lib/viewers/RuleViewer';
6+
import {RulesActionSummaryViewer} from '../../lib/viewers/ActionSummaryViewer';
67
import {RulesAction, RulesDependencies} from '../../lib/actions/RulesAction';
78
import {BundleName, getMessage, getMessages} from '../../lib/messages';
89
import {Displayable, UxDisplay} from '../../lib/Display';
@@ -67,6 +68,7 @@ export default class RulesCommand extends SfCommand<void> implements Displayable
6768
pluginsFactory: new EnginePluginsFactoryImpl(),
6869
logEventListeners: [new LogEventDisplayer(uxDisplay)],
6970
progressListeners: [new RuleSelectionProgressSpinner(uxDisplay)],
71+
actionSummaryViewer: new RulesActionSummaryViewer(uxDisplay),
7072
viewer: view === View.TABLE ? new RuleTableDisplayer(uxDisplay) : new RuleDetailDisplayer(uxDisplay)
7173
};
7274
}

src/lib/actions/RulesAction.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,14 @@ import {ProgressEventListener} from '../listeners/ProgressEventListener';
66
import {LogFileWriter} from '../writers/LogWriter';
77
import {LogEventListener, LogEventLogger} from '../listeners/LogEventListener';
88
import {RuleViewer} from '../viewers/RuleViewer';
9+
import {RulesActionSummaryViewer} from '../viewers/ActionSummaryViewer';
910

1011
export type RulesDependencies = {
1112
configFactory: CodeAnalyzerConfigFactory;
1213
pluginsFactory: EnginePluginsFactory;
1314
logEventListeners: LogEventListener[];
1415
progressListeners: ProgressEventListener[];
16+
actionSummaryViewer: RulesActionSummaryViewer,
1517
viewer: RuleViewer;
1618
}
1719

@@ -58,6 +60,7 @@ export class RulesAction {
5860
const rules: Rule[] = core.getEngineNames().flatMap(name => ruleSelection.getRulesFor(name));
5961

6062
this.dependencies.viewer.view(rules);
63+
this.dependencies.actionSummaryViewer.view(ruleSelection, logWriter.getLogDestination());
6164
}
6265

6366
public static createAction(dependencies: RulesDependencies): RulesAction {

src/lib/listeners/ProgressEventListener.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -138,8 +138,7 @@ export class RuleSelectionProgressSpinner extends ProgressSpinner implements Pro
138138
}
139139

140140
protected createFinishedSpinnerStatus(): string {
141-
return getMessage(BundleName.ProgressEventListener, 'selection-spinner.finished-status',
142-
[[...this.engineNames.keys()].join(', ')]);
141+
return getMessage(BundleName.ProgressEventListener, 'selection-spinner.finished-status');
143142
}
144143
}
145144

src/lib/viewers/ActionSummaryViewer.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import {Display} from '../Display';
2+
import {RuleSelection} from '@salesforce/code-analyzer-core';
23
import {toStyledHeader, indent} from '../utils/StylingUtil';
34
import {BundleName, getMessage} from '../messages';
45

@@ -45,3 +46,35 @@ export class ConfigActionSummaryViewer extends AbstractActionSummaryViewer {
4546
this.display.displayLog(indent(outfile));
4647
}
4748
}
49+
50+
export class RulesActionSummaryViewer extends AbstractActionSummaryViewer {
51+
public constructor(display: Display) {
52+
super(display);
53+
}
54+
55+
public view(ruleSelection: RuleSelection, logFile: string): void {
56+
// Start with separator to cleanly break from anything that's already been logged.
57+
this.displayLineSeparator();
58+
this.displaySummaryHeader();
59+
this.displayLineSeparator();
60+
61+
if (ruleSelection.getCount() === 0) {
62+
this.display.displayLog(getMessage(BundleName.ActionSummaryViewer, 'rules-action.found-no-rules'));
63+
} else {
64+
this.displayRuleSelection(ruleSelection);
65+
}
66+
this.displayLineSeparator();
67+
68+
this.displayLogFileInfo(logFile);
69+
}
70+
71+
private displayRuleSelection(ruleSelection: RuleSelection): void {
72+
this.display.displayLog(getMessage(BundleName.ActionSummaryViewer, 'rules-action.rules-total', [ruleSelection.getCount(), ruleSelection.getEngineNames().length]));
73+
for (const engineName of ruleSelection.getEngineNames()) {
74+
const ruleCountForEngine: number = ruleSelection.getRulesFor(engineName).length;
75+
this.display.displayLog(indent(getMessage(BundleName.ActionSummaryViewer, 'rules-action.rules-item', [ruleCountForEngine, engineName])));
76+
}
77+
}
78+
79+
80+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
2+
=== Summary
3+
4+
Found 0 rules.
5+
6+
Additional log information written to:
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
2+
=== Summary
3+
4+
Found 5 rule(s) from 2 engine(s):
5+
3 stubEngine1 rule(s) found.
6+
2 stubEngine2 rule(s) found.
7+
8+
Additional log information written to:

test/lib/actions/RulesAction.test.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,14 @@
11
import * as path from 'node:path';
2+
import * as fsp from 'node:fs/promises';
3+
import ansis from 'ansis';
24
import {RulesAction, RulesDependencies, RulesInput} from '../../../src/lib/actions/RulesAction';
5+
import {RulesActionSummaryViewer} from '../../../src/lib/viewers/ActionSummaryViewer';
36
import {StubDefaultConfigFactory} from '../../stubs/StubCodeAnalyzerConfigFactories';
47
import * as StubEnginePluginFactories from '../../stubs/StubEnginePluginsFactories';
58
import {SpyRuleViewer} from '../../stubs/SpyRuleViewer';
9+
import {DisplayEventType, SpyDisplay} from '../../stubs/SpyDisplay';
10+
11+
const PATH_TO_GOLDFILES = path.join(__dirname, '..', '..', 'fixtures', 'comparison-files', 'lib', 'actions', 'RulesAction.test.ts');
612

713
describe('RulesAction tests', () => {
814
let viewer: SpyRuleViewer;
@@ -12,11 +18,14 @@ describe('RulesAction tests', () => {
1218
})
1319

1420
it('Submitting the all-selector returns all rules', async () => {
21+
const spyDisplay: SpyDisplay = new SpyDisplay();
22+
const actionSummaryViewer: RulesActionSummaryViewer = new RulesActionSummaryViewer(spyDisplay);
1523
const dependencies: RulesDependencies = {
1624
configFactory: new StubDefaultConfigFactory(),
1725
pluginsFactory: new StubEnginePluginFactories.StubEnginePluginsFactory_withFunctionalStubEngine(),
1826
logEventListeners: [],
1927
progressListeners: [],
28+
actionSummaryViewer,
2029
viewer
2130
};
2231
const action = RulesAction.createAction(dependencies);
@@ -41,11 +50,14 @@ describe('RulesAction tests', () => {
4150
});
4251

4352
it('Submitting a filtering selector returns only matching rules', async () => {
53+
const spyDisplay: SpyDisplay = new SpyDisplay();
54+
const actionSummaryViewer: RulesActionSummaryViewer = new RulesActionSummaryViewer(spyDisplay);
4455
const dependencies: RulesDependencies = {
4556
configFactory: new StubDefaultConfigFactory(),
4657
pluginsFactory: new StubEnginePluginFactories.StubEnginePluginsFactory_withFunctionalStubEngine(),
4758
logEventListeners: [],
4859
progressListeners: [],
60+
actionSummaryViewer,
4961
viewer
5062
};
5163
const action = RulesAction.createAction(dependencies);
@@ -64,12 +76,15 @@ describe('RulesAction tests', () => {
6476
});
6577

6678
it('Engines with target-dependent rules return the right rules', async () => {
79+
const spyDisplay: SpyDisplay = new SpyDisplay();
80+
const actionSummaryViewer: RulesActionSummaryViewer = new RulesActionSummaryViewer(spyDisplay);
6781
const dependencies: RulesDependencies = {
6882
configFactory: new StubDefaultConfigFactory(),
6983
// The engine we're using here will synthesize one rule per target.
7084
pluginsFactory: new StubEnginePluginFactories.StubEnginePluginsFactory_withTargetDependentStubEngine(),
7185
logEventListeners: [],
7286
progressListeners: [],
87+
actionSummaryViewer,
7388
viewer
7489
};
7590
const targetedFilesAndFolders = ['package.json', 'src', 'README.md'];
@@ -98,11 +113,14 @@ describe('RulesAction tests', () => {
98113
* test will help us do that.
99114
*/
100115
it('When no engines are registered, empty results are displayed', async () => {
116+
const spyDisplay: SpyDisplay = new SpyDisplay();
117+
const actionSummaryViewer: RulesActionSummaryViewer = new RulesActionSummaryViewer(spyDisplay);
101118
const dependencies: RulesDependencies = {
102119
configFactory: new StubDefaultConfigFactory(),
103120
pluginsFactory: new StubEnginePluginFactories.StubEnginePluginsFactory_withNoPlugins(),
104121
logEventListeners: [],
105122
progressListeners: [],
123+
actionSummaryViewer,
106124
viewer
107125
};
108126
const action = RulesAction.createAction(dependencies);
@@ -118,11 +136,14 @@ describe('RulesAction tests', () => {
118136
});
119137

120138
it('Throws an error when an engine throws an error', async () => {
139+
const spyDisplay: SpyDisplay = new SpyDisplay();
140+
const actionSummaryViewer: RulesActionSummaryViewer = new RulesActionSummaryViewer(spyDisplay);
121141
const dependencies: RulesDependencies = {
122142
configFactory: new StubDefaultConfigFactory(),
123143
pluginsFactory: new StubEnginePluginFactories.StubEnginePluginsFactory_withThrowingStubPlugin(),
124144
logEventListeners: [],
125145
progressListeners: [],
146+
actionSummaryViewer,
126147
viewer
127148
};
128149
const action = RulesAction.createAction(dependencies);
@@ -133,6 +154,40 @@ describe('RulesAction tests', () => {
133154

134155
await expect(executionPromise).rejects.toThrow('SomeErrorFromGetAvailableEngineNames');
135156
});
157+
158+
describe('Summary generation', () => {
159+
it.each([
160+
{quantifier: 'no', expectation: 'Summary indicates absence of rules', selector: 'NonsensicalTag', goldfile: 'no-rules.txt.goldfile'},
161+
{quantifier: 'some', expectation: 'Summary provides breakdown by engine', selector: 'Recommended', goldfile: 'some-rules.txt.goldfile'}
162+
])('When $quantifier rules are returned, $expectation', async ({selector, goldfile}) => {
163+
const goldfilePath: string = path.join(PATH_TO_GOLDFILES, 'action-summaries', goldfile);
164+
const spyDisplay: SpyDisplay = new SpyDisplay();
165+
const actionSummaryViewer: RulesActionSummaryViewer = new RulesActionSummaryViewer(spyDisplay);
166+
const dependencies: RulesDependencies = {
167+
configFactory: new StubDefaultConfigFactory(),
168+
pluginsFactory: new StubEnginePluginFactories.StubEnginePluginsFactory_withFunctionalStubEngine(),
169+
logEventListeners: [],
170+
progressListeners: [],
171+
actionSummaryViewer,
172+
viewer
173+
};
174+
const action = RulesAction.createAction(dependencies);
175+
const input: RulesInput = {
176+
'rule-selector': [selector]
177+
};
178+
179+
await action.execute(input);
180+
181+
const displayEvents = spyDisplay.getDisplayEvents();
182+
const displayedLogEvents = ansis.strip(displayEvents
183+
.filter(e => e.type === DisplayEventType.LOG)
184+
.map(e => e.data)
185+
.join('\n'));
186+
187+
const goldfileContents: string = await fsp.readFile(goldfilePath, 'utf-8');
188+
expect(displayedLogEvents).toContain(goldfileContents);
189+
});
190+
});
136191
});
137192

138193
// TODO: Whenever we decide to document the custom_engine_plugin_modules flag in our configuration file, then we'll want

test/lib/listeners/ProgressEventListener.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ describe('ProgressEventListener implementations', () => {
8787
expect(percentagesInOrder).toEqual([0, 25, 50, 100]);
8888
const endEvent = displayEvents[displayEvents.length - 1];
8989
expect(endEvent).toHaveProperty('type', DisplayEventType.SPINNER_STOP);
90-
expect(endEvent.data).toContain(`done. Selected rules from stubEngine1, stubEngine2.`);
90+
expect(endEvent.data).toEqual(`done.`);
9191
});
9292

9393
it('Properly aggregates percentages across multiple Cores', async () => {
@@ -123,7 +123,7 @@ describe('ProgressEventListener implementations', () => {
123123
expect(percentagesInOrder).toEqual([0, 12, 25, 50, 62, 75, 100]);
124124
const endEvent = displayEvents[displayEvents.length - 1];
125125
expect(endEvent).toHaveProperty('type', DisplayEventType.SPINNER_STOP);
126-
expect(endEvent.data).toContain('done. Selected rules from stubEngine1, stubEngine2.');
126+
expect(endEvent.data).toEqual('done.');
127127
});
128128

129129
it('Properly interleaves progress updates with ticking', async () => {
@@ -159,7 +159,7 @@ describe('ProgressEventListener implementations', () => {
159159
expect(percentagesInOrder).toEqual([0, 20, 40, 50, 60, 80, 100]);
160160
const endEvent = displayEvents[displayEvents.length - 1];
161161
expect(endEvent).toHaveProperty('type', DisplayEventType.SPINNER_STOP);
162-
expect(endEvent.data).toContain('done. Selected rules from timeableEngine1, timeableEngine2.');
162+
expect(endEvent.data).toEqual('done.');
163163
});
164164
});
165165

0 commit comments

Comments
 (0)