Skip to content

Commit 317fa1e

Browse files
authored
feat: support coverage.include (#585)
1 parent ee30e25 commit 317fa1e

File tree

17 files changed

+361
-56
lines changed

17 files changed

+361
-56
lines changed

e2e/scripts/index.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,17 @@ export async function runRstestCli({
109109
}
110110
};
111111

112+
const expectExecFailed = async () => {
113+
await cli.exec;
114+
const exitCode = cli.exec.process?.exitCode;
115+
if (exitCode === 0) {
116+
const logs = cli.stdout.split('\n').filter(Boolean);
117+
throw new Error(
118+
`expect test failed but passed. Logs:\n\n${logs.join('\n')}`,
119+
);
120+
}
121+
};
122+
112123
const expectLog = (
113124
msg: string | RegExp,
114125
logs: string[] = cli.stdout.split('\n').filter(Boolean),
@@ -125,7 +136,7 @@ export async function runRstestCli({
125136
}
126137
};
127138

128-
return { cli, expectExecSuccess, expectLog };
139+
return { cli, expectExecSuccess, expectExecFailed, expectLog };
129140
}
130141

131142
export async function prepareFixtures({
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { defineConfig } from '@rstest/core';
2+
3+
export default defineConfig({
4+
coverage: {
5+
enabled: true,
6+
provider: 'istanbul',
7+
include: ['src/**/*.{js,jsx,ts,tsx}'],
8+
reporters: ['text'],
9+
},
10+
setupFiles: ['./rstest.setup.ts'],
11+
});

e2e/test-coverage/include.test.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { join } from 'node:path';
2+
import { describe, expect, it } from '@rstest/core';
3+
import { runRstestCli } from '../scripts';
4+
5+
describe('test coverage-istanbul include option', () => {
6+
it('coverage-istanbul should be works with include option', async () => {
7+
const { expectExecSuccess, expectLog, cli } = await runRstestCli({
8+
command: 'rstest',
9+
args: ['run', 'date', '-c', 'rstest.include.config.ts'],
10+
options: {
11+
nodeOptions: {
12+
cwd: join(__dirname, 'fixtures'),
13+
},
14+
},
15+
});
16+
17+
await expectExecSuccess();
18+
19+
const logs = cli.stdout.split('\n').filter(Boolean);
20+
// test coverage
21+
expect(
22+
logs
23+
.find((log) => log.includes('index.ts') && log.includes('|'))
24+
?.replaceAll(' ', ''),
25+
).toMatchInlineSnapshot(`"index.ts|0|100|0|0|1"`);
26+
expect(
27+
logs
28+
.find((log) => log.includes('date.ts') && log.includes('|'))
29+
?.replaceAll(' ', ''),
30+
).toMatchInlineSnapshot(`"date.ts|100|100|100|100|"`);
31+
32+
expectLog('Test Files 1 passed', logs);
33+
});
34+
});

e2e/test-coverage/thresholds.test.ts

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import { join } from 'node:path';
2-
import { describe, expect, it } from '@rstest/core';
2+
import { describe, it } from '@rstest/core';
33
import { runRstestCli } from '../scripts';
44

55
describe('coverageThresholds', () => {
66
it('should check global threshold correctly', async () => {
7-
const { expectLog, cli } = await runRstestCli({
7+
const { expectLog, cli, expectExecFailed } = await runRstestCli({
88
command: 'rstest',
99
args: ['run', '-c', 'rstest.thresholds.config.ts'],
1010
options: {
@@ -14,10 +14,7 @@ describe('coverageThresholds', () => {
1414
},
1515
});
1616

17-
await cli.exec;
18-
const exitCode = cli.exec.process?.exitCode;
19-
20-
expect(exitCode).toBe(1);
17+
await expectExecFailed();
2118

2219
const logs = cli.stdout.split('\n').filter(Boolean);
2320

@@ -33,7 +30,7 @@ describe('coverageThresholds', () => {
3330
});
3431

3532
it('should check glob threshold correctly', async () => {
36-
const { expectLog, cli } = await runRstestCli({
33+
const { expectLog, expectExecFailed, cli } = await runRstestCli({
3734
command: 'rstest',
3835
args: ['run', '-c', 'rstest.globThresholds.config.ts'],
3936
options: {
@@ -43,10 +40,7 @@ describe('coverageThresholds', () => {
4340
},
4441
});
4542

46-
await cli.exec;
47-
const exitCode = cli.exec.process?.exitCode;
48-
49-
expect(exitCode).toBe(1);
43+
await expectExecFailed();
5044

5145
const logs = cli.stdout.split('\n').filter(Boolean);
5246

packages/core/src/config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ const createDefaultConfig = (): NormalizedConfig => ({
117117
coverage: {
118118
exclude: [
119119
'**/node_modules/**',
120+
'**/[.]*',
120121
'**/dist/**',
121122
'**/test/**',
122123
'**/__tests__/**',

packages/core/src/core/runTests.ts

Lines changed: 7 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -269,45 +269,14 @@ export async function runTests(context: Rstest): Promise<void> {
269269

270270
// Generate coverage reports after all tests complete
271271
if (coverageProvider) {
272-
try {
273-
// Collect coverage data from all test results
274-
const finalCoverageMap = coverageProvider.createCoverageMap();
275-
276-
// Merge coverage data from all test files
277-
for (const result of results) {
278-
if ((result as any).coverage) {
279-
finalCoverageMap.merge((result as any).coverage);
280-
}
281-
}
282-
283-
// Generate coverage reports
284-
await coverageProvider.generateReports(
285-
finalCoverageMap,
286-
context.normalizedConfig.coverage,
287-
);
272+
const { generateCoverage } = await import('../coverage/generate');
288273

289-
if (context.normalizedConfig.coverage.thresholds) {
290-
const { checkThresholds } = await import(
291-
'../coverage/checkThresholds'
292-
);
293-
const thresholdResult = checkThresholds({
294-
coverageMap: finalCoverageMap,
295-
thresholds: context.normalizedConfig.coverage.thresholds,
296-
coverageProvider,
297-
rootPath: context.rootPath,
298-
});
299-
if (!thresholdResult.success) {
300-
process.exitCode = 1;
301-
logger.log('');
302-
logger.log(thresholdResult.message);
303-
}
304-
}
305-
306-
// Cleanup
307-
coverageProvider.cleanup();
308-
} catch (error) {
309-
logger.error('Failed to generate coverage reports:', error);
310-
}
274+
await generateCoverage(
275+
context.normalizedConfig.coverage,
276+
context.rootPath,
277+
results,
278+
coverageProvider,
279+
);
311280
}
312281
};
313282

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import { normalize } from 'pathe';
2+
import { glob } from 'tinyglobby';
3+
import type { TestFileResult } from '../types';
4+
import type {
5+
CoverageMap,
6+
CoverageOptions,
7+
CoverageProvider,
8+
} from '../types/coverage';
9+
import { logger } from '../utils';
10+
11+
export async function generateCoverage(
12+
coverage: CoverageOptions,
13+
rootPath: string,
14+
results: TestFileResult[],
15+
coverageProvider: CoverageProvider,
16+
): Promise<void> {
17+
try {
18+
const finalCoverageMap = coverageProvider.createCoverageMap();
19+
20+
// Merge coverage data from all test files
21+
for (const result of results) {
22+
if (result.coverage) {
23+
finalCoverageMap.merge(result.coverage);
24+
}
25+
}
26+
27+
if (coverage.include?.length) {
28+
const allFiles = await glob(coverage.include, {
29+
cwd: rootPath,
30+
absolute: true,
31+
ignore: coverage.exclude,
32+
dot: true,
33+
expandDirectories: false,
34+
});
35+
36+
// should be better to filter files before swc coverage is processed
37+
finalCoverageMap.filter((file) => allFiles.includes(normalize(file)));
38+
39+
const coveredFiles = finalCoverageMap.files();
40+
41+
const uncoveredFiles = allFiles.filter(
42+
(file) => !coveredFiles.includes(normalize(file)),
43+
);
44+
45+
if (uncoveredFiles.length) {
46+
await generateCoverageForUntestedFiles(
47+
uncoveredFiles,
48+
finalCoverageMap,
49+
coverageProvider,
50+
);
51+
}
52+
}
53+
54+
// Generate coverage reports
55+
await coverageProvider.generateReports(finalCoverageMap, coverage);
56+
57+
if (coverage.thresholds) {
58+
const { checkThresholds } = await import('../coverage/checkThresholds');
59+
const thresholdResult = checkThresholds({
60+
coverageMap: finalCoverageMap,
61+
coverageProvider,
62+
rootPath,
63+
thresholds: coverage.thresholds,
64+
});
65+
if (!thresholdResult.success) {
66+
process.exitCode = 1;
67+
logger.log('');
68+
logger.log(thresholdResult.message);
69+
}
70+
}
71+
72+
// Cleanup
73+
coverageProvider.cleanup();
74+
} catch (error) {
75+
logger.error('Failed to generate coverage reports:', error);
76+
}
77+
}
78+
79+
async function generateCoverageForUntestedFiles(
80+
uncoveredFiles: string[],
81+
coverageMap: CoverageMap,
82+
coverageProvider: CoverageProvider,
83+
): Promise<void> {
84+
logger.debug('Generating coverage for untested files...');
85+
86+
const coverages =
87+
await coverageProvider.generateCoverageForUntestedFiles(uncoveredFiles);
88+
89+
coverages.forEach((coverageData) => {
90+
coverageMap.addFileCoverage(coverageData);
91+
});
92+
}

packages/core/src/runtime/worker/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -353,7 +353,7 @@ const runInPool = async (
353353
const coverageMap = coverageProvider.collect();
354354
if (coverageMap) {
355355
// Attach coverage data to test result
356-
(test as any).coverage = coverageMap.toJSON();
356+
test.coverage = coverageMap.toJSON();
357357
}
358358
}
359359
await rpc.onTestFileResult(test);

packages/core/src/types/coverage.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type {
22
CoverageMap,
33
CoverageMapData,
44
CoverageSummary,
5+
FileCoverageData,
56
Totals,
67
} from 'istanbul-lib-coverage';
78
import type { ReportOptions } from 'istanbul-reports';
@@ -40,6 +41,14 @@ export type CoverageOptions = {
4041
*/
4142
enabled?: boolean;
4243

44+
/**
45+
* A list of glob patterns that should be included for coverage collection.
46+
* Only collect coverage for tested files by default.
47+
*
48+
* @default undefined
49+
*/
50+
include?: string[];
51+
4352
/**
4453
* A list of glob patterns that should be excluded from coverage collection.
4554
*
@@ -88,9 +97,10 @@ export type CoverageOptions = {
8897
};
8998

9099
export type NormalizedCoverageOptions = Required<
91-
Omit<CoverageOptions, 'thresholds'>
100+
Omit<CoverageOptions, 'thresholds' | 'include'>
92101
> & {
93102
thresholds?: CoverageThresholds;
103+
include?: string[];
94104
};
95105

96106
export declare class CoverageProvider {
@@ -110,6 +120,13 @@ export declare class CoverageProvider {
110120
*/
111121
createCoverageMap(): CoverageMap;
112122

123+
/**
124+
* Generate coverage for untested files
125+
*/
126+
generateCoverageForUntestedFiles(
127+
untestedFiles: string[],
128+
): Promise<FileCoverageData[]>;
129+
113130
/**
114131
* Generate coverage reports
115132
*/

packages/core/src/types/testSuite.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import type {
55
OnTestFinishedHandler,
66
TestContext,
77
} from './api';
8+
import type { CoverageMapData } from './coverage';
89
import type { MaybePromise, TestPath } from './utils';
910

1011
export type TestRunMode = 'run' | 'skip' | 'todo' | 'only';
@@ -126,6 +127,7 @@ export type TestResult = {
126127
export type TestFileResult = TestResult & {
127128
results: TestResult[];
128129
snapshotResult?: SnapshotResult;
130+
coverage?: CoverageMapData;
129131
};
130132

131133
export interface UserConsoleLog {

0 commit comments

Comments
 (0)