simplecov 0.22.0 → 1.0.2

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 (119) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +1 -1
  3. data/README.md +1058 -508
  4. data/doc/alternate-formatters.md +0 -5
  5. data/doc/commercial-services.md +5 -5
  6. data/exe/simplecov +11 -0
  7. data/lib/minitest/simplecov_plugin.rb +13 -5
  8. data/lib/simplecov/autostart.rb +11 -0
  9. data/lib/simplecov/cli/clean.rb +47 -0
  10. data/lib/simplecov/cli/coverage.rb +93 -0
  11. data/lib/simplecov/cli/diff.rb +152 -0
  12. data/lib/simplecov/cli/dotfile.rb +105 -0
  13. data/lib/simplecov/cli/merge.rb +118 -0
  14. data/lib/simplecov/cli/open.rb +50 -0
  15. data/lib/simplecov/cli/report.rb +89 -0
  16. data/lib/simplecov/cli/run.rb +36 -0
  17. data/lib/simplecov/cli/serve.rb +149 -0
  18. data/lib/simplecov/cli/uncovered.rb +108 -0
  19. data/lib/simplecov/cli.rb +150 -0
  20. data/lib/simplecov/color.rb +79 -0
  21. data/lib/simplecov/combine/branches_combiner.rb +28 -5
  22. data/lib/simplecov/combine/files_combiner.rb +47 -1
  23. data/lib/simplecov/combine/lines_combiner.rb +19 -17
  24. data/lib/simplecov/combine/methods_combiner.rb +49 -0
  25. data/lib/simplecov/combine/results_combiner.rb +7 -5
  26. data/lib/simplecov/command_guesser.rb +46 -32
  27. data/lib/simplecov/configuration/coverage.rb +175 -0
  28. data/lib/simplecov/configuration/coverage_criteria.rb +158 -0
  29. data/lib/simplecov/configuration/filters.rb +197 -0
  30. data/lib/simplecov/configuration/formatting.rb +129 -0
  31. data/lib/simplecov/configuration/ignored_entries.rb +63 -0
  32. data/lib/simplecov/configuration/merging.rb +164 -0
  33. data/lib/simplecov/configuration/thresholds.rb +178 -0
  34. data/lib/simplecov/configuration.rb +105 -412
  35. data/lib/simplecov/coverage_statistics.rb +15 -9
  36. data/lib/simplecov/coverage_violations.rb +149 -0
  37. data/lib/simplecov/defaults.rb +29 -22
  38. data/lib/simplecov/deprecation.rb +45 -0
  39. data/lib/simplecov/directive.rb +167 -0
  40. data/lib/simplecov/exit_codes/exit_code_handling.rb +8 -2
  41. data/lib/simplecov/exit_codes/maximum_coverage_drop_check.rb +19 -57
  42. data/lib/simplecov/exit_codes/maximum_overall_coverage_check.rb +45 -0
  43. data/lib/simplecov/exit_codes/minimum_coverage_by_file_check.rb +17 -27
  44. data/lib/simplecov/exit_codes/minimum_coverage_by_group_check.rb +41 -0
  45. data/lib/simplecov/exit_codes/minimum_overall_coverage_check.rb +38 -21
  46. data/lib/simplecov/exit_codes.rb +15 -0
  47. data/lib/simplecov/exit_handling.rb +158 -0
  48. data/lib/simplecov/file_list.rb +70 -19
  49. data/lib/simplecov/filter.rb +69 -24
  50. data/lib/simplecov/formatter/base.rb +102 -0
  51. data/lib/simplecov/formatter/html_formatter/public/application.css +1 -0
  52. data/lib/simplecov/formatter/html_formatter/public/application.js +18 -0
  53. data/lib/simplecov/formatter/html_formatter/public/favicon_green.png +0 -0
  54. data/lib/simplecov/formatter/html_formatter/public/favicon_red.png +0 -0
  55. data/lib/simplecov/formatter/html_formatter/public/favicon_yellow.png +0 -0
  56. data/lib/simplecov/formatter/html_formatter/public/index.html +56 -0
  57. data/lib/simplecov/formatter/html_formatter.rb +85 -0
  58. data/lib/simplecov/formatter/json_formatter/errors_formatter.rb +90 -0
  59. data/lib/simplecov/formatter/json_formatter/result_hash_formatter.rb +128 -0
  60. data/lib/simplecov/formatter/json_formatter/source_file_formatter.rb +99 -0
  61. data/lib/simplecov/formatter/json_formatter.rb +89 -0
  62. data/lib/simplecov/formatter/multi_formatter.rb +11 -7
  63. data/lib/simplecov/formatter/simple_formatter.rb +11 -11
  64. data/lib/simplecov/formatter.rb +4 -0
  65. data/lib/simplecov/last_run.rb +10 -3
  66. data/lib/simplecov/lines_classifier.rb +25 -13
  67. data/lib/simplecov/load_global_config.rb +9 -4
  68. data/lib/simplecov/parallel_adapters/base.rb +61 -0
  69. data/lib/simplecov/parallel_adapters/generic.rb +44 -0
  70. data/lib/simplecov/parallel_adapters/parallel_tests.rb +93 -0
  71. data/lib/simplecov/parallel_adapters.rb +83 -0
  72. data/lib/simplecov/parallel_coordination.rb +129 -0
  73. data/lib/simplecov/process.rb +52 -12
  74. data/lib/simplecov/profiles/bundler_filter.rb +1 -1
  75. data/lib/simplecov/profiles/hidden_filter.rb +1 -1
  76. data/lib/simplecov/profiles/rails.rb +24 -10
  77. data/lib/simplecov/profiles/root_filter.rb +6 -5
  78. data/lib/simplecov/profiles/strict.rb +32 -0
  79. data/lib/simplecov/profiles/test_frameworks.rb +1 -4
  80. data/lib/simplecov/profiles.rb +32 -3
  81. data/lib/simplecov/result/missing_source_files_reporter.rb +49 -0
  82. data/lib/simplecov/result/source_file_builder.rb +51 -0
  83. data/lib/simplecov/result.rb +108 -19
  84. data/lib/simplecov/result_adapter.rb +105 -8
  85. data/lib/simplecov/result_merger/legacy_format_adapter.rb +28 -0
  86. data/lib/simplecov/result_merger/resultset_file.rb +38 -0
  87. data/lib/simplecov/result_merger/resultset_store.rb +50 -0
  88. data/lib/simplecov/result_merger.rb +59 -91
  89. data/lib/simplecov/result_processing.rb +179 -0
  90. data/lib/simplecov/simulate_coverage.rb +54 -8
  91. data/lib/simplecov/source_file/branch.rb +1 -3
  92. data/lib/simplecov/source_file/branch_builder.rb +114 -0
  93. data/lib/simplecov/source_file/builder_context.rb +28 -0
  94. data/lib/simplecov/source_file/line.rb +9 -4
  95. data/lib/simplecov/source_file/line_builder.rb +43 -0
  96. data/lib/simplecov/source_file/method.rb +52 -0
  97. data/lib/simplecov/source_file/method_builder.rb +60 -0
  98. data/lib/simplecov/source_file/ruby_data_parser.rb +88 -0
  99. data/lib/simplecov/source_file/skip_chunks.rb +75 -0
  100. data/lib/simplecov/source_file/source_loader.rb +63 -0
  101. data/lib/simplecov/source_file/statistics.rb +57 -0
  102. data/lib/simplecov/source_file.rb +66 -232
  103. data/lib/simplecov/static_coverage_extractor/condition_folding.rb +49 -0
  104. data/lib/simplecov/static_coverage_extractor/location_conventions.rb +247 -0
  105. data/lib/simplecov/static_coverage_extractor/method_collector.rb +55 -0
  106. data/lib/simplecov/static_coverage_extractor/value_position.rb +82 -0
  107. data/lib/simplecov/static_coverage_extractor/visitor.rb +196 -0
  108. data/lib/simplecov/static_coverage_extractor.rb +110 -0
  109. data/lib/simplecov/useless_results_remover.rb +16 -7
  110. data/lib/simplecov/version.rb +1 -1
  111. data/lib/simplecov-html.rb +4 -0
  112. data/lib/simplecov.rb +178 -380
  113. data/lib/simplecov_json_formatter.rb +4 -0
  114. data/schemas/coverage-v1.0.schema.json +306 -0
  115. data/schemas/coverage.schema.json +306 -0
  116. data/sig/simplecov.rbs +1638 -0
  117. metadata +94 -57
  118. data/CHANGELOG.md +0 -191
  119. data/lib/simplecov/default_formatter.rb +0 -20
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleCov
4
+ module ExitCodes
5
+ # Fails when any configured group falls below its minimum coverage
6
+ # threshold for any criterion.
7
+ class MinimumCoverageByGroupCheck
8
+ def initialize(result, minimum_coverage_by_group)
9
+ @result = result
10
+ @minimum_coverage_by_group = minimum_coverage_by_group
11
+ end
12
+
13
+ def failing?
14
+ violations.any?
15
+ end
16
+
17
+ def report
18
+ violations.each do |violation|
19
+ ExitCodes.print_error format(
20
+ "%<criterion>s coverage by group (%<actual>s) is below the expected minimum coverage " \
21
+ "(%<expected>.2f%%) in %<group_name>s.",
22
+ criterion: violation.fetch(:criterion).capitalize,
23
+ actual: SimpleCov::Color.colorize_percent(violation.fetch(:actual)),
24
+ expected: violation.fetch(:expected),
25
+ group_name: violation.fetch(:group_name)
26
+ )
27
+ end
28
+ end
29
+
30
+ def exit_code
31
+ SimpleCov::ExitCodes::MINIMUM_COVERAGE
32
+ end
33
+
34
+ private
35
+
36
+ def violations
37
+ @violations ||= SimpleCov::CoverageViolations.minimum_by_group(@result, @minimum_coverage_by_group)
38
+ end
39
+ end
40
+ end
41
+ end
@@ -2,6 +2,8 @@
2
2
 
