simplecov-rspec 0.4.4 → 1.0.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: 66e26ea8073a8373f072d8b58d5a11825fc26798d76b9ecf13b225279edfa06f
4
+ data.tar.gz: 59759b57e676fa16bbebd8d423dcf9852a3d4550c3b3864211c937003191c755
5
5
  SHA512:
6
- metadata.gz: 55816daa25c63ff70efaa2ecd127467384d9cc70bfa6bd0006789dbf4ae731f41a0423a803d8e6d56ec9139c08a98ce72d6577181c8845ae5d85e592e884de42
7
- data.tar.gz: f6e35a9fa9fb61073e1881112be2b035955089b65f86ed7c22beda11aef5d67b4c1d943ecd2a67f964220e6c449445fee8b1d32b3b4214891ce3b62595ad3c8b
6
+ metadata.gz: 253e3bbfe90ac2bc476f7e81eb9926f2457fb0b9a1cf4a066d116cdad7c2db00f15a33fa9a4622265580b309eead7b30ccadb0ef6ddc37936a9b52ecb9d0f369
7
+ data.tar.gz: 0b0c8be4624b3f85d3ede8bbb5f64b4a3e71d8287f27fd7a46e102b01318558c1c91b5b59fa2ff1c5505ff65513295da0a6495415b9a056c15fa736f06b63b41
@@ -1,3 +1,3 @@
1
1
  {
2
- ".": "0.4.4"
2
+ ".": "1.0.0"
3
3
  }
data/.rubocop.yml CHANGED
@@ -4,7 +4,7 @@ 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
8
 
9
9
  Style/StderrPuts:
10
10
  Enabled: false
