diff --git a/.gitignore b/.gitignore index 6e7d7c2..4795e51 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ .rvmrc .bundle +/tmp ## MAC OS .DS_Store diff --git a/.rubocop.yml b/.rubocop.yml index 054cbe1..f09cc63 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -7,6 +7,11 @@ AllCops: DisplayCopNames: true NewCops: enable TargetRubyVersion: 2.5 + Exclude: + - "test/fixtures/*" + - "tmp/**/*" + - "vendor/**/*" + - ".git/**/*" Bundler/DuplicatedGem: Enabled: false diff --git a/Gemfile b/Gemfile index b931438..893c71f 100644 --- a/Gemfile +++ b/Gemfile @@ -19,6 +19,7 @@ group :test do end group :development do + gem "nokogiri" gem "rubocop" gem "rubocop-minitest" gem "rubocop-performance" diff --git a/Gemfile.lock b/Gemfile.lock index 25da2bf..cffbc12 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -24,7 +24,11 @@ GEM json (2.7.2-java) language_server-protocol (3.17.0.3) logger (1.6.1) + mini_portile2 (2.8.8) minitest (5.25.1) + nokogiri (1.18.1) + mini_portile2 (~> 2.8.2) + racc (~> 1.4) parallel (1.26.3) parser (3.3.5.0) ast (~> 2.4.1) @@ -73,13 +77,13 @@ GEM unicode-display_width (2.5.0) PLATFORMS - arm64-darwin-22 ruby universal-java-1.8 DEPENDENCIES logger minitest + nokogiri rake (>= 11) rubocop rubocop-minitest diff --git a/lib/simplecov-html.rb b/lib/simplecov-html.rb index 1e8703e..64f59e6 100644 --- a/lib/simplecov-html.rb +++ b/lib/simplecov-html.rb @@ -55,8 +55,9 @@ def line_status?(source_file, line) def output_message(result) output = "Coverage report generated for #{result.command_name} to #{output_path}." - output += "\nLine Coverage: #{result.covered_percent.round(2)}% (#{result.covered_lines} / #{result.total_lines})" - output += "\nBranch Coverage: #{result.coverage_statistics[:branch].percent.round(2)}% (#{result.covered_branches} / #{result.total_branches})" if branchable_result? + output += "\nLine Coverage: #{result.covered_percent.floor(2)}% (#{result.covered_lines} / #{result.total_lines})" + + output += "\nBranch Coverage: #{result.coverage_statistics[:branch].percent.floor(2)}% (#{result.covered_branches} / #{result.total_branches})" if branchable_result? output end diff --git a/test/fixtures/branch_tester_script.rb b/test/fixtures/branch_tester_script.rb new file mode 100644 index 0000000..090e204 --- /dev/null +++ b/test/fixtures/branch_tester_script.rb @@ -0,0 +1,50 @@ +# Adapted from https://github.com/simplecov-ruby/simplecov/pull/694#issuecomment-562097006 +# rubocop:disable all +x = 1 +x.eql?(4) ? "4" : x + +puts x unless x.eql?(5) + +unless x == 5 + puts "Ola.." +end + +unless x != 5 + puts "Ola.." +end + +unless x != 5 + puts "Ola.." +else + puts "text" +end + +puts x if x.eql?(5) +if x != 5 + puts "Ola.." +end + +if x == 5 + puts "Ola.." +end + +if x != 5 + puts "Ola.." +else + puts "text" +end +x = 4 +if x == 1 + puts "wow 1" + puts "such excite!" +elsif x == 4 + 4.times { puts "4!!!!"} +elsif x == 14 + puts "magic" + + puts "very" +else + puts x +end + +# rubocop:enable all diff --git a/test/fixtures/branches.rb b/test/fixtures/branches.rb new file mode 100644 index 0000000..0f5a634 --- /dev/null +++ b/test/fixtures/branches.rb @@ -0,0 +1,13 @@ +class Branches + def call(arg) + return if arg < 0 + + _val = (arg == 42 ? :yes : :no) + + if arg.odd? + :yes + else + :no + end + end +end diff --git a/test/fixtures/case.rb b/test/fixtures/case.rb new file mode 100644 index 0000000..44edccb --- /dev/null +++ b/test/fixtures/case.rb @@ -0,0 +1,14 @@ +module Case + def self.call(arg) + case arg + when 0...23 + :foo + when 40..50 + :bar + when Integer + :baz + else + :nope + end + end +end diff --git a/test/fixtures/case_without_else.rb b/test/fixtures/case_without_else.rb new file mode 100644 index 0000000..5b39842 --- /dev/null +++ b/test/fixtures/case_without_else.rb @@ -0,0 +1,12 @@ +module Case + def self.call(arg) + case arg + when 0...23 + :foo + when 40..50 + :bar + when Integer + :baz + end + end +end diff --git a/test/fixtures/elsif.rb b/test/fixtures/elsif.rb new file mode 100644 index 0000000..c2e69d4 --- /dev/null +++ b/test/fixtures/elsif.rb @@ -0,0 +1,13 @@ +module Elsif + def self.call(arg) + if arg.odd? + :odd + elsif arg == 30 + :mop + elsif arg == 42 + :yay + else + :nay + end + end +end diff --git a/test/fixtures/inline.rb b/test/fixtures/inline.rb new file mode 100644 index 0000000..22d287b --- /dev/null +++ b/test/fixtures/inline.rb @@ -0,0 +1,13 @@ +class Inline + def call(arg) + String(arg == 42 ? :yes : :no) + + String( + if arg.odd? + :yes + else + :no + end + ) + end +end diff --git a/test/fixtures/nested_branches.rb b/test/fixtures/nested_branches.rb new file mode 100644 index 0000000..6fa9490 --- /dev/null +++ b/test/fixtures/nested_branches.rb @@ -0,0 +1,15 @@ +# yes rubocop you are right but I want to test nesting! +# rubocop:disable Metrics/BlockNesting +module NestedBranches + def self.call(arg) + if arg.even? + if arg == 42 + arg -= 1 while arg > 40 + :ok + end + else + :nope + end + end +end +# rubocop:enable Metrics/BlockNesting diff --git a/test/fixtures/never.rb b/test/fixtures/never.rb new file mode 100644 index 0000000..62d6db8 --- /dev/null +++ b/test/fixtures/never.rb @@ -0,0 +1,2 @@ +# This class is purely some +# comments diff --git a/test/fixtures/nocov_complex.rb b/test/fixtures/nocov_complex.rb new file mode 100644 index 0000000..9005f83 --- /dev/null +++ b/test/fixtures/nocov_complex.rb @@ -0,0 +1,27 @@ +# So much skippping +# rubocop:disable Metrics/MethodLength +module NoCovComplex + def self.call(arg) + # :nocov: + if arg == 42 + 0 + # :nocov: + else + puts "yolo" + end + + arg += 1 if arg.odd? + + # :nocov: + arg -= 1 while arg > 40 + + case arg + when 1..20 + :nope + # :nocov: + when 30..40 + :yas + end + end +end +# rubocop:enable Metrics/MethodLength diff --git a/test/fixtures/sample.rb b/test/fixtures/sample.rb new file mode 100644 index 0000000..bbcf17d --- /dev/null +++ b/test/fixtures/sample.rb @@ -0,0 +1,16 @@ +# Foo class +class Foo + def initialize + @foo = "baz" + end + + def bar + @foo + end + + # :nocov: + def skipped + @foo * 2 + end + # :nocov: +end diff --git a/test/fixtures/single_nocov.rb b/test/fixtures/single_nocov.rb new file mode 100644 index 0000000..d435a1c --- /dev/null +++ b/test/fixtures/single_nocov.rb @@ -0,0 +1,14 @@ +# :nocov: +module SingleNocov + def self.call(arg) + if arg.odd? + :odd + elsif arg == 30 + :mop + elsif arg == 42 + :yay + else + :nay + end + end +end diff --git a/test/fixtures/uneven_nocovs.rb b/test/fixtures/uneven_nocovs.rb new file mode 100644 index 0000000..a439291 --- /dev/null +++ b/test/fixtures/uneven_nocovs.rb @@ -0,0 +1,16 @@ +module UnevenNocov + def self.call(arg) + # :nocov: + if arg.odd? + :odd + elsif arg == 30 + :mop + # :nocov: + elsif arg == 42 + :yay + # :nocov: + else + :nay + end + end +end diff --git a/test/helper.rb b/test/helper.rb index 44bf014..07d78b0 100644 --- a/test/helper.rb +++ b/test/helper.rb @@ -4,3 +4,6 @@ require "simplecov" require "simplecov-html" require "minitest/autorun" + +require "pathname" +require "nokogiri" diff --git a/test/test_simple_cov-html.rb b/test/test_simple_cov-html.rb index 6a2bdda..f330fe8 100644 --- a/test/test_simple_cov-html.rb +++ b/test/test_simple_cov-html.rb @@ -3,8 +3,263 @@ require "helper" class TestSimpleCovHtml < Minitest::Test + def setup + SimpleCov.coverage_dir(output_path) + SimpleCov.enable_coverage(:branch) + end + + def teardown + SimpleCov.coverage_dir(nil) + SimpleCov.clear_coverage_criteria + end + def test_defined - assert defined?(SimpleCov::Formatter::HTMLFormatter) assert defined?(SimpleCov::Formatter::HTMLFormatter::VERSION) end + + def test_output # rubocop:disable Metrics + # Examples copied from simplecov's spec/source_file_spec.rb + 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}, + }, + } + 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], + } + coverage_for_inline = { + "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] => + {[:then, 4, 7, 8, 7, 12] => 0, [:else, 5, 9, 8, 9, 11] => 1}, + }, + } + coverage_for_never_rb = {"lines" => [nil, nil], "branches" => {}} + 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" => { + [: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] => + {[:then, 4, 13, 4, 13, 12] => 1, [:else, 5, 13, 4, 13, 24] => 0}, + [:while, 6, 16, 4, 16, 27] => + {[:body, 7, 16, 4, 16, 12] => 2}, + [:case, 8, 18, 4, 24, 7] => { + [:when, 9, 20, 6, 20, 11] => 0, + [:when, 10, 23, 6, 23, 10] => 1, + [:else, 11, 18, 4, 24, 7] => 0, + }, + }, + } + coverage_for_nested_branches_rb = { + "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] => + {[:then, 3, 7, 8, 8, 11] => 1, [:else, 4, 6, 6, 9, 9] => 0}, + [:if, 5, 5, 4, 12, 7] => + {[:then, 6, 6, 6, 9, 9] => 1, [:else, 7, 11, 6, 11, 11] => 0}, + }, + } + coverage_for_case_statement_rb = { + "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, + [:when, 3, 9, 6, 9, 10] => 0, + [:else, 4, 11, 6, 11, 11] => 0, + }, + }, + } + coverage_for_case_without_else_statement_rb = { + "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, + [:when, 3, 9, 6, 9, 10] => 0, + [:else, 4, 3, 4, 10, 7] => 0, + }, + }, + } + coverage_for_elsif_rb = { + "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] => + {[:then, 4, 6, 6, 6, 10] => 0, [:else, 5, 7, 4, 10, 10] => 1}, + [:if, 6, 3, 4, 11, 7] => + {[:then, 7, 4, 6, 4, 10] => 0, [:else, 8, 5, 4, 10, 10] => 1}, + }, + } + 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" => { + [: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] => + {[:else, 4, 6, 0, 6, 23] => 0, [:then, 5, 6, 0, 6, 6] => 1}, + [:unless, 6, 8, 0, 10, 3] => + {[:else, 7, 8, 0, 10, 3] => 0, [:then, 8, 9, 2, 9, 14] => 1}, + [:unless, 9, 12, 0, 14, 3] => + {[:else, 10, 12, 0, 14, 3] => 1, [:then, 11, 13, 2, 13, 14] => 0}, + [:unless, 12, 16, 0, 20, 3] => + {[:else, 13, 19, 2, 19, 13] => 1, [:then, 14, 17, 2, 17, 14] => 0}, + [:if, 15, 22, 0, 22, 19] => + {[:then, 16, 22, 0, 22, 6] => 0, [:else, 17, 22, 0, 22, 19] => 1}, + [:if, 18, 23, 0, 25, 3] => + {[:then, 19, 24, 2, 24, 14] => 1, [:else, 20, 23, 0, 25, 3] => 0}, + [:if, 21, 27, 0, 29, 3] => + {[:then, 22, 28, 2, 28, 14] => 0, [:else, 23, 27, 0, 29, 3] => 1}, + [:if, 24, 31, 0, 35, 3] => + {[:then, 25, 32, 2, 32, 14] => 1, [:else, 26, 34, 2, 34, 13] => 0}, + [:if, 27, 42, 0, 47, 8] => + {[:then, 28, 43, 2, 45, 13] => 0, [:else, 29, 47, 2, 47, 8] => 0}, + [:if, 30, 40, 0, 47, 8] => + {[:then, 31, 41, 2, 41, 25] => 1, [:else, 32, 42, 0, 47, 8] => 0}, + [:if, 33, 37, 0, 48, 3] => + {[:then, 34, 38, 2, 39, 21] => 0, [:else, 35, 40, 0, 47, 8] => 1}, + }, + } + coverage_for_single_nocov_rb = { + "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] => + {[:then, 4, 7, 6, 7, 10] => 0, [:else, 5, 8, 4, 11, 10] => 1}, + [:if, 6, 4, 4, 12, 7] => + {[:then, 7, 5, 6, 5, 10] => 0, [:else, 8, 6, 4, 11, 10] => 1}, + }, + } + coverage_for_uneven_nocov_rb = { + "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] => + {[:then, 4, 7, 6, 7, 10] => 0, [:else, 5, 9, 4, 13, 10] => 1}, + [:if, 6, 4, 4, 14, 7] => + {[:then, 7, 5, 6, 5, 10] => 0, [:else, 8, 6, 4, 13, 10] => 1}, + }, + } + + html_doc = format_results({ + "branches.rb" => coverage_for_branches_rb, + "sample.rb" => coverage_for_sample_rb_with_more_lines, + "inline.rb" => coverage_for_inline, + "never.rb" => coverage_for_never_rb, + "nocov_complex.rb" => coverage_for_nocov_complex_rb, + "nested_branches.rb" => coverage_for_nested_branches_rb, + "case.rb" => coverage_for_case_statement_rb, + "case_without_else.rb" => coverage_for_case_without_else_statement_rb, + "elsif.rb" => coverage_for_elsif_rb, + "branch_tester_script.rb" => coverage_for_branch_tester_rb, + "single_nocov.rb" => coverage_for_single_nocov_rb, + "uneven_nocovs.rb" => coverage_for_uneven_nocov_rb, + }) + + # All Files ( 74.11% covered at 0.83 hits/line ) + header_line_coverage = html_doc.at_css("div#AllFiles span.covered_percent span").content.strip + + assert_equal("74.11%", header_line_coverage) + + # 85 relevant lines, 63 lines covered and 22 lines missed. ( 74.11% ) + subheader_line_coverage = html_doc.at_css("div#AllFiles div.t-line-summary span:last-child").content.strip + + assert_equal("74.11%", subheader_line_coverage) + + if RUBY_ENGINE != "jruby" + # 58 total branches, 28 branches covered and 30 branches missed. ( 48.27% ) + subheader_branch_coverage = html_doc.at_css("div#AllFiles div.t-branch-summary span:last-child").content.strip + + assert_equal("48.27%", subheader_branch_coverage) + end + + sorted_line_coverages = [ + "57.14%", + "64.28%", + "66.66%", + "66.66%", + "80.00%", + "85.71%", + "85.71%", + "85.71%", + "100.00%", + "100.00%", + "100.00%", + "100.00%", + ] + + sorted_branch_coverages = [ + "25.00%", + "25.00%", + "45.83%", + "50.00%", + "50.00%", + "50.00%", + "60.00%", + "75.00%", + "100.00%", + "100.00%", + "100.00%", + "100.00%", + ] + + # % covered + all_files_table_line_coverages = html_doc.css("div#AllFiles table.file_list tr.t-file td.t-file__coverage").map { |m| m.content.strip } + + assert_equal(sorted_line_coverages, all_files_table_line_coverages.sort_by(&:to_f)) + + if RUBY_ENGINE != "jruby" + # Branch Coverage + all_files_table_branch_coverages = html_doc.css("div#AllFiles table.file_list tr.t-file td.t-file__branch-coverage").map { |m| m.content.strip } + + assert_equal sorted_branch_coverages, all_files_table_branch_coverages.sort_by(&:to_f) + end + + # 66.66 lines covered + single_file_page_line_coverages = html_doc.css("div.source_files div.header h4:nth-child(2) span").map { |m| m.content.strip } + + assert_equal(sorted_line_coverages, single_file_page_line_coverages.sort_by(&:to_f)) + + if RUBY_ENGINE != "jruby" # rubocop:disable Style/GuardClause + # 25.0% branches covered + single_file_page_branch_coverages = html_doc.css("div.source_files div.header h4:nth-child(3) span").map { |m| m.content.strip } + + assert_equal sorted_branch_coverages, single_file_page_branch_coverages.sort_by(&:to_f) + end + end + +private + + def format_results(coverage_results) + coverage_results = coverage_results.to_h do |fixture_file_name, coverage| + [fixtures_path.join(fixture_file_name).to_s, coverage] + end + result = SimpleCov::Result.new(coverage_results) + capture_io { SimpleCov::Formatter::HTMLFormatter.new.format(result) } + + # Return an HTML doc instance + output_path.join("index.html").open { |f| Nokogiri::HTML(f) } + end + + def output_path + Pathname.new(__dir__).parent.join("tmp", "test_output") + end + + def fixtures_path + Pathname.new(__dir__).join("fixtures") + end end diff --git a/views/covered_percent.erb b/views/covered_percent.erb index 8f40f25..aa15fc0 100644 --- a/views/covered_percent.erb +++ b/views/covered_percent.erb @@ -1,3 +1,3 @@ - <%= percent.round(2) %>% + <%= sprintf("%.2f", percent.floor(2)) %>% diff --git a/views/file_list.erb b/views/file_list.erb index 0cd97b6..620dd71 100644 --- a/views/file_list.erb +++ b/views/file_list.erb @@ -7,7 +7,7 @@ covered at - <%= source_files.covered_strength.round(2) %> + <%= sprintf("%.2f", source_files.covered_strength) %> hits/line ) @@ -58,14 +58,14 @@ <% source_files.each do |source_file| %> <%= link_to_source_file(source_file) %> - <%= sprintf("%.2f", source_file.covered_percent.round(2)) %> % + <%= covered_percent(source_file.covered_percent) %> <%= source_file.lines.count %> <%= source_file.covered_lines.count + source_file.missed_lines.count %> <%= source_file.covered_lines.count %> <%= source_file.missed_lines.count %> - <%= sprintf("%.2f", source_file.covered_strength.round(2)) %> + <%= sprintf("%.2f", source_file.covered_strength) %> <% if branchable_result? %> - <%= sprintf("%.2f", source_file.branches_coverage_percent.round(2)) %> % + <%= covered_percent(source_file.branches_coverage_percent) %> <%= source_file.total_branches.count %> <%= source_file.covered_branches.count %> <%= source_file.missed_branches.count %>