3
3
  module SimpleCov
4
4
  module ExitCodes
5
+ # Fails when the overall (project-wide) coverage for any criterion is
6
+ # below the configured minimum.
5
7
  class MinimumOverallCoverageCheck
6
8
  def initialize(result, minimum_coverage)
7
9
  @result = result
@@ -9,18 +11,11 @@ module SimpleCov
9
11
  end
10
12
 
11
13
  def failing?
12
- minimum_violations.any?
14
+ violations.any?
13
15
  end
14
16
 
15
17
  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
18
+ violations.each { |violation| report_violation(violation) }
24
19
  end
25
20
 
26
21
  def exit_code
@@ -29,24 +24,46 @@ module SimpleCov
29
24
 
30
25
  private
31
26
 
32
- attr_reader :result, :minimum_coverage
27
+ WORST_FILES_LIMIT = 5
28
+ private_constant :WORST_FILES_LIMIT
29
+
30
+ def violations
31
+ @violations ||= SimpleCov::CoverageViolations.minimum_overall(@result, @minimum_coverage)
32
+ end
33
33
 
34
- def minimum_violations
35
- @minimum_violations ||= calculate_minimum_violations
34
+ def report_violation(violation)
35
+ criterion = violation.fetch(:criterion)
36
+ actual = violation.fetch(:actual)
37
+ ExitCodes.print_error format(
38
+ "%<criterion>s coverage (%<actual>s) is below the expected minimum coverage (%<expected>.2f%%).",
39
+ criterion: criterion.capitalize,
40
+ actual: SimpleCov::Color.colorize_percent(actual),
41
+ expected: violation.fetch(:expected)
42
+ )
43
+ report_worst_files(criterion)
36
44
  end
