simplecov-rspec 0.4.4 → 1.1.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 484e82254055894678693503184af02dd373567de941c2e23b5171a2b1d43099
4
- data.tar.gz: 2b75d02a33e196bb9a8d0e802d88e5ff56cf4b5afccf6518d5d45db49548aee3
3
+ metadata.gz: fcdb582042f1b21ad1a1bd486c120fd590f143e73cb51b6b693e3d5755396546
4
+ data.tar.gz: 6a39d87792ea1505b4e0b9af8e4e81ec447541f8e1a0aea8413b49918cfa70d1
5
5
  SHA512:
6
- metadata.gz: 55816daa25c63ff70efaa2ecd127467384d9cc70bfa6bd0006789dbf4ae731f41a0423a803d8e6d56ec9139c08a98ce72d6577181c8845ae5d85e592e884de42
7
- data.tar.gz: f6e35a9fa9fb61073e1881112be2b035955089b65f86ed7c22beda11aef5d67b4c1d943ecd2a67f964220e6c449445fee8b1d32b3b4214891ce3b62595ad3c8b
6
+ metadata.gz: 27efb22e915daa0cf8a00692a0f65ecdcf659b1badb7cef5210a86aed05a264cb1c4aee1d89aefe39e53515b3d8edf85481d071ef23e31c93f87f616fe4485ec
7
+ data.tar.gz: 2883aab5cd82ddecbcc46e081cb03048e64dd0297abc0d06da1998cc178b5697e2813bf5cbe4489c4b5d2889425afa39cfc2804ec1e0409bbd857e3142a16422
@@ -1,3 +1,3 @@
1
1
  {
2
- ".": "0.4.4"
2
+ ".": "1.1.0"
3
3
  }
data/.rubocop.yml CHANGED
@@ -4,7 +4,13 @@ inherit_gem:
4
4
  AllCops:
5
5
  # RuboCop enforces rules depending on the oldest version of Ruby which
6
6
  # your project supports:
7
- TargetRubyVersion: 3.1
7
+ TargetRubyVersion: 3.2
8
+
9
+ Metrics/ClassLength:
10
+ # SimpleCov::RSpec is a configuration surface: every option it accepts costs a reader
11
+ # method, an ENV-name constant, and an ivar. The default 100 is a smell threshold for
12
+ # classes that do work, not for one that mostly declares options.
13
+ Max: 120
8
14
 
9
15
  Style/StderrPuts:
10
16
  Enabled: false
data/CHANGELOG.md CHANGED
@@ -4,6 +4,27 @@ Changes for each release are listed in this file.
4
4
 
