simplecov 1.0.2 → 1.1.0

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 (110) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +65 -1425
  3. data/lib/simplecov/atomic_file.rb +70 -0
  4. data/lib/simplecov/cli/clean.rb +43 -3
  5. data/lib/simplecov/cli/command_helpers.rb +55 -0
  6. data/lib/simplecov/cli/coverage.rb +21 -28
  7. data/lib/simplecov/cli/coverage_file.rb +65 -0
  8. data/lib/simplecov/cli/diff.rb +43 -48
  9. data/lib/simplecov/cli/dotfile.rb +12 -6
  10. data/lib/simplecov/cli/merge.rb +12 -10
  11. data/lib/simplecov/cli/open.rb +3 -5
  12. data/lib/simplecov/cli/report.rb +31 -23
  13. data/lib/simplecov/cli/serve/report_preparer.rb +29 -0
  14. data/lib/simplecov/cli/serve/static_file_handler.rb +122 -0
  15. data/lib/simplecov/cli/serve.rb +30 -94
  16. data/lib/simplecov/cli/uncovered.rb +20 -25
  17. data/lib/simplecov/cli.rb +15 -2
  18. data/lib/simplecov/combine/branches_combiner.rb +43 -19
  19. data/lib/simplecov/combine/coverage_accumulator.rb +268 -0
  20. data/lib/simplecov/combine/identity_interner.rb +30 -0
  21. data/lib/simplecov/combine/interned_counts.rb +30 -0
  22. data/lib/simplecov/combine/lines_combiner.rb +48 -20
  23. data/lib/simplecov/combine/methods_combiner.rb +46 -21
  24. data/lib/simplecov/combine/results_combiner.rb +12 -37
  25. data/lib/simplecov/combine.rb +5 -23
  26. data/lib/simplecov/command_guesser.rb +66 -9
  27. data/lib/simplecov/configuration/coverage.rb +12 -15
  28. data/lib/simplecov/configuration/coverage_criteria.rb +34 -38
  29. data/lib/simplecov/configuration/eval_coverage.rb +41 -0
  30. data/lib/simplecov/configuration/filters.rb +11 -44
  31. data/lib/simplecov/configuration/formatting.rb +18 -9
  32. data/lib/simplecov/configuration/groups.rb +42 -0
  33. data/lib/simplecov/configuration/merging.rb +8 -7
  34. data/lib/simplecov/configuration/thresholds.rb +14 -13
  35. data/lib/simplecov/configuration.rb +13 -43
  36. data/lib/simplecov/coverage_json.rb +24 -0
  37. data/lib/simplecov/coverage_statistics.rb +1 -1
  38. data/lib/simplecov/coverage_violations.rb +15 -5
  39. data/lib/simplecov/defaults.rb +7 -3
  40. data/lib/simplecov/directive.rb +1 -1
  41. data/lib/simplecov/exit_codes/check.rb +33 -0
  42. data/lib/simplecov/exit_codes/maximum_coverage_drop_check.rb +11 -20
  43. data/lib/simplecov/exit_codes/maximum_overall_coverage_check.rb +3 -16
  44. data/lib/simplecov/exit_codes/minimum_coverage_by_file_check.rb +14 -23
  45. data/lib/simplecov/exit_codes/minimum_coverage_by_group_check.rb +14 -25
  46. data/lib/simplecov/exit_codes/minimum_overall_coverage_check.rb +4 -17
  47. data/lib/simplecov/exit_codes.rb +1 -0
  48. data/lib/simplecov/exit_handling.rb +11 -41
  49. data/lib/simplecov/file_list.rb +5 -10
  50. data/lib/simplecov/filter.rb +43 -9
  51. data/lib/simplecov/formatter/base.rb +11 -0
  52. data/lib/simplecov/formatter/coverage_json_writer.rb +97 -0
  53. data/lib/simplecov/formatter/html_formatter/public/index.html +35 -5
  54. data/lib/simplecov/formatter/html_formatter/viewer_data_validator.rb +96 -0
  55. data/lib/simplecov/formatter/html_formatter.rb +67 -47
  56. data/lib/simplecov/formatter/json_formatter/errors_formatter.rb +54 -56
  57. data/lib/simplecov/formatter/json_formatter/result_hash_formatter.rb +78 -88
  58. data/lib/simplecov/formatter/json_formatter/source_file_formatter.rb +70 -76
  59. data/lib/simplecov/formatter/json_formatter.rb +5 -48
  60. data/lib/simplecov/formatter/multi_formatter.rb +1 -1
  61. data/lib/simplecov/formatter/simple_formatter.rb +8 -5
  62. data/lib/simplecov/formatter.rb +12 -0
  63. data/lib/simplecov/group_names.rb +32 -0
  64. data/lib/simplecov/last_run.rb +16 -9
  65. data/lib/simplecov/lines_classifier.rb +29 -8
  66. data/lib/simplecov/load_global_config.rb +5 -2
  67. data/lib/simplecov/parallel_adapters/base.rb +17 -0
  68. data/lib/simplecov/parallel_adapters/generic.rb +2 -2
  69. data/lib/simplecov/parallel_adapters/parallel_tests.rb +2 -2
  70. data/lib/simplecov/parallel_coordination.rb +6 -1
  71. data/lib/simplecov/parallel_result_merger.rb +230 -0
  72. data/lib/simplecov/report_deferral.rb +49 -0
  73. data/lib/simplecov/report_stamp.rb +28 -0
  74. data/lib/simplecov/result.rb +40 -10
  75. data/lib/simplecov/result_adapter.rb +48 -16
  76. data/lib/simplecov/result_merger/resultset_file.rb +43 -7
  77. data/lib/simplecov/result_merger/resultset_run_identity.rb +67 -0
  78. data/lib/simplecov/result_merger/resultset_store.rb +15 -12
  79. data/lib/simplecov/result_merger/unloaded_files.rb +103 -0
  80. data/lib/simplecov/result_merger.rb +69 -42
  81. data/lib/simplecov/result_processing.rb +82 -43
  82. data/lib/simplecov/run_identity.rb +77 -0
  83. data/lib/simplecov/simulate_coverage.rb +35 -11
  84. data/lib/simplecov/source_file/method.rb +7 -1
  85. data/lib/simplecov/source_file/ruby_data_parser.rb +25 -3
  86. data/lib/simplecov/source_file/skip_chunks.rb +7 -10
  87. data/lib/simplecov/source_file/source_loader.rb +23 -7
  88. data/lib/simplecov/source_file/statistics.rb +24 -16
  89. data/lib/simplecov/static_coverage_extractor/condition_folding.rb +203 -13
  90. data/lib/simplecov/static_coverage_extractor/location_conventions.rb +19 -30
  91. data/lib/simplecov/static_coverage_extractor/method_collector.rb +7 -0
  92. data/lib/simplecov/static_coverage_extractor/prism_compat.rb +55 -0
  93. data/lib/simplecov/static_coverage_extractor/value_position.rb +6 -14
  94. data/lib/simplecov/static_coverage_extractor/visitor.rb +23 -36
  95. data/lib/simplecov/unloaded_file_injector.rb +75 -0
  96. data/lib/simplecov/version.rb +1 -1
  97. data/lib/simplecov.rb +11 -5
  98. data/schemas/coverage-v1.0.schema.json +2 -2
  99. data/schemas/coverage.schema.json +2 -2
  100. data/sig/simplecov.rbs +206 -67
  101. metadata +28 -14
  102. data/doc/alternate-formatters.md +0 -66
  103. data/doc/commercial-services.md +0 -25
  104. data/doc/editor-integration.md +0 -18
  105. data/lib/simplecov/combine/files_combiner.rb +0 -70
  106. data/lib/simplecov/formatter/html_formatter/public/application.css +0 -1
  107. data/lib/simplecov/formatter/html_formatter/public/application.js +0 -18
  108. data/lib/simplecov/formatter/html_formatter/public/favicon_green.png +0 -0
  109. data/lib/simplecov/formatter/html_formatter/public/favicon_red.png +0 -0
  110. data/lib/simplecov/formatter/html_formatter/public/favicon_yellow.png +0 -0
