simplecov 1.0.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 (117) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +56 -1451
  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 +23 -28
  7. data/lib/simplecov/cli/coverage_file.rb +65 -0
  8. data/lib/simplecov/cli/diff.rb +43 -47
  9. data/lib/simplecov/cli/dotfile.rb +21 -10
  10. data/lib/simplecov/cli/merge.rb +17 -13
  11. data/lib/simplecov/cli/open.rb +3 -5
  12. data/lib/simplecov/cli/report.rb +38 -25
  13. data/lib/simplecov/cli/run.rb +1 -1
  14. data/lib/simplecov/cli/serve/report_preparer.rb +29 -0
  15. data/lib/simplecov/cli/serve/static_file_handler.rb +122 -0
  16. data/lib/simplecov/cli/serve.rb +45 -99
  17. data/lib/simplecov/cli/uncovered.rb +20 -24
  18. data/lib/simplecov/cli.rb +15 -2
  19. data/lib/simplecov/color.rb +10 -1
  20. data/lib/simplecov/combine/branches_combiner.rb +43 -18
  21. data/lib/simplecov/combine/coverage_accumulator.rb +268 -0
  22. data/lib/simplecov/combine/identity_interner.rb +30 -0
  23. data/lib/simplecov/combine/interned_counts.rb +30 -0
  24. data/lib/simplecov/combine/lines_combiner.rb +48 -20
  25. data/lib/simplecov/combine/methods_combiner.rb +53 -5
  26. data/lib/simplecov/combine/results_combiner.rb +12 -36
  27. data/lib/simplecov/combine.rb +5 -23
  28. data/lib/simplecov/command_guesser.rb +66 -9
  29. data/lib/simplecov/configuration/coverage.rb +17 -16
  30. data/lib/simplecov/configuration/coverage_criteria.rb +37 -39
  31. data/lib/simplecov/configuration/eval_coverage.rb +41 -0
  32. data/lib/simplecov/configuration/filters.rb +11 -44
  33. data/lib/simplecov/configuration/formatting.rb +33 -14
  34. data/lib/simplecov/configuration/groups.rb +42 -0
  35. data/lib/simplecov/configuration/merging.rb +8 -7
  36. data/lib/simplecov/configuration/thresholds.rb +27 -22
  37. data/lib/simplecov/configuration.rb +21 -42
  38. data/lib/simplecov/coverage_json.rb +24 -0
  39. data/lib/simplecov/coverage_statistics.rb +5 -2
  40. data/lib/simplecov/coverage_violations.rb +17 -6
  41. data/lib/simplecov/defaults.rb +10 -6
  42. data/lib/simplecov/directive.rb +9 -4
  43. data/lib/simplecov/exit_codes/check.rb +33 -0
  44. data/lib/simplecov/exit_codes/maximum_coverage_drop_check.rb +11 -20
  45. data/lib/simplecov/exit_codes/maximum_overall_coverage_check.rb +4 -17
  46. data/lib/simplecov/exit_codes/minimum_coverage_by_file_check.rb +14 -23
  47. data/lib/simplecov/exit_codes/minimum_coverage_by_group_check.rb +14 -25
  48. data/lib/simplecov/exit_codes/minimum_overall_coverage_check.rb +7 -20
  49. data/lib/simplecov/exit_codes.rb +17 -0
  50. data/lib/simplecov/exit_handling.rb +13 -43
  51. data/lib/simplecov/file_list.rb +14 -12
  52. data/lib/simplecov/filter.rb +43 -9
  53. data/lib/simplecov/formatter/base.rb +18 -1
  54. data/lib/simplecov/formatter/coverage_json_writer.rb +97 -0
  55. data/lib/simplecov/formatter/html_formatter/public/index.html +36 -5
  56. data/lib/simplecov/formatter/html_formatter/viewer_data_validator.rb +96 -0
  57. data/lib/simplecov/formatter/html_formatter.rb +71 -45
  58. data/lib/simplecov/formatter/json_formatter/errors_formatter.rb +54 -50
  59. data/lib/simplecov/formatter/json_formatter/result_hash_formatter.rb +78 -88
  60. data/lib/simplecov/formatter/json_formatter/source_file_formatter.rb +70 -76
  61. data/lib/simplecov/formatter/json_formatter.rb +5 -46
  62. data/lib/simplecov/formatter/multi_formatter.rb +8 -3
  63. data/lib/simplecov/formatter/simple_formatter.rb +8 -3
  64. data/lib/simplecov/formatter.rb +12 -0
  65. data/lib/simplecov/group_names.rb +32 -0
  66. data/lib/simplecov/last_run.rb +16 -9
  67. data/lib/simplecov/lines_classifier.rb +29 -8
  68. data/lib/simplecov/load_global_config.rb +5 -2
  69. data/lib/simplecov/parallel_adapters/base.rb +17 -0
  70. data/lib/simplecov/parallel_adapters/generic.rb +9 -8
  71. data/lib/simplecov/parallel_adapters/parallel_tests.rb +2 -2
  72. data/lib/simplecov/parallel_adapters.rb +4 -2
  73. data/lib/simplecov/parallel_coordination.rb +6 -1
  74. data/lib/simplecov/parallel_result_merger.rb +230 -0
  75. data/lib/simplecov/process.rb +22 -0
  76. data/lib/simplecov/report_deferral.rb +49 -0
  77. data/lib/simplecov/report_stamp.rb +28 -0
  78. data/lib/simplecov/result.rb +40 -10
  79. data/lib/simplecov/result_adapter.rb +79 -10
  80. data/lib/simplecov/result_merger/resultset_file.rb +43 -7
  81. data/lib/simplecov/result_merger/resultset_run_identity.rb +67 -0
  82. data/lib/simplecov/result_merger/resultset_store.rb +15 -12
  83. data/lib/simplecov/result_merger/unloaded_files.rb +103 -0
  84. data/lib/simplecov/result_merger.rb +69 -42
  85. data/lib/simplecov/result_processing.rb +85 -46
  86. data/lib/simplecov/run_identity.rb +77 -0
  87. data/lib/simplecov/simulate_coverage.rb +37 -12
  88. data/lib/simplecov/source_file/line.rb +2 -2
  89. data/lib/simplecov/source_file/method.rb +8 -2
  90. data/lib/simplecov/source_file/method_builder.rb +3 -1
  91. data/lib/simplecov/source_file/ruby_data_parser.rb +25 -3
  92. data/lib/simplecov/source_file/skip_chunks.rb +7 -10
  93. data/lib/simplecov/source_file/source_loader.rb +25 -9
  94. data/lib/simplecov/source_file/statistics.rb +24 -16
  95. data/lib/simplecov/source_file.rb +2 -2
  96. data/lib/simplecov/static_coverage_extractor/condition_folding.rb +239 -0
  97. data/lib/simplecov/static_coverage_extractor/location_conventions.rb +236 -0
  98. data/lib/simplecov/static_coverage_extractor/method_collector.rb +7 -0
  99. data/lib/simplecov/static_coverage_extractor/prism_compat.rb +55 -0
  100. data/lib/simplecov/static_coverage_extractor/value_position.rb +74 -0
  101. data/lib/simplecov/static_coverage_extractor/visitor.rb +74 -62
  102. data/lib/simplecov/unloaded_file_injector.rb +75 -0
  103. data/lib/simplecov/version.rb +1 -1
  104. data/lib/simplecov.rb +37 -17
  105. data/sig/simplecov.rbs +1777 -0
  106. metadata +32 -16
  107. data/doc/alternate-formatters.md +0 -66
  108. data/doc/commercial-services.md +0 -25
  109. data/doc/editor-integration.md +0 -18
  110. data/lib/simplecov/combine/files_combiner.rb +0 -30
  111. data/lib/simplecov/formatter/html_formatter/public/application.css +0 -1
  112. data/lib/simplecov/formatter/html_formatter/public/application.js +0 -18
  113. data/lib/simplecov/formatter/html_formatter/public/favicon_green.png +0 -0
  114. data/lib/simplecov/formatter/html_formatter/public/favicon_red.png +0 -0
  115. data/lib/simplecov/formatter/html_formatter/public/favicon_yellow.png +0 -0
  116. data/schemas/coverage-v1.0.schema.json +0 -306
  117. data/schemas/coverage.schema.json +0 -306
