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.
@@ -1,55 +1,65 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'simplecov'
4
+ require_relative 'simplecov-rspec/described_source_files'
5
+ require_relative 'simplecov-rspec/list_uncovered_files_option'
6
+ require_relative 'simplecov-rspec/list_uncovered_option'
7
+ require_relative 'simplecov-rspec/uncovered_report'
4
8
 
5
9
  # SimpleCov namespace
6
10
  module SimpleCov
7
- # Configure SimpleCov to fail RSpec if the test coverage falls below a given threshold
11
+ # Configure SimpleCov to enforce coverage thresholds for RSpec, on top of what
12
+ # SimpleCov itself already provides.
8
13
  #
9
- # Configures RSpec to:
14
+ # SimpleCov (>= 1.0) can already enforce `minimum_coverage` for line, branch, and
15
+ # method coverage, and will exit with a non-zero status when a threshold is missed.
16
+ # This gem layers four things SimpleCov doesn't do on its own:
10
17
  #
11
- # 1. Fail (and exit with with a non-zero exitcode) if the test
12
- # coverage is below the configured threshold and
13
- # 2. (optionally) list the lines of code not covered by tests.
18
+ # 1. Suppresses coverage failures when RSpec is run in dry-run mode (e.g. from an IDE).
19
+ # 2. Lists (or summarizes) the individual uncovered lines, branches, and methods.
20
+ # 3. Scopes that listing to the files you name, or to the code the run described.
21
+ # 4. Lets all of the above be overridden from the environment, for CI.
14
22
  #
15
- # Simply add the line `SimpleCov::RSpec.start` in place of `SimpleCov::Start` in
23
+ # Simply add the line `SimpleCov::RSpec.start` in place of `SimpleCov.start` in
16
24
  # the project's `spec_helper.rb`. This line must appear before the project is
17
25
  # required.
18
26
  #
19
- # @example Initialize SimpleCov with defaults
27
+ # @example Initialize SimpleCov with defaults (100% line coverage required)
20
28
  # SimpleCov::RSpec.start
21
29
  #
22
- # @example Initialize SimpleCov with a test coverage threshold other than 100%
23
- # SimpleCov::RSpec.start(coverage_threshold: 90, fail_on_low_coverage: true, list_uncovered_lines: false)
30
+ # @example Require 100% line coverage and 90% branch coverage
31
+ # SimpleCov::RSpec.start(minimum_coverage: { line: 100, branch: 90 })
24
32
  #
25
- # @example Pass a configuration block to SimpleCov::RSpec.start
26
- # SimpleCov::RSpec.start { formatter = SimpleCov::Formatter::LcovFormatter }
33
+ # @example List every uncovered line, branch, and method when coverage is incomplete
34
+ # SimpleCov::RSpec.start(minimum_coverage: { line: 100, branch: 90 }, list_uncovered: :all)
27
35
  #
28
- # @example Bash script to run tests in an infinite loop writing failures to `fail.txt`:
29
- # while true; do FAIL_ON_LOW_COVERAGE=TRUE rspec >> fail.txt; done
36
+ # @example Report only counts, with a hint on how to see the details
37
+ # SimpleCov::RSpec.start(list_uncovered: :all, list_uncovered_detail: false)
30
38
  #
31
- # @!attribute [r] env
32
- # Command line environment variables (default: ENV)
33
- # @return [Hash]
34
- # @api private
35
- # @private
39
+ # @example Scope the listing to the code the run described
40
+ # SimpleCov::RSpec.start(list_uncovered: :all, list_uncovered_files: :described)
36
41
  #
37
- # @!attribute [r] simplecov_module
38
- # The SimpleCov module (default: ::SimpleCov)
39
- # @return [Module]
40
- # @api private
41
- # @private
42
- #
43
- # @!attribute [r] start_config_block
44
- # A configuration block to pass to `SimpleCov.start`
45
- # @return [Proc]
46
- # @api private
47
- # @private
42
+ # @example Pass a configuration block to SimpleCov.start
43
+ # SimpleCov::RSpec.start { formatter SimpleCov::Formatter::LcovFormatter }
48
44
  #