@@ -77,27 +77,18 @@ module SimpleCov
77
77
 
78
78
  #
79
79
  # Allows you to configure simplecov in a block instead of
80
- # prepending SimpleCov to each config method.
80
+ # prepending SimpleCov to each config method. Parameterized blocks retain
81
+ # their caller context and receive this configuration target explicitly.
81
82
  #
82
83
  def configure(&block)
83
- # Both locals are read in the ensure clause, where flow analysis
84
- # cannot see the assignments below; anchor their types up front.
85
- saved = nil # : Hash[Symbol, untyped]?
86
- block_context = block.binding.receiver # : untyped
87
-
88
- # If the block was defined in our own context, instance_exec is
89
- # sufficient. The `_ =` erases the block's self-binding, which RBS
90
- # can express but `instance_exec`'s core signature cannot accept.
91
- return instance_exec(&(_ = block)) if equal?(block_context)
92
-
93
- # Copy the caller's instance variables in so that references like @filter
94
- # inside the block resolve to the caller's values, not ours.
95
- saved = swap_ivars_from(block_context)
96
- instance_exec(&(_ = block))
97
- ensure
98
- # @type var block_context: untyped
99
- # @type var saved: Hash[Symbol, untyped]?
100
- restore_ivars(block_context, saved) if saved
84
+ raise ArgumentError, "configuration block required" unless block
85
+
86
+ if block.parameters.empty?
87
+ instance_exec(&(_ = block))
88
+ else
89
+ yield self
90
+ end
91
+ self
101
92
  end
