simplecov 0.6.0 → 0.22.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (131) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +163 -80
  3. data/LICENSE +1 -1
  4. data/README.md +776 -277
  5. data/doc/alternate-formatters.md +71 -0
  6. data/doc/commercial-services.md +25 -0
  7. data/doc/editor-integration.md +18 -0
  8. data/lib/minitest/simplecov_plugin.rb +15 -0
  9. data/lib/simplecov/combine/branches_combiner.rb +32 -0
  10. data/lib/simplecov/combine/files_combiner.rb +24 -0
  11. data/lib/simplecov/combine/lines_combiner.rb +43 -0
  12. data/lib/simplecov/combine/results_combiner.rb +60 -0
  13. data/lib/simplecov/combine.rb +30 -0
  14. data/lib/simplecov/command_guesser.rb +53 -38
  15. data/lib/simplecov/configuration.rb +478 -193
  16. data/lib/simplecov/coverage_statistics.rb +56 -0
  17. data/lib/simplecov/default_formatter.rb +20 -0
  18. data/lib/simplecov/defaults.rb +40 -44
  19. data/lib/simplecov/exit_codes/exit_code_handling.rb +29 -0
  20. data/lib/simplecov/exit_codes/maximum_coverage_drop_check.rb +83 -0
  21. data/lib/simplecov/exit_codes/minimum_coverage_by_file_check.rb +54 -0
  22. data/lib/simplecov/exit_codes/minimum_overall_coverage_check.rb +53 -0
  23. data/lib/simplecov/exit_codes.rb +15 -0
  24. data/lib/simplecov/file_list.rb +112 -36
  25. data/lib/simplecov/filter.rb +54 -4
  26. data/lib/simplecov/formatter/multi_formatter.rb +32 -0
  27. data/lib/simplecov/formatter/simple_formatter.rb +21 -15
  28. data/lib/simplecov/formatter.rb +4 -1
  29. data/lib/simplecov/last_run.rb +28 -0
  30. data/lib/simplecov/lines_classifier.rb +48 -0
  31. data/lib/simplecov/load_global_config.rb +8 -0
  32. data/lib/simplecov/no_defaults.rb +4 -0
  33. data/lib/simplecov/process.rb +19 -0
  34. data/lib/simplecov/profiles/bundler_filter.rb +5 -0
  35. data/lib/simplecov/profiles/hidden_filter.rb +5 -0
  36. data/lib/simplecov/profiles/rails.rb +18 -0
  37. data/lib/simplecov/profiles/root_filter.rb +10 -0
  38. data/lib/simplecov/profiles/test_frameworks.rb +8 -0
  39. data/lib/simplecov/profiles.rb +35 -0
  40. data/lib/simplecov/result.rb +33 -64
  41. data/lib/simplecov/result_adapter.rb +30 -0
  42. data/lib/simplecov/result_merger.rb +178 -64
  43. data/lib/simplecov/simulate_coverage.rb +29 -0
  44. data/lib/simplecov/source_file/branch.rb +84 -0
  45. data/lib/simplecov/source_file/line.rb +72 -0
  46. data/lib/simplecov/source_file.rb +304 -123
  47. data/lib/simplecov/useless_results_remover.rb +18 -0
  48. data/lib/simplecov/version.rb +4 -2
  49. data/lib/simplecov.rb +396 -49
  50. metadata +81 -242
  51. data/.gitignore +0 -30
  52. data/.rvmrc +0 -1
  53. data/.travis.yml +0 -13
  54. data/Gemfile +0 -9
  55. data/Rakefile +0 -16
  56. data/cucumber.yml +0 -13
  57. data/features/config_adapters.feature +0 -44
  58. data/features/config_autoload.feature +0 -46
  59. data/features/config_command_name.feature +0 -33
  60. data/features/config_coverage_dir.feature +0 -20
  61. data/features/config_deactivate_merging.feature +0 -42
  62. data/features/config_merge_timeout.feature +0 -38
  63. data/features/config_nocov_token.feature +0 -79
  64. data/features/config_project_name.feature +0 -27
  65. data/features/config_styles.feature +0 -93
  66. data/features/cucumber_basic.feature +0 -29
  67. data/features/merging_test_unit_and_rspec.feature +0 -44
  68. data/features/rspec_basic.feature +0 -31
  69. data/features/rspec_fails_on_initialization.feature +0 -14
  70. data/features/rspec_groups_and_filters_basic.feature +0 -29
  71. data/features/rspec_groups_and_filters_complex.feature +0 -35
  72. data/features/rspec_groups_using_filter_class.feature +0 -40
  73. data/features/rspec_without_simplecov.feature +0 -20
  74. data/features/skipping_code_blocks_manually.feature +0 -70
  75. data/features/step_definitions/html_steps.rb +0 -45
  76. data/features/step_definitions/simplecov_steps.rb +0 -61
  77. data/features/step_definitions/transformers.rb +0 -13
  78. data/features/step_definitions/web_steps.rb +0 -64
  79. data/features/support/env.rb +0 -26
  80. data/features/test_unit_basic.feature +0 -34
  81. data/features/test_unit_groups_and_filters_basic.feature +0 -29
  82. data/features/test_unit_groups_and_filters_complex.feature +0 -35
  83. data/features/test_unit_groups_using_filter_class.feature +0 -40
  84. data/features/test_unit_without_simplecov.feature +0 -20
  85. data/features/unicode_compatiblity.feature +0 -67
  86. data/lib/simplecov/adapters.rb +0 -29
  87. data/lib/simplecov/merge_helpers.rb +0 -39
  88. data/lib/simplecov/railtie.rb +0 -7
  89. data/lib/simplecov/railties/tasks.rake +0 -11
  90. data/simplecov.gemspec +0 -28
  91. data/test/faked_project/Gemfile +0 -6
  92. data/test/faked_project/Rakefile +0 -8
  93. data/test/faked_project/cucumber.yml +0 -13
  94. data/test/faked_project/features/step_definitions/my_steps.rb +0 -23
  95. data/test/faked_project/features/support/env.rb +0 -12
  96. data/test/faked_project/features/test_stuff.feature +0 -6
  97. data/test/faked_project/lib/faked_project/framework_specific.rb +0 -18
  98. data/test/faked_project/lib/faked_project/meta_magic.rb +0 -24
  99. data/test/faked_project/lib/faked_project/some_class.rb +0 -29
  100. data/test/faked_project/lib/faked_project.rb +0 -11
  101. data/test/faked_project/spec/faked_spec.rb +0 -11
  102. data/test/faked_project/spec/meta_magic_spec.rb +0 -10
  103. data/test/faked_project/spec/some_class_spec.rb +0 -10
  104. data/test/faked_project/spec/spec_helper.rb +0 -15
  105. data/test/faked_project/test/faked_test.rb +0 -11
  106. data/test/faked_project/test/meta_magic_test.rb +0 -13
  107. data/test/faked_project/test/some_class_test.rb +0 -15
  108. data/test/faked_project/test/test_helper.rb +0 -16
  109. data/test/fixtures/app/controllers/sample_controller.rb +0 -10
  110. data/test/fixtures/app/models/user.rb +0 -10
  111. data/test/fixtures/deleted_source_sample.rb +0 -15
  112. data/test/fixtures/frameworks/rspec_bad.rb +0 -9
  113. data/test/fixtures/frameworks/rspec_good.rb +0 -9
  114. data/test/fixtures/frameworks/testunit_bad.rb +0 -9
  115. data/test/fixtures/frameworks/testunit_good.rb +0 -9
  116. data/test/fixtures/resultset1.rb +0 -4
  117. data/test/fixtures/resultset2.rb +0 -5
  118. data/test/fixtures/sample.rb +0 -16
  119. data/test/fixtures/utf-8.rb +0 -3
  120. data/test/helper.rb +0 -35
  121. data/test/shoulda_macros.rb +0 -29
  122. data/test/test_1_8_fallbacks.rb +0 -33
  123. data/test/test_command_guesser.rb +0 -21
  124. data/test/test_deleted_source.rb +0 -16
  125. data/test/test_file_list.rb +0 -24
  126. data/test/test_filters.rb +0 -80
  127. data/test/test_merge_helpers.rb +0 -107
  128. data/test/test_result.rb +0 -147
  129. data/test/test_return_codes.rb +0 -39
  130. data/test/test_source_file.rb +0 -86
  131. data/test/test_source_file_line.rb +0 -110
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleCov
4
+ # Holds the individual data of a coverage result.
5
+ #
6
+ # This is uniform across coverage criteria as they all have:
7
+ #
8
+ # * total - how many things to cover there are (total relevant loc/branches)
9
+ # * covered - how many of the coverables are hit
10
+ # * missed - how many of the coverables are missed
11
+ # * percent - percentage as covered/missed
12
+ # * strength - average hits per/coverable (will not exist for one shot lines format)
13
+ class CoverageStatistics
14
+ attr_reader :total, :covered, :missed, :strength, :percent
15
+
16
+ def self.from(coverage_statistics)
17
+ sum_covered, sum_missed, sum_total_strength =
18
+ coverage_statistics.reduce([0, 0, 0.0]) do |(covered, missed, total_strength), file_coverage_statistics|
19
+ [
20
+ covered + file_coverage_statistics.covered,
21
+ missed + file_coverage_statistics.missed,
22
+ # gotta remultiply with loc because files have different strength and loc
23
+ # giving them a different "weight" in total
24
+ total_strength + (file_coverage_statistics.strength * file_coverage_statistics.total)
25
+ ]
26
+ end
27
+
28
+ new(covered: sum_covered, missed: sum_missed, total_strength: sum_total_strength)
29
+ end
30
+
31
+ # Requires only covered, missed and strength to be initialized.
32
+ #
33
+ # Other values are computed by this class.
34
+ def initialize(covered:, missed:, total_strength: 0.0)
35
+ @covered = covered
36
+ @missed = missed
37
+ @total = covered + missed
38
+ @percent = compute_percent(covered, missed, total)
39
+ @strength = compute_strength(total_strength, total)
40
+ end
41
+
42
+ private
43
+
44
+ def compute_percent(covered, missed, total)
45
+ return 100.0 if missed.zero?
46
+
47
+ covered * 100.0 / total
48
+ end
49
+
50
+ def compute_strength(total_strength, total)
51
+ return 0.0 if total.zero?
52
+
53
+ total_strength.to_f / total
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "simplecov-html"
4
+ module SimpleCov
5
+ module Formatter
6
+ class << self
7
+ def from_env(env)
8
+ formatters = [SimpleCov::Formatter::HTMLFormatter]
9
+
10
+ # When running under a CI that uses CodeClimate, JSON output is expected
11
+ if env.fetch("CC_TEST_REPORTER_ID", nil)
12
+ require "simplecov_json_formatter"
13
+ formatters.push(SimpleCov::Formatter::JSONFormatter)
14
+ end
15
+
16
+ formatters
17
+ end
18
+ end
19
+ end
20
+ end
@@ -1,57 +1,53 @@
1
- # Load default formatter gem
2
- require 'simplecov-html'
3
-
4
- SimpleCov.adapters.define 'root_filter' do
5
- # Exclude all files outside of simplecov root
6
- add_filter do |src|
7
- !(src.filename =~ /^#{SimpleCov.root}/)
8
- end
9
- end
10
-
11
- SimpleCov.adapters.define 'test_frameworks' do
12
- add_filter '/test/'
13
- add_filter '/features/'
14
- add_filter '/spec/'
15
- add_filter '/autotest/'
16
- end
17
-
18
- SimpleCov.adapters.define 'rails' do
19
- load_adapter 'test_frameworks'
20
-
21
- add_filter '/config/'
22
- add_filter '/db/'
23
- add_filter '/vendor/bundle/'
1
+ # frozen_string_literal: true
24
2
 
25
- add_group 'Controllers', 'app/controllers'
26
- add_group 'Models', 'app/models'
27
- add_group 'Mailers', 'app/mailers'
28
- add_group 'Helpers', 'app/helpers'
29
- add_group 'Libraries', 'lib'
30
- add_group 'Plugins', 'vendor/plugins'
31
- end
3
+ # Load default formatter gem
4
+ require "pathname"
5
+ require_relative "default_formatter"
6
+ require_relative "profiles/root_filter"
7
+ require_relative "profiles/test_frameworks"
8
+ require_relative "profiles/bundler_filter"
9
+ require_relative "profiles/hidden_filter"
10
+ require_relative "profiles/rails"
32
11
 
33
12
  # Default configuration
34
13
  SimpleCov.configure do
35
- formatter SimpleCov::Formatter::HTMLFormatter
14
+ formatter SimpleCov::Formatter::MultiFormatter.new(
15
+ SimpleCov::Formatter.from_env(ENV)
16
+ )
17
+
18
+ load_profile "bundler_filter"
19
+ load_profile "hidden_filter"
36
20
  # Exclude files outside of SimpleCov.root
37
- load_adapter 'root_filter'
21
+ load_profile "root_filter"
38
22
  end
39
23
 
40
24
  # Gotta stash this a-s-a-p, see the CommandGuesser class and i.e. #110 for further info
41
- SimpleCov::CommandGuesser.original_run_command = "#{$0} #{ARGV.join(" ")}"
25
+ SimpleCov::CommandGuesser.original_run_command = "#{$PROGRAM_NAME} #{ARGV.join(' ')}"
42
26
 
43
27
  at_exit do
44
- # Store the exit status of the test run since it goes away after calling the at_exit proc...
45
- if $! #was an exception thrown?
46
- #if it was a SystemExit, use the accompanying status
47
- #otherwise set a non-zero status representing termination by some other exception
48
- #(see github issue 41)
49
- @exit_status = $!.is_a?(SystemExit) ? $!.status : 1
50
- end
51
- SimpleCov.at_exit.call
52
- exit @exit_status if @exit_status # Force exit with stored status (see github issue #5)
28
+ next if SimpleCov.external_at_exit?
29
+
30
+ SimpleCov.at_exit_behavior
53
31
  end
54
32
 
33
+ # Autoload config from ~/.simplecov if present
34
+ require_relative "load_global_config"
35
+
55
36
  # Autoload config from .simplecov if present
56
- config_path = File.join(SimpleCov.root, '.simplecov')
57
- load config_path if File.exist?(config_path)
37
+ # Recurse upwards until we find .simplecov or reach the root directory
38
+
39
+ config_path = Pathname.new(SimpleCov.root)
40
+ loop do
41
+ filename = config_path.join(".simplecov")
42
+ if filename.exist?
43
+ begin
44
+ load filename
45
+ rescue LoadError, StandardError
46
+ warn "Warning: Error occurred while trying to load #{filename}. " \
47
+ "Error message: #{$!.message}"
48
+ end
49
+ break
50
+ end
51
+ config_path, = config_path.split
52
+ break if config_path.root?
53
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleCov
4
+ module ExitCodes
5
+ module ExitCodeHandling
6
+ module_function
7
+
8
+ def call(result, coverage_limits:)
9
+ checks = coverage_checks(result, coverage_limits)
10
+
11
+ failing_check = checks.find(&:failing?)
12
+ if failing_check
13
+ failing_check.report
14
+ failing_check.exit_code
15
+ else
16
+ SimpleCov::ExitCodes::SUCCESS
17
+ end
18
+ end
19
+
20
+ def coverage_checks(result, coverage_limits)
21
+ [
22
+ MinimumOverallCoverageCheck.new(result, coverage_limits.minimum_coverage),
23
+ MinimumCoverageByFileCheck.new(result, coverage_limits.minimum_coverage_by_file),
24
+ MaximumCoverageDropCheck.new(result, coverage_limits.maximum_coverage_drop)
25
+ ]
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleCov
4
+ module ExitCodes
5
+ class MaximumCoverageDropCheck
6
+ def initialize(result, maximum_coverage_drop)
7
+ @result = result
8
+ @maximum_coverage_drop = maximum_coverage_drop
9
+ end
10
+
11
+ def failing?
12
+ return false unless maximum_coverage_drop && last_run
13
+
14
+ coverage_drop_violations.any?
15
+ end
16
+
17
+ def report
18
+ coverage_drop_violations.each do |violation|
19
+ $stderr.printf(
20
+ "%<criterion>s coverage has dropped by %<drop_percent>.2f%% since the last time (maximum allowed: %<max_drop>.2f%%).\n",
21
+ criterion: violation[:criterion].capitalize,
22
+ drop_percent: SimpleCov.round_coverage(violation[:drop_percent]),
23
+ max_drop: violation[:max_drop]
24
+ )
25
+ end
26
+ end
27
+
28
+ def exit_code
29
+ SimpleCov::ExitCodes::MAXIMUM_COVERAGE_DROP
30
+ end
31
+
32
+ private
33
+
34
+ attr_reader :result, :maximum_coverage_drop
35
+
36
+ def last_run
37
+ return @last_run if defined?(@last_run)
38
+
39
+ @last_run = SimpleCov::LastRun.read
40
+ end
41
+
42
+ def coverage_drop_violations
43
+ @coverage_drop_violations ||=
44
+ compute_coverage_drop_data.select do |achieved|
45
+ achieved.fetch(:max_drop) < achieved.fetch(:drop_percent)
46
+ end
47
+ end
48
+
49
+ def compute_coverage_drop_data
50
+ maximum_coverage_drop.map do |criterion, percent|
51
+ {
52
+ criterion: criterion,
53
+ max_drop: percent,
54
+ drop_percent: drop_percent(criterion)
55
+ }
56
+ end
57
+ end
58
+
59
+ # if anyone says "max_coverage_drop 0.000000000000000001" I appologize. Please don't.
60
+ MAX_DROP_ACCURACY = 10
61
+ def drop_percent(criterion)
62
+ drop = last_coverage(criterion) -
63
+ SimpleCov.round_coverage(
64
+ result.coverage_statistics.fetch(criterion).percent
65
+ )
66
+
67
+ # floats, I tell ya.
68
+ # irb(main):001:0* 80.01 - 80.0
69
+ # => 0.010000000000005116
70
+ drop.floor(MAX_DROP_ACCURACY)
71
+ end
72
+
73
+ def last_coverage(criterion)
74
+ last_coverage_percent = last_run[:result][criterion]
75
+
76
+ # fallback for old file format
77
+ last_coverage_percent = last_run[:result][:covered_percent] if !last_coverage_percent && criterion == :line
78
+
79
+ last_coverage_percent || 0
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleCov
4
+ module ExitCodes
5
+ class MinimumCoverageByFileCheck
6
+ def initialize(result, minimum_coverage_by_file)
7
+ @result = result
8
+ @minimum_coverage_by_file = minimum_coverage_by_file
9
+ end
10
+
11
+ def failing?
12
+ minimum_violations.any?
13
+ end
14
+
15
+ def report
16
+ minimum_violations.each do |violation|
17
+ $stderr.printf(
18
+ "%<criterion>s coverage by file (%<covered>.2f%%) is below the expected minimum coverage (%<minimum_coverage>.2f%%).\n",
19
+ covered: SimpleCov.round_coverage(violation.fetch(:actual)),
20
+ minimum_coverage: violation.fetch(:minimum_expected),
21
+ criterion: violation.fetch(:criterion).capitalize
22
+ )
23
+ end
24
+ end
25
+
26
+ def exit_code
27
+ SimpleCov::ExitCodes::MINIMUM_COVERAGE
28
+ end
29
+
30
+ private
31
+
32
+ attr_reader :result, :minimum_coverage_by_file
33
+
34
+ def minimum_violations
35
+ @minimum_violations ||=
36
+ compute_minimum_coverage_data.select do |achieved|
37
+ achieved.fetch(:actual) < achieved.fetch(:minimum_expected)
38
+ end
39
+ end
40
+
41
+ def compute_minimum_coverage_data
42
+ minimum_coverage_by_file.flat_map do |criterion, expected_percent|
43
+ result.coverage_statistics_by_file.fetch(criterion).map do |actual_coverage|
44
+ {
45
+ criterion: criterion,
46
+ minimum_expected: expected_percent,
47
+ actual: SimpleCov.round_coverage(actual_coverage.percent)
48
+ }
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleCov
4
+ module ExitCodes
5
+ class MinimumOverallCoverageCheck
6
+ def initialize(result, minimum_coverage)
7
+ @result = result
8
+ @minimum_coverage = minimum_coverage
9
+ end
10
+
11
+ def failing?
12
+ minimum_violations.any?
13
+ end
14
+
15
+ def report
16
+ minimum_violations.each do |violation|
17
+ $stderr.printf(
18
+ "%<criterion>s coverage (%<covered>.2f%%) is below the expected minimum coverage (%<minimum_coverage>.2f%%).\n",
19
+ covered: SimpleCov.round_coverage(violation.fetch(:actual)),
20
+ minimum_coverage: violation.fetch(:minimum_expected),
21
+ criterion: violation.fetch(:criterion).capitalize
22
+ )
23
+ end
24
+ end
25
+
26
+ def exit_code
27
+ SimpleCov::ExitCodes::MINIMUM_COVERAGE
28
+ end
29
+
30
+ private
31
+
32
+ attr_reader :result, :minimum_coverage
33
+
34
+ def minimum_violations
35
+ @minimum_violations ||= calculate_minimum_violations
36
+ end
37
+
38
+ def calculate_minimum_violations
39
+ coverage_achieved = minimum_coverage.map do |criterion, percent|
40
+ {
41
+ criterion: criterion,
42
+ minimum_expected: percent,
43
+ actual: result.coverage_statistics.fetch(criterion).percent
44
+ }
45
+ end
46
+
47
+ coverage_achieved.select do |achieved|
48
+ achieved.fetch(:actual) < achieved.fetch(:minimum_expected)
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleCov
4
+ module ExitCodes
5
+ SUCCESS = 0
6
+ EXCEPTION = 1
7
+ MINIMUM_COVERAGE = 2
8
+ MAXIMUM_COVERAGE_DROP = 3
9
+ end
10
+ end
11
+
12
+ require_relative "exit_codes/exit_code_handling"
13
+ require_relative "exit_codes/maximum_coverage_drop_check"
14
+ require_relative "exit_codes/minimum_coverage_by_file_check"
15
+ require_relative "exit_codes/minimum_overall_coverage_check"
@@ -1,44 +1,120 @@
1
- # An array of SimpleCov SourceFile instances with additional collection helper
2
- # methods for calculating coverage across them etc.
3
- class SimpleCov::FileList < Array
4
- # Returns the count of lines that have coverage
5
- def covered_lines
6
- return 0.0 if empty?
7
- map {|f| f.covered_lines.count }.inject(&:+)
8
- end
1
+ # frozen_string_literal: true
9
2
 
10
- # Returns the count of lines that have been missed
11
- def missed_lines
12
- return 0.0 if empty?
13
- map {|f| f.missed_lines.count }.inject(&:+)
14
- end
3
+ module SimpleCov
4
+ # An array of SimpleCov SourceFile instances with additional collection helper
5
+ # methods for calculating coverage across them etc.
6
+ class FileList
7
+ include Enumerable
8
+ extend Forwardable
15
9
 
16
- # Returns the count of lines that are not relevant for coverage
17
- def never_lines
18
- return 0.0 if empty?
19
- map {|f| f.never_lines.count }.inject(&:+)
20
- end
10
+ def_delegators :@files,
11
+ # For Enumerable
12
+ :each,
13
+ # also delegating methods implemented in Enumerable as they have
14
+ # custom Array implementations which are presumably better/more
15
+ # resource efficient
16
+ :size, :map, :count,
17
+ # surprisingly not in Enumerable
18
+ :empty?, :length,
19
+ # still act like we're kinda an array
20
+ :to_a, :to_ary
21
21
 
22
- # Returns the count of skipped lines
23
- def skipped_lines
24
- return 0.0 if empty?
25
- map {|f| f.skipped_lines.count }.inject(&:+)
26
- end
22
+ def initialize(files)
23
+ @files = files
24
+ end
27
25
 
28
- # Returns the overall amount of relevant lines of code across all files in this list
29
- def lines_of_code
30
- covered_lines + missed_lines
31
- end
26
+ def coverage_statistics
27
+ @coverage_statistics ||= compute_coverage_statistics
28
+ end
32
29
 
33
- # Computes the coverage based upon lines covered and lines missed
34
- def covered_percent
35
- return 100.0 if empty? or lines_of_code == 0
36
- covered_lines * 100.0 / lines_of_code
37
- end
30
+ def coverage_statistics_by_file
31
+ @coverage_statistics_by_file ||= compute_coverage_statistics_by_file
32
+ end
33
+
34
+ # Returns the count of lines that have coverage
35
+ def covered_lines
36
+ coverage_statistics[:line]&.covered
37
+ end
38
+
39
+ # Returns the count of lines that have been missed
40
+ def missed_lines
41
+ coverage_statistics[:line]&.missed
42
+ end
43
+
44
+ # Returns the count of lines that are not relevant for coverage
45
+ def never_lines
46
+ return 0.0 if empty?
47
+
48
+ map { |f| f.never_lines.count }.inject(:+)
49
+ end
50
+
51
+ # Returns the count of skipped lines
52
+ def skipped_lines
53
+ return 0.0 if empty?
54
+
55
+ map { |f| f.skipped_lines.count }.inject(:+)
56
+ end
57
+
58
+ # Computes the coverage based upon lines covered and lines missed for each file
59
+ # Returns an array with all coverage percentages
60
+ def covered_percentages
61
+ map(&:covered_percent)
62
+ end
63
+
64
+ # Finds the least covered file and returns that file's name
65
+ def least_covered_file
66
+ min_by(&:covered_percent).filename
67
+ end
68
+
69
+ # Returns the overall amount of relevant lines of code across all files in this list
70
+ def lines_of_code
71
+ coverage_statistics[:line]&.total
72
+ end
73
+
74
+ # Computes the coverage based upon lines covered and lines missed
75
+ # @return [Float]
76
+ def covered_percent
77
+ coverage_statistics[:line]&.percent
78
+ end
79
+
80
+ # Computes the strength (hits / line) based upon lines covered and lines missed
81
+ # @return [Float]
82
+ def covered_strength
83
+ coverage_statistics[:line]&.strength
84
+ end
85
+
86
+ # Return total count of branches in all files
87
+ def total_branches
88
+ coverage_statistics[:branch]&.total
89
+ end
90
+
91
+ # Return total count of covered branches
92
+ def covered_branches
93
+ coverage_statistics[:branch]&.covered
94
+ end
95
+
96
+ # Return total count of covered branches
97
+ def missed_branches
98
+ coverage_statistics[:branch]&.missed
99
+ end
100
+
101
+ def branch_covered_percent
102
+ coverage_statistics[:branch]&.percent
103
+ end
104
+
105
+ private
106
+
107
+ def compute_coverage_statistics_by_file
108
+ @files.each_with_object(line: [], branch: []) do |file, together|
109
+ together[:line] << file.coverage_statistics.fetch(:line)
110
+ together[:branch] << file.coverage_statistics.fetch(:branch) if SimpleCov.branch_coverage?
111
+ end
112
+ end
38
113
 
39
- # Computes the strength (hits / line) based upon lines covered and lines missed
40
- def covered_strength
41
- return 0 if empty? or lines_of_code == 0
42
- map {|f| f.covered_strength }.inject(&:+) / size
114
+ def compute_coverage_statistics
115
+ coverage_statistics = {line: CoverageStatistics.from(coverage_statistics_by_file[:line])}
116
+ coverage_statistics[:branch] = CoverageStatistics.from(coverage_statistics_by_file[:branch]) if SimpleCov.branch_coverage?
117
+ coverage_statistics
118
+ end
43
119
  end
44
120
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module SimpleCov
2
4
  #
3
5
  # Base filter class. Inherit from this to create custom filters,
@@ -12,25 +14,55 @@ module SimpleCov
12
14
  #
13
15
  class Filter
14
16
  attr_reader :filter_argument
17
+
15
18
  def initialize(filter_argument)
16
19
  @filter_argument = filter_argument
17
20
  end
18
21
 
19
- def matches?(source_file)
22
+ def matches?(_source_file)
20
23
  raise "The base filter class is not intended for direct use"
21
24
  end
22
25
 
23
26
  def passes?(source_file)
24
- warn "DEPRECATION: SimpleCov::Filter#passes?(x) has been renamed to #matches?. Please update your custom filters accordingly!"
27
+ warn "#{Kernel.caller.first}: [DEPRECATION] #passes? is deprecated. Use #matches? instead."
25
28
  matches?(source_file)
26
29
  end
30
+
31
+ def self.build_filter(filter_argument)
32
+ return filter_argument if filter_argument.is_a?(SimpleCov::Filter)
33
+
34
+ class_for_argument(filter_argument).new(filter_argument)
35
+ end
36
+
37
+ def self.class_for_argument(filter_argument)
38
+ case filter_argument
39
+ when String
40
+ SimpleCov::StringFilter
41
+ when Regexp
42
+ SimpleCov::RegexFilter
43
+ when Array
44
+ SimpleCov::ArrayFilter
45
+ when Proc
46
+ SimpleCov::BlockFilter
47
+ else
48
+ raise ArgumentError, "You have provided an unrecognized filter type"
49
+ end
50
+ end
27
51
  end
28
52
 
29
53
  class StringFilter < SimpleCov::Filter
30
54
  # Returns true when the given source file's filename matches the
31
- # string configured when initializing this Filter with StringFilter.new('somestring)
55
+ # string configured when initializing this Filter with StringFilter.new('somestring')
56
+ def matches?(source_file)
57
+ source_file.project_filename.include?(filter_argument)
58
+ end
59
+ end
60
+
61
+ class RegexFilter < SimpleCov::Filter
62
+ # Returns true when the given source file's filename matches the
63
+ # regex configured when initializing this Filter with RegexFilter.new(/someregex/)
32
64
  def matches?(source_file)
33
- (source_file.filename =~ /#{filter_argument}/)
65
+ (source_file.project_filename =~ filter_argument)
34
66
  end
35
67
  end
36
68
 
@@ -41,4 +73,22 @@ module SimpleCov
41
73
  filter_argument.call(source_file)
42
74
  end
43
75
  end
76
+
77
+ class ArrayFilter < SimpleCov::Filter
78
+ def initialize(filter_argument)
79
+ filter_objects = filter_argument.map do |arg|
80
+ Filter.build_filter(arg)
81
+ end
82
+
83
+ super(filter_objects)
84
+ end
85
+
86
+ # Returns true if any of the filters in the array match the given source file.
87
+ # Configure this Filter like StringFilter.new(['some/path', /^some_regex/, Proc.new {|src_file| ... }])
88
+ def matches?(source_files_list)
89
+ filter_argument.any? do |arg|
90
+ arg.matches?(source_files_list)
91
+ end
92
+ end
93
+ end
44
94
  end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleCov
4
+ module Formatter
5
+ class MultiFormatter
6
+ module InstanceMethods
7
+ def format(result)
8
+ formatters.map do |formatter|
9
+ formatter.new.format(result)
10
+ rescue StandardError => e
11
+ warn("Formatter #{formatter} failed with #{e.class}: #{e.message} (#{e.backtrace.first})")
12
+ nil
13
+ end
14
+ end
15
+ end
16
+
17
+ def self.new(formatters = nil)
18
+ Class.new do
19
+ define_method :formatters do
20
+ @formatters ||= Array(formatters)
21
+ end
22
+ include InstanceMethods
23
+ end
24
+ end
25
+
26
+ def self.[](*args)
27
+ warn "#{Kernel.caller.first}: [DEPRECATION] ::[] is deprecated. Use ::new instead."
28
+ new(Array(args))
29
+ end
30
+ end
31
+ end
32
+ end