diff --git a/features/minimum_coverage.feature b/features/minimum_coverage.feature index ccc97a6d..99c1272a 100644 --- a/features/minimum_coverage.feature +++ b/features/minimum_coverage.feature @@ -16,7 +16,7 @@ Feature: When I run `bundle exec rake test` Then the exit status should not be 0 - And the output should contain "Coverage (88.09%) is below the expected minimum coverage (90.00%)." + And the output should contain "Line coverage (88.09%) is below the expected minimum coverage (90.00%)." And the output should contain "SimpleCov failed with exit 2" Scenario: @@ -31,7 +31,7 @@ Feature: When I run `bundle exec rake test` Then the exit status should not be 0 - And the output should contain "Coverage (88.09%) is below the expected minimum coverage (88.10%)." + And the output should contain "Line coverage (88.09%) is below the expected minimum coverage (88.10%)." And the output should contain "SimpleCov failed with exit 2" Scenario: @@ -58,3 +58,21 @@ Feature: When I run `bundle exec rake test` Then the exit status should be 0 + + @branch_coverage + Scenario: Works together with branch coverage and the new criterion announcing both failures + Given SimpleCov for Test/Unit is configured with: + """ + require 'simplecov' + SimpleCov.start do + add_filter 'test.rb' + enable_coverage :branch + minimum_coverage line: 90, branch: 80 + end + """ + + When I run `bundle exec rake test` + Then the exit status should not be 0 + And the output should contain "Line coverage (88.09%) is below the expected minimum coverage (90.00%)." + And the output should contain "Branch coverage (50.00%) is below the expected minimum coverage (80.00%)." + And the output should contain "SimpleCov failed with exit 2" diff --git a/lib/simplecov.rb b/lib/simplecov.rb index f5c50efb..f8edae47 100644 --- a/lib/simplecov.rb +++ b/lib/simplecov.rb @@ -231,12 +231,8 @@ def process_result(result, exit_status) # rubocop:disable Metrics/MethodLength def result_exit_status(result, covered_percent) covered_percentages = result.covered_percentages.map { |percentage| percentage.floor(2) } - if covered_percent < SimpleCov.minimum_coverage - $stderr.printf( - "Coverage (%<covered>.2f%%) is below the expected minimum coverage (%<minimum_coverage>.2f%%).\n", - covered: covered_percent, - minimum_coverage: SimpleCov.minimum_coverage - ) + if (minimum_violations = minimum_coverage_violated(result)).any? + report_minimum_violated(minimum_violations) SimpleCov::ExitCodes::MINIMUM_COVERAGE elsif covered_percentages.any? { |p| p < SimpleCov.minimum_coverage_by_file } $stderr.printf( @@ -409,6 +405,31 @@ def remove_useless_results def result_with_not_loaded_files @result = SimpleCov::Result.new(add_not_loaded_files(@result)) end + + def minimum_coverage_violated(result) + coverage_achieved = minimum_coverage.map do |criterion, percent| + { + criterion: criterion, + minimum_expected: percent, + actual: result.coverage_statistics[criterion].percent + } + end + + coverage_achieved.select do |achieved| + achieved.fetch(:actual) < achieved.fetch(:minimum_expected) + end + end + + def report_minimum_violated(violations) + violations.each do |violation| + $stderr.printf( + "%<criterion>s coverage (%<covered>.2f%%) is below the expected minimum coverage (%<minimum_coverage>.2f%%).\n", + covered: violation.fetch(:actual).floor(2), + minimum_coverage: violation.fetch(:minimum_expected), + criterion: violation.fetch(:criterion).capitalize + ) + end + end end end diff --git a/lib/simplecov/configuration.rb b/lib/simplecov/configuration.rb index daf9106c..bba523a5 100644 --- a/lib/simplecov/configuration.rb +++ b/lib/simplecov/configuration.rb @@ -240,8 +240,15 @@ def merge_timeout(seconds = nil) # Default is 0% (disabled) # def minimum_coverage(coverage = nil) - minimum_possible_coverage_exceeded("minimum_coverage") if coverage && coverage > 100 - @minimum_coverage ||= (coverage || 0).to_f.round(2) + return @minimum_coverage ||= {} unless coverage + + coverage = {DEFAULT_COVERAGE_CRITERION => coverage} if coverage.is_a?(Numeric) + coverage.keys.each { |criterion| raise_if_criterion_disabled(criterion) } + coverage.values.each do |percent| + minimum_possible_coverage_exceeded("minimum_coverage") if percent && percent > 100 + end + + @minimum_coverage = coverage end # @@ -362,12 +369,21 @@ def coverage_start_arguments_supported? private - def raise_if_criterion_unsupported(criterion) - raise_criterion_unsupported(criterion) unless SUPPORTED_COVERAGE_CRITERIA.member?(criterion) + def raise_if_criterion_disabled(criterion) + raise_if_criterion_unsupported(criterion) + # rubocop:disable Style/IfUnlessModifier + unless coverage_criterion_enabled?(criterion) + raise "Coverage criterion #{criterion}, is disabled! Please enable it first through enable_coverage #{criterion} (if supported)" + end + # rubocop:enable Style/IfUnlessModifier end - def raise_criterion_unsupported(criterion) - raise "Unsupported coverage criterion #{criterion}, supported values are #{SUPPORTED_COVERAGE_CRITERIA}" + def raise_if_criterion_unsupported(criterion) + # rubocop:disable Style/IfUnlessModifier + unless SUPPORTED_COVERAGE_CRITERIA.member?(criterion) + raise "Unsupported coverage criterion #{criterion}, supported values are #{SUPPORTED_COVERAGE_CRITERIA}" + end + # rubocop:enable Style/IfUnlessModifier end def minimum_possible_coverage_exceeded(coverage_option) diff --git a/lib/simplecov/file_list.rb b/lib/simplecov/file_list.rb index 4e08329e..d4c9bba9 100644 --- a/lib/simplecov/file_list.rb +++ b/lib/simplecov/file_list.rb @@ -23,18 +23,18 @@ def initialize(files) @files = files end - def coverage - @coverage ||= compute_coverage + def coverage_statistics + @coverage_statistics ||= compute_coverage_statistics end # Returns the count of lines that have coverage def covered_lines - coverage[:line]&.covered + coverage_statistics[:line]&.covered end # Returns the count of lines that have been missed def missed_lines - coverage[:line]&.missed + coverage_statistics[:line]&.missed end # Returns the count of lines that are not relevant for coverage @@ -64,46 +64,46 @@ def least_covered_file # Returns the overall amount of relevant lines of code across all files in this list def lines_of_code - coverage[:line]&.total + coverage_statistics[:line]&.total end # Computes the coverage based upon lines covered and lines missed # @return [Float] def covered_percent - coverage[:line]&.percent + coverage_statistics[:line]&.percent end # Computes the strength (hits / line) based upon lines covered and lines missed # @return [Float] def covered_strength - coverage[:line]&.strength + coverage_statistics[:line]&.strength end # Return total count of branches in all files def total_branches - coverage[:branch]&.total + coverage_statistics[:branch]&.total end # Return total count of covered branches def covered_branches - coverage[:branch]&.covered + coverage_statistics[:branch]&.covered end # Return total count of covered branches def missed_branches - coverage[:branch]&.missed + coverage_statistics[:branch]&.missed end def branch_covered_percent - coverage[:branch]&.percent + coverage_statistics[:branch]&.percent end private - def compute_coverage + def compute_coverage_statistics total_coverage_statistics = @files.each_with_object(line: [], branch: []) do |file, together| - together[:line] << file.coverage[:line] - together[:branch] << file.coverage[:branch] if SimpleCov.branch_coverage? + together[:line] << file.coverage_statistics[:line] + together[:branch] << file.coverage_statistics[:branch] if SimpleCov.branch_coverage? end coverage_statistics = {line: CoverageStatistics.from(total_coverage_statistics[:line])} diff --git a/lib/simplecov/result.rb b/lib/simplecov/result.rb index be3ac58e..959e81f8 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 + 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_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/lib/simplecov/source_file.rb b/lib/simplecov/source_file.rb index 3506341f..f7dcda3b 100644 --- a/lib/simplecov/source_file.rb +++ b/lib/simplecov/source_file.rb @@ -29,11 +29,11 @@ def src end alias source src - def coverage - @coverage ||= + def coverage_statistics + @coverage_statistics ||= { - **line_coverage, - **branch_coverage + **line_coverage_statistics, + **branch_coverage_statistics } end @@ -68,7 +68,7 @@ def skipped_lines # Returns the number of relevant lines (covered + missed) def lines_of_code - coverage[:line]&.total + coverage_statistics[:line]&.total end # Access SimpleCov::SourceFile::Line source lines by line number @@ -78,11 +78,11 @@ def line(number) # The coverage for this file in percent. 0 if the file has no coverage lines def covered_percent - coverage[:line]&.percent + coverage_statistics[:line]&.percent end def covered_strength - coverage[:line]&.strength + coverage_statistics[:line]&.strength end def no_lines? @@ -104,7 +104,7 @@ def no_branches? end def branches_coverage_percent - coverage[:branch]&.percent + coverage_statistics[:branch]&.percent end # @@ -278,7 +278,7 @@ def build_branch(branch_data, hit_count, condition_start_line) ) end - def line_coverage + def line_coverage_statistics { line: CoverageStatistics.new( total_strength: lines_strength, @@ -288,7 +288,7 @@ def line_coverage } end - def branch_coverage + def branch_coverage_statistics { branch: CoverageStatistics.new( covered: covered_branches.size, diff --git a/spec/configuration_spec.rb b/spec/configuration_spec.rb index 7b07df10..ff670c75 100644 --- a/spec/configuration_spec.rb +++ b/spec/configuration_spec.rb @@ -48,6 +48,10 @@ end describe "#minimum_coverage" do + 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) @@ -57,6 +61,44 @@ expect(config).to receive(:warn).with("The coverage you set for minimum_coverage is greater than 100%") config.minimum_coverage(100.01) end + + it "sets the right converage value when called with a number" do + config.minimum_coverage(80) + + expect(config.minimum_coverage).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 + + expect(config.minimum_coverage).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 + + expect(config.minimum_coverage).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 + + expect(config.minimum_coverage).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 + end.to raise_error(/branch.*disabled/i) + end + + it "raises when unknown coverage criteria provided" do + expect do + config.minimum_coverage unknown: 42 + end.to raise_error(/unsupported.*unknown/i) + end end describe "#minimum_coverage_by_file" do diff --git a/spec/simplecov_spec.rb b/spec/simplecov_spec.rb index 90e5076c..f92cf488 100644 --- a/spec/simplecov_spec.rb +++ b/spec/simplecov_spec.rb @@ -193,17 +193,17 @@ end describe ".process_result" do - context "when minimum coverage is 100%" do - let(:result) { SimpleCov::Result.new({}) } + let(:result) { SimpleCov::Result.new({}) } + context "when minimum coverage is 100%" do before do - allow(SimpleCov).to receive(:minimum_coverage).and_return(100) + allow(SimpleCov).to receive(:minimum_coverage).and_return(line: 100) allow(SimpleCov).to receive(:result?).and_return(true) end context "when actual coverage is almost 100%" do before do - allow(result).to receive(:covered_percent).and_return(100 * 32_847.0 / 32_848) + allow(result).to receive(:coverage_statistics).and_return(line: double("statistics", percent: 100 * 32_847.0 / 32_848)) end it "return SimpleCov::ExitCodes::MINIMUM_COVERAGE" do @@ -216,6 +216,9 @@ context "when actual coverage is exactly 100%" do before do allow(result).to receive(:covered_percent).and_return(100.0) + allow(result).to receive(:coverage_statistics).and_return( + line: double("statistics", percent: 100.0) + ) allow(result).to receive(:covered_percentages).and_return([]) allow(SimpleCov::LastRun).to receive(:read).and_return(nil) end @@ -227,6 +230,21 @@ end end end + + context "branch coverage" do + before do + allow(SimpleCov).to receive(:minimum_coverage).and_return(branch: 90) + allow(SimpleCov).to receive(:result?).and_return(true) + end + + it "errors out when the coverage is too low" do + allow(result).to receive(:coverage_statistics).and_return(branch: double("statistics", percent: 89.99)) + + expect( + SimpleCov.process_result(result, SimpleCov::ExitCodes::SUCCESS) + ).to eq(SimpleCov::ExitCodes::MINIMUM_COVERAGE) + end + end end end