simplecov 1.1.0 → 1.2.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 (196) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +165 -57
  3. data/exe/simplecov +0 -3
  4. data/lib/minitest/simplecov_plugin.rb +12 -9
  5. data/lib/simplecov/atomic_file.rb +16 -19
  6. data/lib/simplecov/autostart.rb +5 -7
  7. data/lib/simplecov/baseline/parser.rb +58 -0
  8. data/lib/simplecov/baseline.rb +170 -0
  9. data/lib/simplecov/cli/affected/changed_files.rb +57 -0
  10. data/lib/simplecov/cli/affected/selection.rb +117 -0
  11. data/lib/simplecov/cli/affected.rb +148 -0
  12. data/lib/simplecov/cli/badge/svg.rb +70 -0
  13. data/lib/simplecov/cli/badge.rb +62 -0
  14. data/lib/simplecov/cli/clean.rb +23 -24
  15. data/lib/simplecov/cli/command_helpers.rb +55 -19
  16. data/lib/simplecov/cli/completions/scripts.rb +114 -0
  17. data/lib/simplecov/cli/completions.rb +83 -0
  18. data/lib/simplecov/cli/coverage.rb +25 -28
  19. data/lib/simplecov/cli/coverage_file.rb +58 -14
  20. data/lib/simplecov/cli/dead_code/output.rb +100 -0
  21. data/lib/simplecov/cli/dead_code.rb +139 -0
  22. data/lib/simplecov/cli/diff/output.rb +45 -0
  23. data/lib/simplecov/cli/diff.rb +35 -67
  24. data/lib/simplecov/cli/dotfile.rb +83 -38
  25. data/lib/simplecov/cli/git.rb +41 -0
  26. data/lib/simplecov/cli/history/output.rb +135 -0
  27. data/lib/simplecov/cli/history.rb +67 -0
  28. data/lib/simplecov/cli/merge.rb +34 -39
  29. data/lib/simplecov/cli/open.rb +12 -15
  30. data/lib/simplecov/cli/patch/changed_lines.rb +149 -0
  31. data/lib/simplecov/cli/patch/output.rb +135 -0
  32. data/lib/simplecov/cli/patch.rb +169 -0
  33. data/lib/simplecov/cli/ratchet/output.rb +44 -0
  34. data/lib/simplecov/cli/ratchet.rb +83 -0
  35. data/lib/simplecov/cli/report.rb +18 -24
  36. data/lib/simplecov/cli/run.rb +8 -9
  37. data/lib/simplecov/cli/serve/report_preparer.rb +7 -6
  38. data/lib/simplecov/cli/serve/static_file_handler.rb +62 -56
  39. data/lib/simplecov/cli/serve.rb +27 -24
  40. data/lib/simplecov/cli/show/annotator.rb +87 -0
  41. data/lib/simplecov/cli/show/sweep.rb +36 -0
  42. data/lib/simplecov/cli/show.rb +133 -0
  43. data/lib/simplecov/cli/status/facts.rb +87 -0
  44. data/lib/simplecov/cli/status.rb +93 -0
  45. data/lib/simplecov/cli/tests/redundancy.rb +78 -0
  46. data/lib/simplecov/cli/tests.rb +148 -0
  47. data/lib/simplecov/cli/uncovered/misses.rb +37 -0
  48. data/lib/simplecov/cli/uncovered.rb +70 -32
  49. data/lib/simplecov/cli/usage.rb +152 -0
  50. data/lib/simplecov/cli/watch/live_report.rb +73 -0
  51. data/lib/simplecov/cli/watch/narrator.rb +55 -0
  52. data/lib/simplecov/cli/watch/poller.rb +45 -0
  53. data/lib/simplecov/cli/watch/session.rb +134 -0
  54. data/lib/simplecov/cli/watch/test_plan.rb +43 -0
  55. data/lib/simplecov/cli/watch.rb +85 -0
  56. data/lib/simplecov/cli.rb +43 -92
  57. data/lib/simplecov/color.rb +21 -35
  58. data/lib/simplecov/combine/branches_combiner.rb +7 -31
  59. data/lib/simplecov/combine/coverage_accumulator.rb +49 -142
  60. data/lib/simplecov/combine/identity_interner.rb +10 -13
  61. data/lib/simplecov/combine/interned_counts.rb +7 -9
  62. data/lib/simplecov/combine/lines_combiner.rb +20 -44
  63. data/lib/simplecov/combine/methods_combiner.rb +9 -35
  64. data/lib/simplecov/combine/results_combiner.rb +4 -21
  65. data/lib/simplecov/combine.rb +0 -6
  66. data/lib/simplecov/command_guesser.rb +20 -34
  67. data/lib/simplecov/configuration/baseline.rb +31 -0
  68. data/lib/simplecov/configuration/coverage.rb +91 -79
  69. data/lib/simplecov/configuration/coverage_criteria.rb +43 -54
  70. data/lib/simplecov/configuration/deprecations.rb +21 -0
  71. data/lib/simplecov/configuration/eval_coverage.rb +6 -10
  72. data/lib/simplecov/configuration/filters.rb +40 -73
  73. data/lib/simplecov/configuration/formatting.rb +74 -83
  74. data/lib/simplecov/configuration/groups.rb +19 -13
  75. data/lib/simplecov/configuration/history.rb +44 -0
  76. data/lib/simplecov/configuration/ignored_entries.rb +31 -22
  77. data/lib/simplecov/configuration/merging.rb +94 -62
  78. data/lib/simplecov/configuration/missed_caps.rb +51 -0
  79. data/lib/simplecov/configuration/production.rb +23 -0
  80. data/lib/simplecov/configuration/test_tracking.rb +48 -0
  81. data/lib/simplecov/configuration/thresholds.rb +33 -62
  82. data/lib/simplecov/configuration/view_coverage.rb +38 -0
  83. data/lib/simplecov/configuration.rb +70 -76
  84. data/lib/simplecov/configuration_error.rb +5 -0
  85. data/lib/simplecov/context_map/union.rb +81 -0
  86. data/lib/simplecov/context_map.rb +187 -0
  87. data/lib/simplecov/coverage_json.rb +2 -5
  88. data/lib/simplecov/coverage_statistics.rb +14 -24
  89. data/lib/simplecov/coverage_violations.rb +157 -64
  90. data/lib/simplecov/current_run.rb +61 -0
  91. data/lib/simplecov/defaults.rb +4 -23
  92. data/lib/simplecov/deprecation.rb +45 -27
  93. data/lib/simplecov/directive.rb +46 -62
  94. data/lib/simplecov/exit_codes/baseline_check.rb +46 -0
  95. data/lib/simplecov/exit_codes/check.rb +17 -11
  96. data/lib/simplecov/exit_codes/exit_code_handling.rb +21 -8
  97. data/lib/simplecov/exit_codes/maximum_coverage_drop_check.rb +7 -10
  98. data/lib/simplecov/exit_codes/maximum_missed_check.rb +26 -0
  99. data/lib/simplecov/exit_codes/maximum_missed_per_file_check.rb +34 -0
  100. data/lib/simplecov/exit_codes/maximum_overall_coverage_check.rb +7 -12
  101. data/lib/simplecov/exit_codes/minimum_coverage_by_file_check.rb +11 -10
  102. data/lib/simplecov/exit_codes/minimum_coverage_by_group_check.rb +7 -9
  103. data/lib/simplecov/exit_codes/minimum_overall_coverage_check.rb +16 -17
  104. data/lib/simplecov/exit_codes.rb +17 -7
  105. data/lib/simplecov/exit_handling.rb +94 -59
  106. data/lib/simplecov/file_list.rb +20 -51
  107. data/lib/simplecov/filter.rb +43 -66
  108. data/lib/simplecov/formatter/base.rb +38 -51
  109. data/lib/simplecov/formatter/baseline_formatter.rb +98 -0
  110. data/lib/simplecov/formatter/coverage_json_writer.rb +29 -31
  111. data/lib/simplecov/formatter/html_formatter/public/index.html +17 -10
  112. data/lib/simplecov/formatter/html_formatter/viewer_data_validator.rb +74 -17
  113. data/lib/simplecov/formatter/html_formatter.rb +24 -41
  114. data/lib/simplecov/formatter/json_formatter/errors_formatter.rb +50 -16
  115. data/lib/simplecov/formatter/json_formatter/production_section_formatter.rb +48 -0
  116. data/lib/simplecov/formatter/json_formatter/result_hash_formatter.rb +43 -29
  117. data/lib/simplecov/formatter/json_formatter/source_file_formatter.rb +14 -10
  118. data/lib/simplecov/formatter/json_formatter.rb +7 -13
  119. data/lib/simplecov/formatter/multi_formatter.rb +5 -11
  120. data/lib/simplecov/formatter/simple_formatter.rb +2 -8
  121. data/lib/simplecov/formatter.rb +14 -10
  122. data/lib/simplecov/group_names.rb +13 -12
  123. data/lib/simplecov/history.rb +115 -0
  124. data/lib/simplecov/last_run.rb +6 -9
  125. data/lib/simplecov/lines_classifier.rb +31 -16
  126. data/lib/simplecov/load_global_config.rb +7 -7
  127. data/lib/simplecov/parallel_adapters/base.rb +13 -41
  128. data/lib/simplecov/parallel_adapters/generic.rb +7 -18
  129. data/lib/simplecov/parallel_adapters/parallel_tests.rb +21 -42
  130. data/lib/simplecov/parallel_adapters.rb +24 -53
  131. data/lib/simplecov/parallel_coordination.rb +39 -49
  132. data/lib/simplecov/parallel_result_merger/worker_payload.rb +33 -0
  133. data/lib/simplecov/parallel_result_merger.rb +67 -126
  134. data/lib/simplecov/process.rb +14 -32
  135. data/lib/simplecov/production/error.rb +11 -0
  136. data/lib/simplecov/production/file_sink.rb +130 -0
  137. data/lib/simplecov/production.rb +249 -0
  138. data/lib/simplecov/profiles/rails.rb +1 -12
  139. data/lib/simplecov/profiles/root_filter.rb +0 -4
  140. data/lib/simplecov/profiles/strict.rb +9 -19
  141. data/lib/simplecov/profiles.rb +11 -33
  142. data/lib/simplecov/report_deferral.rb +11 -15
  143. data/lib/simplecov/report_stamp.rb +6 -9
  144. data/lib/simplecov/result/filter_config.rb +19 -0
  145. data/lib/simplecov/result/missing_source_files_reporter.rb +8 -9
  146. data/lib/simplecov/result/serialization.rb +43 -0
  147. data/lib/simplecov/result/source_file_builder.rb +7 -12
  148. data/lib/simplecov/result.rb +68 -117
  149. data/lib/simplecov/result_adapter.rb +58 -68
  150. data/lib/simplecov/result_merger/contexts.rb +22 -0
  151. data/lib/simplecov/result_merger/legacy_format_adapter.rb +4 -8
  152. data/lib/simplecov/result_merger/resultset_file.rb +20 -27
  153. data/lib/simplecov/result_merger/resultset_run_identity.rb +23 -25
  154. data/lib/simplecov/result_merger/resultset_store.rb +23 -14
  155. data/lib/simplecov/result_merger/unloaded_files.rb +24 -32
  156. data/lib/simplecov/result_merger.rb +59 -58
  157. data/lib/simplecov/result_processing.rb +74 -119
  158. data/lib/simplecov/run_identity.rb +16 -18
  159. data/lib/simplecov/simulate_coverage.rb +35 -50
  160. data/lib/simplecov/source_file/branch.rb +8 -33
  161. data/lib/simplecov/source_file/branch_builder.rb +28 -47
  162. data/lib/simplecov/source_file/builder_context.rb +6 -13
  163. data/lib/simplecov/source_file/line.rb +13 -33
  164. data/lib/simplecov/source_file/line_builder.rb +9 -15
  165. data/lib/simplecov/source_file/method.rb +3 -7
  166. data/lib/simplecov/source_file/method_builder.rb +6 -15
  167. data/lib/simplecov/source_file/ruby_data_parser.rb +59 -66
  168. data/lib/simplecov/source_file/skip_chunks.rb +19 -28
  169. data/lib/simplecov/source_file/source_loader.rb +45 -35
  170. data/lib/simplecov/source_file/statistics.rb +10 -9
  171. data/lib/simplecov/source_file.rb +21 -51
  172. data/lib/simplecov/static_coverage_extractor/condition_folding.rb +63 -114
  173. data/lib/simplecov/static_coverage_extractor/location_conventions.rb +76 -121
  174. data/lib/simplecov/static_coverage_extractor/method_collector.rb +11 -24
  175. data/lib/simplecov/static_coverage_extractor/prism_compat.rb +15 -24
  176. data/lib/simplecov/static_coverage_extractor/value_position.rb +25 -38
  177. data/lib/simplecov/static_coverage_extractor/visitor.rb +47 -64
  178. data/lib/simplecov/static_coverage_extractor.rb +46 -62
  179. data/lib/simplecov/test_tracker/accessors.rb +34 -0
  180. data/lib/simplecov/test_tracker/constant_watch.rb +49 -0
  181. data/lib/simplecov/test_tracker/delta.rb +69 -0
  182. data/lib/simplecov/test_tracker/framework_hooks.rb +130 -0
  183. data/lib/simplecov/test_tracker.rb +162 -0
  184. data/lib/simplecov/unloaded_file_injector.rb +18 -31
  185. data/lib/simplecov/useless_results_remover.rb +7 -6
  186. data/lib/simplecov/version.rb +1 -1
  187. data/lib/simplecov/view_coverage/template_compiler.rb +83 -0
  188. data/lib/simplecov/view_coverage.rb +55 -0
  189. data/lib/simplecov-html.rb +1 -0
  190. data/lib/simplecov.rb +59 -114
  191. data/lib/simplecov_json_formatter.rb +1 -0
  192. data/man/simplecov.1 +349 -0
  193. data/sig/simplecov.rbs +523 -595
  194. metadata +72 -7
  195. data/schemas/coverage-v1.0.schema.json +0 -306
  196. data/schemas/coverage.schema.json +0 -306
