diff --git a/.github/workflows/stable.yml b/.github/workflows/stable.yml index b3562f324..35cad2aec 100644 --- a/.github/workflows/stable.yml +++ b/.github/workflows/stable.yml @@ -9,6 +9,7 @@ jobs: env: BUNDLE_WITHOUT: "benchmark" JRUBY_OPTS: "--debug" + SIMPLECOV_HTML_MODE: "methods" # TODO: remove after simplecov-html release strategy: fail-fast: false diff --git a/Gemfile b/Gemfile index aec042b2f..7f4ceb117 100644 --- a/Gemfile +++ b/Gemfile @@ -2,16 +2,25 @@ source "https://rubygems.org" -# Uncomment this to use local copy of simplecov-html in development when checked out -# gem "simplecov-html", path: File.join(__dir__, "../simplecov-html") - -# Uncomment this to use development version of html formatter from github -# gem "simplecov-html", github: "simplecov-ruby/simplecov-html" +case ENV.fetch("SIMPLECOV_HTML_MODE", nil) +when "local" + # Use local copy of simplecov-html in development when checked out + gem "simplecov-html", path: File.join(__dir__, "../simplecov-html") +when "github" + # Use development version of html formatter from github + gem "simplecov-html", git: "https://github.com/simplecov-ruby/simplecov-html.git" +when "methods" # TODO: remove after simplecov-html release + gem "simplecov-html", git: "https://github.com/umbrellio/simplecov-html.git", branch: "add-method-coverage-support" +end +gem "base64" +gem "bigdecimal" gem "matrix" +gem "mutex_m" +gem "ostruct" group :development do - gem "apparition", github: "twalpole/apparition" + gem "apparition", git: "https://github.com/twalpole/apparition.git" gem "activesupport", "~> 6.1" gem "aruba" gem "capybara" @@ -28,6 +37,7 @@ group :development do gem "rubocop", "~> 1.70.0" if RUBY_VERSION > "3.2" gem "test-unit" gem "logger" + gem "power_assert", "~> 2.0" if RUBY_VERSION < "3.0" # Explicitly add webrick because it has been removed from stdlib in Ruby 3.0 gem "webrick" end diff --git a/Gemfile.lock b/Gemfile.lock index aae6a9b10..5e16bf756 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -6,6 +6,13 @@ GIT capybara (~> 3.13, < 4) websocket-driver (>= 0.6.5) +GIT + remote: https://github.com/umbrellio/simplecov-html.git + revision: d85b595f3911564723fdf1fcb83c0bca7f9bddf9 + branch: add-method-coverage-support + specs: + simplecov-html (0.13.2) + PATH remote: . specs: @@ -25,17 +32,16 @@ GEM zeitwerk (~> 2.3) addressable (2.8.7) public_suffix (>= 2.0.2, < 7.0) - aruba (2.3.0) + aruba (2.3.2) bundler (>= 1.17, < 3.0) contracts (>= 0.16.0, < 0.18.0) - cucumber (>= 8.0, < 10.0) - rspec-expectations (~> 3.4) + cucumber (>= 8.0, < 11.0) + rspec-expectations (>= 3.4, < 5.0) thor (~> 1.0) - ast (2.4.2) - base64 (0.2.0) + ast (2.4.3) + base64 (0.3.0) benchmark-ips (2.14.0) - bigdecimal (3.1.9) - bigdecimal (3.1.9-java) + bigdecimal (3.3.1) builder (3.3.0) capybara (3.40.0) addressable @@ -47,88 +53,104 @@ GEM regexp_parser (>= 1.5, < 3.0) xpath (~> 3.2) coderay (1.1.3) - concurrent-ruby (1.3.4) + concurrent-ruby (1.3.5) contracts (0.17.2) - cucumber (9.2.1) + cucumber (10.1.1) + base64 (~> 0.2) builder (~> 3.2) cucumber-ci-environment (> 9, < 11) - cucumber-core (> 13, < 14) - cucumber-cucumber-expressions (~> 17.0) - cucumber-gherkin (> 24, < 28) + cucumber-core (> 15, < 17) + cucumber-cucumber-expressions (> 17, < 19) cucumber-html-formatter (> 20.3, < 22) - cucumber-messages (> 19, < 25) diff-lcs (~> 1.5) + logger (~> 1.6) mini_mime (~> 1.1) multi_test (~> 1.1) - sys-uname (~> 1.2) + sys-uname (~> 1.3) cucumber-ci-environment (10.0.1) - cucumber-core (13.0.3) - cucumber-gherkin (>= 27, < 28) - cucumber-messages (>= 20, < 23) - cucumber-tag-expressions (> 5, < 7) - cucumber-cucumber-expressions (17.1.0) + cucumber-core (15.3.0) + cucumber-gherkin (> 27, < 35) + cucumber-messages (> 26, < 30) + cucumber-tag-expressions (> 5, < 9) + cucumber-cucumber-expressions (18.0.1) bigdecimal - cucumber-gherkin (27.0.0) - cucumber-messages (>= 19.1.4, < 23) - cucumber-html-formatter (21.7.0) - cucumber-messages (> 19, < 27) - cucumber-messages (22.0.0) - cucumber-tag-expressions (6.1.1) - diff-lcs (1.5.1) + cucumber-gherkin (34.0.0) + cucumber-messages (> 25, < 29) + cucumber-html-formatter (21.15.1) + cucumber-messages (> 19, < 28) + cucumber-messages (27.2.0) + cucumber-tag-expressions (8.0.0) + diff-lcs (1.6.2) docile (1.4.1) - ffi (1.17.1) - ffi (1.17.1-java) - i18n (1.14.6) + ffi (1.17.2-aarch64-linux-gnu) + ffi (1.17.2-aarch64-linux-musl) + ffi (1.17.2-arm-linux-gnu) + ffi (1.17.2-arm-linux-musl) + ffi (1.17.2-arm64-darwin) + ffi (1.17.2-x86_64-darwin) + ffi (1.17.2-x86_64-linux-gnu) + ffi (1.17.2-x86_64-linux-musl) + i18n (1.14.7) concurrent-ruby (~> 1.0) - json (2.9.1) - json (2.9.1-java) - language_server-protocol (3.17.0.3) + json (2.15.2) + language_server-protocol (3.17.0.5) logger (1.7.0) - matrix (0.4.2) + matrix (0.4.3) + memoist3 (1.0.0) method_source (1.1.0) mini_mime (1.1.5) - mini_portile2 (2.8.8) - minitest (5.25.4) + minitest (5.26.0) multi_test (1.1.0) - nokogiri (1.18.1) - mini_portile2 (~> 2.8.2) + mutex_m (0.3.0) + nokogiri (1.18.10-aarch64-linux-gnu) + racc (~> 1.4) + nokogiri (1.18.10-aarch64-linux-musl) + racc (~> 1.4) + nokogiri (1.18.10-arm-linux-gnu) + racc (~> 1.4) + nokogiri (1.18.10-arm-linux-musl) + racc (~> 1.4) + nokogiri (1.18.10-arm64-darwin) racc (~> 1.4) - parallel (1.26.3) - parser (3.3.6.0) + nokogiri (1.18.10-x86_64-darwin) + racc (~> 1.4) + nokogiri (1.18.10-x86_64-linux-gnu) + racc (~> 1.4) + nokogiri (1.18.10-x86_64-linux-musl) + racc (~> 1.4) + ostruct (0.6.3) + parallel (1.27.0) + parser (3.3.10.0) ast (~> 2.4.1) racc - power_assert (2.0.5) + power_assert (3.0.0) + prism (1.6.0) pry (0.15.2) coderay (~> 1.1) method_source (~> 1.0) - pry (0.15.2-java) - coderay (~> 1.1) - method_source (~> 1.0) - spoon (~> 0.0) - public_suffix (6.0.1) + public_suffix (6.0.2) racc (1.8.1) - racc (1.8.1-java) - rack (3.1.8) + rack (3.2.4) rack-test (2.2.0) rack (>= 1.3) rackup (2.2.1) rack (>= 3) rainbow (3.1.1) - rake (13.2.1) - regexp_parser (2.10.0) - rspec (3.13.0) + rake (13.3.1) + regexp_parser (2.11.3) + rspec (3.13.2) rspec-core (~> 3.13.0) rspec-expectations (~> 3.13.0) rspec-mocks (~> 3.13.0) - rspec-core (3.13.2) + rspec-core (3.13.6) rspec-support (~> 3.13.0) - rspec-expectations (3.13.3) + rspec-expectations (3.13.5) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.13.0) - rspec-mocks (3.13.2) + rspec-mocks (3.13.7) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.13.0) - rspec-support (3.13.2) + rspec-support (3.13.6) rubocop (1.70.0) json (~> 2.3) language_server-protocol (>= 3.17.0) @@ -139,58 +161,64 @@ GEM rubocop-ast (>= 1.36.2, < 2.0) ruby-progressbar (~> 1.7) unicode-display_width (>= 2.4.0, < 4.0) - rubocop-ast (1.37.0) - parser (>= 3.3.1.0) + rubocop-ast (1.47.1) + parser (>= 3.3.7.2) + prism (~> 1.4) ruby-progressbar (1.13.0) - simplecov-html (0.13.1) simplecov_json_formatter (0.1.4) - spoon (0.0.6) - ffi - sys-uname (1.3.1) + sys-uname (1.4.1) ffi (~> 1.1) - test-unit (3.6.7) + memoist3 (~> 1.0.0) + test-unit (3.7.1) power_assert - thor (1.3.2) + thor (1.4.0) tzinfo (2.0.6) concurrent-ruby (~> 1.0) - unicode-display_width (3.1.4) - unicode-emoji (~> 4.0, >= 4.0.4) - unicode-emoji (4.0.4) + unicode-display_width (3.2.0) + unicode-emoji (~> 4.1) + unicode-emoji (4.1.0) webrick (1.9.1) - websocket-driver (0.7.7) - base64 - websocket-extensions (>= 0.1.0) - websocket-driver (0.7.7-java) + websocket-driver (0.8.0) base64 websocket-extensions (>= 0.1.0) websocket-extensions (0.1.5) xpath (3.2.0) nokogiri (~> 1.8) - zeitwerk (2.7.1) + zeitwerk (2.7.3) PLATFORMS - java - ruby - universal-java-1.8 - universal-java-11 + aarch64-linux-gnu + aarch64-linux-musl + arm-linux-gnu + arm-linux-musl + arm64-darwin + x86_64-darwin + x86_64-linux-gnu + x86_64-linux-musl DEPENDENCIES activesupport (~> 6.1) apparition! aruba + base64 benchmark-ips + bigdecimal capybara cucumber + logger matrix minitest + mutex_m + ostruct pry rackup rake rspec rubocop (~> 1.70.0) simplecov! + simplecov-html! test-unit webrick BUNDLED WITH - 2.7.0.dev + 2.7.2 diff --git a/features/branch_coverage.feature b/features/branch_coverage.feature index 55f0d0963..622cd780b 100644 --- a/features/branch_coverage.feature +++ b/features/branch_coverage.feature @@ -21,7 +21,7 @@ Feature: And I should see a line coverage summary of 56/61 And I should see a branch coverage summary of 2/4 And I should see the source files: - | name | coverage | branch coverage | + | name | coverage | branch coverage | | lib/faked_project.rb | 100.00 % | 100.00 % | | lib/faked_project/some_class.rb | 80.00 % | 50.00 % | | lib/faked_project/framework_specific.rb | 75.00 % | 100.00 % | diff --git a/features/maximum_coverage_drop.feature b/features/maximum_coverage_drop.feature index 5566a79c9..7a8b60c5a 100644 --- a/features/maximum_coverage_drop.feature +++ b/features/maximum_coverage_drop.feature @@ -314,7 +314,6 @@ Feature: When I run `bundle exec rake test` Then the exit status should not be 0 - And the output should not contain "Line coverage" And the output should contain "Branch coverage has dropped by 50.00% since the last time (maximum allowed: 0.00%)." And the output should contain "SimpleCov failed with exit 3" diff --git a/features/method_coverage.feature b/features/method_coverage.feature new file mode 100644 index 000000000..98af2ddb0 --- /dev/null +++ b/features/method_coverage.feature @@ -0,0 +1,40 @@ +@rspec @method_coverage +Feature: + + Simply executing method coverage gives ok results. + + Background: + Given I'm working on the project "faked_project" + + Scenario: + Given SimpleCov for RSpec is configured with: + """ + require 'simplecov' + + SimpleCov.start do + enable_coverage :method + end + """ + When I open the coverage report generated with `bundle exec rspec spec` + Then I should see the groups: + | name | coverage | files | + | All Files | 91.8% | 7 | + And I should see a line coverage summary of 56/61 + And I should see a method coverage summary of 10/13 + And I should see the source files: + | name | coverage | method coverage | + | lib/faked_project.rb | 100.00 % | 100.00 % | + | lib/faked_project/some_class.rb | 80.00 % | 75.00 % | + | lib/faked_project/framework_specific.rb | 75.00 % | 33.33 % | + | lib/faked_project/meta_magic.rb | 100.00 % | 100.00 % | + | spec/forking_spec.rb | 100.00 % | 100.00 % | + | spec/meta_magic_spec.rb | 100.00 % | 100.00 % | + | spec/some_class_spec.rb | 100.00 % | 100.00 % | + + When I open the detailed view for "lib/faked_project/framework_specific.rb" + Then I should see a line coverage summary of 6/8 for the file + And I should see a method coverage summary of 1/3 for the file + And I should see missed methods list: + | name | + | ##test_unit | + | ##cucumber | diff --git a/features/minimum_coverage.feature b/features/minimum_coverage.feature index ba1b8c60f..6d38c869d 100644 --- a/features/minimum_coverage.feature +++ b/features/minimum_coverage.feature @@ -84,5 +84,4 @@ Feature: When I run `bundle exec rake test` Then the exit status should not be 0 And the output should contain "Branch coverage (50.00%) is below the expected minimum coverage (80.00%)." - And the output should not contain "Line coverage" And the output should contain "SimpleCov failed with exit 2" diff --git a/features/minimum_coverage_by_file.feature b/features/minimum_coverage_by_file.feature index e4540ef90..49442a333 100644 --- a/features/minimum_coverage_by_file.feature +++ b/features/minimum_coverage_by_file.feature @@ -68,5 +68,4 @@ Feature: When I run `bundle exec rake test` Then the exit status should not be 0 And the output should contain "Branch coverage by file (50.00%) is below the expected minimum coverage (70.00%)." - And the output should not contain "Line coverage" And the output should contain "SimpleCov failed with exit 2" diff --git a/features/step_definitions/html_steps.rb b/features/step_definitions/html_steps.rb index f4f080e74..69b5d42a6 100644 --- a/features/step_definitions/html_steps.rb +++ b/features/step_definitions/html_steps.rb @@ -25,6 +25,7 @@ available_source_files = all(".t-file", visible: true, count: expected_files.count) include_branch_coverage = table.column_names.include?("branch coverage") + include_method_coverage = table.column_names.include?("method coverage") # Find all filenames and their coverage present in coverage report files = available_source_files.map do |file_row| @@ -35,6 +36,7 @@ } coverage_data["branch coverage"] = file_row.find(".t-file__branch-coverage").text if include_branch_coverage + coverage_data["method coverage"] = file_row.find(".t-file__method-coverage").text if include_method_coverage coverage_data end @@ -68,3 +70,9 @@ Then /^I should see coverage branch data like "(.+)"$/ do |text| expect(find(".hits", visible: true, text: text)).to be_truthy end + +Then /^I should see missed methods list:$/ do |table| + expected_list = table.hashes.map { |x| x.fetch("name") } + list = all(".t-missed-method-summary li", visible: true).map(&:text) + expect(list).to eq(expected_list) +end diff --git a/features/step_definitions/simplecov_steps.rb b/features/step_definitions/simplecov_steps.rb index 406be3902..2761b2a9f 100644 --- a/features/step_definitions/simplecov_steps.rb +++ b/features/step_definitions/simplecov_steps.rb @@ -21,7 +21,7 @@ steps %( Given a file named "#{framework_dir}/simplecov_config.rb" with: """ - #{config_body} +#{config_body.gsub(/^/, ' ' * 6)} """ ) end diff --git a/features/support/env.rb b/features/support/env.rb index d271a1605..082365a5c 100644 --- a/features/support/env.rb +++ b/features/support/env.rb @@ -38,6 +38,10 @@ def extended(base) skip_this_scenario unless SimpleCov.branch_coverage_supported? end +Before("@method_coverage") do + skip_this_scenario unless SimpleCov.method_coverage_supported? +end + Before("@rails6") do # Rails 6 only supports Ruby 2.5+ skip_this_scenario if Gem::Version.new(RUBY_VERSION) < Gem::Version.new("2.5") diff --git a/lib/minitest/simplecov_plugin.rb b/lib/minitest/simplecov_plugin.rb index 268097057..43d226a9c 100644 --- a/lib/minitest/simplecov_plugin.rb +++ b/lib/minitest/simplecov_plugin.rb @@ -4,7 +4,7 @@ # https://github.com/seattlerb/minitest#writing-extensions module Minitest def self.plugin_simplecov_init(_options) - if defined?(SimpleCov) + if defined?(SimpleCov) && SimpleCov.respond_to?(:at_exit_behavior) SimpleCov.external_at_exit = true Minitest.after_run do diff --git a/lib/simplecov.rb b/lib/simplecov.rb index 4250f46b4..797ac4a21 100644 --- a/lib/simplecov.rb +++ b/lib/simplecov.rb @@ -285,18 +285,10 @@ def wait_for_other_processes def write_last_run(result) SimpleCov::LastRun.write(result: result.coverage_statistics.transform_values do |stats| - round_coverage(stats.percent) + SimpleCov::Utils.round_coverage(stats.percent) end) end - # - # @api private - # - # Rounding down to be extra strict, see #679 - def round_coverage(coverage) - coverage.floor(2) - end - private def initial_setup(profile, &block) @@ -364,7 +356,8 @@ def coverage_running? CRITERION_TO_RUBY_COVERAGE = { branch: :branches, - line: :lines + line: :lines, + method: :methods }.freeze def lookup_corresponding_ruby_coverage_name(criterion) CRITERION_TO_RUBY_COVERAGE.fetch(criterion) @@ -453,6 +446,7 @@ def probably_running_parallel_tests? require_relative "simplecov/profiles" require_relative "simplecov/source_file/line" require_relative "simplecov/source_file/branch" +require_relative "simplecov/source_file/method" require_relative "simplecov/source_file" require_relative "simplecov/file_list" require_relative "simplecov/result" @@ -461,16 +455,19 @@ def probably_running_parallel_tests? require_relative "simplecov/last_run" require_relative "simplecov/lines_classifier" require_relative "simplecov/result_merger" +require_relative "simplecov/result_serialization" require_relative "simplecov/command_guesser" require_relative "simplecov/version" require_relative "simplecov/result_adapter" require_relative "simplecov/combine" require_relative "simplecov/combine/branches_combiner" +require_relative "simplecov/combine/methods_combiner" require_relative "simplecov/combine/files_combiner" require_relative "simplecov/combine/lines_combiner" require_relative "simplecov/combine/results_combiner" require_relative "simplecov/useless_results_remover" require_relative "simplecov/simulate_coverage" +require_relative "simplecov/utils" # Load default config require_relative "simplecov/defaults" unless ENV["SIMPLECOV_NO_DEFAULTS"] diff --git a/lib/simplecov/combine/branches_combiner.rb b/lib/simplecov/combine/branches_combiner.rb index c3af9b29c..2f0f4acdd 100644 --- a/lib/simplecov/combine/branches_combiner.rb +++ b/lib/simplecov/combine/branches_combiner.rb @@ -10,7 +10,7 @@ module BranchesCombiner module_function # - # Return merged branches or the existed branch if other is missing. + # Return merged branches or the existing branch if other is missing. # # Branches inside files are always same if they exist, the difference only in coverage count. # Branch coverage report for any conditional case is built from hash, it's key is a condition and diff --git a/lib/simplecov/combine/files_combiner.rb b/lib/simplecov/combine/files_combiner.rb index d41583bff..e27af8584 100644 --- a/lib/simplecov/combine/files_combiner.rb +++ b/lib/simplecov/combine/files_combiner.rb @@ -14,9 +14,19 @@ module FilesCombiner # # @return [Hash] # - def combine(coverage_a, coverage_b) - combination = {"lines" => Combine.combine(LinesCombiner, coverage_a["lines"], coverage_b["lines"])} - combination["branches"] = Combine.combine(BranchesCombiner, coverage_a["branches"], coverage_b["branches"]) || {} if SimpleCov.branch_coverage? + def combine(cov_a, cov_b) + combination = {} + + combination[:lines] = Combine.combine(LinesCombiner, cov_a[:lines], cov_b[:lines]) + + if SimpleCov.branch_coverage? # rubocop:disable Style/IfUnlessModifier + combination[:branches] = Combine.combine(BranchesCombiner, cov_a[:branches], cov_b[:branches]) || {} + end + + if SimpleCov.method_coverage? # rubocop:disable Style/IfUnlessModifier + combination[:methods] = Combine.combine(MethodsCombiner, cov_a[:methods], cov_b[:methods]) + end + combination end end diff --git a/lib/simplecov/combine/methods_combiner.rb b/lib/simplecov/combine/methods_combiner.rb new file mode 100644 index 000000000..676042852 --- /dev/null +++ b/lib/simplecov/combine/methods_combiner.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +module SimpleCov + module Combine + # + # Combine different method coverage results on single file. + # + # Should be called through `SimpleCov.combine`. + module MethodsCombiner + module_function + + # + # Combine method coverage from 2 sources + # + # @return [Hash] + # + def combine(coverage_a, coverage_b) + result_coverage = {} + + keys = (coverage_a.keys + coverage_b.keys).uniq + + keys.each do |method_name| + result_coverage[method_name] = + coverage_a.fetch(method_name, 0) + coverage_b.fetch(method_name, 0) + end + + result_coverage + end + end + end +end diff --git a/lib/simplecov/combine/results_combiner.rb b/lib/simplecov/combine/results_combiner.rb index dd3593028..2ebf8edc2 100644 --- a/lib/simplecov/combine/results_combiner.rb +++ b/lib/simplecov/combine/results_combiner.rb @@ -16,6 +16,7 @@ module ResultsCombiner # ==> FileCombiner: collect result of next combine levels lines and branches. # ===> LinesCombiner: combine lines results. # ===> BranchesCombiner: combine branches results. + # ===> MethodsCombiner: combine methods results. # # @return [Hash] # diff --git a/lib/simplecov/configuration.rb b/lib/simplecov/configuration.rb index 48af982c3..7061fe759 100644 --- a/lib/simplecov/configuration.rb +++ b/lib/simplecov/configuration.rb @@ -377,7 +377,7 @@ def add_group(group_name, filter_argument = nil, &filter_proc) groups[group_name] = parse_filter(filter_argument, &filter_proc) end - SUPPORTED_COVERAGE_CRITERIA = %i[line branch].freeze + SUPPORTED_COVERAGE_CRITERIA = %i[line branch method].freeze DEFAULT_COVERAGE_CRITERION = :line # # Define which coverage criterion should be evaluated. @@ -429,9 +429,12 @@ def branch_coverage? branch_coverage_supported? && coverage_criterion_enabled?(:branch) end + def method_coverage? + method_coverage_supported? && coverage_criterion_enabled?(:method) + end + def coverage_start_arguments_supported? - # safe to cache as within one process this value should never - # change + # safe to cache as within one process this value should never change return @coverage_start_arguments_supported if defined?(@coverage_start_arguments_supported) @coverage_start_arguments_supported = begin @@ -444,6 +447,8 @@ def branch_coverage_supported? coverage_start_arguments_supported? && RUBY_ENGINE != "jruby" end + alias method_coverage_supported? branch_coverage_supported? + def coverage_for_eval_supported? require "coverage" defined?(Coverage.supported?) && Coverage.supported?(:eval) @@ -465,19 +470,16 @@ def enable_coverage_for_eval 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)" + + unless coverage_criterion_enabled?(criterion) # rubocop:disable Style/IfUnlessModifier + raise "Coverage criterion #{criterion} is disabled! Please enable it first through enable_coverage #{criterion} (if supported)" end - # rubocop:enable Style/IfUnlessModifier end def raise_if_criterion_unsupported(criterion) - # rubocop:disable Style/IfUnlessModifier - unless SUPPORTED_COVERAGE_CRITERIA.member?(criterion) + unless SUPPORTED_COVERAGE_CRITERIA.member?(criterion) # rubocop:disable Style/IfUnlessModifier 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/exit_codes/maximum_coverage_drop_check.rb b/lib/simplecov/exit_codes/maximum_coverage_drop_check.rb index 7b65b96b3..b1f3cdd19 100644 --- a/lib/simplecov/exit_codes/maximum_coverage_drop_check.rb +++ b/lib/simplecov/exit_codes/maximum_coverage_drop_check.rb @@ -17,10 +17,11 @@ def failing? def report coverage_drop_violations.each do |violation| $stderr.printf( - "%s coverage has dropped by %.2f%% since the last time (maximum allowed: %.2f%%).\n", + "%s coverage has dropped by %s since the last time " \ + "(maximum allowed: %s).\n", criterion: violation[:criterion].capitalize, - drop_percent: SimpleCov.round_coverage(violation[:drop_percent]), - max_drop: violation[:max_drop] + drop_percent: SimpleCov::Utils.render_coverage(violation[:drop_percent]), + max_drop: SimpleCov::Utils.render_coverage(violation[:max_drop]) ) end end @@ -60,9 +61,7 @@ def compute_coverage_drop_data MAX_DROP_ACCURACY = 10 def drop_percent(criterion) drop = last_coverage(criterion) - - SimpleCov.round_coverage( - result.coverage_statistics.fetch(criterion).percent - ) + SimpleCov::Utils.round_coverage(result.coverage_statistics.fetch(criterion).percent) # floats, I tell ya. # irb(main):001:0* 80.01 - 80.0 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 a276d2756..cf6172011 100644 --- a/lib/simplecov/exit_codes/minimum_coverage_by_file_check.rb +++ b/lib/simplecov/exit_codes/minimum_coverage_by_file_check.rb @@ -15,10 +15,10 @@ def failing? def report minimum_violations.each do |violation| $stderr.printf( - "%s coverage by file (%.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 + "%s coverage by file (%s) is below the expected minimum coverage (%s).\n", + criterion: violation.fetch(:criterion).capitalize, + covered: SimpleCov::Utils.render_coverage(violation.fetch(:actual)), + minimum_coverage: SimpleCov::Utils.render_coverage(violation.fetch(:minimum_expected)) ) end end @@ -44,7 +44,7 @@ def compute_minimum_coverage_data { criterion: criterion, minimum_expected: expected_percent, - actual: SimpleCov.round_coverage(actual_coverage.percent) + actual: SimpleCov::Utils.round_coverage(actual_coverage.percent) } end end diff --git a/lib/simplecov/exit_codes/minimum_overall_coverage_check.rb b/lib/simplecov/exit_codes/minimum_overall_coverage_check.rb index ea3a0ea94..5fd4fa3e4 100644 --- a/lib/simplecov/exit_codes/minimum_overall_coverage_check.rb +++ b/lib/simplecov/exit_codes/minimum_overall_coverage_check.rb @@ -15,10 +15,10 @@ def failing? def report 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 + "%s coverage (%s) is below the expected minimum coverage (%s).\n", + criterion: violation.fetch(:criterion).capitalize, + covered: SimpleCov::Utils.render_coverage(violation.fetch(:actual)), + minimum_coverage: SimpleCov::Utils.render_coverage(violation.fetch(:minimum_expected)) ) end end diff --git a/lib/simplecov/file_list.rb b/lib/simplecov/file_list.rb index 756233b89..787458e84 100644 --- a/lib/simplecov/file_list.rb +++ b/lib/simplecov/file_list.rb @@ -102,18 +102,39 @@ def branch_covered_percent coverage_statistics[:branch]&.percent end + # Return total count of methods in all files + def total_methods + coverage_statistics[:method]&.total + end + + # Return total count of covered methods + def covered_methods + coverage_statistics[:method]&.covered + end + + # Return total count of covered methods + def missed_methods + coverage_statistics[:method]&.missed + end + + def method_covered_percent + coverage_statistics[:method]&.percent + end + private def compute_coverage_statistics_by_file - @files.each_with_object(line: [], branch: []) do |file, together| + @files.each_with_object(line: [], branch: [], method: []) do |file, together| together[:line] << file.coverage_statistics.fetch(:line) together[:branch] << file.coverage_statistics.fetch(:branch) if SimpleCov.branch_coverage? + together[:method] << file.coverage_statistics.fetch(:method) if SimpleCov.method_coverage? end end 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[:method] = CoverageStatistics.from(coverage_statistics_by_file[:method]) if SimpleCov.method_coverage? coverage_statistics end end diff --git a/lib/simplecov/result.rb b/lib/simplecov/result.rb index 7741678c7..04de85778 100644 --- a/lib/simplecov/result.rb +++ b/lib/simplecov/result.rb @@ -20,7 +20,9 @@ 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, :coverage_statistics_by_file + 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 @@ -31,8 +33,9 @@ def initialize(original_result, command_name: nil, created_at: nil) @command_name = command_name @created_at = created_at @files = SimpleCov::FileList.new(result.map do |filename, coverage| - SimpleCov::SourceFile.new(filename, JSON.parse(JSON.dump(coverage))) if File.file?(filename) + SimpleCov::SourceFile.new(filename, coverage) if File.file?(filename) end.compact.sort_by(&:filename)) + filter! end @@ -64,19 +67,12 @@ def command_name # Returns a hash representation of this Result that can be used for marshalling it into JSON def to_hash - { - command_name => { - "coverage" => coverage, - "timestamp" => created_at.to_i - } - } + SimpleCov::ResultSerialization.serialize(self) end # Loads a SimpleCov::Result#to_hash dump def self.from_hash(hash) - hash.map do |command_name, data| - new(data.fetch("coverage"), command_name: command_name, created_at: Time.at(data["timestamp"])) - end + SimpleCov::ResultSerialization.deserialize(hash) end private diff --git a/lib/simplecov/result_adapter.rb b/lib/simplecov/result_adapter.rb index 4b7f07f9e..79cf8e6f3 100644 --- a/lib/simplecov/result_adapter.rb +++ b/lib/simplecov/result_adapter.rb @@ -20,7 +20,7 @@ def adapt result.each_with_object({}) do |(file_name, cover_statistic), adapted_result| if cover_statistic.is_a?(Array) - adapted_result.merge!(file_name => {"lines" => cover_statistic}) + adapted_result.merge!(file_name => {lines: cover_statistic}) else adapted_result.merge!(file_name => cover_statistic) end diff --git a/lib/simplecov/result_merger.rb b/lib/simplecov/result_merger.rb index a6b2e92ec..fbaf54398 100644 --- a/lib/simplecov/result_merger.rb +++ b/lib/simplecov/result_merger.rb @@ -31,20 +31,19 @@ def merge_results(*file_paths, ignore_timeout: false) # In big CI setups you might deal with 100s of CI jobs and each one producing Megabytes # of data. Reading them all in easily produces Gigabytes of memory consumption which # we want to avoid. - # - # For similar reasons a SimpleCov::Result is only created in the end as that'd create - # even more data especially when it also reads in all source files. - initial_memo = valid_results(file_paths.shift, ignore_timeout: ignore_timeout) - command_names, coverage = file_paths.reduce(initial_memo) do |memo, file_path| - merge_coverage(memo, valid_results(file_path, ignore_timeout: ignore_timeout)) - end + file_paths = file_paths.dup + initial_result = merge_file_results(file_paths.shift, ignore_timeout: ignore_timeout) - create_result(command_names, coverage) + file_paths.reduce(initial_result) do |memo, path| + file_result = merge_file_results(path, ignore_timeout: ignore_timeout) + merge_coverage([memo, file_result]) + end end - def valid_results(file_path, ignore_timeout: false) - results = parse_file(file_path) + def merge_file_results(file_path, ignore_timeout:) + raw_results = parse_file(file_path) + results = Result.from_hash(raw_results) merge_valid_results(results, ignore_timeout: ignore_timeout) end @@ -72,42 +71,25 @@ def parse_json(content) end def merge_valid_results(results, ignore_timeout: false) - results = results.select { |_command_name, data| within_merge_timeout?(data) } unless ignore_timeout - - command_plus_coverage = results.map do |command_name, data| - [[command_name], adapt_result(data.fetch("coverage"))] - end - - # one file itself _might_ include multiple test runs - merge_coverage(*command_plus_coverage) + results = results.select { |x| within_merge_timeout?(x) } unless ignore_timeout + merge_coverage(results) end - def within_merge_timeout?(data) - time_since_result_creation(data) < SimpleCov.merge_timeout + def within_merge_timeout?(result) + Time.now - result.created_at < SimpleCov.merge_timeout end - def time_since_result_creation(data) - Time.now - Time.at(data.fetch("timestamp")) - end - - def create_result(command_names, coverage) - return nil unless coverage - - command_name = command_names.reject(&:empty?).sort.join(", ") - SimpleCov::Result.new(coverage, command_name: command_name) - end + def merge_coverage(results) + results = results.compact - def merge_coverage(*results) - return [[""], nil] if results.empty? + return nil if results.empty? return results.first if results.size == 1 - results.reduce do |(memo_command, memo_coverage), (command, coverage)| - # timestamp is dropped here, which is intentional (we merge it, it gets a new time stamp as of now) - merged_coverage = Combine.combine(Combine::ResultsCombiner, memo_coverage, coverage) - merged_command = memo_command + command - - [merged_command, merged_coverage] - end + parsed_results = results.map(&:original_result) + combined_result = SimpleCov::Combine::ResultsCombiner.combine(*parsed_results) + result = SimpleCov::Result.new(combined_result) + result.command_name = results.map(&:command_name).reject(&:empty?).sort.join(", ") + result end # @@ -118,9 +100,8 @@ def merged_result # conceptually this is just doing `merge_results(resultset_path)` # it's more involved to make syre `synchronize_resultset` is only used around reading resultset_hash = read_resultset - command_names, coverage = merge_valid_results(resultset_hash) - - create_result(command_names, coverage) + results = Result.from_hash(resultset_hash) + merge_valid_results(results) end def read_resultset @@ -164,31 +145,6 @@ def synchronize_resultset @resultset_locked = false end end - - # We changed the format of the raw result data in simplecov, as people are likely - # to have "old" resultsets lying around (but not too old so that they're still - # considered we can adapt them). - # See https://github.com/simplecov-ruby/simplecov/pull/824#issuecomment-576049747 - def adapt_result(result) - if pre_simplecov_0_18_result?(result) - adapt_pre_simplecov_0_18_result(result) - else - result - end - end - - # pre 0.18 coverage data pointed from file directly to an array of line coverage - def pre_simplecov_0_18_result?(result) - _key, data = result.first - - data.is_a?(Array) - end - - def adapt_pre_simplecov_0_18_result(result) - result.transform_values do |line_coverage_data| - {"lines" => line_coverage_data} - end - end end end end diff --git a/lib/simplecov/result_serialization.rb b/lib/simplecov/result_serialization.rb new file mode 100644 index 000000000..2a5f6e717 --- /dev/null +++ b/lib/simplecov/result_serialization.rb @@ -0,0 +1,119 @@ +# frozen_string_literal: true + +module SimpleCov + class ResultSerialization + class << self + def serialize(result) + coverage = {} + + result.original_result.each do |file_path, file_data| + serializable_file_data = {} + + file_data.each do |key, value| + serializable_file_data[key] = serialize_value(key, value) + end + + coverage[file_path] = serializable_file_data + end + + data = {"coverage" => coverage, "timestamp" => result.created_at.to_i} + {result.command_name => data} + end + + def deserialize(hash) # rubocop:disable Metrics/MethodLength + hash.map do |command_name, data| + coverage = {} + + data.fetch("coverage").each do |file_name, file_data| + parsed_file_data = {} + + file_data = {lines: file_data} if file_data.is_a?(Array) + + file_data.each do |key, value| + key = key.to_sym + parsed_file_data[key] = deserialize_value(key, value) + end + + coverage[file_name] = parsed_file_data + end + + result = SimpleCov::Result.new(coverage) + result.command_name = command_name + result.created_at = Time.at(data.fetch("timestamp")) + result + end + end + + private + + def serialize_value(key, value) # rubocop:disable Metrics/MethodLength + case key + when :branches + value.map { |k, v| [k, v.to_a] } + when :methods + value.map do |methods_data, coverage| + klass, *info = methods_data + # Replace all memory addresses with 0 since they are inconsistent between test runs + serialized_klass = klass.to_s.sub(/0x[0-9a-f]{16}/, "0x0000000000000000") + serialized_methods_data = [serialized_klass, *info] + [serialized_methods_data, coverage] + end + else + value + end + end + + def deserialize_value(key, value) + case key + when :branches + deserialize_branches(value) + when :methods + deserialize_methods(value) + else + value + end + end + + def deserialize_branches(value) + result = {} + + value.each do |serialized_root, serialized_coverage_data| + root = deserialize_branch_info(serialized_root) + coverage_data = {} + + serialized_coverage_data.each do |serialized_branch, coverage| + branch = deserialize_branch_info(serialized_branch) + coverage_data[branch] = coverage + end + + result[root] = coverage_data + end + + result + end + + def deserialize_branch_info(value) + value = adapt_old_style_branch_info(value) if value.is_a?(Symbol) + type, *info = value + [type.to_sym, *info] + end + + def deserialize_methods(value) + result = Hash.new { |hash, key| hash[key] = 0 } + + value.each do |serialized_info, coverage| + klass, method_name, *info = serialized_info + info = [klass, method_name.to_sym, *info] + # Info keys might be non-unique since we replace memory addresses with 0 + result[info] += coverage + end + + result + end + + def adapt_old_style_branch_info(value) + eval(value.to_s) # rubocop:disable Security/Eval + end + end + end +end diff --git a/lib/simplecov/simulate_coverage.rb b/lib/simplecov/simulate_coverage.rb index 044438652..f75122db4 100644 --- a/lib/simplecov/simulate_coverage.rb +++ b/lib/simplecov/simulate_coverage.rb @@ -19,10 +19,11 @@ def call(absolute_path) lines = File.foreach(absolute_path) { - "lines" => LinesClassifier.new.classify(lines), + lines: LinesClassifier.new.classify(lines), # we don't want to parse branches ourselves... # requiring files can have side effects and we don't want to trigger that - "branches" => {} + branches: {}, + methods: {} } end end diff --git a/lib/simplecov/source_file.rb b/lib/simplecov/source_file.rb index c68dfd2d8..97d56e9f0 100644 --- a/lib/simplecov/source_file.rb +++ b/lib/simplecov/source_file.rb @@ -33,7 +33,8 @@ def coverage_statistics @coverage_statistics ||= { **line_coverage_statistics, - **branch_coverage_statistics + **branch_coverage_statistics, + **method_coverage_statistics } end @@ -154,6 +155,26 @@ def line_with_missed_branch?(line_number) branches_for_line(line_number).any? { |_type, count| count.zero? } end + def methods + @methods ||= build_methods + end + + def total_methods + @total_methods ||= covered_methods + missed_methods + end + + def covered_methods + methods.select(&:covered?) + end + + def missed_methods + methods.select(&:missed?) + end + + def methods_coverage_percent + coverage_statistics[:method]&.percent + end + private # no_cov_chunks is zero indexed to work directly with the array holding the lines @@ -223,9 +244,9 @@ def ensure_remove_undefs(file_lines) end def build_lines - coverage_exceeding_source_warn if coverage_data["lines"].size > src.size + coverage_exceeding_source_warn if lines_data.size > src.size lines = src.map.with_index(1) do |src, i| - SimpleCov::SourceFile::Line.new(src, i, coverage_data["lines"][i - 1]) + SimpleCov::SourceFile::Line.new(src, i, lines_data[i - 1]) end process_skipped_lines(lines) end @@ -243,9 +264,13 @@ def lines_strength lines.sum { |line| line.coverage.to_i } end + def lines_data + coverage_data.fetch(:lines) + end + # Warning to identify condition from Issue #56 def coverage_exceeding_source_warn - warn "Warning: coverage data provided by Coverage [#{coverage_data['lines'].size}] exceeds number of lines in #{filename} [#{src.size}]" + warn "Warning: coverage data provided by Coverage [#{lines_data.size}] exceeds number of lines in #{filename} [#{src.size}]" end # @@ -267,7 +292,7 @@ def build_branches_report # @return [Array] # def build_branches - coverage_branch_data = coverage_data.fetch("branches", {}) + coverage_branch_data = coverage_data.fetch(:branches, {}) branches = coverage_branch_data.flat_map do |condition, coverage_branches| build_branches_from(condition, coverage_branches) end @@ -285,34 +310,15 @@ def process_skipped_branches(branches) branches end - # Since we are dumping to and loading from JSON, and we have arrays as keys those - # don't make their way back to us intact e.g. just as a string - # - # We should probably do something different here, but as it stands these are - # our data structures that we write so eval isn't _too_ bad. - # - # See #801 - # - def restore_ruby_data_structure(structure) - # Tests use the real data structures (except for integration tests) so no need to - # put them through here. - return structure if structure.is_a?(Array) - - # rubocop:disable Security/Eval - eval structure - # rubocop:enable Security/Eval - end - def build_branches_from(condition, branches) # the format handed in from the coverage data is like this: # # [:then, 4, 6, 6, 6, 10] # # which is [type, id, start_line, start_col, end_line, end_col] - _condition_type, _condition_id, condition_start_line, * = restore_ruby_data_structure(condition) + _condition_type, _condition_id, condition_start_line, * = condition branches.map do |branch_data, hit_count| - branch_data = restore_ruby_data_structure(branch_data) build_branch(branch_data, hit_count, condition_start_line) end end @@ -329,12 +335,18 @@ def build_branch(branch_data, hit_count, condition_start_line) ) end + def build_methods + coverage_data.fetch(:methods, []).map do |info, coverage| + SourceFile::Method.new(self, info, coverage) + end + end + def line_coverage_statistics { line: CoverageStatistics.new( total_strength: lines_strength, - covered: covered_lines.size, - missed: missed_lines.size + covered: covered_lines.size, + missed: missed_lines.size ) } end @@ -343,7 +355,16 @@ def branch_coverage_statistics { branch: CoverageStatistics.new( covered: covered_branches.size, - missed: missed_branches.size + missed: missed_branches.size + ) + } + end + + def method_coverage_statistics + { + method: CoverageStatistics.new( + covered: covered_methods.size, + missed: missed_methods.size ) } end diff --git a/lib/simplecov/source_file/method.rb b/lib/simplecov/source_file/method.rb new file mode 100644 index 000000000..3096dad4e --- /dev/null +++ b/lib/simplecov/source_file/method.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +module SimpleCov + class SourceFile + class Method + attr_reader :source_file, :coverage, :klass, :method, :start_line, :start_col, :end_line, :end_col + + def initialize(source_file, info, coverage) + @source_file = source_file + @klass, @method, @start_line, @start_col, @end_line, @end_col = info + @coverage = coverage + end + + def covered? + !skipped? && coverage.positive? + end + + def skipped? + return @skipped if defined?(@skipped) + + @skipped = lines.all?(&:skipped?) + end + + def missed? + !skipped? && coverage.zero? + end + + def lines + @lines ||= source_file.lines[(start_line - 1)..(end_line - 1)] + end + + def to_s + "#{klass}##{method}" + end + end + end +end diff --git a/lib/simplecov/utils.rb b/lib/simplecov/utils.rb new file mode 100644 index 000000000..116351f16 --- /dev/null +++ b/lib/simplecov/utils.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module SimpleCov + # Functionally for rounding coverage results + module Utils + module_function + + # + # @api private + # + # Rounding down to be extra strict, see #679 + def round_coverage(coverage) + coverage.floor(2) + end + + def render_coverage(coverage) + format("%.2f%%", round_coverage(coverage)) + end + end +end diff --git a/spec/combine/results_combiner_spec.rb b/spec/combine/results_combiner_spec.rb index 67e1a96ac..64e8e2036 100644 --- a/spec/combine/results_combiner_spec.rb +++ b/spec/combine/results_combiner_spec.rb @@ -7,38 +7,40 @@ let(:resultset1) do { source_fixture("sample.rb") => { - "lines" => [nil, 1, 1, 1, nil, nil, 1, 1, nil, nil], - "branches" => {[:if, 3, 8, 6, 8, 36] => {[:then, 4, 8, 6, 8, 12] => 47, [:else, 5, 8, 6, 8, 36] => 24}} + lines: [nil, 1, 1, 1, nil, nil, 1, 1, nil, nil], + branches: {[:if, 3, 8, 6, 8, 36] => {[:then, 4, 8, 6, 8, 12] => 47, [:else, 5, 8, 6, 8, 36] => 24}} }, source_fixture("app/models/user.rb") => { - "lines" => [nil, 1, 1, 1, nil, nil, 1, 0, nil, nil], - "branches" => {[:if, 3, 8, 6, 8, 36] => {[:then, 4, 8, 6, 8, 12] => 47, [:else, 5, 8, 6, 8, 36] => 24}} + lines: [nil, 1, 1, 1, nil, nil, 1, 0, nil, nil], + branches: {[:if, 3, 8, 6, 8, 36] => {[:then, 4, 8, 6, 8, 12] => 47, [:else, 5, 8, 6, 8, 36] => 24}}, + methods: {["#", "foo", 4, 2, 6, 5] => 1} }, - source_fixture("app/controllers/sample_controller.rb") => {"lines" => [nil, 1, 1, 1, nil, nil, 1, 0, nil, nil]}, - source_fixture("resultset1.rb") => {"lines" => [1, 1, 1, 1]}, - source_fixture("parallel_tests.rb") => {"lines" => [nil, 0, nil, 0]}, - source_fixture("conditionally_loaded_1.rb") => {"lines" => [nil, 0, 1]}, # loaded only in the first resultset - source_fixture("three.rb") => {"lines" => [nil, 1, 1]} + source_fixture("app/controllers/sample_controller.rb") => {lines: [nil, 1, 1, 1, nil, nil, 1, 0, nil, nil]}, + source_fixture("resultset1.rb") => {lines: [1, 1, 1, 1]}, + source_fixture("parallel_tests.rb") => {lines: [nil, 0, nil, 0]}, + source_fixture("conditionally_loaded_1.rb") => {lines: [nil, 0, 1]}, # loaded only in the first resultset + source_fixture("three.rb") => {lines: [nil, 1, 1]} } end let(:resultset2) do { - source_fixture("sample.rb") => {"lines" => [1, nil, 1, 1, nil, nil, 1, 1, nil, nil]}, + source_fixture("sample.rb") => {lines: [1, nil, 1, 1, nil, nil, 1, 1, nil, nil]}, source_fixture("app/models/user.rb") => { - "lines" => [nil, 1, 5, 1, nil, nil, 1, 0, nil, nil], - "branches" => {[:if, 3, 8, 6, 8, 36] => {[:then, 4, 8, 6, 8, 12] => 1, [:else, 5, 8, 6, 8, 36] => 2}} + lines: [nil, 1, 5, 1, nil, nil, 1, 0, nil, nil], + branches: {[:if, 3, 8, 6, 8, 36] => {[:then, 4, 8, 6, 8, 12] => 1, [:else, 5, 8, 6, 8, 36] => 2}}, + methods: {["#", "foo", 4, 2, 6, 5] => 5, ["#", "bar", 1, 2, 3, 4] => 3} }, - source_fixture("app/controllers/sample_controller.rb") => {"lines" => [nil, 3, 1, nil, nil, nil, 1, 0, nil, nil]}, - source_fixture("resultset2.rb") => {"lines" => [nil, 1, 1, nil]}, - source_fixture("parallel_tests.rb") => {"lines" => [nil, nil, 0, 0]}, - source_fixture("conditionally_loaded_2.rb") => {"lines" => [nil, 0, 1]}, # loaded only in the second resultset - source_fixture("three.rb") => {"lines" => [nil, 1, 4]} + source_fixture("app/controllers/sample_controller.rb") => {lines: [nil, 3, 1, nil, nil, nil, 1, 0, nil, nil]}, + source_fixture("resultset2.rb") => {lines: [nil, 1, 1, nil]}, + source_fixture("parallel_tests.rb") => {lines: [nil, nil, 0, 0]}, + source_fixture("conditionally_loaded_2.rb") => {lines: [nil, 0, 1]}, # loaded only in the second resultset + source_fixture("three.rb") => {lines: [nil, 1, 4]} } end let(:resultset3) do - {source_fixture("three.rb") => {"lines" => [nil, 1, 2]}} + {source_fixture("three.rb") => {lines: [nil, 1, 2]}} end after do @@ -47,6 +49,7 @@ before do SimpleCov.enable_coverage :branch + SimpleCov.enable_coverage :method end context "a merge" do @@ -55,74 +58,77 @@ end it "has proper results for sample.rb" do - expect(subject[source_fixture("sample.rb")]["lines"]).to eq([1, 1, 2, 2, nil, nil, 2, 2, nil, nil]) + expect(subject[source_fixture("sample.rb")][:lines]).to eq([1, 1, 2, 2, nil, nil, 2, 2, nil, nil]) # gotta configure max line so it doesn't get ridiculous - # rubocop:disable Style/IfUnlessModifier if SimpleCov.branch_coverage_supported? - expect(subject[source_fixture("sample.rb")]["branches"][[:if, 3, 8, 6, 8, 36]][[:then, 4, 8, 6, 8, 12]]).to eq(47) + expect(subject[source_fixture("sample.rb")][:branches][[:if, 3, 8, 6, 8, 36]][[:then, 4, 8, 6, 8, 12]]).to eq(47) + expect(subject[source_fixture("sample.rb")][:methods]).to eq(nil) end - # rubocop:enable Style/IfUnlessModifier end it "has proper results for user.rb" do - expect(subject[source_fixture("app/models/user.rb")]["lines"]).to eq([nil, 2, 6, 2, nil, nil, 2, 0, nil, nil]) + expect(subject[source_fixture("app/models/user.rb")][:lines]).to eq([nil, 2, 6, 2, nil, nil, 2, 0, nil, nil]) if SimpleCov.branch_coverage_supported? - expect(subject[source_fixture("app/models/user.rb")]["branches"][[:if, 3, 8, 6, 8, 36]][[:then, 4, 8, 6, 8, 12]]).to eq(48) - expect(subject[source_fixture("app/models/user.rb")]["branches"][[:if, 3, 8, 6, 8, 36]][[:else, 5, 8, 6, 8, 36]]).to eq(26) + expect(subject[source_fixture("app/models/user.rb")][:branches][[:if, 3, 8, 6, 8, 36]][[:then, 4, 8, 6, 8, 12]]).to eq(48) + expect(subject[source_fixture("app/models/user.rb")][:branches][[:if, 3, 8, 6, 8, 36]][[:else, 5, 8, 6, 8, 36]]).to eq(26) + expect(subject[source_fixture("app/models/user.rb")][:methods]).to eq( + ["#", "foo", 4, 2, 6, 5] => 6, + ["#", "bar", 1, 2, 3, 4] => 3 + ) end end it "has proper results for sample_controller.rb" do - expect(subject[source_fixture("app/controllers/sample_controller.rb")]["lines"]).to eq([nil, 4, 2, 1, nil, nil, 2, 0, nil, nil]) + expect(subject[source_fixture("app/controllers/sample_controller.rb")][:lines]).to eq([nil, 4, 2, 1, nil, nil, 2, 0, nil, nil]) end it "has proper results for resultset1.rb" do - expect(subject[source_fixture("resultset1.rb")]["lines"]).to eq([1, 1, 1, 1]) + expect(subject[source_fixture("resultset1.rb")][:lines]).to eq([1, 1, 1, 1]) end it "has proper results for resultset2.rb" do - expect(subject[source_fixture("resultset2.rb")]["lines"]).to eq([nil, 1, 1, nil]) + expect(subject[source_fixture("resultset2.rb")][:lines]).to eq([nil, 1, 1, nil]) end it "has proper results for parallel_tests.rb" do - expect(subject[source_fixture("parallel_tests.rb")]["lines"]).to eq([nil, nil, nil, 0]) + expect(subject[source_fixture("parallel_tests.rb")][:lines]).to eq([nil, nil, nil, 0]) end it "has proper results for conditionally_loaded_1.rb" do - expect(subject[source_fixture("conditionally_loaded_1.rb")]["lines"]).to eq([nil, 0, 1]) + expect(subject[source_fixture("conditionally_loaded_1.rb")][:lines]).to eq([nil, 0, 1]) end it "has proper results for conditionally_loaded_2.rb" do - expect(subject[source_fixture("conditionally_loaded_2.rb")]["lines"]).to eq([nil, 0, 1]) + expect(subject[source_fixture("conditionally_loaded_2.rb")][:lines]).to eq([nil, 0, 1]) end it "has proper results for three.rb" do - expect(subject[source_fixture("three.rb")]["lines"]).to eq([nil, 3, 7]) + expect(subject[source_fixture("three.rb")][:lines]).to eq([nil, 3, 7]) end it "always returns a Hash object for branches", if: SimpleCov.branch_coverage_supported? do - expect(subject[source_fixture("three.rb")]["branches"]).to eq({}) + expect(subject[source_fixture("three.rb")][:branches]).to eq({}) end end end it "merges frozen resultsets" do resultset1 = { - source_fixture("sample.rb").freeze => {"lines" => [nil, 1, 1, 1, nil, nil, 1, 1, nil, nil]}, - source_fixture("app/models/user.rb").freeze => {"lines" => [nil, 1, 1, 1, nil, nil, 1, 0, nil, nil]} + source_fixture("sample.rb").freeze => {lines: [nil, 1, 1, 1, nil, nil, 1, 1, nil, nil]}, + source_fixture("app/models/user.rb").freeze => {lines: [nil, 1, 1, 1, nil, nil, 1, 0, nil, nil]} } resultset2 = { - source_fixture("sample.rb").freeze => {"lines" => [1, nil, 1, 1, nil, nil, 1, 1, nil, nil]} + source_fixture("sample.rb").freeze => {lines: [1, nil, 1, 1, nil, nil, 1, 1, nil, nil]} } merged_result = SimpleCov::Combine::ResultsCombiner.combine(resultset1, resultset2) expect(merged_result.keys).to eq(resultset1.keys) expect(merged_result.values.map(&:frozen?)).to eq([false, false]) - expect(merged_result[source_fixture("sample.rb")]["lines"]).to eq([1, 1, 2, 2, nil, nil, 2, 2, nil, nil]) - expect(merged_result[source_fixture("app/models/user.rb")]["lines"]).to eq([nil, 1, 1, 1, nil, nil, 1, 0, nil, nil]) + expect(merged_result[source_fixture("sample.rb")][:lines]).to eq([1, 1, 2, 2, nil, nil, 2, 2, nil, nil]) + expect(merged_result[source_fixture("app/models/user.rb")][:lines]).to eq([nil, 1, 1, 1, nil, nil, 1, 0, nil, nil]) end end diff --git a/spec/command_guesser_spec.rb b/spec/command_guesser_spec.rb index 21931cacb..a42cf9302 100644 --- a/spec/command_guesser_spec.rb +++ b/spec/command_guesser_spec.rb @@ -6,44 +6,44 @@ subject { SimpleCov::CommandGuesser } it 'correctly guesses "Unit Tests" for unit tests' do - subject.original_run_command = "/some/path/test/units/foo_bar_test.rb" + allow(subject).to receive(:original_run_command) { "/some/path/test/units/foo_bar_test.rb" } expect(subject.guess).to eq("Unit Tests") - subject.original_run_command = "test/units/foo.rb" + allow(subject).to receive(:original_run_command) { "test/units/foo.rb" } expect(subject.guess).to eq("Unit Tests") - subject.original_run_command = "test/foo.rb" + allow(subject).to receive(:original_run_command) { "test/foo.rb" } expect(subject.guess).to eq("Unit Tests") - subject.original_run_command = "test/{models,helpers,unit}/**/*_test.rb" + allow(subject).to receive(:original_run_command) { "test/{models,helpers,unit}/**/*_test.rb" } expect(subject.guess).to eq("Unit Tests") end it 'correctly guesses "Functional Tests" for functional tests' do - subject.original_run_command = "/some/path/test/functional/foo_bar_controller_test.rb" + allow(subject).to receive(:original_run_command) { "/some/path/test/functional/foo_bar_controller_test.rb" } expect(subject.guess).to eq("Functional Tests") - subject.original_run_command = "test/{controllers,mailers,functional}/**/*_test.rb" + allow(subject).to receive(:original_run_command) { "test/{controllers,mailers,functional}/**/*_test.rb" } expect(subject.guess).to eq("Functional Tests") end it 'correctly guesses "Integration Tests" for integration tests' do - subject.original_run_command = "/some/path/test/integration/foo_bar_controller_test.rb" + allow(subject).to receive(:original_run_command) { "/some/path/test/integration/foo_bar_controller_test.rb" } expect(subject.guess).to eq("Integration Tests") - subject.original_run_command = "test/integration/**/*_test.rb" + allow(subject).to receive(:original_run_command) { "test/integration/**/*_test.rb" } expect(subject.guess).to eq("Integration Tests") end it 'correctly guesses "Cucumber Features" for cucumber features' do - subject.original_run_command = "features" + allow(subject).to receive(:original_run_command) { "features" } expect(subject.guess).to eq("Cucumber Features") - subject.original_run_command = "cucumber" + allow(subject).to receive(:original_run_command) { "cucumber" } expect(subject.guess).to eq("Cucumber Features") end it 'correctly guesses "RSpec" for RSpec' do - subject.original_run_command = "/some/path/spec/foo.rb" + allow(subject).to receive(:original_run_command) { "/some/path/spec/foo.rb" } expect(subject.guess).to eq("RSpec") end it "defaults to RSpec because RSpec constant is defined" do - subject.original_run_command = "some_arbitrary_command with arguments" + allow(subject).to receive(:original_run_command) { "some_arbitrary_command with arguments" } expect(subject.guess).to eq("RSpec") end end diff --git a/spec/configuration_spec.rb b/spec/configuration_spec.rb index 7cae394e8..9fd65fe01 100644 --- a/spec/configuration_spec.rb +++ b/spec/configuration_spec.rb @@ -90,6 +90,14 @@ expect(config.public_send(coverage_setting)).to eq branch: 85.0, line: 95.4 end + it "sets the right coverage when called with line, branch and method" do + config.enable_coverage :branch + config.enable_coverage :method + config.minimum_coverage branch: 85.0, line: 95.4, method: 91.5 + + expect(config.minimum_coverage).to eq branch: 85.0, line: 95.4, method: 91.5 + end + it "raises when trying to set branch coverage but not enabled" do expect do config.public_send(coverage_setting, {branch: 42}) @@ -177,6 +185,12 @@ expect(config.coverage_criterion).to eq :branch end + it "works fine with :method" do + config.coverage_criterion :method + + expect(config.coverage_criterion).to eq :method + end + it "works fine setting it back and forth" do config.coverage_criterion :branch config.coverage_criterion :line @@ -231,6 +245,20 @@ end end + describe "#method_coverage?", if: SimpleCov.method_coverage_supported? do + it "returns true of method coverage is being measured" do + config.enable_coverage :method + + expect(config).to be_method_coverage + end + + it "returns false for line coverage" do + config.coverage_criterion :line + + expect(config).not_to be_method_coverage + end + end + describe "#enable_for_subprocesses" do it "returns false by default" do expect(config.enable_for_subprocesses).to eq false diff --git a/spec/coverage_for_eval_spec.rb b/spec/coverage_for_eval_spec.rb index 87d7a871b..37a20f037 100644 --- a/spec/coverage_for_eval_spec.rb +++ b/spec/coverage_for_eval_spec.rb @@ -19,7 +19,7 @@ let(:command) { "bundle e ruby eval_test.rb" } it "records coverage for erb" do - expect(@stdout).to include("Line Coverage: 66.67% (2 / 3)") + expect(@stdout).to include("Line coverage: 2 / 3 (66.67%)") end end end diff --git a/spec/file_list_spec.rb b/spec/file_list_spec.rb index f0d336291..10033216b 100644 --- a/spec/file_list_spec.rb +++ b/spec/file_list_spec.rb @@ -6,16 +6,19 @@ subject do original_result = { source_fixture("sample.rb") => { - "lines" => [nil, 1, 1, 1, nil, nil, 1, 1, nil, nil], - "branches" => {} + lines: [nil, 1, 1, 1, nil, nil, 1, 1, nil, nil], + branches: {}, + methods: {} }, source_fixture("app/models/user.rb") => { - "lines" => [nil, 1, 1, 1, nil, nil, 1, 0, nil, nil], - "branches" => {} + lines: [nil, 1, 1, 1, nil, nil, 1, 0, nil, nil], + branches: {}, + methods: {} }, source_fixture("app/controllers/sample_controller.rb") => { - "lines" => [nil, 2, 2, 0, nil, nil, 0, nil, nil, nil], - "branches" => {} + lines: [nil, 2, 2, 0, nil, nil, 0, nil, nil, nil], + branches: {}, + methods: {} } } SimpleCov::Result.new(original_result).files diff --git a/spec/fixtures/coverer.rb b/spec/fixtures/coverer.rb index b6d9e53b2..381bdeb0f 100644 --- a/spec/fixtures/coverer.rb +++ b/spec/fixtures/coverer.rb @@ -2,8 +2,5 @@ require "coverage" Coverage.start(:all) -require_relative "uneven_nocovs" - -UnevenNocov.call(42) - +require_relative "methods" p Coverage.result diff --git a/spec/fixtures/methods.rb b/spec/fixtures/methods.rb new file mode 100644 index 000000000..de753ef30 --- /dev/null +++ b/spec/fixtures/methods.rb @@ -0,0 +1,18 @@ +class A + def method1 + puts "hello from method1" + method2 + end + +private + + def method2 + puts "hello from method2" + end + + def method3 + puts "hello from method3" + end +end + +A.new.method1 diff --git a/spec/result_merger_spec.rb b/spec/result_merger_spec.rb index 4d4c0e712..9120222dc 100644 --- a/spec/result_merger_spec.rb +++ b/spec/result_merger_spec.rb @@ -11,36 +11,36 @@ let(:resultset1) do { - source_fixture("sample.rb") => {"lines" => [nil, 1, 1, 1, nil, nil, 1, 1, nil, nil]}, - source_fixture("app/models/user.rb") => {"lines" => [nil, 1, 1, 1, nil, nil, 1, 0, nil, nil]}, - source_fixture("app/controllers/sample_controller.rb") => {"lines" => [nil, 1, 1, 1, nil, nil, 1, 0, nil, nil]}, - source_fixture("resultset1.rb") => {"lines" => [1, 1, 1, 1]}, - source_fixture("parallel_tests.rb") => {"lines" => [nil, 0, nil, 0]}, - source_fixture("conditionally_loaded_1.rb") => {"lines" => [nil, 0, 1]} # loaded only in the first resultset + source_fixture("sample.rb") => {lines: [nil, 1, 1, 1, nil, nil, 1, 1, nil, nil]}, + source_fixture("app/models/user.rb") => {lines: [nil, 1, 1, 1, nil, nil, 1, 0, nil, nil]}, + source_fixture("app/controllers/sample_controller.rb") => {lines: [nil, 1, 1, 1, nil, nil, 1, 0, nil, nil]}, + source_fixture("resultset1.rb") => {lines: [1, 1, 1, 1]}, + source_fixture("parallel_tests.rb") => {lines: [nil, 0, nil, 0]}, + source_fixture("conditionally_loaded_1.rb") => {lines: [nil, 0, 1]} # loaded only in the first resultset } end let(:resultset2) do { - source_fixture("sample.rb") => {"lines" => [1, nil, 1, 1, nil, nil, 1, 1, nil, nil]}, - source_fixture("app/models/user.rb") => {"lines" => [nil, 1, 5, 1, nil, nil, 1, 0, nil, nil]}, - source_fixture("app/controllers/sample_controller.rb") => {"lines" => [nil, 3, 1, nil, nil, nil, 1, 0, nil, nil]}, - source_fixture("resultset2.rb") => {"lines" => [nil, 1, 1, nil]}, - source_fixture("parallel_tests.rb") => {"lines" => [nil, nil, 0, 0]}, - source_fixture("conditionally_loaded_2.rb") => {"lines" => [nil, 0, 1]} # loaded only in the second resultset + source_fixture("sample.rb") => {lines: [1, nil, 1, 1, nil, nil, 1, 1, nil, nil]}, + source_fixture("app/models/user.rb") => {lines: [nil, 1, 5, 1, nil, nil, 1, 0, nil, nil]}, + source_fixture("app/controllers/sample_controller.rb") => {lines: [nil, 3, 1, nil, nil, nil, 1, 0, nil, nil]}, + source_fixture("resultset2.rb") => {lines: [nil, 1, 1, nil]}, + source_fixture("parallel_tests.rb") => {lines: [nil, nil, 0, 0]}, + source_fixture("conditionally_loaded_2.rb") => {lines: [nil, 0, 1]} # loaded only in the second resultset } end let(:merged_resultset1_and2) do { - source_fixture("sample.rb") => {"lines" => [1, 1, 2, 2, nil, nil, 2, 2, nil, nil]}, - source_fixture("app/models/user.rb") => {"lines" => [nil, 2, 6, 2, nil, nil, 2, 0, nil, nil]}, - source_fixture("app/controllers/sample_controller.rb") => {"lines" => [nil, 4, 2, 1, nil, nil, 2, 0, nil, nil]}, - source_fixture("resultset1.rb") => {"lines" => [1, 1, 1, 1]}, - source_fixture("parallel_tests.rb") => {"lines" => [nil, nil, nil, 0]}, - source_fixture("conditionally_loaded_1.rb") => {"lines" => [nil, 0, 1]}, - source_fixture("resultset2.rb") => {"lines" => [nil, 1, 1, nil]}, - source_fixture("conditionally_loaded_2.rb") => {"lines" => [nil, 0, 1]} + source_fixture("sample.rb") => {lines: [1, 1, 2, 2, nil, nil, 2, 2, nil, nil]}, + source_fixture("app/models/user.rb") => {lines: [nil, 2, 6, 2, nil, nil, 2, 0, nil, nil]}, + source_fixture("app/controllers/sample_controller.rb") => {lines: [nil, 4, 2, 1, nil, nil, 2, 0, nil, nil]}, + source_fixture("resultset1.rb") => {lines: [1, 1, 1, 1]}, + source_fixture("parallel_tests.rb") => {lines: [nil, nil, nil, 0]}, + source_fixture("conditionally_loaded_1.rb") => {lines: [nil, 0, 1]}, + source_fixture("resultset2.rb") => {lines: [nil, 1, 1, nil]}, + source_fixture("conditionally_loaded_2.rb") => {lines: [nil, 0, 1]} } end @@ -123,7 +123,7 @@ it "has the result stored" do SimpleCov::ResultMerger.merge_and_store(resultset1_path, resultset2_path) - expect_resultset_1_and_2_merged(SimpleCov::ResultMerger.read_resultset) + expect_resultset_1_and_2_merged(SimpleCov::ResultMerger.merged_result.to_hash) end end @@ -165,6 +165,73 @@ expect_resultset_1_and_2_merged(result_hash) end end + + describe "method coverage", if: SimpleCov.method_coverage_supported? do + before do + SimpleCov.enable_coverage :method + store_result(result3, path: resultset3_path) + end + + after do + SimpleCov.clear_coverage_criteria + end + + let(:resultset1) do + { + source_fixture("methods.rb") => { + methods: { + ["A", :method1, 2, 2, 5, 5] => 1, + ["A", :method2, 9, 2, 11, 5] => 0, + ["A", :method3, 13, 2, 15, 5] => 0 + } + } + } + end + + let(:resultset2) do + { + source_fixture("methods.rb") => { + methods: { + ["A", :method1, 2, 2, 5, 5] => 0, + ["A", :method2, 9, 2, 11, 5] => 1, + ["A", :method3, 13, 2, 15, 5] => 0 + } + } + } + end + + let(:resultset3) do + { + source_fixture("methods.rb") => { + methods: { + ["B", :method1, 2, 2, 5, 5] => 1, + ["B", :method2, 9, 2, 11, 5] => 0, + ["B", :method3, 13, 2, 15, 5] => 0 + } + } + } + end + + let(:result3) { SimpleCov::Result.new(resultset3, command_name: "result3") } + let(:resultset3_path) { "#{resultset_prefix}3.json" } + + it "correctly merges the 3 results" do + result = SimpleCov::ResultMerger.merge_and_store( + resultset1_path, resultset2_path, resultset3_path + ) + + merged_coverage = result.original_result.fetch(source_fixture("methods.rb")) + + expect(merged_coverage.fetch(:methods)).to eq( + ["A", :method1, 2, 2, 5, 5] => 1, + ["A", :method2, 9, 2, 11, 5] => 1, + ["A", :method3, 13, 2, 15, 5] => 0, + ["B", :method1, 2, 2, 5, 5] => 1, + ["B", :method2, 9, 2, 11, 5] => 0, + ["B", :method3, 13, 2, 15, 5] => 0 + ) + end + end end context "pre 0.18 result format" do @@ -191,7 +258,7 @@ result = SimpleCov::ResultMerger.merge_and_store(file_path) expect(result.original_result).to eq( - source_fixture("three.rb") => {"lines" => [nil, 1, 2]} + source_fixture("three.rb") => {lines: [nil, 1, 2]} ) end end diff --git a/spec/result_spec.rb b/spec/result_spec.rb index ef3f37cf8..470708870 100644 --- a/spec/result_spec.rb +++ b/spec/result_spec.rb @@ -21,9 +21,9 @@ let(:original_result) do { - source_fixture("sample.rb") => {"lines" => [nil, 1, 1, 1, nil, nil, 1, 1, nil, nil]}, - source_fixture("app/models/user.rb") => {"lines" => [nil, 1, 1, 1, nil, nil, 1, 0, nil, nil]}, - source_fixture("app/controllers/sample_controller.rb") => {"lines" => [nil, 1, 1, 1, nil, nil, 1, 0, nil, nil]} + source_fixture("sample.rb") => {lines: [nil, 1, 1, 1, nil, nil, 1, 1, nil, nil]}, + source_fixture("app/models/user.rb") => {lines: [nil, 1, 1, 1, nil, nil, 1, 0, nil, nil]}, + source_fixture("app/controllers/sample_controller.rb") => {lines: [nil, 1, 1, 1, nil, nil, 1, 0, nil, nil]} } end @@ -206,33 +206,180 @@ end end - describe ".from_hash" do - let(:other_result) do + describe "#to_hash" do + subject { SimpleCov::Result.new(original_result) } + + let(:original_result) do { - source_fixture("sample.rb") => {"lines" => [nil, 1, 1, 1, nil, nil, 0, 0, nil, nil]} + source_fixture("sample.rb") => { + lines: [nil, 1, 1, 1, nil, nil, 1, 1, nil, nil], + branches: { + [:unless, 0, 8, 4, 8, 90] => { + [:else, 1, 8, 4, 8, 90] => 0, + [:then, 2, 8, 4, 8, 35] => 1 + } + }, + methods: { + ["# 2 + } + } } end + + it "dumps all coverage types properly" do + expect(subject.to_hash).to match( + "RSpec" => { + "coverage" => { + source_fixture("sample.rb") => { + lines: [nil, 1, 1, 1, nil, nil, 1, 1, nil, nil], + branches: [ + [ + [:unless, 0, 8, 4, 8, 90], [ + [[:else, 1, 8, 4, 8, 90], 0], + [[:then, 2, 8, 4, 8, 35], 1] + ] + ] + ], + methods: [ + [["# be_a(Integer) + } + ) + end + end + + describe ".from_hash" do let(:created_at) { Time.now.to_i } - it "can consume multiple commands" do - input = { + let(:input) do + { "rspec" => { - "coverage" => original_result, - "timestamp" => created_at - }, - "cucumber" => { - "coverage" => other_result, + "coverage" => dumped_result, "timestamp" => created_at } } + end + + let(:expected_branch_coverage) do + { + [:unless, 0, 8, 4, 8, 90] => { + [:else, 1, 8, 4, 8, 90] => 0, + [:then, 2, 8, 4, 8, 35] => 1 + } + } + end + + context "branch and method coverage present" do + let(:dumped_result) do + { + source_fixture("sample.rb") => { + "lines" => [nil, 1, 1, 1, nil, nil, 1, 1, nil, nil], + "branches" => [ + [ + ["unless", 0, 8, 4, 8, 90], [ + [["else", 1, 8, 4, 8, 90], 0], + [["then", 2, 8, 4, 8, 35], 1] + ] + ] + ], + "methods" => [ + [["RSpec::ExampleGroups::SomeClass::LetDefinitions", "subject", 6, 10, 6, 34], 2] + ] + } + } + end + + it "parses that properly" do + result = described_class.from_hash(input) + + expect(result.size).to eq(1) + expect(result.first.original_result.size).to eq(1) + + expect(result.first.original_result.values.last).to eq( + lines: [nil, 1, 1, 1, nil, nil, 1, 1, nil, nil], + branches: expected_branch_coverage, + methods: { + ["RSpec::ExampleGroups::SomeClass::LetDefinitions", :subject, 6, 10, 6, 34] => 2 + } + ) + end + end + + context "old style line coverage format" do + let(:dumped_result) do + {source_fixture("sample.rb") => [nil, 1, 1, 1, nil, nil, 1, 1, nil, nil]} + end + + it "parses that properly" do + result = described_class.from_hash(input) + + expect(result.size).to eq(1) + expect(result.first.original_result.size).to eq(1) + + expect(result.first.original_result.values.last).to eq( + lines: [nil, 1, 1, 1, nil, nil, 1, 1, nil, nil] + ) + end + end + + context "old style branch coverage format" do + let(:dumped_result) do + { + source_fixture("sample.rb") => { + "branches" => { + "[:unless, 0, 8, 4, 8, 90]": { + "[:else, 1, 8, 4, 8, 90]": 0, + "[:then, 2, 8, 4, 8, 35]": 1 + } + } + } + } + end - result = described_class.from_hash(input) + it "parses that properly" do + result = described_class.from_hash(input) - expect(result.size).to eq 2 - sorted = result.sort_by(&:command_name) - expect(sorted.map(&:command_name)).to eq %w[cucumber rspec] - expect(sorted.map(&:created_at).map(&:to_i)).to eq [created_at, created_at] - expect(sorted.map(&:original_result)).to eq [other_result, original_result] + expect(result.size).to eq(1) + expect(result.first.original_result.size).to eq(1) + + expect(result.first.original_result.values.last).to eq( + branches: expected_branch_coverage + ) + end + end + + context "multiple commands" do + let(:other_result) do + { + source_fixture("sample.rb") => {lines: [nil, 1, 1, 1, nil, nil, 0, 0, nil, nil]} + } + end + + let(:input) do + { + "rspec" => { + "coverage" => original_result, + "timestamp" => created_at + }, + "cucumber" => { + "coverage" => other_result, + "timestamp" => created_at + } + } + end + + it "can consume multiple commands" do + result = described_class.from_hash(input) + + expect(result.size).to eq 2 + sorted = result.sort_by(&:command_name) + expect(sorted.map(&:command_name)).to eq %w[cucumber rspec] + expect(sorted.map(&:created_at).map(&:to_i)).to eq [created_at, created_at] + expect(sorted.map(&:original_result)).to eq [other_result, original_result] + end end end end diff --git a/spec/simplecov_spec.rb b/spec/simplecov_spec.rb index 5e3937ca4..b2bb96300 100644 --- a/spec/simplecov_spec.rb +++ b/spec/simplecov_spec.rb @@ -191,11 +191,11 @@ describe ".collate" do let(:resultset1) do - {source_fixture("sample.rb") => {"lines" => [nil, 1, 1, 1, nil, nil, 1, 1, nil, nil]}} + {source_fixture("sample.rb") => {lines: [nil, 1, 1, 1, nil, nil, 1, 1, nil, nil]}} end let(:resultset2) do - {source_fixture("sample.rb") => {"lines" => [1, nil, 1, 1, nil, nil, 1, 1, nil, nil]}} + {source_fixture("sample.rb") => {lines: [1, nil, 1, 1, nil, nil, 1, 1, nil, nil]}} end let(:resultset_path) { SimpleCov::ResultMerger.resultset_path } @@ -207,7 +207,7 @@ "result1, result2" => { "coverage" => { source_fixture("sample.rb") => { - "lines" => [1, 1, 2, 2, nil, nil, 2, 2, nil, nil] + lines: [1, 1, 2, 2, nil, nil, 2, 2, nil, nil] } } } @@ -215,7 +215,10 @@ end let(:collated) do - JSON.parse(File.read(resultset_path)).transform_values { |v| v.reject { |k| k == "timestamp" } } + JSON.parse(File.read(resultset_path)).transform_values do |data| + data["coverage"].values.first.transform_keys!(&:to_sym) + data.reject { |k| k == "timestamp" } + end end context "when no files to be merged" do @@ -332,12 +335,20 @@ def expect_merged SimpleCov.send :start_coverage_measurement end - it "starts coverage with lines and branches if branches is activated" do + it "starts coverage with lines and branches if branch coverage is activated" do expect(Coverage).to receive(:start).with({lines: true, branches: true}) SimpleCov.enable_coverage :branch SimpleCov.send :start_coverage_measurement end + + it "starts coverage with lines and methods if method coverage is activated" do + expect(Coverage).to receive(:start).with({lines: true, methods: true}) + + SimpleCov.enable_coverage :method + + SimpleCov.send :start_coverage_measurement + end end end diff --git a/spec/source_file/method_spec.rb b/spec/source_file/method_spec.rb new file mode 100644 index 000000000..bebee5697 --- /dev/null +++ b/spec/source_file/method_spec.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +require "helper" + +describe SimpleCov::SourceFile::Method do + subject { described_class.new(source_file, info, coverage) } + + let(:source_file) do + SimpleCov::SourceFile.new(source_fixture("methods.rb"), lines: {}) + end + + let(:info) { ["A", :method1, 2, 2, 5, 5] } + let(:coverage) { 1 } + + it "is covered" do + expect(subject.covered?).to eq(true) + end + + it "is not skipped" do + expect(subject.skipped?).to eq(false) + end + + it "is not missed" do + expect(subject.missed?).to eq(false) + end + + it "has 4 lines" do + expect(subject.lines.size).to eq(4) + end + + it "converts to string properly" do + expect(subject.to_s).to eq("A#method1") + end + + context "uncovered method" do + let(:coverage) { 0 } + + it "is not covered" do + expect(subject.covered?).to eq(false) + end + + it "is not skipped" do + expect(subject.skipped?).to eq(false) + end + + it "is missed" do + expect(subject.missed?).to eq(true) + end + end +end diff --git a/spec/source_file_spec.rb b/spec/source_file_spec.rb index 86f62b3a2..43609c77e 100644 --- a/spec/source_file_spec.rb +++ b/spec/source_file_spec.rb @@ -4,12 +4,12 @@ describe SimpleCov::SourceFile do COVERAGE_FOR_SAMPLE_RB = { - "lines" => [nil, 1, 1, 1, nil, nil, 1, 0, nil, nil, nil, 1, 0, nil, nil, nil], - "branches" => {} + lines: [nil, 1, 1, 1, nil, nil, 1, 0, nil, nil, nil, 1, 0, nil, nil, nil], + branches: {} }.freeze COVERAGE_FOR_SAMPLE_RB_WITH_MORE_LINES = { - "lines" => [nil, 1, 1, 1, nil, nil, 1, 0, nil, nil, nil, nil, nil, nil, nil, nil, nil] + lines: [nil, 1, 1, 1, nil, nil, 1, 0, nil, nil, nil, nil, nil, nil, nil, nil, nil] }.freeze context "a source file initialized with some coverage data" do @@ -90,23 +90,43 @@ expect(subject.branches_report).to eq({}) end end + + describe "method coverage" do + it "has total methods count 0" do + expect(subject.total_methods.size).to eq(0) + end + + it "has covered methods count 0" do + expect(subject.covered_methods.size).to eq(0) + end + + it "has missed methods count 0" do + expect(subject.missed_methods.size).to eq(0) + end + + it "is considered 100% methods covered" do + expect(subject.methods_coverage_percent).to eq(100.0) + end + end end context "file with branches" do - COVERAGE_FOR_BRANCHES_RB = { - "lines" => [1, 1, 1, nil, 1, nil, 1, 0, nil, 1, nil, nil, nil], - "branches" => { - [:if, 0, 3, 4, 3, 21] => - {[:then, 1, 3, 4, 3, 10] => 0, [:else, 2, 3, 4, 3, 21] => 1}, - [:if, 3, 5, 4, 5, 26] => - {[:then, 4, 5, 16, 5, 20] => 1, [:else, 5, 5, 23, 5, 26] => 0}, - [:if, 6, 7, 4, 11, 7] => - {[:then, 7, 8, 6, 8, 10] => 0, [:else, 8, 10, 6, 10, 9] => 1} + let(:coverage_for_branches_rb) do + { + lines: [1, 1, 1, nil, 1, nil, 1, 0, nil, 1, nil, nil, nil], + branches: { + [:if, 0, 3, 4, 3, 21] => + {[:then, 1, 3, 4, 3, 10] => 0, [:else, 2, 3, 4, 3, 21] => 1}, + [:if, 3, 5, 4, 5, 26] => + {[:then, 4, 5, 16, 5, 20] => 1, [:else, 5, 5, 23, 5, 26] => 0}, + [:if, 6, 7, 4, 11, 7] => + {[:then, 7, 8, 6, 8, 10] => 0, [:else, 8, 10, 6, 10, 9] => 1} + } } - }.freeze + end subject do - SimpleCov::SourceFile.new(source_fixture("branches.rb"), COVERAGE_FOR_BRANCHES_RB) + SimpleCov::SourceFile.new(source_fixture("branches.rb"), coverage_for_branches_rb) end describe "branch coverage" do @@ -163,6 +183,60 @@ end end + context "file with methods" do + let(:coverage_for_methods_rb) do + { + lines: [1, 1, 1, 1, nil, nil, 1, nil, 1, 1, nil, nil, 1, 0, nil, nil, nil, 1], + branches: {}, + methods: { + ["A", :method1, 2, 2, 5, 5] => 1, + ["A", :method2, 9, 2, 11, 5] => 1, + ["A", :method3, 13, 2, 15, 5] => 0 + } + } + end + + subject do + SimpleCov::SourceFile.new(source_fixture("methods.rb"), coverage_for_methods_rb) + end + + describe "method coverage" do + it "has total methods count 0" do + expect(subject.total_methods.size).to eq(3) + end + + it "has covered methods count 0" do + expect(subject.covered_methods.size).to eq(2) + end + + it "has missed methods count 0" do + expect(subject.missed_methods.size).to eq(1) + end + + it "is considered 66.(6)% methods covered" do + expect(subject.methods_coverage_percent).to eq(66.66666666666667) + end + end + + describe "line coverage" do + it "has line coverage" do + expect(subject.covered_percent).to eq 90.0 + end + + it "has 9 covered lines" do + expect(subject.covered_lines.size).to eq 9 + end + + it "has 1 missed line" do + expect(subject.missed_lines.size).to eq 1 + end + + it "has 10 relevant lines" do + expect(subject.relevant_lines).to eq 10 + end + end + end + context "simulating potential Ruby 1.9 defect -- see Issue #56" do subject do SimpleCov::SourceFile.new(source_fixture("sample.rb"), COVERAGE_FOR_SAMPLE_RB_WITH_MORE_LINES) @@ -184,8 +258,8 @@ context "A file that has inline branches" do COVERAGE_FOR_INLINE = { - "lines" => [1, 1, 1, nil, 1, 1, 0, nil, 1, nil, nil, nil, nil], - "branches" => { + lines: [1, 1, 1, nil, 1, 1, 0, nil, 1, nil, nil, nil, nil], + branches: { [:if, 0, 3, 11, 3, 33] => {[:then, 1, 3, 23, 3, 27] => 1, [:else, 2, 3, 30, 3, 33] => 0}, [:if, 3, 6, 6, 10, 9] => @@ -216,7 +290,7 @@ end context "a file that is never relevant" do - COVERAGE_FOR_NEVER_RB = {"lines" => [nil, nil], "branches" => {}}.freeze + COVERAGE_FOR_NEVER_RB = {lines: [nil, nil], branches: {}}.freeze subject do SimpleCov::SourceFile.new(source_fixture("never.rb"), COVERAGE_FOR_NEVER_RB) @@ -233,10 +307,14 @@ it "has 100.0 branch coverage" do expect(subject.branches_coverage_percent).to eq(100.00) end + + it "has 100.0 method coverage" do + expect(subject.methods_coverage_percent).to eq(100.00) + end end context "a file where nothing is ever executed mixed with skipping #563" do - COVERAGE_FOR_SKIPPED_RB = {"lines" => [nil, nil, nil, nil]}.freeze + COVERAGE_FOR_SKIPPED_RB = {lines: [nil, nil, nil, nil]}.freeze subject do SimpleCov::SourceFile.new(source_fixture("skipped.rb"), COVERAGE_FOR_SKIPPED_RB) @@ -252,7 +330,7 @@ end context "a file where everything is skipped and missed #563" do - COVERAGE_FOR_SKIPPED_RB_2 = {"lines" => [nil, nil, 0, nil]}.freeze + COVERAGE_FOR_SKIPPED_RB_2 = {lines: [nil, nil, 0, nil]}.freeze subject do SimpleCov::SourceFile.new(source_fixture("skipped.rb"), COVERAGE_FOR_SKIPPED_RB_2) @@ -274,8 +352,8 @@ context "a file where everything is skipped/irrelevant but executed #563" do COVERAGE_FOR_SKIPPED_AND_EXECUTED_RB = { - "lines" => [nil, nil, 1, 1, 0, 0, nil, 0, nil, nil, nil, nil], - "branches" => { + lines: [nil, nil, 1, 1, 0, 0, nil, 0, nil, nil, nil, nil], + branches: { [:if, 0, 5, 4, 9, 7] => {[:then, 1, 6, 6, 6, 7] => 1, [:else, 2, 8, 6, 8, 7] => 0} } @@ -326,12 +404,23 @@ expect(subject.covered_branches.size).to eq 0 end end + + describe "method coverage" do + it "has no methods" do + expect(subject.total_methods.size).to eq 0 + end + + it "does has neither covered nor missed methods" do + expect(subject.missed_methods.size).to eq 0 + expect(subject.covered_methods.size).to eq 0 + end + end end context "a file with more complex skipping" do COVERAGE_FOR_NOCOV_COMPLEX_RB = { - "lines" => [nil, nil, 1, 1, nil, 1, nil, nil, nil, 1, nil, nil, 1, nil, nil, 0, nil, 1, nil, 0, nil, nil, 1, nil, nil, nil, nil], - "branches" => { + lines: [nil, nil, 1, 1, nil, 1, nil, nil, nil, 1, nil, nil, 1, nil, nil, 0, nil, 1, nil, 0, nil, nil, 1, nil, nil, nil, nil], + branches: { [:if, 0, 6, 4, 11, 7] => {[:then, 1, 7, 6, 7, 7] => 0, [:else, 2, 10, 6, 10, 7] => 1}, [:if, 3, 13, 4, 13, 24] => @@ -391,8 +480,8 @@ context "a file with nested branches" do COVERAGE_FOR_NESTED_BRANCHES_RB = { - "lines" => [nil, nil, 1, 1, 1, 1, 1, 1, nil, nil, 0, nil, nil, nil, nil], - "branches" => { + lines: [nil, nil, 1, 1, 1, 1, 1, 1, nil, nil, 0, nil, nil, nil, nil], + branches: { [:while, 0, 7, 8, 7, 31] => {[:body, 1, 7, 8, 7, 16] => 2}, [:if, 2, 6, 6, 9, 9] => @@ -427,8 +516,8 @@ context "a file with case" do COVERAGE_FOR_CASE_STATEMENT_RB = { - "lines" => [1, 1, 1, nil, 0, nil, 1, nil, 0, nil, 0, nil, nil, nil], - "branches" => { + lines: [1, 1, 1, nil, 0, nil, 1, nil, 0, nil, 0, nil, nil, nil], + branches: { [:case, 0, 3, 4, 12, 7] => { [:when, 1, 5, 6, 5, 10] => 0, [:when, 2, 7, 6, 7, 10] => 1, @@ -470,8 +559,8 @@ context "a file with case without else" do COVERAGE_FOR_CASE_WITHOUT_ELSE_STATEMENT_RB = { - "lines" => [1, 1, 1, nil, 0, nil, 1, nil, 0, nil, nil, nil], - "branches" => { + lines: [1, 1, 1, nil, 0, nil, 1, nil, 0, nil, nil, nil], + branches: { [:case, 0, 3, 4, 10, 7] => { [:when, 1, 5, 6, 5, 10] => 0, [:when, 2, 7, 6, 7, 10] => 1, @@ -517,8 +606,8 @@ context "a file with if/elsif" do COVERAGE_FOR_ELSIF_RB = { - "lines" => [1, 1, 1, 0, 1, 0, 1, 1, nil, 0, nil, nil, nil], - "branches" => { + lines: [1, 1, 1, 0, 1, 0, 1, 1, nil, 0, nil, nil, nil], + branches: { [:if, 0, 7, 4, 10, 10] => {[:then, 1, 8, 6, 8, 10] => 1, [:else, 2, 10, 6, 10, 10] => 0}, [:if, 3, 5, 4, 10, 10] => @@ -555,8 +644,8 @@ context "the branch tester script" do COVERAGE_FOR_BRANCH_TESTER_RB = { - "lines" => [nil, nil, 1, 1, nil, 1, nil, 1, 1, nil, nil, 1, 0, nil, nil, 1, 0, nil, 1, nil, nil, 1, 1, 1, nil, nil, 1, 0, nil, nil, 1, 1, nil, 0, nil, 1, 1, 0, 0, 1, 5, 0, 0, nil, 0, nil, 0, nil, nil, nil], - "branches" => { + lines: [nil, nil, 1, 1, nil, 1, nil, 1, 1, nil, nil, 1, 0, nil, nil, 1, 0, nil, 1, nil, nil, 1, 1, 1, nil, nil, 1, 0, nil, nil, 1, 1, nil, 0, nil, 1, 1, 0, 0, 1, 5, 0, 0, nil, 0, nil, 0, nil, nil, nil], + branches: { [:if, 0, 4, 0, 4, 19] => {[:then, 1, 4, 12, 4, 15] => 0, [:else, 2, 4, 18, 4, 19] => 1}, [:unless, 3, 6, 0, 6, 23] => @@ -609,8 +698,8 @@ context "a file entirely ignored with a single # :nocov:" do COVERAGE_FOR_SINGLE_NOCOV_RB = { - "lines" => [nil, 1, 1, 1, 0, 1, 0, 1, 1, nil, 0, nil, nil, nil], - "branches" => { + lines: [nil, 1, 1, 1, 0, 1, 0, 1, 1, nil, 0, nil, nil, nil], + branches: { [:if, 0, 8, 4, 11, 10] => {[:then, 1, 9, 6, 9, 10] => 1, [:else, 2, 11, 6, 11, 10] => 0}, [:if, 3, 6, 4, 11, 10] => @@ -654,8 +743,8 @@ context "a file with an uneven usage of # :nocov:s" do COVERAGE_FOR_UNEVEN_NOCOV_RB = { - "lines" => [1, 1, nil, 1, 0, 1, 0, nil, 1, 1, nil, nil, 0, nil, nil, nil], - "branches" => { + lines: [1, 1, nil, 1, 0, 1, 0, nil, 1, 1, nil, nil, 0, nil, nil, nil], + branches: { [:if, 0, 9, 4, 13, 10] => {[:then, 1, 10, 6, 10, 10] => 1, [:else, 2, 13, 6, 13, 10] => 0}, [:if, 3, 6, 4, 13, 10] => @@ -697,9 +786,9 @@ end context "a file contains non-ASCII characters" do - COVERAGE_FOR_SINGLE_LINE = {"lines" => [nil]}.freeze - COVERAGE_FOR_DOUBLE_LINES = {"lines" => [nil, 1]}.freeze - COVERAGE_FOR_TRIPLE_LINES = {"lines" => [nil, nil, 1]}.freeze + COVERAGE_FOR_SINGLE_LINE = {lines: [nil]}.freeze + COVERAGE_FOR_DOUBLE_LINES = {lines: [nil, 1]}.freeze + COVERAGE_FOR_TRIPLE_LINES = {lines: [nil, nil, 1]}.freeze DEGREE_135_LINE = "puts \"135°C\"\n" shared_examples_for "converting to UTF-8" do @@ -765,7 +854,7 @@ describe "empty euc-jp file" do subject do - SimpleCov::SourceFile.new(source_fixture("empty_euc-jp.rb"), "lines" => []) + SimpleCov::SourceFile.new(source_fixture("empty_euc-jp.rb"), lines: []) end it "has empty lines" do diff --git a/spec/useless_results_remover_spec.rb b/spec/useless_results_remover_spec.rb index 0ab6a9961..e7750078c 100644 --- a/spec/useless_results_remover_spec.rb +++ b/spec/useless_results_remover_spec.rb @@ -13,12 +13,14 @@ let(:result_set) do { gem_file_path => { - "lines" => [nil, 1, 1, 1, nil, nil, 1, 1, nil, nil], - "branches" => {[:if, 3, 8, 6, 8, 36] => {[:then, 4, 8, 6, 8, 12] => 47, [:else, 5, 8, 6, 8, 36] => 24}} + lines: [nil, 1, 1, 1, nil, nil, 1, 1, nil, nil], + branches: {[:if, 3, 8, 6, 8, 36] => {[:then, 4, 8, 6, 8, 12] => 47, [:else, 5, 8, 6, 8, 36] => 24}}, + methods: {} }, source_path => { - "lines" => [nil, 1, 1, 1, nil, nil, 1, 0, nil, nil], - "branches" => {[:if, 3, 8, 6, 8, 36] => {[:then, 4, 8, 6, 8, 12] => 47, [:else, 5, 8, 6, 8, 36] => 24}} + lines: [nil, 1, 1, 1, nil, nil, 1, 0, nil, nil], + branches: {[:if, 3, 8, 6, 8, 36] => {[:then, 4, 8, 6, 8, 12] => 47, [:else, 5, 8, 6, 8, 36] => 24}}, + methods: {} } } end @@ -30,6 +32,6 @@ it "still retains the app path" do expect(subject).to have_key(source_path) - expect(subject[source_path]["lines"]).to be_a(Array) + expect(subject[source_path][:lines]).to be_a(Array) end end