102
93
 
103
94
  #
@@ -113,7 +104,7 @@ module SimpleCov
113
104
 
114
105
  @at_exit = proc do
115
106
  result = SimpleCov.result
116
- result.format! if result && SimpleCov.merge_finalization_owner? && SimpleCov.final_result_process?
107
+ result.format! if result && SimpleCov.merge_finalization_owner?
117
108
  end
118
109
  end
119
110
 
@@ -158,35 +149,14 @@ module SimpleCov
158
149
  @project_name = new_name if new_name.is_a?(String)
159
150
  @project_name ||= File.basename(root).capitalize.tr("_", " ")
160
151
  end
161
-
162
- private
163
-
164
- # Copy instance variables from block_context into self, saving any
165
- # of ours that would be clobbered. Returns the saved values for
166
- # later restoration.
167
- def swap_ivars_from(block_context)
168
- saved = {} # : Hash[Symbol, untyped]
169
- our_ivars = instance_variables
170
- block_context.instance_variables.each do |ivar|
171
- saved[ivar] = instance_variable_get(ivar) if our_ivars.include?(ivar)
172
- instance_variable_set(ivar, block_context.instance_variable_get(ivar))
173
- end
174
- saved
175
- end
176
-
177
- # Copy instance variables back to block_context and restore our saved values.
178
- def restore_ivars(block_context, saved)
179
- block_context.instance_variables.each do |ivar|
180
- block_context.instance_variable_set(ivar, instance_variable_get(ivar))
181
- end
182
- saved.each { |ivar, value| instance_variable_set(ivar, value) }
183
- end
184
152
  end
185
153
  end
186
154
 
187
155
  require_relative "configuration/coverage"
188
156
  require_relative "configuration/coverage_criteria"
157
+ require_relative "configuration/eval_coverage"
189
158
  require_relative "configuration/filters"
159
+ require_relative "configuration/groups"
190
160
  require_relative "configuration/formatting"
191
161
  require_relative "configuration/ignored_entries"
192
162
  require_relative "configuration/merging"
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module SimpleCov
6
+ # Shared parser for coverage.json consumers. It enforces the encoding and
7
+ # stable outermost shape while leaving command- or viewer-specific fields to
8
+ # their respective callers.
9
+ module CoverageJSON
10
+ class Error < StandardError; end
11
+
12
+ def self.load(path)
13
+ source = File.binread(path).force_encoding(Encoding::UTF_8)
14
+ raise Error, "input is not valid UTF-8" unless source.valid_encoding?
15
+
16
+ document = JSON.parse(source)
17
+ return document if document.is_a?(Hash)
18
+
19
+ raise Error, "top-level value must be an object"
20
+ rescue JSON::ParserError => e
21
+ raise Error, e.message
22
+ end
23
+ end
24
+ end
@@ -10,7 +10,7 @@ module SimpleCov
10
10
  # * missed - how many of the coverables are missed
