Skip to content

Commit a50809a

Browse files
committed
Merge branch 'main' into refactor-table-model
2 parents 71b9bed + 7b2834c commit a50809a

File tree

2 files changed

+189
-60
lines changed

2 files changed

+189
-60
lines changed

packages/core/src/lib/implementation/execute-plugin.ts

Lines changed: 46 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,11 @@ import {
1010
auditOutputsSchema,
1111
} from '@code-pushup/models';
1212
import {
13+
ProgressBar,
1314
getProgressBar,
1415
groupByStatus,
1516
logMultipleResults,
17+
pluralizeToken,
1618
} from '@code-pushup/utils';
1719
import { normalizeAuditOutputs } from '../normalize';
1820
import { executeRunnerConfig, executeRunnerFunction } from './runner';
@@ -22,7 +24,11 @@ import { executeRunnerConfig, executeRunnerFunction } from './runner';
2224
*/
2325
export class PluginOutputMissingAuditError extends Error {
2426
constructor(auditSlug: string) {
25-
super(`Audit metadata not found for slug ${auditSlug}`);
27+
super(
28+
`Audit metadata not present in plugin config. Missing slug: ${chalk.bold(
29+
auditSlug,
30+
)}`,
31+
);
2632
}
2733
}
2834

@@ -69,7 +75,11 @@ export async function executePlugin(
6975
const { audits: unvalidatedAuditOutputs, ...executionMeta } = runnerResult;
7076

7177
// validate auditOutputs
72-
const auditOutputs = auditOutputsSchema.parse(unvalidatedAuditOutputs);
78+
const result = auditOutputsSchema.safeParse(unvalidatedAuditOutputs);
79+
if (!result.success) {
80+
throw new Error(`Audit output is invalid: ${result.error.message}`);
81+
}
82+
const auditOutputs = result.data;
7383
auditOutputsCorrelateWithPluginOutput(auditOutputs, pluginConfigAudits);
7484

7585
const normalizedAuditOutputs = await normalizeAuditOutputs(auditOutputs);
@@ -95,6 +105,28 @@ export async function executePlugin(
95105
};
96106
}
97107