37
45
 
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
- }
46
+ def report_worst_files(criterion)
47
+ worst = worst_files_for(criterion)
48
+ return if worst.empty?
49
+
50
+ ExitCodes.print_error " Lowest-coverage files (#{criterion}):"
51
+ worst.each do |path, percent|
52
+ ExitCodes.print_error format(
53
+ " %<percent>s %<path>s",
54
+ percent: SimpleCov::Color.colorize_percent(percent, format("%6.2f%%", percent)),
55
+ path: path
56
+ )
45
57
  end
58
+ end
46
59
 
47
- coverage_achieved.select do |achieved|
48
- achieved.fetch(:actual) < achieved.fetch(:minimum_expected)
60
+ def worst_files_for(criterion)
61
+ stats_key = SimpleCov.coverage_statistics_key(criterion)
62
+ with_stats = @result.files.filter_map do |source_file|
63
+ stats = source_file.coverage_statistics[stats_key]
64
+ [source_file.project_filename, stats.percent] if stats
49
65
  end
66
+ with_stats.sort_by { |_path, percent| percent }.first(WORST_FILES_LIMIT)
50
67
  end
51
68
  end
52
69
  end
@@ -1,15 +1,30 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SimpleCov
4
+ # Exit statuses SimpleCov sets when coverage checks fail, and the output
5
+ # helper the enforcement machinery reports through.
4
6
  module ExitCodes
