From d61ae99c87968130346f8c1ae7d1d41b559910ba Mon Sep 17 00:00:00 2001 From: Jemma Issroff Date: Wed, 7 Oct 2020 15:46:56 -0400 Subject: [PATCH] Implement branch coverage support for exit status modifiers --- CHANGELOG.md | 6 +++ README.md | 18 +++++--- features/maximum_coverage_drop.feature | 10 ++--- features/refuse_coverage_drop.feature | 8 ++-- lib/simplecov.rb | 5 ++- lib/simplecov/configuration.rb | 35 ++++++++++----- .../exit_codes/maximum_coverage_drop_check.rb | 36 ++++++++++----- .../minimum_coverage_by_file_check.rb | 43 +++++++++++++----- lib/simplecov/file_list.rb | 14 ++++-- lib/simplecov/result.rb | 2 +- spec/configuration_spec.rb | 44 +++++++++---------- 11 files changed, 145 insertions(+), 76 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dd2ff4bc..f94d93e5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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) ========== diff --git a/README.md b/README.md index a30426b8..d84d02d4 100644 --- a/README.md +++ b/README.md @@ -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: diff --git a/features/maximum_coverage_drop.feature b/features/maximum_coverage_drop.feature index 66a3b4a2..d4cac399 100644 --- a/features/maximum_coverage_drop.feature +++ b/features/maximum_coverage_drop.feature @@ -40,7 +40,7 @@ Feature: """ { "result": { - "covered_percent": 88.09 + "line": 88.09 } } """ @@ -61,7 +61,7 @@ Feature: """ { "result": { - "covered_percent": 88.09 + "line": 88.09 } } """ @@ -84,7 +84,7 @@ Feature: """ { "result": { - "covered_percent": 84.78 + "line": 84.78 } } """ @@ -123,7 +123,7 @@ Feature: """ { "result": { - "covered_percent": 100.0 + "line": 100.0 } } """ @@ -135,7 +135,7 @@ Feature: """ { "result": { - "covered_percent": 100.0 + "line": 100.0 } } """ diff --git a/features/refuse_coverage_drop.feature b/features/refuse_coverage_drop.feature index f4be179b..c25c847a 100644 --- a/features/refuse_coverage_drop.feature +++ b/features/refuse_coverage_drop.feature @@ -24,7 +24,7 @@ Feature: """ { "result": { - "covered_percent": 88.09 + "line": 88.09 } } """ @@ -48,7 +48,7 @@ Feature: """ { "result": { - "covered_percent": 88.09 + "line": 88.09 } } """ @@ -69,7 +69,7 @@ Feature: """ { "result": { - "covered_percent": 88.09 + "line": 88.09 } } """ @@ -92,7 +92,7 @@ Feature: """ { "result": { - "covered_percent": 84.78 + "line": 84.78 } } """ diff --git a/lib/simplecov.rb b/lib/simplecov.rb index 4d4ccfff..5391b163 100644 --- a/lib/simplecov.rb +++ b/lib/simplecov.rb @@ -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 # diff --git a/lib/simplecov/configuration.rb b/lib/simplecov/configuration.rb index 94f662c9..5d31ca7c 100644 --- a/lib/simplecov/configuration.rb +++ b/lib/simplecov/configuration.rb @@ -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. @@ -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 # @@ -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 # diff --git a/lib/simplecov/exit_codes/maximum_coverage_drop_check.rb b/lib/simplecov/exit_codes/maximum_coverage_drop_check.rb index 4be52044..bd0d195e 100644 --- a/lib/simplecov/exit_codes/maximum_coverage_drop_check.rb +++ b/lib/simplecov/exit_codes/maximum_coverage_drop_check.rb @@ -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 %.2f%% since the last time (maximum allowed: %.2f%%).\n", - drop_percent: coverage_diff, - max_drop: maximum_coverage_drop - ) + coverage_drop_violations.each do |violation| + $stderr.printf( + "Coverage has dropped by %.2f%% since the last time (maximum allowed: %.2f%%).\n", + drop_percent: SimpleCov.round_coverage(violation[:drop_percent]), + max_drop: violation[:max_drop] + ) + end end def exit_code @@ -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 diff --git a/lib/simplecov/exit_codes/minimum_coverage_by_file_check.rb b/lib/simplecov/exit_codes/minimum_coverage_by_file_check.rb index 15aef13a..696572f2 100644 --- a/lib/simplecov/exit_codes/minimum_coverage_by_file_check.rb +++ b/lib/simplecov/exit_codes/minimum_coverage_by_file_check.rb @@ -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 (%s) is only (%.2f%%) covered. This is below the expected minimum coverage per file of (%.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( + "%s coverage (%.2f%%) is below the expected 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 @@ -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 diff --git a/lib/simplecov/file_list.rb b/lib/simplecov/file_list.rb index d4c9bba9..20f466f7 100644 --- a/lib/simplecov/file_list.rb +++ b/lib/simplecov/file_list.rb @@ -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 @@ -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 diff --git a/lib/simplecov/result.rb b/lib/simplecov/result.rb index 7eb4f8ff..21dcbaf4 100644 --- a/lib/simplecov/result.rb +++ b/lib/simplecov/result.rb @@ -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 diff --git a/spec/configuration_spec.rb b/spec/configuration_spec.rb index ca2e8ccb..0ce8ca4d 100644 --- a/spec/configuration_spec.rb +++ b/spec/configuration_spec.rb @@ -47,70 +47,70 @@ end end - describe "#minimum_coverage" do + shared_examples "checks coverage settings" do |coverage_setting| after :each do config.clear_coverage_criteria end it "does not warn you about your usage" do expect(config).not_to receive(:warn) - config.minimum_coverage(100.00) + config.send(coverage_setting, 100.00) end it "warns you about your usage" do - expect(config).to receive(:warn).with("The coverage you set for minimum_coverage is greater than 100%") - config.minimum_coverage(100.01) + expect(config).to receive(:warn).with("The coverage you set for #{coverage_setting} is greater than 100%") + config.send(coverage_setting, 100.01) end it "sets the right coverage value when called with a number" do - config.minimum_coverage(80) + config.send(coverage_setting, 80) - expect(config.minimum_coverage).to eq line: 80 + expect(config.send(coverage_setting)).to eq line: 80 end it "sets the right coverage when called with a hash of just line" do - config.minimum_coverage line: 85.0 + config.send(coverage_setting, {line: 85.0}) - expect(config.minimum_coverage).to eq line: 85.0 + expect(config.send(coverage_setting)).to eq 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 branch: 85.0 + config.send(coverage_setting, {branch: 85.0}) - expect(config.minimum_coverage).to eq branch: 85.0 + expect(config.send(coverage_setting)).to eq branch: 85.0 end it "sets the right coverage when called withboth line and branch" do config.enable_coverage :branch - config.minimum_coverage branch: 85.0, line: 95.4 + config.send(coverage_setting, {branch: 85.0, line: 95.4}) - expect(config.minimum_coverage).to eq branch: 85.0, line: 95.4 + expect(config.send(coverage_setting)).to eq branch: 85.0, line: 95.4 end it "raises when trying to set branch coverage but not enabled" do expect do - config.minimum_coverage branch: 42 + config.send(coverage_setting, {branch: 42}) end.to raise_error(/branch.*disabled/i) end it "raises when unknown coverage criteria provided" do expect do - config.minimum_coverage unknown: 42 + config.send(coverage_setting, {unknown: 42}) end.to raise_error(/unsupported.*unknown/i) end end + describe "#minimum_coverage" do + it_behaves_like "checks coverage settings", :minimum_coverage + end + describe "#minimum_coverage_by_file" do - it "does not warn you about your usage" do - expect(config).not_to receive(:warn) - config.minimum_coverage_by_file(100.00) - end + it_behaves_like "checks coverage settings", :minimum_coverage_by_file + end - it "warns you about your usage" do - expect(config).to receive(:warn).with("The coverage you set for minimum_coverage_by_file is greater than 100%") - config.minimum_coverage_by_file(100.01) - end + describe "#maximum_coverage_drop" do + it_behaves_like "checks coverage settings", :maximum_coverage_drop end describe "#coverage_criterion" do