49
45
  # @api public
50
46
  #
51
47
  class RSpec
52
- # rubocop:disable Layout/LineLength
48
+ # @!attribute [r] env
49
+ # The environment variables consulted for overrides
50
+ # @return [Hash]
51
+ # @api private
52
+ #
53
+ # @!attribute [r] simplecov_module
54
+ # The SimpleCov module being configured
55
+ # @return [Module]
56
+ # @api private
57
+ #
58
+ # @!attribute [r] start_config_block
59
+ # A configuration block passed through to `SimpleCov.start`
60
+ # @return [Proc, nil]
61
+ # @api private
62
+ #
53
63
 
54
64
  # Configure and start SimpleCov for RSpec
55
65
  #
@@ -60,128 +70,216 @@ module SimpleCov
60
70
  #
61
71
  # @api public
62
72
  #
63
- # @overload start(coverage_threshold: 100, fail_on_low_coverage: true, list_uncovered_lines: false, rspec_dry_run: ::RSpec.configuration.dry_run?, env: ENV, &start_config_block)
73
+ # @overload start(minimum_coverage: { line: 100 }, fail_on_low_coverage: true, list_uncovered: false, list_uncovered_detail: true, rspec_dry_run: ::RSpec.configuration.dry_run?, env: ENV, &start_config_block) # rubocop:disable Layout/LineLength
64
74
  #
65
- # @param coverage_threshold [Integer] the test coverage threshold (default: 100)
75
+ # @param minimum_coverage [Integer, Hash] the minimum coverage threshold (default: `{ line: 100 }`)
66
76
  #
67
- # Coverage below this threshold will cause the rspec to fail if
68
- # fail_on_low_coverage is true.
77
+ # An Integer sets the line coverage threshold. A Hash sets a threshold per
78
+ # criterion, e.g. `{ line: 100, branch: 90, method: 100 }`. Passed straight
79
+ # through to `SimpleCov.minimum_coverage`; any criterion given here is
80
+ # automatically enabled via `SimpleCov.enable_coverage`.
69
81
  #
70
- # @param fail_on_low_coverage [Boolean] whether to fail if the coverage is below the threshold (default: true)
82
+ # @param fail_on_low_coverage [Boolean] whether to fail if coverage is below the threshold (default: true)
71
83
  #
72
- # This setting will be read from the environment variable FAIL_ON_LOW_COVERAGE if
73
- # it is NOT given in the `.start` method. Setting that environment variable
74
- # to 'true', 'yes', 'on', or '1' will cause this setting to be `false`
75
- # (the logic is inverted). Any other value will cause this seetting to
76
- # be `true`.
84
+ # When `false` (or when RSpec is in dry-run mode), `SimpleCov.minimum_coverage`
85
+ # is never set, so SimpleCov will not fail the build regardless of coverage.
77
86
  #
78
- # @param list_uncovered_lines [Boolean] whether to list the lines not covered by tests (default: false)
87
+ # Read from the `FAIL_ON_LOW_COVERAGE` environment variable if set: `true`,
88
+ # `yes`, `on`, or `1` (case-insensitive) enables it; anything else disables it.
79
89
  #
80
- # All lines not covered by tests will be listed if the coverage is below the threshold.
81
- # Probably only makes sense to use if the threshold is 100%.
90
+ # @param list_uncovered [false, :all, Symbol, Array<Symbol>] which coverage criteria to
91
+ # list uncovered items for (default: false)
82
92
  #
83
- # This setting will be read from the environment variable LIST_UNCOVERED_LINES
84
- # if it is NOT given in the `.start` method. Setting that environment variable
85
- # to 'true', 'yes', 'on', or '1' will cause this setting to be `true`. Any
86
- # other value will cause this setting to be `false`.
93
+ # `false` reports nothing. `:all` reports line, branch, and method. A Symbol or
94
+ # Array of Symbols (`:line`, `:branch`, `:method`) reports just those criteria,
95
+ # independent of what `minimum_coverage` enforces.
87
96
  #