5
7
  SUCCESS = 0
6
8
  EXCEPTION = 1
7
9
  MINIMUM_COVERAGE = 2
8
10
  MAXIMUM_COVERAGE_DROP = 3
11
+ MAXIMUM_COVERAGE = 4
12
+
13
+ # Threshold-violation reports and exit-status notices are the output of
14
+ # the enforcement feature, not Ruby warnings: routing them through
15
+ # `Kernel#warn` made `-W0` swallow the explanation for a failing exit
16
+ # code, let `Warning.warn` hooks (warning trackers, raise-on-warning
17
+ # test setups) intercept them mid-`at_exit`, and fed colorized text to
18
+ # warning logs. `print_errors false` remains the intended opt-out.
19
+ def self.print_error(message)
20
+ $stderr.puts message # rubocop:disable Style/StderrPuts
21
+ end
9
22
  end
10
23
  end
11
24
 
12
25
  require_relative "exit_codes/exit_code_handling"
13
26
  require_relative "exit_codes/maximum_coverage_drop_check"
27
+ require_relative "exit_codes/maximum_overall_coverage_check"
14
28
  require_relative "exit_codes/minimum_coverage_by_file_check"
29
+ require_relative "exit_codes/minimum_coverage_by_group_check"
15
30
  require_relative "exit_codes/minimum_overall_coverage_check"
