@rpcbase/test 0.177.0 → 0.179.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +31 -0
- package/index.d.ts +17 -1
- package/package.json +2 -1
- package/src/coverage/config.js +74 -4
- package/src/coverage/report.js +79 -4
- package/src/coverage/reporter.js +26 -3
package/README.md
CHANGED
|
@@ -27,6 +27,37 @@ export default {
|
|
|
27
27
|
|
|
28
28
|
Only `RB_DISABLE_COVERAGE=1` skips the hooks; every other option lives inside this config file.
|
|
29
29
|
|
|
30
|
+
### Per-folder thresholds
|
|
31
|
+
|
|
32
|
+
Need stricter coverage in a sub-tree? Extend the same `thresholds` object with glob keys (mirroring Jest's `coverageThreshold` syntax):
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
export default {
|
|
36
|
+
workspaceRoot,
|
|
37
|
+
thresholds: {
|
|
38
|
+
global: {
|
|
39
|
+
statements: 90,
|
|
40
|
+
lines: 85,
|
|
41
|
+
functions: 85,
|
|
42
|
+
branches: 70,
|
|
43
|
+
},
|
|
44
|
+
"lib/core/**": {
|
|
45
|
+
statements: 98,
|
|
46
|
+
lines: 95,
|
|
47
|
+
},
|
|
48
|
+
"lib/components/**": {
|
|
49
|
+
functions: 92,
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
- When `thresholds` only has metric keys, it behaves exactly like before.
|
|
56
|
+
- Adding `thresholds.global` lets you keep a default floor while overriding specific directories.
|
|
57
|
+
- Globs run against POSIX-style paths relative to `workspaceRoot`, but absolute paths work too if you prefer.
|
|
58
|
+
- Metrics you omit inside an override inherit from the global thresholds (or the 75/75/75/60 defaults).
|
|
59
|
+
- If a glob matches no files you'll get a warning and the override is skipped, so typos are easy to spot.
|
|
60
|
+
|
|
30
61
|
## 2. Import `test` / `expect` directly
|
|
31
62
|
|
|
32
63
|
```ts
|
package/index.d.ts
CHANGED
|
@@ -7,6 +7,19 @@ interface CoverageThresholds {
|
|
|
7
7
|
statements: number
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
+
type CoverageThresholdMap = {
|
|
11
|
+
global?: Partial<CoverageThresholds>
|
|
12
|
+
[pattern: string]: Partial<CoverageThresholds> | number | undefined
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
type CoverageThresholdOption = Partial<CoverageThresholds> | CoverageThresholdMap
|
|
16
|
+
|
|
17
|
+
interface CoverageThresholdTarget {
|
|
18
|
+
id: string
|
|
19
|
+
pattern: string
|
|
20
|
+
thresholds: CoverageThresholds
|
|
21
|
+
}
|
|
22
|
+
|
|
10
23
|
interface CoverageHarnessOptions {
|
|
11
24
|
workspaceRoot: string
|
|
12
25
|
libDirs?: string[]
|
|
@@ -14,7 +27,7 @@ interface CoverageHarnessOptions {
|
|
|
14
27
|
coverageReportSubdir?: string
|
|
15
28
|
coverageFileName?: string
|
|
16
29
|
envPrefix?: string
|
|
17
|
-
thresholds?:
|
|
30
|
+
thresholds?: CoverageThresholdOption
|
|
18
31
|
disabledEnvVar?: string
|
|
19
32
|
}
|
|
20
33
|
|
|
@@ -24,6 +37,7 @@ interface CoverageConfig extends CoverageHarnessOptions {
|
|
|
24
37
|
coverageReportDir: string
|
|
25
38
|
coverageFileName: string
|
|
26
39
|
thresholds: CoverageThresholds
|
|
40
|
+
thresholdTargets: CoverageThresholdTarget[]
|
|
27
41
|
coverageEnabled: boolean
|
|
28
42
|
disabledEnvVar: string
|
|
29
43
|
}
|
|
@@ -44,4 +58,6 @@ export type {
|
|
|
44
58
|
CoverageHarnessOptions,
|
|
45
59
|
CoverageConfig,
|
|
46
60
|
CoverageThresholds,
|
|
61
|
+
CoverageThresholdOption,
|
|
62
|
+
CoverageThresholdTarget,
|
|
47
63
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rpcbase/test",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.179.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"types": "./index.d.ts",
|
|
6
6
|
"exports": {
|
|
@@ -51,6 +51,7 @@
|
|
|
51
51
|
"istanbul-reports": "3.1.6",
|
|
52
52
|
"lodash": "4.17.21",
|
|
53
53
|
"mongoose": "8.18.0",
|
|
54
|
+
"picomatch": "2.3.1",
|
|
54
55
|
"v8-to-istanbul": "9.2.0"
|
|
55
56
|
},
|
|
56
57
|
"peerDependencies": {},
|
package/src/coverage/config.js
CHANGED
|
@@ -8,6 +8,8 @@ const DEFAULT_THRESHOLDS = {
|
|
|
8
8
|
statements: 75,
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
+
const THRESHOLD_KEYS = Object.keys(DEFAULT_THRESHOLDS)
|
|
12
|
+
|
|
11
13
|
function resolveDir(root, target, fallback) {
|
|
12
14
|
if (!target) {
|
|
13
15
|
return path.resolve(root, fallback)
|
|
@@ -42,10 +44,7 @@ export function createCoverageConfig(options = {}) {
|
|
|
42
44
|
const disabledEnvVar = options.disabledEnvVar ?? "RB_DISABLE_COVERAGE"
|
|
43
45
|
const coverageEnabled = process.env[disabledEnvVar] !== "1"
|
|
44
46
|
|
|
45
|
-
const thresholds =
|
|
46
|
-
...DEFAULT_THRESHOLDS,
|
|
47
|
-
...(options.thresholds ?? {}),
|
|
48
|
-
}
|
|
47
|
+
const { global: thresholds, targets: thresholdTargets } = normalizeThresholdOptions(options.thresholds)
|
|
49
48
|
|
|
50
49
|
return {
|
|
51
50
|
workspaceRoot: resolvedWorkspaceRoot,
|
|
@@ -54,7 +53,78 @@ export function createCoverageConfig(options = {}) {
|
|
|
54
53
|
coverageReportDir,
|
|
55
54
|
coverageFileName,
|
|
56
55
|
thresholds,
|
|
56
|
+
thresholdTargets,
|
|
57
57
|
coverageEnabled,
|
|
58
58
|
disabledEnvVar,
|
|
59
59
|
}
|
|
60
60
|
}
|
|
61
|
+
|
|
62
|
+
function normalizeThresholdOptions(rawThresholds) {
|
|
63
|
+
const globalThresholds = { ...DEFAULT_THRESHOLDS }
|
|
64
|
+
const targets = []
|
|
65
|
+
|
|
66
|
+
if (!isPlainObject(rawThresholds)) {
|
|
67
|
+
return { global: globalThresholds, targets }
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
for (const key of THRESHOLD_KEYS) {
|
|
71
|
+
const value = rawThresholds[key]
|
|
72
|
+
if (isThresholdValue(value)) {
|
|
73
|
+
globalThresholds[key] = value
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (Object.prototype.hasOwnProperty.call(rawThresholds, "global")) {
|
|
78
|
+
if (!isPlainObject(rawThresholds.global)) {
|
|
79
|
+
throw new Error("coverage thresholds: the `global` override must be an object of metric values")
|
|
80
|
+
}
|
|
81
|
+
Object.assign(globalThresholds, pickThresholdOverrides(rawThresholds.global))
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
for (const [pattern, overrides] of Object.entries(rawThresholds)) {
|
|
85
|
+
if (pattern === "global" || THRESHOLD_KEYS.includes(pattern)) {
|
|
86
|
+
continue
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (!isPlainObject(overrides)) {
|
|
90
|
+
throw new Error(
|
|
91
|
+
`coverage thresholds: override for "${pattern}" must be an object containing coverage metrics`,
|
|
92
|
+
)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
targets.push({
|
|
96
|
+
id: pattern,
|
|
97
|
+
pattern,
|
|
98
|
+
thresholds: {
|
|
99
|
+
...globalThresholds,
|
|
100
|
+
...pickThresholdOverrides(overrides),
|
|
101
|
+
},
|
|
102
|
+
})
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return { global: globalThresholds, targets }
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function pickThresholdOverrides(source) {
|
|
109
|
+
const overrides = {}
|
|
110
|
+
if (!isPlainObject(source)) {
|
|
111
|
+
return overrides
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
for (const key of THRESHOLD_KEYS) {
|
|
115
|
+
const value = source[key]
|
|
116
|
+
if (isThresholdValue(value)) {
|
|
117
|
+
overrides[key] = value
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return overrides
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function isPlainObject(value) {
|
|
125
|
+
return value !== null && typeof value === "object" && !Array.isArray(value)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function isThresholdValue(value) {
|
|
129
|
+
return typeof value === "number" && Number.isFinite(value)
|
|
130
|
+
}
|
package/src/coverage/report.js
CHANGED
|
@@ -4,6 +4,7 @@ import path from "node:path"
|
|
|
4
4
|
import * as libCoverage from "istanbul-lib-coverage"
|
|
5
5
|
import { createContext } from "istanbul-lib-report"
|
|
6
6
|
import reports from "istanbul-reports"
|
|
7
|
+
import picomatch from "picomatch"
|
|
7
8
|
import v8ToIstanbul from "v8-to-istanbul"
|
|
8
9
|
|
|
9
10
|
import { findCoverageFiles } from "./files.js"
|
|
@@ -11,6 +12,13 @@ import { findCoverageFiles } from "./files.js"
|
|
|
11
12
|
|
|
12
13
|
const TEXT_REPORT_FILENAME = "coverage.txt"
|
|
13
14
|
|
|
15
|
+
export class CoverageThresholdError extends Error {
|
|
16
|
+
constructor(message) {
|
|
17
|
+
super(message)
|
|
18
|
+
this.name = "CoverageThresholdError"
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
14
22
|
export async function generateCoverageReport(config) {
|
|
15
23
|
const coverageFiles = await findCoverageFiles(config)
|
|
16
24
|
|
|
@@ -19,7 +27,8 @@ export async function generateCoverageReport(config) {
|
|
|
19
27
|
return
|
|
20
28
|
}
|
|
21
29
|
|
|
22
|
-
const
|
|
30
|
+
const coverageLib = resolveCoverageLib()
|
|
31
|
+
const coverageMap = coverageLib.createCoverageMap({})
|
|
23
32
|
|
|
24
33
|
for (const file of coverageFiles) {
|
|
25
34
|
const payload = await readCoverageFile(file)
|
|
@@ -52,7 +61,25 @@ export async function generateCoverageReport(config) {
|
|
|
52
61
|
console.log(`[coverage] Full text report saved to ${path.join(config.coverageReportDir, TEXT_REPORT_FILENAME)}`)
|
|
53
62
|
|
|
54
63
|
const summary = coverageMap.getCoverageSummary()
|
|
55
|
-
enforceThresholds(summary, config.thresholds)
|
|
64
|
+
enforceThresholds(summary, config.thresholds, "global")
|
|
65
|
+
|
|
66
|
+
const targets = Array.isArray(config.thresholdTargets) ? config.thresholdTargets : []
|
|
67
|
+
if (targets.length > 0) {
|
|
68
|
+
const fileSummaries = buildFileSummaries(coverageMap, config.workspaceRoot)
|
|
69
|
+
for (const target of targets) {
|
|
70
|
+
const matcher = createGlobMatcher(target.pattern)
|
|
71
|
+
const matchResult = collectTargetSummary(fileSummaries, matcher, coverageLib)
|
|
72
|
+
|
|
73
|
+
if (matchResult.matched === 0) {
|
|
74
|
+
console.warn(
|
|
75
|
+
`[coverage] threshold pattern "${target.pattern}" did not match any files — skipping`,
|
|
76
|
+
)
|
|
77
|
+
continue
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
enforceThresholds(matchResult.summary, target.thresholds, target.pattern)
|
|
81
|
+
}
|
|
82
|
+
}
|
|
56
83
|
}
|
|
57
84
|
|
|
58
85
|
async function mergeScriptCoverage(coverageMap, script) {
|
|
@@ -81,7 +108,7 @@ async function readCoverageFile(file) {
|
|
|
81
108
|
}
|
|
82
109
|
}
|
|
83
110
|
|
|
84
|
-
function enforceThresholds(summary, thresholds) {
|
|
111
|
+
function enforceThresholds(summary, thresholds, label = "global") {
|
|
85
112
|
const failures = []
|
|
86
113
|
|
|
87
114
|
for (const metric of Object.keys(thresholds)) {
|
|
@@ -100,7 +127,7 @@ function enforceThresholds(summary, thresholds) {
|
|
|
100
127
|
.map(({ metric, actual, minimum }) => `${metric}: ${actual.toFixed(2)}% < ${minimum}%`)
|
|
101
128
|
.join("; ")
|
|
102
129
|
|
|
103
|
-
throw new
|
|
130
|
+
throw new CoverageThresholdError(`[coverage] thresholds not met (target: ${label}) — ${details}`)
|
|
104
131
|
}
|
|
105
132
|
|
|
106
133
|
function resolveCoverageLib() {
|
|
@@ -115,3 +142,51 @@ function resolveCoverageLib() {
|
|
|
115
142
|
|
|
116
143
|
throw new Error("istanbul-lib-coverage exports are unavailable")
|
|
117
144
|
}
|
|
145
|
+
|
|
146
|
+
function buildFileSummaries(coverageMap, workspaceRoot) {
|
|
147
|
+
const normalizedRoot = path.resolve(workspaceRoot)
|
|
148
|
+
return coverageMap.files().map((filePath) => {
|
|
149
|
+
const normalizedAbsolute = path.resolve(filePath)
|
|
150
|
+
const summary = coverageMap.fileCoverageFor(filePath).toSummary()
|
|
151
|
+
const relativePath = path.relative(normalizedRoot, normalizedAbsolute)
|
|
152
|
+
const candidates = new Set([toPosix(normalizedAbsolute)])
|
|
153
|
+
|
|
154
|
+
if (relativePath && !relativePath.startsWith("..")) {
|
|
155
|
+
const relativePosix = toPosix(relativePath)
|
|
156
|
+
candidates.add(relativePosix)
|
|
157
|
+
candidates.add(`./${relativePosix}`)
|
|
158
|
+
candidates.add(`/${relativePosix}`)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return {
|
|
162
|
+
summary,
|
|
163
|
+
candidates: Array.from(candidates),
|
|
164
|
+
}
|
|
165
|
+
})
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function collectTargetSummary(fileSummaries, matcher, coverageLib) {
|
|
169
|
+
const summary = coverageLib.createCoverageSummary()
|
|
170
|
+
let matched = 0
|
|
171
|
+
|
|
172
|
+
for (const file of fileSummaries) {
|
|
173
|
+
if (file.candidates.some((candidate) => matcher(candidate))) {
|
|
174
|
+
summary.merge(file.summary)
|
|
175
|
+
matched += 1
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return { summary, matched }
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function createGlobMatcher(pattern) {
|
|
183
|
+
const normalized = toPosix(String(pattern ?? "")).trim()
|
|
184
|
+
if (!normalized) {
|
|
185
|
+
return () => false
|
|
186
|
+
}
|
|
187
|
+
return picomatch(normalized, { dot: true })
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function toPosix(input) {
|
|
191
|
+
return input.split(path.sep).join("/")
|
|
192
|
+
}
|
package/src/coverage/reporter.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import fs from "node:fs/promises"
|
|
2
2
|
|
|
3
|
-
import { generateCoverageReport } from "./report.js"
|
|
3
|
+
import { CoverageThresholdError, generateCoverageReport } from "./report.js"
|
|
4
4
|
import { removeCoverageFiles } from "./files.js"
|
|
5
5
|
|
|
6
6
|
|
|
@@ -21,14 +21,37 @@ class CoverageReporter {
|
|
|
21
21
|
await fs.rm(this.config.coverageReportDir, { recursive: true, force: true })
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
async onEnd() {
|
|
24
|
+
async onEnd(result) {
|
|
25
25
|
if (!this.config.coverageEnabled) {
|
|
26
26
|
return
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
try {
|
|
30
|
+
await generateCoverageReport(this.config)
|
|
31
|
+
} catch (error) {
|
|
32
|
+
if (error instanceof CoverageThresholdError) {
|
|
33
|
+
console.error(error.message)
|
|
34
|
+
setFailureExitCode(result)
|
|
35
|
+
return
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
throw error
|
|
39
|
+
}
|
|
30
40
|
}
|
|
31
41
|
}
|
|
32
42
|
|
|
33
43
|
export { CoverageReporter }
|
|
34
44
|
export default CoverageReporter
|
|
45
|
+
|
|
46
|
+
function setFailureExitCode(result) {
|
|
47
|
+
if (result && typeof result === "object") {
|
|
48
|
+
result.status = "failed"
|
|
49
|
+
if (typeof result.exitCode !== "number" || result.exitCode === 0) {
|
|
50
|
+
result.exitCode = 1
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (!process.exitCode || process.exitCode === 0) {
|
|
55
|
+
process.exitCode = 1
|
|
56
|
+
}
|
|
57
|
+
}
|