11
11
  # * omitted - how many lines cannot be covered (blank lines/comments); only meaningful for line coverage
12
12
  # * percent - percentage as covered/missed
13
- # * strength - average hits per/coverable (will not exist for one shot lines format)
13
+ # * strength - average hits per coverable unit (will not exist for one shot lines format)
14
14
  class CoverageStatistics
15
15
  attr_reader :total, :covered, :missed, :omitted, :strength, :percent
16
16
 
@@ -122,11 +122,15 @@ module SimpleCov
122
122
  end
123
123
  end
124
124
 
125
+ # The misconfiguration notice is enforcement output, not a Ruby
126
+ # warning: like every other enforcement message it must survive
127
+ # `-W0` and `Warning.warn` hooks (see ExitCodes.print_error) and
128
+ # honor the `print_errors` opt-out.
125
129
  def lookup_group(result, group_name)
126
130
  group = result.groups[group_name]
127
- unless group
128
- warn "minimum_coverage_by_group: no group named '#{group_name}' exists. " \
129
- "Available groups: #{result.groups.keys.join(', ')}"
131
+ if group.nil? && SimpleCov.print_errors
132
+ ExitCodes.print_error "minimum_coverage_by_group: no group named '#{group_name}' exists. " \
133
+ "Available groups: #{result.groups.keys.join(', ')}"
130
134
  end
131
135
  group
132
136
  end
@@ -134,8 +138,14 @@ module SimpleCov
134
138
  def compute_drop(criterion, result, last_run)
135
139
  stats_key = SimpleCov.coverage_statistics_key(criterion)
136
140
  last_coverage_percent = last_run.dig(:result, stats_key)
137
- last_coverage_percent ||= last_run.dig(:result, :covered_percent) if stats_key == :line
138
- return unless last_coverage_percent
141
+ if last_coverage_percent.nil? && stats_key == :line
142
+ last_coverage_percent = last_run.dig(:result, :covered_percent)
143
+ end
144
+ # A hand-edited .last_run.json can carry any value type, and
145
+ # LastRun.read only vouches for the top level being a Hash.
146
+ # Treat a non-numeric percent like a missing one instead of
147
+ # raising out of the at_exit hook.
148
+ return unless last_coverage_percent.is_a?(Numeric)
139
149
 
140
150
  current = percent_for(result, criterion) or return
141
151
  (last_coverage_percent - current).floor(10)
@@ -24,8 +24,11 @@ SimpleCov.configure do
24
24
  load_profile "test_frameworks"
25
25
  end
26
26
 
27
- # Gotta stash this a-s-a-p, see the CommandGuesser class and i.e. #110 for further info
27
+ # Gotta stash this a-s-a-p, see the CommandGuesser class and i.e. #110 for further info.
28
+ # The program name is kept separately as well: the join below is lossy for a
29
+ # path containing a space, and the guesser reads the executable from it.
28
30
  SimpleCov::CommandGuesser.original_run_command = "#{$PROGRAM_NAME} #{ARGV.join(' ')}"
31
+ SimpleCov::CommandGuesser.original_program_name = $PROGRAM_NAME
29
32
 
30
33
  # Autoload config from ~/.simplecov if present
31
34
  require_relative "load_global_config"
@@ -38,8 +41,9 @@ loop do
38
41
  filename = config_path.join(".simplecov")
39
42
  if filename.exist?
40
43
  # `.simplecov` is a configuration file; SimpleCov.start calls inside