88
- # @param start_config_block [Proc] a configuration block to pass to `SimpleCov.start` (default: nil)
97
+ # Read from the `LIST_UNCOVERED` environment variable if set: `all`, `true`,
98
+ # `yes`, `on`, or `1` means every criterion; `false`, `no`, `off`, or `0` means
99
+ # none; otherwise a comma-separated list of criteria, e.g. `line,branch`.
89
100
  #
90
- # @param rspec_dry_run [Boolean] whether the rspec run is a dry run
101
+ # @param list_uncovered_detail [Boolean] list individual items, or just a count (default: true)
91
102
  #
92
- # Typically not set by the user. Used for this gem's unit testing.
103
+ # When `false`, only the count of uncovered items per criterion is printed,
104
+ # followed by a hint on how to see the details.
105
+ #
106
+ # Read from the `LIST_UNCOVERED_DETAIL` environment variable if set: `true`,
107
+ # `yes`, `on`, or `1` (case-insensitive) shows details; anything else summarizes.
108
+ #
109
+ # @param list_uncovered_files [nil, Symbol, String, Array<String>, #call] which files to
110
+ # list uncovered items for (default: nil)
93
111
  #
94
- # If RSpec is being run in dry run mode, test coverage under the threshold will not fail the build.
112
+ # `nil` reports on every file in the result. A String or Array of Strings names
113
+ # the files to report on, as `Dir.glob` patterns resolved against `SimpleCov.root`.
114
+ # `:described` reports on the files defining the classes the run described, which
115
+ # is the scope a focused run usually wants. A callable returning a String or an
116
+ # Array of Strings is resolved after the run rather than at `start`, which is what
117
+ # any scope derived from the run needs: `start` runs before any example is defined.
95
118
  #
96
- # The purpose of this is to allow the test coverage to be run in a dry run by an IDE
97
- # so it can report failed tests and coverage without reporting that the entire RSpec
98
- # run has failed.
119
+ # This narrows only the uncovered listing. Coverage is still measured, enforced,
120
+ # and formatted for the whole project, so the percentage and the HTML report mean
121
+ # the same thing whether or not this is set.
122
+ #
123
+ # A scoped report names how many of the result's files it covered, and says so
124
+ # explicitly when they are fully covered or when nothing matched.
125
+ #
126
+ # Read from the `LIST_UNCOVERED_FILES` environment variable if set: a
127
+ # comma-separated list of patterns, `described`, or `all` (or `false`, `no`,
128
+ # `off`, `0`, or empty) for every file.
129
+ #
130
+ # @param start_config_block [Proc] a configuration block to pass to `SimpleCov.start` (default: nil)
131
+ #
132
+ # @param rspec_dry_run [Boolean] whether the rspec run is a dry run
133
+ #
134
+ # Typically not set by the user. If RSpec is being run in dry run mode, test
135
+ # coverage under the threshold will not fail the build. This allows test
136
+ # coverage to be run in a dry run by an IDE so it can report failed tests and
137
+ # coverage without reporting that the entire RSpec run has failed.
99
138
  #
100
139
  # @param simplecov_module [Module] the SimpleCov module (default: ::SimpleCov)
101
140
  #
102
141
  # Typically not set by the user. Used for this gem's unit testing.
103
142
  #
104
- # Allows the SimpleCov module to be mocked.
105
- #
106
143
  # @param env [Hash] the environment variables (default: ENV)
107
144
  #
108
145
  # Typically not set by the user. Used for this gem's unit testing.
109
146
  #
110
147
  # @example Initialize SimpleCov with a test coverage threshold other than 100%
111
- # SimpleCov::RSpec.start(coverage_threshold: 90)
148
+ # SimpleCov::RSpec.start(minimum_coverage: 90)
112
149
  #
113
150
  # @example Initialize SimpleCov to not fail the test run if the coverage is below the threshold
114
151
  # SimpleCov::RSpec.start(fail_on_low_coverage: false)
115
152
  #
116
153
  # # OR use an environment variable to override the default
