Skip to content

Commit 26d5f7b

Browse files
committed
test: add int tests for caching
1 parent 894cf68 commit 26d5f7b

File tree

11 files changed

+209
-39
lines changed

11 files changed

+209
-39
lines changed

e2e/cli-e2e/tests/__snapshots__/help.e2e.test.ts.snap

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,14 @@ Upload Options:
5454
[string]
5555
5656
Options:
57-
--version Show version [boolean]
58-
-h, --help Show help [boolean]
57+
--version Show version [boolean]
58+
--cache Cache runner outputs (both read and write)
59+
[boolean]
60+
--cache.read Read runner-output.json to file system
61+
[boolean]
62+
--cache.write Write runner-output.json to file system
63+
[boolean]
64+
-h, --help Show help [boolean]
5965
6066
Examples:
6167
code-pushup Run collect followed by upload based

packages/cli/README.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -328,3 +328,28 @@ In addition to the [Common Command Options](#common-command-options), the follow
328328
| Option | Required | Type | Description |
329329
| ------------- | :------: | ---------- | --------------------------------- |
330330
| **`--files`** | yes | `string[]` | List of `report-diff.json` paths. |
331+
332+
## Caching
333+
334+
The CLI supports caching to speed up subsequent runs and is compatible with Nx and turborepo.
335+
336+
Depending on your strategy, you can cache the generated reports files or plugin runner output.
337+
For fine-grained caching, we suggest caching plugin runner output.
338+
339+
### Caching Example Nx
340+
341+
To cache plugin runner output, you can use the `--cache.write` and `--cache.read` options.
342+
343+
Cache plugin level output files:
344+
345+
```bash
346+
# CACHE OUTPUTS
347+
# write cache to persist.outputDir/<pluginName>/runner-output.json
348+
# npx @code-pushup/cli collect --persist.outputDir {projectRoot}/.code-pushup --persist.skipReports --onlyPlugins coverage --cache
349+
nx run-many -t code-pushup:collect:coverage
350+
# npx @code-pushup/cli collect --persist.outputDir {projectRoot}/.code-pushup --persist.skipReports --onlyPlugins eslint --cache
351+
nx run-many -t code-pushup:collect:eslint
352+
# READ CACHED OUTPUTS
353+
# npx @code-pushup/cli collect --cache.read
354+
nx run-many -t code-pushup:collect
355+
```

packages/cli/src/lib/implementation/core-config.middleware.ts

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { autoloadRc, readRcByPath } from '@code-pushup/core';
22
import {
3+
type CacheConfig,
4+
type CacheConfigObject,
35
type CoreConfig,
46
DEFAULT_PERSIST_FILENAME,
57
DEFAULT_PERSIST_FORMAT,
@@ -42,13 +44,10 @@ export async function coreConfigMiddleware<
4244
...rcUpload,
4345
...cliUpload,
4446
});
47+
4548
return {
4649
...(config != null && { config }),
47-
cache: {
48-
write: false,
49-
read: false,
50-
...cliCache,
51-
},
50+
cache: normalizeCache(cliCache),
5251
persist: {
5352
outputDir:
5453
cliPersist?.outputDir ??
@@ -66,5 +65,15 @@ export async function coreConfigMiddleware<
6665
};
6766
}
6867

68+
export const normalizeCache = (cache?: CacheConfig): CacheConfigObject => {
69+
if (cache == null) {
70+
return { write: false, read: false };
71+
}
72+
if (typeof cache === 'boolean') {
73+
return { write: cache, read: cache };
74+
}
75+
return { write: cache.write ?? false, read: cache.read ?? false };
76+
};
77+
6978
export const normalizeFormats = (formats?: string[]): Format[] =>
7079
(formats ?? []).flatMap(format => format.split(',') as Format[]);

