Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions 47 src/config/src/jack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -486,6 +486,53 @@ export default jack({
},
})

.num({
statements: {
description: `what % of statements must be covered?`,
hint: 'n',
validate: (n: unknown) => {
if ((n as number) < 0 || (n as number) > 100) {
throw new Error('Coverage percentage must be between 0 and 100')
}
return true
},
default: 100
},
branches: {
description: `what % of branches must be covered?`,
hint: 'n',
validate: (n: unknown) => {
if ((n as number) < 0 || (n as number) > 100) {
throw new Error('Coverage percentage must be between 0 and 100')
}
return true
},
default: 100
},
lines: {
description: `what % of lines must be covered?`,
hint: 'n',
validate: (n: unknown) => {
if ((n as number) < 0 || (n as number) > 100) {
throw new Error('Coverage percentage must be between 0 and 100')
}
return true
},
default: 100
},
functions: {
description: `what % of functions must be covered?`,
hint: 'n',
validate: (n: unknown) => {
if ((n as number) < 0 || (n as number) > 100) {
throw new Error('Coverage percentage must be between 0 and 100')
}
return true
},
default: 100
}
})

.flag({
bail: {
short: 'b',
Expand Down
28 changes: 28 additions & 0 deletions 28 src/config/tap-snapshots/test/jack.ts.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,13 @@ Object {
"hint": "module",
"type": "string",
},
"branches": Object {
"default": 100,
"description": "what % of branches must be covered?",
"hint": "n",
"type": "number",
"validate": Function validate(n),
},
"browser": Object {
"default": true,
"description": String(
Expand Down Expand Up @@ -187,6 +194,13 @@ Object {
"multiple": true,
"type": "string",
},
"functions": Object {
"default": 100,
"description": "what % of functions must be covered?",
"hint": "n",
"type": "number",
"validate": Function validate(n),
},
"help": Object {
"description": "show this help banner",
"short": "h",
Expand Down Expand Up @@ -245,6 +259,13 @@ Object {
"short": "j",
"type": "number",
},
"lines": Object {
"default": 100,
"description": "what % of lines must be covered?",
"hint": "n",
"type": "number",
"validate": Function validate(n),
},
"no-bail": Object {
"description": "Do not bail out on first failure (default)",
"short": "B",
Expand Down Expand Up @@ -428,6 +449,13 @@ Object {
),
"type": "boolean",
},
"statements": Object {
"default": 100,
"description": "what % of statements must be covered?",
"hint": "n",
"type": "number",
"validate": Function validate(n),
},
"test-arg": Object {
"default": Array [],
"description": "Pass an argument to test files spawned by the tap command line executable. This can be specified multiple times to pass multiple args to test scripts.",
Expand Down
55 changes: 55 additions & 0 deletions 55 src/config/test/jack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,58 @@ t.throws(() =>
//@ts-expect-error
jack.setConfigValues({ 'unknown key': ['invalid'] }),
)

t.test('coverage threshold validation', async t => {
const coverageSettings = [
'statements',
'branches',
'functions',
'lines'
]
for (const setting of coverageSettings) {
t.throws(() => {
const conf = jack.parse([`--${setting}=invalid`])
const data: Record<string, any> = Object.fromEntries(
Object.entries(conf.values).map(([k, v]) => [
k,
Array.isArray(v) ? [...v] : v,
]),
)
//@ts-expect-error
jack.validate(data)
})
t.throws(() => {
const conf = jack.parse([`--${setting}=-1`])
const data: Record<string, any> = Object.fromEntries(
Object.entries(conf.values).map(([k, v]) => [
k,
Array.isArray(v) ? [...v] : v,
]),
)
//@ts-expect-error
jack.validate(data)
})
t.throws(() => {
const conf = jack.parse([`--${setting}=101`])
const data: Record<string, any> = Object.fromEntries(
Object.entries(conf.values).map(([k, v]) => [
k,
Array.isArray(v) ? [...v] : v,
]),
)
//@ts-expect-error
jack.validate(data)
})
t.doesNotThrow(() => {
const conf = jack.parse([`--${setting}=0`])
const data: Record<string, any> = Object.fromEntries(
Object.entries(conf.values).map(([k, v]) => [
k,
Array.isArray(v) ? [...v] : v,
]),
)
//@ts-expect-error
jack.validate(data)
})
}
})
28 changes: 27 additions & 1 deletion 28 src/docs/content/coverage.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ judge of test completeness. Code coverage is thus the "test for
the tests", verifying that the tests are in fact testing the
code. Nothing is perfect, and it is of course possible to write
bad tests with full code coverage, but _lacking_ test coverage
virtually gaurantees that tests are inadequate.
virtually guarantees that tests are inadequate.

As the saying goes, seatbelts don't make you immortal, but
they're still a good idea.
Expand All @@ -32,6 +32,32 @@ generated. If it is incomplete, or if no coverage is generated at
all, then a report is printed and the process exits with an error
status code.

For those of you who do not wish to subscribe to the 100% coverage or
fail philosophy, you can set minimum code coverage thresholds
for statements, branches, functions, and lines. The default will be 100
for any coverage threshold that is not set explicitly.

Minimum coverage can be set via configuration files:

```
statements: 90
branches: 90
functions: 90
lines: 90
```

Minimum coverage can be set via command line switches:

```.shell
tap --statements=90 --branches=90 --functions=90 --lines=90
```

In the following example, functions and lines will have the
default of 100 because they are not explicitly set.
```.shell
tap --statements=90 --branches=90
```

## Reporting Coverage

Tap uses essentially the same strategy as
Expand Down
2 changes: 2 additions & 0 deletions 2 src/docs/content/upgrading-from-16.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ In tap v18, this interface has changed:

- Coverage is enabled by default, and checked
- Missing or incomplete coverage is treated as an error
- Coverage is considered incomplete when it does not meet minimum coverage thresholds
- The default minimum coverage threshold is 100%

You can get the v16 style `--no-cov` behavior by doing:

Expand Down
10 changes: 5 additions & 5 deletions 10 src/run/src/report.ts
Original file line number Diff line number Diff line change
Expand Up @@ -201,20 +201,20 @@ const checkCoverage = async (
return
}

// TODO: make levels configurable, just *default* to 100
// only comment it if not using the text reporter, because that makes
// it pretty obvious where the shortcomings are already.
// TODO: Only comment lack of coverage if not using the text reporter,
// because that makes it pretty obvious where the shortcomings are already.
for (const th of thresholds) {
const coverage = Number(summary[th].pct) || 0
if (coverage < 100) {
const minCoverage = Number(config.get(th))
if (coverage < minCoverage) {
if (comment) {
t.comment(`ERROR: incomplete ${th} coverage (${coverage}%)`)
}
success = false
}
}
if (success === false) {
t.debug('run/report exit=1 coverage not 100', summary)
t.debug(`run/report exit=1 coverage is incomplete`, summary)
if (!config.get('allow-incomplete-coverage')) {
process.exitCode = 1
}
Expand Down
53 changes: 53 additions & 0 deletions 53 src/run/test/report.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ class MockConfig {
showFullCoverage?: boolean
allowEmptyCoverage?: boolean
allowIncompleteCoverage?: boolean
statements: number = 100
branches: number = 100
functions: number = 100
lines: number = 100
browser?: boolean
constructor(coverageReport: string[] = []) {
this.#coverageReport = coverageReport
Expand All @@ -57,6 +61,14 @@ class MockConfig {
return this.allowIncompleteCoverage
case 'browser':
return this.browser !== false
case 'statements':
return this.statements
case 'branches':
return this.branches
case 'functions':
return this.functions
case 'lines':
return this.lines
default:
throw new Error('should only look up coverage configs')
}
Expand All @@ -76,6 +88,12 @@ const summary50: Summary = {
statements: { pct: 50 },
branches: { pct: 50 },
}
const summary95: Summary = {
lines: { pct: 95 },
functions: { pct: 95 },
statements: { pct: 95 },
branches: { pct: 95 },
}
const summary100: Summary = {
lines: { pct: 100 },
functions: { pct: 100 },
Expand Down Expand Up @@ -468,6 +486,41 @@ t.test('not full coverage', async t => {
process.exitCode = 0
})

t.test('coverage meets minimum thresholds', async t => {
summary = summary95
t.testdir({
'.tap': {
coverage: { 'file.json': '{}' },
},
})
const comments = t.capture(mockTap, 'comment')
let openerRan = false
const htmlReport = resolve(projectRoot, '.tap/report/index.html')
const { report } = await t.mockImport<
typeof import('../dist/esm/report.js')
>('../dist/esm/report.js', {
c8: { Report: MockReport },
'@tapjs/core': mockCore,
opener: (file: string) => {
t.equal(file, htmlReport)
openerRan = true
},
})
const config = new MockConfig([])
config.statements = 90
config.branches = 90
config.functions = 90
config.lines = 90
const logs = t.capture(console, 'log')
await report(['html'], config as unknown as LoadedConfig)
t.strictSame(logs.args(), [])
t.strictSame(comments.args(), [])
t.equal(openerRan, true)
t.equal(readFileSync(htmlReport, 'utf8'), 'report')
t.equal(process.exitCode, 0)
process.exitCode = 0
})

t.test('not full coverage, allowed', async t => {
summary = summary50
t.testdir({
Expand Down
Morty Proxy This is a proxified and sanitized view of the page, visit original site.