simplecov 0.22.0 → 1.1.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 (137) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +1 -1
  3. data/README.md +71 -916
  4. data/exe/simplecov +11 -0
  5. data/lib/minitest/simplecov_plugin.rb +13 -5
  6. data/lib/simplecov/atomic_file.rb +70 -0
  7. data/lib/simplecov/autostart.rb +11 -0
  8. data/lib/simplecov/cli/clean.rb +87 -0
  9. data/lib/simplecov/cli/command_helpers.rb +55 -0
  10. data/lib/simplecov/cli/coverage.rb +86 -0
  11. data/lib/simplecov/cli/coverage_file.rb +65 -0
  12. data/lib/simplecov/cli/diff.rb +147 -0
  13. data/lib/simplecov/cli/dotfile.rb +111 -0
  14. data/lib/simplecov/cli/merge.rb +120 -0
  15. data/lib/simplecov/cli/open.rb +48 -0
  16. data/lib/simplecov/cli/report.rb +97 -0
  17. data/lib/simplecov/cli/run.rb +36 -0
  18. data/lib/simplecov/cli/serve/report_preparer.rb +29 -0
  19. data/lib/simplecov/cli/serve/static_file_handler.rb +122 -0
  20. data/lib/simplecov/cli/serve.rb +85 -0
  21. data/lib/simplecov/cli/uncovered.rb +103 -0
  22. data/lib/simplecov/cli.rb +163 -0
  23. data/lib/simplecov/color.rb +83 -0
  24. data/lib/simplecov/combine/branches_combiner.rb +54 -7
  25. data/lib/simplecov/combine/coverage_accumulator.rb +268 -0
  26. data/lib/simplecov/combine/identity_interner.rb +30 -0
  27. data/lib/simplecov/combine/interned_counts.rb +30 -0
  28. data/lib/simplecov/combine/lines_combiner.rb +53 -23
  29. data/lib/simplecov/combine/methods_combiner.rb +74 -0
  30. data/lib/simplecov/combine/results_combiner.rb +13 -36
  31. data/lib/simplecov/combine.rb +5 -23
  32. data/lib/simplecov/command_guesser.rb +104 -33
  33. data/lib/simplecov/configuration/coverage.rb +172 -0
  34. data/lib/simplecov/configuration/coverage_criteria.rb +154 -0
  35. data/lib/simplecov/configuration/eval_coverage.rb +41 -0
  36. data/lib/simplecov/configuration/filters.rb +164 -0
  37. data/lib/simplecov/configuration/formatting.rb +138 -0
  38. data/lib/simplecov/configuration/groups.rb +42 -0
  39. data/lib/simplecov/configuration/ignored_entries.rb +63 -0
  40. data/lib/simplecov/configuration/merging.rb +165 -0
  41. data/lib/simplecov/configuration/thresholds.rb +179 -0
  42. data/lib/simplecov/configuration.rb +83 -420
  43. data/lib/simplecov/coverage_json.rb +24 -0
  44. data/lib/simplecov/coverage_statistics.rb +16 -10
  45. data/lib/simplecov/coverage_violations.rb +159 -0
  46. data/lib/simplecov/defaults.rb +34 -23
  47. data/lib/simplecov/deprecation.rb +45 -0
  48. data/lib/simplecov/directive.rb +167 -0
  49. data/lib/simplecov/exit_codes/check.rb +33 -0
  50. data/lib/simplecov/exit_codes/exit_code_handling.rb +8 -2
  51. data/lib/simplecov/exit_codes/maximum_coverage_drop_check.rb +18 -65
  52. data/lib/simplecov/exit_codes/maximum_overall_coverage_check.rb +32 -0
  53. data/lib/simplecov/exit_codes/minimum_coverage_by_file_check.rb +17 -36
  54. data/lib/simplecov/exit_codes/minimum_coverage_by_group_check.rb +30 -0
  55. data/lib/simplecov/exit_codes/minimum_overall_coverage_check.rb +37 -33
  56. data/lib/simplecov/exit_codes.rb +20 -0
  57. data/lib/simplecov/exit_handling.rb +128 -0
  58. data/lib/simplecov/file_list.rb +70 -24
  59. data/lib/simplecov/filter.rb +105 -26
  60. data/lib/simplecov/formatter/base.rb +118 -0
  61. data/lib/simplecov/formatter/coverage_json_writer.rb +97 -0
  62. data/lib/simplecov/formatter/html_formatter/public/index.html +87 -0
  63. data/lib/simplecov/formatter/html_formatter/viewer_data_validator.rb +96 -0
  64. data/lib/simplecov/formatter/html_formatter.rb +105 -0
  65. data/lib/simplecov/formatter/json_formatter/errors_formatter.rb +88 -0
  66. data/lib/simplecov/formatter/json_formatter/result_hash_formatter.rb +118 -0
  67. data/lib/simplecov/formatter/json_formatter/source_file_formatter.rb +93 -0
  68. data/lib/simplecov/formatter/json_formatter.rb +46 -0
  69. data/lib/simplecov/formatter/multi_formatter.rb +12 -8
  70. data/lib/simplecov/formatter/simple_formatter.rb +14 -11
  71. data/lib/simplecov/formatter.rb +16 -0
  72. data/lib/simplecov/group_names.rb +32 -0
  73. data/lib/simplecov/last_run.rb +18 -4
  74. data/lib/simplecov/lines_classifier.rb +48 -15
  75. data/lib/simplecov/load_global_config.rb +12 -4
  76. data/lib/simplecov/parallel_adapters/base.rb +78 -0
  77. data/lib/simplecov/parallel_adapters/generic.rb +45 -0
  78. data/lib/simplecov/parallel_adapters/parallel_tests.rb +93 -0
  79. data/lib/simplecov/parallel_adapters.rb +85 -0
  80. data/lib/simplecov/parallel_coordination.rb +134 -0
  81. data/lib/simplecov/parallel_result_merger.rb +230 -0
  82. data/lib/simplecov/process.rb +52 -12
  83. data/lib/simplecov/profiles/bundler_filter.rb +1 -1
  84. data/lib/simplecov/profiles/hidden_filter.rb +1 -1
  85. data/lib/simplecov/profiles/rails.rb +24 -10
  86. data/lib/simplecov/profiles/root_filter.rb +6 -5
  87. data/lib/simplecov/profiles/strict.rb +32 -0
  88. data/lib/simplecov/profiles/test_frameworks.rb +1 -4
  89. data/lib/simplecov/profiles.rb +32 -3
  90. data/lib/simplecov/report_deferral.rb +49 -0
  91. data/lib/simplecov/report_stamp.rb +28 -0
  92. data/lib/simplecov/result/missing_source_files_reporter.rb +49 -0
  93. data/lib/simplecov/result/source_file_builder.rb +51 -0
  94. data/lib/simplecov/result.rb +145 -26
  95. data/lib/simplecov/result_adapter.rb +139 -8
  96. data/lib/simplecov/result_merger/legacy_format_adapter.rb +28 -0
  97. data/lib/simplecov/result_merger/resultset_file.rb +74 -0
  98. data/lib/simplecov/result_merger/resultset_run_identity.rb +67 -0
  99. data/lib/simplecov/result_merger/resultset_store.rb +53 -0
  100. data/lib/simplecov/result_merger/unloaded_files.rb +103 -0
  101. data/lib/simplecov/result_merger.rb +109 -114
  102. data/lib/simplecov/result_processing.rb +218 -0
  103. data/lib/simplecov/run_identity.rb +77 -0
  104. data/lib/simplecov/simulate_coverage.rb +83 -12
  105. data/lib/simplecov/source_file/branch.rb +1 -3
  106. data/lib/simplecov/source_file/branch_builder.rb +114 -0
  107. data/lib/simplecov/source_file/builder_context.rb +28 -0
  108. data/lib/simplecov/source_file/line.rb +9 -4
  109. data/lib/simplecov/source_file/line_builder.rb +43 -0
  110. data/lib/simplecov/source_file/method.rb +58 -0
  111. data/lib/simplecov/source_file/method_builder.rb +60 -0
  112. data/lib/simplecov/source_file/ruby_data_parser.rb +110 -0
  113. data/lib/simplecov/source_file/skip_chunks.rb +72 -0
  114. data/lib/simplecov/source_file/source_loader.rb +79 -0
  115. data/lib/simplecov/source_file/statistics.rb +65 -0
  116. data/lib/simplecov/source_file.rb +66 -232
  117. data/lib/simplecov/static_coverage_extractor/condition_folding.rb +239 -0
  118. data/lib/simplecov/static_coverage_extractor/location_conventions.rb +236 -0
  119. data/lib/simplecov/static_coverage_extractor/method_collector.rb +62 -0
  120. data/lib/simplecov/static_coverage_extractor/prism_compat.rb +55 -0
  121. data/lib/simplecov/static_coverage_extractor/value_position.rb +74 -0
  122. data/lib/simplecov/static_coverage_extractor/visitor.rb +183 -0
  123. data/lib/simplecov/static_coverage_extractor.rb +110 -0
  124. data/lib/simplecov/unloaded_file_injector.rb +75 -0
  125. data/lib/simplecov/useless_results_remover.rb +16 -7
  126. data/lib/simplecov/version.rb +1 -1
  127. data/lib/simplecov-html.rb +4 -0
  128. data/lib/simplecov.rb +185 -380
  129. data/lib/simplecov_json_formatter.rb +4 -0
  130. data/sig/simplecov.rbs +1777 -0
  131. metadata +111 -62
  132. data/CHANGELOG.md +0 -191
  133. data/doc/alternate-formatters.md +0 -71
  134. data/doc/commercial-services.md +0 -25
  135. data/doc/editor-integration.md +0 -18
  136. data/lib/simplecov/combine/files_combiner.rb +0 -24
  137. data/lib/simplecov/default_formatter.rb +0 -20