data/CHANGELOG.md CHANGED
@@ -4,6 +4,19 @@ 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.0.0](https://github.com/main-branch/simplecov-rspec/compare/v0.4.4...v1.0.0) (2026-08-05)
8
+
9
+
10
+ ### ⚠ BREAKING CHANGES
11
+
12
+ * `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.
13
+ * 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).
14
+
15
+ ### Features
16
+
17
+ * Rely on SimpleCov's own minimum_coverage instead of reimplementing it ([d2fed78](https://github.com/main-branch/simplecov-rspec/commit/d2fed78ff5e5cb2bbf18bc89cb3b301455de0bce))
18
+ * Require Ruby >= 3.2, matching SimpleCov 1.0's own floor ([9452587](https://github.com/main-branch/simplecov-rspec/commit/9452587137e1b698019d4214981f82ed796b9c1d))
19
+
7
20
  ## [0.4.4](https://github.com/main-branch/simplecov-rspec/compare/v0.4.3...v0.4.4) (2026-04-24)
8
21
 
9
22
 
data/README.md CHANGED
@@ -8,10 +8,14 @@
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 three 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. Lets all of the above be overridden from the environment, for CI.
15
19
 
16
20
  When `simplecov-rspec` is used, RSpec will report an error if the percent of test
17
21
  coverage falls below a defined threshold:
@@ -19,10 +23,10 @@ coverage falls below a defined threshold:
19
23
  ```text
20
24
  Coverage report generated for RSpec to /Projects/example_project/coverage. 284 / 286 LOC (99.3%) covered.
21
25
 
22
- FAIL: RSpec Test coverage fell below 100%
26
+ Line coverage (99.3%) is below the expected minimum coverage (100.00%).
23
27
  ```
24
28
 
25
- If configured to list the lines that were not covered by tests, RSpec will additionally output:
29
+ If configured to list the items that were not covered by tests, RSpec will additionally output:
26
30
 
27
31
  ```text
28
32
  2 lines are not covered by tests:
@@ -30,16 +34,18 @@ If configured to list the lines that were not covered by tests, RSpec will addit
30
34
  ./lib/example_project.rb:75
31
35
  ```
32
36
 
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)
37
+ - [Installation](#installation)
38
+ - [Getting started](#getting-started)
39
+ - [Basic setup](#basic-setup)
40
+ - [Listing uncovered items](#listing-uncovered-items)
41
+ - [Configuration block](#configuration-block)
42
+ - [Configuration from environment variables](#configuration-from-environment-variables)
43
+ - [Development](#development)
44
+ - [Contributing](#contributing)
45
+ - [Commit message guidelines](#commit-message-guidelines)
46
+ - [Pull request guidelines](#pull-request-guidelines)
47
+ - [License](#license)
48
+ - [Code of conduct](#code-of-conduct)
43
49
 
44
50
  ## Installation
45
51
 
@@ -48,13 +54,13 @@ To install the gem, add to the following line to your application's gemspec OR G
48
54
  gemspec:
49
55
 
50
56
  ```ruby
51
- spec.add_development_dependency "simplecov-rspec", '~> 0.1'
57
+ spec.add_development_dependency "simplecov-rspec", '~> 1.0'
52
58
  ```
53
59
 
54
60
  Gemfile:
55
61
 
56
62
  ```ruby
57
- gem "simplecov-rspec", "~> 0.1", groups: [:development, :test]
63
+ gem "simplecov-rspec", "~> 1.0", groups: [:development, :test]
58
64
  ```
59
65
 
60
66
  and then run `bundle install`
@@ -102,54 +108,105 @@ This is equivalent to starting with the following options:
102
108
 
103
109
  ```ruby
104
110
  SimpleCov::RSpec.start(
105
- coverage_threshold: 100,
111
+ minimum_coverage: { line: 100 },
106
112
  fail_on_low_coverage: true,
107
- list_uncovered_lines: false
113
+ list_uncovered: false,
114
+ list_uncovered_detail: true
108
115
  )
109
116
  ```
110
117
 
111
- The test coverage threshold is the minimum percent of lines covered by tests as
112
- tracked by SimpleCov.
118
+ `minimum_coverage` is the minimum percent of lines (and, optionally, branches and
119
+ methods) covered by tests, enforced by SimpleCov itself.
113
120
 
114
- To initialize SimpleCov with a test coverage threshold less than 100%:
121
+ To require less than 100% line coverage:
115
122
 
116
123
  ```ruby
117
- SimpleCov::RSpec.start(coverage_threshold: 90)
124
+ SimpleCov::RSpec.start(minimum_coverage: 90)
118
125
  ```
119
126
 
127
+ To also require branch (and/or method) coverage, pass a Hash. Any criterion named
128
+ here is automatically enabled via `SimpleCov.enable_coverage`:
129
+
130
+ ```ruby
131
+ SimpleCov::RSpec.start(minimum_coverage: { line: 100, branch: 90 })
132
+ ```
133
+
134
+ ### Listing uncovered items
135
+
136
+ To list the individual lines, branches, and/or methods that are not covered, set
137
+ `list_uncovered`. It accepts `:all`, a single criterion, or an Array of criteria —
138
+ independent of what `minimum_coverage` enforces:
139
+
140
+ ```ruby
141
+ SimpleCov::RSpec.start(minimum_coverage: { line: 100, branch: 90 }, list_uncovered: :all)
142
+ ```
143
+
144
+ ```text
145
+ 1 line is not covered by tests:
146
+ ./lib/example_project.rb:74
147
+
148
+ 1 branch is not covered by tests:
149
+ ./lib/example_project.rb:82 (else branch)
150
+ ```
151
+
152
+ For a quieter CI log, set `list_uncovered_detail: false` to print only the count per
153
+ criterion, along with a hint on how to see the details:
154
+
155
+ ```ruby
156
+ SimpleCov::RSpec.start(list_uncovered: :all, list_uncovered_detail: false)
157
+ ```
158
+
159
+ ```text
160
+ 2 lines are not covered by tests.
161
+ 1 branch is not covered by tests.
162
+
163
+ Run with LIST_UNCOVERED_DETAIL=true to see the uncovered lines and branches.
164
+ ```
165
+
166
+ ### Configuration block
167
+
120
168
  A configuration block can be given to the `start` method to further configure
121
169
  SimpleCov:
122
170
 
123
171
  ```ruby
124
172
  # Initialize SimpleCov with a specific formatter
125
- SimpleCov::RSpec.start { formatter = SimpleCov::Formatter::LcovFormatter }
173
+ SimpleCov::RSpec.start { formatter SimpleCov::Formatter::LcovFormatter }
126
174
  ```
127
175
 
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)
176
+ This block is passed on to `SimpleCov.start`. See [Configuring
177
+ SimpleCov](https://github.com/simplecov-ruby/simplecov?tab=readme-ov-file#configuration)
130
178
  for details.
131
179
 
132
180
  ### Configuration from environment variables
133
181
 
134
182
  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`.
183
+ variables take precedence over the values passed to `SimpleCov::RSpec.start`.
184
+
185
+ * **`COVERAGE_THRESHOLD`**: Sets the minimum line coverage threshold (0-100). Overrides
186
+ `minimum_coverage[:line]`.
187
+ * **`COVERAGE_THRESHOLD_BRANCH`**: Sets the minimum branch coverage threshold (0-100), and
188
+ enables branch coverage. Overrides `minimum_coverage[:branch]`.
189
+ * **`COVERAGE_THRESHOLD_METHOD`**: Sets the minimum method coverage threshold (0-100), and
190
+ enables method coverage. Overrides `minimum_coverage[:method]`.
139
191
  * **`FAIL_ON_LOW_COVERAGE`**: Controls whether tests fail if coverage is below the threshold.
140
192
  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.
193
+ * **`LIST_UNCOVERED`**: Controls which criteria to list uncovered items for. Set to 'all',
194
+ 'true', 'yes', 'on', or '1' to report every criterion; 'false', 'no', 'off', or '0' to
195
+ report none; or a comma-separated list, e.g. `line,branch`.
196
+ * **`LIST_UNCOVERED_DETAIL`**: Controls whether uncovered items are listed individually, or
197
+ just summarized as a count per criterion. Set to 'true', 'yes', 'on', or '1' (case
198
+ insensitive) to show individual items.
143
199
 
144
200
  For example, here is a bash script to run tests in an infinite loop while writing
145
201
  test output to `fail.txt`:
146
202
 
147
203
  ```bash
148
- while true; do COV_NO_FAIL=TRUE rspec >> fail.txt; done
204
+ while true; do FAIL_ON_LOW_COVERAGE=false rspec >> fail.txt; done
149
205
  ```
150
206
 
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.
207
+ In a CI system, you might want to set `LIST_UNCOVERED=all` in order to list uncovered
208
+ lines, branches, and methods on a platform other than the one you use for local
209
+ development.
153
210
 
154
211
  ## Development
155
212
 
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleCov
4
+ class RSpec
5
+ # Resolves the `list_uncovered:` option (plus its `LIST_UNCOVERED` ENV override)
6
+ # into a normalized Array<Symbol>, in a fixed line/branch/method order.
7
+ #
8
+ # @api private
9
+ #
10
+ module ListUncoveredOption
11
+ # The coverage criteria this gem knows how to report on
12
+ ALL_CRITERIA = %i[line branch method].freeze
13
+
14
+ # LIST_UNCOVERED environment variable values that mean "every criterion"
15
+ ALL_ENV_VALUES = %w[yes on true 1 all].freeze
16
+
17
+ # LIST_UNCOVERED environment variable values that mean "no criteria"
18
+ NONE_ENV_VALUES = %w[false no off 0].freeze
19
+
20
+ module_function
21
+
22
+ # Resolve the effective criteria, applying the ENV override if present
23
+ # @param value [false, :all, Symbol, Array<Symbol>] the `list_uncovered:` argument
24
+ # @param env [Hash] the environment variables
25
+ # @param env_var [String] the ENV var name that overrides `value`
26
+ # @return [Array<Symbol>]
27
+ # @raise [ArgumentError] if value is not one of the accepted forms, or names an unknown criterion
28
+ # @example
29
+ # ListUncoveredOption.resolve(:all, env: {}, env_var: 'LIST_UNCOVERED') # => [:line, :branch, :method]
30
+ def resolve(value, env:, env_var:)
31
+ value = from_env(env.fetch(env_var)) if env.key?(env_var)
32
+ ALL_CRITERIA & normalize(value)
33
+ end
34
+
35
+ # Parse a raw LIST_UNCOVERED environment variable value
36
+ # @param raw [String] the raw LIST_UNCOVERED environment variable value
37
+ # @return [false, :all, Array<Symbol>]
38
+ # @example
39
+ # ListUncoveredOption.from_env('line,branch') # => [:line, :branch]
40
+ def from_env(raw)
41
+ raw = raw.strip.downcase
42
+ return false if raw.empty? || NONE_ENV_VALUES.include?(raw)
43
+ return :all if ALL_ENV_VALUES.include?(raw)
44
+
45
+ raw.split(',').map { |criterion| criterion.strip.to_sym }
46
+ end
47
+
48
+ # Normalize a `list_uncovered:`-style value into an Array of Symbols
49
+ # @param value [false, true, :all, Symbol, Array<Symbol>]
50
+ # @return [Array<Symbol>]
51
+ # @example
52
+ # ListUncoveredOption.normalize(:branch) # => [:branch]
53
+ def normalize(value)
54
+ case value
55
+ when nil, false then []
56
+ when :all then ALL_CRITERIA
57
+ when Symbol then validate([value])
58
+ when Array then validate(value)
59
+ else
60
+ raise ArgumentError,
61
+ "list_uncovered must be false, :all, a Symbol, or an Array of Symbols; got #{value.inspect}"
62
+ end
63
+ end
64
+
65
+ # Raise unless every given criterion is one this gem knows how to report on
66
+ # @param criteria [Array<Symbol>]
67
+ # @return [Array<Symbol>]
68
+ # @raise [ArgumentError] if criteria contains anything outside ALL_CRITERIA
69
+ # @example
70
+ # ListUncoveredOption.validate([:line]) # => [:line]
71
+ def validate(criteria)
72
+ invalid = criteria - ALL_CRITERIA
73
+ return criteria if invalid.empty?
74
+
75
+ raise ArgumentError, "Unknown coverage criterion #{invalid.inspect}; must be one of #{ALL_CRITERIA.inspect}"
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,186 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleCov
4
+ class RSpec
5
+ # Formats the "uncovered lines/branches/methods" report printed after a run: either
6
+ # a full listing of every uncovered item, or a per-criterion count with a hint on
7
+ # how to see the details.
8
+ #
9
+ # @api private
10
+ #
11
+ class UncoveredReport
12
+ # Maps a coverage criterion to its [singular, plural] noun, for report formatting
13
+ # @api private
14
+ CRITERION_LABELS = {
15
+ line: %w[line lines],
16
+ branch: %w[branch branches],
17
+ method: %w[method methods]
18
+ }.freeze
19
+
20
+ # Build a report for the given result, criteria, and detail level
21
+ # @param result [SimpleCov::Result] the SimpleCov result to report on
22
+ # @param criteria [Array<Symbol>] which criteria (:line, :branch, :method) to report
23
+ # @param detail [Boolean] list individual items, or just a count per criterion
24
+ # @param detail_env_var [String] the ENV var name to suggest for switching to detail
25
+ # @example
26
+ # UncoveredReport.new(result: SimpleCov.result, criteria: [:line], detail: true, detail_env_var: 'X')
27
+ def initialize(result:, criteria:, detail:, detail_env_var:)
28
+ @result = result
29
+ @criteria = criteria
30
+ @detail = detail
31
+ @detail_env_var = detail_env_var
32
+ end
33
+
34
+ # The formatted report text, or an empty string if there is nothing to report
35
+ # @return [String]
36
+ def to_s = detail ? detailed_text : summary_text
37
+
38
+ private
39
+
40
+ # The SimpleCov result being reported on
41
+ # @return [SimpleCov::Result]
42
+ # @api private
43
+ attr_reader :result
44
+
45
+ # Which criteria (:line, :branch, :method) to report on
46
+ # @return [Array<Symbol>]
47
+ # @api private
48
+ attr_reader :criteria
49
+
50
+ # Whether to list individual items, or just a count per criterion
51
+ # @return [Boolean]
52
+ # @api private
53
+ attr_reader :detail
54
+
55
+ # The ENV var name to suggest for switching to detail
56
+ # @return [String]
57
+ # @api private
58
+ attr_reader :detail_env_var
59
+
60
+ # The full listing, one blank-line-separated section per criterion
61
+ # @return [String]
62
+ def detailed_text
63
+ criteria.filter_map { |criterion| section(criterion) }.join("\n\n")
64
+ end
65
+
66
+ # A criterion's report section, or nil when nothing is uncovered
67
+ # @param criterion [Symbol]
68
+ # @return [String, nil]
69
+ def section(criterion)
70
+ items = uncovered_items(criterion)
71
+ return nil if items.empty?
72
+
73
+ ["#{header(criterion, items.count)}:", *items.map { |item| " #{item}" }].join("\n")
74
+ end
75
+
76
+ # The per-criterion counts, plus a hint on how to see the details
77
+ # @return [String]
78
+ def summary_text
79
+ summarized = criteria.filter_map do |criterion|
80
+ count = uncovered_count(criterion)
81
+ [criterion, count] if count.positive?
82
+ end
83
+ return '' if summarized.empty?
84
+
85
+ counts = summarized.map { |criterion, count| "#{header(criterion, count)}." }.join("\n")
86
+ "#{counts}\n\nRun with #{detail_env_var}=true to see the uncovered #{noun_list(summarized.map(&:first))}."
87
+ end
88
+
89
+ # The "N lines are not covered by tests" header for a criterion
90
+ # @param criterion [Symbol]
91
+ # @param count [Integer]
92
+ # @return [String]
93
+ def header(criterion, count)
94
+ singular, plural = CRITERION_LABELS.fetch(criterion)
95
+ "#{count} #{pluralize(count, "#{singular} is", "#{plural} are")} not covered by tests"
96
+ end
97
+
98
+ # The formatted, uncovered items for a single criterion, across all files
99
+ # @param criterion [Symbol]
100
+ # @return [Array<String>]
101
+ def uncovered_items(criterion)
102
+ result.files.flat_map { |file| items_for(file, criterion) }
103
+ end
104
+
105
+ # The count of uncovered items for a single criterion across all files
106
+ #
107
+ # @param criterion [Symbol]
108
+ # @return [Integer]
109
+ def uncovered_count(criterion)
110
+ result.files.sum { |file| count_for(file, criterion) }
111
+ end
112
+
113
+ # The formatted, uncovered items of one criterion within a single file
114
+ # @param file [SimpleCov::SourceFile]
115
+ # @param criterion [Symbol]
116
+ # @return [Array<String>]
117
+ def items_for(file, criterion) = send(:"#{criterion}_items", file)
118
+
119
+ # The count of uncovered items of one criterion within a single file
120
+ # @param file [SimpleCov::SourceFile]
121
+ # @param criterion [Symbol]
122
+ # @return [Integer]
123
+ def count_for(file, criterion) = send(:"#{criterion}_count", file)
124
+
125
+ # The formatted, uncovered lines within a single file
126
+ # @param file [SimpleCov::SourceFile]
127
+ # @return [Array<String>]
128
+ def line_items(file) = file.missed_lines.map { |line| "#{project_path(file)}:#{line.number}" }
129
+
130
+ # The formatted, uncovered branches within a single file
131
+ # @param file [SimpleCov::SourceFile]
132
+ # @return [Array<String>]
133
+ def branch_items(file) = file.missed_branches.map { |branch| branch_text(file, branch) }
134
+
135
+ # The formatted, uncovered methods within a single file
136
+ # @param file [SimpleCov::SourceFile]
137
+ # @return [Array<String>]
138
+ def method_items(file)
139
+ file.missed_methods.map { |method| "#{project_path(file)}:#{method.start_line} #{method}" }
140
+ end
141
+
142
+ # The count of uncovered lines within a single file
143
+ # @param file [SimpleCov::SourceFile]
144
+ # @return [Integer]
145
+ def line_count(file) = file.missed_lines.count
146
+
147
+ # The count of uncovered branches within a single file
148
+ # @param file [SimpleCov::SourceFile]
149
+ # @return [Integer]
150
+ def branch_count(file) = file.missed_branches.count
151
+
152
+ # The count of uncovered methods within a single file
153
+ # @param file [SimpleCov::SourceFile]
154
+ # @return [Integer]
155
+ def method_count(file) = file.missed_methods.count
156
+
157
+ # A single formatted, uncovered branch
158
+ # @param file [SimpleCov::SourceFile]
159
+ # @param branch [SimpleCov::SourceFile::Branch]
160
+ # @return [String]
161
+ def branch_text(file, branch) = "#{project_path(file)}:#{branch.report_line} (#{branch.type} branch)"
162
+
163
+ # The path to a source file, relative to the project root
164
+ # @param file [SimpleCov::SourceFile]
165
+ # @return [String]
166
+ def project_path(file) = File.join('.', file.project_filename)
167
+
168
+ # Join plural nouns into a friendly list, e.g. "lines and branches"
169
+ # @param criteria_list [Array<Symbol>]
170
+ # @return [String]
171
+ def noun_list(criteria_list)
172
+ nouns = criteria_list.map { |criterion| CRITERION_LABELS.fetch(criterion).last }
173
+ return nouns.first if nouns.size == 1
174
+
175
+ "#{nouns[0..-2].join(', ')} and #{nouns.last}"
176
+ end
177
+
178
+ # Return the singular or plural form of a word based on the count
179
+ # @param count [Integer] the count
180
+ # @param singular [String] the singular form of the phrase
181
+ # @param plural [String] the plural form of the phrase
182
+ # @return [String]
183
+ def pluralize(count, singular, plural) = count == 1 ? singular : plural
184
+ end
185
+ end
186
+ end
@@ -3,6 +3,6 @@
3
3
  module Simplecov
4
4
  class Rspec
5
5
  # This gem's version
6
- VERSION = '0.4.4'
6
+ VERSION = '1.0.0'
7
7
  end
8
8
  end
@@ -1,55 +1,59 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'simplecov'
4
+ require_relative 'simplecov-rspec/list_uncovered_option'
5
+ require_relative 'simplecov-rspec/uncovered_report'
4
6
 
5
7
  # SimpleCov namespace
6
8
  module SimpleCov
7
- # Configure SimpleCov to fail RSpec if the test coverage falls below a given threshold
9
+ # Configure SimpleCov to enforce coverage thresholds for RSpec, on top of what
10
+ # SimpleCov itself already provides.
8
11
  #
9
- # Configures RSpec to:
12
+ # SimpleCov (>= 1.0) can already enforce `minimum_coverage` for line, branch, and
13
+ # method coverage, and will exit with a non-zero status when a threshold is missed.
14
+ # This gem layers three things SimpleCov doesn't do on its own:
10
15
  #
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.
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. Lets all of the above be overridden from the environment, for CI.
14
19
  #
15
- # Simply add the line `SimpleCov::RSpec.start` in place of `SimpleCov::Start` in
20
+ # Simply add the line `SimpleCov::RSpec.start` in place of `SimpleCov.start` in
16
21
  # the project's `spec_helper.rb`. This line must appear before the project is
17
22
  # required.
18
23
  #
19
- # @example Initialize SimpleCov with defaults
24
+ # @example Initialize SimpleCov with defaults (100% line coverage required)
20
25
  # SimpleCov::RSpec.start
21
26
  #
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)
27
+ # @example Require 100% line coverage and 90% branch coverage
28
+ # SimpleCov::RSpec.start(minimum_coverage: { line: 100, branch: 90 })
24
29
  #