@@ -3,42 +3,24 @@
3
3
  require "ripper"
4
4
 
5
5
  module SimpleCov
6
- # Parses `# simplecov:disable` / `# simplecov:enable` directive comments.
6
+ # Parses `# simplecov:disable` / `# simplecov:enable` directive comments, in
7
+ # both the block form (the directive is the entire comment on its own line,
8
+ # opening a region that runs until the matching enable) and the inline form
9
+ # (the directive trails real code, affecting that line alone).
7
10
  #
8
- # Two forms are supported:
9
- #
10
- # Block form (the directive is the entire comment on its own line) opens a
11
- # region that runs until the matching `# simplecov:enable`:
12
- #
13
- # # simplecov:disable line
14
- # ...
15
- # # simplecov:enable line
16
- #
17
- # Inline form (the directive trails real code on the same line) only affects
18
- # that single line and does not need to be re-enabled:
19
- #
20
- # raise "absurd" # simplecov:disable
21
- #
22
- # Categories are `:line`, `:branch`, and `:method`. They may be combined
23
- # with commas. Omitting categories targets all three.
24
- #
25
- # Any text after the directive (and the optional category list) is treated
26
- # as a free-form reason and discarded:
27
- #
28
- # # simplecov:disable line not worth testing this glue
29
- #
30
- # As a consequence, an unrecognised category name silently falls into the
31
- # reason bucket. `# simplecov:disable cyclomatic` is parsed as the bare
32
- # form (disable everything) with reason "cyclomatic" — a deliberate
33
- # over-disable so the typo is visible in the report rather than silently
34
- # disabling nothing.
11
+ # Categories are `:line`, `:branch`, and `:method`, combinable with commas.
12
+ # Omitting categories targets all three. Any text after the directive is a
13
+ # free-form reason and is discarded, so an unrecognised category name
14
+ # silently falls into the reason bucket and disables everything: a
15
+ # deliberate over-disable, so the typo is visible in the report rather than
16
+ # silently disabling nothing.
35
17
  #