117
- # ENV['FAIL_ON_LOW_COVERAGE'] = 'true'
118
- # SimpleCov::RSpec.start
154
+ # FAIL_ON_LOW_COVERAGE=true rspec
155
+ #
156
+ # @example Initialize SimpleCov to list the lines, branches, and methods not covered by tests
157
+ # SimpleCov::RSpec.start(list_uncovered: :all)
119
158
  #
120
- # # OR use an environment variable to override the default from the rspec command line
121
- # FAIL_ON_LOW_COVERAGE=TRUE rspec
159
+ # # OR use an environment variable to override the default
160
+ # LIST_UNCOVERED=all rspec
122
161
  #
123
- # @example Initialize SimpleCov to list the lines not covered by tests
124
- # SimpleCov::RSpec.start(list_uncovered_lines: true)
162
+ # @example List uncovered items for one file only
163
+ # SimpleCov::RSpec.start(list_uncovered: :all, list_uncovered_files: 'lib/example_project/parser.rb')
125
164
  #
126
165
  # # OR use an environment variable to override the default
127
- # ENV['LIST_UNCOVERED_LINES'] = 'true'
128
- # SimpleCov::RSpec.start
166
+ # LIST_UNCOVERED_FILES=lib/example_project/parser.rb rspec
129
167
  #
130
- # # OR use an environment variable to override the default from the rspec command line
131
- # LIST_UNCOVERED_LINES=TRUE rspec
168
+ # @example Scope the listing to the classes the run actually described
169
+ # SimpleCov::RSpec.start(list_uncovered: :all, list_uncovered_files: :described)
170
+ #
171
+ # # OR use an environment variable to override the default
172
+ # LIST_UNCOVERED_FILES=described rspec
132
173
  #
133
174
  def self.start(...) = new(...).send(:start)
134
175
 
135
- # rubocop:enable Layout/LineLength
176
+ # The source files defining the classes this RSpec run described
177
+ #
178
+ # What `list_uncovered_files: :described` scopes to. Call it directly to build a
179
+ # scope of your own — to add files the run touches but does not describe, say:
180
+ #
181
+ # list_uncovered_files: -> { SimpleCov::RSpec.described_source_files + ['lib/support.rb'] }
182
+ #
183
+ # Only meaningful once the run has defined its examples, which is why it is passed
184
+ # as a callable rather than called at `start`.
185
+ #
186
+ # A described class contributes nothing when it is anonymous, defined in C, or no
187
+ # longer reachable by name, since there is no source file to report on.
188
+ #
189
+ # @example Scope the listing to the code under test, plus one more file
190
+ # SimpleCov::RSpec.start(
191
+ # list_uncovered: :all,
192
+ # list_uncovered_files: -> { SimpleCov::RSpec.described_source_files + ['lib/support.rb'] }
193
+ # )
194
+ #
195
+ # @return [Array<String>] absolute paths, without duplicates
196
+ #
197
+ # @api public
198
+ #
199
+ def self.described_source_files = DescribedSourceFiles.call
136
200
 
137
- # Environment variable to override coverage_threshold
201
+ # Environment variable to override minimum_coverage[:line]
138
202
  # @api private
139
203
  # @private
140
204
  COVERAGE_THRESHOLD = 'COVERAGE_THRESHOLD'
141
205
 
206
+ # Environment variable to override minimum_coverage[:branch]
207
+ # @api private
208
+ # @private
209
+ COVERAGE_THRESHOLD_BRANCH = 'COVERAGE_THRESHOLD_BRANCH'
210
+
211
+ # Environment variable to override minimum_coverage[:method]
212
+ # @api private
213
+ # @private
214
+ COVERAGE_THRESHOLD_METHOD = 'COVERAGE_THRESHOLD_METHOD'
215
+
142
216
  # Environment variable to override fail_on_low_coverage
143
217
  # @api private
144
218
  # @private
145
219
  FAIL_ON_LOW_COVERAGE = 'FAIL_ON_LOW_COVERAGE'
146
220
 
147
- # Environment variable to override list_uncovered_lines
221
+ # Environment variable to override list_uncovered
222
+ # @api private
223
+ # @private
224
+ LIST_UNCOVERED = 'LIST_UNCOVERED'
225
+
226
+ # Environment variable to override list_uncovered_detail
227
+ # @api private
228
+ # @private
229
+ LIST_UNCOVERED_DETAIL = 'LIST_UNCOVERED_DETAIL'
230
+
231
+ # Environment variable to override list_uncovered_files
148
232
  # @api private