@@ -2,25 +2,12 @@
2
2
 
3
3
  module SimpleCov
4
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
5
+ # Fails when any individual file falls below the configured minimum
6
+ # coverage for any criterion.
7
+ class MinimumCoverageByFileCheck < Check
8
+ def initialize(result, minimum_coverage_by_file, overrides = {})
9
+ super(result, minimum_coverage_by_file)
10
+ @overrides = overrides
24
11
  end
25
12
 
26
13
  def exit_code
@@ -29,25 +16,19 @@ module SimpleCov
29
16
 
30
17
  private
31
18
 
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
19
+ def compute_violations
20
+ SimpleCov::CoverageViolations.minimum_by_file(result, thresholds, @overrides)
39
21
  end
40
22
 
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
23
+ def report_violation(violation)
24
+ ExitCodes.print_error format(
25
+ "%<criterion>s coverage by file (%<actual>s) is below the expected minimum coverage " \
26
+ "(%<expected>.2f%%) in %<filename>s.",
27
+ criterion: violation.fetch(:criterion).capitalize,
28
+ actual: SimpleCov::Color.colorize_percent(violation.fetch(:actual)),
29
+ expected: violation.fetch(:expected),
30
+ filename: violation.fetch(:project_filename)
31
+ )
51
32
  end