36
18
  # Comment extraction goes through `Ripper.lex` so directive markers inside
37
19
  # string literals or heredocs are correctly ignored.
38
20
  class Directive
39
21
  CATEGORIES = %i[line branch method].freeze
40
22
 
41
- CATEGORY_PATTERN = "(?:#{CATEGORIES.join('|')})".freeze
23
+ CATEGORY_PATTERN = "(?:#{CATEGORIES.join("|")})".freeze
42
24
  CATEGORIES_PATTERN = "(?:#{CATEGORY_PATTERN}(?:\\s*,\\s*#{CATEGORY_PATTERN})*)".freeze
43
25
  PATTERN = /
44
26
  \#\s*simplecov\s*:\s*
@@ -50,11 +32,9 @@ module SimpleCov
50
32
 
51
33
  attr_reader :line_number, :mode, :categories
52
34
 
53
- # Walk an array of source lines and return the disabled line ranges per
54
- # category as `{ line: [Range, ...], branch: [...], method: [...] }`.
55
- # An unclosed `disable` block extends to the end of the file.
56
- def self.disabled_ranges(src_lines)
57
- lines = src_lines.to_a
35
+ # The disabled line ranges per category. An unclosed `disable` block
36
+ # extends to the end of the file.
37
+ def self.disabled_ranges(lines)
58
38
  ranges = CATEGORIES.to_h do |category|