149
233
  # @private
150
- LIST_UNCOVERED_LINES = 'LIST_UNCOVERED_LINES'
234
+ LIST_UNCOVERED_FILES = 'LIST_UNCOVERED_FILES'
151
235
 
152
- # Default value for coverage_threshold
236
+ # Maps a coverage criterion to the environment variable that overrides its threshold
153
237
  # @api private
154
238
  # @private
155
- DEFAULT_TEST_COVERAGE_THRESHOLD = 100
239
+ CRITERION_ENV_VARS = {
240
+ line: COVERAGE_THRESHOLD,
241
+ branch: COVERAGE_THRESHOLD_BRANCH,
242
+ method: COVERAGE_THRESHOLD_METHOD
243
+ }.freeze
244
+
245
+ # Default value for minimum_coverage
246
+ # @api private
247
+ # @private
248
+ DEFAULT_MINIMUM_COVERAGE = { line: 100 }.freeze
156
249
 
157
250
  # Default value for fail_on_low_coverage
158
251
  # @api private
159
252
  # @private
160
- DEFAULT_FAIL_ON_LOW_COVERAGEERAGE = true
253
+ DEFAULT_FAIL_ON_LOW_COVERAGE = true
254
+
255
+ # Default value for list_uncovered_detail
256
+ # @api private
257
+ # @private
258
+ DEFAULT_LIST_UNCOVERED_DETAIL = true
161
259
 
162
- # Default value for list_uncovered_lines
260
+ # Environment variable values that mean "true"
163
261
  # @api private
164
262
  # @private
165
- DEFAULT_LIST_UNCOVERED_LINES = false
263
+ TRUTHY_ENV_VALUES = %w[yes on true 1].freeze
166
264
 
167
265
  attr_reader :env, :simplecov_module, :start_config_block
168
266
 
169
- # The coverage threshold
267
+ # The minimum coverage threshold, per criterion
170
268
  #
171
- # Searches the ENV, the value given in the `.start` method, and the default value
172
- # and returns the first value found.
269
+ # Searches the ENV, the value given in the `.start` method, and the default value,
270
+ # merging them (ENV takes precedence per-criterion).
173
271
  #
174
- # @return [Integer]
272
+ # @return [Hash{Symbol => Integer}]
175
273
  #
176
274
  # @api private
177
275
  # @private
178
276
  #
179
- def coverage_threshold
180
- return env.fetch(COVERAGE_THRESHOLD).to_i if env.key?(COVERAGE_THRESHOLD)
181
-
182
- return @coverage_threshold.to_i unless @coverage_threshold.nil?
277
+ def minimum_coverage
278
+ base = @minimum_coverage
279
+ base = { line: base } if base.is_a?(Integer)
280
+ validate_minimum_coverage!(base)
183
281
 
184
- DEFAULT_TEST_COVERAGE_THRESHOLD
282
+ (base || DEFAULT_MINIMUM_COVERAGE).merge(env_minimum_coverage_overrides)
185
283
  end
186
284
 
187
285
  # Whether to fail if the coverage is below the threshold
@@ -196,55 +294,65 @@ module SimpleCov
196
294
  #
197
295
  def fail_on_low_coverage?
198
296
  return false if rspec_dry_run?
199
-
200
297
  return env_true?(FAIL_ON_LOW_COVERAGE) if env.key?(FAIL_ON_LOW_COVERAGE)
201
-
202
298
  return @fail_on_low_coverage unless @fail_on_low_coverage.nil?
203
299
 
204
- DEFAULT_FAIL_ON_LOW_COVERAGEERAGE
300
+ DEFAULT_FAIL_ON_LOW_COVERAGE
205
301
  end
206
302
 
207
- # Whether to list the lines not covered by tests
303
+ # The coverage criteria to list uncovered items for
208
304
  #
