Skip to content

Commit 4846449

Browse files
committed
Added minimum coverage by group check
1 parent c7102e4 commit 4846449

8 files changed

+203
-2
lines changed

README.md

+13
Original file line numberDiff line numberDiff line change
@@ -818,6 +818,19 @@ SimpleCov.minimum_coverage_by_file line: 80
818818
SimpleCov.minimum_coverage_by_file line: 90, branch: 80
819819
```
820820

821+
### Minimum coverage by group
822+
823+
You can define the minimum coverage percentage expected for specific groups. SimpleCov will return non-zero if unmet,
824+
ensuring that coverage is consistent across different parts of your codebase.
825+
826+
```ruby
827+
SimpleCov.minimum_coverage_by_group 'Models' => 80, 'Controllers' => 60
828+
# same as above (the default is to check line coverage)
829+
SimpleCov.minimum_coverage_by_group 'Models' => { line: 80 }, 'Controllers' => { line: 60 }
830+
# check for a minimum line and branch coverage for 'Models' and 'Controllers' groups
831+
SimpleCov.minimum_coverage_by_group 'Models' => { line: 90, branch: 80 }, 'Controllers' => { line: 60, branch: 50 }
832+
```
833+
821834
### Maximum coverage drop
822835

823836
You can define the maximum coverage drop percentage at once. SimpleCov will return non-zero if exceeded.

lib/simplecov.rb

+2-2
Original file line numberDiff line numberDiff line change
@@ -252,11 +252,11 @@ def process_result(result)
252252
end
253253

254254
# @api private
255-
CoverageLimits = Struct.new(:minimum_coverage, :minimum_coverage_by_file, :maximum_coverage_drop, keyword_init: true)
255+
CoverageLimits = Struct.new(:minimum_coverage, :minimum_coverage_by_file, :minimum_coverage_by_group, :maximum_coverage_drop, keyword_init: true)
256256
def result_exit_status(result)
257257
coverage_limits = CoverageLimits.new(
258258
minimum_coverage: minimum_coverage, minimum_coverage_by_file: minimum_coverage_by_file,
259-
maximum_coverage_drop: maximum_coverage_drop
259+
minimum_coverage_by_group: minimum_coverage_by_group, maximum_coverage_drop: maximum_coverage_drop
260260
)
261261

262262
ExitCodes::ExitCodeHandling.call(result, coverage_limits: coverage_limits)

lib/simplecov/configuration.rb

+19
Original file line numberDiff line numberDiff line change
@@ -337,6 +337,25 @@ def minimum_coverage_by_file(coverage = nil)
337337
@minimum_coverage_by_file = coverage
338338
end
339339

340+
#
341+
# Defines the minimum coverage per group required for the testsuite to pass.
342+
# SimpleCov will return non-zero if the current coverage of the least covered group
343+
# is below this threshold.
344+
#
345+
# Default is 0% (disabled)
346+
#
347+
def minimum_coverage_by_group(coverage = nil)
348+
return @minimum_coverage_by_group ||= {} unless coverage
349+
350+
@minimum_coverage_by_group = coverage.dup.transform_values do |group_coverage|
351+
group_coverage = {primary_coverage => group_coverage} if group_coverage.is_a?(Numeric)
352+
353+
raise_on_invalid_coverage(group_coverage, "minimum_coverage_by_group")
354+
355+
group_coverage
356+
end
357+
end
358+
340359
#
341360
# Refuses any coverage drop. That is, coverage is only allowed to increase.
342361
# SimpleCov will return non-zero if the coverage decreases.

lib/simplecov/exit_codes.rb

+1
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,5 @@ module ExitCodes
1212
require_relative "exit_codes/exit_code_handling"
1313
require_relative "exit_codes/maximum_coverage_drop_check"
1414
require_relative "exit_codes/minimum_coverage_by_file_check"
15+
require_relative "exit_codes/minimum_coverage_by_group_check"
1516
require_relative "exit_codes/minimum_overall_coverage_check"

lib/simplecov/exit_codes/exit_code_handling.rb

+1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ def coverage_checks(result, coverage_limits)
2121
[
2222
MinimumOverallCoverageCheck.new(result, coverage_limits.minimum_coverage),
2323
MinimumCoverageByFileCheck.new(result, coverage_limits.minimum_coverage_by_file),
24+
MinimumCoverageByGroupCheck.new(result, coverage_limits.minimum_coverage_by_group),
2425
MaximumCoverageDropCheck.new(result, coverage_limits.maximum_coverage_drop)
2526
]
2627
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
# frozen_string_literal: true
2+
3+
module SimpleCov
4+
module ExitCodes
5+
class MinimumCoverageByGroupCheck
6+
def initialize(result, minimum_coverage_by_group)
7+
@result = result
8+
@minimum_coverage_by_group = minimum_coverage_by_group
9+
end
10+
11+
def failing?
12+
minimum_violations.any?
13+
end
14+
15+
def report
16+
minimum_violations.each do |violation|
17+
$stderr.printf(
18+
"%<criterion>s coverage by group %<group_name>s (%<covered>.2f%%) is below the expected minimum coverage (%<minimum_coverage>.2f%%).\n",
19+
group_name: violation.fetch(:group_name),
20+
covered: SimpleCov.round_coverage(violation.fetch(:actual)),
21+
minimum_coverage: violation.fetch(:minimum_expected),
22+
criterion: violation.fetch(:criterion).capitalize
23+
)
24+
end
25+
end
26+
27+
def exit_code
28+
SimpleCov::ExitCodes::MINIMUM_COVERAGE
29+
end
30+
31+
private
32+
33+
attr_reader :result, :minimum_coverage_by_group
34+
35+
def minimum_violations
36+
@minimum_violations ||=
37+
compute_minimum_coverage_data.select do |achieved|
38+
achieved.fetch(:actual) < achieved.fetch(:minimum_expected)
39+
end
40+
end
41+
42+
def compute_minimum_coverage_data
43+
minimum_coverage_data = []
44+
45+
minimum_coverage_by_group.each do |group_name, minimum_group_coverage|
46+
minimum_group_coverage.each do |criterion, expected_percent|
47+
actual_coverage = result.groups.fetch(group_name).coverage_statistics.fetch(criterion)
48+
minimum_coverage_data << minimum_coverage_hash(group_name, criterion, expected_percent, SimpleCov.round_coverage(actual_coverage.percent))
49+
end
50+
end
51+
52+
minimum_coverage_data
53+
end
54+
55+
def minimum_coverage_hash(group_name, criterion, minimum_expected, actual)
56+
{
57+
group_name: group_name,
58+
criterion: criterion,
59+
minimum_expected: minimum_expected,
60+
actual: actual
61+
}
62+
end
63+
end
64+
end
65+
end

spec/configuration_spec.rb

+67
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,73 @@
124124
it_behaves_like "setting coverage expectations", :minimum_coverage_by_file
125125
end
126126

127+
describe "#minimum_coverage_by_group" do
128+
after do
129+
config.clear_coverage_criteria
130+
end
131+
132+
it "does not warn you about your usage" do
133+
expect(config).not_to receive(:warn)
134+
config.minimum_coverage_by_group({"Test Group 1" => 100.00})
135+
end
136+
137+
it "warns you about your usage" do
138+
expect(config).to receive(:warn).with("The coverage you set for minimum_coverage_by_group is greater than 100%")
139+
config.minimum_coverage_by_group({"Test Group 1" => 100.01})
140+
end
141+
142+
it "sets the right coverage value when called with a number" do
143+
config.minimum_coverage_by_group({"Test Group 1" => 80})
144+
145+
expect(config.minimum_coverage_by_group).to eq({"Test Group 1" => {line: 80}})
146+
end
147+
148+
it "sets the right coverage when called with a hash of just line" do
149+
config.minimum_coverage_by_group({"Test Group 1" => {line: 85.0}})
150+
151+
expect(config.minimum_coverage_by_group).to eq({"Test Group 1" => {line: 85.0}})
152+
end
153+
154+
it "sets the right coverage when called with a hash of just branch" do
155+
config.enable_coverage :branch
156+
config.minimum_coverage_by_group({"Test Group 1" => {branch: 85.0}})
157+
158+
expect(config.minimum_coverage_by_group).to eq({"Test Group 1" => {branch: 85.0}})
159+
end
160+
161+
it "sets the right coverage when called with both line and branch" do
162+
config.enable_coverage :branch
163+
config.minimum_coverage_by_group({"Test Group 1" => {branch: 85.0, line: 95.4}})
164+
165+
expect(config.minimum_coverage_by_group).to eq({"Test Group 1" => {branch: 85.0, line: 95.4}})
166+
end
167+
168+
it "raises when trying to set branch coverage but not enabled" do
169+
expect do
170+
config.minimum_coverage_by_group({"Test Group 1" => {branch: 42}})
171+
end.to raise_error(/branch.*disabled/i)
172+
end
173+
174+
it "raises when unknown coverage criteria provided" do
175+
expect do
176+
config.minimum_coverage_by_group({"Test Group 1" => {unknown: 42}})
177+
end.to raise_error(/unsupported.*unknown/i)
178+
end
179+
180+
context "when primary coverage is set" do
181+
before do
182+
config.enable_coverage :branch
183+
config.primary_coverage :branch
184+
end
185+
186+
it "sets the right coverage value when called with a number" do
187+
config.minimum_coverage_by_group({"Test Group 1" => 80})
188+
189+
expect(config.minimum_coverage_by_group).to eq({"Test Group 1" => {branch: 80}})
190+
end
191+
end
192+
end
193+
127194
describe "#maximum_coverage_drop" do
128195
it_behaves_like "setting coverage expectations", :maximum_coverage_drop
129196
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# frozen_string_literal: true
2+
3+
require "helper"
4+
5+
RSpec.describe SimpleCov::ExitCodes::MinimumCoverageByGroupCheck do
6+
subject { described_class.new(result, minimum_coverage_by_group) }
7+
8+
let(:coverage_statistics) { {line: SimpleCov::CoverageStatistics.new(covered: 8, missed: 2), branch: SimpleCov::CoverageStatistics.new(covered: 8, missed: 2)} }
9+
let(:result) { instance_double(SimpleCov::Result, groups: {"Test Group 1" => instance_double(SimpleCov::FileList, coverage_statistics: coverage_statistics)}) }
10+
let(:stats) { {"Test Group 1" => coverage_statistics} }
11+
12+
context "everything exactly ok" do
13+
let(:minimum_coverage_by_group) { {"Test Group 1" => {line: 80.0}} }
14+
15+
it { is_expected.not_to be_failing }
16+
end
17+
18+
context "coverage violated" do
19+
let(:minimum_coverage_by_group) { {"Test Group 1" => {line: 90.0}} }
20+
21+
it { is_expected.to be_failing }
22+
end
23+
24+
context "coverage slightly violated" do
25+
let(:minimum_coverage_by_group) { {"Test Group 1" => {line: 80.01}} }
26+
27+
it { is_expected.to be_failing }
28+
end
29+
30+
context "one criterion violated" do
31+
let(:minimum_coverage_by_group) { {"Test Group 1" => {line: 80.0, branch: 90.0}} }
32+
33+
it { is_expected.to be_failing }
34+
end
35+
end

0 commit comments

Comments
 (0)