52
33
  end
53
34
  end
@@ -0,0 +1,30 @@
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 < Check
8
+ def exit_code
9
+ SimpleCov::ExitCodes::MINIMUM_COVERAGE
10
+ end
11
+
12
+ private
13
+
14
+ def compute_violations
15
+ SimpleCov::CoverageViolations.minimum_by_group(result, thresholds)
16
+ end
17
+
18
+ def report_violation(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
+ end
30
+ end
@@ -2,51 +2,55 @@
2
2
 
3
3
  module SimpleCov
4
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
-
5
+ # Fails when the overall (project-wide) coverage for any criterion is
6
+ # below the configured minimum.
7
+ class MinimumOverallCoverageCheck < Check
26
8
  def exit_code
27
9
  SimpleCov::ExitCodes::MINIMUM_COVERAGE
28
10
  end
29
11
 
30
12
  private
31
13
 
32
- attr_reader :result, :minimum_coverage
14
+ WORST_FILES_LIMIT = 5
15
+ private_constant :WORST_FILES_LIMIT
16
+
17
+ def compute_violations
18
+ SimpleCov::CoverageViolations.minimum_overall(result, thresholds)
19
+ end
33
20
 
34
- def minimum_violations
35
- @minimum_violations ||= calculate_minimum_violations
21
+ def report_violation(violation)
22
+ criterion = violation.fetch(:criterion)
23
+ actual = violation.fetch(:actual)
24
+ ExitCodes.print_error format(
25
+ "%<criterion>s coverage (%<actual>s) is below the expected minimum coverage (%<expected>.2f%%).",
26
+ criterion: criterion.capitalize,
27
+ actual: SimpleCov::Color.colorize_percent(actual),
28
+ expected: violation.fetch(:expected)
29
+ )
30
+ report_worst_files(criterion)
36
31
  end
37
32
 
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
- }
33
+ def report_worst_files(criterion)
34
+ worst = worst_files_for(criterion)
35
+ return if worst.empty?
36
+
37
+ ExitCodes.print_error " Lowest-coverage files (#{criterion}):"
38
+ worst.each do |path, percent|
39
+ ExitCodes.print_error format(
40
+ " %<percent>s %<path>s",
41
+ percent: SimpleCov::Color.colorize_percent(percent, format("%6.2f%%", percent)),
42
+ path: path
43
+ )
45
44
  end