209
- # Searches the ENV, the value given in the `.start` method, and the default value
210
- # and returns the first value found.
305
+ # Searches the ENV and the value given in the `.start` method, and normalizes
306
+ # the result to an Array of Symbols in a fixed (line, branch, method) order.
211
307
  #
212
- # @return [Boolean]
308
+ # @return [Array<Symbol>]
213
309
  #
214
310
  # @api private
215
311
  # @private
216
312
  #
217
- def list_uncovered_lines?
218
- return env_true?(LIST_UNCOVERED_LINES) if env.key?(LIST_UNCOVERED_LINES)
219
-
220
- return @list_uncovered_lines unless @list_uncovered_lines.nil?
313
+ def list_uncovered_criteria
314
+ @list_uncovered_criteria ||= ListUncoveredOption.resolve(@list_uncovered, env: env, env_var: LIST_UNCOVERED)
315
+ end
221
316
 
222
- DEFAULT_LIST_UNCOVERED_LINES
317
+ # The files to list uncovered items for, or nil for every file in the result
318
+ #
319
+ # Resolved from the `at_exit` hook rather than at `start`: `start` runs before any
320
+ # example is defined, so a caller scoping the report to the code under test cannot
321
+ # know which files those are until the run is over.
322
+ #
323
+ # @return [Array<String>, nil]
324
+ #
325
+ # @api private
326
+ # @private
327
+ #
328
+ def list_uncovered_files
329
+ ListUncoveredFilesOption.resolve(
330
+ @list_uncovered_files, env: env, env_var: LIST_UNCOVERED_FILES, root: simplecov_module.root
331
+ )
223
332
  end
224
333
 
225
- # Whether the rspec run is a dry run
334
+ # Whether to list individual uncovered items, or just a count per criterion
226
335
  #
227
336
  # @return [Boolean]
228
337
  #
229
338
  # @api private
230
339
  # @private
231
340
  #
232
- def rspec_dry_run? = @rspec_dry_run
341
+ def list_uncovered_detail?
342
+ return env_true?(LIST_UNCOVERED_DETAIL) if env.key?(LIST_UNCOVERED_DETAIL)
343
+ return @list_uncovered_detail unless @list_uncovered_detail.nil?
233
344
 
234
- # An uncovered line
235
- #
236
- # @!attribute project_filename [rw]
237
- # The path to the file with uncovered lines relative to the project root
238
- # @return [String]
239
- # @api private
345
+ DEFAULT_LIST_UNCOVERED_DETAIL
346
+ end
347
+
348
+ # Whether the rspec run is a dry run
240
349
  #
241
- # @!attribute line_number [rw]
242
- # The line number of the uncovered line
243
- # @return [Integer]
244
- # @api private
350
+ # @return [Boolean]
245
351
  #
246
352
  # @api private
247
- UncoveredLine = Struct.new(:project_filename, :line_number)
353
+ # @private
354
+ #
355
+ def rspec_dry_run? = @rspec_dry_run
248
356
 
249
357
  private
250
358
 
@@ -255,17 +363,21 @@ module SimpleCov
255
363
  # @api private
256
364
  # @private
257
365
  def initialize(
258
- coverage_threshold: nil,
366
+ minimum_coverage: nil,
259
367
  fail_on_low_coverage: nil,
260
- list_uncovered_lines: nil,
368
+ list_uncovered: nil,
369
+ list_uncovered_detail: nil,
370
+ list_uncovered_files: nil,
261
371
  rspec_dry_run: ::RSpec.configuration.dry_run?,
262
372
  env: ENV,
263
373
  simplecov_module: ::SimpleCov,
264
374
  &start_config_block
265
375
  )
266
- @coverage_threshold = coverage_threshold
376
+ @minimum_coverage = minimum_coverage
267
377
  @fail_on_low_coverage = fail_on_low_coverage
268
- @list_uncovered_lines = list_uncovered_lines
378
+ @list_uncovered = list_uncovered
379
+ @list_uncovered_detail = list_uncovered_detail
380
+ @list_uncovered_files = list_uncovered_files
269
381
  @start_config_block = start_config_block
270
382
  @rspec_dry_run = rspec_dry_run