25
- # @example Pass a configuration block to SimpleCov::RSpec.start
26
- # SimpleCov::RSpec.start { formatter = SimpleCov::Formatter::LcovFormatter }
30
+ # @example List every uncovered line, branch, and method when coverage is incomplete
31
+ # SimpleCov::RSpec.start(minimum_coverage: { line: 100, branch: 90 }, list_uncovered: :all)
27
32
  #
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
33
+ # @example Report only counts, with a hint on how to see the details
34
+ # SimpleCov::RSpec.start(list_uncovered: :all, list_uncovered_detail: false)
30
35
  #
31
- # @!attribute [r] env
32
- # Command line environment variables (default: ENV)
33
- # @return [Hash]
34
- # @api private
35
- # @private
36
- #
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
36
+ # @example Pass a configuration block to SimpleCov.start
37
+ # SimpleCov::RSpec.start { formatter SimpleCov::Formatter::LcovFormatter }
48
38
  #
49
39
  # @api public
50
40
  #
51
41
  class RSpec
52
- # rubocop:disable Layout/LineLength
42
+ # @!attribute [r] env
43
+ # The environment variables consulted for overrides
44
+ # @return [Hash]
45
+ # @api private
46
+ #
47
+ # @!attribute [r] simplecov_module
48
+ # The SimpleCov module being configured
49
+ # @return [Module]
50
+ # @api private
51
+ #
52
+ # @!attribute [r] start_config_block
53
+ # A configuration block passed through to `SimpleCov.start`
54
+ # @return [Proc, nil]
55
+ # @api private
56
+ #
53
57
 
