Skip to content

Commit 7daf201

Browse files
feat: add audit output caching for execute plugin (#1057)
This PR includes: - add `utils` for runner result caching - add cache options to `executePlugin` and `executePlugins` - add `CacheConfig` to `models` package and use it in `core` package Related: #1048 --------- Co-authored-by: Matěj Chalk <34691111+matejchalk@users.noreply.github.com>
1 parent f5f00bc commit 7daf201

File tree

11 files changed

+692
-256
lines changed

11 files changed

+692
-256
lines changed

examples/plugins/src/file-size/src/file-size.plugin.int.test.ts

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,12 @@ describe('create', () => {
6363

6464
it('should return PluginConfig that executes correctly', async () => {
6565
const pluginConfig = create(baseOptions);
66-
await expect(executePlugin(pluginConfig)).resolves.toMatchObject({
66+
await expect(
67+
executePlugin(pluginConfig, {
68+
persist: { outputDir: '.code-pushup' },
69+
cache: { read: false, write: false },
70+
}),
71+
).resolves.toMatchObject({
6772
description:
6873
'A plugin to measure and assert size of files in a directory.',
6974
slug,
@@ -79,7 +84,10 @@ describe('create', () => {
7984
...baseOptions,
8085
pattern: /\.js$/,
8186
});
82-
const { audits: auditOutputs } = await executePlugin(pluginConfig);
87+
const { audits: auditOutputs } = await executePlugin(pluginConfig, {
88+
persist: { outputDir: '.code-pushup' },
89+
cache: { read: false, write: false },
90+
});
8391

8492
expect(auditOutputs).toHaveLength(1);
8593
expect(auditOutputs[0]?.score).toBe(1);
@@ -91,7 +99,10 @@ describe('create', () => {
9199
...baseOptions,
92100
budget: 0,
93101
});
94-
const { audits: auditOutputs } = await executePlugin(pluginConfig);
102+
const { audits: auditOutputs } = await executePlugin(pluginConfig, {
103+
persist: { outputDir: '.code-pushup' },
104+
cache: { read: false, write: false },
105+
});
95106

96107
expect(auditOutputs).toHaveLength(1);
97108
expect(auditOutputs[0]?.score).toBe(0);

examples/plugins/src/package-json/src/package-json.plugin.int.test.ts

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,10 @@ describe('create-package-json', () => {
4747

4848
it('should return PluginConfig that executes correctly', async () => {
4949
const pluginConfig = create(baseOptions);
50-
const pluginOutput = await executePlugin(pluginConfig);
50+
const pluginOutput = await executePlugin(pluginConfig, {
51+
persist: { outputDir: '.code-pushup' },
52+
cache: { read: false, write: false },
53+
});
5154

5255
expect(() => pluginReportSchema.parse(pluginOutput)).not.toThrow();
5356
expect(pluginOutput).toMatchObject(
@@ -68,7 +71,10 @@ describe('create-package-json', () => {
6871
...baseOptions,
6972
license: 'MIT',
7073
});
71-
const { audits: auditOutputs } = await executePlugin(pluginConfig);
74+
const { audits: auditOutputs } = await executePlugin(pluginConfig, {
75+
persist: { outputDir: '.code-pushup' },
76+
cache: { read: false, write: false },
77+
});
7278

7379
expect(auditOutputs[0]?.value).toBe(1);
7480
expect(auditOutputs[0]?.score).toBe(0);
@@ -85,7 +91,10 @@ describe('create-package-json', () => {
8591
...baseOptions,
8692
type: 'module',
8793
});
88-
const { audits: auditOutputs } = await executePlugin(pluginConfig);
94+
const { audits: auditOutputs } = await executePlugin(pluginConfig, {
95+
persist: { outputDir: '.code-pushup' },
96+
cache: { read: false, write: false },
97+
});
8998

9099
expect(auditOutputs[1]?.slug).toBe('package-type');
91100
expect(auditOutputs[1]?.score).toBe(0);
@@ -104,7 +113,10 @@ describe('create-package-json', () => {
104113
test: '0',
105114
},
106115
});
107-
const { audits: auditOutputs } = await executePlugin(pluginConfig);
116+
const { audits: auditOutputs } = await executePlugin(pluginConfig, {
117+
persist: { outputDir: '.code-pushup' },
118+
cache: { read: false, write: false },
119+
});
108120

109121
expect(auditOutputs).toHaveLength(audits.length);
110122
expect(auditOutputs[2]?.slug).toBe('package-dependencies');
@@ -125,7 +137,10 @@ describe('create-package-json', () => {
125137
test: '0',
126138
},
127139
});
128-
const { audits: auditOutputs } = await executePlugin(pluginConfig);
140+
const { audits: auditOutputs } = await executePlugin(pluginConfig, {
141+
persist: { outputDir: '.code-pushup' },
142+
cache: { read: false, write: false },
143+
});
129144

130145
expect(auditOutputs).toHaveLength(audits.length);
131146
expect(auditOutputs[2]?.score).toBe(0);
@@ -146,7 +161,10 @@ describe('create-package-json', () => {
146161
test: '0',
147162
},
148163
});
149-
const { audits: auditOutputs } = await executePlugin(pluginConfig);
164+
const { audits: auditOutputs } = await executePlugin(pluginConfig, {
165+
persist: { outputDir: '.code-pushup' },
166+
cache: { read: false, write: false },
167+
});
150168

151169
expect(auditOutputs).toHaveLength(audits.length);
152170
expect(auditOutputs[2]?.score).toBe(0);

packages/core/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ export type { ReportsToCompare } from './lib/implementation/compare-scorables.js
1717
export {
1818
executePlugin,
1919
executePlugins,
20-
PluginOutputMissingAuditError,
2120
} from './lib/implementation/execute-plugin.js';
21+
export { AuditOutputsMissingAuditError } from './lib/implementation/runner.js';
2222
export {
2323
PersistDirError,
2424
PersistError,

packages/core/src/lib/implementation/collect.ts

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,36 @@
11
import { createRequire } from 'node:module';
2-
import type { CoreConfig, Report } from '@code-pushup/models';
2+
import {
3+
type CoreConfig,
4+
DEFAULT_PERSIST_OUTPUT_DIR,
5+
type PersistConfig,
6+
type Report,
7+
} from '@code-pushup/models';
38
import { calcDuration, getLatestCommit } from '@code-pushup/utils';
49
import type { GlobalOptions } from '../types.js';
510
import { executePlugins } from './execute-plugin.js';
611

7-
export type CollectOptions = Pick<CoreConfig, 'plugins' | 'categories'> &
8-
Partial<GlobalOptions>;
12+
export type CollectOptions = Pick<CoreConfig, 'plugins' | 'categories'> & {
13+
persist?: Required<Pick<PersistConfig, 'outputDir'>>;
14+
} & Partial<GlobalOptions>;
915

1016
/**
1117
* Run audits, collect plugin output and aggregate it into a JSON object
1218
* @param options
1319
*/
1420
export async function collect(options: CollectOptions): Promise<Report> {
15-
const { plugins, categories } = options;
21+
const { plugins, categories, persist, ...otherOptions } = options;
1622
const date = new Date().toISOString();
1723
const start = performance.now();
1824
const commit = await getLatestCommit();
19-
const pluginOutputs = await executePlugins(plugins, options);
25+
const pluginOutputs = await executePlugins(
26+
{
27+
plugins,
28+
persist: { outputDir: DEFAULT_PERSIST_OUTPUT_DIR, ...persist },
29+
// implement together with CLI option
30+
cache: { read: false, write: false },
31+
},
32+
otherOptions,
33+
);
2034
const packageJson = createRequire(import.meta.url)(
2135
'../../../package.json',
2236
) as typeof import('../../../package.json');

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

Lines changed: 55 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import { bold } from 'ansis';
2-
import {
3-
type Audit,
4-
type AuditOutput,
5-
type AuditOutputs,
6-
type AuditReport,
7-
type PluginConfig,
8-
type PluginReport,
9-
auditOutputsSchema,
2+
import type {
3+
Audit,
4+
AuditOutput,
5+
AuditReport,
6+
CacheConfigObject,
7+
PersistConfig,
8+
PluginConfig,
9+
PluginReport,
1010
} from '@code-pushup/models';
1111
import {
1212
type ProgressBar,
@@ -15,29 +15,20 @@ import {
1515
logMultipleResults,
1616
pluralizeToken,
1717
} from '@code-pushup/utils';
18-
import { normalizeAuditOutputs } from '../normalize.js';
19-
import { executeRunnerConfig, executeRunnerFunction } from './runner.js';
20-
21-
/**
22-
* Error thrown when plugin output is invalid.
23-
*/
24-
export class PluginOutputMissingAuditError extends Error {
25-
constructor(auditSlug: string) {
26-
super(
27-
`Audit metadata not present in plugin config. Missing slug: ${bold(
28-
auditSlug,
29-
)}`,
30-
);
31-
}
32-
}
18+
import {
19+
executePluginRunner,
20+
readRunnerResults,
21+
writeRunnerResults,
22+
} from './runner.js';
3323

3424
/**
3525
* Execute a plugin.
3626
*
3727
* @public
3828
* @param pluginConfig - {@link ProcessConfig} object with runner and meta
29+
* @param opt
3930
* @returns {Promise<AuditOutput[]>} - audit outputs from plugin runner
40-
* @throws {PluginOutputMissingAuditError} - if plugin runner output is invalid
31+
* @throws {AuditOutputsMissingAuditError} - if plugin runner output is invalid
4132
*
4233
* @example
4334
* // plugin execution
@@ -54,7 +45,12 @@ export class PluginOutputMissingAuditError extends Error {
5445
*/
5546
export async function executePlugin(
5647
pluginConfig: PluginConfig,
48+
opt: {
49+
cache: CacheConfigObject;
50+
persist: Required<Pick<PersistConfig, 'outputDir'>>;
51+
},
5752
): Promise<PluginReport> {
53+
const { cache, persist } = opt;
5854
const {
5955
runner,
6056
audits: pluginConfigAudits,
@@ -63,26 +59,25 @@ export async function executePlugin(
6359
groups,
6460
...pluginMeta
6561
} = pluginConfig;
66-
67-
// execute plugin runner
68-
const runnerResult =
69-
typeof runner === 'object'
70-
? await executeRunnerConfig(runner)
71-
: await executeRunnerFunction(runner);
72-
const { audits: unvalidatedAuditOutputs, ...executionMeta } = runnerResult;
73-
74-
// validate auditOutputs
75-
const result = auditOutputsSchema.safeParse(unvalidatedAuditOutputs);
76-
if (!result.success) {
77-
throw new Error(`Audit output is invalid: ${result.error.message}`);
62+
const { write: cacheWrite = false, read: cacheRead = false } = cache;
63+
const { outputDir } = persist;
64+
65+
const { audits, ...executionMeta } = cacheRead
66+
? // IF not null, take the result from cache
67+
((await readRunnerResults(pluginMeta.slug, outputDir)) ??
68+
// ELSE execute the plugin runner
69+
(await executePluginRunner(pluginConfig)))
70+
: await executePluginRunner(pluginConfig);
71+
72+
if (cacheWrite) {
73+
await writeRunnerResults(pluginMeta.slug, outputDir, {
74+
...executionMeta,
75+
audits,
76+
});
7877
}
79-
const auditOutputs = result.data;
80-
auditOutputsCorrelateWithPluginOutput(auditOutputs, pluginConfigAudits);
81-
82-
const normalizedAuditOutputs = await normalizeAuditOutputs(auditOutputs);
8378

8479
// enrich `AuditOutputs` to `AuditReport`
85-
const auditReports: AuditReport[] = normalizedAuditOutputs.map(
80+
const auditReports: AuditReport[] = audits.map(
8681
(auditOutput: AuditOutput) => ({
8782
...auditOutput,
8883
...(pluginConfigAudits.find(
@@ -103,13 +98,18 @@ export async function executePlugin(
10398
}
10499

105100
const wrapProgress = async (
106-
pluginCfg: PluginConfig,
101+
cfg: {
102+
plugin: PluginConfig;
103+
persist: Required<Pick<PersistConfig, 'outputDir'>>;
104+
cache: CacheConfigObject;
105+
},
107106
steps: number,
108107
progressBar: ProgressBar | null,
109108
) => {
109+
const { plugin: pluginCfg, ...rest } = cfg;
110110
progressBar?.updateTitle(`Executing ${bold(pluginCfg.title)}`);
111111
try {
112-
const pluginReport = await executePlugin(pluginCfg);
112+
const pluginReport = await executePlugin(pluginCfg, rest);
113113
progressBar?.incrementInSteps(steps);
114114
return pluginReport;
115115
} catch (error) {
@@ -127,7 +127,7 @@ const wrapProgress = async (
127127
/**
128128
* Execute multiple plugins and aggregates their output.
129129
* @public
130-
* @param plugins array of {@link PluginConfig} objects
130+
* @param cfg
131131
* @param {Object} [options] execution options
132132
* @param {boolean} options.progress show progress bar
133133
* @returns {Promise<PluginReport[]>} plugin report
@@ -146,15 +146,24 @@ const wrapProgress = async (
146146
*
147147
*/
148148
export async function executePlugins(
149-
plugins: PluginConfig[],
149+
cfg: {
150+
plugins: PluginConfig[];
151+
persist: Required<Pick<PersistConfig, 'outputDir'>>;
152+
cache: CacheConfigObject;
153+
},
150154
options?: { progress?: boolean },
151155
): Promise<PluginReport[]> {
156+
const { plugins, ...cacheCfg } = cfg;
152157
const { progress = false } = options ?? {};
153158

154159
const progressBar = progress ? getProgressBar('Run plugins') : null;
155160

156161
const pluginsResult = plugins.map(pluginCfg =>
157-
wrapProgress(pluginCfg, plugins.length, progressBar),
162+
wrapProgress(
163+
{ plugin: pluginCfg, ...cacheCfg },
164+
plugins.length,
165+
progressBar,
166+
),
158167
);
159168

160169
const errorsTransform = ({ reason }: PromiseRejectedResult) => String(reason);
@@ -179,17 +188,3 @@ export async function executePlugins(
179188

180189
return fulfilled.map(result => result.value);
181190
}
182-
183-
function auditOutputsCorrelateWithPluginOutput(
184-
auditOutputs: AuditOutputs,
185-
pluginConfigAudits: PluginConfig['audits'],
186-
) {
187-
auditOutputs.forEach(auditOutput => {
188-
const auditMetadata = pluginConfigAudits.find(
189-
audit => audit.slug === auditOutput.slug,
190-
);
191-
if (!auditMetadata) {
192-
throw new PluginOutputMissingAuditError(auditOutput.slug);
193-
}
194-
});
195-
}

0 commit comments

Comments
 (0)