Skip to content

Commit fb6dae2

Browse files
9aoyCopilot
andauthored
feat: coverage.includes works in projects (#593)
Co-authored-by: Copilot <[email protected]>
1 parent 1a2eacb commit fb6dae2

File tree

10 files changed

+132
-49
lines changed

10 files changed

+132
-49
lines changed

e2e/projects/coverage.test.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,31 @@ describe('test projects coverage', () => {
3838
),
3939
).toBeTruthy();
4040

41+
expect(
42+
logs.find((log) => log.includes('App1.ts') && log.includes('|')),
43+
).toBeFalsy();
44+
4145
expect(
4246
fs.existsSync(join(__dirname, 'fixtures/coverage/index.html')),
4347
).toBeTruthy();
4448
}, 15000);
49+
50+
it('should run projects correctly with coverage.include', async () => {
51+
const { cli, expectExecSuccess } = await runRstestCli({
52+
command: 'rstest',
53+
args: ['run', '--globals', '-c', 'rstest.coverage.include.config.ts'],
54+
options: {
55+
nodeOptions: {
56+
cwd: join(__dirname, 'fixtures'),
57+
},
58+
},
59+
});
60+
61+
await expectExecSuccess();
62+
const logs = cli.stdout.split('\n').filter(Boolean);
63+
64+
expect(
65+
logs.find((log) => log.includes('App1.ts') && log.includes('|')),
66+
).toBeTruthy();
67+
}, 15000);
4568
});
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
const App = () => {
2+
return (
3+
<div className="content">
4+
<h1>Rsbuild with React</h1>
5+
<p>Start building amazing things with Rsbuild.</p>
6+
</div>
7+
);
8+
};
9+
10+
export default App;

e2e/projects/fixtures/rstest.coverage.config.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,8 @@ import { defineConfig } from '@rstest/core';
22

33
export default defineConfig({
44
projects: ['packages/*'],
5-
globals: true,
65
coverage: {
76
enabled: true,
7+
reporters: ['text', 'html'],
88
},
9-
setupFiles: ['./setup.ts'],
109
});
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { defineConfig } from '@rstest/core';
2+
3+
export default defineConfig({
4+
projects: ['packages/*'],
5+
coverage: {
6+
enabled: true,
7+
reporters: ['text'],
8+
include: ['src/**/*'],
9+
},
10+
});

e2e/test-coverage/fixtures/rstest.config.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,7 @@ import { defineConfig } from '@rstest/core';
22

33
export default defineConfig({
44
setupFiles: ['./rstest.setup.ts'],
5+
coverage: {
6+
reporters: ['text'],
7+
},
58
});

packages/core/src/core/runTests.ts

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -271,12 +271,7 @@ export async function runTests(context: Rstest): Promise<void> {
271271
if (coverageProvider) {
272272
const { generateCoverage } = await import('../coverage/generate');
273273

274-
await generateCoverage(
275-
context.normalizedConfig.coverage,
276-
context.rootPath,
277-
results,
278-
coverageProvider,
279-
);
274+
await generateCoverage(context, results, coverageProvider);
280275
}
281276
};
282277

packages/core/src/coverage/generate.ts