54
58
  # Configure and start SimpleCov for RSpec
55
59
  #
@@ -60,128 +64,153 @@ module SimpleCov
60
64
  #
61
65
  # @api public
62
66
  #
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)
67
+ # @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
68
  #
65
- # @param coverage_threshold [Integer] the test coverage threshold (default: 100)
69
+ # @param minimum_coverage [Integer, Hash] the minimum coverage threshold (default: `{ line: 100 }`)
66
70
  #
67
- # Coverage below this threshold will cause the rspec to fail if
68
- # fail_on_low_coverage is true.
71
+ # An Integer sets the line coverage threshold. A Hash sets a threshold per
72
+ # criterion, e.g. `{ line: 100, branch: 90, method: 100 }`. Passed straight
73
+ # through to `SimpleCov.minimum_coverage`; any criterion given here is
74
+ # automatically enabled via `SimpleCov.enable_coverage`.
69
75
  #
70
- # @param fail_on_low_coverage [Boolean] whether to fail if the coverage is below the threshold (default: true)
76
+ # @param fail_on_low_coverage [Boolean] whether to fail if coverage is below the threshold (default: true)
71
77
  #
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`.
78
+ # When `false` (or when RSpec is in dry-run mode), `SimpleCov.minimum_coverage`
79
+ # is never set, so SimpleCov will not fail the build regardless of coverage.
77
80
  #
78
- # @param list_uncovered_lines [Boolean] whether to list the lines not covered by tests (default: false)
81
+ # Read from the `FAIL_ON_LOW_COVERAGE` environment variable if set: `true`,
82
+ # `yes`, `on`, or `1` (case-insensitive) enables it; anything else disables it.
79
83
  #
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%.
84
+ # @param list_uncovered [false, :all, Symbol, Array<Symbol>] which coverage criteria to
85
+ # list uncovered items for (default: false)
82
86
  #
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`.
87
+ # `false` reports nothing. `:all` reports line, branch, and method. A Symbol or
88
+ # Array of Symbols (`:line`, `:branch`, `:method`) reports just those criteria,
89
+ # independent of what `minimum_coverage` enforces.
87
90
  #
