Skip to content

Commit 3b4c84f

Browse files
authored
feat: support check coverage threshold for per file (#590)
1 parent 317fa1e commit 3b4c84f

File tree

8 files changed

+121
-23
lines changed

8 files changed

+121
-23
lines changed
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { defineConfig } from '@rstest/core';
2+
3+
export default defineConfig({
4+
coverage: {
5+
enabled: true,
6+
provider: 'istanbul',
7+
reporters: [],
8+
thresholds: {
9+
'src/**': {
10+
perFile: true,
11+
statements: 100,
12+
},
13+
},
14+
},
15+
setupFiles: ['./rstest.setup.ts'],
16+
});

e2e/test-coverage/thresholds.test.ts

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,12 @@ describe('coverageThresholds', () => {
1919
const logs = cli.stdout.split('\n').filter(Boolean);
2020

2121
expectLog(
22-
/Coverage for statements .* does not meet global threshold/,
22+
/Coverage for statements .* does not meet global threshold/i,
2323
logs,
2424
);
2525

2626
expectLog(
27-
/Uncovered lines .* exceeds maximum global threshold allowed/,
27+
/Uncovered lines .* exceeds maximum global threshold allowed/i,
2828
logs,
2929
);
3030
});
@@ -45,10 +45,31 @@ describe('coverageThresholds', () => {
4545
const logs = cli.stdout.split('\n').filter(Boolean);
4646

4747
expectLog(
48-
/Coverage for statements .* does not meet "src\/\*\*" threshold/,
48+
/Error: coverage for statements .* does not meet "src\/\*\*" threshold/i,
4949
logs,
5050
);
5151

52-
expectLog(/Coverage data for "node\/\*\*" was not found/, logs);
52+
expectLog(/Coverage data for "node\/\*\*" was not found/i, logs);
53+
});
54+
55+
it('should check per files threshold correctly', async () => {
56+
const { expectLog, expectExecFailed, cli } = await runRstestCli({
57+
command: 'rstest',
58+
args: ['run', '-c', 'rstest.perFileThresholds.config.ts'],
59+
options: {
60+
nodeOptions: {
61+
cwd: join(__dirname, 'fixtures'),
62+
},
63+
},
64+
});
65+
66+
await expectExecFailed();
67+
68+
const logs = cli.stdout.split('\n').filter(Boolean);
69+
70+
expectLog(
71+
/src\/string.ts coverage for statements .* does not meet "src\/\*\*" threshold/,
72+
logs,
73+
);
5374
});
5475
});

packages/core/src/coverage/checkThresholds.ts

