Skip to content

Commit fa1a993

Browse files
committed
chore(plugin-coverage): adjust nx helper to search reports dir in test config
1 parent be8701a commit fa1a993

2 files changed

Lines changed: 221 additions & 29 deletions

File tree

packages/plugin-coverage/src/lib/nx/coverage-paths.ts

Lines changed: 71 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1+
/// <reference types="vitest" />
12
import type { ProjectGraphProjectNode, TargetConfiguration } from '@nx/devkit';
23
import chalk from 'chalk';
34
import { join } from 'node:path';
4-
import { ui } from '@code-pushup/utils';
5+
import { importEsmModule, ui } from '@code-pushup/utils';
56
import { CoverageResult } from '../config';
67

78
/**
@@ -21,25 +22,33 @@ export async function getNxCoveragePaths(
2122
const { createProjectGraphAsync } = await import('@nx/devkit');
2223
const { nodes } = await createProjectGraphAsync({ exitOnError: false });
2324

24-
const coverageResults = targets.map(target => {
25-
const relevantNodes = Object.values(nodes).filter(graph =>
26-
hasNxTarget(graph, target),
27-
);
25+
const coverageResults = await Promise.all(
26+
targets.map(async target => {
27+
const relevantNodes = Object.values(nodes).filter(graph =>
28+
hasNxTarget(graph, target),
29+
);
2830

29-
return relevantNodes.map<CoverageResult>(({ name, data }) => {
30-
const targetConfig = data.targets?.[target] as TargetConfiguration;
31-
const coveragePath = getCoveragePathForTarget(target, targetConfig, name);
32-
const rootToReportsDir = join(data.root, coveragePath);
33-
34-
if (verbose) {
35-
ui().logger.info(`- ${name}: ${target}`);
36-
}
37-
return {
38-
pathToProject: data.root,
39-
resultsPath: join(rootToReportsDir, 'lcov.info'),
40-
};
41-
});
42-
});
31+
return await Promise.all(
32+
relevantNodes.map<Promise<CoverageResult>>(async ({ name, data }) => {
33+
const targetConfig = data.targets?.[target] as TargetConfiguration;
34+
const coveragePath = await getCoveragePathForTarget(
35+
target,
36+
targetConfig,
37+
name,
38+
);
39+
const rootToReportsDir = join(data.root, coveragePath);
40+
41+
if (verbose) {
42+
ui().logger.info(`- ${name}: ${target}`);
43+
}
44+
return {
45+
pathToProject: data.root,
46+
resultsPath: join(rootToReportsDir, 'lcov.info'),
47+
};
48+
}),
49+
);
50+
}),
51+
);
4352

4453
if (verbose) {
4554
ui().logger.info('\n');
@@ -55,33 +64,66 @@ function hasNxTarget(
5564
return project.data.targets != null && target in project.data.targets;
5665
}
5766

58-
function getCoveragePathForTarget(
67+
export type VitestCoverageConfig = {
68+
test: {
69+
coverage?: {
70+
reporter?: string[];
71+
reportsDirectory?: string;
72+
};
73+
};
74+
};
75+
76+
export type JestCoverageConfig = {
77+
coverageDirectory?: string;
78+
coverageReporters?: string[];
79+
};
80+
81+
export async function getCoveragePathForTarget(
5982
target: string,
6083
targetConfig: TargetConfiguration,
6184
projectName: string,
62-
): string {
85+
): Promise<string> {
86+
const { config } = targetConfig.options as { config: string };
87+
6388
if (targetConfig.executor?.includes('@nx/vite')) {
64-
const { reportsDirectory } = targetConfig.options as {
65-
reportsDirectory?: string;
66-
};
89+
const testConfig = await importEsmModule<VitestCoverageConfig>({
90+
filepath: config,
91+
});
92+
93+
const reportsDirectory = testConfig.test.coverage?.reportsDirectory;
94+
const reporter = testConfig.test.coverage?.reporter;
6795

6896
if (reportsDirectory == null) {
6997
throw new Error(
70-
`Coverage configuration not found for target ${target} in ${projectName}. Define your Vitest coverage directory in the reportsDirectory option.`,
98+
`Vitest coverage configuration at ${config} does not include coverage path for target ${target} in ${projectName}. Add the path under coverage > reportsDirectory.`,
99+
);
100+
}
101+
102+
if (!reporter?.includes('lcov')) {
103+
throw new Error(
104+
`Vitest coverage configuration at ${config} does not include LCOV report format for target ${target} in ${projectName}. Add 'lcov' format under coverage > reporter.`,
71105
);
72106
}
73107

74108
return reportsDirectory;
75109
}
76110

77111
if (targetConfig.executor?.includes('@nx/jest')) {
78-
const { coverageDirectory } = targetConfig.options as {
79-
coverageDirectory?: string;
80-
};
112+
const testConfig = await importEsmModule<JestCoverageConfig>({
113+
filepath: config,
114+
});
115+
116+
const coverageDirectory = testConfig.coverageDirectory;
81117

82118
if (coverageDirectory == null) {
83119
throw new Error(
84-
`Coverage configuration not found for target ${target} in ${projectName}. Define your Jest coverage directory in the coverageDirectory option.`,
120+
`Jest coverage configuration at ${config} does not include coverage path for target ${target} in ${projectName}. Add the path under coverageDirectory.`,
121+
);
122+
}
123+
124+
if (!testConfig.coverageReporters?.includes('lcov')) {
125+
throw new Error(
126+
`Jest coverage configuration at ${config} does not include LCOV report format for target ${target} in ${projectName}. Add 'lcov' format under coverageReporters.`,
85127
);
86128
}
87129
return coverageDirectory;
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
import { vol } from 'memfs';
2+
import { describe, expect, it } from 'vitest';
3+
import { MEMFS_VOLUME } from '@code-pushup/test-utils';
4+
import {
5+
JestCoverageConfig,
6+
VitestCoverageConfig,
7+
getCoveragePathForTarget,
8+
} from './coverage-paths';
9+
10+
vi.mock('bundle-require', () => ({
11+
bundleRequire: vi.fn().mockImplementation((options: { filepath: string }) => {
12+
const config = options.filepath.split('.')[0];
13+
const VITEST_VALID: VitestCoverageConfig = {
14+
test: {
15+
coverage: { reporter: ['lcov'], reportsDirectory: 'coverage/cli' },
16+
},
17+
};
18+
19+
const VITEST_NO_DIR: VitestCoverageConfig = {
20+
test: { coverage: { reporter: ['lcov'] } },
21+
};
22+
23+
const JEST_VALID: JestCoverageConfig = {
24+
coverageReporters: ['lcov'],
25+
coverageDirectory: 'coverage/core',
26+
};
27+
28+
const JEST_NO_DIR: JestCoverageConfig = {
29+
coverageReporters: ['lcov'],
30+
};
31+
32+
const JEST_NO_LCOV: JestCoverageConfig = {
33+
coverageReporters: ['json'],
34+
coverageDirectory: 'coverage/utils',
35+
};
36+
37+
return {
38+
mod: {
39+
default:
40+
config === 'vitest-valid'
41+
? VITEST_VALID
42+
: config === 'jest-valid'
43+
? JEST_VALID
44+
: config === 'vitest-no-dir'
45+
? VITEST_NO_DIR
46+
: config === 'jest-no-dir'
47+
? JEST_NO_DIR
48+
: JEST_NO_LCOV,
49+
},
50+
};
51+
}),
52+
}));
53+
54+
describe('getCoveragePathForTarget', () => {
55+
beforeEach(() => {
56+
vol.fromJSON(
57+
{
58+
// values come from bundle-require mock above
59+
'vitest-valid.config.unit.ts': '',
60+
'jest-valid.config.unit.ts': '',
61+
'vitest-no-dir.config.integration.ts': '',
62+
'jest-no-dir.config.integration.ts': '',
63+
'jest-no-lcov.config.integration.ts': '',
64+
},
65+
MEMFS_VOLUME,
66+
);
67+
});
68+
69+
it('should fetch Vitest reportsDirectory', async () => {
70+
await expect(
71+
getCoveragePathForTarget(
72+
'unit-test',
73+
{
74+
executor: '@nx/vite:test',
75+
options: { config: 'vitest-valid.config.unit.ts' },
76+
},
77+
'cli',
78+
),
79+
).resolves.toBe('coverage/cli');
80+
});
81+
82+
it('should fetch Jest coverageDirectory', async () => {
83+
await expect(
84+
getCoveragePathForTarget(
85+
'unit-test',
86+
{
87+
executor: '@nx/jest:jest',
88+
options: { config: 'jest-valid.config.unit.ts' },
89+
},
90+
'core',
91+
),
92+
).resolves.toBe('coverage/core');
93+
});
94+
95+
it('should throw when reportsDirectory is not set in vitest config', async () => {
96+
await expect(() =>
97+
getCoveragePathForTarget(
98+
'integration-test',
99+
{
100+
executor: '@nx/vite:test',
101+
options: { config: 'vitest-no-dir.config.integration.ts' },
102+
},
103+
'cli',
104+
),
105+
).rejects.toThrow(
106+
/configuration .* does not include coverage path .* Add the path under coverage > reportsDirectory/,
107+
);
108+
});
109+
110+
it('should throw when reportsDirectory is not set in jest config', async () => {
111+
await expect(() =>
112+
getCoveragePathForTarget(
113+
'integration-test',
114+
{
115+
executor: '@nx/jest:jest',
116+
options: { config: 'jest-no-dir.config.integration.ts' },
117+
},
118+
'core',
119+
),
120+
).rejects.toThrow(
121+
/configuration .* does not include coverage path .* Add the path under coverageDirectory/,
122+
);
123+
});
124+
125+
it('should throw when config does not include lcov reporter', async () => {
126+
await expect(() =>
127+
getCoveragePathForTarget(
128+
'integration-test',
129+
{
130+
executor: '@nx/jest:jest',
131+
options: { config: 'jest-no-lcov.config.integration.ts' },
132+
},
133+
'core',
134+
),
135+
).rejects.toThrow(/configuration .* does not include LCOV report format/);
136+
});
137+
138+
it('should throw for unsupported executor (only vitest and jest are supported)', async () => {
139+
await expect(() =>
140+
getCoveragePathForTarget(
141+
'component-test',
142+
{
143+
executor: '@nx/cypress',
144+
options: { config: 'cypress.config.ts' },
145+
},
146+
'ui',
147+
),
148+
).rejects.toThrow('Unsupported executor @nx/cypress.');
149+
});
150+
});

0 commit comments

Comments
 (0)