88
- # @param start_config_block [Proc] a configuration block to pass to `SimpleCov.start` (default: nil)
91
+ # Read from the `LIST_UNCOVERED` environment variable if set: `all`, `true`,
92
+ # `yes`, `on`, or `1` means every criterion; `false`, `no`, `off`, or `0` means
93
+ # none; otherwise a comma-separated list of criteria, e.g. `line,branch`.
89
94
  #
90
- # @param rspec_dry_run [Boolean] whether the rspec run is a dry run
95
+ # @param list_uncovered_detail [Boolean] list individual items, or just a count (default: true)
91
96
  #
92
- # Typically not set by the user. Used for this gem's unit testing.
97
+ # When `false`, only the count of uncovered items per criterion is printed,
98
+ # followed by a hint on how to see the details.
93
99
  #
94
- # If RSpec is being run in dry run mode, test coverage under the threshold will not fail the build.
100
+ # Read from the `LIST_UNCOVERED_DETAIL` environment variable if set: `true`,
101
+ # `yes`, `on`, or `1` (case-insensitive) shows details; anything else summarizes.
95
102
  #
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.
103
+ # @param start_config_block [Proc] a configuration block to pass to `SimpleCov.start` (default: nil)
104
+ #
105
+ # @param rspec_dry_run [Boolean] whether the rspec run is a dry run
106
+ #
107
+ # Typically not set by the user. If RSpec is being run in dry run mode, test
108
+ # coverage under the threshold will not fail the build. This allows test
109
+ # coverage to be run in a dry run by an IDE so it can report failed tests and
110
+ # coverage without reporting that the entire RSpec run has failed.
99
111
  #
100
112
  # @param simplecov_module [Module] the SimpleCov module (default: ::SimpleCov)
101
113
  #
102
114
  # Typically not set by the user. Used for this gem's unit testing.
103
115
  #
104
- # Allows the SimpleCov module to be mocked.
105
- #
106
116
  # @param env [Hash] the environment variables (default: ENV)
107
117
  #
108
118
  # Typically not set by the user. Used for this gem's unit testing.
109
119
  #
110
120
  # @example Initialize SimpleCov with a test coverage threshold other than 100%
111
- # SimpleCov::RSpec.start(coverage_threshold: 90)
121
+ # SimpleCov::RSpec.start(minimum_coverage: 90)
112
122
  #
113
123
  # @example Initialize SimpleCov to not fail the test run if the coverage is below the threshold
114
124
  # SimpleCov::RSpec.start(fail_on_low_coverage: false)
115
125
  #
116
126
  # # OR use an environment variable to override the default
117
- # ENV['FAIL_ON_LOW_COVERAGE'] = 'true'
118
- # SimpleCov::RSpec.start
119
- #
120
- # # OR use an environment variable to override the default from the rspec command line
121
- # FAIL_ON_LOW_COVERAGE=TRUE rspec
127
+ # FAIL_ON_LOW_COVERAGE=true rspec
122
128
  #
123
- # @example Initialize SimpleCov to list the lines not covered by tests
124
- # SimpleCov::RSpec.start(list_uncovered_lines: true)
129
+ # @example Initialize SimpleCov to list the lines, branches, and methods not covered by tests
130
+ # SimpleCov::RSpec.start(list_uncovered: :all)
125
131
  #
126
132
  # # OR use an environment variable to override the default
127
- # ENV['LIST_UNCOVERED_LINES'] = 'true'
128
- # SimpleCov::RSpec.start
129
- #
130
- # # OR use an environment variable to override the default from the rspec command line
131
- # LIST_UNCOVERED_LINES=TRUE rspec
133
+ # LIST_UNCOVERED=all rspec
132
134
  #
133
135
  def self.start(...) = new(...).send(:start)
134
136
 
135
- # rubocop:enable Layout/LineLength
136
-
137
- # Environment variable to override coverage_threshold
137
+ # Environment variable to override minimum_coverage[:line]
138
138
  # @api private
139
139
  # @private
140
140
  COVERAGE_THRESHOLD = 'COVERAGE_THRESHOLD'
141
141
 
142
+ # Environment variable to override minimum_coverage[:branch]
143
+ # @api private
144
+ # @private
145
+ COVERAGE_THRESHOLD_BRANCH = 'COVERAGE_THRESHOLD_BRANCH'
146
+
147
+ # Environment variable to override minimum_coverage[:method]
148
+ # @api private
149
+ # @private
150
+ COVERAGE_THRESHOLD_METHOD = 'COVERAGE_THRESHOLD_METHOD'
151
+
142
152
  # Environment variable to override fail_on_low_coverage
143
153
  # @api private
144
154
  # @private
145
155
  FAIL_ON_LOW_COVERAGE = 'FAIL_ON_LOW_COVERAGE'
