Skip to content

Commit

Permalink
Implement branch coverage support for exit status modifiers
Browse files Browse the repository at this point in the history
  • Loading branch information
jemmaissroff committed Oct 7, 2020
1 parent dd35066 commit d61ae99
Show file tree
Hide file tree
Showing 11 changed files with 145 additions and 76 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
Unreleased
==========

## Enhancements
* Can now define the minimum_coverage_by_file, maximum_coverage_drop and refuse_coverage_drop by branch as well as line

0.19.0 (2020-08-16)
==========

Expand Down
18 changes: 12 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -786,30 +786,36 @@ to help ensure coverage is relatively consistent, rather than being skewed by pa

```ruby
SimpleCov.minimum_coverage_by_file 80
# same as above (the default is to check line coverage by file)
SimpleCov.minimum_coverage_by_file line: 80
# check for a minimum line coverage by file of 90% and minimum 80% branch coverage
SimpleCov.minimum_coverage_by_file line: 90, branch: 80
```

(not yet supported for branch coverage)

### Maximum coverage drop

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

```ruby
SimpleCov.maximum_coverage_drop 5
# same as above (the default is to check line drop)
SimpleCov.maximum_coverage_drop line: 5
# check for a maximum line drop of 5% and maximum 10% branch drop
SimpleCov.maximum_coverage_drop line: 5, branch: 10
```

(not yet supported for branch coverage)

### Refuse dropping coverage

You can also entirely refuse dropping coverage between test runs:

```ruby
SimpleCov.refuse_coverage_drop
# same as above (the default is to only refuse line drop)
SimpleCov.refuse_coverage_drop :line
# refuse drop for line and branch
SimpleCov.refuse_coverage_drop :line, :branch
```

(not yet supported for branch coverage)

## Using your own formatter

You can use your own formatter with:
Expand Down
10 changes: 5 additions & 5 deletions features/maximum_coverage_drop.feature
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ Feature:
"""
{
"result": {
"covered_percent": 88.09
"line": 88.09
}
}
"""
Expand All @@ -61,7 +61,7 @@ Feature:
"""
{
"result": {
"covered_percent": 88.09
"line": 88.09
}
}
"""
Expand All @@ -84,7 +84,7 @@ Feature:
"""
{
"result": {
"covered_percent": 84.78
"line": 84.78
}
}
"""
Expand Down Expand Up @@ -123,7 +123,7 @@ Feature:
"""
{
"result": {
"covered_percent": 100.0
"line": 100.0
}
}
"""
Expand All @@ -135,7 +135,7 @@ Feature:
"""
{
"result": {
"covered_percent": 100.0
"line": 100.0
}
}
"""
8 changes: 4 additions & 4 deletions features/refuse_coverage_drop.feature
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ Feature:
"""
{
"result": {
"covered_percent": 88.09
"line": 88.09
}
}
"""
Expand All @@ -48,7 +48,7 @@ Feature:
"""
{
"result": {
"covered_percent": 88.09
"line": 88.09
}
}
"""
Expand All @@ -69,7 +69,7 @@ Feature:
"""
{
"result": {
"covered_percent": 88.09
"line": 88.09
}
}
"""
Expand All @@ -92,7 +92,7 @@ Feature:
"""
{
"result": {
"covered_percent": 84.78
"line": 84.78
}
}
"""
5 changes: 4 additions & 1 deletion lib/simplecov.rb
Original file line number Diff line number Diff line change
Expand Up @@ -285,7 +285,10 @@ def wait_for_other_processes
# @api private
#
def write_last_run(result)
SimpleCov::LastRun.write(result: {covered_percent: round_coverage(result.covered_percent)})
SimpleCov::LastRun.write(result:
result.coverage_statistics.transform_values do |stats|
round_coverage(stats.percent)
end)
end

#
Expand Down
35 changes: 24 additions & 11 deletions lib/simplecov/configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -285,20 +285,22 @@ def merge_timeout(seconds = nil)
#
# Default is 0% (disabled)
#

# rubocop:disable Metrics/CyclomaticComplexity
def minimum_coverage(coverage = nil)
return @minimum_coverage ||= {} unless coverage

