Skip to content

Commit b496b6f

Browse files
committed
Extract CoverageViolations module for threshold checks
The four ExitCodes::*Check classes and the JSON formatter's `errors` section each reimplemented the same logic: iterate configured thresholds, fetch per-criterion actuals, round via `SimpleCov.round_coverage`, and filter where `actual` fails the threshold. Centralize that in `SimpleCov::CoverageViolations` with four module methods (`minimum_overall`, `minimum_by_file`, `minimum_by_group`, `maximum_drop`). Each returns an array of canonical violation hashes with consistent `:criterion`/`:expected`/`:actual` keys (plus `:filename`/`:project_filename` or `:group_name`/`:maximum` as appropriate). Exit-code checks shrink to thin wrappers that render the violations; the JSON formatter's four error-formatting methods collapse to a single call + map each.
1 parent e3eb58f commit b496b6f

8 files changed

Lines changed: 134 additions & 198 deletions

lib/simplecov.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -478,6 +478,7 @@ def probably_running_parallel_tests?
478478
require_relative "simplecov/configuration"
479479
SimpleCov.extend SimpleCov::Configuration
480480
require_relative "simplecov/coverage_statistics"
481+
require_relative "simplecov/coverage_violations"
481482
require_relative "simplecov/exit_codes"
482483
require_relative "simplecov/profiles"
483484
require_relative "simplecov/source_file/line"
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
# frozen_string_literal: true
2+
3+
module SimpleCov
4+
# Computes coverage threshold violations for a given result. Shared by
5+
# the exit-code checks and the JSON formatter's `errors` section.
6+
#
7+
# Each method returns an array of violation hashes. All percents are
8+
# rounded via `SimpleCov.round_coverage` so downstream consumers don't
9+
# need to round again.
10+
module CoverageViolations
11+
class << self
12+
# @return [Array<Hash>] {:criterion, :expected, :actual}
13+
def minimum_overall(result, thresholds)
14+
thresholds.filter_map do |criterion, expected|
15+
actual = round(result.coverage_statistics.fetch(criterion).percent)
16+
{criterion: criterion, expected: expected, actual: actual} if actual < expected
17+
end
18+
end
19+
20+
# @return [Array<Hash>] {:criterion, :expected, :actual, :filename, :project_filename}
21+
def minimum_by_file(result, thresholds)
22+
thresholds.flat_map do |criterion, expected|
23+
result.files.filter_map { |file| file_minimum_violation(file, criterion, expected) }
24+
end
25+
end
26+
27+
# @return [Array<Hash>] {:group_name, :criterion, :expected, :actual}
28+
def minimum_by_group(result, thresholds)
29+
thresholds.flat_map do |group_name, minimums|
30+
group = lookup_group(result, group_name)
31+
group ? group_minimum_violations(group_name, group, minimums) : []
32+
end
33+
end
34+
35+
# @return [Array<Hash>] {:criterion, :maximum, :actual} where `actual`
36+
# is the observed drop (in percentage points) vs. the last run.
37+
def maximum_drop(result, thresholds, last_run: SimpleCov::LastRun.read)
38+
return [] unless last_run
39+
40+
thresholds.filter_map do |criterion, maximum|
41+
actual = compute_drop(criterion, result, last_run)
42+
{criterion: criterion, maximum: maximum, actual: actual} if actual && actual > maximum
43+
end
44+
end
45+
46+
private
47+
48+
def file_minimum_violation(file, criterion, expected)
49+
actual = round(file.coverage_statistics.fetch(criterion).percent)
50+
return unless actual < expected
51+
52+
{
53+
criterion: criterion,
54+
expected: expected,
55+
actual: actual,
56+
filename: file.filename,
57+
project_filename: file.project_filename
58+
}
59+
end
60+
61+
def group_minimum_violations(group_name, group, minimums)
62+
minimums.filter_map do |criterion, expected|
63+
actual = round(group.coverage_statistics.fetch(criterion).percent)
64+
{group_name: group_name, criterion: criterion, expected: expected, actual: actual} if actual < expected
65+
end
66+
end
67+
68+
def lookup_group(result, group_name)
69+
group = result.groups[group_name]
70+
warn "minimum_coverage_by_group: no group named '#{group_name}' exists. Available groups: #{result.groups.keys.join(', ')}" unless group
71+
group
72+
end
73+
74+
def compute_drop(criterion, result, last_run)
75+
last_coverage_percent = last_run.dig(:result, criterion)
76+
last_coverage_percent ||= last_run.dig(:result, :covered_percent) if criterion == :line
77+
return unless last_coverage_percent
78+
79+
current = round(result.coverage_statistics.fetch(criterion).percent)
80+
(last_coverage_percent - current).floor(10)
81+
end
82+
83+
def round(percent)
84+
SimpleCov.round_coverage(percent)
85+
end
86+
end
87+
end
88+
end

