From 10d3b9a83d66bc73f491e1d0e09cf022113fb8d1 Mon Sep 17 00:00:00 2001 From: Michael Date: Fri, 8 Aug 2025 20:38:12 +0200 Subject: [PATCH 1/9] feat: add audit output caching for execute plugin --- packages/core/src/index.ts | 2 +- .../core/src/lib/implementation/collect.ts | 24 +- .../src/lib/implementation/execute-plugin.ts | 112 +++-- .../execute-plugin.unit.test.ts | 412 ++++++++++-------- .../src/lib/implementation/runner.int.test.ts | 118 +++++ .../core/src/lib/implementation/runner.ts | 107 ++++- .../lib/implementation/runner.unit.test.ts | 79 ++++ packages/models/src/index.ts | 1 + packages/models/src/lib/cache-config.ts | 13 + 9 files changed, 623 insertions(+), 245 deletions(-) create mode 100644 packages/core/src/lib/implementation/runner.int.test.ts create mode 100644 packages/models/src/lib/cache-config.ts diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 25cf1d343..0c3e18080 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -17,8 +17,8 @@ export type { ReportsToCompare } from './lib/implementation/compare-scorables.js export { executePlugin, executePlugins, - PluginOutputMissingAuditError, } from './lib/implementation/execute-plugin.js'; +export { AuditOutputsMissingAuditError } from './lib/implementation/runner.js'; export { PersistDirError, PersistError, diff --git a/packages/core/src/lib/implementation/collect.ts b/packages/core/src/lib/implementation/collect.ts index 99cc47740..e88bdf260 100644 --- a/packages/core/src/lib/implementation/collect.ts +++ b/packages/core/src/lib/implementation/collect.ts @@ -1,22 +1,36 @@ import { createRequire } from 'node:module'; -import type { CoreConfig, Report } from '@code-pushup/models'; +import { + type CoreConfig, + DEFAULT_PERSIST_OUTPUT_DIR, + type PersistConfig, + type Report, +} from '@code-pushup/models'; import { calcDuration, getLatestCommit } from '@code-pushup/utils'; import type { GlobalOptions } from '../types.js'; import { executePlugins } from './execute-plugin.js'; -export type CollectOptions = Pick & - Partial; +export type CollectOptions = Pick & { + persist?: Required>; +} & Partial; /** * Run audits, collect plugin output and aggregate it into a JSON object * @param options */ export async function collect(options: CollectOptions): Promise { - const { plugins, categories } = options; + const { plugins, categories, persist, ...otherOptions } = options; const date = new Date().toISOString(); const start = performance.now(); const commit = await getLatestCommit(); - const pluginOutputs = await executePlugins(plugins, options); + const pluginOutputs = await executePlugins( + { + plugins, + persist: { ...persist, outputDir: DEFAULT_PERSIST_OUTPUT_DIR }, + // implement together with CLI option + cache: {}, + }, + otherOptions, + ); const packageJson = createRequire(import.meta.url)( '../../../package.json', ) as typeof import('../../../package.json'); diff --git a/packages/core/src/lib/implementation/execute-plugin.ts b/packages/core/src/lib/implementation/execute-plugin.ts index c283aa144..d427dc086 100644 --- a/packages/core/src/lib/implementation/execute-plugin.ts +++ b/packages/core/src/lib/implementation/execute-plugin.ts @@ -1,12 +1,12 @@ import { bold } from 'ansis'; -import { - type Audit, - type AuditOutput, - type AuditOutputs, - type AuditReport, - type PluginConfig, - type PluginReport, - auditOutputsSchema, +import type { + Audit, + AuditOutput, + AuditReport, + CacheConfig, + PersistConfig, + PluginConfig, + PluginReport, } from '@code-pushup/models'; import { type ProgressBar, @@ -15,21 +15,11 @@ import { logMultipleResults, pluralizeToken, } from '@code-pushup/utils'; -import { normalizeAuditOutputs } from '../normalize.js'; -import { executeRunnerConfig, executeRunnerFunction } from './runner.js'; - -/** - * Error thrown when plugin output is invalid. - */ -export class PluginOutputMissingAuditError extends Error { - constructor(auditSlug: string) { - super( - `Audit metadata not present in plugin config. Missing slug: ${bold( - auditSlug, - )}`, - ); - } -} +import { + executePluginRunner, + readRunnerResults, + writeRunnerResults, +} from './runner.js'; /** * Execute a plugin. @@ -37,7 +27,7 @@ export class PluginOutputMissingAuditError extends Error { * @public * @param pluginConfig - {@link ProcessConfig} object with runner and meta * @returns {Promise} - audit outputs from plugin runner - * @throws {PluginOutputMissingAuditError} - if plugin runner output is invalid + * @throws {AuditOutputsMissingAuditError} - if plugin runner output is invalid * * @example * // plugin execution @@ -54,7 +44,12 @@ export class PluginOutputMissingAuditError extends Error { */ export async function executePlugin( pluginConfig: PluginConfig, + opt: { + cache: CacheConfig; + persist: Required>; + }, ): Promise { + const { cache, persist } = opt; const { runner, audits: pluginConfigAudits, @@ -63,26 +58,25 @@ export async function executePlugin( groups, ...pluginMeta } = pluginConfig; - - // execute plugin runner - const runnerResult = - typeof runner === 'object' - ? await executeRunnerConfig(runner) - : await executeRunnerFunction(runner); - const { audits: unvalidatedAuditOutputs, ...executionMeta } = runnerResult; - - // validate auditOutputs - const result = auditOutputsSchema.safeParse(unvalidatedAuditOutputs); - if (!result.success) { - throw new Error(`Audit output is invalid: ${result.error.message}`); + const { write: cacheWrite = false, read: cacheRead = false } = cache; + const { outputDir } = persist; + + const { audits, ...executionMeta } = cacheRead + ? // IF not null, take the result from cache + ((await readRunnerResults(pluginMeta.slug, outputDir)) ?? + // ELSE execute the plugin runner + (await executePluginRunner(pluginConfig))) + : await executePluginRunner(pluginConfig); + + if (cacheWrite) { + await writeRunnerResults(pluginMeta.slug, outputDir, { + ...executionMeta, + audits, + }); } - const auditOutputs = result.data; - auditOutputsCorrelateWithPluginOutput(auditOutputs, pluginConfigAudits); - - const normalizedAuditOutputs = await normalizeAuditOutputs(auditOutputs); // enrich `AuditOutputs` to `AuditReport` - const auditReports: AuditReport[] = normalizedAuditOutputs.map( + const auditReports: AuditReport[] = audits.map( (auditOutput: AuditOutput) => ({ ...auditOutput, ...(pluginConfigAudits.find( @@ -103,13 +97,18 @@ export async function executePlugin( } const wrapProgress = async ( - pluginCfg: PluginConfig, + cfg: { + plugin: PluginConfig; + persist: Required>; + cache: CacheConfig; + }, steps: number, progressBar: ProgressBar | null, ) => { + const { plugin: pluginCfg, ...rest } = cfg; progressBar?.updateTitle(`Executing ${bold(pluginCfg.title)}`); try { - const pluginReport = await executePlugin(pluginCfg); + const pluginReport = await executePlugin(pluginCfg, rest); progressBar?.incrementInSteps(steps); return pluginReport; } catch (error) { @@ -146,15 +145,24 @@ const wrapProgress = async ( * */ export async function executePlugins( - plugins: PluginConfig[], + cfg: { + plugins: PluginConfig[]; + persist: Required>; + cache: CacheConfig; + }, options?: { progress?: boolean }, ): Promise { + const { plugins, ...cacheCfg } = cfg; const { progress = false } = options ?? {}; const progressBar = progress ? getProgressBar('Run plugins') : null; const pluginsResult = plugins.map(pluginCfg => - wrapProgress(pluginCfg, plugins.length, progressBar), + wrapProgress( + { plugin: pluginCfg, ...cacheCfg }, + plugins.length, + progressBar, + ), ); const errorsTransform = ({ reason }: PromiseRejectedResult) => String(reason); @@ -179,17 +187,3 @@ export async function executePlugins( return fulfilled.map(result => result.value); } - -function auditOutputsCorrelateWithPluginOutput( - auditOutputs: AuditOutputs, - pluginConfigAudits: PluginConfig['audits'], -) { - auditOutputs.forEach(auditOutput => { - const auditMetadata = pluginConfigAudits.find( - audit => audit.slug === auditOutput.slug, - ); - if (!auditMetadata) { - throw new PluginOutputMissingAuditError(auditOutput.slug); - } - }); -} diff --git a/packages/core/src/lib/implementation/execute-plugin.unit.test.ts b/packages/core/src/lib/implementation/execute-plugin.unit.test.ts index cc63bf82c..d593a4da3 100644 --- a/packages/core/src/lib/implementation/execute-plugin.unit.test.ts +++ b/packages/core/src/lib/implementation/execute-plugin.unit.test.ts @@ -1,95 +1,127 @@ import { bold } from 'ansis'; import { vol } from 'memfs'; -import { describe, expect, it } from 'vitest'; +import { describe, expect, it, vi } from 'vitest'; import type { AuditOutputs, PluginConfig } from '@code-pushup/models'; import { MEMFS_VOLUME, MINIMAL_PLUGIN_CONFIG_MOCK, } from '@code-pushup/test-utils'; -import { - PluginOutputMissingAuditError, - executePlugin, - executePlugins, -} from './execute-plugin.js'; +import { executePlugin, executePlugins } from './execute-plugin.js'; +import * as runnerModule from './runner.js'; describe('executePlugin', () => { - it('should execute a valid plugin config', async () => { - const pluginResult = await executePlugin(MINIMAL_PLUGIN_CONFIG_MOCK); - expect(pluginResult.audits[0]?.slug).toBe('node-version'); + beforeEach(() => { + vi.clearAllMocks(); }); - it('should yield audit outputs for valid runner config', async () => { - vol.fromJSON( - { - 'output.json': JSON.stringify([ - { - slug: 'node-version', - score: 0.3, - value: 16, - }, - ]), - }, - MEMFS_VOLUME, - ); + afterEach(() => { + vi.restoreAllMocks(); + }); - const pluginResult = await executePlugin({ - ...MINIMAL_PLUGIN_CONFIG_MOCK, - runner: { - command: 'node', - args: ['-v'], - outputFile: 'output.json', - }, + it('should execute a valid plugin config', async () => { + await expect( + executePlugin(MINIMAL_PLUGIN_CONFIG_MOCK, { + persist: { outputDir: '' }, + cache: {}, + }), + ).resolves.toStrictEqual({ + slug: 'node', + title: 'Node', + icon: 'javascript', + duration: expect.any(Number), + date: expect.any(String), + audits: expect.arrayContaining([ + expect.objectContaining({ + ...MINIMAL_PLUGIN_CONFIG_MOCK.audits.at(0), + title: 'Node version', + description: 'Returns node version', + docsUrl: 'https://nodejs.org/', + }), + ]), }); - expect(pluginResult.audits[0]?.slug).toBe('node-version'); }); - it('should yield audit outputs for a valid runner function', async () => { - const pluginResult = await executePlugin({ - ...MINIMAL_PLUGIN_CONFIG_MOCK, - runner: () => [ + it('should try to read cache if cache.read is true', async () => { + const readRunnerResultsSpy = vi.spyOn(runnerModule, 'readRunnerResults'); + + const validRunnerResult = { + duration: 0, // readRunnerResults now automatically sets this to 0 for cache hits + date: new Date().toISOString(), // readRunnerResults sets this to current time + audits: [ { - slug: 'node-version', + slug: 'node-version', // Must match the plugin config audit slug for enrichment score: 0.3, value: 16, }, ], - }); - expect(pluginResult.audits).toEqual([ - expect.objectContaining({ - slug: 'node-version', - title: 'Node version', - score: 0.3, - value: 16, - }), - ]); - }); + }; + readRunnerResultsSpy.mockResolvedValue(validRunnerResult); - it('should throw when audit slug is invalid', async () => { - await expect(() => - executePlugin({ - ...MINIMAL_PLUGIN_CONFIG_MOCK, - audits: [{ slug: '-invalid-slug', title: 'Invalid audit' }], + await expect( + executePlugin(MINIMAL_PLUGIN_CONFIG_MOCK, { + persist: { outputDir: 'dummy-path-result-is-mocked' }, + cache: { read: true }, }), - ).rejects.toThrow(new PluginOutputMissingAuditError('node-version')); + ).resolves.toStrictEqual({ + slug: 'node', + title: 'Node', + icon: 'javascript', + duration: 0, + date: expect.any(String), + audits: expect.arrayContaining([ + expect.objectContaining({ + ...validRunnerResult.audits.at(0), + title: 'Node version', + description: 'Returns node version', + docsUrl: 'https://nodejs.org/', + }), + ]), + }); }); - it('should throw for missing audit', async () => { - const missingSlug = 'missing-audit-slug'; - await expect(() => - executePlugin({ - ...MINIMAL_PLUGIN_CONFIG_MOCK, - runner: () => [ - { - slug: missingSlug, - score: 0, - value: 0, - }, - ], + it('should try to execute runner if cache.read is true and file not present', async () => { + const readRunnerResultsSpy = vi.spyOn(runnerModule, 'readRunnerResults'); + const executePluginRunnerSpy = vi.spyOn( + runnerModule, + 'executePluginRunner', + ); + + readRunnerResultsSpy.mockResolvedValue(null); + const runnerResult = { + duration: 1000, + date: '2021-01-01', + audits: [ + { + slug: 'node-version', + score: 0.3, + value: 16, + }, + ], + }; + executePluginRunnerSpy.mockResolvedValue(runnerResult); + + await expect( + executePlugin(MINIMAL_PLUGIN_CONFIG_MOCK, { + persist: { outputDir: 'dummy-path-result-is-mocked' }, + cache: { read: true }, }), - ).rejects.toThrow( - `Audit metadata not present in plugin config. Missing slug: ${bold( - missingSlug, - )}`, + ).resolves.toStrictEqual({ + slug: 'node', + title: 'Node', + icon: 'javascript', + ...runnerResult, + audits: [ + { + ...runnerResult.audits.at(0), + title: 'Node version', + description: 'Returns node version', + docsUrl: 'https://nodejs.org/', + }, + ], + }); + + expect(executePluginRunnerSpy).toHaveBeenCalledWith( + MINIMAL_PLUGIN_CONFIG_MOCK, ); }); }); @@ -97,13 +129,17 @@ describe('executePlugin', () => { describe('executePlugins', () => { it('should execute valid plugins', async () => { const pluginResult = await executePlugins( - [ - MINIMAL_PLUGIN_CONFIG_MOCK, - { - ...MINIMAL_PLUGIN_CONFIG_MOCK, - icon: 'nodejs', - }, - ], + { + plugins: [ + MINIMAL_PLUGIN_CONFIG_MOCK, + { + ...MINIMAL_PLUGIN_CONFIG_MOCK, + icon: 'nodejs', + }, + ], + persist: { outputDir: '.code-pushup' }, + cache: {}, + }, { progress: false }, ); @@ -117,21 +153,25 @@ describe('executePlugins', () => { const title = 'Simulate an invalid audit slug in outputs'; await expect(() => executePlugins( - [ - { - ...MINIMAL_PLUGIN_CONFIG_MOCK, - slug, - title, - runner: () => [ - { - slug: 'invalid-audit-slug-', - score: 0.3, - value: 16, - displayValue: '16.0.0', - }, - ], - }, - ] satisfies PluginConfig[], + { + plugins: [ + { + ...MINIMAL_PLUGIN_CONFIG_MOCK, + slug, + title, + runner: () => [ + { + slug: 'invalid-audit-slug-', + score: 0.3, + value: 16, + displayValue: '16.0.0', + }, + ], + }, + ] satisfies PluginConfig[], + persist: { outputDir: '.code-pushup' }, + cache: {}, + }, { progress: false }, ), ).rejects.toThrow( @@ -145,21 +185,25 @@ describe('executePlugins', () => { const missingAuditSlug = 'missing-audit-slug'; await expect(() => executePlugins( - [ - { - ...MINIMAL_PLUGIN_CONFIG_MOCK, - slug: 'plg1', - title: 'plg1', - runner: () => [ - { - slug: `${missingAuditSlug}-a`, - score: 0.3, - value: 16, - displayValue: '16.0.0', - }, - ], - }, - ] satisfies PluginConfig[], + { + plugins: [ + { + ...MINIMAL_PLUGIN_CONFIG_MOCK, + slug: 'plg1', + title: 'plg1', + runner: () => [ + { + slug: `${missingAuditSlug}-a`, + score: 0.3, + value: 16, + displayValue: '16.0.0', + }, + ], + }, + ] satisfies PluginConfig[], + persist: { outputDir: '.code-pushup' }, + cache: {}, + }, { progress: false }, ), ).rejects.toThrow('Executing 1 plugin failed.\n\n'); @@ -169,34 +213,38 @@ describe('executePlugins', () => { const missingAuditSlug = 'missing-audit-slug'; await expect(() => executePlugins( - [ - { - ...MINIMAL_PLUGIN_CONFIG_MOCK, - slug: 'plg1', - title: 'plg1', - runner: () => [ - { - slug: `${missingAuditSlug}-a`, - score: 0.3, - value: 16, - displayValue: '16.0.0', - }, - ], - }, - { - ...MINIMAL_PLUGIN_CONFIG_MOCK, - slug: 'plg2', - title: 'plg2', - runner: () => [ - { - slug: `${missingAuditSlug}-b`, - score: 0.3, - value: 16, - displayValue: '16.0.0', - }, - ], - }, - ] satisfies PluginConfig[], + { + plugins: [ + { + ...MINIMAL_PLUGIN_CONFIG_MOCK, + slug: 'plg1', + title: 'plg1', + runner: () => [ + { + slug: `${missingAuditSlug}-a`, + score: 0.3, + value: 16, + displayValue: '16.0.0', + }, + ], + }, + { + ...MINIMAL_PLUGIN_CONFIG_MOCK, + slug: 'plg2', + title: 'plg2', + runner: () => [ + { + slug: `${missingAuditSlug}-b`, + score: 0.3, + value: 16, + displayValue: '16.0.0', + }, + ], + }, + ] satisfies PluginConfig[], + persist: { outputDir: '.code-pushup' }, + cache: {}, + }, { progress: false }, ), ).rejects.toThrow('Executing 2 plugins failed.\n\n'); @@ -207,34 +255,38 @@ describe('executePlugins', () => { await expect(() => executePlugins( - [ - { - ...MINIMAL_PLUGIN_CONFIG_MOCK, - slug: 'plg1', - title: 'plg1', - runner: () => [ - { - slug: `${missingAuditSlug}-a`, - score: 0.3, - value: 16, - displayValue: '16.0.0', - }, - ], - }, - { - ...MINIMAL_PLUGIN_CONFIG_MOCK, - slug: 'plg2', - title: 'plg2', - runner: () => [ - { - slug: `${missingAuditSlug}-b`, - score: 0.3, - value: 16, - displayValue: '16.0.0', - }, - ], - }, - ] satisfies PluginConfig[], + { + plugins: [ + { + ...MINIMAL_PLUGIN_CONFIG_MOCK, + slug: 'plg1', + title: 'plg1', + runner: () => [ + { + slug: `${missingAuditSlug}-a`, + score: 0.3, + value: 16, + displayValue: '16.0.0', + }, + ], + }, + { + ...MINIMAL_PLUGIN_CONFIG_MOCK, + slug: 'plg2', + title: 'plg2', + runner: () => [ + { + slug: `${missingAuditSlug}-b`, + score: 0.3, + value: 16, + displayValue: '16.0.0', + }, + ], + }, + ] satisfies PluginConfig[], + persist: { outputDir: '.code-pushup' }, + cache: {}, + }, { progress: false }, ), ).rejects.toThrow( @@ -265,25 +317,29 @@ describe('executePlugins', () => { ); const pluginResult = await executePlugins( - [ - { - ...MINIMAL_PLUGIN_CONFIG_MOCK, - runner: { - command: 'node', - args: ['-v'], - outputFile: 'output.json', - outputTransform: (outputs: unknown): Promise => - Promise.resolve([ - { - slug: (outputs as AuditOutputs)[0]!.slug, - score: 0.3, - value: 16, - displayValue: '16.0.0', - }, - ]), + { + plugins: [ + { + ...MINIMAL_PLUGIN_CONFIG_MOCK, + runner: { + command: 'node', + args: ['-v'], + outputFile: 'output.json', + outputTransform: (outputs: unknown): Promise => + Promise.resolve([ + { + slug: (outputs as AuditOutputs)[0]!.slug, + score: 0.3, + value: 16, + displayValue: '16.0.0', + }, + ]), + }, }, - }, - ], + ], + persist: { outputDir: '.code-pushup' }, + cache: {}, + }, { progress: false }, ); expect(pluginResult[0]?.audits[0]?.slug).toBe('node-version'); diff --git a/packages/core/src/lib/implementation/runner.int.test.ts b/packages/core/src/lib/implementation/runner.int.test.ts new file mode 100644 index 000000000..72cf991c8 --- /dev/null +++ b/packages/core/src/lib/implementation/runner.int.test.ts @@ -0,0 +1,118 @@ +import { writeFile } from 'node:fs/promises'; +import path from 'node:path'; +import { cleanTestFolder } from '@code-pushup/test-utils'; +import { ensureDirectoryExists } from '@code-pushup/utils'; +import { readRunnerResults, writeRunnerResults } from './runner.js'; + +describe('readRunnerResults', () => { + const outputDir = path.join( + 'tmp', + 'int', + 'core', + 'runner', + 'read-result', + '.code-pushup', + ); + const pluginSlug = 'plugin-with-cache'; + const cacheDir = path.join(outputDir, pluginSlug); + + beforeEach(async () => { + await ensureDirectoryExists(cacheDir); + await writeFile( + path.join(cacheDir, 'audit-outputs.json'), + JSON.stringify([ + { + slug: 'node-version', + score: 0.3, + value: 16, + }, + ]), + ); + }); + + afterEach(async () => { + await cleanTestFolder(outputDir); + }); + + it('should read runner results from a file', async () => { + const runnerResults = await readRunnerResults(pluginSlug, outputDir); + expect(runnerResults).toEqual({ + duration: 0, // Duration is overridden to 0 when reading from cache + date: expect.any(String), // Date is overridden to current time when reading from cache + audits: [ + { + slug: 'node-version', + score: 0.3, + value: 16, + }, + ], + }); + }); + + it('should return null if file does not exist', async () => { + const runnerResults = await readRunnerResults( + 'plugin-with-no-cache', + outputDir, + ); + expect(runnerResults).toBeNull(); + }); +}); + +describe('writeRunnerResults', () => { + const outputDir = path.join( + 'tmp', + 'int', + 'core', + 'runner', + 'write-result', + '.code-pushup', + ); + const pluginSlug = 'plugin-with-cache'; + const cacheDir = path.join(outputDir, pluginSlug); + + beforeEach(async () => { + await ensureDirectoryExists(cacheDir); + await writeFile( + path.join(cacheDir, 'audit-outputs.json'), + JSON.stringify([ + { + slug: 'node-version', + score: 0.3, + value: 16, + }, + ]), + ); + }); + + afterEach(async () => { + await cleanTestFolder(outputDir); + }); + + it('should write runner results from a file', async () => { + await expect( + writeRunnerResults(pluginSlug, outputDir, { + duration: 1000, + date: '2021-01-01', + audits: [ + { + slug: 'node-version', + score: 0.3, + value: 16, + }, + ], + }), + ).resolves.toBeUndefined(); + + await expect(readRunnerResults(pluginSlug, outputDir)).resolves.toEqual({ + duration: 0, // Duration is overridden to 0 when reading from cache + date: expect.any(String), // Date is overridden to current time when reading from cache + audits: [ + { + slug: 'node-version', + score: 0.3, + value: 16, + }, + ], + }); + }); +}); diff --git a/packages/core/src/lib/implementation/runner.ts b/packages/core/src/lib/implementation/runner.ts index 04ba10dc2..23c1c5d09 100644 --- a/packages/core/src/lib/implementation/runner.ts +++ b/packages/core/src/lib/implementation/runner.ts @@ -1,13 +1,24 @@ +import { bold } from 'ansis'; +import { writeFile } from 'node:fs/promises'; import path from 'node:path'; -import type { RunnerConfig, RunnerFunction } from '@code-pushup/models'; +import { + type AuditOutputs, + type PluginConfig, + type RunnerConfig, + type RunnerFunction, + auditOutputsSchema, +} from '@code-pushup/models'; import { calcDuration, + ensureDirectoryExists, executeProcess, + fileExists, isVerbose, readJsonFile, removeDirectoryIfExists, ui, } from '@code-pushup/utils'; +import { normalizeAuditOutputs } from '../normalize.js'; export type RunnerResult = { date: string; @@ -15,12 +26,15 @@ export type RunnerResult = { audits: unknown; }; +export type ValidatedRunnerResult = Omit & { + audits: AuditOutputs; +}; + export async function executeRunnerConfig( cfg: RunnerConfig, ): Promise { const { args, command, outputFile, outputTransform } = cfg; - // execute process const { duration, date } = await executeProcess({ command, args, @@ -66,3 +80,92 @@ export async function executeRunnerFunction( audits, }; } + +/** + * Error thrown when plugin output is invalid. + */ +export class AuditOutputsMissingAuditError extends Error { + constructor(auditSlug: string) { + super( + `Audit metadata not present in plugin config. Missing slug: ${bold( + auditSlug, + )}`, + ); + } +} + +export async function executePluginRunner( + pluginConfig: Pick, +): Promise & { audits: AuditOutputs }> { + const { audits: pluginConfigAudits, runner } = pluginConfig; + const runnerResult: RunnerResult = + typeof runner === 'object' + ? await executeRunnerConfig(runner) + : await executeRunnerFunction(runner); + const { audits: unvalidatedAuditOutputs, ...executionMeta } = runnerResult; + + const result = auditOutputsSchema.safeParse(unvalidatedAuditOutputs); + if (!result.success) { + throw new Error(`Audit output is invalid: ${result.error.message}`); + } + const auditOutputs = result.data; + auditOutputsCorrelateWithPluginOutput(auditOutputs, pluginConfigAudits); + + return { + ...executionMeta, + audits: await normalizeAuditOutputs(auditOutputs), + }; +} + +function auditOutputsCorrelateWithPluginOutput( + auditOutputs: AuditOutputs, + pluginConfigAudits: PluginConfig['audits'], +) { + auditOutputs.forEach(auditOutput => { + const auditMetadata = pluginConfigAudits.find( + audit => audit.slug === auditOutput.slug, + ); + if (!auditMetadata) { + throw new AuditOutputsMissingAuditError(auditOutput.slug); + } + }); +} + +export function getAuditOutputsPath(pluginSlug: string, outputDir: string) { + return path.join(outputDir, pluginSlug, `audit-outputs.json`); +} + +/** + * Save audit outputs to a file to be able to cache the results + * @param auditOutputs + * @param pluginSlug + * @param outputDir + */ +export async function writeRunnerResults( + pluginSlug: string, + outputDir: string, + runnerResult: ValidatedRunnerResult, +): Promise { + await ensureDirectoryExists(outputDir); + await writeFile( + getAuditOutputsPath(pluginSlug, outputDir), + JSON.stringify(runnerResult.audits, null, 2), + ); +} + +export async function readRunnerResults( + pluginSlug: string, + outputDir: string, +): Promise { + const auditOutputsPath = getAuditOutputsPath(pluginSlug, outputDir); + if (await fileExists(auditOutputsPath)) { + const cachedResult = await readJsonFile(auditOutputsPath); + + return { + audits: cachedResult, + duration: 0, + date: new Date().toISOString(), + }; + } + return null; +} diff --git a/packages/core/src/lib/implementation/runner.unit.test.ts b/packages/core/src/lib/implementation/runner.unit.test.ts index db012e761..7b85b9517 100644 --- a/packages/core/src/lib/implementation/runner.unit.test.ts +++ b/packages/core/src/lib/implementation/runner.unit.test.ts @@ -3,15 +3,26 @@ import { type AuditOutputs, auditOutputsSchema } from '@code-pushup/models'; import { ISO_STRING_REGEXP, MEMFS_VOLUME, + MINIMAL_PLUGIN_CONFIG_MOCK, MINIMAL_RUNNER_CONFIG_MOCK, MINIMAL_RUNNER_FUNCTION_MOCK, } from '@code-pushup/test-utils'; import { type RunnerResult, + executePluginRunner, executeRunnerConfig, executeRunnerFunction, + getAuditOutputsPath, } from './runner.js'; +describe('getAuditOutputsPath', () => { + it('should read runner results from a file', async () => { + expect(getAuditOutputsPath('plugin-with-cache', 'output')).toBe( + 'output/plugin-with-cache/audit-outputs.json', + ); + }); +}); + describe('executeRunnerConfig', () => { beforeEach(() => { vol.fromJSON( @@ -107,3 +118,71 @@ describe('executeRunnerFunction', () => { ).rejects.toThrow('runner is not a function'); }); }); + +describe('executePluginRunner', () => { + it('should execute a valid plugin config', async () => { + const pluginResult = await executePluginRunner(MINIMAL_PLUGIN_CONFIG_MOCK); + expect(pluginResult.audits[0]?.slug).toBe('node-version'); + }); + + it('should yield audit outputs for valid runner config', async () => { + vol.fromJSON( + { + 'output.json': JSON.stringify([ + { + slug: 'node-version', + score: 0.3, + value: 16, + }, + ]), + }, + MEMFS_VOLUME, + ); + + await expect( + executePluginRunner({ + ...MINIMAL_PLUGIN_CONFIG_MOCK, + runner: { + command: 'node', + args: ['-v'], + outputFile: 'output.json', + }, + }), + ).resolves.toStrictEqual({ + duration: expect.any(Number), + date: expect.any(String), + audits: expect.arrayContaining([ + { + slug: 'node-version', + score: 0.3, + value: 16, + }, + ]), + }); + }); + + it('should yield audit outputs for a valid runner function', async () => { + await expect( + executePluginRunner({ + ...MINIMAL_PLUGIN_CONFIG_MOCK, + runner: () => [ + { + slug: 'node-version', + score: 0.3, + value: 16, + }, + ], + }), + ).resolves.toStrictEqual({ + duration: expect.any(Number), + date: expect.any(String), + audits: expect.arrayContaining([ + { + slug: 'node-version', + score: 0.3, + value: 16, + }, + ]), + }); + }); +}); diff --git a/packages/models/src/index.ts b/packages/models/src/index.ts index 91c22fb10..2ed9bf0a3 100644 --- a/packages/models/src/index.ts +++ b/packages/models/src/index.ts @@ -16,6 +16,7 @@ export { type AuditOutputs, } from './lib/audit-output.js'; export { auditSchema, type Audit } from './lib/audit.js'; +export { cacheConfigSchema, type CacheConfig } from './lib/cache-config.js'; export { categoryConfigSchema, categoryRefSchema, diff --git a/packages/models/src/lib/cache-config.ts b/packages/models/src/lib/cache-config.ts new file mode 100644 index 000000000..96b5cff35 --- /dev/null +++ b/packages/models/src/lib/cache-config.ts @@ -0,0 +1,13 @@ +import { z } from 'zod'; + +export const cacheConfigSchema = z + .object({ + read: z + .boolean() + .describe('Whether to read from cache if available') + .optional(), + write: z.boolean().describe('Whether to write results to cache').optional(), + }) + .describe('Cache configuration for read and write operations'); + +export type CacheConfig = z.infer; From 3ca2f45eb9aa0a83b167a81ebe7242f2ad11dfca Mon Sep 17 00:00:00 2001 From: Michael Date: Sat, 9 Aug 2025 00:24:08 +0200 Subject: [PATCH 2/9] test: adjust tests for new cache options --- .../src/file-size.plugin.int.test.ts | 14 ++++++++--- .../src/package-json.plugin.int.test.ts | 24 ++++++++++++++----- .../src/lib/implementation/execute-plugin.ts | 5 ++-- 3 files changed, 32 insertions(+), 11 deletions(-) diff --git a/examples/plugins/src/file-size/src/file-size.plugin.int.test.ts b/examples/plugins/src/file-size/src/file-size.plugin.int.test.ts index e647be9c6..0ffb16b1c 100644 --- a/examples/plugins/src/file-size/src/file-size.plugin.int.test.ts +++ b/examples/plugins/src/file-size/src/file-size.plugin.int.test.ts @@ -63,7 +63,11 @@ describe('create', () => { it('should return PluginConfig that executes correctly', async () => { const pluginConfig = create(baseOptions); - await expect(executePlugin(pluginConfig)).resolves.toMatchObject({ + await expect( + executePlugin(pluginConfig, { + persist: { outputDir: '.code-pushup' }, + }), + ).resolves.toMatchObject({ description: 'A plugin to measure and assert size of files in a directory.', slug, @@ -79,7 +83,9 @@ describe('create', () => { ...baseOptions, pattern: /\.js$/, }); - const { audits: auditOutputs } = await executePlugin(pluginConfig); + const { audits: auditOutputs } = await executePlugin(pluginConfig, { + persist: { outputDir: '.code-pushup' }, + }); expect(auditOutputs).toHaveLength(1); expect(auditOutputs[0]?.score).toBe(1); @@ -91,7 +97,9 @@ describe('create', () => { ...baseOptions, budget: 0, }); - const { audits: auditOutputs } = await executePlugin(pluginConfig); + const { audits: auditOutputs } = await executePlugin(pluginConfig, { + persist: { outputDir: '.code-pushup' }, + }); expect(auditOutputs).toHaveLength(1); expect(auditOutputs[0]?.score).toBe(0); diff --git a/examples/plugins/src/package-json/src/package-json.plugin.int.test.ts b/examples/plugins/src/package-json/src/package-json.plugin.int.test.ts index 634ec9653..0f233a73f 100644 --- a/examples/plugins/src/package-json/src/package-json.plugin.int.test.ts +++ b/examples/plugins/src/package-json/src/package-json.plugin.int.test.ts @@ -47,7 +47,9 @@ describe('create-package-json', () => { it('should return PluginConfig that executes correctly', async () => { const pluginConfig = create(baseOptions); - const pluginOutput = await executePlugin(pluginConfig); + const pluginOutput = await executePlugin(pluginConfig, { + persist: { outputDir: '.code-pushup' }, + }); expect(() => pluginReportSchema.parse(pluginOutput)).not.toThrow(); expect(pluginOutput).toMatchObject( @@ -68,7 +70,9 @@ describe('create-package-json', () => { ...baseOptions, license: 'MIT', }); - const { audits: auditOutputs } = await executePlugin(pluginConfig); + const { audits: auditOutputs } = await executePlugin(pluginConfig, { + persist: { outputDir: '.code-pushup' }, + }); expect(auditOutputs[0]?.value).toBe(1); expect(auditOutputs[0]?.score).toBe(0); @@ -85,7 +89,9 @@ describe('create-package-json', () => { ...baseOptions, type: 'module', }); - const { audits: auditOutputs } = await executePlugin(pluginConfig); + const { audits: auditOutputs } = await executePlugin(pluginConfig, { + persist: { outputDir: '.code-pushup' }, + }); expect(auditOutputs[1]?.slug).toBe('package-type'); expect(auditOutputs[1]?.score).toBe(0); @@ -104,7 +110,9 @@ describe('create-package-json', () => { test: '0', }, }); - const { audits: auditOutputs } = await executePlugin(pluginConfig); + const { audits: auditOutputs } = await executePlugin(pluginConfig, { + persist: { outputDir: '.code-pushup' }, + }); expect(auditOutputs).toHaveLength(audits.length); expect(auditOutputs[2]?.slug).toBe('package-dependencies'); @@ -125,7 +133,9 @@ describe('create-package-json', () => { test: '0', }, }); - const { audits: auditOutputs } = await executePlugin(pluginConfig); + const { audits: auditOutputs } = await executePlugin(pluginConfig, { + persist: { outputDir: '.code-pushup' }, + }); expect(auditOutputs).toHaveLength(audits.length); expect(auditOutputs[2]?.score).toBe(0); @@ -146,7 +156,9 @@ describe('create-package-json', () => { test: '0', }, }); - const { audits: auditOutputs } = await executePlugin(pluginConfig); + const { audits: auditOutputs } = await executePlugin(pluginConfig, { + persist: { outputDir: '.code-pushup' }, + }); expect(auditOutputs).toHaveLength(audits.length); expect(auditOutputs[2]?.score).toBe(0); diff --git a/packages/core/src/lib/implementation/execute-plugin.ts b/packages/core/src/lib/implementation/execute-plugin.ts index d427dc086..e0232f9fc 100644 --- a/packages/core/src/lib/implementation/execute-plugin.ts +++ b/packages/core/src/lib/implementation/execute-plugin.ts @@ -26,6 +26,7 @@ import { * * @public * @param pluginConfig - {@link ProcessConfig} object with runner and meta + * @param opt * @returns {Promise} - audit outputs from plugin runner * @throws {AuditOutputsMissingAuditError} - if plugin runner output is invalid * @@ -45,7 +46,7 @@ import { export async function executePlugin( pluginConfig: PluginConfig, opt: { - cache: CacheConfig; + cache?: CacheConfig; persist: Required>; }, ): Promise { @@ -58,7 +59,7 @@ export async function executePlugin( groups, ...pluginMeta } = pluginConfig; - const { write: cacheWrite = false, read: cacheRead = false } = cache; + const { write: cacheWrite = false, read: cacheRead = false } = cache ?? {}; const { outputDir } = persist; const { audits, ...executionMeta } = cacheRead From e146f2f641a48acc154ad9abc8cb33ab1bf0265d Mon Sep 17 00:00:00 2001 From: Michael Date: Sat, 9 Aug 2025 00:41:21 +0200 Subject: [PATCH 3/9] fix: add OS ptah helper --- packages/core/src/lib/implementation/runner.unit.test.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/core/src/lib/implementation/runner.unit.test.ts b/packages/core/src/lib/implementation/runner.unit.test.ts index 7b85b9517..1ef33e5fd 100644 --- a/packages/core/src/lib/implementation/runner.unit.test.ts +++ b/packages/core/src/lib/implementation/runner.unit.test.ts @@ -6,6 +6,7 @@ import { MINIMAL_PLUGIN_CONFIG_MOCK, MINIMAL_RUNNER_CONFIG_MOCK, MINIMAL_RUNNER_FUNCTION_MOCK, + osAgnosticPath, } from '@code-pushup/test-utils'; import { type RunnerResult, @@ -17,9 +18,9 @@ import { describe('getAuditOutputsPath', () => { it('should read runner results from a file', async () => { - expect(getAuditOutputsPath('plugin-with-cache', 'output')).toBe( - 'output/plugin-with-cache/audit-outputs.json', - ); + expect( + osAgnosticPath(getAuditOutputsPath('plugin-with-cache', 'output')), + ).toBe(osAgnosticPath('output/plugin-with-cache/audit-outputs.json')); }); }); From d576d11fd6c6cdafa487e6dc8c5c802898e9bc62 Mon Sep 17 00:00:00 2001 From: Michael Hladky <10064416+BioPhoton@users.noreply.github.com> Date: Mon, 11 Aug 2025 15:47:13 +0200 Subject: [PATCH 4/9] Update runner.int.test.ts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Matěj Chalk <34691111+matejchalk@users.noreply.github.com> --- packages/core/src/lib/implementation/runner.int.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/lib/implementation/runner.int.test.ts b/packages/core/src/lib/implementation/runner.int.test.ts index 72cf991c8..ed3059db1 100644 --- a/packages/core/src/lib/implementation/runner.int.test.ts +++ b/packages/core/src/lib/implementation/runner.int.test.ts @@ -88,7 +88,7 @@ describe('writeRunnerResults', () => { await cleanTestFolder(outputDir); }); - it('should write runner results from a file', async () => { + it('should write runner results to a file', async () => { await expect( writeRunnerResults(pluginSlug, outputDir, { duration: 1000, From 2655e25011b9468fc6896da8460216dd6f104fb9 Mon Sep 17 00:00:00 2001 From: Michael Date: Mon, 11 Aug 2025 19:36:57 +0200 Subject: [PATCH 5/9] fix: adjust filename --- packages/core/src/lib/implementation/runner.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/core/src/lib/implementation/runner.ts b/packages/core/src/lib/implementation/runner.ts index 23c1c5d09..517d1e971 100644 --- a/packages/core/src/lib/implementation/runner.ts +++ b/packages/core/src/lib/implementation/runner.ts @@ -48,7 +48,7 @@ export async function executeRunnerConfig( }, }); - // read process output from file system and parse it + // read process output from the file system and parse it const outputs = await readJsonFile(outputFile); // clean up plugin individual runner output directory await removeDirectoryIfExists(path.dirname(outputFile)); @@ -132,7 +132,7 @@ function auditOutputsCorrelateWithPluginOutput( } export function getAuditOutputsPath(pluginSlug: string, outputDir: string) { - return path.join(outputDir, pluginSlug, `audit-outputs.json`); + return path.join(outputDir, pluginSlug, 'runner-output.json'); } /** From 719fcb9d1fb1a6bfade8a903e7d56717b2dccdd5 Mon Sep 17 00:00:00 2001 From: Michael Date: Mon, 11 Aug 2025 19:57:25 +0200 Subject: [PATCH 6/9] fix: add cache types --- packages/models/src/index.ts | 9 ++++++++- packages/models/src/lib/cache-config.ts | 14 +++++++++++++- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/packages/models/src/index.ts b/packages/models/src/index.ts index 2ed9bf0a3..a305ea673 100644 --- a/packages/models/src/index.ts +++ b/packages/models/src/index.ts @@ -16,7 +16,14 @@ export { type AuditOutputs, } from './lib/audit-output.js'; export { auditSchema, type Audit } from './lib/audit.js'; -export { cacheConfigSchema, type CacheConfig } from './lib/cache-config.js'; +export { + cacheConfigSchema, + type CacheConfig, + cacheConfigObjectSchema, + type CacheConfigObject, + cacheConfigShorthandSchema, + type CacheConfigShorthand, +} from './lib/cache-config.js'; export { categoryConfigSchema, categoryRefSchema, diff --git a/packages/models/src/lib/cache-config.ts b/packages/models/src/lib/cache-config.ts index 96b5cff35..7609c9cec 100644 --- a/packages/models/src/lib/cache-config.ts +++ b/packages/models/src/lib/cache-config.ts @@ -1,6 +1,6 @@ import { z } from 'zod'; -export const cacheConfigSchema = z +export const cacheConfigObjectSchema = z .object({ read: z .boolean() @@ -8,6 +8,18 @@ export const cacheConfigSchema = z .optional(), write: z.boolean().describe('Whether to write results to cache').optional(), }) + .describe('Cache configuration object for read and/or write operations'); +export type CacheConfigObject = z.infer; + +export const cacheConfigShorthandSchema = z + .boolean() + .describe( + 'Cache configuration shorthand for both, read and write operations', + ); +export type CacheConfigShorthand = z.infer; + +export const cacheConfigSchema = z + .union(cacheConfigShorthandSchema, cacheConfigObjectSchema) .describe('Cache configuration for read and write operations'); export type CacheConfig = z.infer; From c2d951b4f5520603f874cad0d04b06613a66d0fb Mon Sep 17 00:00:00 2001 From: Michael Date: Mon, 11 Aug 2025 20:00:53 +0200 Subject: [PATCH 7/9] fix: add cache defaults in zod schema --- packages/models/src/lib/cache-config.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/models/src/lib/cache-config.ts b/packages/models/src/lib/cache-config.ts index 7609c9cec..731e8812f 100644 --- a/packages/models/src/lib/cache-config.ts +++ b/packages/models/src/lib/cache-config.ts @@ -5,8 +5,11 @@ export const cacheConfigObjectSchema = z read: z .boolean() .describe('Whether to read from cache if available') - .optional(), - write: z.boolean().describe('Whether to write results to cache').optional(), + .default(false), + write: z + .boolean() + .describe('Whether to write results to cache') + .default(false), }) .describe('Cache configuration object for read and/or write operations'); export type CacheConfigObject = z.infer; From a5dafd27a78926f0d199d83faea098d19e5a19c2 Mon Sep 17 00:00:00 2001 From: Michael Date: Mon, 11 Aug 2025 20:21:38 +0200 Subject: [PATCH 8/9] fix: adjust caching tests and types --- .../core/src/lib/implementation/collect.ts | 4 ++-- .../src/lib/implementation/execute-plugin.ts | 12 ++++++------ .../implementation/execute-plugin.unit.test.ts | 18 +++++++++--------- packages/models/src/lib/cache-config.ts | 5 +++-- 4 files changed, 20 insertions(+), 19 deletions(-) diff --git a/packages/core/src/lib/implementation/collect.ts b/packages/core/src/lib/implementation/collect.ts index e88bdf260..59803c406 100644 --- a/packages/core/src/lib/implementation/collect.ts +++ b/packages/core/src/lib/implementation/collect.ts @@ -25,9 +25,9 @@ export async function collect(options: CollectOptions): Promise { const pluginOutputs = await executePlugins( { plugins, - persist: { ...persist, outputDir: DEFAULT_PERSIST_OUTPUT_DIR }, + persist: { outputDir: DEFAULT_PERSIST_OUTPUT_DIR, ...persist }, // implement together with CLI option - cache: {}, + cache: { read: false, write: false }, }, otherOptions, ); diff --git a/packages/core/src/lib/implementation/execute-plugin.ts b/packages/core/src/lib/implementation/execute-plugin.ts index e0232f9fc..8f0a07860 100644 --- a/packages/core/src/lib/implementation/execute-plugin.ts +++ b/packages/core/src/lib/implementation/execute-plugin.ts @@ -3,7 +3,7 @@ import type { Audit, AuditOutput, AuditReport, - CacheConfig, + CacheConfigObject, PersistConfig, PluginConfig, PluginReport, @@ -46,7 +46,7 @@ import { export async function executePlugin( pluginConfig: PluginConfig, opt: { - cache?: CacheConfig; + cache: CacheConfigObject; persist: Required>; }, ): Promise { @@ -59,7 +59,7 @@ export async function executePlugin( groups, ...pluginMeta } = pluginConfig; - const { write: cacheWrite = false, read: cacheRead = false } = cache ?? {}; + const { write: cacheWrite = false, read: cacheRead = false } = cache; const { outputDir } = persist; const { audits, ...executionMeta } = cacheRead @@ -101,7 +101,7 @@ const wrapProgress = async ( cfg: { plugin: PluginConfig; persist: Required>; - cache: CacheConfig; + cache: CacheConfigObject; }, steps: number, progressBar: ProgressBar | null, @@ -127,7 +127,7 @@ const wrapProgress = async ( /** * Execute multiple plugins and aggregates their output. * @public - * @param plugins array of {@link PluginConfig} objects + * @param cfg * @param {Object} [options] execution options * @param {boolean} options.progress show progress bar * @returns {Promise} plugin report @@ -149,7 +149,7 @@ export async function executePlugins( cfg: { plugins: PluginConfig[]; persist: Required>; - cache: CacheConfig; + cache: CacheConfigObject; }, options?: { progress?: boolean }, ): Promise { diff --git a/packages/core/src/lib/implementation/execute-plugin.unit.test.ts b/packages/core/src/lib/implementation/execute-plugin.unit.test.ts index d593a4da3..8067f180c 100644 --- a/packages/core/src/lib/implementation/execute-plugin.unit.test.ts +++ b/packages/core/src/lib/implementation/execute-plugin.unit.test.ts @@ -22,7 +22,7 @@ describe('executePlugin', () => { await expect( executePlugin(MINIMAL_PLUGIN_CONFIG_MOCK, { persist: { outputDir: '' }, - cache: {}, + cache: { read: false, write: false }, }), ).resolves.toStrictEqual({ slug: 'node', @@ -60,7 +60,7 @@ describe('executePlugin', () => { await expect( executePlugin(MINIMAL_PLUGIN_CONFIG_MOCK, { persist: { outputDir: 'dummy-path-result-is-mocked' }, - cache: { read: true }, + cache: { read: true, write: false }, }), ).resolves.toStrictEqual({ slug: 'node', @@ -103,7 +103,7 @@ describe('executePlugin', () => { await expect( executePlugin(MINIMAL_PLUGIN_CONFIG_MOCK, { persist: { outputDir: 'dummy-path-result-is-mocked' }, - cache: { read: true }, + cache: { read: true, write: false }, }), ).resolves.toStrictEqual({ slug: 'node', @@ -138,7 +138,7 @@ describe('executePlugins', () => { }, ], persist: { outputDir: '.code-pushup' }, - cache: {}, + cache: { read: false, write: false }, }, { progress: false }, ); @@ -170,7 +170,7 @@ describe('executePlugins', () => { }, ] satisfies PluginConfig[], persist: { outputDir: '.code-pushup' }, - cache: {}, + cache: { read: false, write: false }, }, { progress: false }, ), @@ -202,7 +202,7 @@ describe('executePlugins', () => { }, ] satisfies PluginConfig[], persist: { outputDir: '.code-pushup' }, - cache: {}, + cache: { read: false, write: false }, }, { progress: false }, ), @@ -243,7 +243,7 @@ describe('executePlugins', () => { }, ] satisfies PluginConfig[], persist: { outputDir: '.code-pushup' }, - cache: {}, + cache: { read: false, write: false }, }, { progress: false }, ), @@ -285,7 +285,7 @@ describe('executePlugins', () => { }, ] satisfies PluginConfig[], persist: { outputDir: '.code-pushup' }, - cache: {}, + cache: { read: false, write: false }, }, { progress: false }, ), @@ -338,7 +338,7 @@ describe('executePlugins', () => { }, ], persist: { outputDir: '.code-pushup' }, - cache: {}, + cache: { read: false, write: false }, }, { progress: false }, ); diff --git a/packages/models/src/lib/cache-config.ts b/packages/models/src/lib/cache-config.ts index 731e8812f..e595a4320 100644 --- a/packages/models/src/lib/cache-config.ts +++ b/packages/models/src/lib/cache-config.ts @@ -22,7 +22,8 @@ export const cacheConfigShorthandSchema = z export type CacheConfigShorthand = z.infer; export const cacheConfigSchema = z - .union(cacheConfigShorthandSchema, cacheConfigObjectSchema) - .describe('Cache configuration for read and write operations'); + .union([cacheConfigShorthandSchema, cacheConfigObjectSchema]) + .describe('Cache configuration for read and write operations') + .default(false); export type CacheConfig = z.infer; From 3082015974b5f31756aa7af1e5ca6eba899a05dc Mon Sep 17 00:00:00 2001 From: Michael Date: Mon, 11 Aug 2025 20:44:37 +0200 Subject: [PATCH 9/9] test: adjust tests --- .../src/file-size/src/file-size.plugin.int.test.ts | 3 +++ .../package-json/src/package-json.plugin.int.test.ts | 6 ++++++ .../core/src/lib/implementation/runner.int.test.ts | 10 +++++++--- .../core/src/lib/implementation/runner.unit.test.ts | 2 +- 4 files changed, 17 insertions(+), 4 deletions(-) diff --git a/examples/plugins/src/file-size/src/file-size.plugin.int.test.ts b/examples/plugins/src/file-size/src/file-size.plugin.int.test.ts index 0ffb16b1c..30ff8334d 100644 --- a/examples/plugins/src/file-size/src/file-size.plugin.int.test.ts +++ b/examples/plugins/src/file-size/src/file-size.plugin.int.test.ts @@ -66,6 +66,7 @@ describe('create', () => { await expect( executePlugin(pluginConfig, { persist: { outputDir: '.code-pushup' }, + cache: { read: false, write: false }, }), ).resolves.toMatchObject({ description: @@ -85,6 +86,7 @@ describe('create', () => { }); const { audits: auditOutputs } = await executePlugin(pluginConfig, { persist: { outputDir: '.code-pushup' }, + cache: { read: false, write: false }, }); expect(auditOutputs).toHaveLength(1); @@ -99,6 +101,7 @@ describe('create', () => { }); const { audits: auditOutputs } = await executePlugin(pluginConfig, { persist: { outputDir: '.code-pushup' }, + cache: { read: false, write: false }, }); expect(auditOutputs).toHaveLength(1); diff --git a/examples/plugins/src/package-json/src/package-json.plugin.int.test.ts b/examples/plugins/src/package-json/src/package-json.plugin.int.test.ts index 0f233a73f..22407cf84 100644 --- a/examples/plugins/src/package-json/src/package-json.plugin.int.test.ts +++ b/examples/plugins/src/package-json/src/package-json.plugin.int.test.ts @@ -49,6 +49,7 @@ describe('create-package-json', () => { const pluginConfig = create(baseOptions); const pluginOutput = await executePlugin(pluginConfig, { persist: { outputDir: '.code-pushup' }, + cache: { read: false, write: false }, }); expect(() => pluginReportSchema.parse(pluginOutput)).not.toThrow(); @@ -72,6 +73,7 @@ describe('create-package-json', () => { }); const { audits: auditOutputs } = await executePlugin(pluginConfig, { persist: { outputDir: '.code-pushup' }, + cache: { read: false, write: false }, }); expect(auditOutputs[0]?.value).toBe(1); @@ -91,6 +93,7 @@ describe('create-package-json', () => { }); const { audits: auditOutputs } = await executePlugin(pluginConfig, { persist: { outputDir: '.code-pushup' }, + cache: { read: false, write: false }, }); expect(auditOutputs[1]?.slug).toBe('package-type'); @@ -112,6 +115,7 @@ describe('create-package-json', () => { }); const { audits: auditOutputs } = await executePlugin(pluginConfig, { persist: { outputDir: '.code-pushup' }, + cache: { read: false, write: false }, }); expect(auditOutputs).toHaveLength(audits.length); @@ -135,6 +139,7 @@ describe('create-package-json', () => { }); const { audits: auditOutputs } = await executePlugin(pluginConfig, { persist: { outputDir: '.code-pushup' }, + cache: { read: false, write: false }, }); expect(auditOutputs).toHaveLength(audits.length); @@ -158,6 +163,7 @@ describe('create-package-json', () => { }); const { audits: auditOutputs } = await executePlugin(pluginConfig, { persist: { outputDir: '.code-pushup' }, + cache: { read: false, write: false }, }); expect(auditOutputs).toHaveLength(audits.length); diff --git a/packages/core/src/lib/implementation/runner.int.test.ts b/packages/core/src/lib/implementation/runner.int.test.ts index ed3059db1..b26ee6ebe 100644 --- a/packages/core/src/lib/implementation/runner.int.test.ts +++ b/packages/core/src/lib/implementation/runner.int.test.ts @@ -2,7 +2,11 @@ import { writeFile } from 'node:fs/promises'; import path from 'node:path'; import { cleanTestFolder } from '@code-pushup/test-utils'; import { ensureDirectoryExists } from '@code-pushup/utils'; -import { readRunnerResults, writeRunnerResults } from './runner.js'; +import { + getAuditOutputsPath, + readRunnerResults, + writeRunnerResults, +} from './runner.js'; describe('readRunnerResults', () => { const outputDir = path.join( @@ -19,7 +23,7 @@ describe('readRunnerResults', () => { beforeEach(async () => { await ensureDirectoryExists(cacheDir); await writeFile( - path.join(cacheDir, 'audit-outputs.json'), + getAuditOutputsPath(pluginSlug, outputDir), JSON.stringify([ { slug: 'node-version', @@ -73,7 +77,7 @@ describe('writeRunnerResults', () => { beforeEach(async () => { await ensureDirectoryExists(cacheDir); await writeFile( - path.join(cacheDir, 'audit-outputs.json'), + getAuditOutputsPath(pluginSlug, outputDir), JSON.stringify([ { slug: 'node-version', diff --git a/packages/core/src/lib/implementation/runner.unit.test.ts b/packages/core/src/lib/implementation/runner.unit.test.ts index 1ef33e5fd..a9aea1270 100644 --- a/packages/core/src/lib/implementation/runner.unit.test.ts +++ b/packages/core/src/lib/implementation/runner.unit.test.ts @@ -20,7 +20,7 @@ describe('getAuditOutputsPath', () => { it('should read runner results from a file', async () => { expect( osAgnosticPath(getAuditOutputsPath('plugin-with-cache', 'output')), - ).toBe(osAgnosticPath('output/plugin-with-cache/audit-outputs.json')); + ).toBe(osAgnosticPath('output/plugin-with-cache/runner-output.json')); }); });