---
url: https://thekevinscott.github.io/testing-conventions/guide/configure.md
description: >-
  Respond to a red check in testing-conventions.toml — relax a coverage floor or
  exempt a file or line with a required reason.
---

# Respond to a red check

When a check goes red, there are two responses: fix the code, or record a deliberate omission
here. A single `testing-conventions.toml` at your repo root (or per package, in a
[monorepo](../monorepo)) tunes what the checks require — coverage floors and reason-required
exemptions. Anything you don't set keeps its strict default; the
[configuration reference](../reference/config) carries every key.

## Relax a coverage floor

Every coverage check ships a strict 100% floor. Lower one under the language's table:

```toml
# Drop the Python floor from the strict default 100 to 90:
[python]
coverage = { branch = true, fail_under = 90 }

[typescript]
coverage = { branches = 90 }   # lines / functions / statements stay at 100

[rust]
coverage = { regions = 90 }    # lines stays at 100
```

A `[<language>].coverage` table is a **partial override** — set only the fields you want to move and
the rest keep their default. (A typo'd key is still rejected; only *missing* keys fall back.) See
[Reference — coverage keys](../reference/config#coverage) for every key and default, and
[Why a 100% floor](../explanation/coverage) before you lower one.

## Raise the one-function-per-file threshold

`one-function-per-file` runs for Python and TypeScript with no configuration; Rust opts in by
naming a threshold. It allows one module-scope function per file whose body runs longer than
`max_lines`, and the default is `1`. A codebase whose natural grain is longer functions moves the
line under the language's table:

```toml
[python]
one_function_per_file = { max_lines = 5 }

[typescript]
one_function_per_file = { max_lines = 20 }
```

A function at or under the threshold is trivial and shares a file freely. Raising the number moves
where "trivial" ends; the one-per-file ceiling above it holds at every setting. See
[One function per file](../explanation/one-function-per-file) for what counts as a function and as
a line.

## Exempt a file

Some files genuinely shouldn't be tested — a launcher shim, a process entry point, generated code.
A blocking gate needs that escape hatch, but here it's **explicit and reason-required**, never a
silent ignore — see [Scoping and exemptions](../explanation/scoping) for the design.

### Declaration-only modules need no exemption

A file with no function and no control flow anywhere in it has nothing a test can drive and is
never flagged: an empty or comment-only file, a re-export barrel `__init__.py` / `index.ts`, a
constants module, an `Enum`, a type-only module. A TypeScript declaration file (`*.d.ts`) is
ignored too. The parser decides from the file's contents, never from its name — the moment a file
gains a `def`, an arrow, a method, an `if`, or a ternary, it becomes a subject and needs either a
colocated test or an exemption. [What it enforces](../explanation/colocated-test#what-it-enforces)
carries the node list per language.

### Exempt a real file

Add a `[[<language>.exempt]]` entry naming the rules it lifts and **why**. Whole-file exemptions are
for the **presence and lint** checks — a launcher shim or a process entry point with no colocated
test:

```toml
# A launcher shim with no unit test:
[[python.exempt]]
path = "mypkg/cli.py"
rules = ["colocated-test"]
reason = "thin launcher; logic lives in run(), tested in run_test.py"

# A process entry point, exempt from the colocated-test check:
[[typescript.exempt]]
path = "src/bin.ts"
rules = ["colocated-test"]
reason = "process entry; maps run() to an exit code, tested in run.test.ts"
```

* `path` is relative to the scanned `source`, and must point to a file that exists — a stale entry
  is a hard error, so the list can't silently rot.
* `rules` names the checks the entry lifts (`colocated-test`, `one-function-per-file`) or an
  individual lint rule (`no-monkeypatch`) — see the [glossary](../reference/glossary) for how this
  key spans both. For `coverage` / `mutation`, see the line-scoped form below — those are never
  whole-file.
* `reason` is required; a reason-less entry is rejected when the config loads.

Because every exemption lives in this one file, names its rules, and carries a reason, the whole
exemption surface is auditable in a single diff — unlike scattered ignore comments. See
[Reference — Exemptions](../reference/config#exemptions) for the exact schema.

### Exempt specific lines (`coverage` / `mutation`)

`coverage` and `mutation` exemptions are never whole-file — they must carry a `lines` list naming the
exact lines they lift:

```toml
[[python.exempt]]
path = "mypkg/config/tomlcompat.py"
rules = ["coverage", "mutation"]
lines = [9, 10, "12-13"]   # single lines and inclusive "start-end" ranges
reason = "version-conditional tomllib/tomli import; one branch is dead on any single interpreter"
```

A **determinism guard** checks the list:

* A listed line that **isn't failing** (it's covered, has a killed mutant, or carries no measured
  code) is a **hard error**.
* A failing line that **isn't listed** fails the gate as normal.

So the list is exactly the failing lines. `lines` is required with `coverage` / `mutation` and
rejected with any whole-file check, so the two never share an entry — a file exempt from both
`colocated-test` and `coverage` is two entries.

For a Rust mutation survivor asserted by an integration or e2e test, name that test in a
`[[rust.exempt]]` entry with `rules = ["mutation"]` and the survivor's exact line. The `reason`
states which behavior the other tier verifies.

## See also

* [Reference — Configuration](../reference/config): every key and the full schema.
* [Scoping and exemptions](../explanation/scoping): why a blocking gate needs a reasoned door.