146
156
 
147
- # Environment variable to override list_uncovered_lines
157
+ # Environment variable to override list_uncovered
158
+ # @api private
159
+ # @private
160
+ LIST_UNCOVERED = 'LIST_UNCOVERED'
161
+
162
+ # Environment variable to override list_uncovered_detail
148
163
  # @api private
149
164
  # @private
150
- LIST_UNCOVERED_LINES = 'LIST_UNCOVERED_LINES'
165
+ LIST_UNCOVERED_DETAIL = 'LIST_UNCOVERED_DETAIL'
151
166
 
152
- # Default value for coverage_threshold
167
+ # Maps a coverage criterion to the environment variable that overrides its threshold
153
168
  # @api private
154
169
  # @private
155
- DEFAULT_TEST_COVERAGE_THRESHOLD = 100
170
+ CRITERION_ENV_VARS = {
171
+ line: COVERAGE_THRESHOLD,
172
+ branch: COVERAGE_THRESHOLD_BRANCH,
173
+ method: COVERAGE_THRESHOLD_METHOD
174
+ }.freeze
175
+
176
+ # Default value for minimum_coverage
177
+ # @api private
178
+ # @private
179
+ DEFAULT_MINIMUM_COVERAGE = { line: 100 }.freeze
156
180
 
157
181
  # Default value for fail_on_low_coverage
158
182
  # @api private
159
183
  # @private
160
- DEFAULT_FAIL_ON_LOW_COVERAGEERAGE = true
184
+ DEFAULT_FAIL_ON_LOW_COVERAGE = true
185
+
186
+ # Default value for list_uncovered_detail
187
+ # @api private
188
+ # @private
189
+ DEFAULT_LIST_UNCOVERED_DETAIL = true
161
190
 
162
- # Default value for list_uncovered_lines
191
+ # Environment variable values that mean "true"
163
192
  # @api private
164
193
  # @private
165
- DEFAULT_LIST_UNCOVERED_LINES = false
194
+ TRUTHY_ENV_VALUES = %w[yes on true 1].freeze
166
195
 
167
196
  attr_reader :env, :simplecov_module, :start_config_block
168
197
 
169
- # The coverage threshold
198
+ # The minimum coverage threshold, per criterion
170
199
  #
171
- # Searches the ENV, the value given in the `.start` method, and the default value
172
- # and returns the first value found.
200
+ # Searches the ENV, the value given in the `.start` method, and the default value,
201
+ # merging them (ENV takes precedence per-criterion).
173
202
  #
174
- # @return [Integer]
203
+ # @return [Hash{Symbol => Integer}]
175
204
  #
176
205
  # @api private
177
206
  # @private
178
207
  #
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?
208
+ def minimum_coverage
209
+ base = @minimum_coverage
210
+ base = { line: base } if base.is_a?(Integer)
211
+ validate_minimum_coverage!(base)
183
212
 
184
- DEFAULT_TEST_COVERAGE_THRESHOLD
213
+ (base || DEFAULT_MINIMUM_COVERAGE).merge(env_minimum_coverage_overrides)
185
214
  end
186
215
 
187
216
  # Whether to fail if the coverage is below the threshold
@@ -196,55 +225,48 @@ module SimpleCov
196
225
  #
197
226
  def fail_on_low_coverage?
198
227
  return false if rspec_dry_run?
199
-
200
228
  return env_true?(FAIL_ON_LOW_COVERAGE) if env.key?(FAIL_ON_LOW_COVERAGE)
201
-
202
229
  return @fail_on_low_coverage unless @fail_on_low_coverage.nil?
203
230
 
204
- DEFAULT_FAIL_ON_LOW_COVERAGEERAGE
231
+ DEFAULT_FAIL_ON_LOW_COVERAGE
205
232
  end
206
233
 
207
- # Whether to list the lines not covered by tests
234
+ # The coverage criteria to list uncovered items for
208
235
  #
209
- # Searches the ENV, the value given in the `.start` method, and the default value
210
- # and returns the first value found.
236
+ # Searches the ENV and the value given in the `.start` method, and normalizes
237
+ # the result to an Array of Symbols in a fixed (line, branch, method) order.
211
238
  #
212
- # @return [Boolean]
239
+ # @return [Array<Symbol>]
213
240
  #
214
241
  # @api private
215
242
  # @private
216
243
  #
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?
221
-
222
- DEFAULT_LIST_UNCOVERED_LINES
244
+ def list_uncovered_criteria
245
+ @list_uncovered_criteria ||= ListUncoveredOption.resolve(@list_uncovered, env: env, env_var: LIST_UNCOVERED)
223
246
  end
224
247
 
225
- # Whether the rspec run is a dry run
248
+ # Whether to list individual uncovered items, or just a count per criterion
226
249
  #
227
250
  # @return [Boolean]
228
251
  #
229
252
  # @api private
230
253
  # @private
231
254
  #
232
- def rspec_dry_run? = @rspec_dry_run
255
+ def list_uncovered_detail?
256
+ return env_true?(LIST_UNCOVERED_DETAIL) if env.key?(LIST_UNCOVERED_DETAIL)
257
+ return @list_uncovered_detail unless @list_uncovered_detail.nil?
233
258
 
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
259
+ DEFAULT_LIST_UNCOVERED_DETAIL
260
+ end
261
+
262
+ # Whether the rspec run is a dry run
240
263
  #
241
- # @!attribute line_number [rw]
242
- # The line number of the uncovered line
243
- # @return [Integer]
244
- # @api private
264
+ # @return [Boolean]
245
265
  #
246
266
  # @api private
247
- UncoveredLine = Struct.new(:project_filename, :line_number)
267
+ # @private
268
+ #
269
+ def rspec_dry_run? = @rspec_dry_run
248
270
 
249
271
  private
250
272
 
@@ -255,17 +277,19 @@ module SimpleCov
255
277
  # @api private
256
278
  # @private
257
279
  def initialize(
258
- coverage_threshold: nil,
280
+ minimum_coverage: nil,
259
281
  fail_on_low_coverage: nil,
260
- list_uncovered_lines: nil,
282
+ list_uncovered: nil,
283
+ list_uncovered_detail: nil,
261
284
  rspec_dry_run: ::RSpec.configuration.dry_run?,
262
285
  env: ENV,
263
286
  simplecov_module: ::SimpleCov,
264
287
  &start_config_block
265
288
  )