@@ -0,0 +1,158 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "English"
4
+
5
+ # `at_exit` orchestration: post-suite report generation, threshold
6
+ # checks, deferral when a sibling subprocess already wrote a fresher
7
+ # report, and exit-status propagation.
8
+ module SimpleCov
9
+ class << self
10
+ # @api private
11
+ CoverageLimits = Data.define(
12
+ :minimum_coverage,
13
+ :minimum_coverage_by_file,
14
+ :minimum_coverage_by_file_overrides,
15
+ :minimum_coverage_by_group,
16
+ :maximum_coverage,
17
+ :maximum_coverage_drop
18
+ )
19
+
20
+ def at_exit_behavior
21
+ # If we are in a different process than called start, don't interfere.
22
+ return if SimpleCov.pid != Process.pid
23
+
24
+ # If Coverage is no longer running (e.g. someone manually stopped it
25
+ # or a test consumed the result) then don't run exit tasks.
26
+ return unless Coverage.running?
27
+
28
+ # Stand down when we'd only clobber a fresher report. See
29
+ # `defer_to_existing_report?` and issue #581.
30
+ return if defer_to_existing_report?
31
+
32
+ SimpleCov.run_exit_tasks!
33
+ end
34
+
35
+ # Returns true when our process has no coverage data to contribute
36
+ # (after the resultset merge) and a newer report already exists on
37
+ # disk. Typically fires when `SimpleCov.start` ran in a parent
38
+ # process — e.g. a Rakefile or Rails' `Bundler.require` — that
39
+ # shelled out to the test runner. See issue #581.
40
+ def defer_to_existing_report?
41
+ return false unless existing_report_newer_than_us?
42
+
43
+ res = result
44
+ empty = res.nil? || res.files.empty?
45
+ warn_about_deferred_report if empty
46
+ empty
47
+ end
48
+
49
+ def existing_report_newer_than_us?
50
+ return false unless process_start_time
51
+
52
+ last_run_path = File.join(coverage_path, ".last_run.json")
53
+ File.exist?(last_run_path) && File.mtime(last_run_path) > process_start_time
54
+ end
55
+
56
+ def warn_about_deferred_report
57
+ return unless print_errors
58
+
59
+ ExitCodes.print_error SimpleCov::Color.colorize(
60
+ "Skipping SimpleCov report — this process tracked no application code and a newer " \
61
+ "report already exists at #{coverage_path}. This usually means SimpleCov.start ran in a " \
62
+ "parent process (e.g. a Rakefile or Rails' Bundler.require) that shelled out to the test " \
63
+ "runner. See https://github.com/simplecov-ruby/simplecov/issues/581.",
64
+ :yellow
65
+ )
66
+ end
67
+
68
+ # @api private — called from the at_exit block.
69
+ def run_exit_tasks!
70
+ error_exit_status = exit_status_from_exception
71
+
72
+ at_exit.call
73
+
74
+ exit_and_report_previous_error(error_exit_status) if previous_error?(error_exit_status)
75
+ process_results_and_report_error if ready_to_process_results?
76
+ end
77
+
78
+ # @api private — returns the exit status from the exit exception.
79
+ def exit_status_from_exception
80
+ @exit_exception = $ERROR_INFO
81
+ return nil unless @exit_exception
82
+
83
+ if @exit_exception.is_a?(SystemExit)
84
+ @exit_exception.status
85
+ else
86
+ SimpleCov::ExitCodes::EXCEPTION
87
+ end
88
+ end
89
+
90
+ # @api private — strict boolean so rspec-mocks 4's predicate matcher
91
+ # accepts it. test_unit sets status 0 on success, so SUCCESS must
92
+ # also be treated as "not a previous error".
93
+ def previous_error?(error_exit_status)
94
+ !!(error_exit_status && error_exit_status != SimpleCov::ExitCodes::SUCCESS)
95
+ end
96
+
97
+ # @api private
98
+ def exit_and_report_previous_error(exit_status)
99
+ if print_errors
100
+ ExitCodes.print_error SimpleCov::Color.colorize(
101
+ "Stopped processing SimpleCov as a previous error not related to SimpleCov has been detected",
102
+ :yellow
103
+ )
104
+ end
105
+ Kernel.exit(exit_status)
106
+ end
107
+
108
+ # @api private — the process that owns final merge processing is the
109
+ # only one that reports against thresholds, and only when its
110
+ # `wait_for_other_processes` confirmed every sibling reported.
111
+ # When the wait times out, the merged total is partial and
112
+ # comparing it against `minimum_coverage` / `maximum_coverage`
113
+ # would surface a spurious "below minimum" violation about the
114
+ # missing slice rather than a real shortfall.
115
+ def ready_to_process_results?
116
+ merge_finalization_owner? && final_result_process? && result? &&
117
+ (collating_result? || parallel_results_complete?)
118
+ end
119
+
120
+ def process_results_and_report_error
121
+ exit_status = process_result(result)
122
+
123
+ # Force exit with stored status (see github issue #5)
124
+ return unless exit_status.positive?
125
+
126
+ if print_errors
127
+ ExitCodes.print_error SimpleCov::Color.colorize(
128
+ "SimpleCov failed with exit #{exit_status} due to a coverage related error", :red
129
+ )
130
+ end
131
+ Kernel.exit exit_status
132
+ end
133
+
134
+ # @api private — `exit_status = SimpleCov.process_result(SimpleCov.result)`.
135
+ def process_result(result)
136
+ result_exit_status = result_exit_status(result)
137
+ write_last_run(result) if result_exit_status == SimpleCov::ExitCodes::SUCCESS
138
+ result_exit_status
139
+ end
140
+
141
+ def result_exit_status(result)
142
+ ExitCodes::ExitCodeHandling.call(result, coverage_limits: build_coverage_limits)
143
+ end
144
+
145
+ private
146
+
147
+ def build_coverage_limits
148
+ CoverageLimits.new(
149
+ minimum_coverage: minimum_coverage,
150
+ minimum_coverage_by_file: minimum_coverage_by_file,
151
+ minimum_coverage_by_file_overrides: minimum_coverage_by_file_overrides,
152
+ minimum_coverage_by_group: minimum_coverage_by_group,
153
+ maximum_coverage: maximum_coverage,
154
+ maximum_coverage_drop: maximum_coverage_drop
155
+ )
156
+ end
157
+ end
158
+ end
@@ -23,8 +23,12 @@ module SimpleCov
23
23
  @files = files