59
39
  empty = [] # : Array[Range[Integer]]
60
40
  [category, empty]
@@ -62,14 +42,14 @@ module SimpleCov
62
42
  open_starts = {} # : Hash[Symbol, Integer]
63
43
 
64
44
  directives_in(lines).each { |directive| directive.apply(ranges, open_starts) }
65
- open_starts.each { |category, start| ranges[category] << (start..lines.size) }
45
+ open_starts.each { |category, start| ranges.fetch(category) << (start..lines.size) }
66
46
 
67
47
  ranges
68
48
  end
69
49
 
70
- # Extract every directive in the file, in source order. Comments inside
71
- # string literals or heredocs are skipped because Ripper.lex doesn't tag
72
- # them as :on_comment tokens.
50
+ # Every directive in the file, in source order. Comments inside string
51
+ # literals or heredocs are skipped because Ripper.lex doesn't tag them as
52
+ # :on_comment tokens.
73
53
  def self.directives_in(lines)
74
54
  return [] unless source_might_contain_directive?(lines)
75
55
 
@@ -78,13 +58,13 @@ module SimpleCov
78
58
  end
79
59
  end
80
60
 
81
- # Cheap pre-check so we don't tokenize files that obviously can't contain
82
- # a directive.
61
+ # Cheap pre-check, so files that obviously can't contain a directive are
62
+ # never tokenized.
83
63
  def self.source_might_contain_directive?(lines)
84
64
  lines.any? do |line|
85
65
  line.include?("simplecov")
86
66
  rescue ArgumentError, EncodingError
87
- false # simplecov:disable — defensive guard for invalid byte sequences in source
67
+ false
88
68
  end
89
69
  end
90
70
 
@@ -94,72 +74,76 @@ module SimpleCov
94
74
 
95
75
  new(
96
76
  line_number: line_number,
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,
77
+ # The pattern admits exactly two modes, so asking whether this one is
78
+ # `disable` names the other by elimination. Comparing the captured text
79
+ # also keeps the nilable capture out of `to_sym`.
80
+ mode: match[:mode].eql?("disable") ? :disable : :enable,
100
81
  categories: parse_categories(match[:categories]),
101
82
  inline: inline?(lines, line_number, column + match.begin(0))
102
83
  )
103
84
  rescue ArgumentError, EncodingError
104
- # E.g., comment text contains an invalid byte sequence in UTF-8.
85
+ # The comment text contains an invalid byte sequence, say.
105
86
  nil
106
87
  end
107
88
 