packages/cli/src/lib/implementation/core-config.model.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export type UploadConfigCliOptions = {
2121
export type CacheConfigCliOptions = {
2222
'cache.read'?: boolean;
2323
'cache.write'?: boolean;
24+
cache?: boolean;
2425
};
2526

2627
export type ConfigCliOptions = {
@@ -31,4 +32,5 @@ export type ConfigCliOptions = {
3132

3233
export type CoreConfigCliOptions = Pick<CoreConfig, 'persist'> & {
3334
upload?: Partial<Omit<UploadConfig, 'timeout'>>;
34-
} & CacheConfig;
35+
cache?: CacheConfig;
36+
};

packages/cli/src/lib/implementation/core-config.options.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@ import type {
66
} from './core-config.model.js';
77

88
export function yargsCoreConfigOptionsDefinition(): Record<
9-
keyof (PersistConfigCliOptions & UploadConfigCliOptions),
9+
keyof (PersistConfigCliOptions &
10+
UploadConfigCliOptions &
11+
CacheConfigCliOptions),
1012
Options
1113
> {
1214
return {
@@ -65,6 +67,10 @@ export function yargsCacheConfigOptionsDefinition(): Record<
6567
Options
6668
> {
6769
return {
70+
cache: {
71+
describe: 'Cache runner outputs (both read and write)',
72+
type: 'boolean',
73+
},
6874
'cache.read': {
6975
describe: 'Read runner-output.json to file system',
7076
type: 'boolean',

packages/core/src/index.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import type { ValidatedRunnerResult } from './lib/implementation/runner';
2-
31
export {
42
collectAndPersistReports,
53
type CollectAndPersistReportsOptions,

packages/core/src/lib/collect-and-persist.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import {
2+
type CacheConfigObject,
23
type CoreConfig,
34
type PersistConfig,
45
pluginReportSchema,
@@ -19,7 +20,10 @@ import type { GlobalOptions } from './types.js';
1920
export type CollectAndPersistReportsOptions = Pick<
2021
CoreConfig,
2122
'plugins' | 'categories'
22-
> & { persist: Required<PersistConfig> } & Partial<GlobalOptions>;
23+
> & {
24+
persist: Required<PersistConfig>;
25+
cache: CacheConfigObject;
26+
} & Partial<GlobalOptions>;
2327

2428
export async function collectAndPersistReports(
2529
options: CollectAndPersistReportsOptions,

packages/core/src/lib/collect-and-persist.unit.test.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,10 @@ describe('collectAndPersistReports', () => {
5858
filename: 'report',
5959
format: ['md'],
6060
},
61+
cache: {
62+
read: false,
63+
write: false,
64+
},
6165
progress: false,
6266
};
6367
await collectAndPersistReports(nonVerboseConfig);
@@ -99,6 +103,10 @@ describe('collectAndPersistReports', () => {
99103
filename: 'report',
100104
format: ['md'],
101105
},
106+
cache: {
107+
read: false,
108+
write: false,
109+
},
102110
progress: false,
103111
};
104112
await collectAndPersistReports(verboseConfig);
Lines changed: 132 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,147 @@
1-
import { vol } from 'memfs';
1+
import { writeFile } from 'node:fs/promises';
2+
import path from 'node:path';
23
import { describe, expect, it } from 'vitest';
34
import { commitSchema } from '@code-pushup/models';
4-
import { MEMFS_VOLUME, MINIMAL_CONFIG_MOCK } from '@code-pushup/test-utils';
5+
import { MINIMAL_CONFIG_MOCK, cleanTestFolder } from '@code-pushup/test-utils';
6+
import {
7+
ensureDirectoryExists,
8+
fileExists,
9+
readJsonFile,
10+
} from '@code-pushup/utils';
511
import { collect } from './collect.js';
12+
import { getRunnerOutputsPath } from './runner.js';
613

714
describe('collect', () => {
15+
const outputDir = path.join('tmp', 'int', 'core', 'collect', '.code-pushup');
16+
17+
const expectedCachedOutput = [
18+
{
19+
slug: 'node-version',
20+
score: 0.3,
21+
value: 16,
22+
displayValue: '16.0.0',
23+
details: {
24+
issues: [
25+
{
26+
severity: 'error',
27+
message: 'The required Node version to run Code PushUp CLI is 18.',
28+
},
29+
],
30+
},
31+
},
32+
];
33+
34+
const expectedCachedTestData = [
35+
{
36+
slug: 'node-version',
37+
score: 0.8,
38+
value: 18,
39+
displayValue: '18.0.0',
40+
details: { issues: [] },
41+
},
42+
];
43+
44+
const expectedPluginOutput = {
45+
slug: 'node',
46+
title: 'Node',
47+
icon: 'javascript',
48+
date: expect.any(String),
49+
audits: expectedCachedOutput.map(audit => ({
50+
...audit,
51+
title: 'Node version',
52+
description: 'Returns node version',
53+
docsUrl: 'https://nodejs.org/',
54+
})),
55+
};
56+
57+
beforeEach(async () => {
58+
await cleanTestFolder(outputDir);
59+
await ensureDirectoryExists(outputDir);
60+
});
61+
862
it('should execute with valid options', async () => {
9-
vol.fromJSON({}, MEMFS_VOLUME);
1063
const report = await collect({
1164
...MINIMAL_CONFIG_MOCK,
12-
verbose: true,
65+
persist: { outputDir },
66+
cache: { read: false, write: false },
1367
progress: false,
1468
});
1569

16-
expect(report.plugins[0]?.audits[0]).toEqual(
17-
expect.objectContaining({
18-
slug: 'node-version',
19-
displayValue: '16.0.0',
20-
details: {
21-
issues: [
22-
{
23-
severity: 'error',
24-
message:
25-
'The required Node version to run Code PushUp CLI is 18.',
26-
},
27-
],
28-
},
29-
}),
30-
);
70+
expect(report.plugins[0]).toStrictEqual({
71+
...expectedPluginOutput,
72+
duration: expect.any(Number),
73+
});
74+
expect(report.plugins[0]?.duration).toBeGreaterThanOrEqual(0);
3175

3276
expect(() => commitSchema.parse(report.commit)).not.toThrow();
77+
78+
await expect(
79+
fileExists(getRunnerOutputsPath('node', outputDir)),
80+
).resolves.toBeFalsy();
81+
});
82+
83+
it('should write runner outputs with --cache.write option', async () => {
84+
const report = await collect({
85+
...MINIMAL_CONFIG_MOCK,
86+
persist: { outputDir },
87+
cache: { read: false, write: true },
88+
progress: false,
89+
});
90+
91+
expect(report.plugins[0]).toStrictEqual({
92+
...expectedPluginOutput,
93+
duration: expect.any(Number),
94+
});
95+
expect(report.plugins[0]?.duration).toBeGreaterThanOrEqual(0);
96+
97+
await expect(
98+
readJsonFile(getRunnerOutputsPath('node', outputDir)),
99+
).resolves.toStrictEqual(expectedCachedOutput);
100+
});
101+
102+
it('should read runner outputs with --cache.read option and have plugin duraton 0', async () => {
103+
const cacheFilePath = getRunnerOutputsPath('node', outputDir);
104+
await ensureDirectoryExists(path.dirname(cacheFilePath));
105+
await writeFile(cacheFilePath, JSON.stringify(expectedCachedTestData));
106+
107+
const report = await collect({
108+
...MINIMAL_CONFIG_MOCK,
109+
persist: { outputDir },
110+
cache: { read: true, write: false },
111+
progress: false,
112+
});
113+
114+
expect(report.plugins[0]?.audits[0]).toStrictEqual(
115+
expect.objectContaining(expectedCachedTestData[0]),
116+
);
117+
118+
expect(report.plugins[0]).toStrictEqual({
119+
...expectedPluginOutput,
120+
duration: 0,
121+
audits: expect.any(Array),
122+
});
123+
124+
await expect(readJsonFile(cacheFilePath)).resolves.toStrictEqual(
125+
expectedCachedTestData,
126+
);
127+
});
128+
129+
it('should execute runner and write cache with --cache option', async () => {
130+
const report = await collect({
131+
...MINIMAL_CONFIG_MOCK,
132+
persist: { outputDir },
133+
cache: { read: true, write: true },
134+
progress: false,
135+
});
136+
137+
expect(report.plugins[0]).toStrictEqual({
138+
...expectedPluginOutput,
139+
duration: expect.any(Number),
140+
});
141+
expect(report.plugins[0]?.duration).toBeGreaterThanOrEqual(0);
142+
143+
await expect(
144+
readJsonFile(getRunnerOutputsPath('node', outputDir)),
145+
).resolves.toStrictEqual(expectedCachedOutput);
33146
});
34147
});

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

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { createRequire } from 'node:module';
22
import {
3+
type CacheConfigObject,
34
type CoreConfig,
45
DEFAULT_PERSIST_OUTPUT_DIR,
56
type PersistConfig,
@@ -11,23 +12,23 @@ import { executePlugins } from './execute-plugin.js';
1112

1213
export type CollectOptions = Pick<CoreConfig, 'plugins' | 'categories'> & {
1314
persist?: Required<Pick<PersistConfig, 'outputDir'>>;
15+
cache: CacheConfigObject;
1416
} & Partial<GlobalOptions>;
1517

1618
/**
1719
* Run audits, collect plugin output and aggregate it into a JSON object
1820
* @param options
1921
*/
2022
export async function collect(options: CollectOptions): Promise<Report> {
21-
const { plugins, categories, persist, ...otherOptions } = options;
23+
const { plugins, categories, persist, cache, ...otherOptions } = options;
2224
const date = new Date().toISOString();
2325
const start = performance.now();
2426
const commit = await getLatestCommit();
2527
const pluginOutputs = await executePlugins(
2628
{
2729
plugins,
2830
persist: { outputDir: DEFAULT_PERSIST_OUTPUT_DIR, ...persist },
29-
// implement together with CLI option
30-
cache: { read: false, write: false },
31+
cache,
3132
},
3233
otherOptions,
3334
);

0 commit comments

Comments
 (0)