45
+ end
46
46
 
47
- coverage_achieved.select do |achieved|
48
- achieved.fetch(:actual) < achieved.fetch(:minimum_expected)
47
+ def worst_files_for(criterion)
48
+ stats_key = SimpleCov.coverage_statistics_key(criterion)
49
+ with_stats = result.files.filter_map do |source_file|
50
+ stats = source_file.coverage_statistics[stats_key]
51
+ [source_file.project_filename, stats.percent] if stats
49
52
  end
53
+ with_stats.sort_by { |_path, percent| percent }.first(WORST_FILES_LIMIT)
50
54
  end
51
55
  end
52
56
  end
@@ -1,15 +1,35 @@
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
+ rescue IOError
22
+ # A parallel runner can close a worker's stderr before its at_exit
23
+ # hooks run (rspec-conductor does). The violation still has to set
24
+ # the exit status even when its explanation cannot be printed.
25
+ end
9
26
  end
10
27
  end
11
28
 
29
+ require_relative "exit_codes/check"
12
30
  require_relative "exit_codes/exit_code_handling"
13
31
  require_relative "exit_codes/maximum_coverage_drop_check"
32
+ require_relative "exit_codes/maximum_overall_coverage_check"
14
33
  require_relative "exit_codes/minimum_coverage_by_file_check"
34
+ require_relative "exit_codes/minimum_coverage_by_group_check"
15
35
  require_relative "exit_codes/minimum_overall_coverage_check"