Lines changed: 47 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { normalize } from 'pathe';
22
import { glob, isDynamicPattern } from 'tinyglobby';
3-
import type { TestFileResult } from '../types';
3+
import type { RstestContext, TestFileResult } from '../types';
44
import type {
55
CoverageMap,
66
CoverageOptions,
@@ -45,11 +45,15 @@ const getIncludedFiles = async (
4545
};
4646

4747
export async function generateCoverage(
48-
coverage: CoverageOptions,
49-
rootPath: string,
48+
context: RstestContext,
5049
results: TestFileResult[],
5150
coverageProvider: CoverageProvider,
5251
): Promise<void> {
52+
const {
53+
rootPath,
54+
normalizedConfig: { coverage },
55+
projects,
56+
} = context;
5357
try {
5458
const finalCoverageMap = coverageProvider.createCoverageMap();
5559

@@ -61,24 +65,46 @@ export async function generateCoverage(
6165
}
6266

6367
if (coverage.include?.length) {
64-
const allFiles = await getIncludedFiles(coverage, rootPath);
68+
const coveredFiles = finalCoverageMap.files();
6569

66-
// should be better to filter files before swc coverage is processed
67-
finalCoverageMap.filter((file) => allFiles.includes(normalize(file)));
70+
let isTimeout = false;
6871

69-
const coveredFiles = finalCoverageMap.files();
72+
const timeoutId = setTimeout(() => {
73+
isTimeout = true;
74+
logger.info('Generating coverage for untested files...');
75+
}, 1000);
76+
77+
const allFiles = (
78+
await Promise.all(
79+
projects.map(async (p) => {
80+
const includedFiles = await getIncludedFiles(coverage, p.rootPath);
81+
82+
const uncoveredFiles = includedFiles.filter(
83+
(file) => !coveredFiles.includes(normalize(file)),
84+
);
7085

71-
const uncoveredFiles = allFiles.filter(
72-
(file) => !coveredFiles.includes(normalize(file)),
73-
);
86+
if (uncoveredFiles.length) {
87+
await generateCoverageForUntestedFiles(
88+
p.environmentName,
89+
uncoveredFiles,
90+
finalCoverageMap,
91+
coverageProvider,
92+
);
93+
}
7494

75-
if (uncoveredFiles.length) {
76-
await generateCoverageForUntestedFiles(
77-
uncoveredFiles,
78-
finalCoverageMap,
79-
coverageProvider,
80-
);
95+
return includedFiles;
96+
}),
97+
)
98+
).flat();
99+
100+
clearTimeout(timeoutId);
101+
102+
if (isTimeout) {
103+
logger.info('Coverage for untested files generated.');
81104
}
105+
106+
// should be better to filter files before swc coverage is processed
107+
finalCoverageMap.filter((file) => allFiles.includes(normalize(file)));
82108
}
83109

84110
// Generate coverage reports
@@ -104,21 +130,22 @@ export async function generateCoverage(
104130
}
105131

106132
async function generateCoverageForUntestedFiles(
133+
environmentName: string,
107134
uncoveredFiles: string[],
108135
coverageMap: CoverageMap,
109136
coverageProvider: CoverageProvider,
110137
): Promise<void> {
111-
logger.debug('Generating coverage for untested files...');
112-
113138
if (!coverageProvider.generateCoverageForUntestedFiles) {
114139
logger.warn(
115140
'Current coverage provider does not support generating coverage for untested files.',
116141
);
117142
return;
118143
}
119144

120-
const coverages =
121-
await coverageProvider.generateCoverageForUntestedFiles(uncoveredFiles);
145+
const coverages = await coverageProvider.generateCoverageForUntestedFiles({
146+
environmentName,
147+
files: uncoveredFiles,
148+
});
122149

123150
coverages.forEach((coverageData) => {
124151
coverageMap.addFileCoverage(coverageData);

packages/core/src/types/coverage.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -129,9 +129,10 @@ export declare class CoverageProvider {
129129
/**
130130
* Generate coverage for untested files
131131
*/
132-
generateCoverageForUntestedFiles(
133-
untestedFiles: string[],
134-
): Promise<FileCoverageData[]>;
132+
generateCoverageForUntestedFiles(params: {
133+
environmentName: string;
134+
files: string[];
135+
}): Promise<FileCoverageData[]>;
135136

136137
/**
137138
* Generate coverage reports

packages/coverage-istanbul/src/plugin.ts

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,24 @@
11
import { createRequire } from 'node:module';
22
import type { NormalizedCoverageOptions, RsbuildPlugin } from '@rstest/core';
33

4-
let transformCoverageFn:
5-
| ((code: string, filename: string) => Promise<{ code: string; map?: any }>)
6-
| undefined;
4+
type TransformCoverageFn = (
5+
code: string,
6+
filename: string,
7+
) => Promise<{ code: string; map?: any }>;
78

8-
const transformCoverage: NonNullable<typeof transformCoverageFn> = async (
9-
code,
10-
filename,
11-
) => {
12-
if (!transformCoverageFn) {
9+
const transformCoverageFns: Record<string, TransformCoverageFn> = {};
10+
11+
const transformCoverage = async (
12+
environmentName: string,
13+
code: string,
14+
filename: string,
15+
): ReturnType<TransformCoverageFn> => {
16+
if (!transformCoverageFns[environmentName]) {
1317
throw new Error(
14-
'Can not transform coverage since swc transform function is not registered',
18+
`Can not transform coverage since swc transform function for ${environmentName} is not registered`,
1519
);
1620
}
17-
return transformCoverageFn(code, filename);
21+
return transformCoverageFns[environmentName](code, filename);
1822
};
1923

2024
export { transformCoverage };
@@ -50,14 +54,17 @@ export const pluginCoverage: (
5054
});
5155

5256
api.modifyBundlerChain({
53-
handler: (chain, { rspack, CHAIN_ID }) => {
57+
handler: (chain, { rspack, CHAIN_ID, environment }) => {
5458
const { rspackExperiments: _rspackExperiments, ...swcOptions } =
5559
chain.module
5660
.rule(CHAIN_ID.RULE.JS)
5761
.use(CHAIN_ID.USE.SWC)
5862
.get('options') || {};
5963

60-
transformCoverageFn = async (code: string, filename: string) =>
64+
transformCoverageFns[environment.name] = async (
65+
code: string,
66+
filename: string,
67+
) =>
6168
rspack.experiments.swc.transform(code, {
6269
...swcOptions,
6370
filename,
@@ -67,7 +74,7 @@ export const pluginCoverage: (
6774
});
6875

6976
api.onExit(() => {
70-
transformCoverageFn = undefined;
77+
Object.assign(transformCoverageFns, {});
7178
});
7279
},
7380
});

packages/coverage-istanbul/src/provider.ts

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,13 @@ export class CoverageProvider implements RstestCoverageProvider {
2626
}
2727
}
2828

29-
async generateCoverageForUntestedFiles(
30-
uncoveredFiles: string[],
31-
): Promise<FileCoverageData[]> {
29+
async generateCoverageForUntestedFiles({
30+
environmentName,
31+
files,
32+
}: {
33+
environmentName: string;
34+
files: string[];
35+
}): Promise<FileCoverageData[]> {
3236
const { transformCoverage } = await import('./plugin');
3337

3438
const { readInitialCoverage } = await import('istanbul-lib-instrument');
@@ -41,10 +45,14 @@ export class CoverageProvider implements RstestCoverageProvider {
4145
const { readFile } = await import('node:fs/promises');
4246

4347
return await Promise.all(
44-
uncoveredFiles.map(async (file) => {
48+
files.map(async (file) => {
4549
try {
4650
const content = await readFile(file, 'utf-8');
47-
const { code } = await transformCoverage(content, file);
51+
const { code } = await transformCoverage(
52+
environmentName,
53+
content,
54+
file,
55+
);
4856
// replace _coverageSchema: "${swc_value}" to _coverageSchema: ${MAGIC_VALUE}
4957
const { coverageData } =
5058
readInitialCoverage(

0 commit comments

Comments
 (0)