Skip to content

Commit 0005e5a

Browse files
authored
feat: support check coverage thresholds (#560)
1 parent 95f3c3e commit 0005e5a

File tree

6 files changed

+127
-4
lines changed

6 files changed

+127
-4
lines changed

e2e/scripts/index.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -110,10 +110,15 @@ export async function runRstestCli({
110110
};
111111

112112
const expectLog = (
113-
msg: string,
113+
msg: string | RegExp,
114114
logs: string[] = cli.stdout.split('\n').filter(Boolean),
115115
) => {
116-
const matchedLog = logs.find((log) => log.includes(msg));
116+
const matchedLog = logs.find((log) => {
117+
if (typeof msg === 'string') {
118+
return log.includes(msg);
119+
}
120+
return log.match(msg);
121+
});
117122

118123
if (!matchedLog) {
119124
throw new Error(`Can't find log(${msg}) in:\n${logs.join('\n')}`);
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { defineConfig } from '@rstest/core';
2+
3+
export default defineConfig({
4+
coverage: {
5+
enabled: true,
6+
provider: 'istanbul',
7+
reporters: [],
8+
thresholds: {
9+
statements: 100,
10+
},
11+
},
12+
setupFiles: ['./rstest.setup.ts'],
13+
});

e2e/test-coverage/index.test.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,4 +124,28 @@ describe('test coverage-istanbul', () => {
124124
fs.existsSync(join(__dirname, 'fixtures/test-temp-coverage/index.html')),
125125
).toBeTruthy();
126126
});
127+
128+
it('coverage-istanbul with thresholds check', async () => {
129+
const { expectLog, cli } = await runRstestCli({
130+
command: 'rstest',
131+
args: ['run', '-c', 'rstest.thresholds.config.ts'],
132+
options: {
133+
nodeOptions: {
134+
cwd: join(__dirname, 'fixtures'),
135+
},
136+
},
137+
});
138+
139+
await cli.exec;
140+
const exitCode = cli.exec.process?.exitCode;
141+
142+
expect(exitCode).toBe(1);
143+
144+
const logs = cli.stdout.split('\n').filter(Boolean);
145+
146+
expectLog(
147+
/Coverage for statements .* does not meet global threshold/,
148+
logs,
149+
);
150+
});
127151
});

packages/core/src/core/runTests.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,21 @@ export async function runTests(context: Rstest): Promise<void> {
286286
context.normalizedConfig.coverage,
287287
);
288288

289+
if (context.normalizedConfig.coverage.thresholds) {
290+
const { checkThresholds } = await import(
291+
'../coverage/checkThresholds'
292+
);
293+
const thresholdResult = checkThresholds(
294+
finalCoverageMap,
295+
context.normalizedConfig.coverage.thresholds,
296+
);
297+
if (!thresholdResult.success) {
298+
process.exitCode = 1;
299+
logger.log('');
300+
logger.log(thresholdResult.message);
301+
}
302+
}
303+
289304
// Cleanup
290305
coverageProvider.cleanup();
291306
} catch (error) {
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import type {
2+
CoverageMap,
3+
CoverageSummary,
4+
CoverageThresholds,
5+
} from '../types/coverage';
6+
import { color } from '../utils';
7+
8+
export function checkThresholds(
9+
coverageMap: CoverageMap,
10+
thresholds?: CoverageThresholds,
11+
): { success: boolean; message: string } {
12+
if (!thresholds) {
13+
return { success: true, message: '' };
14+
}
15+
const summary = coverageMap.getCoverageSummary();
16+
const failedThresholds: string[] = [];
17+
18+
const check = (
19+
name: keyof CoverageSummary,
20+
type: string,
21+
actual: number,
22+
expected?: number,
23+
) => {
24+
if (expected !== undefined && actual < expected) {
25+
failedThresholds.push(
26+
`Coverage for ${name} ${color.red(`${actual}%`)} does not meet ${type} threshold ${color.yellow(`${expected}%`)}`,
27+
);
28+
}
29+
};
30+
// Check global thresholds
31+
check('statements', 'global', summary.statements.pct, thresholds.statements);
32+
check('functions', 'global', summary.functions.pct, thresholds.functions);
33+
check('branches', 'global', summary.branches.pct, thresholds.branches);
34+
check('lines', 'global', summary.lines.pct, thresholds.lines);
35+
36+
return {
37+
success: failedThresholds.length === 0,
38+
message: failedThresholds.join('\n'),
39+
};
40+
}

packages/core/src/types/coverage.ts

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,26 @@
1-
import type { CoverageMap } from 'istanbul-lib-coverage';
1+
import type { CoverageMap, CoverageSummary } from 'istanbul-lib-coverage';
22
import type { ReportOptions } from 'istanbul-reports';
33

44
type ReportWithOptions<Name extends keyof ReportOptions = keyof ReportOptions> =
55
Name extends keyof ReportOptions
66
? [Name, Partial<ReportOptions[Name]>]
77
: [Name, Record<string, unknown>];
88

9+
type Thresholds = {
10+
/** Thresholds for statements */
11+
statements?: number;
12+
/** Thresholds for functions */
13+
functions?: number;
14+
/** Thresholds for branches */
15+
branches?: number;
16+
/** Thresholds for lines */
17+
lines?: number;
18+
};
19+
20+
export type { CoverageMap, CoverageSummary };
21+
22+
export type CoverageThresholds = Thresholds;
23+
924
export type CoverageOptions = {
1025
/**
1126
* Enable coverage collection.
@@ -51,9 +66,20 @@ export type CoverageOptions = {
5166
* @default true
5267
*/
5368
clean?: boolean;
69+
70+
/**
71+
* Coverage thresholds
72+
*
73+
* @default undefined
74+
*/
75+
thresholds?: CoverageThresholds;
5476
};
5577

56-
export type NormalizedCoverageOptions = Required<CoverageOptions>;
78+
export type NormalizedCoverageOptions = Required<
79+
Omit<CoverageOptions, 'thresholds'>
80+
> & {
81+
thresholds?: CoverageThresholds;
82+
};
5783

5884
export declare class CoverageProvider {
5985
constructor(options: CoverageOptions);

0 commit comments

Comments
 (0)