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.
Files changed (194) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +148 -5
  3. data/exe/simplecov +0 -3
  4. data/lib/minitest/simplecov_plugin.rb +12 -9
  5. data/lib/simplecov/atomic_file.rb +16 -19
  6. data/lib/simplecov/autostart.rb +5 -7
  7. data/lib/simplecov/baseline/parser.rb +58 -0
  8. data/lib/simplecov/baseline.rb +170 -0
  9. data/lib/simplecov/cli/affected/changed_files.rb +57 -0
  10. data/lib/simplecov/cli/affected/selection.rb +117 -0
  11. data/lib/simplecov/cli/affected.rb +148 -0
  12. data/lib/simplecov/cli/badge/svg.rb +70 -0
  13. data/lib/simplecov/cli/badge.rb +62 -0
  14. data/lib/simplecov/cli/clean.rb +23 -24
  15. data/lib/simplecov/cli/command_helpers.rb +55 -19
  16. data/lib/simplecov/cli/completions/scripts.rb +114 -0
  17. data/lib/simplecov/cli/completions.rb +83 -0
  18. data/lib/simplecov/cli/coverage.rb +25 -28
  19. data/lib/simplecov/cli/coverage_file.rb +58 -14
  20. data/lib/simplecov/cli/dead_code/output.rb +100 -0
  21. data/lib/simplecov/cli/dead_code.rb +139 -0
  22. data/lib/simplecov/cli/diff/output.rb +45 -0
  23. data/lib/simplecov/cli/diff.rb +35 -67
  24. data/lib/simplecov/cli/dotfile.rb +83 -38
  25. data/lib/simplecov/cli/git.rb +41 -0
  26. data/lib/simplecov/cli/history/output.rb +135 -0
  27. data/lib/simplecov/cli/history.rb +67 -0
  28. data/lib/simplecov/cli/merge.rb +34 -39
  29. data/lib/simplecov/cli/open.rb +12 -15
  30. data/lib/simplecov/cli/patch/changed_lines.rb +149 -0
  31. data/lib/simplecov/cli/patch/output.rb +135 -0
  32. data/lib/simplecov/cli/patch.rb +169 -0
  33. data/lib/simplecov/cli/ratchet/output.rb +44 -0
  34. data/lib/simplecov/cli/ratchet.rb +83 -0
  35. data/lib/simplecov/cli/report.rb +18 -24
  36. data/lib/simplecov/cli/run.rb +8 -9
  37. data/lib/simplecov/cli/serve/report_preparer.rb +7 -6
  38. data/lib/simplecov/cli/serve/static_file_handler.rb +62 -56
  39. data/lib/simplecov/cli/serve.rb +27 -24
  40. data/lib/simplecov/cli/show/annotator.rb +87 -0
  41. data/lib/simplecov/cli/show/sweep.rb +36 -0
  42. data/lib/simplecov/cli/show.rb +133 -0
  43. data/lib/simplecov/cli/status/facts.rb +87 -0
  44. data/lib/simplecov/cli/status.rb +93 -0
  45. data/lib/simplecov/cli/tests/redundancy.rb +78 -0
  46. data/lib/simplecov/cli/tests.rb +148 -0
  47. data/lib/simplecov/cli/uncovered/misses.rb +37 -0
  48. data/lib/simplecov/cli/uncovered.rb +70 -32
  49. data/lib/simplecov/cli/usage.rb +152 -0
  50. data/lib/simplecov/cli/watch/live_report.rb +73 -0
  51. data/lib/simplecov/cli/watch/narrator.rb +55 -0
  52. data/lib/simplecov/cli/watch/poller.rb +45 -0
  53. data/lib/simplecov/cli/watch/session.rb +134 -0
  54. data/lib/simplecov/cli/watch/test_plan.rb +43 -0
  55. data/lib/simplecov/cli/watch.rb +85 -0
  56. data/lib/simplecov/cli.rb +43 -92
  57. data/lib/simplecov/color.rb +19 -37
  58. data/lib/simplecov/combine/branches_combiner.rb +7 -31
  59. data/lib/simplecov/combine/coverage_accumulator.rb +49 -142
  60. data/lib/simplecov/combine/identity_interner.rb +10 -13
  61. data/lib/simplecov/combine/interned_counts.rb +7 -9
  62. data/lib/simplecov/combine/lines_combiner.rb +20 -44
  63. data/lib/simplecov/combine/methods_combiner.rb +9 -35
  64. data/lib/simplecov/combine/results_combiner.rb +4 -21
  65. data/lib/simplecov/combine.rb +0 -6
  66. data/lib/simplecov/command_guesser.rb +20 -34
  67. data/lib/simplecov/configuration/baseline.rb +31 -0
  68. data/lib/simplecov/configuration/coverage.rb +91 -79
  69. data/lib/simplecov/configuration/coverage_criteria.rb +43 -54
  70. data/lib/simplecov/configuration/deprecations.rb +21 -0
  71. data/lib/simplecov/configuration/eval_coverage.rb +6 -10
  72. data/lib/simplecov/configuration/filters.rb +40 -73
  73. data/lib/simplecov/configuration/formatting.rb +74 -83
  74. data/lib/simplecov/configuration/groups.rb +19 -13
  75. data/lib/simplecov/configuration/history.rb +44 -0
  76. data/lib/simplecov/configuration/ignored_entries.rb +31 -22
  77. data/lib/simplecov/configuration/merging.rb +94 -62
  78. data/lib/simplecov/configuration/missed_caps.rb +51 -0
  79. data/lib/simplecov/configuration/production.rb +23 -0
  80. data/lib/simplecov/configuration/test_tracking.rb +48 -0
  81. data/lib/simplecov/configuration/thresholds.rb +33 -62
  82. data/lib/simplecov/configuration/view_coverage.rb +38 -0
  83. data/lib/simplecov/configuration.rb +70 -76
  84. data/lib/simplecov/configuration_error.rb +5 -0
  85. data/lib/simplecov/context_map/union.rb +81 -0
  86. data/lib/simplecov/context_map.rb +187 -0
  87. data/lib/simplecov/coverage_json.rb +2 -5
  88. data/lib/simplecov/coverage_statistics.rb +14 -24
  89. data/lib/simplecov/coverage_violations.rb +157 -64
  90. data/lib/simplecov/current_run.rb +61 -0
  91. data/lib/simplecov/defaults.rb +4 -23
  92. data/lib/simplecov/deprecation.rb +45 -27
  93. data/lib/simplecov/directive.rb +46 -62
  94. data/lib/simplecov/exit_codes/baseline_check.rb +46 -0
  95. data/lib/simplecov/exit_codes/check.rb +17 -11
  96. data/lib/simplecov/exit_codes/exit_code_handling.rb +21 -8
  97. data/lib/simplecov/exit_codes/maximum_coverage_drop_check.rb +7 -10
  98. data/lib/simplecov/exit_codes/maximum_missed_check.rb +26 -0
  99. data/lib/simplecov/exit_codes/maximum_missed_per_file_check.rb +34 -0
  100. data/lib/simplecov/exit_codes/maximum_overall_coverage_check.rb +7 -12
  101. data/lib/simplecov/exit_codes/minimum_coverage_by_file_check.rb +11 -10
  102. data/lib/simplecov/exit_codes/minimum_coverage_by_group_check.rb +7 -9
  103. data/lib/simplecov/exit_codes/minimum_overall_coverage_check.rb +16 -17
  104. data/lib/simplecov/exit_codes.rb +16 -10
  105. data/lib/simplecov/exit_handling.rb +94 -59
  106. data/lib/simplecov/file_list.rb +20 -51
  107. data/lib/simplecov/filter.rb +43 -66
  108. data/lib/simplecov/formatter/base.rb +37 -55
  109. data/lib/simplecov/formatter/baseline_formatter.rb +98 -0
  110. data/lib/simplecov/formatter/coverage_json_writer.rb +29 -31
  111. data/lib/simplecov/formatter/html_formatter/public/index.html +15 -9
  112. data/lib/simplecov/formatter/html_formatter/viewer_data_validator.rb +74 -17
  113. data/lib/simplecov/formatter/html_formatter.rb +24 -41
  114. data/lib/simplecov/formatter/json_formatter/errors_formatter.rb +50 -16
  115. data/lib/simplecov/formatter/json_formatter/production_section_formatter.rb +48 -0
  116. data/lib/simplecov/formatter/json_formatter/result_hash_formatter.rb +43 -29
  117. data/lib/simplecov/formatter/json_formatter/source_file_formatter.rb +14 -10
  118. data/lib/simplecov/formatter/json_formatter.rb +7 -13
  119. data/lib/simplecov/formatter/multi_formatter.rb +5 -11
  120. data/lib/simplecov/formatter/simple_formatter.rb +2 -8
  121. data/lib/simplecov/formatter.rb +14 -10
  122. data/lib/simplecov/group_names.rb +13 -12
  123. data/lib/simplecov/history.rb +115 -0
  124. data/lib/simplecov/last_run.rb +6 -9
  125. data/lib/simplecov/lines_classifier.rb +31 -16
  126. data/lib/simplecov/load_global_config.rb +7 -7
  127. data/lib/simplecov/parallel_adapters/base.rb +13 -41
  128. data/lib/simplecov/parallel_adapters/generic.rb +7 -19
  129. data/lib/simplecov/parallel_adapters/parallel_tests.rb +21 -42
  130. data/lib/simplecov/parallel_adapters.rb +24 -55
  131. data/lib/simplecov/parallel_coordination.rb +39 -49
  132. data/lib/simplecov/parallel_result_merger/worker_payload.rb +33 -0
  133. data/lib/simplecov/parallel_result_merger.rb +67 -126
  134. data/lib/simplecov/process.rb +14 -32
  135. data/lib/simplecov/production/error.rb +11 -0
  136. data/lib/simplecov/production/file_sink.rb +130 -0
  137. data/lib/simplecov/production.rb +249 -0
  138. data/lib/simplecov/profiles/rails.rb +1 -12
  139. data/lib/simplecov/profiles/root_filter.rb +0 -4
  140. data/lib/simplecov/profiles/strict.rb +9 -19
  141. data/lib/simplecov/profiles.rb +11 -33
  142. data/lib/simplecov/report_deferral.rb +11 -15
  143. data/lib/simplecov/report_stamp.rb +6 -9
  144. data/lib/simplecov/result/filter_config.rb +19 -0
  145. data/lib/simplecov/result/missing_source_files_reporter.rb +8 -9
  146. data/lib/simplecov/result/serialization.rb +43 -0
  147. data/lib/simplecov/result/source_file_builder.rb +7 -12
  148. data/lib/simplecov/result.rb +68 -117
  149. data/lib/simplecov/result_adapter.rb +56 -68
  150. data/lib/simplecov/result_merger/contexts.rb +22 -0
  151. data/lib/simplecov/result_merger/legacy_format_adapter.rb +4 -8
  152. data/lib/simplecov/result_merger/resultset_file.rb +20 -27
  153. data/lib/simplecov/result_merger/resultset_run_identity.rb +23 -25
  154. data/lib/simplecov/result_merger/resultset_store.rb +23 -14
  155. data/lib/simplecov/result_merger/unloaded_files.rb +24 -32
  156. data/lib/simplecov/result_merger.rb +59 -58
  157. data/lib/simplecov/result_processing.rb +74 -119
  158. data/lib/simplecov/run_identity.rb +16 -18
  159. data/lib/simplecov/simulate_coverage.rb +34 -50
  160. data/lib/simplecov/source_file/branch.rb +8 -33
  161. data/lib/simplecov/source_file/branch_builder.rb +28 -47
  162. data/lib/simplecov/source_file/builder_context.rb +6 -13
  163. data/lib/simplecov/source_file/line.rb +13 -33
  164. data/lib/simplecov/source_file/line_builder.rb +9 -15
  165. data/lib/simplecov/source_file/method.rb +3 -7
  166. data/lib/simplecov/source_file/method_builder.rb +6 -15
  167. data/lib/simplecov/source_file/ruby_data_parser.rb +59 -66
  168. data/lib/simplecov/source_file/skip_chunks.rb +19 -28
  169. data/lib/simplecov/source_file/source_loader.rb +45 -35
  170. data/lib/simplecov/source_file/statistics.rb +10 -9
  171. data/lib/simplecov/source_file.rb +21 -51
  172. data/lib/simplecov/static_coverage_extractor/condition_folding.rb +63 -114
  173. data/lib/simplecov/static_coverage_extractor/location_conventions.rb +76 -121
  174. data/lib/simplecov/static_coverage_extractor/method_collector.rb +11 -24
  175. data/lib/simplecov/static_coverage_extractor/prism_compat.rb +15 -24
  176. data/lib/simplecov/static_coverage_extractor/value_position.rb +25 -38
  177. data/lib/simplecov/static_coverage_extractor/visitor.rb +47 -64
  178. data/lib/simplecov/static_coverage_extractor.rb +46 -62
  179. data/lib/simplecov/test_tracker/accessors.rb +34 -0
  180. data/lib/simplecov/test_tracker/constant_watch.rb +49 -0
  181. data/lib/simplecov/test_tracker/delta.rb +69 -0
  182. data/lib/simplecov/test_tracker/framework_hooks.rb +130 -0
  183. data/lib/simplecov/test_tracker.rb +162 -0
  184. data/lib/simplecov/unloaded_file_injector.rb +18 -31
  185. data/lib/simplecov/useless_results_remover.rb +7 -6
  186. data/lib/simplecov/version.rb +1 -1
  187. data/lib/simplecov/view_coverage/template_compiler.rb +83 -0
  188. data/lib/simplecov/view_coverage.rb +55 -0
  189. data/lib/simplecov-html.rb +1 -0
  190. data/lib/simplecov.rb +57 -113
  191. data/lib/simplecov_json_formatter.rb +1 -0
  192. data/man/simplecov.1 +349 -0
  193. data/sig/simplecov.rbs +523 -595
  194. metadata +72 -5
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9446e795d35024cc5d47004e88956620c65c6f666fe387323f4192947e942125
4
- data.tar.gz: 85596147a4854e414574ab09b7329ffd1e949aa6a1f108308e1364712f19b491
3
+ metadata.gz: 774f2590be4fc8efd720e8c61b8e0678b50794dbc8c881c5872c51ac7b318c08
4
+ data.tar.gz: a67efdab2414345fa195aea9015d5cfc5128c452be20888edc1814c05fff98e0
5
5
  SHA512:
