diff --git a/.github/workflows/stable.yml b/.github/workflows/stable.yml index af67714a..5557333f 100644 --- a/.github/workflows/stable.yml +++ b/.github/workflows/stable.yml @@ -13,6 +13,7 @@ jobs: env: BUNDLE_WITHOUT: "benchmark" JRUBY_OPTS: "--debug" + SIMPLECOV_HTML_MODE: "methods" # TODO: remove after simplecov-html release steps: - uses: actions/checkout@v4 @@ -21,7 +22,6 @@ jobs: uses: ruby/setup-ruby@v1 with: ruby-version: ${{ matrix.ruby }} - rubygems: latest bundler-cache: true # 'bundle install' and cache - name: Run tests diff --git a/.gitignore b/.gitignore index 43711f56..ba3d114e 100644 --- a/.gitignore +++ b/.gitignore @@ -26,6 +26,7 @@ tmp ## PROJECT::SPECIFIC .yardoc +.ruby-version spec/fixtures/coverage spec/fixtures/frameworks/coverage spec/fixtures/eval_test/coverage diff --git a/Gemfile b/Gemfile index 48ac4fb8..707853b9 100644 --- a/Gemfile +++ b/Gemfile @@ -2,11 +2,16 @@ 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["SIMPLECOV_HTML_MODE"] +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", github: "simplecov-ruby/simplecov-html" +when "methods" # TODO: remove after simplecov-html release + gem "simplecov-html", github: "umbrellio/simplecov-html", branch: "add-method-coverage-support" +end gem "matrix" diff --git a/Gemfile.lock b/Gemfile.lock index 8c38898f..a62b117b 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: 54879bd1080865cf8013bcda12e0c03ac687d7a9 + branch: add-method-coverage-support + specs: + simplecov-html (0.12.3) + PATH remote: . specs: @@ -154,7 +161,6 @@ GEM rubocop-ast (1.24.1) parser (>= 3.1.1.0) ruby-progressbar (1.11.0) - simplecov-html (0.12.3) simplecov_json_formatter (0.1.3) spoon (0.0.6) ffi @@ -197,6 +203,7 @@ DEPENDENCIES rspec (~> 3.2) rubocop simplecov! + simplecov-html! test-unit webrick diff --git a/features/branch_coverage.feature b/features/branch_coverage.feature index 55f0d096..622cd780 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 5566a79c..7a8b60c5 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 00000000..98af2ddb --- /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 ba1b8c60..6d38c869 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 e4540ef9..49442a33 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 f4f080e7..69b5d42a 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 406be390..3422b516 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.indent(6)} """ ) end diff --git a/features/support/env.rb b/features/support/env.rb index e5d3fb3a..75e39e29 100644 --- a/features/support/env.rb +++ b/features/support/env.rb @@ -28,6 +28,10 @@ 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/simplecov.rb b/lib/simplecov.rb index 12202c9e..c8863836 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) @@ -358,7 +350,8 @@ def start_coverage_with_criteria 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) @@ -447,6 +440,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" @@ -455,16 +449,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 320f648f..2f0f4acd 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 brach 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 32df1184..390bd0d6 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 00000000..67604285 --- /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 dd359302..2ebf8edc 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 617e5f6e..81e2cdd3 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 7b65b96b..b1f3cdd1 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 a276d275..cf617201 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 ea3a0ea9..5fd4fa3e 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 756233b8..787458e8 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 51726f1a..89e7cf83 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 4b7f07f9..79cf8e6f 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 a6b2e92e..fbaf5439 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 00000000..2a5f6e71 --- /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 04443865..f75122db 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 1cdbc7a9..782406dd 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).select { |_type, count| count.zero? }.any? 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 @@ -227,9 +248,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 @@ -247,9 +268,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 # @@ -271,7 +296,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 @@ -289,34 +314,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 @@ -333,12 +339,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 @@ -347,7 +359,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 00000000..3096dad4 --- /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 00000000..116351f1 --- /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 05e1b55e..2e4e2759 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,70 +58,73 @@ 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 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 9817f3f2..4fbb1e0e 100644 --- a/spec/command_guesser_spec.rb +++ b/spec/command_guesser_spec.rb @@ -5,44 +5,44 @@ describe SimpleCov::CommandGuesser do 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 3a6686b1..1ac1ed1d 100644 --- a/spec/configuration_spec.rb +++ b/spec/configuration_spec.rb @@ -88,6 +88,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}) @@ -175,6 +183,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 @@ -229,6 +243,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 7677b3a3..5e582cb9 100644 --- a/spec/coverage_for_eval_spec.rb +++ b/spec/coverage_for_eval_spec.rb @@ -19,7 +19,7 @@ let(:command) { "ruby eval_test.rb" } it "records coverage for erb" do - expect(@stdout).to include(" 2 / 3 LOC") + expect(@stdout).to include("Line coverage: 2 / 3") end end end diff --git a/spec/file_list_spec.rb b/spec/file_list_spec.rb index f0d33629..10033216 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 b6d9e53b..381bdeb0 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 00000000..de753ef3 --- /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 0bbb70f0..0617131f 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 a8fe8ec0..9a6bd43d 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 dc5cd0f0..62dfa829 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 00000000..bebee569 --- /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 86f62b3a..43609c77 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 223b8f55..b24e96ed 100644 --- a/spec/useless_results_remover_spec.rb +++ b/spec/useless_results_remover_spec.rb @@ -9,12 +9,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_kind_of(Array) + expect(subject[source_path][:lines]).to be_kind_of(Array) end end