41
- # it are intercepted and converted to configuration, with a deprecation
42
- # warning. See `SimpleCov.with_dot_simplecov_autoload` and issue #581.
44
+ # it draw a deprecation warning, and tracking still begins for
45
+ # backward compatibility. See `SimpleCov.with_dot_simplecov_autoload`
46
+ # and issue #581.
43
47
  SimpleCov.with_dot_simplecov_autoload do
44
48
  load filename.to_s
45
49
  rescue LoadError, StandardError => e
@@ -43,7 +43,7 @@ module SimpleCov
43
43
  PATTERN = /
44
44
  \#\s*simplecov\s*:\s*
45
45
  (?<mode>disable|enable)\b
46
- (?:\s+(?<categories>#{CATEGORIES_PATTERN}))?
46
+ (?:\s+(?<categories>#{CATEGORIES_PATTERN})\b)?
47
47
  .*?
48
48
  \s*\z
49
49
  /x
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleCov
4
+ module ExitCodes
5
+ # Shared skeleton for the threshold checks: stash the result and the
6
+ # threshold configuration, fail when any violation exists, and
7
+ # report one message per violation. Subclasses supply `exit_code`,
8
+ # `compute_violations` (memoized here — `failing?` and `report` both
9
+ # consult it), and `report_violation`.
10
+ class Check
11
+ def initialize(result, thresholds)
12
+ @result = result
13
+ @thresholds = thresholds
14
+ end
15
+
16
+ def failing?
17
+ violations.any?
18
+ end
19
+
20
+ def report
21
+ violations.each { |violation| report_violation(violation) }
22
+ end
23
+
24
+ private
25
+
26
+ attr_reader :result, :thresholds
27
+
28
+ def violations
29
+ @violations ||= compute_violations
30
+ end
31
+ end
32
+ end
33
+ end
@@ -4,29 +4,24 @@ module SimpleCov
4
4
  module ExitCodes
5
5
  # Fails when any coverage criterion has dropped by more than the
6
6
  # configured maximum since the last recorded run.
7
- class MaximumCoverageDropCheck
8
- def initialize(result, maximum_coverage_drop)
9
- @result = result
10
- @maximum_coverage_drop = maximum_coverage_drop
11
- end
12
-
13
- def failing?
14
- violations.any?
15
- end
16
-
17
- def report
18
- violations.each { |violation| ExitCodes.print_error SimpleCov::Color.colorize(message_for(violation), :red) }
19
- end
20
-
7
+ class MaximumCoverageDropCheck < Check
21
8
  def exit_code
22
9
  SimpleCov::ExitCodes::MAXIMUM_COVERAGE_DROP
23
10
  end
24
11
 
25
12
  private
26
13
 
14
+ def compute_violations
15
+ SimpleCov::CoverageViolations.maximum_drop(result, thresholds)
16
+ end
17
+
27
18
  # The "drop percent" is a delta, not a coverage level — it has no
28
- # natural green/yellow/red mapping. Callers color the whole line red
29
- # so the failure is still visible at a glance.
19
+ # natural green/yellow/red mapping, so the whole line goes red to
20
+ # keep the failure visible at a glance.
21
+ def report_violation(violation)
22
+ ExitCodes.print_error SimpleCov::Color.colorize(message_for(violation), :red)
23
+ end
24
+
30
25
  def message_for(violation)
31
26
  format(
32
27
  "%<criterion>s coverage has dropped by %<drop_percent>.2f%% since the last time " \
@@ -36,10 +31,6 @@ module SimpleCov
36
31
  max_drop: violation.fetch(:maximum)
37
32
  )
38
33
  end
39
-
40
- def violations
41
- @violations ||= SimpleCov::CoverageViolations.maximum_drop(@result, @maximum_coverage_drop)
42
- end
43
34
  end
44
35
  end
45
36
  end
@@ -7,28 +7,15 @@ module SimpleCov
7
7
  # `SimpleCov::ExitCodes::MinimumOverallCoverageCheck` (or use
8
8
  # `SimpleCov.expected_coverage`) to pin coverage to an exact value
9
9
  # and surface unexpected increases instead of silently absorbing them.
10
- class MaximumOverallCoverageCheck
11
- def initialize(result, maximum_coverage)
12
- @result = result
13
- @maximum_coverage = maximum_coverage
14
- end
15
-
16
- def failing?
17
- violations.any?
18
- end
19
-
20
- def report
21
- violations.each { |violation| report_violation(violation) }
22
- end
23
-
10
+ class MaximumOverallCoverageCheck < Check
24
11
  def exit_code
25
12
  SimpleCov::ExitCodes::MAXIMUM_COVERAGE
26
13
  end
27
14
 
28
15
  private
29
16
 
30
- def violations
31
- @violations ||= SimpleCov::CoverageViolations.maximum_overall(@result, @maximum_coverage)
17
+ def compute_violations
18
+ SimpleCov::CoverageViolations.maximum_overall(result, thresholds)
32
19
  end
33
20
 
34
21
  def report_violation(violation)
@@ -4,39 +4,30 @@ module SimpleCov
4
4
  module ExitCodes
5
5
  # Fails when any individual file falls below the configured minimum
6
6
  # coverage for any criterion.
7
- class MinimumCoverageByFileCheck
7
+ class MinimumCoverageByFileCheck < Check
8
8
  def initialize(result, minimum_coverage_by_file, overrides = {})
9
- @result = result
10
- @minimum_coverage_by_file = minimum_coverage_by_file
9
+ super(result, minimum_coverage_by_file)
11
10
  @overrides = overrides
12
11
  end
13
12
 
14
- def failing?
15
- violations.any?
16
- end
17
-
18
- def report
19
- violations.each do |violation|
20
- ExitCodes.print_error format(
21
- "%<criterion>s coverage by file (%<actual>s) is below the expected minimum coverage " \
22
- "(%<expected>.2f%%) in %<filename>s.",
23
- criterion: violation.fetch(:criterion).capitalize,
24
- actual: SimpleCov::Color.colorize_percent(violation.fetch(:actual)),
25
- expected: violation.fetch(:expected),
26
- filename: violation.fetch(:project_filename)
27
- )
28
- end
29
- end
30
-
31
13
  def exit_code
32
14
  SimpleCov::ExitCodes::MINIMUM_COVERAGE
33
15
  end
34
16
 
35
17
  private
36
18
 
37
- def violations
38
- @violations ||= SimpleCov::CoverageViolations.minimum_by_file(
39
- @result, @minimum_coverage_by_file, @overrides
19
+ def compute_violations
20
+ SimpleCov::CoverageViolations.minimum_by_file(result, thresholds, @overrides)
21
+ end
22
+
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)
40
31
  )
41
32
  end
42
33
  end
@@ -4,37 +4,26 @@ module SimpleCov
4
4
  module ExitCodes
5
5
  # Fails when any configured group falls below its minimum coverage
6
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
-
7
+ class MinimumCoverageByGroupCheck < Check
30
8
  def exit_code
31
9
  SimpleCov::ExitCodes::MINIMUM_COVERAGE
32
10
  end
33
11
 
34
12
  private
35
13
 
36
- def violations
37
- @violations ||= SimpleCov::CoverageViolations.minimum_by_group(@result, @minimum_coverage_by_group)
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
+ )
38
27
  end
39
28
  end
40
29
  end
@@ -4,20 +4,7 @@ module SimpleCov
4
4
  module ExitCodes
5
5
  # Fails when the overall (project-wide) coverage for any criterion is
6
6
  # below the configured minimum.
7
- class MinimumOverallCoverageCheck
8
- def initialize(result, minimum_coverage)
9
- @result = result
10
- @minimum_coverage = minimum_coverage
11
- end
12
-
13
- def failing?
14
- violations.any?
15
- end
16
-
17
- def report
18
- violations.each { |violation| report_violation(violation) }
19
- end
20
-
7
+ class MinimumOverallCoverageCheck < Check
21
8
  def exit_code
22
9
  SimpleCov::ExitCodes::MINIMUM_COVERAGE
23
10
  end
@@ -27,8 +14,8 @@ module SimpleCov
27
14
  WORST_FILES_LIMIT = 5
28
15
  private_constant :WORST_FILES_LIMIT
29
16
 
30
- def violations
31
- @violations ||= SimpleCov::CoverageViolations.minimum_overall(@result, @minimum_coverage)
17
+ def compute_violations
18
+ SimpleCov::CoverageViolations.minimum_overall(result, thresholds)
32
19
  end
33
20
 
34
21
  def report_violation(violation)
@@ -59,7 +46,7 @@ module SimpleCov
59
46
 
60
47
  def worst_files_for(criterion)
61
48
  stats_key = SimpleCov.coverage_statistics_key(criterion)
62
- with_stats = @result.files.filter_map do |source_file|
49
+ with_stats = result.files.filter_map do |source_file|
63
50
  stats = source_file.coverage_statistics[stats_key]
64
51
  [source_file.project_filename, stats.percent] if stats
65
52
  end
@@ -22,6 +22,7 @@ module SimpleCov
22
22
  end
23
23
  end
24
24
 
25
+ require_relative "exit_codes/check"
25
26
  require_relative "exit_codes/exit_code_handling"
26
27
  require_relative "exit_codes/maximum_coverage_drop_check"
27
28
  require_relative "exit_codes/maximum_overall_coverage_check"
@@ -25,50 +25,20 @@ module SimpleCov
25
25
  # or a test consumed the result) then don't run exit tasks.
26
26
  return unless Coverage.running?
27
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
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
55
32
 
56
- def warn_about_deferred_report
57
- return unless print_errors
33
+ # Stand down when we'd only clobber a fresher report (#581).
34
+ return if defer_to_existing_report?
58
35
 
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
- )
36
+ SimpleCov.run_exit_tasks!(error_exit_status)
66
37
  end
67
38
 
68
- # @api private — called from the at_exit block.
69
- def run_exit_tasks!
70
- error_exit_status = exit_status_from_exception
71
-
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)
72
42
  at_exit.call