@@ -14,9 +14,7 @@ module SimpleCov
14
14
  def minimum_coverage(coverage = nil)
15
15
  return @minimum_coverage ||= {} unless coverage
16
16
 
17
- coverage = {primary_coverage => coverage} if coverage.is_a?(Numeric)
18
- raise_on_invalid_coverage(coverage, "minimum_coverage")
19
- @minimum_coverage = coverage
17
+ @minimum_coverage = normalized_threshold(coverage, "minimum_coverage")
20
18
  end
21
19
 
22
20
  def raise_on_invalid_coverage(coverage, coverage_setting)
@@ -35,9 +33,7 @@ module SimpleCov
35
33
  def maximum_coverage(coverage = nil)
36
34
  return @maximum_coverage ||= {} unless coverage
37
35
 
38
- coverage = {primary_coverage => coverage} if coverage.is_a?(Numeric)
39
- raise_on_invalid_coverage(coverage, "maximum_coverage")
40
- @maximum_coverage = coverage
36
+ @maximum_coverage = normalized_threshold(coverage, "maximum_coverage")
41
37
  end
42
38
 
43
39
  #
@@ -58,9 +54,7 @@ module SimpleCov
58
54
  def maximum_coverage_drop(coverage_drop = nil)
59
55
  return @maximum_coverage_drop ||= {} unless coverage_drop
