Skip to content

Commit

Permalink
Added minimum coverage by group check
Browse files Browse the repository at this point in the history
  • Loading branch information
mikhliuk-k committed Sep 2, 2024
1 parent c7102e4 commit 4846449
Show file tree
Hide file tree
Showing 8 changed files with 203 additions and 2 deletions.
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -818,6 +818,19 @@ SimpleCov.minimum_coverage_by_file line: 80
SimpleCov.minimum_coverage_by_file line: 90, branch: 80
```

### Minimum coverage by group

You can define the minimum coverage percentage expected for specific groups. SimpleCov will return non-zero if unmet,
ensuring that coverage is consistent across different parts of your codebase.

```ruby
SimpleCov.minimum_coverage_by_group 'Models' => 80, 'Controllers' => 60
# same as above (the default is to check line coverage)
SimpleCov.minimum_coverage_by_group 'Models' => { line: 80 }, 'Controllers' => { line: 60 }
# check for a minimum line and branch coverage for 'Models' and 'Controllers' groups
SimpleCov.minimum_coverage_by_group 'Models' => { line: 90, branch: 80 }, 'Controllers' => { line: 60, branch: 50 }
```

### Maximum coverage drop

You can define the maximum coverage drop percentage at once. SimpleCov will return non-zero if exceeded.
Expand Down
4 changes: 2 additions & 2 deletions lib/simplecov.rb
Original file line number Diff line number Diff line change
Expand Up @@ -252,11 +252,11 @@ def process_result(result)
end

# @api private
CoverageLimits = Struct.new(:minimum_coverage, :minimum_coverage_by_file, :maximum_coverage_drop, keyword_init: true)
CoverageLimits = Struct.new(:minimum_coverage, :minimum_coverage_by_file, :minimum_coverage_by_group, :maximum_coverage_drop, keyword_init: true)
def result_exit_status(result)
coverage_limits = CoverageLimits.new(
minimum_coverage: minimum_coverage, minimum_coverage_by_file: minimum_coverage_by_file,
maximum_coverage_drop: maximum_coverage_drop
minimum_coverage_by_group: minimum_coverage_by_group, maximum_coverage_drop: maximum_coverage_drop
)

ExitCodes::ExitCodeHandling.call(result, coverage_limits: coverage_limits)
Expand Down
19 changes: 19 additions & 0 deletions lib/simplecov/configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,25 @@ def minimum_coverage_by_file(coverage = nil)
@minimum_coverage_by_file = coverage
end

#
# Defines the minimum coverage per group required for the testsuite to pass.
# SimpleCov will return non-zero if the current coverage of the least covered group
# is below this threshold.
#
# Default is 0% (disabled)
#
def minimum_coverage_by_group(coverage = nil)
return @minimum_coverage_by_group ||= {} unless coverage

@minimum_coverage_by_group = coverage.dup.transform_values do |group_coverage|
group_coverage = {primary_coverage => group_coverage} if group_coverage.is_a?(Numeric)

raise_on_invalid_coverage(group_coverage, "minimum_coverage_by_group")

group_coverage
end
end

#
# Refuses any coverage drop. That is, coverage is only allowed to increase.
# SimpleCov will return non-zero if the coverage decreases.
Expand Down
1 change: 1 addition & 0 deletions lib/simplecov/exit_codes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,5 @@ module ExitCodes
require_relative "exit_codes/exit_code_handling"
require_relative "exit_codes/maximum_coverage_drop_check"
require_relative "exit_codes/minimum_coverage_by_file_check"
require_relative "exit_codes/minimum_coverage_by_group_check"
require_relative "exit_codes/minimum_overall_coverage_check"
1 change: 1 addition & 0 deletions lib/simplecov/exit_codes/exit_code_handling.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ def coverage_checks(result, coverage_limits)
[
MinimumOverallCoverageCheck.new(result, coverage_limits.minimum_coverage),
MinimumCoverageByFileCheck.new(result, coverage_limits.minimum_coverage_by_file),
MinimumCoverageByGroupCheck.new(result, coverage_limits.minimum_coverage_by_group),
MaximumCoverageDropCheck.new(result, coverage_limits.maximum_coverage_drop)
]
end
Expand Down
65 changes: 65 additions & 0 deletions lib/simplecov/exit_codes/minimum_coverage_by_group_check.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# frozen_string_literal: true

module SimpleCov
module ExitCodes
class MinimumCoverageByGroupCheck
def initialize(result, minimum_coverage_by_group)
@result = result
@minimum_coverage_by_group = minimum_coverage_by_group
end

def failing?
minimum_violations.any?
end

def report
minimum_violations.each do |violation|
$stderr.printf(
"%<criterion>s coverage by group %<group_name>s (%<covered>.2f%%) is below the expected minimum coverage (%<minimum_coverage>.2f%%).\n",
group_name: violation.fetch(:group_name),
covered: SimpleCov.round_coverage(violation.fetch(:actual)),
minimum_coverage: violation.fetch(:minimum_expected),
criterion: violation.fetch(:criterion).capitalize
)
end
end

def exit_code
SimpleCov::ExitCodes::MINIMUM_COVERAGE
end

private

attr_reader :result, :minimum_coverage_by_group

def minimum_violations
@minimum_violations ||=
compute_minimum_coverage_data.select do |achieved|
achieved.fetch(:actual) < achieved.fetch(:minimum_expected)
end
end

def compute_minimum_coverage_data
minimum_coverage_data = []

minimum_coverage_by_group.each do |group_name, minimum_group_coverage|
minimum_group_coverage.each do |criterion, expected_percent|
actual_coverage = result.groups.fetch(group_name).coverage_statistics.fetch(criterion)
minimum_coverage_data << minimum_coverage_hash(group_name, criterion, expected_percent, SimpleCov.round_coverage(actual_coverage.percent))
end
end

minimum_coverage_data
end

def minimum_coverage_hash(group_name, criterion, minimum_expected, actual)
{
group_name: group_name,
criterion: criterion,
minimum_expected: minimum_expected,
actual: actual
}
end
end
end
end
67 changes: 67 additions & 0 deletions spec/configuration_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,73 @@
it_behaves_like "setting coverage expectations", :minimum_coverage_by_file
end

describe "#minimum_coverage_by_group" do
after do
config.clear_coverage_criteria
end

it "does not warn you about your usage" do
expect(config).not_to receive(:warn)
config.minimum_coverage_by_group({"Test Group 1" => 100.00})
end

it "warns you about your usage" do
expect(config).to receive(:warn).with("The coverage you set for minimum_coverage_by_group is greater than 100%")
config.minimum_coverage_by_group({"Test Group 1" => 100.01})
end

it "sets the right coverage value when called with a number" do
config.minimum_coverage_by_group({"Test Group 1" => 80})

expect(config.minimum_coverage_by_group).to eq({"Test Group 1" => {line: 80}})
end

it "sets the right coverage when called with a hash of just line" do
config.minimum_coverage_by_group({"Test Group 1" => {line: 85.0}})

expect(config.minimum_coverage_by_group).to eq({"Test Group 1" => {line: 85.0}})
end

it "sets the right coverage when called with a hash of just branch" do
config.enable_coverage :branch
config.minimum_coverage_by_group({"Test Group 1" => {branch: 85.0}})

expect(config.minimum_coverage_by_group).to eq({"Test Group 1" => {branch: 85.0}})
end

it "sets the right coverage when called with both line and branch" do
config.enable_coverage :branch
config.minimum_coverage_by_group({"Test Group 1" => {branch: 85.0, line: 95.4}})

expect(config.minimum_coverage_by_group).to eq({"Test Group 1" => {branch: 85.0, line: 95.4}})
end

it "raises when trying to set branch coverage but not enabled" do
expect do
config.minimum_coverage_by_group({"Test Group 1" => {branch: 42}})
end.to raise_error(/branch.*disabled/i)
end

it "raises when unknown coverage criteria provided" do
expect do
config.minimum_coverage_by_group({"Test Group 1" => {unknown: 42}})
end.to raise_error(/unsupported.*unknown/i)
end

context "when primary coverage is set" do
before do
config.enable_coverage :branch
config.primary_coverage :branch
end

it "sets the right coverage value when called with a number" do
config.minimum_coverage_by_group({"Test Group 1" => 80})

expect(config.minimum_coverage_by_group).to eq({"Test Group 1" => {branch: 80}})
end
end
end

describe "#maximum_coverage_drop" do
it_behaves_like "setting coverage expectations", :maximum_coverage_drop
end
Expand Down
35 changes: 35 additions & 0 deletions spec/exit_codes/minimum_coverage_by_group_check_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# frozen_string_literal: true

require "helper"

RSpec.describe SimpleCov::ExitCodes::MinimumCoverageByGroupCheck do
subject { described_class.new(result, minimum_coverage_by_group) }

let(:coverage_statistics) { {line: SimpleCov::CoverageStatistics.new(covered: 8, missed: 2), branch: SimpleCov::CoverageStatistics.new(covered: 8, missed: 2)} }
let(:result) { instance_double(SimpleCov::Result, groups: {"Test Group 1" => instance_double(SimpleCov::FileList, coverage_statistics: coverage_statistics)}) }
let(:stats) { {"Test Group 1" => coverage_statistics} }

context "everything exactly ok" do
let(:minimum_coverage_by_group) { {"Test Group 1" => {line: 80.0}} }

it { is_expected.not_to be_failing }
end

context "coverage violated" do
let(:minimum_coverage_by_group) { {"Test Group 1" => {line: 90.0}} }

it { is_expected.to be_failing }
end

context "coverage slightly violated" do
let(:minimum_coverage_by_group) { {"Test Group 1" => {line: 80.01}} }

it { is_expected.to be_failing }
end

context "one criterion violated" do
let(:minimum_coverage_by_group) { {"Test Group 1" => {line: 80.0, branch: 90.0}} }

it { is_expected.to be_failing }
end
end

0 comments on commit 4846449

Please sign in to comment.