coverage = {DEFAULT_COVERAGE_CRITERION => coverage} if coverage.is_a?(Numeric)

validate_coverage!(coverage, "minimum_coverage")

@minimum_coverage = coverage
end

def validate_coverage!(coverage, coverage_setting)
coverage.each_key { |criterion| raise_if_criterion_disabled(criterion) }
coverage.each_value do |percent|
minimum_possible_coverage_exceeded("minimum_coverage") if percent && percent > 100
minimum_possible_coverage_exceeded(coverage_setting) if percent && percent > 100
end

@minimum_coverage = coverage
end
# rubocop:enable Metrics/CyclomaticComplexity

#
# Defines the maximum coverage drop at once allowed for the testsuite to pass.
Expand All @@ -307,7 +309,13 @@ def minimum_coverage(coverage = nil)
# Default is 100% (disabled)
#
def maximum_coverage_drop(coverage_drop = nil)
@maximum_coverage_drop ||= (coverage_drop || 100).to_f.round(2)
return @maximum_coverage_drop ||= {} unless coverage_drop

coverage_drop = {DEFAULT_COVERAGE_CRITERION => coverage_drop} if coverage_drop.is_a?(Numeric)

validate_coverage!(coverage_drop, "maximum_coverage_drop")

@maximum_coverage_drop = coverage_drop
end

#
Expand All @@ -318,16 +326,21 @@ def maximum_coverage_drop(coverage_drop = nil)
# Default is 0% (disabled)
#
def minimum_coverage_by_file(coverage = nil)
minimum_possible_coverage_exceeded("minimum_coverage_by_file") if coverage && coverage > 100
@minimum_coverage_by_file ||= (coverage || 0).to_f.round(2)
return @minimum_coverage_by_file ||= {} unless coverage

coverage = {DEFAULT_COVERAGE_CRITERION => coverage} if coverage.is_a?(Numeric)

validate_coverage!(coverage, "minimum_coverage_by_file")

@minimum_coverage_by_file = coverage
end

#
# Refuses any coverage drop. That is, coverage is only allowed to increase.
# SimpleCov will return non-zero if the coverage decreases.
#
def refuse_coverage_drop
maximum_coverage_drop 0
def refuse_coverage_drop(criteria = [DEFAULT_COVERAGE_CRITERION])
maximum_coverage_drop(criteria.map { |c| [c, 0] }.to_h)
end

#
Expand Down
36 changes: 24 additions & 12 deletions lib/simplecov/exit_codes/maximum_coverage_drop_check.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,17 @@ def initialize(result, maximum_coverage_drop)
def failing?
return false unless maximum_coverage_drop && last_run

coverage_diff > maximum_coverage_drop
coverage_drop_violations.any?
end

def report
$stderr.printf(
"Coverage has dropped by %<drop_percent>.2f%% since the last time (maximum allowed: %<max_drop>.2f%%).\n",
drop_percent: coverage_diff,
max_drop: maximum_coverage_drop
)
coverage_drop_violations.each do |violation|
$stderr.printf(
"Coverage has dropped by %<drop_percent>.2f%% since the last time (maximum allowed: %<max_drop>.2f%%).\n",
drop_percent: SimpleCov.round_coverage(violation[:drop_percent]),
max_drop: violation[:max_drop]
)
end
end

def exit_code
Expand All @@ -36,14 +38,24 @@ def last_run
@last_run = SimpleCov::LastRun.read
end

def coverage_diff
raise "Trying to access coverage_diff although there is no last run" unless last_run

@coverage_diff ||= last_run[:result][:covered_percent] - covered_percent
def coverage_drop_violations
@coverage_drop_violations ||=
compute_coverage_drop_violations.select do |achieved|
achieved.fetch(:max_drop) < achieved.fetch(:drop_percent)
end
end

def covered_percent
SimpleCov.round_coverage(result.covered_percent)
def compute_coverage_drop_violations
maximum_coverage_drop.map do |criterion, percent|
{
criterion: criterion,
max_drop: percent,
drop_percent: last_run[:result][criterion] -
SimpleCov.round_coverage(
result.coverage_statistics.fetch(criterion).percent
)
}
end
end
end
end
Expand Down
43 changes: 33 additions & 10 deletions lib/simplecov/exit_codes/minimum_coverage_by_file_check.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,18 @@ def initialize(result, minimum_coverage_by_file)
end