60
56
 
61
- coverage_drop = {primary_coverage => coverage_drop} if coverage_drop.is_a?(Numeric)
62
- raise_on_invalid_coverage(coverage_drop, "maximum_coverage_drop")
63
- @maximum_coverage_drop = coverage_drop
57
+ @maximum_coverage_drop = normalized_threshold(coverage_drop, "maximum_coverage_drop")
64
58
  end
65
59
 
66
60
  #
@@ -100,10 +94,8 @@ module SimpleCov
100
94
 
101
95
  SimpleCov::Deprecation.warn("`SimpleCov.minimum_coverage_by_group` is deprecated. " \
102
96
  "Replace it with:\n#{per_group_coverage_replacement(coverage)}")
103
- @minimum_coverage_by_group = coverage.dup.transform_values do |group_coverage|
104
- group_coverage = {primary_coverage => group_coverage} if group_coverage.is_a?(Numeric)
105
- raise_on_invalid_coverage(group_coverage, "minimum_coverage_by_group")
106
- group_coverage
97
+ @minimum_coverage_by_group = coverage.to_h do |group_name, group_coverage|
98
+ [GroupNames.normalize(group_name), normalized_threshold(group_coverage, "minimum_coverage_by_group")]
107
99
  end
108
100
  end
109
101
 
@@ -117,13 +109,26 @@ module SimpleCov
117
109
 
118
110
  private
119
111
 
112
+ # Shared normalize-and-validate step behind every threshold setter:
113
+ # a bare Numeric targets the primary criterion, and the resulting
114
+ # per-criterion hash is validated before it is stored.
115
+ def normalized_threshold(coverage, setting)
116
+ coverage = {primary_coverage => coverage} if coverage.is_a?(Numeric)
117
+ raise_on_invalid_coverage(coverage, setting)
118
+ coverage
119
+ end
120
+
120
121
  # Split a `minimum_coverage_by_file` argument into Symbol-keyed
121
122
  # criterion defaults and String/Regexp-keyed per-path overrides;
122
123
  # normalize Numeric override values to `{primary_coverage => N}`
123
124
  # so downstream code only has one shape to handle.
124
125
  def partition_per_file_thresholds(coverage)
125
126
  coverage.each_key { |key| validate_per_file_key(key) }