Lines changed: 27 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ export function checkThresholds({
3838
const thresholdGroup: (CoverageThreshold & {
3939
name: string;
4040
coverageMap: CoverageMap;
41+
perFile?: boolean;
4142
})[] = [
4243
{
4344
statements: thresholds.statements,
@@ -46,6 +47,7 @@ export function checkThresholds({
4647
lines: thresholds.lines,
4748
name: 'global',
4849
coverageMap,
50+
perFile: false,
4951
},
5052
];
5153

@@ -66,7 +68,7 @@ export function checkThresholds({
6668

6769
if (!matchedFiles.length) {
6870
failedThresholds.push(
69-
`${color.red('Error')}: Coverage data for "${key}" was not found`,
71+
`${color.red('Error')}: coverage data for "${key}" was not found`,
7072
);
7173
continue;
7274
}
@@ -87,34 +89,46 @@ export function checkThresholds({
8789
name: keyof CoverageSummary,
8890
type: string,
8991
actual: CoverageSummaryTotals,
90-
expected?: number,
92+
expected: number,
93+
file?: string,
9194
) => {
95+
let errorMsg = '';
9296
if (expected !== undefined) {
9397
// Thresholds specified as a negative number represent the maximum number of uncovered entities allowed.
9498
if (expected < 0) {
9599
const uncovered = actual.total - actual.covered;
96100
if (uncovered > -expected) {
97-
failedThresholds.push(
98-
`${color.red('Error')}: Uncovered ${name} ${color.red(`${uncovered}`)} exceeds maximum ${type === 'global' ? 'global' : `"${type}"`} threshold allowed ${color.yellow(`${-expected}`)}`,
99-
);
101+
errorMsg += `uncovered ${name} ${color.red(`${uncovered}`)} exceeds maximum ${type === 'global' ? 'global' : `"${type}"`} threshold allowed ${color.yellow(`${-expected}`)}`;
100102
}
101103
}
102104
// Thresholds specified as a positive number are taken to be the minimum percentage required.
103105
else if (actual.pct < expected) {
104-
failedThresholds.push(
105-
`${color.red('Error')}: Coverage for ${name} ${color.red(`${actual.pct}%`)} does not meet ${type === 'global' ? 'global' : `"${type}"`} threshold ${color.yellow(`${expected}%`)}`,
106-
);
106+
errorMsg += `coverage for ${name} ${color.red(`${actual.pct}%`)} does not meet ${type === 'global' ? 'global' : `"${type}"`} threshold ${color.yellow(`${expected}%`)}`;
107107
}
108108
}
109+
110+
if (errorMsg) {
111+
failedThresholds.push(
112+
`${color.red('Error')}: ${file ? `${relative(rootPath, file)} ` : ''}${errorMsg}`,
113+
);
114+
}
109115
};
110116

111117
thresholdGroup.forEach(({ name, coverageMap, ...thresholds }) => {
112-
const summary = coverageMap.getCoverageSummary();
113-
THRESHOLD_KEYS.forEach((key) => {
114-
if (thresholds[key] !== undefined) {
115-
check(key, name, summary[key], thresholds[key]);
118+
const summaries = thresholds.perFile
119+
? coverageMap.files().map((file) => ({
120+
file,
121+
summary: coverageMap.fileCoverageFor(file).toSummary(),
122+
}))
123+
: [{ file: '', summary: coverageMap.getCoverageSummary() }];
124+
125+
for (const { summary, file } of summaries) {
126+
for (const key of THRESHOLD_KEYS) {
127+
if (thresholds[key] !== undefined) {
128+
check(key, name, summary[key], thresholds[key], file);
129+
}
116130
}
117-
});
131+
}
118132
});
119133

120134
return {

packages/core/src/coverage/generate.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -63,14 +63,11 @@ export async function generateCoverage(
6363
thresholds: coverage.thresholds,
6464
});
6565
if (!thresholdResult.success) {
66-
process.exitCode = 1;
6766
logger.log('');
6867
logger.log(thresholdResult.message);
68+
process.exitCode = 1;
6969
}
7070
}
71-
72-
// Cleanup
73-
coverageProvider.cleanup();
7471
} catch (error) {
7572
logger.error('Failed to generate coverage reports:', error);
7673
}

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -355,6 +355,8 @@ const runInPool = async (
355355
// Attach coverage data to test result
356356
test.coverage = coverageMap.toJSON();
357357
}
358+
// Cleanup
359+
coverageProvider.cleanup();
358360
}
359361
await rpc.onTestFileResult(test);
360362
},

packages/core/src/types/coverage.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,13 @@ export type CoverageThresholds =
3131
| CoverageThreshold
3232
| (CoverageThreshold & {
3333
/** check thresholds for matched files */
34-
[glob: string]: CoverageThreshold;
34+
[glob: string]: CoverageThreshold & {
35+
/**
36+
* check thresholds per file
37+
* @default false
38+
*/
39+
perFile?: boolean;
40+
};
3541
});
3642

3743
export type CoverageOptions = {

website/docs/en/config/test/coverage.mdx

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -222,7 +222,9 @@ type CoverageThreshold = {
222222

223223
type CoverageThresholds = CoverageThreshold & {
224224
/** check thresholds for matched files */
225-
[glob: string]: CoverageThreshold;
225+
[glob: string]: CoverageThreshold & {
226+
perFile?: boolean;
227+
};
226228
};
227229
```
228230

@@ -287,3 +289,23 @@ Following the above configuration, rstest will fail if:
287289
- The total code coverage of all files in `src/**` is below 100%.
288290
- The total code coverage of all files in `node/**/*.js` is below 90%.
289291
- The global code coverage is below 80% for statements.
292+
293+
#### check threshold for per file
294+
295+
Rstest also supports checking thresholds for each matched file by setting `perFile` to `true`.
296+
297+
```ts title='rstest.config.ts'
298+
import { defineConfig } from '@rstest/core';
299+
300+
export default defineConfig({
301+
coverage: {
302+
enabled: true,
303+
thresholds: {
304+
'src/**': {
305+
statements: 90,
306+
perFile: true,
307+
},
308+
},
309+
},
310+
});
311+
```

website/docs/zh/config/test/coverage.mdx

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,3 +285,23 @@ export default defineConfig({
285285
- `src/**` 匹配到的文件的总语句覆盖率低于 100%。
286286
- `node/**/*.js` 匹配到的文件的总语句覆盖率低于 90%。
287287
- 全局的语句覆盖率低于 80%。
288+
289+
#### 单文件检查
290+
291+
Rstest 支持通过将 `perFile` 设置为 `true` 来为每个匹配文件分别检查阈值。
292+
293+
```ts title='rstest.config.ts'
294+
import { defineConfig } from '@rstest/core';
295+
296+
export default defineConfig({
297+
coverage: {
298+
enabled: true,
299+
thresholds: {
300+
'src/**': {
301+
statements: 90,
302+
perFile: true,
303+
},
304+
},
305+
},
306+
});
307+
```

0 commit comments

Comments
 (0)