266
- @coverage_threshold = coverage_threshold
289
+ @minimum_coverage = minimum_coverage
267
290
  @fail_on_low_coverage = fail_on_low_coverage
268
- @list_uncovered_lines = list_uncovered_lines
291
+ @list_uncovered = list_uncovered
292
+ @list_uncovered_detail = list_uncovered_detail
269
293
  @start_config_block = start_config_block
270
294
  @rspec_dry_run = rspec_dry_run
271
295
  @env = env
@@ -274,15 +298,29 @@ module SimpleCov
274
298
 
275
299
  # rubocop:enable Metrics/ParameterLists
276
300
 
277
- # Set the at_exit hook and then configure and start SimpleCov
301
+ # Configure SimpleCov and start it
278
302
  # @return [Void]
279
303
  # @api private
280
304
  # @private
281
305
  def start
282
306
  simplecov_module.at_exit(&at_exit_hook)
307
+ simplecov_module.enable_coverage(*criteria_to_enable) if criteria_to_enable.any?
308
+ simplecov_module.minimum_coverage(minimum_coverage) if enforce_minimum_coverage?
283
309
  simplecov_module.start(&start_config_block)
284
310
  end
285
311
 
312
+ # Whether SimpleCov should be configured to enforce minimum_coverage itself
313
+ # @return [Boolean]
314
+ # @api private
315
+ # @private
316
+ def enforce_minimum_coverage? = !rspec_dry_run? && fail_on_low_coverage?
317
+
318
+ # The coverage criteria that need to be enabled via SimpleCov.enable_coverage
319
+ # @return [Array<Symbol>]
320
+ # @api private
321
+ # @private
322
+ def criteria_to_enable = (minimum_coverage.keys + list_uncovered_criteria).uniq
323
+
286
324
  # Called by SimpleCov.at_exit
287
325
  # @return [Proc]
288
326
  # @api private
@@ -290,102 +328,87 @@ module SimpleCov
290
328
  def at_exit_hook
291
329
  lambda do
292
330
  simplecov_module.result.format!
293
- output_at_exit_report
294
- exit 1 if coverage_below_threshold? && fail_on_low_coverage?
331
+ output_uncovered_report
295
332
  end
296
333
  end
297
334
 
298
- # Output the at_exit report
335
+ # Output the uncovered items report (detailed or summary), if requested
299
336
  # @return [Void]
300
337
  # @api private
301
338
  # @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
339
+ def output_uncovered_report
340
+ return if list_uncovered_criteria.empty?
307
341
 
308
- # Whether to show the low coverage
309
- # @return [Boolean]
310
- # @api private
311
- # @private
312
- def show_low_coverage_report? = coverage_below_threshold?
342
+ report = UncoveredReport.new(
343
+ result: simplecov_module.result,
344
+ criteria: list_uncovered_criteria,
345
+ detail: list_uncovered_detail?,
346
+ detail_env_var: LIST_UNCOVERED_DETAIL
347
+ ).to_s
348
+ return if report.empty?
313
349
 
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
350
+ $stderr.puts
351
+ $stderr.puts report
352
+ end
319
353
 
320
- # Output the low coverage report
354
+ # Raise unless `base` is a valid `minimum_coverage` value
355
+ #
356
+ # Valid values include `nil`, or a Hash keyed by `:line`, `:branch`, and/or
357
+ # `:method`, with Numeric values
358
+ #
359
+ # @param base [nil, Hash] the normalized `minimum_coverage` value
321
360
  # @return [Void]
361
+ # @raise [ArgumentError] if `base` is not nil or a Hash, has an unknown key, or has a
362
+ # non-Numeric value
322
363
  # @api private
323
364
  # @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
365
+ def validate_minimum_coverage!(base)
366
+ unless base.nil? || base.is_a?(Hash)
367
+ raise ArgumentError, "minimum_coverage must be nil, an Integer, or a Hash; got #{base.inspect}"
368
+ end
329
369
 
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? }
370
+ base&.each_pair { |criterion, threshold| validate_minimum_coverage_entry!(criterion, threshold) }
371
+ end
334
372
 
335
- # Whether to show the uncovered lines
336
- # @return [Boolean]
373
+ # Raise unless `criterion` is a known criterion with a Numeric `threshold`
374
+ # @param criterion [Object] the candidate `minimum_coverage` key
375
+ # @param threshold [Object] the candidate `minimum_coverage` value
376
+ # @return [Void]
377
+ # @raise [ArgumentError] if `criterion` is unknown, or `threshold` is not Numeric
337
378
  # @api private
338
379
  # @private
339
- def show_uncovered_lines_report? = list_uncovered_lines? && uncovered_lines_found?
380
+ def validate_minimum_coverage_entry!(criterion, threshold)
381
+ unless CRITERION_ENV_VARS.key?(criterion)
382
+ raise ArgumentError,
383
+ "minimum_coverage keys must be #{CRITERION_ENV_VARS.keys.inspect}; got #{criterion.inspect}"
384
+ end
340
385
 
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
352
- end
386
+ return if threshold.is_a?(Numeric)
353
387
 
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
388
+ raise ArgumentError,
389
+ "minimum_coverage[#{criterion.inspect}] must be a Numeric; got #{threshold.inspect}"
390
+ end
361
391
 
362
- # Output the uncovered lines
363
- # @return [Void]
392
+ # The per-criterion COVERAGE_THRESHOLD* ENV overrides, as a minimum_coverage Hash
393
+ # @return [Hash{Symbol => Integer}]
364
394
  # @api private
365
395
  # @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}"
396
+ def env_minimum_coverage_overrides
397
+ CRITERION_ENV_VARS.each_with_object({}) do |(criterion, var), overrides|
398
+ overrides[criterion] = env.fetch(var).to_i if env.key?(var)
373
399
  end
374
400
  end
375
401
 
376
402
  # Return `true` if the environment variable is set to a truthy value
377
403
  #
378
404
  # @example
379
- # env_true?('LIST_UNCOVERED_LINES')
405
+ # env_true?('LIST_UNCOVERED_DETAIL')
380
406
  #
381
407
  # @param name [String] the name of the environment variable
382
408
  # @return [Boolean]
383
409
  # @api private
384
410
  # @private
385
411
  #
386
- def env_true?(name)
387
- value = env.fetch(name, '').downcase
388
- %w[yes on true 1].include?(value)
389
- end
412
+ def env_true?(name) = TRUTHY_ENV_VALUES.include?(env.fetch(name, '').downcase)
390
413
  end