lib/simplecov/exit_codes/maximum_coverage_drop_check.rb

Lines changed: 7 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -3,26 +3,22 @@
33
module SimpleCov
44
module ExitCodes
55
class MaximumCoverageDropCheck
6-
MAX_DROP_ACCURACY = 10
7-
86
def initialize(result, maximum_coverage_drop)
97
@result = result
108
@maximum_coverage_drop = maximum_coverage_drop
119
end
1210

1311
def failing?
14-
return false unless maximum_coverage_drop && last_run
15-
16-
coverage_drop_violations.any?
12+
violations.any?
1713
end
1814

1915
def report
20-
coverage_drop_violations.each do |violation|
16+
violations.each do |violation|
2117
$stderr.printf(
2218
"%<criterion>s coverage has dropped by %<drop_percent>.2f%% since the last time (maximum allowed: %<max_drop>.2f%%).\n",
23-
criterion: violation[:criterion].capitalize,
24-
drop_percent: SimpleCov.round_coverage(violation[:drop_percent]),
25-
max_drop: violation[:max_drop]
19+
criterion: violation.fetch(:criterion).capitalize,
20+
drop_percent: violation.fetch(:actual),
21+
max_drop: violation.fetch(:maximum)
2622
)
2723
end
2824
end
@@ -33,50 +29,8 @@ def exit_code
3329

3430
private
3531

36-
attr_reader :result, :maximum_coverage_drop
37-
38-
def last_run
39-
return @last_run if defined?(@last_run)
40-
41-
@last_run = SimpleCov::LastRun.read
42-
end
43-
44-
def coverage_drop_violations
45-
@coverage_drop_violations ||=
46-
compute_coverage_drop_data.select do |achieved|
47-
achieved.fetch(:max_drop) < achieved.fetch(:drop_percent)
48-
end
49-
end
50-
51-
def compute_coverage_drop_data
52-
maximum_coverage_drop.map do |criterion, percent|
53-
{
54-
criterion: criterion,
55-
max_drop: percent,
56-
drop_percent: drop_percent(criterion)
57-
}
58-
end
59-
end
60-
61-
def drop_percent(criterion)
62-
drop = last_coverage(criterion) -
63-
SimpleCov.round_coverage(
64-
result.coverage_statistics.fetch(criterion).percent
65-
)
66-
67-
# floats, I tell ya.
68-
# irb(main):001:0* 80.01 - 80.0
69-
# => 0.010000000000005116
70-
drop.floor(MAX_DROP_ACCURACY)
71-
end
72-
73-
def last_coverage(criterion)
74-
last_coverage_percent = last_run[:result][criterion]
75-
76-
# fallback for old file format
77-
last_coverage_percent = last_run[:result][:covered_percent] if !last_coverage_percent && criterion == :line
78-
79-
last_coverage_percent || 0
32+
def violations
33+
@violations ||= SimpleCov::CoverageViolations.maximum_drop(@result, @maximum_coverage_drop)
8034
end
8135
end
8236
end

lib/simplecov/exit_codes/minimum_coverage_by_file_check.rb

Lines changed: 7 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,17 @@ def initialize(result, minimum_coverage_by_file)
99
end
1010

1111
def failing?
12-
minimum_violations.any?
12+
violations.any?
1313
end
1414

1515
def report
16-
minimum_violations.each do |violation|
16+
violations.each do |violation|
1717
$stderr.printf(
1818
"%<criterion>s coverage by file (%<covered>.2f%%) is below the expected minimum coverage (%<minimum_coverage>.2f%%) in %<filename>s.\n",
19-
covered: SimpleCov.round_coverage(violation.fetch(:actual)),
20-
minimum_coverage: violation.fetch(:minimum_expected),
19+
covered: violation.fetch(:actual),
20+
minimum_coverage: violation.fetch(:expected),
2121
criterion: violation.fetch(:criterion).capitalize,
22-
filename: violation.fetch(:filename)
22+
filename: violation.fetch(:project_filename)
2323
)
2424
end
2525
end
@@ -30,27 +30,8 @@ def exit_code
3030

3131
private
3232