126
- defaults, raw = coverage.partition { |key, _| key.is_a?(Symbol) }.map(&:to_h)
127
+ pairs = coverage.partition { |key, _| key.is_a?(Symbol) }
128
+ # The assertions restate what the partition predicate guarantees:
129
+ # Symbol keys carry per-criterion Numeric defaults, the rest are paths.
130
+ defaults = pairs[0].to_h #: coverage_thresholds
131
+ raw = pairs[1].to_h #: Hash[String | Regexp, Numeric | coverage_thresholds]
127
132
  overrides = raw.transform_values { |value| value.is_a?(Numeric) ? {primary_coverage => value} : value }
128
133
  [defaults, overrides]
129
134
  end
@@ -143,11 +148,11 @@ module SimpleCov
143
148
  # `minimum_coverage_by_file` argument so the deprecation warning can be
144
149
  # copy-pasted verbatim into the user's config.
145
150
  def per_file_coverage_replacement(defaults, overrides)
146
- by_criterion = Hash.new { |hash, criterion| hash[criterion] = [] }
147
- defaults.each { |criterion, percent| by_criterion[criterion] << "minimum_per_file #{percent}" }
151
+ by_criterion = {} #: Hash[Symbol, Array[String]]
152
+ defaults.each { |criterion, percent| (by_criterion[criterion] ||= []) << "minimum_per_file #{percent}" }
148
153
  overrides.each do |target, criteria|
149
154
  criteria.each do |criterion, percent|
150
- by_criterion[criterion] << "minimum_per_file #{percent}, only: #{target.inspect}"
155
+ (by_criterion[criterion] ||= []) << "minimum_per_file #{percent}, only: #{target.inspect}"
151
156
  end
152
157
  end
153
158
  render_coverage_blocks(by_criterion)
@@ -155,11 +160,11 @@ module SimpleCov
155
160
 
156
161
  # Same, for a (deprecated) `minimum_coverage_by_group` argument.
157
162
  def per_group_coverage_replacement(coverage)
158
- by_criterion = Hash.new { |hash, criterion| hash[criterion] = [] }
159
- coverage.each do |group_name, group_coverage|
160
- group_coverage = {primary_coverage => group_coverage} if group_coverage.is_a?(Numeric)
161
- group_coverage.each do |criterion, percent|
162
- by_criterion[criterion] << "minimum_per_group #{percent}, only: #{group_name.inspect}"
163
+ by_criterion = {} #: Hash[Symbol, Array[String]]
164
+ coverage.each do |group_name, thresholds|
165
+ normalized = (thresholds.is_a?(Numeric) ? {primary_coverage => thresholds} : thresholds) #: coverage_thresholds
166
+ normalized.each do |criterion, percent|
167
+ (by_criterion[criterion] ||= []) << "minimum_per_group #{percent}, only: #{group_name.inspect}"
163
168
  end
164
169
  end
165
170
  render_coverage_blocks(by_criterion)
@@ -51,9 +51,10 @@ module SimpleCov
51
51
  #
52
52
  def coverage_path(path = nil)
53
53
  if path
54
- @coverage_path = File.expand_path(path)
54
+ expanded = File.expand_path(path)
55
+ @coverage_path = expanded
55
56
  @coverage_path_explicit = true
56
- FileUtils.mkdir_p @coverage_path
57
+ FileUtils.mkdir_p expanded
57
58
  end
58
59
 
59
60
  @coverage_path ||= File.expand_path(coverage_dir, root)
@@ -65,9 +66,8 @@ module SimpleCov
65
66
  # with SimpleCov.command_name("test:units").
66
67
  #
67
68
  def command_name(name = nil)
68
- @name = name unless name.nil?
69
- @name ||= SimpleCov::CommandGuesser.guess
70
- @name
69
+ @command_name = name unless name.nil?
70
+ @command_name ||= SimpleCov::CommandGuesser.guess
71
71
  end
72
72
 
73
73
  # Returns the hash of available profiles
@@ -77,20 +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
- block_context = block.binding.receiver
84
+ raise ArgumentError, "configuration block required" unless block
84
85
 