24
24
  end
25
25
 
26
- def coverage_statistics
27
- @coverage_statistics ||= compute_coverage_statistics
26
+ # The per-criterion coverage statistics across all files. With no argument
27
+ # returns the `{line:, branch:, method:}` Hash; pass a criterion symbol
28
+ # (`:line` / `:branch` / `:method`) to get that one CoverageStatistics.
29
+ def coverage_statistics(criterion = nil)
30
+ stats = (@coverage_statistics ||= compute_coverage_statistics)
31
+ criterion ? stats[criterion] : stats
28
32
  end
29
33
 
30
34
  def coverage_statistics_by_file
@@ -45,14 +49,14 @@ module SimpleCov
45
49
  def never_lines
46
50
  return 0.0 if empty?
47
51
 
48
- map { |f| f.never_lines.count }.inject(:+)
52
+ sum { |f| f.never_lines.size }
49
53
  end
50
54
 
51
55
  # Returns the count of skipped lines
52
56
  def skipped_lines
53
57
  return 0.0 if empty?
54
58
 
55
- map { |f| f.skipped_lines.count }.inject(:+)
59
+ sum { |f| f.skipped_lines.size }
56
60
  end
57
61
 
58
62
  # Computes the coverage based upon lines covered and lines missed for each file
@@ -63,7 +67,11 @@ module SimpleCov
63
67
 
64
68
  # Finds the least covered file and returns that file's name
65
69
  def least_covered_file
66
- min_by(&:covered_percent).filename
70
+ # `covered_percent` is nil only for an unmeasured criterion, and :line
71
+ # is always measured, so the `|| 0.0` arm never fires at runtime; it
72
+ # (and the cast) exist to satisfy min_by's Comparable requirement.
73
+ least_covered = min_by { |file| file.covered_percent || 0.0 }
74
+ (_ = least_covered).filename
67
75
  end
68
76
 
69
77
  # Returns the overall amount of relevant lines of code across all files in this list
@@ -71,16 +79,18 @@ module SimpleCov
71
79
  coverage_statistics[:line]&.total
72
80
  end
73
81
 
74
- # Computes the coverage based upon lines covered and lines missed
75
- # @return [Float]
76
- def covered_percent
77
- coverage_statistics[:line]&.percent
82
+ # The coverage across all files in percent, for the given criterion (line
83
+ # by default). Returns nil if the criterion was not measured.
84
+ # @return [Float, nil]
85
+ def covered_percent(criterion = :line)
86
+ coverage_statistics(criterion)&.percent
78
87
  end
79
88
 
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
89
+ # The strength (average hits per relevant unit) for the given criterion
90
+ # (line by default).
91
+ # @return [Float, nil]
92
+ def covered_strength(criterion = :line)
93
+ coverage_statistics(criterion)&.strength
84
94
  end
85
95
 
86
96
  # Return total count of branches in all files
@@ -102,19 +112,60 @@ module SimpleCov
102
112
  coverage_statistics[:branch]&.percent
