simplecov 1.1.1 → 1.2.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.
- checksums.yaml +4 -4
- data/README.md +148 -5
- data/exe/simplecov +0 -3
- data/lib/minitest/simplecov_plugin.rb +12 -9
- data/lib/simplecov/atomic_file.rb +16 -19
- data/lib/simplecov/autostart.rb +5 -7
- data/lib/simplecov/baseline/parser.rb +58 -0
- data/lib/simplecov/baseline.rb +170 -0
- data/lib/simplecov/cli/affected/changed_files.rb +57 -0
- data/lib/simplecov/cli/affected/selection.rb +117 -0
- data/lib/simplecov/cli/affected.rb +148 -0
- data/lib/simplecov/cli/badge/svg.rb +70 -0
- data/lib/simplecov/cli/badge.rb +62 -0
- data/lib/simplecov/cli/clean.rb +23 -24
- data/lib/simplecov/cli/command_helpers.rb +55 -19
- data/lib/simplecov/cli/completions/scripts.rb +114 -0
- data/lib/simplecov/cli/completions.rb +83 -0
- data/lib/simplecov/cli/coverage.rb +25 -28
- data/lib/simplecov/cli/coverage_file.rb +58 -14
- data/lib/simplecov/cli/dead_code/output.rb +100 -0
- data/lib/simplecov/cli/dead_code.rb +139 -0
- data/lib/simplecov/cli/diff/output.rb +45 -0
- data/lib/simplecov/cli/diff.rb +35 -67
- data/lib/simplecov/cli/dotfile.rb +83 -38
- data/lib/simplecov/cli/git.rb +41 -0
- data/lib/simplecov/cli/history/output.rb +135 -0
- data/lib/simplecov/cli/history.rb +67 -0
- data/lib/simplecov/cli/merge.rb +34 -39
- data/lib/simplecov/cli/open.rb +12 -15
- data/lib/simplecov/cli/patch/changed_lines.rb +149 -0
- data/lib/simplecov/cli/patch/output.rb +135 -0
- data/lib/simplecov/cli/patch.rb +169 -0
- data/lib/simplecov/cli/ratchet/output.rb +44 -0
- data/lib/simplecov/cli/ratchet.rb +83 -0
- data/lib/simplecov/cli/report.rb +18 -24
- data/lib/simplecov/cli/run.rb +8 -9
- data/lib/simplecov/cli/serve/report_preparer.rb +7 -6
- data/lib/simplecov/cli/serve/static_file_handler.rb +62 -56
- data/lib/simplecov/cli/serve.rb +27 -24
- data/lib/simplecov/cli/show/annotator.rb +87 -0
- data/lib/simplecov/cli/show/sweep.rb +36 -0
- data/lib/simplecov/cli/show.rb +133 -0
- data/lib/simplecov/cli/status/facts.rb +87 -0
- data/lib/simplecov/cli/status.rb +93 -0
- data/lib/simplecov/cli/tests/redundancy.rb +78 -0
- data/lib/simplecov/cli/tests.rb +148 -0
- data/lib/simplecov/cli/uncovered/misses.rb +37 -0
- data/lib/simplecov/cli/uncovered.rb +70 -32
- data/lib/simplecov/cli/usage.rb +152 -0
- data/lib/simplecov/cli/watch/live_report.rb +73 -0
- data/lib/simplecov/cli/watch/narrator.rb +55 -0
- data/lib/simplecov/cli/watch/poller.rb +45 -0
- data/lib/simplecov/cli/watch/session.rb +134 -0
- data/lib/simplecov/cli/watch/test_plan.rb +43 -0
- data/lib/simplecov/cli/watch.rb +85 -0
- data/lib/simplecov/cli.rb +43 -92
- data/lib/simplecov/color.rb +19 -37
- data/lib/simplecov/combine/branches_combiner.rb +7 -31
- data/lib/simplecov/combine/coverage_accumulator.rb +49 -142
- data/lib/simplecov/combine/identity_interner.rb +10 -13
- data/lib/simplecov/combine/interned_counts.rb +7 -9
- data/lib/simplecov/combine/lines_combiner.rb +20 -44
- data/lib/simplecov/combine/methods_combiner.rb +9 -35
- data/lib/simplecov/combine/results_combiner.rb +4 -21
- data/lib/simplecov/combine.rb +0 -6
- data/lib/simplecov/command_guesser.rb +20 -34
- data/lib/simplecov/configuration/baseline.rb +31 -0
- data/lib/simplecov/configuration/coverage.rb +91 -79
- data/lib/simplecov/configuration/coverage_criteria.rb +43 -54
- data/lib/simplecov/configuration/deprecations.rb +21 -0
- data/lib/simplecov/configuration/eval_coverage.rb +6 -10
- data/lib/simplecov/configuration/filters.rb +40 -73
- data/lib/simplecov/configuration/formatting.rb +74 -83
- data/lib/simplecov/configuration/groups.rb +19 -13
- data/lib/simplecov/configuration/history.rb +44 -0
- data/lib/simplecov/configuration/ignored_entries.rb +31 -22
- data/lib/simplecov/configuration/merging.rb +94 -62
- data/lib/simplecov/configuration/missed_caps.rb +51 -0
- data/lib/simplecov/configuration/production.rb +23 -0
- data/lib/simplecov/configuration/test_tracking.rb +48 -0
- data/lib/simplecov/configuration/thresholds.rb +33 -62
- data/lib/simplecov/configuration/view_coverage.rb +38 -0
- data/lib/simplecov/configuration.rb +70 -76
- data/lib/simplecov/configuration_error.rb +5 -0
- data/lib/simplecov/context_map/union.rb +81 -0
- data/lib/simplecov/context_map.rb +187 -0
- data/lib/simplecov/coverage_json.rb +2 -5
- data/lib/simplecov/coverage_statistics.rb +14 -24
- data/lib/simplecov/coverage_violations.rb +157 -64
- data/lib/simplecov/current_run.rb +61 -0
- data/lib/simplecov/defaults.rb +4 -23
- data/lib/simplecov/deprecation.rb +45 -27
- data/lib/simplecov/directive.rb +46 -62
- data/lib/simplecov/exit_codes/baseline_check.rb +46 -0
- data/lib/simplecov/exit_codes/check.rb +17 -11
- data/lib/simplecov/exit_codes/exit_code_handling.rb +21 -8
- data/lib/simplecov/exit_codes/maximum_coverage_drop_check.rb +7 -10
- data/lib/simplecov/exit_codes/maximum_missed_check.rb +26 -0
- data/lib/simplecov/exit_codes/maximum_missed_per_file_check.rb +34 -0
- data/lib/simplecov/exit_codes/maximum_overall_coverage_check.rb +7 -12
- data/lib/simplecov/exit_codes/minimum_coverage_by_file_check.rb +11 -10
- data/lib/simplecov/exit_codes/minimum_coverage_by_group_check.rb +7 -9
- data/lib/simplecov/exit_codes/minimum_overall_coverage_check.rb +16 -17
- data/lib/simplecov/exit_codes.rb +16 -10
- data/lib/simplecov/exit_handling.rb +94 -59
- data/lib/simplecov/file_list.rb +20 -51
- data/lib/simplecov/filter.rb +43 -66
- data/lib/simplecov/formatter/base.rb +37 -55
- data/lib/simplecov/formatter/baseline_formatter.rb +98 -0
- data/lib/simplecov/formatter/coverage_json_writer.rb +29 -31
- data/lib/simplecov/formatter/html_formatter/public/index.html +15 -9
- data/lib/simplecov/formatter/html_formatter/viewer_data_validator.rb +74 -17
- data/lib/simplecov/formatter/html_formatter.rb +24 -41
- data/lib/simplecov/formatter/json_formatter/errors_formatter.rb +50 -16
- data/lib/simplecov/formatter/json_formatter/production_section_formatter.rb +48 -0
- data/lib/simplecov/formatter/json_formatter/result_hash_formatter.rb +43 -29
- data/lib/simplecov/formatter/json_formatter/source_file_formatter.rb +14 -10
- data/lib/simplecov/formatter/json_formatter.rb +7 -13
- data/lib/simplecov/formatter/multi_formatter.rb +5 -11
- data/lib/simplecov/formatter/simple_formatter.rb +2 -8
- data/lib/simplecov/formatter.rb +14 -10
- data/lib/simplecov/group_names.rb +13 -12
- data/lib/simplecov/history.rb +115 -0
- data/lib/simplecov/last_run.rb +6 -9
- data/lib/simplecov/lines_classifier.rb +31 -16
- data/lib/simplecov/load_global_config.rb +7 -7
- data/lib/simplecov/parallel_adapters/base.rb +13 -41
- data/lib/simplecov/parallel_adapters/generic.rb +7 -19
- data/lib/simplecov/parallel_adapters/parallel_tests.rb +21 -42
- data/lib/simplecov/parallel_adapters.rb +24 -55
- data/lib/simplecov/parallel_coordination.rb +39 -49
- data/lib/simplecov/parallel_result_merger/worker_payload.rb +33 -0
- data/lib/simplecov/parallel_result_merger.rb +67 -126
- data/lib/simplecov/process.rb +14 -32
- data/lib/simplecov/production/error.rb +11 -0
- data/lib/simplecov/production/file_sink.rb +130 -0
- data/lib/simplecov/production.rb +249 -0
- data/lib/simplecov/profiles/rails.rb +1 -12
- data/lib/simplecov/profiles/root_filter.rb +0 -4
- data/lib/simplecov/profiles/strict.rb +9 -19
- data/lib/simplecov/profiles.rb +11 -33
- data/lib/simplecov/report_deferral.rb +11 -15
- data/lib/simplecov/report_stamp.rb +6 -9
- data/lib/simplecov/result/filter_config.rb +19 -0
- data/lib/simplecov/result/missing_source_files_reporter.rb +8 -9
- data/lib/simplecov/result/serialization.rb +43 -0
- data/lib/simplecov/result/source_file_builder.rb +7 -12
- data/lib/simplecov/result.rb +68 -117
- data/lib/simplecov/result_adapter.rb +56 -68
- data/lib/simplecov/result_merger/contexts.rb +22 -0
- data/lib/simplecov/result_merger/legacy_format_adapter.rb +4 -8
- data/lib/simplecov/result_merger/resultset_file.rb +20 -27
- data/lib/simplecov/result_merger/resultset_run_identity.rb +23 -25
- data/lib/simplecov/result_merger/resultset_store.rb +23 -14
- data/lib/simplecov/result_merger/unloaded_files.rb +24 -32
- data/lib/simplecov/result_merger.rb +59 -58
- data/lib/simplecov/result_processing.rb +74 -119
- data/lib/simplecov/run_identity.rb +16 -18
- data/lib/simplecov/simulate_coverage.rb +34 -50
- data/lib/simplecov/source_file/branch.rb +8 -33
- data/lib/simplecov/source_file/branch_builder.rb +28 -47
- data/lib/simplecov/source_file/builder_context.rb +6 -13
- data/lib/simplecov/source_file/line.rb +13 -33
- data/lib/simplecov/source_file/line_builder.rb +9 -15
- data/lib/simplecov/source_file/method.rb +3 -7
- data/lib/simplecov/source_file/method_builder.rb +6 -15
- data/lib/simplecov/source_file/ruby_data_parser.rb +59 -66
- data/lib/simplecov/source_file/skip_chunks.rb +19 -28
- data/lib/simplecov/source_file/source_loader.rb +45 -35
- data/lib/simplecov/source_file/statistics.rb +10 -9
- data/lib/simplecov/source_file.rb +21 -51
- data/lib/simplecov/static_coverage_extractor/condition_folding.rb +63 -114
- data/lib/simplecov/static_coverage_extractor/location_conventions.rb +76 -121
- data/lib/simplecov/static_coverage_extractor/method_collector.rb +11 -24
- data/lib/simplecov/static_coverage_extractor/prism_compat.rb +15 -24
- data/lib/simplecov/static_coverage_extractor/value_position.rb +25 -38
- data/lib/simplecov/static_coverage_extractor/visitor.rb +47 -64
- data/lib/simplecov/static_coverage_extractor.rb +46 -62
- data/lib/simplecov/test_tracker/accessors.rb +34 -0
- data/lib/simplecov/test_tracker/constant_watch.rb +49 -0
- data/lib/simplecov/test_tracker/delta.rb +69 -0
- data/lib/simplecov/test_tracker/framework_hooks.rb +130 -0
- data/lib/simplecov/test_tracker.rb +162 -0
- data/lib/simplecov/unloaded_file_injector.rb +18 -31
- data/lib/simplecov/useless_results_remover.rb +7 -6
- data/lib/simplecov/version.rb +1 -1
- data/lib/simplecov/view_coverage/template_compiler.rb +83 -0
- data/lib/simplecov/view_coverage.rb +55 -0
- data/lib/simplecov-html.rb +1 -0
- data/lib/simplecov.rb +57 -113
- data/lib/simplecov_json_formatter.rb +1 -0
- data/man/simplecov.1 +349 -0
- data/sig/simplecov.rbs +523 -595
- metadata +72 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 774f2590be4fc8efd720e8c61b8e0678b50794dbc8c881c5872c51ac7b318c08
|
|
4
|
+
data.tar.gz: a67efdab2414345fa195aea9015d5cfc5128c452be20888edc1814c05fff98e0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 32062ccc9c83227f868788d5f2a0fa43eda93e8f714ed4ffe068fea3ac05ee7dd21f26d4234903da7679a6c72325955282000055892deaabf4a0703f924f06ca
|
|
7
|
+
data.tar.gz: c4a9a68bfbed7bc9c38de07afd89bedfefd3ea1ff1ee2b5294701c588c6cca7a10ae571313e0d836478045c07fcee81e4a1dc20c258d92e13a46f1e044c4d6d5
|
data/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
SimpleCov [](https://badge.fury.io/rb/simplecov) [][ci] [](https://github.com/simplecov-ruby/simplecov/actions/workflows/lint.yml) [](https://github.com/simplecov-ruby/simplecov/actions/workflows/typecheck.yml) [](https://codeclimate.com/github/simplecov-ruby/simplecov/maintainability)
|
|
1
|
+
SimpleCov [](https://badge.fury.io/rb/simplecov) [][ci] [](https://github.com/simplecov-ruby/simplecov/actions/workflows/lint.yml) [](https://github.com/simplecov-ruby/simplecov/actions/workflows/typecheck.yml) [](https://github.com/simplecov-ruby/simplecov/actions/workflows/mutation.yml) [](https://codeclimate.com/github/simplecov-ruby/simplecov/maintainability)
|
|
2
2
|
=========
|
|
3
3
|
|
|
4
4
|
**Code coverage for Ruby**
|
|
@@ -108,8 +108,10 @@ SimpleCov.start 'rails'
|
|
|
108
108
|
## Configuration at a glance
|
|
109
109
|
|
|
110
110
|
Configuration goes in your start block, or in a `.simplecov` file at the
|
|
111
|
-
project root when several test suites share it.
|
|
112
|
-
|
|
111
|
+
project root when several test suites share it. The API is built around a
|
|
112
|
+
small set of consistent verbs: formatters are picked by name, thresholds
|
|
113
|
+
live in a per-criterion `coverage` block where scope is a uniform `per:`
|
|
114
|
+
argument, and misses can be capped as absolute counts rather than ratios:
|
|
113
115
|
|
|
114
116
|
```ruby
|
|
115
117
|
SimpleCov.start do
|
|
@@ -121,9 +123,150 @@ SimpleCov.start do
|
|
|
121
123
|
coverage :line do
|
|
122
124
|
minimum 90 # fail the suite below 90% line coverage
|
|
123
125
|
maximum_drop 1 # ...or when coverage drops more than 1%
|
|
126
|
+
maximum_missed 5, per: :file # no file may carry more than 5 uncovered lines
|
|
124
127
|
end
|
|
128
|
+
|
|
129
|
+
coverage :branch, minimum: 80, ignore: :implicit_else
|
|
130
|
+
end
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Everything you're using today keeps working. Legacy spellings warn and name
|
|
134
|
+
their replacement, and once you've migrated, `deprecations :raise` turns any
|
|
135
|
+
old spelling that creeps back in into an error. The
|
|
136
|
+
[migration map](docs/Configuration.md#migrating-from-the-legacy-configuration-api)
|
|
137
|
+
has the full before and after, and every option is documented in
|
|
138
|
+
[docs/Configuration.md](docs/Configuration.md), including criteria, filters,
|
|
139
|
+
groups, profiles, and thresholds.
|
|
140
|
+
|
|
141
|
+
## Tracking which test covers each line
|
|
142
|
+
|
|
143
|
+
Coverage normally tells you whether a line ran, not what ran it. `track_tests`
|
|
144
|
+
records the other half of the story:
|
|
145
|
+
|
|
146
|
+
```ruby
|
|
147
|
+
SimpleCov.start do
|
|
148
|
+
track_tests
|
|
125
149
|
end
|
|
126
150
|
```
|
|
127
151
|
|
|
128
|
-
|
|
129
|
-
|
|
152
|
+
RSpec examples and Minitest tests are wrapped automatically. In the HTML
|
|
153
|
+
report, covered lines that no test executed (they only ran at load time, or in
|
|
154
|
+
suite setup) drain to a distinct tint, so coverage that merely *loads* code
|
|
155
|
+
stops passing for coverage that *tests* it, and clicking a line's badge lists
|
|
156
|
+
the tests that cover it. The same recording answers from the terminal:
|
|
157
|
+
|
|
158
|
+
```sh
|
|
159
|
+
$ simplecov tests lib/simplecov/result.rb:42
|
|
160
|
+
spec/result_spec.rb:42
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
The output is one test id per line and nothing else, so it pipes straight into
|
|
164
|
+
a runner. `simplecov tests --redundant` inverts the question, listing the
|
|
165
|
+
tests whose covered lines other tests also cover, which is where a
|
|
166
|
+
[test-pruning session](docs/Redundant_Tests.md) starts. Recording has a real
|
|
167
|
+
cost, which is why it's opt-in and comes with levers to control it. See
|
|
168
|
+
[the configuration docs](docs/Configuration.md#tracking-which-test-covers-each-line).
|
|
169
|
+
|
|
170
|
+
## Finding dead code in production
|
|
171
|
+
|
|
172
|
+
SimpleCov can also measure production code usage, the surest way to find
|
|
173
|
+
dead code. The old trick was to plant a log line in a suspect method and
|
|
174
|
+
watch production for a while. Oneshot coverage runs that experiment for
|
|
175
|
+
every line at once: a line reports its first execution and nothing after,
|
|
176
|
+
so a live process records what real traffic uses with the least possible
|
|
177
|
+
impact on performance. `simplecov dead-code` then crosses the recording
|
|
178
|
+
with the test report and turns it into insight you can act on. Code
|
|
179
|
+
neither tests nor traffic touch is safe to delete, and code production
|
|
180
|
+
runs but tests skip is the most valuable test you haven't written. The
|
|
181
|
+
HTML report and `coverage.json` include the same production data. See
|
|
182
|
+
[docs/Production.md](docs/Production.md) for more details on why and how
|
|
183
|
+
to set it up.
|
|
184
|
+
|
|
185
|
+
## Coverage of just your change
|
|
186
|
+
|
|
187
|
+
An overall number moves slowly on a mature codebase, but "is the code in this
|
|
188
|
+
change tested?" has a crisp answer the day you ask it. `simplecov patch` reads
|
|
189
|
+
the git diff against a base ref and scores only the lines you touched:
|
|
190
|
+
|
|
191
|
+
```sh
|
|
192
|
+
$ simplecov patch --base main --minimum 100
|
|
193
|
+
88.00% (22/25) lines lib/simplecov/cli/patch.rb missing 41-43
|
|
194
|
+
100.00% (4/4) lines lib/simplecov/result.rb
|
|
195
|
+
Patch coverage: 89.66% (26/29) lines
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
`--minimum` turns it into a gate, so a project that can't lift its overall
|
|
199
|
+
number in one pull request can still require that everything it adds is
|
|
200
|
+
covered. The flip side is `simplecov affected`, which uses a `track_tests`
|
|
201
|
+
recording to select the tests that touch your changed code and hand them to
|
|
202
|
+
the runner, falling back (loudly) to the full suite whenever the map can't be
|
|
203
|
+
trusted:
|
|
204
|
+
|
|
205
|
+
```sh
|
|
206
|
+
$ simplecov affected --base main --run bundle exec rspec
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
Both commands are documented in [the CLI docs](docs/CLI.md).
|
|
210
|
+
|
|
211
|
+
## Covering views
|
|
212
|
+
|
|
213
|
+
View templates execute real logic, and now they can be part of the report.
|
|
214
|
+
`cover_views` brings ERB, Haml, and Slim templates in, measured through eval
|
|
215
|
+
coverage (CRuby 3.2+):
|
|
216
|
+
|
|
217
|
+
```ruby
|
|
218
|
+
SimpleCov.start 'rails' do
|
|
219
|
+
cover_views
|
|
220
|
+
end
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
Templates are ordinary files in the report, highlighted in their own language
|
|
224
|
+
and grouped under Views by the `rails` profile, and a template no test renders
|
|
225
|
+
shows up at 0% instead of being quietly missing. Expect your overall number to
|
|
226
|
+
drop the first time you turn this on. That's the point. See
|
|
227
|
+
[view coverage](docs/Configuration.md#view-coverage).
|
|
228
|
+
|
|
229
|
+
## Per-file ratchets and coverage history
|
|
230
|
+
|
|
231
|
+
On a legacy codebase, one per-file minimum does nothing useful: set it to what
|
|
232
|
+
the worst file scores and every other file is allowed to sink to that level.
|
|
233
|
+
`simplecov ratchet` writes a checked-in baseline instead, giving each file its
|
|
234
|
+
own floor at the coverage it has already reached:
|
|
235
|
+
|
|
236
|
+
```sh
|
|
237
|
+
$ simplecov ratchet
|
|
238
|
+
simplecov ratchet: wrote .simplecov_baseline.yml (3 tightened, 1 pruned, 148 unchanged)
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
Floors only ever tighten, so touching a legacy file drags its coverage upward
|
|
242
|
+
and it can never slide back. Think `.rubocop_todo.yml`, applied to coverage.
|
|
243
|
+
To ratchet automatically at the end of every run, add the baseline formatter
|
|
244
|
+
with `formats :html, :baseline`.
|
|
245
|
+
|
|
246
|
+
Alongside the floors, every successful run now appends to
|
|
247
|
+
`coverage/.history.json`, so you have a recorded trend rather than just the
|
|
248
|
+
last number. `simplecov history` draws it as sparklines in the terminal, and
|
|
249
|
+
`drop_baseline :median` judges coverage drops against the recorded median
|
|
250
|
+
instead of whatever the previous run happened to score. See
|
|
251
|
+
[the baseline](docs/Configuration.md#per-file-baseline-ratchet) and
|
|
252
|
+
[run history](docs/Configuration.md#run-history) docs.
|
|
253
|
+
|
|
254
|
+
## More from the command line
|
|
255
|
+
|
|
256
|
+
The `simplecov` CLI has grown from a report opener into a toolbelt. A few
|
|
257
|
+
favorites:
|
|
258
|
+
|
|
259
|
+
```sh
|
|
260
|
+
$ simplecov watch bundle exec rspec # re-run on save, live-reload the served report
|
|
261
|
+
$ simplecov show lib/foo.rb # annotated source in the terminal
|
|
262
|
+
$ simplecov status # is this report fresh, and for which commit?
|
|
263
|
+
$ simplecov uncovered --missing # worst files, with the exact line ranges to test
|
|
264
|
+
$ simplecov badge --output badge.svg # a shields.io-style SVG, no badge service needed
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
`watch` deserves the highlight: with a `track_tests` recording in the report,
|
|
268
|
+
a save re-runs only the tests that touch the files you changed, which turns
|
|
269
|
+
the report into something you keep open while writing the test. There is also
|
|
270
|
+
shell tab completion (`simplecov completions fish|bash|zsh`), a man page, and
|
|
271
|
+
a real `--help` on every command. The full tour is in
|
|
272
|
+
[docs/CLI.md](docs/CLI.md).
|
data/exe/simplecov
CHANGED
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env ruby
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
|
-
# Make `./exe/simplecov` work from a fresh checkout, not only via
|
|
5
|
-
# `bundle exec`. RubyGems and Bundler already inject lib/ for an
|
|
6
|
-
# installed gem; this is a no-op in that case.
|
|
7
4
|
lib = File.expand_path("../lib", __dir__)
|
|
8
5
|
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
|
9
6
|
|
|
@@ -1,17 +1,20 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
# How minitest plugins. See https://github.com/simplecov-ruby/simplecov/pull/756 for why we need this.
|
|
4
|
-
# https://github.com/seattlerb/minitest#writing-extensions
|
|
5
|
-
#
|
|
6
|
-
# Handles the SimpleCov-first / Minitest-second ordering: SimpleCov.start
|
|
7
|
-
# runs before `require "minitest/autorun"`, so the SimpleCov.start-time
|
|
8
|
-
# detection in `install_at_exit_hook` can't see Minitest yet. By the time
|
|
9
|
-
# this plugin fires (inside `Minitest.run`), Minitest is loaded and we
|
|
10
|
-
# can flip the same switch. The opposite ordering (Minitest first) is
|
|
11
|
-
# handled in `SimpleCov.install_at_exit_hook` — see `#minitest_autorun_pending?`.
|
|
12
3
|
module Minitest
|
|
4
|
+
# Handles the SimpleCov-first / Minitest-second ordering: `SimpleCov.start`
|
|
5
|
+
# runs before `require "minitest/autorun"`, so its start-time detection can't
|
|
6
|
+
# see Minitest yet, but by the time this plugin fires Minitest is loaded and
|
|
7
|
+
# the same switch can be flipped. The opposite ordering is handled in
|
|
8
|
+
# `SimpleCov.install_at_exit_hook` (#756).
|
|
9
|
+
#
|
|
10
|
+
# The respond_to? guards cover a process where only simplecov's version file
|
|
11
|
+
# was loaded (#877), which defines the SimpleCov constant without any of its
|
|
12
|
+
# configuration.
|
|
13
13
|
def self.plugin_simplecov_init(_options)
|
|
14
14
|
return unless defined?(SimpleCov)
|
|
15
|
+
|
|
16
|
+
SimpleCov::TestTracker.install_minitest_hook if SimpleCov.respond_to?(:track_tests?) && SimpleCov.track_tests?
|
|
17
|
+
|
|
15
18
|
return if SimpleCov.external_at_exit?
|
|
16
19
|
|
|
17
20
|
SimpleCov.external_at_exit = true
|
|
@@ -5,7 +5,8 @@ require "tempfile"
|
|
|
5
5
|
|
|
6
6
|
module SimpleCov
|
|
7
7
|
# Replaces an artifact through a collision-safe temporary file in the
|
|
8
|
-
# destination directory, so readers see either the old or complete new
|
|
8
|
+
# destination directory, so readers see either the old or the complete new
|
|
9
|
+
# file.
|
|
9
10
|
module AtomicFile
|
|
10
11
|
DEFAULT_MODE = 0o666
|
|
11
12
|
private_constant :DEFAULT_MODE
|
|
@@ -22,38 +23,34 @@ module SimpleCov
|
|
|
22
23
|
nil
|
|
23
24
|
end
|
|
24
25
|
|
|
26
|
+
# Closing flushes the last buffered bytes, so the rename publishes a complete
|
|
27
|
+
# file rather than whatever happened to reach disk. No cleanup of its own:
|
|
28
|
+
# `Tempfile.create` closes and removes the file when its block ends, however
|
|
29
|
+
# it ends.
|
|
25
30
|
def self.replace(temp, path, content, mode, binary:)
|
|
26
31
|
temp.binmode if binary
|
|
27
32
|
temp.write(content)
|
|
28
33
|
temp.close
|
|
29
|
-
File.chmod(mode, temp
|
|
30
|
-
rename_over(temp
|
|
31
|
-
ensure
|
|
32
|
-
temp.close unless temp.closed?
|
|
33
|
-
FileUtils.rm_f(temp.path)
|
|
34
|
+
File.chmod(mode, temp)
|
|
35
|
+
rename_over(temp, path)
|
|
34
36
|
end
|
|
35
37
|
private_class_method :replace
|
|
36
38
|
|
|
37
|
-
# On POSIX systems rename replaces the destination atomically. On
|
|
38
|
-
#
|
|
39
|
-
#
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
attempts = 0
|
|
39
|
+
# On POSIX systems rename replaces the destination atomically. On Windows it
|
|
40
|
+
# fails with EACCES when the destination is open in a reader or
|
|
41
|
+
# mid-replacement by a concurrent writer, so retry briefly there.
|
|
42
|
+
def self.rename_over(temp, path)
|
|
43
|
+
remaining = 10
|
|
43
44
|
begin
|
|
44
|
-
File.rename(
|
|
45
|
+
File.rename(temp, path)
|
|
45
46
|
rescue Errno::EACCES
|
|
46
|
-
# simplecov:disable branch — which arm runs is fixed by the platform
|
|
47
47
|
raise unless Gem.win_platform?
|
|
48
48
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
attempts += 1
|
|
52
|
-
raise if attempts >= 10
|
|
49
|
+
remaining -= 1
|
|
50
|
+
raise if remaining.zero?
|
|
53
51
|
|
|
54
52
|
sleep(0.01)
|
|
55
53
|
retry
|
|
56
|
-
# simplecov:enable
|
|
57
54
|
end
|
|
58
55
|
end
|
|
59
56
|
private_class_method :rename_over
|
data/lib/simplecov/autostart.rb
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
# Loaded via `RUBYOPT="-rsimplecov/autostart"` from `simplecov run`. The
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
|
|
8
|
-
# guards against double-installing — so calling it unconditionally is
|
|
9
|
-
# the safe way to ensure the report is formatted at exit.
|
|
3
|
+
# Loaded via `RUBYOPT="-rsimplecov/autostart"` from `simplecov run`. The require
|
|
4
|
+
# below also auto-loads `~/.simplecov` and the project's `.simplecov`, which may
|
|
5
|
+
# already call `SimpleCov.start`. That call is idempotent, so making it
|
|
6
|
+
# unconditionally is the safe way to ensure the report is formatted at exit.
|
|
7
|
+
|
|
10
8
|
require "simplecov"
|
|
11
9
|
SimpleCov.start
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SimpleCov
|
|
4
|
+
class Baseline
|
|
5
|
+
# Turns the YAML document of a baseline file into the
|
|
6
|
+
# `{path => {criterion => Floor}}` entries `Baseline` holds. The canonical
|
|
7
|
+
# shape is the criteria Hash `to_yaml` writes, but the lenient shapes stay
|
|
8
|
+
# readable: a bare number is a line-percent floor, and a criterion may carry
|
|
9
|
+
# a bare percent instead of the percent/missed pair. Anything else raises
|
|
10
|
+
# ConfigurationError, naming the file and the offending entry: a malformed
|
|
11
|
+
# policy must fail loudly rather than silently un-enforce every floor.
|
|
12
|
+
module Parser
|
|
13
|
+
extend self
|
|
14
|
+
|
|
15
|
+
def call(data, path)
|
|
16
|
+
raise ConfigurationError, "baseline file #{path} must map file paths to floors" unless data.is_a?(Hash)
|
|
17
|
+
|
|
18
|
+
data.to_h { |file, entry| [file.to_s, parse_entry(file, entry, path)] }
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def parse_entry(file, entry, path)
|
|
22
|
+
return {line: Floor.new(percent: entry, missed: nil)} if entry.is_a?(Numeric)
|
|
23
|
+
|
|
24
|
+
unless entry.is_a?(Hash)
|
|
25
|
+
raise ConfigurationError, "baseline file #{path}: entry for #{file} must be a number or a criteria Hash"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
entry.to_h do |criterion_key, floor|
|
|
29
|
+
[parse_criterion(file, criterion_key, path), parse_floor(file, floor, path)]
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def parse_criterion(file, criterion_key, path)
|
|
34
|
+
CRITERIA[criterion_key.to_s] || raise(
|
|
35
|
+
ConfigurationError,
|
|
36
|
+
"baseline file #{path}: unknown criterion #{criterion_key.inspect} for #{file} " \
|
|
37
|
+
"(expected lines, branches, or methods)"
|
|
38
|
+
)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def parse_floor(file, floor, path)
|
|
42
|
+
return Floor.new(percent: floor, missed: nil) if floor.is_a?(Numeric)
|
|
43
|
+
|
|
44
|
+
percent = floor["percent"] if floor.is_a?(Hash)
|
|
45
|
+
unless percent.is_a?(Numeric)
|
|
46
|
+
raise ConfigurationError, "baseline file #{path}: floor for #{file} needs a numeric percent"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
missed = floor["missed"]
|
|
50
|
+
unless missed.nil? || missed.instance_of?(Integer)
|
|
51
|
+
raise ConfigurationError, "baseline file #{path}: missed count for #{file} must be an integer"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
Floor.new(percent: percent, missed: missed)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "yaml"
|
|
4
|
+
|
|
5
|
+
module SimpleCov
|
|
6
|
+
# The checked-in per-file coverage floor behind `simplecov ratchet` (#1268):
|
|
7
|
+
# `.rubocop_todo.yml` applied to coverage. Each entry records, per criterion,
|
|
8
|
+
# the percent a file has already reached and the number of misses it had when
|
|
9
|
+
# that percent was recorded.
|
|
10
|
+
#
|
|
11
|
+
# A floor carries both numbers because each covers for the other's blind
|
|
12
|
+
# spot. The percent is the policy, but a percent moves when a file is edited
|
|
13
|
+
# without any coverage change at all, so the missed count acts as the
|
|
14
|
+
# dampener. A violation therefore requires both a lower percent and more
|
|
15
|
+
# misses, which is the honest reading of "this file got worse".
|
|
16
|
+
class Baseline
|
|
17
|
+
DEFAULT_FILENAME = ".simplecov_baseline.yml"
|
|
18
|
+
|
|
19
|
+
HEADER = <<~HEADER
|
|
20
|
+
# Per-file coverage floors. A file that drops below its floor fails the run,
|
|
21
|
+
# and files with no entry fall through to the global per-file minimum.
|
|
22
|
+
# Regenerate with `simplecov ratchet`. Floors only ever tighten.
|
|
23
|
+
HEADER
|
|
24
|
+
private_constant :HEADER
|
|
25
|
+
|
|
26
|
+
# The YAML file speaks the report's vocabulary; internally floors are keyed
|
|
27
|
+
# by criterion the way the threshold configuration is.
|
|
28
|
+
CRITERIA = {"lines" => :line, "branches" => :branch, "methods" => :method}.freeze
|
|
29
|
+
private_constant :CRITERIA
|
|
30
|
+
|
|
31
|
+
# `missed` is nil for a hand-written percent-only entry, meaning the
|
|
32
|
+
# dampener is absent and the percent alone decides.
|
|
33
|
+
Floor = Data.define(:percent, :missed)
|
|
34
|
+
|
|
35
|
+
# The tightened baseline plus the path lists the CLI summarizes (each path
|
|
36
|
+
# appears in exactly one).
|
|
37
|
+
Outcome = Data.define(:baseline, :tightened, :pruned, :regressed, :unchanged)
|
|
38
|
+
|
|
39
|
+
# A file that exists but cannot be read as a baseline raises
|
|
40
|
+
# ConfigurationError: a malformed policy must fail loudly rather than
|
|
41
|
+
# silently un-enforce every floor it carried.
|
|
42
|
+
def self.read_if_exists(path)
|
|
43
|
+
return nil unless File.exist?(path)
|
|
44
|
+
|
|
45
|
+
new(Parser.call(YAML.safe_load_file(path), path))
|
|
46
|
+
rescue Psych::Exception => e
|
|
47
|
+
# Interpolating the exception renders the message Psych objected with.
|
|
48
|
+
raise ConfigurationError, "baseline file #{path} is not valid YAML: #{e}"
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# `current` is a `{project_filename => {criterion => {percent:, missed:}}}`
|
|
52
|
+
# Hash of the measured state. Everything the report carries gets a floor at
|
|
53
|
+
# its current coverage.
|
|
54
|
+
def self.read(path)
|
|
55
|
+
Deprecation.warn("`SimpleCov::Baseline.read` is deprecated. Replace with `read_if_exists`.")
|
|
56
|
+
read_if_exists(path)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def self.generate(current)
|
|
60
|
+
new(current.transform_values do |criteria|
|
|
61
|
+
criteria.transform_values { |floor| Floor.new(percent: floor.fetch(:percent), missed: floor.fetch(:missed)) }
|
|
62
|
+
end)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
attr_reader :entries
|
|
66
|
+
|
|
67
|
+
def initialize(entries)
|
|
68
|
+
@entries = entries
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def entry_for(project_filename)
|
|
72
|
+
entries[project_filename]
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def floor_for(project_filename, criterion)
|
|
76
|
+
entries.dig(project_filename, criterion)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Which is also what exempts the pair from the per-file thresholds.
|
|
80
|
+
def covers?(project_filename, criterion)
|
|
81
|
+
!floor_for(project_filename, criterion).nil?
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Floors only move in the tightening direction, each axis independently:
|
|
85
|
+
# the percent keeps its best, the missed count its lowest. Entries for
|
|
86
|
+
# files `current` does not carry are pruned; files `current` carries
|
|
87
|
+
# without an entry stay uncovered, so new code answers to the global
|
|
88
|
+
# standard rather than to a floor cut at whatever it launched with. A
|
|
89
|
+
# criterion the current run did not measure keeps its floor, and a newly
|
|
90
|
+
# measured criterion joins the entry, so a later `enable_coverage :branch`
|
|
91
|
+
# grandfathers the legacy files' branch state.
|
|
92
|
+
def ratchet(current)
|
|
93
|
+
buckets = {tightened: [], pruned: [], regressed: [], unchanged: []} #: Hash[Symbol, Array[String]]
|
|
94
|
+
ratcheted = entries.filter_map do |file, entry|
|
|
95
|
+
step = ratchet_entry(entry, current[file])
|
|
96
|
+
buckets.fetch(step.fetch(:bucket)) << file
|
|
97
|
+
merged = step[:entry]
|
|
98
|
+
[file, merged] if merged
|
|
99
|
+
end.to_h
|
|
100
|
+
|
|
101
|
+
Outcome.new(baseline: Baseline.new(ratcheted), tightened: buckets.fetch(:tightened),
|
|
102
|
+
pruned: buckets.fetch(:pruned), regressed: buckets.fetch(:regressed),
|
|
103
|
+
unchanged: buckets.fetch(:unchanged))
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Sorted by path so a ratchet rewrite diffs as the set of floors that
|
|
107
|
+
# actually moved.
|
|
108
|
+
def to_yaml
|
|
109
|
+
document = entries.sort.to_h do |file, entry|
|
|
110
|
+
[file, entry.to_h { |criterion, floor| [CRITERIA.key(criterion), dump_floor(floor)] }]
|
|
111
|
+
end
|
|
112
|
+
HEADER + YAML.dump(document).delete_prefix("---\n")
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
private
|
|
116
|
+
|
|
117
|
+
# A nil current prunes the entry, which carries no replacement and so
|
|
118
|
+
# answers with the bucket alone. Otherwise every measured criterion
|
|
119
|
+
# tightens, keeping the ones this run did not measure.
|
|
120
|
+
def ratchet_entry(entry, current_entry)
|
|
121
|
+
return {bucket: :pruned} unless current_entry
|
|
122
|
+
|
|
123
|
+
merged = (entry.keys | current_entry.keys).to_h do |criterion|
|
|
124
|
+
[criterion, tighten(entry[criterion], current_entry[criterion])]
|
|
125
|
+
end
|
|
126
|
+
{bucket: bucket_for(entry, merged, current_entry), entry: merged}
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# The regressed bucket mirrors the violation rule exactly, so "below its
|
|
130
|
+
# floor" in the ratchet summary always means the exit check fails the file,
|
|
131
|
+
# never a percent drift the dampener tolerates.
|
|
132
|
+
def bucket_for(entry, merged, current_entry)
|
|
133
|
+
# Exact equality rather than numeric: tightening keeps the entry's own
|
|
134
|
+
# floor on a tie, so an entry that did not move is the very values it
|
|
135
|
+
# came in with.
|
|
136
|
+
return :tightened unless merged.eql?(entry)
|
|
137
|
+
|
|
138
|
+
regressed = entry.any? do |criterion, floor|
|
|
139
|
+
current = current_entry[criterion]
|
|
140
|
+
current && current.fetch(:percent) < floor.percent &&
|
|
141
|
+
(floor.missed.nil? || current.fetch(:missed) > floor.missed)
|
|
142
|
+
end
|
|
143
|
+
regressed ? :regressed : :unchanged
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Either side may be absent: a floor with no current measurement stays, a
|
|
147
|
+
# measurement with no floor becomes one.
|
|
148
|
+
def tighten(floor, current)
|
|
149
|
+
# The keys union in ratchet_entry guarantees at least one side is present,
|
|
150
|
+
# which the cast restates for the type checker.
|
|
151
|
+
return (_ = floor) unless current
|
|
152
|
+
|
|
153
|
+
current_floor = Floor.new(percent: current.fetch(:percent), missed: current.fetch(:missed))
|
|
154
|
+
return current_floor unless floor
|
|
155
|
+
|
|
156
|
+
Floor.new(
|
|
157
|
+
percent: [floor.percent, current_floor.percent].max,
|
|
158
|
+
missed: [floor.missed, current_floor.missed].compact.min
|
|
159
|
+
)
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def dump_floor(floor)
|
|
163
|
+
dumped = {"percent" => floor.percent} #: Hash[String, untyped]
|
|
164
|
+
dumped["missed"] = floor.missed unless floor.missed.nil?
|
|
165
|
+
dumped
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
require_relative "baseline/parser"
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../git"
|
|
4
|
+
|
|
5
|
+
module SimpleCov
|
|
6
|
+
module CLI
|
|
7
|
+
module Affected
|
|
8
|
+
# The change as git sees it, as `{root:, changed:}`: the files that differ
|
|
9
|
+
# between the merge base of the given ref and the working tree, so commits
|
|
10
|
+
# that landed on the base after the branch point stay out while uncommitted
|
|
11
|
+
# work stays in, unioned with untracked files, since a brand-new spec exists
|
|
12
|
+
# in no diff. Everything runs against the repository toplevel rather than the
|
|
13
|
+
# cwd, so a subdirectory run selects over the whole change. NUL separation
|
|
14
|
+
# keeps exotic filenames literal instead of quoted.
|
|
15
|
+
module ChangedFiles
|
|
16
|
+
extend self
|
|
17
|
+
|
|
18
|
+
def call(base, stderr)
|
|
19
|
+
return report(stderr, "invalid base ref #{base.inspect}") if Git.option_like_ref?(base)
|
|
20
|
+
|
|
21
|
+
root = repository_toplevel(stderr)
|
|
22
|
+
return nil unless root
|
|
23
|
+
|
|
24
|
+
diff = list("diff", ["-C", root, "diff", "--name-only", "-z", "--merge-base", base, "--"], stderr)
|
|
25
|
+
return nil unless diff
|
|
26
|
+
|
|
27
|
+
untracked = list("ls-files", ["-C", root, "ls-files", "--others", "--exclude-standard", "-z"], stderr)
|
|
28
|
+
untracked && {root: root, changed: (diff + untracked).uniq}
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# nil stdout from `Git.capture` means git never ran; a run that failed
|
|
32
|
+
# means the cwd is outside a working tree.
|
|
33
|
+
def repository_toplevel(stderr)
|
|
34
|
+
stdout, detail, success = Git.capture("rev-parse", "--show-toplevel")
|
|
35
|
+
return (_ = stdout).chomp if success
|
|
36
|
+
return report(stderr, "cannot run git (#{detail})") if stdout.nil?
|
|
37
|
+
|
|
38
|
+
report(stderr, "not inside a git working tree")
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# git's own first stderr line is relayed on failure, so a bad ref or an old
|
|
42
|
+
# git names itself.
|
|
43
|
+
def list(command, argv, stderr)
|
|
44
|
+
stdout, detail, success = Git.capture(*argv)
|
|
45
|
+
return (_ = stdout).split("\0") if success
|
|
46
|
+
return report(stderr, "cannot run git (#{detail})") if stdout.nil?
|
|
47
|
+
|
|
48
|
+
report(stderr, "`git #{command}` failed: #{detail}")
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def report(stderr, message)
|
|
52
|
+
stderr.puts("simplecov affected: #{message}")
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|