6
- metadata.gz: 12e270b345de7dd7f2abaaafe9ab9d3034b4c6489e9c465a7bfb0d71de1f66382053e0c43621ac57a49f4194503bbcc692107f1770e3775986ac023dbb06a23c
7
- data.tar.gz: 50a81cb51a4186ac664ca01003c4d7ae3ed8b1e0dff7abdf83ce359c490e016fb6f8d4f0fa5e69433934978b8be1f08e8cd530900538d377f4d508771e055323
6
+ metadata.gz: 32062ccc9c83227f868788d5f2a0fa43eda93e8f714ed4ffe068fea3ac05ee7dd21f26d4234903da7679a6c72325955282000055892deaabf4a0703f924f06ca
7
+ data.tar.gz: c4a9a68bfbed7bc9c38de07afd89bedfefd3ea1ff1ee2b5294701c588c6cca7a10ae571313e0d836478045c07fcee81e4a1dc20c258d92e13a46f1e044c4d6d5
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- SimpleCov [![Gem Version](https://badge.fury.io/rb/simplecov.svg)](https://badge.fury.io/rb/simplecov) [![Build Status](https://github.com/simplecov-ruby/simplecov/actions/workflows/stable.yml/badge.svg?branch=main)][ci] [![Lint](https://github.com/simplecov-ruby/simplecov/actions/workflows/lint.yml/badge.svg?branch=main)](https://github.com/simplecov-ruby/simplecov/actions/workflows/lint.yml) [![Typecheck](https://github.com/simplecov-ruby/simplecov/actions/workflows/typecheck.yml/badge.svg?branch=main)](https://github.com/simplecov-ruby/simplecov/actions/workflows/typecheck.yml) [![Maintainability](https://api.codeclimate.com/v1/badges/c071d197d61953a7e482/maintainability)](https://codeclimate.com/github/simplecov-ruby/simplecov/maintainability)
1
+ SimpleCov [![Gem Version](https://badge.fury.io/rb/simplecov.svg)](https://badge.fury.io/rb/simplecov) [![Build Status](https://github.com/simplecov-ruby/simplecov/actions/workflows/stable.yml/badge.svg?branch=main)][ci] [![Lint](https://github.com/simplecov-ruby/simplecov/actions/workflows/lint.yml/badge.svg?branch=main)](https://github.com/simplecov-ruby/simplecov/actions/workflows/lint.yml) [![Typecheck](https://github.com/simplecov-ruby/simplecov/actions/workflows/typecheck.yml/badge.svg?branch=main)](https://github.com/simplecov-ruby/simplecov/actions/workflows/typecheck.yml) [![Mutation](https://github.com/simplecov-ruby/simplecov/actions/workflows/mutation.yml/badge.svg?branch=main)](https://github.com/simplecov-ruby/simplecov/actions/workflows/mutation.yml) [![Maintainability](https://api.codeclimate.com/v1/badges/c071d197d61953a7e482/maintainability)](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. Some of the most common
112
- settings:
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
- Every option is documented in [docs/Configuration.md](docs/Configuration.md),
129
- including criteria, filters, groups, profiles, and thresholds.
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 file.
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.path)
30
- rename_over(temp.path, path)
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
- # Windows it fails with EACCES when the destination is open in a
39
- # reader or mid-replacement by a concurrent writer, so retry briefly
40
- # there before giving up.
41
- def self.rename_over(temp_path, path)
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(temp_path, path)
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
- # simplecov:enable branch
50
- # simplecov:disable — Windows-only; unreachable where dogfood runs
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
@@ -1,11 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Loaded via `RUBYOPT="-rsimplecov/autostart"` from `simplecov run`. The
4
- # `require "simplecov"` here also auto-loads `~/.simplecov` and the
5
- # project's `.simplecov` (per simplecov/defaults.rb), which may already
6
- # call `SimpleCov.start`. `SimpleCov.start` is idempotent it won't
7
- # restart Coverage if it's already running, and `install_at_exit_hook`
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