@@ -0,0 +1,128 @@
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
+ # Capture the suite's exit status BEFORE the deferral probe: its
29
+ # freshness check rescues filesystem errors, and completing any
30
+ # rescue inside an at_exit handler resets $ERROR_INFO to nil.
31
+ error_exit_status = exit_status_from_exception
32
+
33
+ # Stand down when we'd only clobber a fresher report (#581).
34
+ return if defer_to_existing_report?
35
+
36
+ SimpleCov.run_exit_tasks!(error_exit_status)
37
+ end
38
+
39
+ # @api private — called from the at_exit block (which pre-captures
40
+ # the status, see there) and by `collate` (the default argument).
41
+ def run_exit_tasks!(error_exit_status = exit_status_from_exception)
42
+ at_exit.call
43
+
44
+ exit_and_report_previous_error(error_exit_status) if previous_error?(error_exit_status)
45
+ process_results_and_report_error if ready_to_process_results?
46
+ end
47
+
48
+ # @api private — returns the exit status from the exit exception.
49
+ def exit_status_from_exception
50
+ @exit_exception = $ERROR_INFO
51
+ return nil unless @exit_exception
52
+
53
+ if @exit_exception.is_a?(SystemExit)
54
+ @exit_exception.status
55
+ else
56
+ SimpleCov::ExitCodes::EXCEPTION
57
+ end
58
+ end
59
+
60
+ # @api private — strict boolean so rspec-mocks 4's predicate matcher
61
+ # accepts it. test_unit sets status 0 on success, so SUCCESS must
62
+ # also be treated as "not a previous error".
63
+ def previous_error?(error_exit_status)
64
+ !!(error_exit_status && error_exit_status != SimpleCov::ExitCodes::SUCCESS)
65
+ end
66
+
67
+ # @api private
68
+ def exit_and_report_previous_error(exit_status)
69
+ if print_errors
70
+ ExitCodes.print_error SimpleCov::Color.colorize(
71
+ "Stopped processing SimpleCov as a previous error not related to SimpleCov has been detected",
72
+ :yellow
73
+ )
74
+ end
75
+ Kernel.exit(exit_status)
76
+ end
77
+
78
+ # @api private — the process that owns final merge processing is the
79
+ # only one that reports against thresholds, and only when its
80
+ # `wait_for_other_processes` confirmed every sibling reported.
81
+ # When the wait times out, the merged total is partial and
82
+ # comparing it against `minimum_coverage` / `maximum_coverage`
83
+ # would surface a spurious "below minimum" violation about the
84
+ # missing slice rather than a real shortfall.
85
+ def ready_to_process_results?
86
+ merge_finalization_owner? && result? &&
87
+ (collating_result? || parallel_results_complete?)
88
+ end
89
+
90
+ def process_results_and_report_error
91
+ exit_status = process_result(result)
92
+
93
+ # Force exit with stored status (see github issue #5)
94
+ return unless exit_status.positive?
95
+
96
+ if print_errors
97
+ ExitCodes.print_error SimpleCov::Color.colorize(
98
+ "SimpleCov failed with exit #{exit_status} due to a coverage related error", :red
99
+ )
100
+ end
101
+ Kernel.exit exit_status
102
+ end
103
+
104
+ # @api private — `exit_status = SimpleCov.process_result(SimpleCov.result)`.
105
+ def process_result(result)
106
+ result_exit_status = result_exit_status(result)
107
+ write_last_run(result) if result_exit_status == SimpleCov::ExitCodes::SUCCESS
108
+ result_exit_status
109
+ end
110
+
111
+ def result_exit_status(result)
112
+ ExitCodes::ExitCodeHandling.call(result, coverage_limits: build_coverage_limits)
113
+ end
114
+
115
+ private
116
+
117
+ def build_coverage_limits
118
+ CoverageLimits.new(
119
+ minimum_coverage: minimum_coverage,
120
+ minimum_coverage_by_file: minimum_coverage_by_file,
121
+ minimum_coverage_by_file_overrides: minimum_coverage_by_file_overrides,
122
+ minimum_coverage_by_group: minimum_coverage_by_group,
123
+ maximum_coverage: maximum_coverage,
124
+ maximum_coverage_drop: maximum_coverage_drop
125
+ )
126
+ end
127
+ end
128
+ 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
@@ -43,16 +47,12 @@ module SimpleCov
43
47
 
44
48
  # Returns the count of lines that are not relevant for coverage
45
49
  def never_lines