5
5
  This project adheres to [Semantic Versioning](https://semver.org/) for its releases.
6
6
 
7
+ ## [1.1.0](https://github.com/main-branch/simplecov-rspec/compare/v1.0.0...v1.1.0) (2026-08-06)
8
+
9
+
10
+ ### Features
11
+
12
+ * Add :described to scope the listing to the code under test ([71fe0c7](https://github.com/main-branch/simplecov-rspec/commit/71fe0c715063582ff207a6ac15e9a2c6dbe26243))
13
+ * Add list_uncovered_files to scope the uncovered listing ([1981e65](https://github.com/main-branch/simplecov-rspec/commit/1981e6528a418340c4e211ff0b34ed745693151a))
14
+
15
+ ## [1.0.0](https://github.com/main-branch/simplecov-rspec/compare/v0.4.4...v1.0.0) (2026-08-05)
16
+
17
+
18
+ ### ⚠ BREAKING CHANGES
19
+
20
+ * `coverage_threshold:` and `list_uncovered_lines:` are removed in favor of `minimum_coverage:` and `list_uncovered:`; the `LIST_UNCOVERED_LINES` env var is removed in favor of `LIST_UNCOVERED`; and `COVERAGE_THRESHOLD` now sets only the line-coverage threshold instead of an overall coverage percentage. Update calls to `SimpleCov::RSpec.start` and any CI environment variables accordingly.
21
+ * drops support for Ruby 3.1 and JRuby 9.4; the minimum is now Ruby 3.2 (or JRuby 10, which reports RUBY_VERSION 3.4).
22
+
23
+ ### Features
24
+
25
+ * Rely on SimpleCov's own minimum_coverage instead of reimplementing it ([d2fed78](https://github.com/main-branch/simplecov-rspec/commit/d2fed78ff5e5cb2bbf18bc89cb3b301455de0bce))
26
+ * Require Ruby >= 3.2, matching SimpleCov 1.0's own floor ([9452587](https://github.com/main-branch/simplecov-rspec/commit/9452587137e1b698019d4214981f82ed796b9c1d))
27
+
7
28
  ## [0.4.4](https://github.com/main-branch/simplecov-rspec/compare/v0.4.3...v0.4.4) (2026-04-24)
8
29
 
9
30
 
data/README.md CHANGED
@@ -8,38 +8,77 @@
8
8
  Commits](https://img.shields.io/badge/Conventional%20Commits-1.0.0-%23FE5196?logo=conventionalcommits&logoColor=white)](https://conventionalcommits.org)
9
9
  [![Slack](https://img.shields.io/badge/slack-main--branch/simplecov--rspec-yellow.svg?logo=slack)](https://main-branch.slack.com/archives/C07MCM9J72B)
10
10
 
11
- `simplecov-rspec` is a Ruby gem that integrates SimpleCov with RSpec to ensure your
12
- tests meet a minimum coverage threshold. It enhances your test suite by automatically
13
- failing tests when coverage falls below a specified threshold and, optionally,
14
- listing uncovered lines to help you improve coverage.
11
+ `simplecov-rspec` is a Ruby gem that integrates SimpleCov with RSpec. SimpleCov
12
+ (`>= 1.0`) already enforces `minimum_coverage` for line, branch, and method coverage
13
+ and fails the build when a threshold is missed. This gem layers four things on top
14
+ that SimpleCov doesn't do on its own:
15
+
16
+ 1. Suppresses coverage failures when RSpec is run in dry-run mode (e.g. from an IDE).
17
+ 2. Lists (or summarizes) the individual uncovered lines, branches, and methods.
18
+ 3. Scopes that listing to the files you name, or to the code the run described.
19
+ 4. Lets all of the above be overridden from the environment, for CI.
15
20
 
16
21
  When `simplecov-rspec` is used, RSpec will report an error if the percent of test
17
22
  coverage falls below a defined threshold:
18
23
 
19
24
  ```text
20
- Coverage report generated for RSpec to /Projects/example_project/coverage. 284 / 286 LOC (99.3%) covered.
21
-
22
- FAIL: RSpec Test coverage fell below 100%
25
+ Coverage report generated for RSpec to coverage/index.html
26
+ Line coverage: 284 / 286 (99.30%)
27
+ Line coverage (99.30%) is below the expected minimum coverage (100.00%).
28
+ Lowest-coverage files (line):
29
+ 99.30% lib/example_project.rb
30
+ SimpleCov failed with exit 2 due to a coverage related error
23
31
  ```
24
32
 
25
- If configured to list the lines that were not covered by tests, RSpec will additionally output:
33
+ All of that comes from SimpleCov itself. If configured to list the items that were not
34
+ covered by tests, this gem adds its own listing between SimpleCov's summary and its
35
+ failure message:
26
36
 
27
37
  ```text
38
+ Coverage report generated for RSpec to coverage/index.html
39
+ Line coverage: 284 / 286 (99.30%)
40
+
28
41
  2 lines are not covered by tests:
29
42
  ./lib/example_project.rb:74
30
43
  ./lib/example_project.rb:75
44
+ Line coverage (99.30%) is below the expected minimum coverage (100.00%).
31
45
  ```
32
46
 
33
- * [Installation](#installation)
34
- * [Getting started](#getting-started)
35
- * [Basic setup](#basic-setup)
36
- * [Configuration from environment variables](#configuration-from-environment-variables)
37
- * [Development](#development)
38
- * [Contributing](#contributing)
39
- * [Commit message guidelines](#commit-message-guidelines)
40
- * [Pull request guidelines](#pull-request-guidelines)
41
- * [License](#license)
42
- * [Code of conduct](#code-of-conduct)
47
+ Scoping the listing to particular files changes its shape again. It is marked off with a
48
+ header saying how much of the result it covers, and each criterion reports what the
49
+ scoped files cover directly above what they miss — see [Scoping the listing to specific
50
+ files](#scoping-the-listing-to-specific-files):
51
+
52
+ ```text
53
+ Coverage report generated for RSpec to coverage/index.html
54
+ Line coverage: 284 / 286 (99.30%)
55
+ Branch coverage: 138 / 150 (92.00%)
56
+
57
+ -- Reporting uncovered lines and branches for 1 of 12 files --
58
+
59
+ Scoped line coverage: 73 / 74 (98.64%)
60
+ 1 line is not covered by tests:
61
+ ./lib/example_project/parser.rb:74
62
+
63
+ Scoped branch coverage: 11 / 12 (91.66%)
64
+ 1 branch is not covered by tests:
65
+ ./lib/example_project/parser.rb:82 (then branch)
66
+ ```
67
+
68
+ - [Installation](#installation)
69
+ - [Getting started](#getting-started)
70
+ - [Basic setup](#basic-setup)
71
+ - [Listing uncovered items](#listing-uncovered-items)
72
+ - [Scoping the listing to specific files](#scoping-the-listing-to-specific-files)
73
+ - [Scoping to the code under test](#scoping-to-the-code-under-test)
74
+ - [Configuration block](#configuration-block)
75
+ - [Configuration from environment variables](#configuration-from-environment-variables)
76
+ - [Development](#development)
77
+ - [Contributing](#contributing)
78
+ - [Commit message guidelines](#commit-message-guidelines)
79
+ - [Pull request guidelines](#pull-request-guidelines)
80
+ - [License](#license)
81
+ - [Code of conduct](#code-of-conduct)
43
82
 
44
83
  ## Installation
45
84
 
@@ -48,13 +87,13 @@ To install the gem, add to the following line to your application's gemspec OR G
48
87
  gemspec:
49
88
 
50
89
  ```ruby
51
- spec.add_development_dependency "simplecov-rspec", '~> 0.1'
90
+ spec.add_development_dependency "simplecov-rspec", '~> 1.0'
52
91
  ```
53
92
 
54
93
  Gemfile:
55
94
 
56
95
  ```ruby
57
- gem "simplecov-rspec", "~> 0.1", groups: [:development, :test]
96
+ gem "simplecov-rspec", "~> 1.0", groups: [:development, :test]
58
97
  ```
59
98
 
60
99
  and then run `bundle install`
@@ -102,54 +141,236 @@ This is equivalent to starting with the following options:
102
141
 
103
142
  ```ruby
104
143
  SimpleCov::RSpec.start(
105
- coverage_threshold: 100,
144
+ minimum_coverage: { line: 100 },
106
145
  fail_on_low_coverage: true,
107
- list_uncovered_lines: false
146
+ list_uncovered: false,
147
+ list_uncovered_detail: true,
148
+ list_uncovered_files: nil
108
149
  )
109
150
  ```
110
151
 
111
- The test coverage threshold is the minimum percent of lines covered by tests as
112
- tracked by SimpleCov.
152
+ `minimum_coverage` is the minimum percent of lines (and, optionally, branches and
153
+ methods) covered by tests, enforced by SimpleCov itself.
154
+
155
+ To require less than 100% line coverage:
156
+
157
+ ```ruby
158
+ SimpleCov::RSpec.start(minimum_coverage: 90)
159
+ ```
160
+
161
+ To also require branch (and/or method) coverage, pass a Hash. Any criterion named
162
+ here is automatically enabled via `SimpleCov.enable_coverage`:
163
+
164
+ ```ruby
165
+ SimpleCov::RSpec.start(minimum_coverage: { line: 100, branch: 90 })
166
+ ```
167
+
168
+ ### Listing uncovered items
169
+
170
+ To list the individual lines, branches, and/or methods that are not covered, set
171
+ `list_uncovered`. It accepts `:all`, a single criterion, or an Array of criteria —
172
+ independent of what `minimum_coverage` enforces:
173
+
174
+ ```ruby
175
+ SimpleCov::RSpec.start(minimum_coverage: { line: 100, branch: 90 }, list_uncovered: :all)
176
+ ```
177
+
178
+ ```text
179
+ 2 lines are not covered by tests:
180
+ ./lib/example_project.rb:74
181
+ ./lib/example_project.rb:75
182
+
183
+ 1 branch is not covered by tests:
184
+ ./lib/example_project.rb:82 (else branch)
185
+
186
+ 1 method is not covered by tests:
187
+ ./lib/example_project.rb:96 ExampleProject#unused
188
+ ```
189
+
190
+ A criterion with nothing uncovered is left out entirely, so `:all` prints fewer sections
191
+ than this when there is less to say.
192
+
193
+ For a quieter CI log, set `list_uncovered_detail: false` to print only the count per
194
+ criterion, along with a hint on how to see the details:
195
+
196
+ ```ruby
197
+ SimpleCov::RSpec.start(list_uncovered: :all, list_uncovered_detail: false)
198
+ ```
199
+
200
+ ```text
201
+ 2 lines are not covered by tests.
202
+ 1 branch is not covered by tests.
203
+ 1 method is not covered by tests.
113
204
 
114
- To initialize SimpleCov with a test coverage threshold less than 100%:
205
+ Run with LIST_UNCOVERED_DETAIL=true to see the uncovered lines, branches and methods.
206
+ ```
207
+
208
+ ### Scoping the listing to specific files
209
+
210
+ By default the uncovered listing covers every file SimpleCov tracked. On a focused run
211
+ — one spec file, or one directory — that listing is mostly noise: `spec_helper`
212
+ requires the whole project, so nearly all of it is legitimately unexercised.
213
+
214
+ `list_uncovered_files` (available since version 1.1) narrows the listing to the files
215
+ you care about, given as `Dir.glob` patterns resolved against `SimpleCov.root`. An
216
+ absolute path is used as given:
115
217
 
116
218
  ```ruby
117
- SimpleCov::RSpec.start(coverage_threshold: 90)
219
+ SimpleCov::RSpec.start(list_uncovered: :all, list_uncovered_files: 'lib/example_project/parser.rb')
118
220
  ```
119
221
 
222
+ This option scopes the listing that `list_uncovered` asks for; it does not ask for one.
223
+ `list_uncovered` defaults to `false`, which lists nothing, so setting only
224
+ `list_uncovered_files` produces no output at all. Set both.
225
+
226
+ ```text
227
+ -- Reporting uncovered lines, branches and methods for 1 of 218 files --
228
+
229
+ Scoped line coverage: 73 / 74 (98.64%)
230
+ 1 line is not covered by tests:
231
+ ./lib/example_project/parser.rb:74
232
+
233
+ Scoped branch coverage: 12 / 12 (100.00%)
234
+ Scoped method coverage: 8 / 8 (100.00%)
235
+ ```
236
+
237
+ Each criterion reports what the scoped files cover directly above what they miss, so a
238
+ count like "1 line is not covered" arrives with the denominator that makes it readable,
239
+ and a blank line always means "next criterion". These are deliberately labelled
240
+ differently from SimpleCov's own project-wide summary, and the report is marked off with
241
+ a header, because SimpleCov prints that summary a few lines earlier on the same stream
242
+ while counting different things.
243
+
244
+ This narrows **only the listing**. Coverage is still measured, enforced, and formatted
245
+ for the whole project, so the reported percentage and the HTML report mean the same
246
+ thing whether or not this option is set. There is one definition of "the coverage
247
+ number", and this option does not change it.
248
+
249
+ A scoped report always prints something, and always says how much of the result it
250
+ covered, so it can never be mistaken for a clean run of the whole suite:
251
+
252
+ ```text
253
+ -- Reporting uncovered lines, branches and methods for 1 of 218 files --
254
+
255
+ Scoped line coverage: 74 / 74 (100.00%)
256
+ Scoped branch coverage: 12 / 12 (100.00%)
257
+ Scoped method coverage: 8 / 8 (100.00%)
258
+
259
+ No uncovered lines, branches and methods in this file.
260
+ ```
261
+
262
+ When it comes up empty, it says which of the three reasons applies, since only one of
263
+ them means you mistyped a pattern:
264
+
265
+ ```text
266
+ -- Reporting uncovered lines, branches and methods for 0 of 218 files --
267
+
268
+ No files matched, so no coverage was reported.
269
+ ```
270
+
271
+ ```text
272
+ -- Reporting uncovered lines, branches and methods for 0 of 218 files --
273
+
274
+ 1 file matched, but it is not in the coverage result. It may not have been loaded by
275
+ this run, or may be excluded by a SimpleCov filter.
276
+ ```
277
+
278
+ ```text
279
+ -- Reporting uncovered lines, branches and methods for 0 of 218 files --
280
+
281
+ No files were requested, so no coverage was reported.
282
+ ```
283
+
284
+ ### Scoping to the code under test
285
+
286
+ Naming the files by hand is the awkward part of a focused run: the file you want is
287
+ whatever you happen to be testing right now. `:described` resolves to the source files
288
+ defining the classes the run described, so it follows you from run to run:
289
+
290
+ ```ruby
291
+ SimpleCov::RSpec.start(list_uncovered: :all, list_uncovered_files: :described)
292
+ ```
293
+
294
+ ```bash
295
+ # Report on lib/example_project/parser.rb, because that is what these specs describe
296
+ bundle exec rspec spec/example_project/parser_spec.rb
297
+ ```
298
+
299
+ It walks nested groups too, so a `describe` of one class inside another contributes
300
+ both. A group describing something that is not a class contributes nothing, as does one
301
+ whose class is anonymous, defined in C, or no longer reachable by name — there is no
302
+ source file to report on in those cases.
303
+
304
+ The scope is the classes the run *described*, not the ones it exercised. A class that a
305
+ described class delegates to is not included, and a group written as `describe 'the
306
+ parser' do` names no class at all. Where that matters, build the scope yourself:
307
+ `SimpleCov::RSpec.described_source_files` is public, and you can add to what it returns.
308
+
309
+ ```ruby
310
+ SimpleCov::RSpec.start(
311
+ list_uncovered: :all,
312
+ list_uncovered_files: -> { SimpleCov::RSpec.described_source_files + ['lib/example_project/lexer.rb'] }
313
+ )
314
+ ```
315
+
316
+ The lambda is doing real work there, and leaving it off is a mistake worth naming.
317
+ `SimpleCov::RSpec.start` runs before any example is defined, so calling
318
+ `described_source_files` at that point returns an empty list and you get a report scoped
319
+ to nothing. Any scope derived from the run has to be passed as a callable and resolved
320
+ afterwards — which is exactly what `:described` does for you.
321
+
322
+ ### Configuration block
323
+
120
324
  A configuration block can be given to the `start` method to further configure
121
325
  SimpleCov:
122
326
 
123
327
  ```ruby
124
328
  # Initialize SimpleCov with a specific formatter
125
- SimpleCov::RSpec.start { formatter = SimpleCov::Formatter::LcovFormatter }
329
+ SimpleCov::RSpec.start { formatter SimpleCov::Formatter::LcovFormatter }
126
330
  ```
127
331
 
128
- This block is passed on to `SimpleCov::RSpec.start`. See [Configuring
129
- SimpleCov](https://github.com/simplecov-ruby/simplecov?tab=readme-ov-file#configuring-simplecov)
332
+ This block is passed on to `SimpleCov.start`. See [Configuring
333
+ SimpleCov](https://github.com/simplecov-ruby/simplecov?tab=readme-ov-file#configuration)
130
334
  for details.
131
335
 
132
336
  ### Configuration from environment variables
133
337
 
134
338
  Environment variables can be used to configure `simplecov-rspec`. These environment
135
- variables take presidence over the values passed to `SimpleCov::RSpec.start`.
136
-
137
- * **`COVERAGE_THRESHOLD`**: Sets the minimum coverage threshold (0-100). Overrides
138
- `coverage_threshold`.
339
+ variables take precedence over the values passed to `SimpleCov::RSpec.start`.
340
+
341
+ * **`COVERAGE_THRESHOLD`**: Sets the minimum line coverage threshold (0-100). Overrides
342
+ `minimum_coverage[:line]`.
343
+ * **`COVERAGE_THRESHOLD_BRANCH`**: Sets the minimum branch coverage threshold (0-100), and
344
+ enables branch coverage. Overrides `minimum_coverage[:branch]`.
345
+ * **`COVERAGE_THRESHOLD_METHOD`**: Sets the minimum method coverage threshold (0-100), and
346
+ enables method coverage. Overrides `minimum_coverage[:method]`.
139
347
  * **`FAIL_ON_LOW_COVERAGE`**: Controls whether tests fail if coverage is below the threshold.
140
348
  Set to 'true', 'yes', 'on', or '1' (case insensitive) to enable.
141
- * **`LIST_UNCOVERED_LINES`**: Determines if uncovered lines are listed. Set to 'true',
142
- 'yes', 'on', or '1' (case insensitive) to enable.
349
+ * **`LIST_UNCOVERED`**: Controls which criteria to list uncovered items for. Set to 'all',
350
+ 'true', 'yes', 'on', or '1' to report every criterion; 'false', 'no', 'off', or '0' to
351
+ report none; or a comma-separated list, e.g. `line,branch`.
352
+ * **`LIST_UNCOVERED_DETAIL`**: Controls whether uncovered items are listed individually, or
353
+ just summarized as a count per criterion. Set to 'true', 'yes', 'on', or '1' (case
354
+ insensitive) to show individual items.
355
+ * **`LIST_UNCOVERED_FILES`**: Controls which files uncovered items are listed for. Set to a
356
+ comma-separated list of `Dir.glob` patterns, relative to `SimpleCov.root`; to
357
+ 'described', for the files defining the classes the run described; or to 'all'
358
+ (or 'false', 'no', 'off', '0', or empty) to list them for every file. Since the
359
+ separator is a comma, a brace pattern such as `lib/{a,b}.rb` cannot be used here —
360
+ give the alternatives separately, as `lib/a.rb,lib/b.rb`. Like `list_uncovered_files`,
361
+ this scopes the listing rather than asking for one: it has no effect unless
362
+ `LIST_UNCOVERED` (or `list_uncovered:`) names at least one criterion.
143
363
 
144
364
  For example, here is a bash script to run tests in an infinite loop while writing
145
365
  test output to `fail.txt`:
146
366
 
147
367
  ```bash
148
- while true; do COV_NO_FAIL=TRUE rspec >> fail.txt; done
368
+ while true; do FAIL_ON_LOW_COVERAGE=false rspec >> fail.txt; done
149
369
  ```
150
370
 
151
- In a CI system, you might want to set `LIST_UNCOVERED_LINES=yes` in order to list
152
- uncovered lines on different platforms than the one you run for local development.
371
+ In a CI system, you might want to set `LIST_UNCOVERED=all` in order to list uncovered
372
+ lines, branches, and methods on a platform other than the one you use for local
373
+ development.
153
374
 
154
375
  ## Development
155
376
 
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleCov
4
+ class RSpec
5
+ # Finds the source files that define the classes an RSpec run described.
6
+ #
7
+ # This is the scope most people want from `list_uncovered_files:` on a focused run:
8
+ # "report on the code I am actually testing". Deriving it means reaching into
9
+ # `RSpec.world`, which is not part of RSpec's public API, so it lives here rather
10
+ # than in each project's `spec_helper.rb`, where it could not be fixed centrally.
11
+ #
12
+ # @api private
13
+ #
14
+ module DescribedSourceFiles
15
+ module_function
16
+
17
+ # The source files defining the classes the run described
18
+ #
19
+ # A group that describes something other than a Module contributes nothing, as
20
+ # does one whose described class is anonymous, defined in C, or no longer
21
+ # reachable by name. Such a group has no source file to report on, and saying so
22
+ # is not useful, so it is skipped silently.
23
+ #
24
+ # @param example_groups [Array<Class>] the run's top-level example groups
25
+ #
26
+ # @return [Array<String>] absolute paths, without duplicates
27
+ #
28
+ # @example
29
+ # DescribedSourceFiles.call # => ['/project/lib/parser.rb']
30
+ #
31
+ # @api private
32
+ #
33
+ def call(example_groups: ::RSpec.world.example_groups)
34
+ with_nested(example_groups).filter_map { |group| source_file(group) }.uniq
35
+ end
36
+
37
+ # The given example groups, plus every group nested within them
38
+ #
39
+ # A nested group can describe a different class than the group containing it, so
40
+ # the whole tree is walked rather than just the top level.
41
+ #
42
+ # @param groups [Array<Class>] the example groups to walk
43
+ #
44
+ # @return [Array<Class>]
45
+ #
46
+ # @example
47
+ # DescribedSourceFiles.with_nested(::RSpec.world.example_groups)
48
+ #
49
+ # @api private
50
+ #
51
+ def with_nested(groups)
52
+ groups.flat_map { |group| [group, *with_nested(group.children)] }
53
+ end
54
+
55
+ # The source file defining one group's described class, if it has one
56
+ #
57
+ # @param group [Class] the example group
58
+ #
59
+ # @return [String, nil] an absolute path, or nil if there is nothing to report on
60
+ #
61
+ # @example
62
+ # DescribedSourceFiles.source_file(group) # => '/project/lib/parser.rb'
63
+ #
64
+ # @api private
65
+ #
66
+ def source_file(group)
67
+ described_class = group.described_class
68
+ return nil unless described_class.is_a?(Module)
69
+
70
+ # A class defined in C answers [], one whose constant is gone answers nil
71
+ name = described_class.name
72
+ name && Object.const_source_location(name)&.first
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,130 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleCov
4
+ class RSpec
5
+ # Resolves the `list_uncovered_files:` option (plus its `LIST_UNCOVERED_FILES` ENV
6
+ # override) into a normalized Array of absolute paths, or nil for "every file".
7
+ #
8
+ # The option may be given as a callable so that it can be evaluated after the test
9
+ # run rather than when `SimpleCov::RSpec.start` is called. `start` runs before any
10
+ # example is defined, so a caller that wants to scope the report to the code under
11
+ # test cannot know which files those are yet.
12
+ #
13
+ # @api private
14
+ #
15
+ module ListUncoveredFilesOption
16
+ # LIST_UNCOVERED_FILES environment variable values that mean "every file"
17
+ ALL_ENV_VALUES = %w[all false no off 0].freeze
18
+
19
+ # The value that means "the files defining the classes this run described"
20
+ #
21
+ # Sugar over passing a callable that does the same thing: the scope is only
22
+ # knowable after the run, so it resolves to one.
23
+ #
24
+ # @see SimpleCov::RSpec.described_source_files
25
+ DESCRIBED = :described
26
+
27
+ module_function
28
+
29
+ # Resolve the effective file list, applying the ENV override if present
30
+ #
31
+ # @param value [nil, Symbol, String, Array<String>, #call] the `list_uncovered_files:` argument
32
+ # @param env [Hash] the environment variables
33
+ # @param env_var [String] the ENV var name that overrides `value`
34
+ # @param root [String] the directory that relative patterns are resolved against
35
+ #
36
+ # @return [Array<String>, nil] absolute paths, or nil to report on every file
37
+ #
38
+ # @raise [ArgumentError] if value is not one of the accepted forms
39
+ #
40
+ # @example
41
+ # ListUncoveredFilesOption.resolve('lib/a.rb', env: {}, env_var: 'X', root: '/p') # => ['/p/lib/a.rb']
42
+ #
43
+ def resolve(value, env:, env_var:, root:)
44
+ value = from_env(env.fetch(env_var)) if env.key?(env_var)
45
+ value = -> { ::SimpleCov::RSpec.described_source_files } if value == DESCRIBED
46
+ value = value.call if value.respond_to?(:call)
47
+ return nil if value.nil?
48
+
49
+ expand(normalize(value), root)
50
+ end
51
+
52
+ # Parse a raw LIST_UNCOVERED_FILES environment variable value
53
+ #
54
+ # A value that lists no patterns at all — `''`, `' '`, or separators alone such as
55
+ # `', ,'` — means "every file". Dropping the empty entries can empty the list even
56
+ # though the value was not empty to start with, so the check is made after parsing
57
+ # as well as before it.
58
+ #
59
+ # @param raw [String] the raw LIST_UNCOVERED_FILES environment variable value
60
+ # @return [nil, Symbol, Array<String>] nil for "every file", {DESCRIBED}, or the listed patterns
61
+ # @example
62
+ # ListUncoveredFilesOption.from_env('lib/a.rb,lib/b.rb') # => ['lib/a.rb', 'lib/b.rb']
63
+ #
64
+ def from_env(raw)
65
+ stripped = raw.strip
66
+ return nil if stripped.empty? || ALL_ENV_VALUES.include?(stripped.downcase)
67
+ return DESCRIBED if stripped.casecmp?(DESCRIBED.to_s)
68
+
69
+ patterns = stripped.split(',').map(&:strip).reject(&:empty?)
70
+ patterns.empty? ? nil : patterns
71
+ end
72
+
73
+ # Normalize a `list_uncovered_files:`-style value into an Array of Strings
74
+ #
75
+ # @param value [String, Array<String>] the pattern or patterns
76
+ # @return [Array<String>]
77
+ # @raise [ArgumentError] if value is not a String or an Array of Strings
78
+ # @example
79
+ # ListUncoveredFilesOption.normalize('lib/a.rb') # => ['lib/a.rb']
80
+ #
81
+ def normalize(value)
82
+ case value
83
+ when String then [value]
84
+ when Array then validate(value)
85
+ else
86
+ raise ArgumentError,
87
+ 'list_uncovered_files must be nil, :described, a String, an Array of Strings, or a callable ' \
88
+ "returning one of those; got #{value.inspect}"
89
+ end
90
+ end
91
+
92
+ # Raise unless every element of patterns is a String
93
+ #
94
+ # @param patterns [Array<String>]
95
+ # @return [Array<String>]
96
+ # @raise [ArgumentError] if patterns contains a non-String
97
+ # @example
98
+ # ListUncoveredFilesOption.validate(['lib/a.rb']) # => ['lib/a.rb']
99
+ #
100
+ def validate(patterns)
101
+ invalid = patterns.grep_v(String)
102
+ return patterns if invalid.empty?
103
+
104
+ raise ArgumentError, "list_uncovered_files entries must be Strings; got #{invalid.inspect}"
105
+ end
106
+
107
+ # Expand patterns to absolute paths, resolving relative patterns against root
108
+ #
109
+ # Each pattern is expanded with `Dir.glob`. A pattern that matches nothing on disk
110
+ # expands to itself, so the literal pattern is kept rather than dropped. No file in
111
+ # the coverage result can carry that path, so it adds nothing to the listing; it is
112
+ # kept because the report tests these paths against the file system to tell a
113
+ # pattern that matched nothing from one whose files SimpleCov never tracked.
114
+ #
115
+ # @param patterns [Array<String>] the patterns to expand
116
+ # @param root [String] the directory that relative patterns are resolved against
117
+ # @return [Array<String>] absolute paths, without duplicates
118
+ # @example
119
+ # ListUncoveredFilesOption.expand(['lib/*.rb'], '/p') # => ['/p/lib/a.rb', '/p/lib/b.rb']
120
+ #
121
+ def expand(patterns, root)
122
+ patterns.flat_map do |pattern|
123
+ absolute = File.absolute_path(pattern, root)
124
+ matches = Dir.glob(absolute)
125
+ matches.empty? ? [absolute] : matches
126
+ end.uniq
127
+ end
128
+ end
129
+ end
130
+ end