108+
const wrapProgress = async (
109+
pluginCfg: PluginConfig,
110+
steps: number,
111+
progressBar: ProgressBar | null,
112+
) => {
113+
progressBar?.updateTitle(`Executing ${chalk.bold(pluginCfg.title)}`);
114+
try {
115+
const pluginReport = await executePlugin(pluginCfg);
116+
progressBar?.incrementInSteps(steps);
117+
return pluginReport;
118+
} catch (error) {
119+
progressBar?.incrementInSteps(steps);
120+
throw new Error(
121+
error instanceof Error
122+
? `- Plugin ${chalk.bold(pluginCfg.title)} (${chalk.bold(
123+
pluginCfg.slug,
124+
)}) produced the following error:\n - ${error.message}`
125+
: String(error),
126+
);
127+
}
128+
};
129+
98130
/**
99131
* Execute multiple plugins and aggregates their output.
100132
* @public
@@ -124,21 +156,13 @@ export async function executePlugins(
124156

125157
const progressBar = progress ? getProgressBar('Run plugins') : null;
126158

127-
const pluginsResult = await plugins.reduce(async (acc, pluginCfg) => {
128-
progressBar?.updateTitle(`Executing ${chalk.bold(pluginCfg.title)}`);
129-
130-
try {
131-
const pluginReport = await executePlugin(pluginCfg);
132-
progressBar?.incrementInSteps(plugins.length);
133-
return [...(await acc), Promise.resolve(pluginReport)];
134-
} catch (error) {
135-
progressBar?.incrementInSteps(plugins.length);
136-
return [
137-
...(await acc),
138-
Promise.reject(error instanceof Error ? error.message : String(error)),
139-
];
140-
}
141-
}, Promise.resolve([] as Promise<PluginReport>[]));
159+
const pluginsResult = await plugins.reduce(
160+
async (acc, pluginCfg) => [
161+
...(await acc),
162+
wrapProgress(pluginCfg, plugins.length, progressBar),
163+
],
164+
Promise.resolve([] as Promise<PluginReport>[]),
165+
);
142166

143167
progressBar?.endProgress('Done running plugins');
144168

@@ -151,9 +175,12 @@ export async function executePlugins(
151175
if (rejected.length > 0) {
152176
const errorMessages = rejected
153177
.map(({ reason }) => String(reason))
154-
.join(', ');
178+
.join('\n');
155179
throw new Error(
156-
`Plugins failed: ${rejected.length} errors: ${errorMessages}`,
180+
`Executing ${pluralizeToken(
181+
'plugin',
182+
rejected.length,
183+
)} failed.\n\n${errorMessages}\n\n`,
157184
);
158185
}
159186

packages/core/src/lib/implementation/execute-plugin.unit.test.ts

Lines changed: 143 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
1+
import chalk from 'chalk';
12
import { vol } from 'memfs';
2-
import { describe, expect, it, vi } from 'vitest';
3+
import { describe, expect, it } from 'vitest';
34
import { AuditOutputs, PluginConfig } from '@code-pushup/models';
45
import {
56
MEMFS_VOLUME,
67
MINIMAL_PLUGIN_CONFIG_MOCK,
7-
getLogMessages,
88
} from '@code-pushup/test-utils';
9-
import { ui } from '@code-pushup/utils';
109
import {
1110
PluginOutputMissingAuditError,
1211
executePlugin,
@@ -65,7 +64,7 @@ describe('executePlugin', () => {
6564
]);
6665
});
6766

68-
it('should throw when plugin slug is invalid', async () => {
67+
it('should throw when audit slug is invalid', async () => {
6968
await expect(() =>
7069
executePlugin({
7170
...MINIMAL_PLUGIN_CONFIG_MOCK,
@@ -74,19 +73,24 @@ describe('executePlugin', () => {
7473
).rejects.toThrow(new PluginOutputMissingAuditError('node-version'));
7574
});
7675

77-
it('should throw if invalid runnerOutput is produced', async () => {
76+
it('should throw for missing audit', async () => {
77+
const missingSlug = 'missing-audit-slug';
7878
await expect(() =>
7979
executePlugin({
8080
...MINIMAL_PLUGIN_CONFIG_MOCK,
8181
runner: () => [
8282
{
83-
slug: '-invalid-audit-slug',
83+
slug: missingSlug,
8484
score: 0,
8585
value: 0,
8686
},
8787
],
8888
}),
89-
).rejects.toThrow('The slug has to follow the pattern');
89+
).rejects.toThrow(
90+
`Audit metadata not present in plugin config. Missing slug: ${chalk.bold(
91+
missingSlug,
92+
)}`,
93+
);
9094
});
9195
});
9296

@@ -108,55 +112,153 @@ describe('executePlugins', () => {
108112
expect(pluginResult[0]?.audits[0]?.slug).toBe('node-version');
109113
});
110114

111-
it('should throw for invalid plugins', async () => {
115+
it('should throw for invalid audit output', async () => {
116+
const slug = 'simulate-invalid-audit-slug';
117+
const title = 'Simulate an invalid audit slug in outputs';
112118
await expect(() =>
113119
executePlugins(
114120
[
115-
MINIMAL_PLUGIN_CONFIG_MOCK,
116121
{
117122
...MINIMAL_PLUGIN_CONFIG_MOCK,
118-
audits: [{ slug: '-invalid-slug', title: 'Invalid audit' }],
123+
slug,
124+
title,
125+
runner: () => [
126+
{
127+
slug: 'invalid-audit-slug-',
128+
score: 0.3,
129+
value: 16,
130+
displayValue: '16.0.0',
131+
},
132+
],
119133
},
120134
] satisfies PluginConfig[],
121135
{ progress: false },
122136
),
123-
).rejects.toThrow(
124-
/Plugins failed: 1 errors:.*Audit metadata not found for slug node-version/,
125-
);
137+
).rejects
138+
.toThrow(`Executing 1 plugin failed.\n\nError: - Plugin ${chalk.bold(
139+
title,
140+
)} (${chalk.bold(slug)}) produced the following error:
141+
- Audit output is invalid: [
142+
{
143+
"validation": "regex",
144+
"code": "invalid_string",
145+
"message": "The slug has to follow the pattern [0-9a-z] followed by multiple optional groups of -[0-9a-z]. e.g. my-slug",
146+
"path": [
147+
0,
148+
"slug"
149+
]
150+
}
151+
]
152+
`);
126153
});
127154

128-
it('should print invalid plugin errors and throw', async () => {
129-
const pluginConfig = {
130-
...MINIMAL_PLUGIN_CONFIG_MOCK,
131-
runner: vi
132-
.fn()
133-
.mockRejectedValue('Audit metadata not found for slug node-version'),
134-
};
135-
const pluginConfig2 = {
136-
...MINIMAL_PLUGIN_CONFIG_MOCK,
137-
runner: vi.fn().mockResolvedValue([]),
138-
};
139-
const pluginConfig3 = {
140-
...MINIMAL_PLUGIN_CONFIG_MOCK,
141-
runner: vi.fn().mockRejectedValue('plugin 3 error'),
142-
};
155+
it('should throw for one failing plugin', async () => {
156+
const missingAuditSlug = 'missing-audit-slug';
157+
await expect(() =>
158+
executePlugins(
159+
[
160+
{
161+
...MINIMAL_PLUGIN_CONFIG_MOCK,
162+
slug: 'plg1',
163+
title: 'plg1',
164+
runner: () => [
165+
{
166+
slug: `${missingAuditSlug}-a`,
167+
score: 0.3,
168+
value: 16,
169+
displayValue: '16.0.0',
170+
},
171+
],
172+
},
173+
] satisfies PluginConfig[],
174+
{ progress: false },
175+
),
176+
).rejects.toThrow('Executing 1 plugin failed.\n\n');
177+
});
143178

179+
it('should throw for multiple failing plugins', async () => {
180+
const missingAuditSlug = 'missing-audit-slug';
144181
await expect(() =>
145-
executePlugins([pluginConfig, pluginConfig2, pluginConfig3], {
146-
progress: false,
147-
}),
182+
executePlugins(
183+
[
184+
{
185+
...MINIMAL_PLUGIN_CONFIG_MOCK,
186+
slug: 'plg1',
187+
title: 'plg1',
188+
runner: () => [
189+
{
190+
slug: `${missingAuditSlug}-a`,
191+
score: 0.3,
192+
value: 16,
193+
displayValue: '16.0.0',
194+
},
195+
],
196+
},
197+
{
198+
...MINIMAL_PLUGIN_CONFIG_MOCK,
199+
slug: 'plg2',
200+
title: 'plg2',
201+
runner: () => [
202+
{
203+
slug: `${missingAuditSlug}-b`,
204+
score: 0.3,
205+
value: 16,
206+
displayValue: '16.0.0',
207+
},
208+
],
209+
},
210+
] satisfies PluginConfig[],
211+
{ progress: false },
212+
),
213+
).rejects.toThrow('Executing 2 plugins failed.\n\n');
214+
});
215+
216+
it('should throw with indentation in message', async () => {
217+
const missingAuditSlug = 'missing-audit-slug';
218+
219+
await expect(() =>
220+
executePlugins(
221+
[
222+
{
223+
...MINIMAL_PLUGIN_CONFIG_MOCK,
224+
slug: 'plg1',
225+
title: 'plg1',
226+
runner: () => [
227+
{
228+
slug: `${missingAuditSlug}-a`,
229+
score: 0.3,
230+
value: 16,
231+
displayValue: '16.0.0',
232+
},
233+
],
234+
},
235+
{
236+
...MINIMAL_PLUGIN_CONFIG_MOCK,
237+
slug: 'plg2',
238+
title: 'plg2',
239+
runner: () => [
240+
{
241+
slug: `${missingAuditSlug}-b`,
242+
score: 0.3,
243+
value: 16,
244+
displayValue: '16.0.0',
245+
},
246+
],
247+
},
248+
] satisfies PluginConfig[],
249+
{ progress: false },
250+
),
148251
).rejects.toThrow(
149-
'Plugins failed: 2 errors: Audit metadata not found for slug node-version, plugin 3 error',
252+
`Error: - Plugin ${chalk.bold('plg1')} (${chalk.bold(
253+
'plg1',
254+
)}) produced the following error:\n - Audit metadata not present in plugin config. Missing slug: ${chalk.bold(
255+
'missing-audit-slug-a',
256+
)}\nError: - Plugin ${chalk.bold('plg2')} (${chalk.bold(
257+
'plg2',
258+
)}) produced the following error:\n - Audit metadata not present in plugin config. Missing slug: ${chalk.bold(
259+
'missing-audit-slug-b',
260+
)}`,
150261
);
151-
const logs = getLogMessages(ui().logger);
152-
expect(logs[0]).toBe('[ yellow(warn) ] Plugins failed: ');
153-
expect(logs[1]).toBe(
154-
'[ yellow(warn) ] Audit metadata not found for slug node-version',
155-
);
156-
157-
expect(pluginConfig.runner).toHaveBeenCalled();
158-
expect(pluginConfig2.runner).toHaveBeenCalled();
159-
expect(pluginConfig3.runner).toHaveBeenCalled();
160262
});
161263

162264
it('should use outputTransform if provided', async () => {

0 commit comments

Comments
 (0)