89
+ # The bare form targets every category, and answers the frozen constant
90
+ # itself: a directive's categories are only ever iterated.
108
91
  def self.parse_categories(text)
109
- return CATEGORIES.dup if text.nil?
92
+ return CATEGORIES if text.nil?
110
93
 
111
94
  text.split(/\s*,\s*/).map(&:to_sym)
112
95
  end
113
96
 
114
- # Whether the directive sits after non-whitespace content on its line.
115
97
  # `column` is the byte column of the directive's `#` in the source line,
116
- # adjusted for any prefix that may precede it within the comment token
117
- # (e.g., `# prefix # simplecov:disable line`).
98
+ # adjusted for any prefix that may precede it within the comment token.
118
99
  def self.inline?(lines, line_number, column)
119
- line = lines[line_number - 1].to_s
120
- !line.byteslice(0, column).to_s.strip.empty?
100
+ line = lines.at(line_number - 1).to_s
101
+ # Indentation is all a non-inline directive may have before it, so
102
+ # stripping the prefix from the left decides the question. Doing it from
103
+ # the right instead would raise on a line whose trailing bytes are
104
+ # invalid, which is a property of the code after the directive.
105
+ !line.byteslice(0, column).to_s.lstrip.empty?
121
106
  rescue ArgumentError, EncodingError
122
- false # simplecov:disable — defensive guard for invalid byte sequences
107
+ false
123
108
  end
124
109
 
125
110
  def self.comments_in(lines)
126
111
  source = lines.map { |line| line.end_with?("\n") ? line : "#{line}\n" }.join
127
112
  Ripper.lex(source).filter_map do |(line_number, column), type, text|
128
- [line_number, column, text] if type == :on_comment
113
+ [line_number, column, text] if type.equal?(:on_comment)
129
114
  end
130
115
  rescue ArgumentError, EncodingError
131
- [] # simplecov:disable — Ripper.lex can raise on invalid byte sequences
116
+ []
132
117
  end
133
118
 
134
119
  private_class_method :directives_in, :source_might_contain_directive?,
135
- :parse_comment, :parse_categories, :inline?, :comments_in
120
+ :parse_comment, :parse_categories, :inline?, :comments_in
136
121
 
137
122
  def initialize(line_number:, mode:, categories:, inline:)
138
123
  @line_number = line_number
139
- @mode = mode
140
- @categories = categories
141
- @inline = inline
124
+ @mode = mode
125
+ @categories = categories
126
+ @inline = inline
142
127
  end
143
128
 
144
129
  def disabled?
145
- mode == :disable
130
+ mode.equal?(:disable)
146
131
  end
147
132
 
148
133
  def inline?
149
134
  @inline
150
135
  end
151
136
 
152
- # Apply this directive's effect to the in-flight per-category state.
153
137
  # Inline directives mark just their line; block disables open a region;
154
138
  # block enables close one. Re-opening an already-open block is a no-op.
155
139
  def apply(ranges, open_starts)
156
140
  categories.each do |category|
157
141
  if inline?
158
- ranges[category] << (line_number..line_number) if disabled?
142
+ ranges.fetch(category) << (line_number..line_number) if disabled?
159
143
  elsif disabled?
160
144
  open_starts[category] ||= line_number
161
145
  elsif (start = open_starts.delete(category))
162
- ranges[category] << (start..line_number)
146
+ ranges.fetch(category) << (start..line_number)
163
147
  end
164
148
  end
165
149
  end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleCov
4
+ module ExitCodes
5
+ # Fails when a file listed in the checked-in baseline drops below its own
6
+ # floor on both axes: a lower percent than the floor records and more misses
7
+ # than it allows. The second condition is the dampener that keeps an edit
8
+ # with no coverage change from failing the run; a percent-only floor is
9
+ # decided by the percent alone.
10
+ class BaselineCheck < Check
11
+ def initialize(result, baseline)
12
+ super(result, nil)
13
+ @baseline = baseline
14
+ end
15
+
16
+ def exit_code
17
+ MINIMUM_COVERAGE
18
+ end
19
+
20
+ private
21
+
22
+ def compute_violations
23
+ CoverageViolations.baseline(result, @baseline)
24
+ end
25
+
26
+ def violation_lines(violation)
27
+ message = format(
28
+ "%<criterion>s coverage (%<actual>s) dropped below its baseline floor (%<expected>s%%) in %<filename>s",
29
+ criterion: violation.fetch(:criterion).capitalize,
30
+ actual: Color.colorize_percent(violation.fetch(:actual)),
31
+ expected: violation.fetch(:expected),
32
+ filename: violation.fetch(:project_filename)
33
+ )
34
+ ["#{message}#{missed_clause(violation)}."]
35
+ end
36
+
37
+ def missed_clause(violation)
38
+ allowed = violation.fetch(:allowed_missed)
39
+ return unless allowed
40
+
41
+ units = UNITS.fetch(SimpleCov.coverage_statistics_key(violation.fetch(:criterion)))
42
+ " (#{violation.fetch(:actual_missed)} uncovered #{units}, #{allowed} allowed)"
43
+ end
44
+ end
45
+ end
46
+ end
@@ -2,11 +2,9 @@
2
2
 