103
113
  end
104
114
 
115
+ # Return total count of methods in all files
116
+ def total_methods
117
+ coverage_statistics[:method]&.total
118
+ end
119
+
120
+ # Return total count of covered methods
121
+ def covered_methods
122
+ coverage_statistics[:method]&.covered
123
+ end
124
+
125
+ # Return total count of missed methods
126
+ def missed_methods
127
+ coverage_statistics[:method]&.missed
128
+ end
129
+
130
+ def method_covered_percent
131
+ coverage_statistics[:method]&.percent
132
+ end
133
+
105
134
  private
106
135
 
136
+ # Seed the result hash with one entry per criterion the user
137
+ # enabled — so an empty FileList (e.g. a group with no files) still
138
+ # yields the right shape — then fold each file's stats into the
139
+ # matching bucket. `SourceFile#coverage_statistics` always reports
140
+ # all three criteria; FileList is the layer that filters to the
141
+ # enabled set so disabled criteria don't surface in totals, JSON,
142
+ # or the HTML report.
107
143
  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?
144
+ seed = enabled_criteria_for_reporting.to_h do |criterion|
145
+ bucket = [] #: Array[CoverageStatistics]
146
+ [criterion, bucket]
147
+ end
148
+ @files.each_with_object(seed) do |file, together|
149
+ file.coverage_statistics.each do |criterion, stats|
150
+ together[criterion] << stats if together.key?(criterion)
151
+ end
111
152
  end
112
153
  end
113
154
 
114
155
  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
156
+ coverage_statistics_by_file.transform_values { |stats| CoverageStatistics.from(stats) }
157
+ end
158
+
159
+ # `:line` (or its `:oneshot_line` synonym) is reported when either
160
+ # criterion is enabled; the JRuby-gated branch/method criteria are
161
+ # reported when they pass their own engine-support check.
162
+ def enabled_criteria_for_reporting
163
+ criteria = [] #: Array[SimpleCov::criterion]
164
+ criteria << :line if SimpleCov.coverage_criterion_enabled?(:line) ||
165
+ SimpleCov.coverage_criterion_enabled?(:oneshot_line)
166
+ criteria << :branch if SimpleCov.branch_coverage?
167
+ criteria << :method if SimpleCov.method_coverage?
168
+ criteria
118
169
  end
119
170
  end
120
171
  end
@@ -3,11 +3,11 @@
3
3
  module SimpleCov
4
4
  #
5
5
  # Base filter class. Inherit from this to create custom filters,
6
- # and overwrite the passes?(source_file) instance method
6
+ # and overwrite the matches?(source_file) instance method
7
7
  #
8
8
  # # A sample class that rejects all source files.
9
9
  # class StupidFilter < SimpleCov::Filter
10
- # def passes?(source_file)
10
+ # def matches?(source_file)
11
11
  # false
12
12
  # end
13
13
  # end
@@ -20,12 +20,7 @@ module SimpleCov
20
20
  end
21
21
 
22
22
  def matches?(_source_file)
23
- raise "The base filter class is not intended for direct use"
24
- end
25
-
26
- def passes?(source_file)
27
- warn "#{Kernel.caller.first}: [DEPRECATION] #passes? is deprecated. Use #matches? instead."
28
- matches?(source_file)
23
+ raise NotImplementedError, "The base filter class is not intended for direct use"
29
24
  end
30
25
 
31
26
  def self.build_filter(filter_argument)
@@ -35,37 +30,75 @@ module SimpleCov
35
30
  end
36
31
 
37
32
  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
33
+ filter_classes_by_argument_type.find { |type, _| filter_argument.is_a?(type) }&.last ||
34
+ raise(SimpleCov::ConfigurationError, "You have provided an unrecognized filter type")
50
35
  end
36
+
37
+ def self.filter_classes_by_argument_type
38
+ @filter_classes_by_argument_type ||= {
39
+ String => SimpleCov::StringFilter,
40
+ Regexp => SimpleCov::RegexFilter,
41
+ Array => SimpleCov::ArrayFilter,
42
+ Proc => SimpleCov::BlockFilter
43
+ }.freeze
44
+ end
45
+ private_class_method :filter_classes_by_argument_type
51
46
  end