73
43
 
74
44
  exit_and_report_previous_error(error_exit_status) if previous_error?(error_exit_status)
@@ -113,7 +83,7 @@ module SimpleCov
113
83
  # would surface a spurious "below minimum" violation about the
114
84
  # missing slice rather than a real shortfall.
115
85
  def ready_to_process_results?
116
- merge_finalization_owner? && final_result_process? && result? &&
86
+ merge_finalization_owner? && result? &&
117
87
  (collating_result? || parallel_results_complete?)
118
88
  end
119
89
 
@@ -47,15 +47,11 @@ module SimpleCov
47
47
 
48
48
  # Returns the count of lines that are not relevant for coverage
49
49
  def never_lines
50
- return 0.0 if empty?
51
-
52
50
  sum { |f| f.never_lines.size }
53
51
  end
54
52
 
55
53
  # Returns the count of skipped lines
56
54
  def skipped_lines
57
- return 0.0 if empty?
58
-
59
55
  sum { |f| f.skipped_lines.size }
60
56
  end
61
57
 
@@ -65,13 +61,13 @@ module SimpleCov
65
61
  map(&:covered_percent)
66
62
  end
67
63
 
68
- # 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).
69
66
  def least_covered_file
70
67
  # `covered_percent` is nil only for an unmeasured criterion, and :line
71
68
  # 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
69
+ # exists to satisfy min_by's Comparable requirement.
70
+ min_by { |file| file.covered_percent || 0.0 }&.filename
75
71
  end
76
72
 
77
73
  # Returns the overall amount of relevant lines of code across all files in this list
@@ -161,8 +157,7 @@ module SimpleCov
161
157
  # reported when they pass their own engine-support check.
162
158
  def enabled_criteria_for_reporting
163
159
  criteria = [] #: Array[SimpleCov::criterion]
164
- criteria << :line if SimpleCov.coverage_criterion_enabled?(:line) ||
165
- SimpleCov.coverage_criterion_enabled?(:oneshot_line)
160
+ criteria << :line if SimpleCov.line_coverage?
166
161
  criteria << :branch if SimpleCov.branch_coverage?
167
162
  criteria << :method if SimpleCov.method_coverage?
168
163
  criteria