46
- return 0.0 if empty?
47
-
48
- map { |f| f.never_lines.count }.inject(:+)
50
+ sum { |f| f.never_lines.size }
49
51
  end
50
52
 
51
53
  # Returns the count of skipped lines
52
54
  def skipped_lines
53
- return 0.0 if empty?
54
-
55
- map { |f| f.skipped_lines.count }.inject(:+)
55
+ sum { |f| f.skipped_lines.size }
56
56
  end
57
57
 
58
58
  # Computes the coverage based upon lines covered and lines missed for each file
@@ -61,9 +61,13 @@ module SimpleCov
61
61
  map(&:covered_percent)
62
62
  end
63
63
 
64
- # Finds the least covered file and returns that file's name
64
+ # Finds the least covered file and returns that file's name, or nil
65
+ # for an empty list (e.g. a fully filtered result).
65
66
  def least_covered_file
66
- min_by(&:covered_percent).filename
67
+ # `covered_percent` is nil only for an unmeasured criterion, and :line
68
+ # is always measured, so the `|| 0.0` arm never fires at runtime; it
69
+ # exists to satisfy min_by's Comparable requirement.
70
+ min_by { |file| file.covered_percent || 0.0 }&.filename
67
71
  end
68
72
 
69
73
  # Returns the overall amount of relevant lines of code across all files in this list
@@ -71,16 +75,18 @@ module SimpleCov
71
75
  coverage_statistics[:line]&.total
72
76
  end
73
77
 
74
- # Computes the coverage based upon lines covered and lines missed
75
- # @return [Float]
76
- def covered_percent
77
- coverage_statistics[:line]&.percent
78
+ # The coverage across all files in percent, for the given criterion (line
79
+ # by default). Returns nil if the criterion was not measured.
80
+ # @return [Float, nil]
81
+ def covered_percent(criterion = :line)
82
+ coverage_statistics(criterion)&.percent
78
83
  end
79
84
 
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
85
+ # The strength (average hits per relevant unit) for the given criterion
86
+ # (line by default).
87
+ # @return [Float, nil]
88
+ def covered_strength(criterion = :line)
89
+ coverage_statistics(criterion)&.strength
84
90
  end
85
91
 
86
92
  # Return total count of branches in all files
@@ -102,19 +108,59 @@ module SimpleCov
102
108
  coverage_statistics[:branch]&.percent
103
109
  end
104
110
 
111
+ # Return total count of methods in all files
112
+ def total_methods
113
+ coverage_statistics[:method]&.total
114
+ end
115
+
116
+ # Return total count of covered methods
117
+ def covered_methods
118
+ coverage_statistics[:method]&.covered
119
+ end
120
+
121
+ # Return total count of missed methods
122
+ def missed_methods
123
+ coverage_statistics[:method]&.missed
124
+ end
125
+
126
+ def method_covered_percent
127
+ coverage_statistics[:method]&.percent
128
+ end
129
+
105
130
  private
106
131
 
132
+ # Seed the result hash with one entry per criterion the user
133
+ # enabled — so an empty FileList (e.g. a group with no files) still
134
+ # yields the right shape — then fold each file's stats into the
135
+ # matching bucket. `SourceFile#coverage_statistics` always reports
136
+ # all three criteria; FileList is the layer that filters to the
137
+ # enabled set so disabled criteria don't surface in totals, JSON,
138
+ # or the HTML report.
107
139
  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?
140
+ seed = enabled_criteria_for_reporting.to_h do |criterion|
141
+ bucket = [] #: Array[CoverageStatistics]
142
+ [criterion, bucket]
143
+ end
144
+ @files.each_with_object(seed) do |file, together|
145
+ file.coverage_statistics.each do |criterion, stats|
146
+ together[criterion] << stats if together.key?(criterion)
147
+ end
111
148
  end
112
149
  end
113
150
 
114
151
  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