271
383
  @env = env
@@ -274,15 +386,29 @@ module SimpleCov
274
386
 
275
387
  # rubocop:enable Metrics/ParameterLists
276
388
 
277
- # Set the at_exit hook and then configure and start SimpleCov
389
+ # Configure SimpleCov and start it
278
390
  # @return [Void]
279
391
  # @api private
280
392
  # @private
281
393
  def start
282
394
  simplecov_module.at_exit(&at_exit_hook)
395
+ simplecov_module.enable_coverage(*criteria_to_enable) if criteria_to_enable.any?
396
+ simplecov_module.minimum_coverage(minimum_coverage) if enforce_minimum_coverage?
283
397
  simplecov_module.start(&start_config_block)
284
398
  end
285
399
 
400
+ # Whether SimpleCov should be configured to enforce minimum_coverage itself
401
+ # @return [Boolean]
402
+ # @api private
403
+ # @private
404
+ def enforce_minimum_coverage? = !rspec_dry_run? && fail_on_low_coverage?
405
+
406
+ # The coverage criteria that need to be enabled via SimpleCov.enable_coverage
407
+ # @return [Array<Symbol>]
408
+ # @api private
409
+ # @private
410
+ def criteria_to_enable = (minimum_coverage.keys + list_uncovered_criteria).uniq
411
+
286
412
  # Called by SimpleCov.at_exit
287
413
  # @return [Proc]
288
414
  # @api private
@@ -290,102 +416,85 @@ module SimpleCov
290
416
  def at_exit_hook
291
417
  lambda do
292
418
  simplecov_module.result.format!
293
- output_at_exit_report
294
- exit 1 if coverage_below_threshold? && fail_on_low_coverage?
419
+ output_uncovered_report
295
420
  end
296
421
  end
297
422
 
298
- # Output the at_exit report
423
+ # Output the uncovered items report (detailed or summary), if requested
299
424
  # @return [Void]
300
425
  # @api private
301
426
  # @private
302
- def output_at_exit_report
303
- low_coverage_report if show_low_coverage_report?
304
- uncovered_lines_report if show_uncovered_lines_report?
305
- $stderr.puts if show_low_coverage_report? || show_uncovered_lines_report?
306
- end
427
+ def output_uncovered_report
428
+ return if list_uncovered_criteria.empty?
307
429
 
308
- # Whether to show the low coverage
309
- # @return [Boolean]
310
- # @api private
311
- # @private
312
- def show_low_coverage_report? = coverage_below_threshold?
430
+ report = UncoveredReport.new(
431
+ result: simplecov_module.result, criteria: list_uncovered_criteria, detail: list_uncovered_detail?,
432
+ detail_env_var: LIST_UNCOVERED_DETAIL, files: list_uncovered_files
433
+ ).to_s
434
+ return if report.empty?
313
435
 
314
- # Whether the test coverage is below the threshold
315
- # @return [Boolean]
316
- # @api private
317
- # @private
318
- def coverage_below_threshold? = simplecov_module.result.covered_percent < coverage_threshold
436
+ $stderr.puts
437
+ $stderr.puts report
438
+ end
319
439
 
320
- # Output the low coverage report
440
+ # Raise unless `base` is a valid `minimum_coverage` value
441
+ #
442
+ # Valid values include `nil`, or a Hash keyed by `:line`, `:branch`, and/or
443
+ # `:method`, with Numeric values
444
+ #
445
+ # @param base [nil, Hash] the normalized `minimum_coverage` value
321
446
  # @return [Void]
447
+ # @raise [ArgumentError] if `base` is not nil or a Hash, has an unknown key, or has a
448
+ # non-Numeric value
322
449
  # @api private
323
450
  # @private
324
- def low_coverage_report
325
- $stderr.puts
326
- $stderr.print 'FAIL: ' if fail_on_low_coverage?
327
- $stderr.puts "Test coverage is below the low coverage threshold of #{coverage_threshold}%"
328
- end
451
+ def validate_minimum_coverage!(base)
452
+ unless base.nil? || base.is_a?(Hash)
453
+ raise ArgumentError, "minimum_coverage must be nil, an Integer, or a Hash; got #{base.inspect}"
454
+ end
329
455
 