3
3
  module SimpleCov
4
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`.
5
+ # Shared skeleton for the threshold checks. Subclasses supply `exit_code`,
6
+ # `compute_violations` (memoized here, since `failing?` and `report_lines`
7
+ # both consult it), and `violation_lines`.
10
8
  class Check
11
9
  def initialize(result, thresholds)
12
10
  @result = result
@@ -17,17 +15,25 @@ module SimpleCov
17
15
  violations.any?
18
16
  end
19
17
 
18
+ def violations
19
+ @violations ||= compute_violations
20
+ end
21
+
22
+ # Concatenated rather than flat-mapped: a check answering a bare string
23
+ # instead of its list of lines is an error this raises, not a spelling the
24
+ # flattening would forgive.
25
+ def report_lines
26
+ lines = [] #: Array[String]
27
+ violations.each_with_object(lines) { |violation, into| into.concat(violation_lines(violation)) }
28
+ end
29
+
20
30
  def report
21
- violations.each { |violation| report_violation(violation) }
31
+ report_lines.each { |line| ExitCodes.print_error(line) }
22
32
  end
23
33
 
24
- private
34
+ private
25
35
 
26
36
  attr_reader :result, :thresholds
27
-
28
- def violations
29
- @violations ||= compute_violations
30
- end
31
37
  end
32
38
  end
33
39
  end
@@ -2,10 +2,8 @@
2
2
 
3
3
  module SimpleCov
4
4
  module ExitCodes
5
- # Runs every coverage check against the result and returns the exit
6
- # code from the first failing one (or SUCCESS if all pass).
7
5
  module ExitCodeHandling
8
- module_function
6
+ extend self
9
7
 
10
8
  def call(result, coverage_limits:)
11
9
  checks = coverage_checks(result, coverage_limits)
@@ -15,21 +13,36 @@ module SimpleCov
15
13
  failing_check.report if SimpleCov.print_errors
16
14
  failing_check.exit_code
17
15
  else
18
- SimpleCov::ExitCodes::SUCCESS
16
+ SUCCESS
19
17
  end
20
18
  end
21
19
 
22
20
  def coverage_checks(result, coverage_limits)
23
21
  [
24
22
  MinimumOverallCoverageCheck.new(result, coverage_limits.minimum_coverage),
25
- MinimumCoverageByFileCheck.new(
26
- result, coverage_limits.minimum_coverage_by_file, coverage_limits.minimum_coverage_by_file_overrides
27
- ),
23
+ minimum_by_file_check(result, coverage_limits),
24
+ BaselineCheck.new(result, coverage_limits.baseline),
28
25
  MinimumCoverageByGroupCheck.new(result, coverage_limits.minimum_coverage_by_group),
29
26
  MaximumOverallCoverageCheck.new(result, coverage_limits.maximum_coverage),
30
- MaximumCoverageDropCheck.new(result, coverage_limits.maximum_coverage_drop)
27
+ MaximumCoverageDropCheck.new(result, coverage_limits.maximum_coverage_drop),
28
+ MaximumMissedCheck.new(result, coverage_limits.maximum_missed),
29
+ maximum_missed_per_file_check(result, coverage_limits)
31
30
  ]
32
31
  end
32
+
33
+ def minimum_by_file_check(result, coverage_limits)
34
+ MinimumCoverageByFileCheck.new(
35
+ result, coverage_limits.minimum_coverage_by_file, coverage_limits.minimum_coverage_by_file_overrides,
36
+ baseline: coverage_limits.baseline
37
+ )
38
+ end
39
+
40
+ def maximum_missed_per_file_check(result, coverage_limits)
41
+ MaximumMissedPerFileCheck.new(
42
+ result, coverage_limits.maximum_missed_per_file, coverage_limits.maximum_missed_per_file_overrides,
43
+ baseline: coverage_limits.baseline
44
+ )
45
+ end
33
46
  end
34
47
  end
35
48
  end
@@ -2,24 +2,21 @@
2
2
 
3
3
  module SimpleCov
4
4
  module ExitCodes
5
- # Fails when any coverage criterion has dropped by more than the
6
- # configured maximum since the last recorded run.
7
5
  class MaximumCoverageDropCheck < Check
8
6
  def exit_code
9
- SimpleCov::ExitCodes::MAXIMUM_COVERAGE_DROP
7
+ MAXIMUM_COVERAGE_DROP
10
8
  end
11
9
 
12
- private
10
+ private
13
11
 
14
12
  def compute_violations
15
- SimpleCov::CoverageViolations.maximum_drop(result, thresholds)
13
+ CoverageViolations.maximum_drop(result, thresholds)
16
14
  end
17
15
 
18
- # The "drop percent" is a delta, not a coverage level it has no
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)
16
+ # The drop percent is a delta, not a coverage level, so it has no natural
17
+ # green/yellow/red mapping and the whole line goes red.
18
+ def violation_lines(violation)
19
+ [Color.colorize(message_for(violation), :red)]
23
20
  end
24
21
 
25
22
  def message_for(violation)
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleCov
4
+ module ExitCodes
5
+ class MaximumMissedCheck < Check
6
+ def exit_code
7
+ MINIMUM_COVERAGE
8
+ end
9
+
10
+ private
11
+
12
+ def compute_violations
13
+ CoverageViolations.maximum_missed(result, thresholds)
14
+ end
15
+
16
+ def violation_lines(violation)
17
+ [format(
18
+ "Missed %<units>s (%<actual>d) exceed the configured maximum_missed (%<maximum>d).",
19
+ units: UNITS.fetch(SimpleCov.coverage_statistics_key(violation.fetch(:criterion))),
20
+ actual: violation.fetch(:actual),
21
+ maximum: violation.fetch(:maximum)
22
+ )]
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleCov
4
+ module ExitCodes
5
+ class MaximumMissedPerFileCheck < Check
6
+ def initialize(result, maximum_missed_per_file, overrides = {}, baseline: nil)
7
+ super(result, maximum_missed_per_file)
8
+ @overrides = overrides
9
+ @baseline = baseline
10
+ end
11
+
12
+ def exit_code
13
+ MINIMUM_COVERAGE
14
+ end
15
+
16
+ private
17
+
18
+ def compute_violations
19
+ CoverageViolations.maximum_missed_by_file(result, thresholds, @overrides, baseline: @baseline)
20
+ end
21
+
22
+ def violation_lines(violation)
23
+ [format(
24
+ "Missed %<units>s (%<actual>d) exceed the configured maximum_missed_per_file (%<maximum>d) " \
25
+ "in %<filename>s.",
26
+ units: UNITS.fetch(SimpleCov.coverage_statistics_key(violation.fetch(:criterion))),
27
+ actual: violation.fetch(:actual),
28
+ maximum: violation.fetch(:maximum),
29
+ filename: violation.fetch(:project_filename)
30
+ )]
31
+ end
32
+ end
33
+ end
34
+ end
@@ -2,30 +2,25 @@
2
2
 
3
3
  module SimpleCov
4
4
  module ExitCodes
5
- # Fails when the overall (project-wide) coverage for any criterion is
6
- # above the configured maximum. Pair with
7
- # `SimpleCov::ExitCodes::MinimumOverallCoverageCheck` (or use
8
- # `SimpleCov.expected_coverage`) to pin coverage to an exact value
9
- # and surface unexpected increases instead of silently absorbing them.
10
5
  class MaximumOverallCoverageCheck < Check
11
6
  def exit_code
12
- SimpleCov::ExitCodes::MAXIMUM_COVERAGE
7
+ MAXIMUM_COVERAGE
13
8
  end
14
9
 
15
- private
10
+ private
16
11
 
17
12
  def compute_violations
18
- SimpleCov::CoverageViolations.maximum_overall(result, thresholds)
13
+ CoverageViolations.maximum_overall(result, thresholds)
19
14
  end
20
15
 
21
- def report_violation(violation)
22
- ExitCodes.print_error format(
16
+ def violation_lines(violation)
17
+ [format(
23
18
  "%<criterion>s coverage (%<actual>s) is above the expected maximum coverage (%<expected>.2f%%). " \
24
19
  "Time to bump the threshold!",
25
20
  criterion: violation.fetch(:criterion).capitalize,
26
- actual: SimpleCov::Color.colorize_percent(violation.fetch(:actual)),
21
+ actual: Color.colorize_percent(violation.fetch(:actual)),
27
22
  expected: violation.fetch(:expected)
28
- )
23
+ )]
29
24
  end
30
25
  end
31
26
  end
@@ -2,33 +2,34 @@
2
2
 
3
3
  module SimpleCov
4
4
  module ExitCodes
5
- # Fails when any individual file falls below the configured minimum
6
- # coverage for any criterion.
7
5
  class MinimumCoverageByFileCheck < Check
8
- def initialize(result, minimum_coverage_by_file, overrides = {})
6
+ # `baseline:` exempts the file-and-criterion pairs it covers: those answer to
7
+ # their own floor via `BaselineCheck` instead.
8
+ def initialize(result, minimum_coverage_by_file, overrides = {}, baseline: nil)
9
9
  super(result, minimum_coverage_by_file)
10
10
  @overrides = overrides
11
+ @baseline = baseline
11
12
  end
12
13
 
13
14
  def exit_code
14
- SimpleCov::ExitCodes::MINIMUM_COVERAGE
15
+ MINIMUM_COVERAGE
15
16
  end
16
17
 
17
- private
18
+ private
18
19
 
19
20
  def compute_violations
20
- SimpleCov::CoverageViolations.minimum_by_file(result, thresholds, @overrides)
21
+ CoverageViolations.minimum_by_file(result, thresholds, @overrides, baseline: @baseline)
21
22
  end
22
23
 
23
- def report_violation(violation)
24
- ExitCodes.print_error format(
24
+ def violation_lines(violation)
25
+ [format(
25
26
  "%<criterion>s coverage by file (%<actual>s) is below the expected minimum coverage " \
26
27
  "(%<expected>.2f%%) in %<filename>s.",
27
28
  criterion: violation.fetch(:criterion).capitalize,
28
- actual: SimpleCov::Color.colorize_percent(violation.fetch(:actual)),
29
+ actual: Color.colorize_percent(violation.fetch(:actual)),
29
30
  expected: violation.fetch(:expected),
30
31
  filename: violation.fetch(:project_filename)
31
- )
32
+ )]
32
33
  end
33
34
  end
34
35
  end
@@ -2,28 +2,26 @@
2
2
 
3
3
  module SimpleCov
4
4
  module ExitCodes
5
- # Fails when any configured group falls below its minimum coverage
6
- # threshold for any criterion.
7
5
  class MinimumCoverageByGroupCheck < Check
8
6
  def exit_code
9
- SimpleCov::ExitCodes::MINIMUM_COVERAGE
7
+ MINIMUM_COVERAGE
10
8
  end
11
9
 
12
- private
10
+ private
13
11
 
14
12
  def compute_violations
15
- SimpleCov::CoverageViolations.minimum_by_group(result, thresholds)
13
+ CoverageViolations.minimum_by_group(result, thresholds)
16
14
  end
17
15
 
18
- def report_violation(violation)
19
- ExitCodes.print_error format(
16
+ def violation_lines(violation)
17
+ [format(
20
18
  "%<criterion>s coverage by group (%<actual>s) is below the expected minimum coverage " \
21
19
  "(%<expected>.2f%%) in %<group_name>s.",
22
20
  criterion: violation.fetch(:criterion).capitalize,
23
- actual: SimpleCov::Color.colorize_percent(violation.fetch(:actual)),
21
+ actual: Color.colorize_percent(violation.fetch(:actual)),
24
22
  expected: violation.fetch(:expected),
25
23
  group_name: violation.fetch(:group_name)
26
- )
24
+ )]
27
25
  end
28
26
  end
29
27
  end
@@ -2,53 +2,52 @@
2
2
 
3
3
  module SimpleCov
4
4
  module ExitCodes
5
- # Fails when the overall (project-wide) coverage for any criterion is
6
- # below the configured minimum.
7
5
  class MinimumOverallCoverageCheck < Check
8
6
  def exit_code
9
- SimpleCov::ExitCodes::MINIMUM_COVERAGE
7
+ MINIMUM_COVERAGE
10
8
  end
11
9
 
12
- private
10
+ private
13
11
 
14
12
  WORST_FILES_LIMIT = 5
15
13
  private_constant :WORST_FILES_LIMIT
16
14
 
17
15
  def compute_violations
18
- SimpleCov::CoverageViolations.minimum_overall(result, thresholds)
16
+ CoverageViolations.minimum_overall(result, thresholds)
19
17
  end
20
18
 
21
- def report_violation(violation)
19
+ def violation_lines(violation)
22
20
  criterion = violation.fetch(:criterion)
23
- actual = violation.fetch(:actual)
24
- ExitCodes.print_error format(
21
+ headline = format(
25
22
  "%<criterion>s coverage (%<actual>s) is below the expected minimum coverage (%<expected>.2f%%).",
26
23
  criterion: criterion.capitalize,
27
- actual: SimpleCov::Color.colorize_percent(actual),
24
+ actual: Color.colorize_percent(violation.fetch(:actual)),
28
25
  expected: violation.fetch(:expected)
29
26
  )
30
- report_worst_files(criterion)
27
+ [headline, *worst_files_lines(criterion)]
31
28
  end
32
29
 
33
- def report_worst_files(criterion)
30
+ def worst_files_lines(criterion)
34
31
  worst = worst_files_for(criterion)
35
- return if worst.empty?
32
+ return [] if worst.empty?
36
33
 
37
- ExitCodes.print_error " Lowest-coverage files (#{criterion}):"
38
- worst.each do |path, percent|
39
- ExitCodes.print_error format(
34
+ [" Lowest-coverage files (#{criterion}):"] + worst.map do |path, percent|
35
+ format(
40
36
  " %<percent>s %<path>s",
41
- percent: SimpleCov::Color.colorize_percent(percent, format("%6.2f%%", percent)),
37
+ percent: Color.colorize_percent(percent, format("%6.2f%%", percent)),
42
38
  path: path
43
39
  )
44
40
  end
45
41
  end
46
42
 
43
+ # The list is a hint at where to add tests, so a fully covered file has no
44
+ # business on it: without the filter, files at 100% padded the list out to
45
+ # its five entries (#1286).
47
46
  def worst_files_for(criterion)
48
47
  stats_key = SimpleCov.coverage_statistics_key(criterion)
49
48
  with_stats = result.files.filter_map do |source_file|
50
49
  stats = source_file.coverage_statistics[stats_key]
51
- [source_file.project_filename, stats.percent] if stats
50
+ [source_file.project_filename, stats.percent] if stats && stats.percent < 100
52
51
  end
53
52
  with_stats.sort_by { |_path, percent| percent }.first(WORST_FILES_LIMIT)
54
53
  end