152
+ coverage_statistics_by_file.transform_values { |stats| CoverageStatistics.from(stats) }
153
+ end
154
+
155
+ # `:line` (or its `:oneshot_line` synonym) is reported when either
156
+ # criterion is enabled; the JRuby-gated branch/method criteria are
157
+ # reported when they pass their own engine-support check.
158
+ def enabled_criteria_for_reporting
159
+ criteria = [] #: Array[SimpleCov::criterion]
160
+ criteria << :line if SimpleCov.line_coverage?
161
+ criteria << :branch if SimpleCov.branch_coverage?
162
+ criteria << :method if SimpleCov.method_coverage?
163
+ criteria
118
164
  end
119
165
  end
120
166
  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,52 +20,110 @@ 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"
23
+ raise NotImplementedError, "The base filter class is not intended for direct use"
24
24
  end
25
25
 
26
- def passes?(source_file)
27
- warn "#{Kernel.caller.first}: [DEPRECATION] #passes? is deprecated. Use #matches? instead."
28
- matches?(source_file)
26
+ # Whether this filter's verdict depends only on the file's path, so it can
27
+ # be decided before that file has any coverage. Used when recording which
28
+ # tracked files a process did not load, where no coverage exists yet.
29
+ # Defaults to false so a custom filter is never guessed at. See #1250.
30
+ def path_only?
31
+ false
29
32
  end
30
33
 
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)
34
+ # `string_filter` selects the semantics of bare String arguments —
35
+ # StringFilter's segment-substring match for `add_filter`/`skip`,
36
+ # GlobFilter for `cover` — and threads through Array elements so a
37
+ # list gets the same treatment as its members.
38
+ def self.build_filter(filter_argument, string_filter: SimpleCov::StringFilter)
38
39
  case filter_argument
39
- when String
40
- SimpleCov::StringFilter
41
- when Regexp
42
- SimpleCov::RegexFilter
40
+ when SimpleCov::Filter then filter_argument
41
+ when String then string_filter.new(filter_argument)
43
42
  when Array
44
- SimpleCov::ArrayFilter
45
- when Proc
46
- SimpleCov::BlockFilter
47
- else
48
- raise ArgumentError, "You have provided an unrecognized filter type"
43
+ SimpleCov::ArrayFilter.new(filter_argument.map { |arg| build_filter(arg, string_filter: string_filter) })
44
+ else class_for_argument(filter_argument).new(filter_argument)
49
45
  end
50
46
  end
47
+
48
+ def self.class_for_argument(filter_argument)
49
+ filter_classes_by_argument_type.find { |type, _| filter_argument.is_a?(type) }&.last ||
50
+ raise(SimpleCov::ConfigurationError, "You have provided an unrecognized filter type")
51
+ end
52
+
53
+ def self.filter_classes_by_argument_type
54
+ @filter_classes_by_argument_type ||= {
55
+ String => SimpleCov::StringFilter,
56
+ Regexp => SimpleCov::RegexFilter,
57
+ Array => SimpleCov::ArrayFilter,
58
+ Proc => SimpleCov::BlockFilter
59
+ }.freeze
60
+ end
61
+ private_class_method :filter_classes_by_argument_type
51
62
  end
52
63
 
64
+ # Filter that matches when the source file's project path contains the
65
+ # configured string at a path-segment boundary.
53
66
  class StringFilter < SimpleCov::Filter
54
67
  # Returns true when the given source file's filename matches the
55
- # string configured when initializing this Filter with StringFilter.new('somestring')
68
+ # string configured when initializing this Filter with StringFilter.new('somestring').
69
+ # Matching is path-segment-aware: the argument must appear immediately after a "/"
70
+ # and be followed by "/" or end-of-string, so "lib" matches "/lib/foo.rb" but not
71
+ # "/app/models/library.rb".
56
72
  def matches?(source_file)