330
- # Whether there are uncovered lines
331
- # @return [Boolean]
332
- # @api private
333
- def uncovered_lines_found? = simplecov_module.result.files.any? { |source_file| source_file.missed_lines.any? }
456
+ base&.each_pair { |criterion, threshold| validate_minimum_coverage_entry!(criterion, threshold) }
457
+ end
334
458
 
335
- # Whether to show the uncovered lines
336
- # @return [Boolean]
459
+ # Raise unless `criterion` is a known criterion with a Numeric `threshold`
460
+ # @param criterion [Object] the candidate `minimum_coverage` key
461
+ # @param threshold [Object] the candidate `minimum_coverage` value
462
+ # @return [Void]
463
+ # @raise [ArgumentError] if `criterion` is unknown, or `threshold` is not Numeric
337
464
  # @api private
338
465
  # @private
339
- def show_uncovered_lines_report? = list_uncovered_lines? && uncovered_lines_found?
466
+ def validate_minimum_coverage_entry!(criterion, threshold)
467
+ unless CRITERION_ENV_VARS.key?(criterion)
468
+ raise ArgumentError,
469
+ "minimum_coverage keys must be #{CRITERION_ENV_VARS.keys.inspect}; got #{criterion.inspect}"
470
+ end
340
471
 
341
- # Return the uncovered lines from the SimpleCov result
342
- # @return [Array<UncoveredLine>]
343
- # @api private
344
- def uncovered_lines
345
- @uncovered_lines ||=
346
- simplecov_module.result.files.flat_map do |source_file|
347
- source_file.missed_lines.map do |line|
348
- project_filename = File.join('.', source_file.project_filename)
349
- UncoveredLine.new(project_filename, line.number)
350
- end
351
- end
472
+ return if threshold.is_a?(Numeric)
473
+
474
+ raise ArgumentError,
475
+ "minimum_coverage[#{criterion.inspect}] must be a Numeric; got #{threshold.inspect}"
352
476
  end
353
477
 
354
- # Return the singular or plural form of a word based on the count
355
- # @param count [Integer] the count
356
- # @param singular [String] the singular form of the phrase
357
- # @param plural [String] the plural form of the phrase
358
- # @return [String]
359
- # @api private
360
- def pluralize(count, singular, plural) = count == 1 ? singular : plural
361
-
362
- # Output the uncovered lines
363
- # @return [Void]
478
+ # The per-criterion COVERAGE_THRESHOLD* ENV overrides, as a minimum_coverage Hash
479
+ # @return [Hash{Symbol => Integer}]
364
480
  # @api private
365
481
  # @private
366
- def uncovered_lines_report
367
- $stderr.puts
368
- count = uncovered_lines.count
369
- things = pluralize(uncovered_lines.count, 'line is', 'lines are')
370
- $stderr.puts "#{count} #{things} not covered by tests:\n"
371
- uncovered_lines.each do |uncovered_line|
372
- $stderr.puts " #{uncovered_line.project_filename}:#{uncovered_line.line_number}"
482
+ def env_minimum_coverage_overrides
483
+ CRITERION_ENV_VARS.each_with_object({}) do |(criterion, var), overrides|
484
+ overrides[criterion] = env.fetch(var).to_i if env.key?(var)
373
485
  end
374
486
  end
375
487
 
376
488
  # Return `true` if the environment variable is set to a truthy value
377
489
  #
378
490
  # @example
379
- # env_true?('LIST_UNCOVERED_LINES')
491
+ # env_true?('LIST_UNCOVERED_DETAIL')
380
492
  #
381
493
  # @param name [String] the name of the environment variable
382
494
  # @return [Boolean]
383
495
  # @api private
384
496
  # @private
385
497
  #
386
- def env_true?(name)
387
- value = env.fetch(name, '').downcase
388
- %w[yes on true 1].include?(value)
389
- end
498
+ def env_true?(name) = TRUTHY_ENV_VALUES.include?(env.fetch(name, '').downcase)
390
499
  end
391
500
  end