simplecov 0.17.1 → 0.21.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +100 -434
  3. data/README.md +375 -93
  4. data/doc/alternate-formatters.md +16 -1
  5. data/doc/commercial-services.md +5 -0
  6. data/lib/minitest/simplecov_plugin.rb +15 -0
  7. data/lib/simplecov.rb +294 -128
  8. data/lib/simplecov/combine.rb +30 -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/command_guesser.rb +6 -3
  14. data/lib/simplecov/configuration.rb +191 -15
  15. data/lib/simplecov/coverage_statistics.rb +56 -0
  16. data/lib/simplecov/default_formatter.rb +20 -0
  17. data/lib/simplecov/defaults.rb +14 -13
  18. data/lib/simplecov/exit_codes.rb +5 -0
  19. data/lib/simplecov/exit_codes/exit_code_handling.rb +29 -0
  20. data/lib/simplecov/exit_codes/maximum_coverage_drop_check.rb +73 -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/file_list.rb +72 -13
  24. data/lib/simplecov/filter.rb +9 -6
  25. data/lib/simplecov/formatter.rb +2 -2
  26. data/lib/simplecov/formatter/multi_formatter.rb +5 -7
  27. data/lib/simplecov/formatter/simple_formatter.rb +4 -4
  28. data/lib/simplecov/last_run.rb +3 -1
  29. data/lib/simplecov/lines_classifier.rb +5 -5
  30. data/lib/simplecov/no_defaults.rb +1 -1
  31. data/lib/simplecov/process.rb +19 -0
  32. data/lib/simplecov/profiles.rb +9 -7
  33. data/lib/simplecov/result.rb +18 -12
  34. data/lib/simplecov/result_adapter.rb +30 -0
  35. data/lib/simplecov/result_merger.rb +130 -59
  36. data/lib/simplecov/simulate_coverage.rb +29 -0
  37. data/lib/simplecov/source_file.rb +272 -126
  38. data/lib/simplecov/source_file/branch.rb +84 -0
  39. data/lib/simplecov/source_file/line.rb +72 -0
  40. data/lib/simplecov/useless_results_remover.rb +18 -0
  41. data/lib/simplecov/version.rb +1 -1
  42. metadata +44 -158
  43. data/CONTRIBUTING.md +0 -51
  44. data/ISSUE_TEMPLATE.md +0 -23
  45. data/lib/simplecov/jruby_fix.rb +0 -44
  46. data/lib/simplecov/railtie.rb +0 -9
  47. data/lib/simplecov/railties/tasks.rake +0 -13
  48. data/lib/simplecov/raw_coverage.rb +0 -41
@@ -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,17 +1,20 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Load default formatter gem
4
- require "simplecov-html"
5
4
  require "pathname"
6
- require "simplecov/profiles/root_filter"
7
- require "simplecov/profiles/test_frameworks"
8
- require "simplecov/profiles/bundler_filter"
9
- require "simplecov/profiles/hidden_filter"
10
- require "simplecov/profiles/rails"
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"
11
11
 
12
12
  # Default configuration
13
13
  SimpleCov.configure do
14
- formatter SimpleCov::Formatter::HTMLFormatter
14
+ formatter SimpleCov::Formatter::MultiFormatter.new(
15
+ SimpleCov::Formatter.from_env(ENV)
16
+ )
17
+
15
18
  load_profile "bundler_filter"
16
19
  load_profile "hidden_filter"
17
20
  # Exclude files outside of SimpleCov.root
@@ -22,15 +25,13 @@ end
22
25
  SimpleCov::CommandGuesser.original_run_command = "#{$PROGRAM_NAME} #{ARGV.join(' ')}"
23
26
 
24
27
  at_exit do
25
- # If we are in a different process than called start, don't interfere.
26
- next if SimpleCov.pid != Process.pid
28
+ next if SimpleCov.external_at_exit?
27
29
 
28
- SimpleCov.set_exit_exception
29
- SimpleCov.run_exit_tasks!
30
+ SimpleCov.at_exit_behavior
30
31
  end
31
32
 
32
33
  # Autoload config from ~/.simplecov if present