52
47
 
48
+ # Filter that matches when the source file's project path contains the
49
+ # configured string at a path-segment boundary.
53
50
  class StringFilter < SimpleCov::Filter
54
51
  # Returns true when the given source file's filename matches the
55
- # string configured when initializing this Filter with StringFilter.new('somestring')
52
+ # string configured when initializing this Filter with StringFilter.new('somestring').
53
+ # Matching is path-segment-aware: the argument must appear immediately after a "/"
54
+ # and be followed by "/" or end-of-string, so "lib" matches "/lib/foo.rb" but not
55
+ # "/app/models/library.rb".
56
56
  def matches?(source_file)
57
- source_file.project_filename.include?(filter_argument)
57
+ source_file.project_filename.match?(segment_pattern)
58
+ end
59
+
60
+ private
61
+
62
+ def segment_pattern
63
+ @segment_pattern ||= compute_segment_pattern
64
+ end
65
+
66
+ def compute_segment_pattern
67
+ normalized = filter_argument.delete_prefix("/")
68
+ escaped = Regexp.escape(normalized)
69
+ boundary = '(?:\A|/)'
70
+
71
+ if normalized.include?(".")
72
+ # Filename pattern (e.g. "test.rb" matches "faked_test.rb"): allow
73
+ # substring match within the last path segment, anchored to a
74
+ # segment boundary.
75
+ %r{#{boundary}[^/]*#{escaped}}
76
+ elsif normalized.end_with?("/")
77
+ # Trailing slash signals directory-only matching.
78
+ /#{boundary}#{escaped}/
79
+ else
80
+ # Directory or path: require a segment-boundary match so "lib"
81
+ # matches "lib/" but not "library/".
82
+ %r{#{boundary}#{escaped}(?=[/.]|\z)}
83
+ end
58
84
  end
59
85
  end
60
86
 
87
+ # Filter that matches when the source file's project path matches the
88
+ # configured Regexp.
61
89
  class RegexFilter < SimpleCov::Filter
62
90
  # Returns true when the given source file's filename matches the
63
- # regex configured when initializing this Filter with RegexFilter.new(/someregex/)
91
+ # regex configured when initializing this Filter with RegexFilter.new(/someregex/).
92
+ # Uses `Regexp#match?` so the predicate returns a real boolean — `=~`
93
+ # would return the match position (an Integer or nil), which trips
94
+ # rspec-mocks 4's stricter predicate-matcher type check.
64
95
  def matches?(source_file)
65
- (source_file.project_filename =~ filter_argument)
96
+ filter_argument.match?(source_file.project_filename)
66
97
  end
67
98
  end
68
99
 
100
+ # Filter that matches when the configured block returns truthy for the
101
+ # source file.
69
102
  class BlockFilter < SimpleCov::Filter
70
103
  # Returns true if the block given when initializing this filter with BlockFilter.new {|src_file| ... }
71
104
  # returns true for the given source file.
@@ -74,6 +107,18 @@ module SimpleCov
74
107
  end
75
108
  end
76
109
 
110
+ # Filter that matches when the source file's project path matches the
111
+ # configured shell glob (e.g. "lib/**/*.rb"). Used by `cover` and
112
+ # `skip` when callers want glob semantics instead of the substring
113
+ # match of `StringFilter`.
114
+ class GlobFilter < SimpleCov::Filter
115
+ def matches?(source_file)
116
+ File.fnmatch?(filter_argument, source_file.project_filename, File::FNM_PATHNAME | File::FNM_EXTGLOB)
117
+ end
118
+ end
119
+
120
+ # Filter that matches when any of its component filters (built from the
121
+ # array's elements) match the source file.
77
122
  class ArrayFilter < SimpleCov::Filter
78
123
  def initialize(filter_argument)
79
124
  filter_objects = filter_argument.map do |arg|