33-
attr_reader :result, :minimum_coverage_by_file
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_by_file.flat_map do |criterion, expected_percent|
44-
result.files.map do |file|
45-
actual_coverage = file.coverage_statistics.fetch(criterion)
46-
{
47-
criterion: criterion,
48-
minimum_expected: expected_percent,
49-
actual: SimpleCov.round_coverage(actual_coverage.percent),
50-
filename: file.project_filename
51-
}
52-
end
53-
end
33+
def violations
34+
@violations ||= SimpleCov::CoverageViolations.minimum_by_file(@result, @minimum_coverage_by_file)
5435
end
5536
end
5637
end

lib/simplecov/exit_codes/minimum_coverage_by_group_check.rb

Lines changed: 6 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,16 @@ def initialize(result, minimum_coverage_by_group)
99
end
1010

1111
def failing?
12-
minimum_violations.any?
12+
violations.any?
1313
end
1414

1515
def report
16-
minimum_violations.each do |violation|
16+
violations.each do |violation|
1717
$stderr.printf(
1818
"%<criterion>s coverage by group (%<covered>.2f%%) is below the expected minimum coverage (%<minimum_coverage>.2f%%) in %<group_name>s.\n",
1919
group_name: violation.fetch(:group_name),
20-
covered: SimpleCov.round_coverage(violation.fetch(:actual)),
21-
minimum_coverage: violation.fetch(:minimum_expected),
20+
covered: violation.fetch(:actual),
21+
minimum_coverage: violation.fetch(:expected),
2222
criterion: violation.fetch(:criterion).capitalize
2323
)
2424
end
@@ -30,41 +30,8 @@ def exit_code
3030

3131
private
3232

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_by_group.flat_map do |group_name, minimum_group_coverage|
44-
group = find_group(group_name)
45-
next [] unless group
46-
47-
minimum_group_coverage.map do |criterion, expected_percent|
48-
actual_coverage = group.coverage_statistics.fetch(criterion)
49-
minimum_coverage_hash(group_name, criterion, expected_percent, SimpleCov.round_coverage(actual_coverage.percent))
50-
end
51-
end
52-
end
53-
54-
def find_group(group_name)
55-
result.groups[group_name] || begin
56-
warn "minimum_coverage_by_group: no group named '#{group_name}' exists. Available groups: #{result.groups.keys.join(', ')}"
57-
nil
58-
end
59-
end
60-
61-
def minimum_coverage_hash(group_name, criterion, minimum_expected, actual)
62-
{
63-
group_name: group_name,
64-
criterion: criterion,
65-
minimum_expected: minimum_expected,
66-
actual: actual
67-
}
33+
def violations
34+
@violations ||= SimpleCov::CoverageViolations.minimum_by_group(@result, @minimum_coverage_by_group)
6835
end
6936
end
7037
end

lib/simplecov/exit_codes/minimum_overall_coverage_check.rb

Lines changed: 6 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,15 @@ def initialize(result, minimum_coverage)
99
end
1010

1111
def failing?
12-
minimum_violations.any?
12+
violations.any?
1313
end
1414

1515
def report
16-
minimum_violations.each do |violation|
16+
violations.each do |violation|
1717
$stderr.printf(
1818
"%<criterion>s coverage (%<covered>.2f%%) is below the expected minimum coverage (%<minimum_coverage>.2f%%).\n",
19-
covered: SimpleCov.round_coverage(violation.fetch(:actual)),
20-
minimum_coverage: violation.fetch(:minimum_expected),
19+
covered: violation.fetch(:actual),
20+
minimum_coverage: violation.fetch(:expected),
2121
criterion: violation.fetch(:criterion).capitalize
2222
)
2323
end
@@ -29,24 +29,8 @@ def exit_code
2929

3030
private
3131

32-
attr_reader :result, :minimum_coverage
33-
34-
def minimum_violations
35-
@minimum_violations ||= calculate_minimum_violations
36-
end
37-
38-
def calculate_minimum_violations
39-
coverage_achieved = minimum_coverage.map do |criterion, percent|
40-
{
41-
criterion: criterion,
42-
minimum_expected: percent,
43-
actual: result.coverage_statistics.fetch(criterion).percent
44-
}
45-
end
46-
47-
coverage_achieved.select do |achieved|
48-
achieved.fetch(:actual) < achieved.fetch(:minimum_expected)
49-
end
32+
def violations
33+
@violations ||= SimpleCov::CoverageViolations.minimum_overall(@result, @minimum_coverage)
5034
end
5135
end
5236
end

0 commit comments

Comments
 (0)