Skip to content

Commit 320090c

Browse files
committed
Pitch the production dead-code mode before explaining it
The README's closing paragraph and the top of docs/Production.md described the mode without saying why anyone would want it. Both now lead with the case: tests cannot prove code is needed and static analysis cannot prove Ruby is unused, so a window of real traffic is the strongest deletion evidence available, gathered at near-zero overhead by oneshot coverage. The cross pays out twice, as a deletion list with the evidence window attached and as a priority list of untested code production actually runs. The doc's mechanics now sit under an explicit how-to heading after the why, with the linked subsection anchors unchanged. The wording borrows the field's own messaging. Coverband promotes itself on production code usage and the least possible performance impact, and the oneshot coverage proposal motivated the feature as a replacement for planting a log line in suspect code and watching production, so the pitch speaks that language: the log-line experiment run for every line at once, full speed once a line has fired, every kind of process feeding one store, and the cross turning the record into insight you can act on.
1 parent 6d73bd1 commit 320090c

2 files changed

Lines changed: 187 additions & 12 deletions

File tree

README.md

Lines changed: 159 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,162 @@ end
128128
Every option is documented in [docs/Configuration.md](docs/Configuration.md),
129129
including criteria, filters, groups, profiles, and thresholds.
130130
131-
Measuring coverage in production to find dead code is its own mode with its
132-
own document, [docs/Production.md](docs/Production.md): a live process
133-
accumulates what real traffic executes, and `simplecov dead-code`, the HTML
134-
report, and `coverage.json` cross that with what the tests cover.
131+
## Tracking which test covers each line
132+
133+
Coverage normally tells you whether a line ran, not what ran it. `track_tests`
134+
records the other half of the story:
135+
136+
```ruby
137+
SimpleCov.start do
138+
track_tests
139+
end
140+
```
141+
142+
RSpec examples and Minitest tests are wrapped automatically. In the HTML
143+
report, covered lines that no test executed (they only ran at load time, or in
144+
suite setup) drain to a distinct tint, so coverage that merely *loads* code
145+
stops passing for coverage that *tests* it, and clicking a line's badge lists
146+
the tests that cover it. The same recording answers from the terminal:
147+
148+
```sh
149+
$ simplecov tests lib/simplecov/result.rb:42
150+
spec/result_spec.rb:42
151+
```
152+
153+
The output is one test id per line and nothing else, so it pipes straight into
154+
a runner. `simplecov tests --redundant` inverts the question, listing the
155+
tests whose covered lines other tests also cover, which is where a
156+
[test-pruning session](docs/Redundant_Tests.md) starts. Recording has a real
157+
cost, which is why it's opt-in and comes with levers to control it. See
158+
[the configuration docs](docs/Configuration.md#tracking-which-test-covers-each-line).
159+
160+
## Finding dead code in production
161+
162+
SimpleCov can also measure production code usage, the surest way to find
163+
dead code. The old trick was to plant a log line in a suspect method and
164+
watch production for a while. Oneshot coverage runs that experiment for
165+
every line at once: a line reports its first execution and nothing after,
166+
so a live process records what real traffic uses with the least possible
167+
impact on performance. `simplecov dead-code` then crosses the recording
168+
with the test report and turns it into insight you can act on. Code
169+
neither tests nor traffic touch is safe to delete, and code production
170+
runs but tests skip is the most valuable test you haven't written. The
171+
HTML report and `coverage.json` include the same production data. See
172+
[docs/Production.md](docs/Production.md) for more details on why and how
173+
to set it up.
174+
175+
## Coverage of just your change
176+
177+
An overall number moves slowly on a mature codebase, but "is the code in this
178+
change tested?" has a crisp answer the day you ask it. `simplecov patch` reads
179+
the git diff against a base ref and scores only the lines you touched:
180+
181+
```sh
182+
$ simplecov patch --base main --minimum 100
183+
88.00% (22/25) lines lib/simplecov/cli/patch.rb missing 41-43
184+
100.00% (4/4) lines lib/simplecov/result.rb
185+
Patch coverage: 89.66% (26/29) lines
186+
```
187+
188+
`--minimum` turns it into a gate, so a project that can't lift its overall
189+
number in one pull request can still require that everything it adds is
190+
covered. The flip side is `simplecov affected`, which uses a `track_tests`
191+
recording to select the tests that touch your changed code and hand them to
192+
the runner, falling back (loudly) to the full suite whenever the map can't be
193+
trusted:
194+
195+
```sh
196+
$ simplecov affected --base main --run bundle exec rspec
197+
```
198+
199+
Both commands are documented in [the CLI docs](docs/CLI.md).
200+
201+
## Covering views
202+
203+
View templates execute real logic, and now they can be part of the report.
204+
`cover_views` brings ERB, Haml, and Slim templates in, measured through eval
205+
coverage (CRuby 3.2+):
206+
207+
```ruby
208+
SimpleCov.start 'rails' do
209+
cover_views
210+
end
211+
```
212+
213+
Templates are ordinary files in the report, highlighted in their own language
214+
and grouped under Views by the `rails` profile, and a template no test renders
215+
shows up at 0% instead of being quietly missing. Expect your overall number to
216+
drop the first time you turn this on. That's the point. See
217+
[view coverage](docs/Configuration.md#view-coverage).
218+
219+
## A consistent configuration grammar
220+
221+
The configuration API was redesigned around a small set of consistent verbs.
222+
Formatters are picked by name, thresholds live in a per-criterion `coverage`
223+
block where scope is a uniform `per:` argument, and misses can be capped as
224+
absolute counts rather than ratios:
225+
226+
```ruby
227+
SimpleCov.start do
228+
formats :html, :json
229+
230+
coverage :line do
231+
minimum 90
232+
minimum 100, per: "lib/simplecov/result.rb"
233+
maximum_missed 5, per: :file # no file may carry more than 5 uncovered lines
234+
end
235+
236+
coverage :branch, minimum: 80, ignore: :implicit_else
237+
end
238+
```
239+
240+
Everything you're using today keeps working. Legacy spellings warn and name
241+
their replacement, and once you've migrated, `deprecations :raise` turns any
242+
old spelling that creeps back in into an error. The
243+
[migration map](docs/Configuration.md#migrating-from-the-legacy-configuration-api)
244+
has the full before and after.
245+
246+
## Per-file ratchets and coverage history
247+
248+
On a legacy codebase, one per-file minimum does nothing useful: set it to what
249+
the worst file scores and every other file is allowed to sink to that level.
250+
`simplecov ratchet` writes a checked-in baseline instead, giving each file its
251+
own floor at the coverage it has already reached:
252+
253+
```sh
254+
$ simplecov ratchet
255+
simplecov ratchet: wrote .simplecov_baseline.yml (3 tightened, 1 pruned, 148 unchanged)
256+
```
257+
258+
Floors only ever tighten, so touching a legacy file drags its coverage upward
259+
and it can never slide back. Think `.rubocop_todo.yml`, applied to coverage.
260+
To ratchet automatically at the end of every run, add the baseline formatter
261+
with `formats :html, :baseline`.
262+
263+
Alongside the floors, every successful run now appends to
264+
`coverage/.history.json`, so you have a recorded trend rather than just the
265+
last number. `simplecov history` draws it as sparklines in the terminal, and
266+
`drop_baseline :median` judges coverage drops against the recorded median
267+
instead of whatever the previous run happened to score. See
268+
[the baseline](docs/Configuration.md#per-file-baseline-ratchet) and
269+
[run history](docs/Configuration.md#run-history) docs.
270+
271+
## More from the command line
272+
273+
The `simplecov` CLI has grown from a report opener into a toolbelt. A few
274+
favorites:
275+
276+
```sh
277+
$ simplecov watch bundle exec rspec # re-run on save, live-reload the served report
278+
$ simplecov show lib/foo.rb # annotated source in the terminal
279+
$ simplecov status # is this report fresh, and for which commit?
280+
$ simplecov uncovered --missing # worst files, with the exact line ranges to test
281+
$ simplecov badge --output badge.svg # a shields.io-style SVG, no badge service needed
282+
```
283+
284+
`watch` deserves the highlight: with a `track_tests` recording in the report,
285+
a save re-runs only the tests that touch the files you changed, which turns
286+
the report into something you keep open while writing the test. There is also
287+
shell tab completion (`simplecov completions fish|bash|zsh`), a man page, and
288+
a real `--help` on every command. The full tour is in
289+
[docs/CLI.md](docs/CLI.md).

docs/Production.md

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,34 @@ Measure what real traffic executes, and cross it with the test report to find de
44

55
*Part of the [SimpleCov](../README.md) documentation.*
66

7-
## Coverage in production
8-
9-
Oneshot lines coverage is cheap enough to leave running in production: each line reports its first execution and
10-
nothing after, so the steady-state overhead is near zero. `SimpleCov::Production` turns that into a supported mode: a
11-
live process measures which of its lines ever run and drains the measurements to a shared store on an interval, and
12-
`simplecov dead-code`, the HTML report, and `coverage.json` later cross that store with what the tests cover. The
13-
union of a test suite and real traffic answers a question neither answers alone, and coverage over a long enough
14-
window is far better evidence for deleting Ruby than any static analysis of it.
7+
## Why measure coverage in production
8+
9+
Every codebase that has lived a few years carries code nobody is sure about. It has tests, the tests pass, and still
10+
no one can say whether anything real ever calls it, so it survives every cleanup and keeps taxing every reader, every
11+
upgrade, and every security audit. The traditional way to find out is to plant a log line in the suspect code and
12+
watch production for a while, one candidate at a time. Production coverage runs that experiment automatically, for
13+
every line at once, with nothing added to the code under suspicion.
14+
15+
Nothing else settles the question. A test suite cannot, because a test proves code works, not that anything needs it,
16+
and dead code's own spec is usually green, which makes the suite the dead code's best defender. Static analysis
17+
cannot either, because Ruby resolves calls at runtime. Watching real traffic is the only evidence there is, and
18+
oneshot lines coverage collects it with the least possible impact on performance: each line reports its first
19+
execution and nothing after, so once a line has fired the process runs at full speed, and the steady-state overhead
20+
of measuring for weeks is near zero. Any Ruby process can measure, so web workers, background jobs, and scheduled
21+
tasks all pour their slice of the truth into the same store.
22+
23+
What accumulates is a record of production code usage, and crossing it with the test report turns it into insight
24+
you can act on. Code that neither tests nor traffic touched is dead, and `simplecov dead-code` prints it with the
25+
evidence window attached, a deletion list you can trust. Code that production runs but no test covers is the most
26+
valuable test you have not written yet, a priority list. And where the store carries `last_seen` stamps, each
27+
candidate comes dated: "this file last ran in March" argues for deletion far better than a bare uncovered bit ever
28+
could.
29+
30+
## How to set it up and use it
31+
32+
`SimpleCov::Production` is the supported mode behind all of this: a live process measures which of its lines ever run
33+
and drains the measurements to a shared store on an interval, and `simplecov dead-code`, the HTML report, and
34+
`coverage.json` later cross that store with what the tests cover.
1535

1636
Nothing here runs unless explicitly started, and `require "simplecov/production"` loads none of the reporting
1737
machinery, no formatters, and no at-exit report. A process that starts production coverage gets a measurement tap and

0 commit comments

Comments
 (0)