85
- # If the block was defined in our own context, instance_exec is sufficient
86
- return instance_exec(&block) if equal?(block_context)
87
-
88
- # Copy the caller's instance variables in so that references like @filter
89
- # inside the block resolve to the caller's values, not ours.
90
- saved = swap_ivars_from(block_context)
91
- instance_exec(&block)
92
- ensure
93
- restore_ivars(block_context, saved) if defined?(saved) && saved
86
+ if block.parameters.empty?
87
+ instance_exec(&(_ = block))
88
+ else
89
+ yield self
90
+ end
91
+ self
94
92
  end
95
93
 
96
94
  #
@@ -100,12 +98,13 @@ module SimpleCov
100
98
  #
101
99
  def at_exit(&block)
102
100
  @at_exit = block if block
103
- return @at_exit if @at_exit
101
+ configured = @at_exit
102
+ return configured if configured
104
103
  return proc {} unless active_session?
105
104
 
106
105
  @at_exit = proc do
107
106
  result = SimpleCov.result
108
- result.format! if result && SimpleCov.merge_finalization_owner? && SimpleCov.final_result_process?
107
+ result.format! if result && SimpleCov.merge_finalization_owner?
109
108
  end
110
109
  end
111
110
 
@@ -144,40 +143,20 @@ module SimpleCov
144
143
  # SimpleCov.root, capitalized with underscores → spaces.
145
144
  #
146
145
  def project_name(new_name = nil)
147
- return @project_name if defined?(@project_name) && @project_name && new_name.nil?
146
+ current = defined?(@project_name) ? @project_name : nil
147
+ return current if current && new_name.nil?
148
148
 
149
149
  @project_name = new_name if new_name.is_a?(String)
150
150
  @project_name ||= File.basename(root).capitalize.tr("_", " ")
151
151
  end
152
-
153
- private
154
-
155
- # Copy instance variables from block_context into self, saving any
156
- # of ours that would be clobbered. Returns the saved values for
157
- # later restoration.
158
- def swap_ivars_from(block_context)
159
- saved = {}
160
- our_ivars = instance_variables
161
- block_context.instance_variables.each do |ivar|
162
- saved[ivar] = instance_variable_get(ivar) if our_ivars.include?(ivar)
163
- instance_variable_set(ivar, block_context.instance_variable_get(ivar))
164
- end
165
- saved
166
- end
167
-
168
- # Copy instance variables back to block_context and restore our saved values.
169
- def restore_ivars(block_context, saved)
170
- block_context.instance_variables.each do |ivar|
171
- block_context.instance_variable_set(ivar, instance_variable_get(ivar))
172
- end
173
- saved.each { |ivar, value| instance_variable_set(ivar, value) }
174
- end
175
152
  end
176
153
  end
177
154
 
178
155
  require_relative "configuration/coverage"
179
156
  require_relative "configuration/coverage_criteria"
157
+ require_relative "configuration/eval_coverage"
180
158
  require_relative "configuration/filters"
159
+ require_relative "configuration/groups"
181
160
  require_relative "configuration/formatting"
182
161
  require_relative "configuration/ignored_entries"
183
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,13 +10,16 @@ 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
 
17
+ # Seed for the reduce in `.from`: covered, missed, omitted, strength.
18
+ ZERO_STATS = [0, 0, 0, 0.0].freeze #: [Integer, Integer, Integer, Float]
19
+
17
20
  def self.from(coverage_statistics)
18
21
  sum_covered, sum_missed, sum_omitted, sum_total_strength =
19
- coverage_statistics.reduce([0, 0, 0, 0.0]) do |(covered, missed, omitted, total_strength), stats|
22
+ coverage_statistics.reduce(ZERO_STATS) do |(covered, missed, omitted, total_strength), stats|
20
23
  [
21
24
  covered + stats.covered,
22
25
  missed + stats.missed,
@@ -46,7 +46,8 @@ module SimpleCov
46
46
  def minimum_by_group(result, thresholds)
47
47
  thresholds.flat_map do |group_name, minimums|
48
48
  group = lookup_group(result, group_name)
49
- group ? group_minimum_violations(group_name, group, minimums) : []
49
+ none = [] # : Array[Hash[Symbol, untyped]]
50
+ group ? group_minimum_violations(group_name, group, minimums) : none
50
51
  end
51
52
  end
52
53
 
@@ -121,11 +122,15 @@ module SimpleCov
121
122
  end
122
123
  end
123
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.
124
129
  def lookup_group(result, group_name)
125
130
  group = result.groups[group_name]
126
- unless group
127
- warn "minimum_coverage_by_group: no group named '#{group_name}' exists. " \
128
- "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(', ')}"
129
134
  end
130
135
  group
131
136
  end
@@ -133,8 +138,14 @@ module SimpleCov
133
138
  def compute_drop(criterion, result, last_run)
134
139
  stats_key = SimpleCov.coverage_statistics_key(criterion)
135
140
  last_coverage_percent = last_run.dig(:result, stats_key)
136
- last_coverage_percent ||= last_run.dig(:result, :covered_percent) if stats_key == :line
137
- 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)
138
149
 