391
414
  end
@@ -6,8 +6,8 @@
6
6
  "package-name": "simplecov-rspec",
7
7
  "changelog-path": "CHANGELOG.md",
8
8
  "version-file": "lib/simplecov-rspec/version.rb",
9
- "bump-minor-pre-major": true,
10
- "bump-patch-for-minor-pre-major": true,
9
+ "bump-minor-pre-major": false,
10
+ "bump-patch-for-minor-pre-major": false,
11
11
  "draft": false,
12
12
  "prerelease": false,
13
13
  "include-component-in-tag": false,
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: simplecov-rspec
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.4
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - James Couball
@@ -15,14 +15,14 @@ dependencies:
15
15
  requirements:
16
16
  - - "~>"
17
17
  - !ruby/object:Gem::Version
18
- version: '0.22'
18
+ version: '1.0'
19
19
  type: :runtime
20
20
  prerelease: false
21
21
  version_requirements: !ruby/object:Gem::Requirement
22
22
  requirements:
23
23
  - - "~>"
24
24
  - !ruby/object:Gem::Version
25
- version: '0.22'
25
+ version: '1.0'
26
26
  - !ruby/object:Gem::Dependency
27
27
  name: bundler-audit
28
28
  requirement: !ruby/object:Gem::Requirement
@@ -85,14 +85,14 @@ dependencies:
85
85
  requirements:
86
86
  - - "~>"
87
87
  - !ruby/object:Gem::Version
88
- version: '13.2'
88
+ version: '13.4'
89
89
  type: :development
90
90
  prerelease: false
91
91
  version_requirements: !ruby/object:Gem::Requirement
92
92
  requirements:
93
93
  - - "~>"
94
94
  - !ruby/object:Gem::Version
95
- version: '13.2'
95
+ version: '13.4'
96
96
  - !ruby/object:Gem::Dependency
97
97
  name: rspec
98
98
  requirement: !ruby/object:Gem::Requirement
@@ -113,14 +113,14 @@ dependencies:
113
113
  requirements:
114
114
  - - "~>"
115
115
  - !ruby/object:Gem::Version
116
- version: '1.66'
116
+ version: '1.89'
117
117
  type: :development
118
118
  prerelease: false
119
119
  version_requirements: !ruby/object:Gem::Requirement
120
120
  requirements:
121
121
  - - "~>"
122
122
  - !ruby/object:Gem::Version
123
- version: '1.66'
123
+ version: '1.89'
124
124
  - !ruby/object:Gem::Dependency
125
125
  name: simplecov-lcov
126
126
  requirement: !ruby/object:Gem::Requirement
@@ -149,6 +149,20 @@ dependencies:
149
149
  - - "~>"
150
150
  - !ruby/object:Gem::Version
151
151
  version: '4.4'
152
+ - !ruby/object:Gem::Dependency
153
+ name: irb
154
+ requirement: !ruby/object:Gem::Requirement
155
+ requirements:
156
+ - - "~>"
157
+ - !ruby/object:Gem::Version
158
+ version: '1.13'
159
+ type: :development
160
+ prerelease: false
161
+ version_requirements: !ruby/object:Gem::Requirement
162
+ requirements:
163
+ - - "~>"
164
+ - !ruby/object:Gem::Version
165
+ version: '1.13'
152
166
  - !ruby/object:Gem::Dependency
153
167
  name: redcarpet
154
168
  requirement: !ruby/object:Gem::Requirement
@@ -198,9 +212,9 @@ dependencies:
198
212
  - !ruby/object:Gem::Version
199
213
  version: '0.9'
200
214
  description: |
201
- Configures RSpec to fail (and exit with with a non-zero exitcode) if the
202
- test coverage is below the configured threshold and (optionally) list the
203
- lines of code not covered by tests.
215
+ Configures SimpleCov's line, branch, and method coverage thresholds for RSpec,
216
+ suppresses failures during RSpec dry runs, and (optionally) lists or summarizes
217
+ the lines, branches, and methods not covered by tests.
204
218
  email:
205
219
  - jcouball@yahoo.com
206
220
  executables: []
@@ -220,6 +234,8 @@ files:
220
234
  - README.md
221
235
  - Rakefile
222
236
  - lib/simplecov-rspec.rb
237
+ - lib/simplecov-rspec/list_uncovered_option.rb
238
+ - lib/simplecov-rspec/uncovered_report.rb
223
239
  - lib/simplecov-rspec/version.rb
224
240
  - package.json
225
241
  - release-please-config.json
@@ -230,8 +246,8 @@ metadata:
230
246
  allowed_push_host: https://rubygems.org
231
247
  homepage_uri: https://github.com/main-branch/simplecov-rspec
232
248
  source_code_uri: https://github.com/main-branch/simplecov-rspec
233
- documentation_uri: https://rubydoc.info/gems/simplecov-rspec/0.4.4
234
- changelog_uri: https://rubydoc.info/gems/simplecov-rspec/0.4.4/file/CHANGELOG.md
249
+ documentation_uri: https://rubydoc.info/gems/simplecov-rspec/1.0.0
250
+ changelog_uri: https://rubydoc.info/gems/simplecov-rspec/1.0.0/file/CHANGELOG.md
235
251
  rubygems_mfa_required: 'true'
236
252
  rdoc_options: []
237
253
  require_paths:
@@ -240,7 +256,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
240
256
  requirements:
241
257
  - - ">="
242
258
  - !ruby/object:Gem::Version
243
- version: 3.1.0
259
+ version: 3.2.0
244
260
  required_rubygems_version: !ruby/object:Gem::Requirement
245
261
  requirements:
246
262
  - - ">="
@@ -248,9 +264,9 @@ required_rubygems_version: !ruby/object:Gem::Requirement
248
264
  version: '0'
249
265
  requirements:
250
266
  - 'Platform: Mac, Linux, or Windows'
251
- - 'Ruby: MRI 3.1 or later, TruffleRuby 24 or later, or JRuby 9.4 or later'
252
- rubygems_version: 4.0.6
267
+ - 'Ruby: MRI 3.2 or later, TruffleRuby 24 or later, or JRuby 10 or later'
268
+ rubygems_version: 4.0.16
253
269
  specification_version: 4
254
- summary: Configure SimpleCov to fail RSpec if the test coverage falls below a given
255
- threshold
270
+ summary: Configure SimpleCov coverage thresholds and uncovered item reporting for
271
+ RSpec
256
272
  test_files: []