33
- require "simplecov/load_global_config"
34
+ require_relative "load_global_config"
34
35
 
35
36
  # Autoload config from .simplecov if present
36
37
  # Recurse upwards until we find .simplecov or reach the root directory
@@ -42,7 +43,7 @@ loop do
42
43
  begin
43
44
  load filename
44
45
  rescue LoadError, StandardError
45
- $stderr.puts "Warning: Error occurred while trying to load #{filename}. " \
46
+ warn "Warning: Error occurred while trying to load #{filename}. " \
46
47
  "Error message: #{$!.message}"
47
48
  end
48
49
  break
@@ -8,3 +8,8 @@ module SimpleCov
8
8
  MAXIMUM_COVERAGE_DROP = 3
9
9
  end
10
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"
@@ -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,73 @@
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: last_coverage(criterion) -
55
+ SimpleCov.round_coverage(
56
+ result.coverage_statistics.fetch(criterion).percent
57
+ )
58
+ }
59
+ end
60
+ end
61
+
62
+ def last_coverage(criterion)
63
+ last_coverage_percent = last_run[:result][criterion]
64
+
65
+ if !last_coverage_percent && criterion == "line"
66
+ last_run[:result][:covered_percent]
67
+ else
68
+ last_coverage_percent
69
+ end
70
+ end
71
+ end
72
+ end
73
+ 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
@@ -1,30 +1,57 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # An array of SimpleCov SourceFile instances with additional collection helper
4
- # methods for calculating coverage across them etc.
5
3
  module SimpleCov
6
- class FileList < Array
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
9
+
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
+
22
+ def initialize(files)
23
+ @files = files
24
+ end
25
+
26
+ def coverage_statistics
27
+ @coverage_statistics ||= compute_coverage_statistics
28
+ end
29
+
30
+ def coverage_statistics_by_file
31
+ @coverage_statistics_by_file ||= compute_coverage_statistics_by_file
32
+ end
33
+
7
34
  # Returns the count of lines that have coverage
8
35
  def covered_lines
9
- return 0.0 if empty?
10
- map { |f| f.covered_lines.count }.inject(:+)
36
+ coverage_statistics[:line]&.covered
11
37
  end
12
38
 
13
39
  # Returns the count of lines that have been missed
14
40
  def missed_lines
15
- return 0.0 if empty?
16
- map { |f| f.missed_lines.count }.inject(:+)
41
+ coverage_statistics[:line]&.missed
17
42
  end
18
43
 
19
44
  # Returns the count of lines that are not relevant for coverage
20
45
  def never_lines
21
46
  return 0.0 if empty?
47
+
22
48
  map { |f| f.never_lines.count }.inject(:+)
23
49
  end
24
50
 
25
51
  # Returns the count of skipped lines
26
52
  def skipped_lines
27
53
  return 0.0 if empty?
54
+
28
55
  map { |f| f.skipped_lines.count }.inject(:+)
29
56
  end
30
57
 
@@ -36,26 +63,58 @@ module SimpleCov
36
63
 
37
64
  # Finds the least covered file and returns that file's name
38
65
  def least_covered_file
39
- sort_by(&:covered_percent).first.filename
66
+ min_by(&:covered_percent).filename
40
67
  end
41
68
 
42
69
  # Returns the overall amount of relevant lines of code across all files in this list
43
70
  def lines_of_code
44
- covered_lines + missed_lines
71
+ coverage_statistics[:line]&.total
45
72
  end
46
73
 
47
74
  # Computes the coverage based upon lines covered and lines missed
48
75
  # @return [Float]
49
76
  def covered_percent
50
- return 100.0 if empty? || lines_of_code.zero?
51
- Float(covered_lines * 100.0 / lines_of_code)
77
+ coverage_statistics[:line]&.percent
52
78
  end
53
79
 
54
80
  # Computes the strength (hits / line) based upon lines covered and lines missed
55
81
  # @return [Float]
56
82
  def covered_strength
57
- return 0.0 if empty? || lines_of_code.zero?
58
- Float(map { |f| f.covered_strength * f.lines_of_code }.inject(:+) / lines_of_code)
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
113
+
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
59
118
  end
60
119
  end
61
120
  end