139
150
  current = percent_for(result, criterion) or return
140
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,15 +41,16 @@ 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
- load filename
45
- rescue LoadError, StandardError
48
+ load filename.to_s
49
+ rescue LoadError, StandardError => e
46
50
  # simplecov:disable — only fires when .simplecov is unreadable
47
51
  # or raises during load
48
52
  warn "Warning: Error occurred while trying to load #{filename}. " \
49
- "Error message: #{$ERROR_INFO.message}"
53
+ "Error message: #{e.message}"
50
54
  # simplecov:enable
51
55
  end
52
56
  break
@@ -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
@@ -55,8 +55,11 @@ module SimpleCov
55
55
  # An unclosed `disable` block extends to the end of the file.
56
56
  def self.disabled_ranges(src_lines)
57
57
  lines = src_lines.to_a
58
- ranges = CATEGORIES.to_h { |category| [category, []] }
59
- open_starts = {}
58
+ ranges = CATEGORIES.to_h do |category|
59
+ empty = [] # : Array[Range[Integer]]
60
+ [category, empty]
61
+ end
62
+ open_starts = {} # : Hash[Symbol, Integer]
60
63
 
61
64
  directives_in(lines).each { |directive| directive.apply(ranges, open_starts) }
62
65
  open_starts.each { |category, start| ranges[category] << (start..lines.size) }
@@ -91,7 +94,9 @@ module SimpleCov
91
94
 
92
95
  new(
93
96
  line_number: line_number,
94
- mode: match[:mode].to_sym,
97
+ # `to_s` is a no-op: the pattern requires the `mode` group, so it
98
+ # is present whenever `match` is; it also satisfies the type checker.
99
+ mode: match[:mode].to_s.to_sym,
95
100
  categories: parse_categories(match[:categories]),
96
101
  inline: inline?(lines, line_number, column + match.begin(0))
97
102
  )
@@ -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| warn 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,32 +7,19 @@ 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)
35
- warn format(
22
+ ExitCodes.print_error format(
36
23
  "%<criterion>s coverage (%<actual>s) is above the expected maximum coverage (%<expected>.2f%%). " \
37
24
  "Time to bump the threshold!",
38
25
  criterion: violation.fetch(:criterion).capitalize,
@@ -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
- warn 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
- warn 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,14 +14,14 @@ 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)
35
22
  criterion = violation.fetch(:criterion)
36
23
  actual = violation.fetch(:actual)
37
- warn format(
24
+ ExitCodes.print_error format(
38
25
  "%<criterion>s coverage (%<actual>s) is below the expected minimum coverage (%<expected>.2f%%).",
39
26
  criterion: criterion.capitalize,
40
27
  actual: SimpleCov::Color.colorize_percent(actual),
@@ -47,9 +34,9 @@ module SimpleCov
47
34
  worst = worst_files_for(criterion)
48
35
  return if worst.empty?
49
36
 
50
- warn " Lowest-coverage files (#{criterion}):"
37
+ ExitCodes.print_error " Lowest-coverage files (#{criterion}):"
51
38
  worst.each do |path, percent|
52
- warn format(
39
+ ExitCodes.print_error format(
53
40
  " %<percent>s %<path>s",
54
41
  percent: SimpleCov::Color.colorize_percent(percent, format("%6.2f%%", percent)),
55
42
  path: path
@@ -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