57
- source_file.project_filename.include?(filter_argument)
73
+ source_file.project_filename.match?(segment_pattern)
74
+ end
75
+
76
+ def path_only?
77
+ true
78
+ end
79
+
80
+ private
81
+
82
+ def segment_pattern
83
+ @segment_pattern ||= compute_segment_pattern
84
+ end
85
+
86
+ def compute_segment_pattern
87
+ normalized = filter_argument.delete_prefix("/")
88
+ escaped = Regexp.escape(normalized)
89
+ boundary = '(?:\A|/)'
90
+
91
+ if normalized.end_with?("/")
92
+ # Trailing slash signals directory-only matching.
93
+ /#{boundary}#{escaped}/
94
+ elsif normalized.include?(".") && !normalized.include?("/")
95
+ # Bare filename pattern (e.g. "test.rb" matches "faked_test.rb"):
96
+ # allow a substring match within a single path segment. Multi-segment
97
+ # arguments must not get this relaxation, or "app/models/user.rb"
98
+ # would also match "webapp/models/user.rb".
99
+ %r{#{boundary}[^/]*#{escaped}(?=[/.]|\z)}
100
+ else
101
+ # Directory or path: require a segment-boundary match so "lib"
102
+ # matches "lib/" but not "library/".
103
+ %r{#{boundary}#{escaped}(?=[/.]|\z)}
104
+ end
58
105
  end
59
106
  end
60
107
 
108
+ # Filter that matches when the source file's project path matches the
109
+ # configured Regexp.
61
110
  class RegexFilter < SimpleCov::Filter
62
111
  # Returns true when the given source file's filename matches the
63
- # regex configured when initializing this Filter with RegexFilter.new(/someregex/)
112
+ # regex configured when initializing this Filter with RegexFilter.new(/someregex/).
113
+ # Uses `Regexp#match?` so the predicate returns a real boolean — `=~`
114
+ # would return the match position (an Integer or nil), which trips
115
+ # rspec-mocks 4's stricter predicate-matcher type check.
64
116
  def matches?(source_file)
65
- (source_file.project_filename =~ filter_argument)
117
+ filter_argument.match?(source_file.project_filename)
118
+ end
119
+
120
+ def path_only?
121
+ true
66
122
  end
67
123
  end
68
124
 
125
+ # Filter that matches when the configured block returns truthy for the
126
+ # source file.
69
127
  class BlockFilter < SimpleCov::Filter
70
128
  # Returns true if the block given when initializing this filter with BlockFilter.new {|src_file| ... }
71
129
  # returns true for the given source file.
@@ -74,6 +132,22 @@ module SimpleCov
74
132
  end
75
133
  end
76
134
 
135
+ # Filter that matches when the source file's project path matches the
136
+ # configured shell glob (e.g. "lib/**/*.rb"). Used by `cover` and
137
+ # `skip` when callers want glob semantics instead of the substring
138
+ # match of `StringFilter`.
139
+ class GlobFilter < SimpleCov::Filter
140
+ def matches?(source_file)
141
+ File.fnmatch?(filter_argument, source_file.project_filename, File::FNM_PATHNAME | File::FNM_EXTGLOB)
142
+ end
143
+
144
+ def path_only?
145
+ true
146
+ end
147
+ end
148
+
149
+ # Filter that matches when any of its component filters (built from the
150
+ # array's elements) match the source file.
77
151
  class ArrayFilter < SimpleCov::Filter
78
152
  def initialize(filter_argument)
79
153
  filter_objects = filter_argument.map do |arg|
@@ -83,6 +157,11 @@ module SimpleCov
83
157
  super(filter_objects)
84
158
  end
85
159
 
160
+ # Path-decidable only when every component filter is.
161
+ def path_only?
162
+ filter_argument.all?(&:path_only?)
163
+ end
164
+
86
165
  # Returns true if any of the filters in the array match the given source file.
87
166
  # Configure this Filter like StringFilter.new(['some/path', /^some_regex/, Proc.new {|src_file| ... }])
88
167
  def matches?(source_files_list)