def failing?
covered_percentages.any? { |p| p < minimum_coverage_by_file }
minimum_violations.any?
end

def report
$stderr.printf(
"File (%<file>s) is only (%<least_covered_percentage>.2f%%) covered. This is below the expected minimum coverage per file of (%<min_coverage>.2f%%).\n",
file: result.least_covered_file,
least_covered_percentage: covered_percentages.min,
min_coverage: minimum_coverage_by_file
)
minimum_violations.each do |violation|
$stderr.printf(
"%<criterion>s coverage (%<covered>.2f%%) is below the expected minimum coverage (%<minimum_coverage>.2f%%).\n",
covered: SimpleCov.round_coverage(violation.fetch(:actual)),
minimum_coverage: violation.fetch(:minimum_expected),
criterion: violation.fetch(:criterion).capitalize
)
end
end

def exit_code
Expand All @@ -29,9 +31,30 @@ def exit_code

attr_reader :result, :minimum_coverage_by_file

def covered_percentages
@covered_percentages ||=
result.covered_percentages.map { |percentage| SimpleCov.round_coverage(percentage) }
def coverage_statistics_by_file
@coverage_statistics_by_file ||=
(res = result.coverage_statistics_by_file).each do |criteria, stats|
res[criteria] = stats.map { |stat| SimpleCov.round_coverage(stat.percent) }
end
end

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

def compute_minimum_violations
minimum_coverage_by_file.flat_map do |criterion, expected_percent|
coverage_statistics_by_file[criterion].map do |actual_percent|
{
criterion: criterion,
minimum_expected: expected_percent,
actual: actual_percent
}
end
end
end
end
end
Expand Down
14 changes: 10 additions & 4 deletions lib/simplecov/file_list.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ def coverage_statistics
@coverage_statistics ||= compute_coverage_statistics
end

def coverage_statistics_by_file
@coverage_statistics_by_file ||= compute_coverage_statistics_by_file
end

# Returns the count of lines that have coverage
def covered_lines
coverage_statistics[:line]&.covered
Expand Down Expand Up @@ -100,14 +104,16 @@ def branch_covered_percent

private

def compute_coverage_statistics
total_coverage_statistics = @files.each_with_object(line: [], branch: []) do |file, together|
def compute_coverage_statistics_by_file
@files.each_with_object(line: [], branch: []) do |file, together|
together[:line] << file.coverage_statistics[:line]
together[:branch] << file.coverage_statistics[:branch] if SimpleCov.branch_coverage?
end
end

coverage_statistics = {line: CoverageStatistics.from(total_coverage_statistics[:line])}
coverage_statistics[:branch] = CoverageStatistics.from(total_coverage_statistics[:branch]) if SimpleCov.branch_coverage?
def compute_coverage_statistics
coverage_statistics = {line: CoverageStatistics.from(coverage_statistics_by_file[:line])}
coverage_statistics[:branch] = CoverageStatistics.from(coverage_statistics_by_file[:branch]) if SimpleCov.branch_coverage?
coverage_statistics
end
end
Expand Down
2 changes: 1 addition & 1 deletion lib/simplecov/result.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ class Result
# Explicitly set the command name that was used for this coverage result. Defaults to SimpleCov.command_name
attr_writer :command_name

def_delegators :files, :covered_percent, :covered_percentages, :least_covered_file, :covered_strength, :covered_lines, :missed_lines, :total_branches, :covered_branches, :missed_branches, :coverage_statistics
def_delegators :files, :covered_percent, :covered_percentages, :least_covered_file, :covered_strength, :covered_lines, :missed_lines, :total_branches, :covered_branches, :missed_branches, :coverage_statistics, :coverage_statistics_by_file
def_delegator :files, :lines_of_code, :total_lines

# Initialize a new SimpleCov::Result from given Coverage.result (a Hash of filenames each containing an array of
Expand Down
Loading

0 comments on commit d61ae99

Please sign in to comment.