simplecov 0.22.0 → 1.3.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 (210) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +1 -1
  3. data/README.md +173 -875
  4. data/exe/simplecov +8 -0
  5. data/lib/minitest/simplecov_plugin.rb +18 -7
  6. data/lib/simplecov/atomic_file.rb +67 -0
  7. data/lib/simplecov/autostart.rb +9 -0
  8. data/lib/simplecov/baseline/parser.rb +58 -0
  9. data/lib/simplecov/baseline.rb +170 -0
  10. data/lib/simplecov/cli/affected/changed_files.rb +57 -0
  11. data/lib/simplecov/cli/affected/selection.rb +117 -0
  12. data/lib/simplecov/cli/affected.rb +148 -0
  13. data/lib/simplecov/cli/annotations.rb +166 -0
  14. data/lib/simplecov/cli/badge/svg.rb +70 -0
  15. data/lib/simplecov/cli/badge.rb +62 -0
  16. data/lib/simplecov/cli/clean.rb +86 -0
  17. data/lib/simplecov/cli/command_helpers.rb +91 -0
  18. data/lib/simplecov/cli/completions/scripts.rb +114 -0
  19. data/lib/simplecov/cli/completions.rb +83 -0
  20. data/lib/simplecov/cli/coverage.rb +83 -0
  21. data/lib/simplecov/cli/coverage_file.rb +109 -0
  22. data/lib/simplecov/cli/dead_code/output.rb +100 -0
  23. data/lib/simplecov/cli/dead_code.rb +139 -0
  24. data/lib/simplecov/cli/diff/output.rb +45 -0
  25. data/lib/simplecov/cli/diff.rb +115 -0
  26. data/lib/simplecov/cli/dotfile.rb +156 -0
  27. data/lib/simplecov/cli/git.rb +41 -0
  28. data/lib/simplecov/cli/history/output.rb +135 -0
  29. data/lib/simplecov/cli/history.rb +67 -0
  30. data/lib/simplecov/cli/merge.rb +115 -0
  31. data/lib/simplecov/cli/open.rb +45 -0
  32. data/lib/simplecov/cli/patch/changed_lines.rb +149 -0
  33. data/lib/simplecov/cli/patch/output.rb +151 -0
  34. data/lib/simplecov/cli/patch.rb +175 -0
  35. data/lib/simplecov/cli/ratchet/output.rb +44 -0
  36. data/lib/simplecov/cli/ratchet.rb +83 -0
  37. data/lib/simplecov/cli/report.rb +91 -0
  38. data/lib/simplecov/cli/run.rb +35 -0
  39. data/lib/simplecov/cli/serve/report_preparer.rb +30 -0
  40. data/lib/simplecov/cli/serve/static_file_handler.rb +128 -0
  41. data/lib/simplecov/cli/serve.rb +88 -0
  42. data/lib/simplecov/cli/show/annotator.rb +87 -0
  43. data/lib/simplecov/cli/show/sweep.rb +36 -0
  44. data/lib/simplecov/cli/show.rb +133 -0
  45. data/lib/simplecov/cli/status/facts.rb +87 -0
  46. data/lib/simplecov/cli/status.rb +93 -0
  47. data/lib/simplecov/cli/tests/redundancy.rb +78 -0
  48. data/lib/simplecov/cli/tests.rb +148 -0
  49. data/lib/simplecov/cli/uncovered/misses.rb +33 -0
  50. data/lib/simplecov/cli/uncovered.rb +127 -0
  51. data/lib/simplecov/cli/usage.rb +153 -0
  52. data/lib/simplecov/cli/watch/live_report.rb +73 -0
  53. data/lib/simplecov/cli/watch/narrator.rb +55 -0
  54. data/lib/simplecov/cli/watch/poller.rb +45 -0
  55. data/lib/simplecov/cli/watch/session.rb +134 -0
  56. data/lib/simplecov/cli/watch/test_plan.rb +43 -0
  57. data/lib/simplecov/cli/watch.rb +85 -0
  58. data/lib/simplecov/cli.rb +114 -0
  59. data/lib/simplecov/color.rb +65 -0
  60. data/lib/simplecov/combine/branches_combiner.rb +44 -21
  61. data/lib/simplecov/combine/coverage_accumulator.rb +175 -0
  62. data/lib/simplecov/combine/identity_interner.rb +27 -0
  63. data/lib/simplecov/combine/interned_counts.rb +28 -0
  64. data/lib/simplecov/combine/lines_combiner.rb +35 -29
  65. data/lib/simplecov/combine/methods_combiner.rb +48 -0
  66. data/lib/simplecov/combine/results_combiner.rb +9 -49
  67. data/lib/simplecov/combine.rb +0 -24
  68. data/lib/simplecov/command_guesser.rb +99 -42
  69. data/lib/simplecov/configuration/baseline.rb +31 -0
  70. data/lib/simplecov/configuration/coverage.rb +184 -0
  71. data/lib/simplecov/configuration/coverage_criteria.rb +143 -0
  72. data/lib/simplecov/configuration/deprecations.rb +21 -0
  73. data/lib/simplecov/configuration/eval_coverage.rb +37 -0
  74. data/lib/simplecov/configuration/filters.rb +131 -0
  75. data/lib/simplecov/configuration/formatting.rb +129 -0
  76. data/lib/simplecov/configuration/groups.rb +48 -0
  77. data/lib/simplecov/configuration/history.rb +44 -0
  78. data/lib/simplecov/configuration/ignored_entries.rb +72 -0
  79. data/lib/simplecov/configuration/merging.rb +197 -0
  80. data/lib/simplecov/configuration/missed_caps.rb +51 -0
  81. data/lib/simplecov/configuration/production.rb +23 -0
  82. data/lib/simplecov/configuration/test_tracking.rb +48 -0
  83. data/lib/simplecov/configuration/thresholds.rb +150 -0
  84. data/lib/simplecov/configuration/view_coverage.rb +37 -0
  85. data/lib/simplecov/configuration.rb +100 -443
  86. data/lib/simplecov/configuration_error.rb +5 -0
  87. data/lib/simplecov/context_map/union.rb +81 -0
  88. data/lib/simplecov/context_map.rb +187 -0
  89. data/lib/simplecov/coverage_json.rb +21 -0
  90. data/lib/simplecov/coverage_statistics.rb +24 -28
  91. data/lib/simplecov/coverage_violations.rb +254 -0
  92. data/lib/simplecov/current_run.rb +61 -0
  93. data/lib/simplecov/defaults.rb +20 -28
  94. data/lib/simplecov/deprecation.rb +63 -0
  95. data/lib/simplecov/directive/erb.rb +44 -0
  96. data/lib/simplecov/directive/haml.rb +33 -0
  97. data/lib/simplecov/directive/indented_template.rb +37 -0
  98. data/lib/simplecov/directive/slim.rb +35 -0
  99. data/lib/simplecov/directive/template.rb +29 -0
  100. data/lib/simplecov/directive.rb +152 -0
  101. data/lib/simplecov/exit_codes/baseline_check.rb +46 -0
  102. data/lib/simplecov/exit_codes/check.rb +39 -0
  103. data/lib/simplecov/exit_codes/exit_code_handling.rb +24 -5
  104. data/lib/simplecov/exit_codes/maximum_coverage_drop_check.rb +17 -67
  105. data/lib/simplecov/exit_codes/maximum_missed_check.rb +26 -0
  106. data/lib/simplecov/exit_codes/maximum_missed_per_file_check.rb +34 -0
  107. data/lib/simplecov/exit_codes/maximum_overall_coverage_check.rb +27 -0
  108. data/lib/simplecov/exit_codes/minimum_coverage_by_file_check.rb +20 -38
  109. data/lib/simplecov/exit_codes/minimum_coverage_by_group_check.rb +28 -0
  110. data/lib/simplecov/exit_codes/minimum_overall_coverage_check.rb +37 -34
  111. data/lib/simplecov/exit_codes.rb +26 -0
  112. data/lib/simplecov/exit_handling.rb +163 -0
  113. data/lib/simplecov/file_list.rb +63 -48
  114. data/lib/simplecov/filter.rb +98 -42
  115. data/lib/simplecov/formatter/base.rb +100 -0
  116. data/lib/simplecov/formatter/baseline_formatter.rb +98 -0
  117. data/lib/simplecov/formatter/coverage_json_writer.rb +95 -0
  118. data/lib/simplecov/formatter/html_formatter/public/index.html +93 -0
  119. data/lib/simplecov/formatter/html_formatter/viewer_data_validator.rb +153 -0
  120. data/lib/simplecov/formatter/html_formatter.rb +88 -0
  121. data/lib/simplecov/formatter/json_formatter/errors_formatter.rb +122 -0
  122. data/lib/simplecov/formatter/json_formatter/production_section_formatter.rb +48 -0
  123. data/lib/simplecov/formatter/json_formatter/result_hash_formatter.rb +132 -0
  124. data/lib/simplecov/formatter/json_formatter/source_file_formatter.rb +97 -0
  125. data/lib/simplecov/formatter/json_formatter.rb +40 -0
  126. data/lib/simplecov/formatter/multi_formatter.rb +8 -10
  127. data/lib/simplecov/formatter/simple_formatter.rb +12 -15
  128. data/lib/simplecov/formatter.rb +21 -1
  129. data/lib/simplecov/group_names.rb +33 -0
  130. data/lib/simplecov/history.rb +115 -0
  131. data/lib/simplecov/last_run.rb +16 -5
  132. data/lib/simplecov/lines_classifier.rb +67 -19
  133. data/lib/simplecov/load_global_config.rb +12 -4
  134. data/lib/simplecov/parallel_adapters/base.rb +50 -0
  135. data/lib/simplecov/parallel_adapters/generic.rb +33 -0
  136. data/lib/simplecov/parallel_adapters/parallel_tests.rb +72 -0
  137. data/lib/simplecov/parallel_adapters.rb +54 -0
  138. data/lib/simplecov/parallel_coordination.rb +124 -0
  139. data/lib/simplecov/parallel_result_merger/worker_payload.rb +33 -0
  140. data/lib/simplecov/parallel_result_merger.rb +171 -0
  141. data/lib/simplecov/process.rb +34 -12
  142. data/lib/simplecov/production/error.rb +11 -0
  143. data/lib/simplecov/production/file_sink.rb +130 -0
  144. data/lib/simplecov/production.rb +249 -0
  145. data/lib/simplecov/profiles/bundler_filter.rb +1 -1
  146. data/lib/simplecov/profiles/hidden_filter.rb +1 -1
  147. data/lib/simplecov/profiles/rails.rb +13 -10
  148. data/lib/simplecov/profiles/root_filter.rb +2 -5
  149. data/lib/simplecov/profiles/strict.rb +22 -0
  150. data/lib/simplecov/profiles/test_frameworks.rb +1 -4
  151. data/lib/simplecov/profiles.rb +26 -19
  152. data/lib/simplecov/report_deferral.rb +45 -0
  153. data/lib/simplecov/report_stamp.rb +25 -0
  154. data/lib/simplecov/result/filter_config.rb +19 -0
  155. data/lib/simplecov/result/missing_source_files_reporter.rb +48 -0
  156. data/lib/simplecov/result/serialization.rb +43 -0
  157. data/lib/simplecov/result/source_file_builder.rb +46 -0
  158. data/lib/simplecov/result.rb +123 -47
  159. data/lib/simplecov/result_adapter.rb +130 -11
  160. data/lib/simplecov/result_merger/contexts.rb +22 -0
  161. data/lib/simplecov/result_merger/legacy_format_adapter.rb +24 -0
  162. data/lib/simplecov/result_merger/resultset_file.rb +67 -0
  163. data/lib/simplecov/result_merger/resultset_run_identity.rb +65 -0
  164. data/lib/simplecov/result_merger/resultset_store.rb +62 -0
  165. data/lib/simplecov/result_merger/unloaded_files.rb +95 -0
  166. data/lib/simplecov/result_merger.rb +120 -124
  167. data/lib/simplecov/result_processing.rb +188 -0
  168. data/lib/simplecov/run_identity.rb +75 -0
  169. data/lib/simplecov/simulate_coverage.rb +72 -17
  170. data/lib/simplecov/source_file/branch.rb +8 -35
  171. data/lib/simplecov/source_file/branch_builder.rb +95 -0
  172. data/lib/simplecov/source_file/builder_context.rb +28 -0
  173. data/lib/simplecov/source_file/line.rb +17 -32
  174. data/lib/simplecov/source_file/line_builder.rb +37 -0
  175. data/lib/simplecov/source_file/method.rb +54 -0
  176. data/lib/simplecov/source_file/method_builder.rb +51 -0
  177. data/lib/simplecov/source_file/ruby_data_parser.rb +103 -0
  178. data/lib/simplecov/source_file/skip_chunks.rb +72 -0
  179. data/lib/simplecov/source_file/source_loader.rb +89 -0
  180. data/lib/simplecov/source_file/statistics.rb +76 -0
  181. data/lib/simplecov/source_file.rb +64 -260
  182. data/lib/simplecov/static_coverage_extractor/condition_folding.rb +155 -0
  183. data/lib/simplecov/static_coverage_extractor/location_conventions.rb +191 -0
  184. data/lib/simplecov/static_coverage_extractor/method_collector.rb +45 -0
  185. data/lib/simplecov/static_coverage_extractor/prism_compat.rb +46 -0
  186. data/lib/simplecov/static_coverage_extractor/value_position.rb +61 -0
  187. data/lib/simplecov/static_coverage_extractor/visitor.rb +163 -0
  188. data/lib/simplecov/static_coverage_extractor.rb +78 -0
  189. data/lib/simplecov/test_tracker/accessors.rb +34 -0
  190. data/lib/simplecov/test_tracker/constant_watch.rb +49 -0
  191. data/lib/simplecov/test_tracker/delta.rb +69 -0
  192. data/lib/simplecov/test_tracker/framework_hooks.rb +130 -0
  193. data/lib/simplecov/test_tracker.rb +162 -0
  194. data/lib/simplecov/unloaded_file_injector.rb +62 -0
  195. data/lib/simplecov/useless_results_remover.rb +17 -7
  196. data/lib/simplecov/version.rb +1 -1
  197. data/lib/simplecov/view_coverage/template_compiler.rb +83 -0
  198. data/lib/simplecov/view_coverage.rb +55 -0
  199. data/lib/simplecov-html.rb +5 -0
  200. data/lib/simplecov.rb +143 -394
  201. data/lib/simplecov_json_formatter.rb +5 -0
  202. data/man/simplecov.1 +354 -0
  203. data/sig/simplecov.rbs +1710 -0
  204. metadata +183 -61
  205. data/CHANGELOG.md +0 -191
  206. data/doc/alternate-formatters.md +0 -71
  207. data/doc/commercial-services.md +0 -25
  208. data/doc/editor-integration.md +0 -18
  209. data/lib/simplecov/combine/files_combiner.rb +0 -24
  210. data/lib/simplecov/default_formatter.rb +0 -20
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "configuration_error"
4
+
5
+ module SimpleCov
6
+ # Emits legacy-API deprecation warnings, deduplicated by the source location
7
+ # that triggered them. A deprecated method called in a loop, or a config
8
+ # block re-evaluated once per parallel worker, otherwise repeats the same
9
+ # notice until stderr is unreadable. Keying on the caller location collapses
10
+ # those repeats while still warning separately about each distinct call site
11
+ # the user needs to fix (#1204).
12
+ module Deprecation
13
+ extend self
14
+
15
+ MODES = %i[warn raise].freeze
16
+
17
+ # The mode lives here rather than on the configuration singleton so that
18
+ # `warn` depends on nothing but itself. `SimpleCov.deprecations` is the
19
+ # front door that writes it, and the CLI, which loads none of the
20
+ # configuration surface, can still warn.
21
+ def mode
22
+ @mode ||= :warn
23
+ end
24
+
25
+ def mode=(mode)
26
+ raise ConfigurationError, "deprecations takes :warn or :raise, got #{mode.inspect}" unless MODES.include?(mode)
27
+
28
+ @mode = mode
29
+ end
30
+
31
+ # `message` is the notice without the `[DEPRECATION]` tag or location
32
+ # prefix, both of which are added here. `deprecations :raise` turns every
33
+ # deprecated API into an error before the dedup: an error is not a notice to
34
+ # collapse, and a CI guard must fail on the second offender as surely as on
35
+ # the first. A missing backtrace falls back to keying on the message, so it
36
+ # never silently swallows every notice.
37
+ def warn(message, location: caller_location)
38
+ raise ConfigurationError, message if mode.equal?(:raise)
39
+
40
+ return unless emitted.add?(location || message)
41
+
42
+ Kernel.warn "#{"#{location}: " if location}[DEPRECATION] #{message}"
43
+ end
44
+
45
+ # Every shipped call site is a one-level alias such as `track_files`, so the
46
+ # frame three up from here is the user's own code. `Array(...)` coerces a
47
+ # missing backtrace to `[]` so `.first` yields nil rather than raising.
48
+ def caller_location
49
+ Array(Kernel.caller(3..3)).first
50
+ end
51
+
52
+ # Parallel workers are separate processes with their own set, so each warns
53
+ # at most once.
54
+ def emitted
55
+ @emitted ||= Set.new
56
+ end
57
+
58
+ def reset!
59
+ @emitted = Set.new
60
+ @mode = :warn
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleCov
4
+ class Directive
5
+ # The Ruby an ERB template holds, at the template's own line numbers, so
6
+ # directive comments in a template are found the way they are in a `.rb`
7
+ # file. Lexing the template itself as Ruby is not an option: `%>` opens a
8
+ # percent literal that swallows everything up to the next `>`, so comments
9
+ # after the first tag are never tokenized.
10
+ #
11
+ # Text outside the tags is blanked and the tag delimiters become spaces, so
12
+ # every token keeps its line and column. A comment tag becomes a Ruby
13
+ # comment, which makes `<%# simplecov:disable %>` the template's own form of
14
+ # the directive.
15
+ module Erb
16
+ SEGMENT = /
17
+ (?<escaped><%%)
18
+ | <%(?<kind>\#|==?|-)?(?<body>.*?)(?<close>-?%>)
19
+ | (?<text>[^<]+|<)
20
+ /mx
21
+
22
+ def self.ruby_lines(lines)
23
+ source = lines.map { |line| line.end_with?("\n") ? line : "#{line}\n" }.join
24
+ source.gsub(SEGMENT) { convert(Regexp.last_match) }.lines
25
+ rescue ArgumentError, EncodingError
26
+ lines
27
+ end
28
+
29
+ def self.convert(match)
30
+ body = match[:body]
31
+ return blank(match[0]) if body.nil?
32
+ return " #" + body.gsub("\n", "\n#") + blank(match[:close]) if match[:kind].eql?("#")
33
+
34
+ blank("<%" + match[:kind].to_s) + body + blank(match[:close]).sub(" ", ";")
35
+ end
36
+
37
+ def self.blank(text)
38
+ Template.blank(text)
39
+ end
40
+
41
+ private_class_method :convert, :blank
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "indented_template"
4
+
5
+ module SimpleCov
6
+ class Directive
7
+ # The Ruby a Haml template holds. Script lines (`-`, `=`, and their `!`,
8
+ # `&`, and `~` variants) and the script a tag outputs keep their Ruby with
9
+ # the markers blanked, a `-#` comment becomes a Ruby comment, a `:ruby`
10
+ # filter's lines are Ruby as written, and everything else is blanked.
11
+ module Haml
12
+ include IndentedTemplate
13
+ extend self
14
+
15
+ SCRIPT = /\A[!&]?[-=~]/
16
+ TAG_SCRIPT = /\A[%.#][\w:.#-]*(?:\{[^{}]*\}|\([^()]*\)|\[[^\[\]]*\])*[<>]*[!&]?[=~]/
17
+
18
+ def convert(indent, rest)
19
+ if rest.start_with?("-#")
20
+ ["#{indent} ##{rest[2..]}", COMMENT]
21
+ elsif rest.match?(/\A:ruby[ \t]*\n\z/)
22
+ [Template.blank(indent + rest), RUBY]
23
+ elsif rest.start_with?(":")
24
+ [Template.blank(indent + rest), TEXT]
25
+ elsif (marker = rest[SCRIPT] || rest[TAG_SCRIPT])
26
+ [indent + Template.blank(marker) + rest[marker.length..]]
27
+ else
28
+ [Template.blank(indent + rest)]
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleCov
4
+ class Directive
5
+ # The driver the line-oriented template languages share. Each line is
6
+ # converted on its own unless the line above it opened a block, which the
7
+ # language decides by answering a continuation alongside the converted
8
+ # text: `COMMENT` for a comment marker, whose deeper-indented lines
9
+ # continue the comment, `RUBY` for an embedded Ruby block, whose lines are
10
+ # Ruby as written, and `TEXT` for any other embedded block, whose lines are
11
+ # blanked. A blank line neither ends a block nor belongs to it.
12
+ module IndentedTemplate
13
+ COMMENT = ->(line) { line.start_with?("\n") ? line : "##{line[1..]}" }
14
+ RUBY = ->(line) { line }
15
+ TEXT = ->(line) { Template.blank(line) }
16
+
17
+ def ruby_lines(lines)
18
+ open = nil #: untyped
19
+ lines.map do |line|
20
+ line = "#{line}\n" unless line.end_with?("\n")
21
+ indent = line[/\A[ \t]*/].length
22
+ rest = line[indent..]
23
+ if open && (rest.eql?("\n") || indent > open.last)
24
+ open.first.call(line)
25
+ else
26
+ converted = convert(line[0, indent], rest)
27
+ continuation = converted.at(1)
28
+ open = continuation && [continuation, indent]
29
+ converted.fetch(0)
30
+ end
31
+ end
32
+ rescue ArgumentError, EncodingError
33
+ lines
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "indented_template"
4
+
5
+ module SimpleCov
6
+ class Directive
7
+ # The Ruby a Slim template holds. Control and output lines (`-`, `=`, `==`,
8
+ # and their whitespace variants) and the output a bare tag carries keep
9
+ # their Ruby with the markers blanked, a `/` comment becomes a Ruby
10
+ # comment, a `ruby:` block's lines are Ruby as written, and everything else
11
+ # is blanked. A tag with attributes is blanked whole, since `=` inside them
12
+ # is not the output marker.
13
+ module Slim
14
+ include IndentedTemplate
15
+ extend self
16
+
17
+ CODE = /\A(?:-|={1,2}[<>']*)(?=\s)/
18
+ TAG_CODE = /\A[\w.#-]+\s*={1,2}[<>']*(?=\s)/
19
+
20
+ def convert(indent, rest)
21
+ if rest.start_with?("/")
22
+ ["#{indent}##{rest[1..]}", COMMENT]
23
+ elsif rest.match?(/\Aruby:[ \t]*\n\z/)
24
+ [Template.blank(indent + rest), RUBY]
25
+ elsif rest.match?(/\A\w+:[ \t]*\n\z/)
26
+ [Template.blank(indent + rest), TEXT]
27
+ elsif (marker = rest[CODE] || rest[TAG_CODE])
28
+ [indent + Template.blank(marker) + rest[marker.length..]]
29
+ else
30
+ [Template.blank(indent + rest)]
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "erb"
4
+ require_relative "haml"
5
+ require_relative "slim"
6
+
7
+ module SimpleCov
8
+ class Directive
9
+ # Hands a template's lines to the extractor for its language, so the
10
+ # directive scan sees only the Ruby a template holds, at the template's own
11
+ # line numbers. A file in no template language is Ruby already.
12
+ module Template
13
+ EXTRACTORS = {".erb" => Erb, ".haml" => Haml, ".slim" => Slim}.freeze
14
+
15
+ def self.template?(filename)
16
+ EXTRACTORS.key?(File.extname(filename))
17
+ end
18
+
19
+ def self.ruby_lines(filename, lines)
20
+ extractor = EXTRACTORS[File.extname(filename)]
21
+ extractor ? extractor.ruby_lines(lines) : lines
22
+ end
23
+
24
+ def self.blank(text)
25
+ text.gsub(/[^\n]/, " ")
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,152 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ripper"
4
+ require_relative "directive/template"
5
+
6
+ module SimpleCov
7
+ # Parses `# simplecov:disable` / `# simplecov:enable` directive comments, in
8
+ # both the block form (the directive is the entire comment on its own line,
9
+ # opening a region that runs until the matching enable) and the inline form
10
+ # (the directive trails real code, affecting that line alone).
11
+ #
12
+ # Categories are `:line`, `:branch`, and `:method`, combinable with commas.
13
+ # Omitting categories targets all three. Any text after the directive is a
14
+ # free-form reason and is discarded, so an unrecognised category name
15
+ # silently falls into the reason bucket and disables everything: a
16
+ # deliberate over-disable, so the typo is visible in the report rather than
17
+ # silently disabling nothing.
18
+ #
19
+ # Comment extraction goes through `Ripper.lex` so directive markers inside
20
+ # string literals or heredocs are correctly ignored.
21
+ class Directive
22
+ CATEGORIES = %i[line branch method].freeze
23
+
24
+ CATEGORY_PATTERN = "(?:#{CATEGORIES.join("|")})".freeze
25
+ CATEGORIES_PATTERN = "(?:#{CATEGORY_PATTERN}(?:\\s*,\\s*#{CATEGORY_PATTERN})*)".freeze
26
+ PATTERN = /
27
+ \#\s*simplecov\s*:\s*
28
+ (?<mode>disable|enable)\b
29
+ (?:\s+(?<categories>#{CATEGORIES_PATTERN})\b)?
30
+ .*?
31
+ \s*\z
32
+ /x
33
+
34
+ attr_reader :line_number, :mode, :categories
35
+
36
+ # The disabled line ranges per category. An unclosed `disable` block
37
+ # extends to the end of the file.
38
+ def self.disabled_ranges(lines)
39
+ ranges = CATEGORIES.to_h do |category|
40
+ empty = [] # : Array[Range[Integer]]
41
+ [category, empty]
42
+ end
43
+ open_starts = {} # : Hash[Symbol, Integer]
44
+
45
+ directives_in(lines).each { |directive| directive.apply(ranges, open_starts) }
46
+ open_starts.each { |category, start| ranges.fetch(category) << (start..lines.size) }
47
+
48
+ ranges
49
+ end
50
+
51
+ # Every directive in the file, in source order. Comments inside string
52
+ # literals or heredocs are skipped because Ripper.lex doesn't tag them as
53
+ # :on_comment tokens.
54
+ def self.directives_in(lines)
55
+ return [] unless source_might_contain_directive?(lines)
56
+
57
+ comments_in(lines).filter_map do |line_number, column, text|
58
+ parse_comment(lines, line_number, column, text)
59
+ end
60
+ end
61
+
62
+ # Cheap pre-check, so files that obviously can't contain a directive are
63
+ # never tokenized.
64
+ def self.source_might_contain_directive?(lines)
65
+ lines.any? do |line|
66
+ line.include?("simplecov")
67
+ rescue ArgumentError, EncodingError
68
+ false
69
+ end
70
+ end
71
+
72
+ def self.parse_comment(lines, line_number, column, text)
73
+ match = PATTERN.match(text)
74
+ return nil unless match
75
+
76
+ new(
77
+ line_number: line_number,
78
+ # The pattern admits exactly two modes, so asking whether this one is
79
+ # `disable` names the other by elimination. Comparing the captured text
80
+ # also keeps the nilable capture out of `to_sym`.
81
+ mode: match[:mode].eql?("disable") ? :disable : :enable,
82
+ categories: parse_categories(match[:categories]),
83
+ inline: inline?(lines, line_number, column + match.begin(0))
84
+ )
85
+ rescue ArgumentError, EncodingError
86
+ # The comment text contains an invalid byte sequence, say.
87
+ nil
88
+ end
89
+
90
+ # The bare form targets every category, and answers the frozen constant
91
+ # itself: a directive's categories are only ever iterated.
92
+ def self.parse_categories(text)
93
+ return CATEGORIES if text.nil?
94
+
95
+ text.split(/\s*,\s*/).map(&:to_sym)
96
+ end
97
+
98
+ # `column` is the byte column of the directive's `#` in the source line,
99
+ # adjusted for any prefix that may precede it within the comment token.
100
+ def self.inline?(lines, line_number, column)
101
+ line = lines.at(line_number - 1).to_s
102
+ # Indentation is all a non-inline directive may have before it, so
103
+ # stripping the prefix from the left decides the question. Doing it from
104
+ # the right instead would raise on a line whose trailing bytes are
105
+ # invalid, which is a property of the code after the directive.
106
+ !line.byteslice(0, column).to_s.lstrip.empty?
107
+ rescue ArgumentError, EncodingError
108
+ false
109
+ end
110
+
111
+ def self.comments_in(lines)
112
+ source = lines.map { |line| line.end_with?("\n") ? line : "#{line}\n" }.join
113
+ Ripper.lex(source).filter_map do |(line_number, column), type, text|
114
+ [line_number, column, text] if type.equal?(:on_comment)
115
+ end
116
+ rescue ArgumentError, EncodingError
117
+ []
118
+ end
119
+
120
+ private_class_method :directives_in, :source_might_contain_directive?,
121
+ :parse_comment, :parse_categories, :inline?, :comments_in
122
+
123
+ def initialize(line_number:, mode:, categories:, inline:)
124
+ @line_number = line_number
125
+ @mode = mode
126
+ @categories = categories
127
+ @inline = inline
128
+ end
129
+
130
+ def disabled?
131
+ mode.equal?(:disable)
132
+ end
133
+
134
+ def inline?
135
+ @inline
136
+ end
137
+
138
+ # Inline directives mark just their line; block disables open a region;
139
+ # block enables close one. Re-opening an already-open block is a no-op.
140
+ def apply(ranges, open_starts)
141
+ categories.each do |category|
142
+ if inline?
143
+ ranges.fetch(category) << (line_number..line_number) if disabled?
144
+ elsif disabled?
145
+ open_starts[category] ||= line_number
146
+ elsif (start = open_starts.delete(category))
147
+ ranges.fetch(category) << (start..line_number)
148
+ end
149
+ end
150
+ end
151
+ end
152
+ 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
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleCov
4
+ module ExitCodes
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`.
8
+ class Check
9
+ def initialize(result, thresholds)
10
+ @result = result
11
+ @thresholds = thresholds
12
+ end
13
+
14
+ def failing?
15
+ violations.any?
16
+ end
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
+
30
+ def report
31
+ report_lines.each { |line| ExitCodes.print_error(line) }
32
+ end
33
+
34
+ private
35
+
36
+ attr_reader :result, :thresholds
37
+ end
38
+ end
39
+ end
@@ -3,27 +3,46 @@
3
3
  module SimpleCov
4
4
  module ExitCodes
5
5
  module ExitCodeHandling
6
- module_function
6
+ extend self
7
7
 
8
8
  def call(result, coverage_limits:)
9
9
  checks = coverage_checks(result, coverage_limits)
10
10
 
11
11
  failing_check = checks.find(&:failing?)
12
12
  if failing_check
13
- failing_check.report
13
+ failing_check.report if SimpleCov.print_errors
14
14
  failing_check.exit_code
15
15
  else
16
- SimpleCov::ExitCodes::SUCCESS
16
+ SUCCESS
17
17
  end
18
18
  end
19
19
 
20
20
  def coverage_checks(result, coverage_limits)
21
21
  [
22
22
  MinimumOverallCoverageCheck.new(result, coverage_limits.minimum_coverage),
23
- MinimumCoverageByFileCheck.new(result, coverage_limits.minimum_coverage_by_file),
24
- MaximumCoverageDropCheck.new(result, coverage_limits.maximum_coverage_drop)
23
+ minimum_by_file_check(result, coverage_limits),
24
+ BaselineCheck.new(result, coverage_limits.baseline),
25
+ MinimumCoverageByGroupCheck.new(result, coverage_limits.minimum_coverage_by_group),
26
+ MaximumOverallCoverageCheck.new(result, coverage_limits.maximum_coverage),
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)
25
30
  ]
26
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
27
46
  end
28
47
  end
29
48
  end
@@ -2,81 +2,31 @@
2
2
 
3
3
  module SimpleCov
4
4
  module ExitCodes
5
- class MaximumCoverageDropCheck
6
- def initialize(result, maximum_coverage_drop)
7
- @result = result
8
- @maximum_coverage_drop = maximum_coverage_drop
9
- end
10
-
11
- def failing?
12
- return false unless maximum_coverage_drop && last_run
13
-
14
- coverage_drop_violations.any?
15
- end
16
-
17
- def report
18
- coverage_drop_violations.each do |violation|
19
- $stderr.printf(
20
- "%<criterion>s coverage has dropped by %<drop_percent>.2f%% since the last time (maximum allowed: %<max_drop>.2f%%).\n",
21
- criterion: violation[:criterion].capitalize,
22
- drop_percent: SimpleCov.round_coverage(violation[:drop_percent]),
23
- max_drop: violation[:max_drop]
24
- )
25
- end
26
- end
27
-
5
+ class MaximumCoverageDropCheck < Check
28
6
  def exit_code
29
- SimpleCov::ExitCodes::MAXIMUM_COVERAGE_DROP
30
- end
31
-
32
- private
33
-
34
- attr_reader :result, :maximum_coverage_drop
35
-
36
- def last_run
37
- return @last_run if defined?(@last_run)
38
-
39
- @last_run = SimpleCov::LastRun.read
7
+ MAXIMUM_COVERAGE_DROP
40
8
  end
41
9
 
42
- def coverage_drop_violations
43
- @coverage_drop_violations ||=
44
- compute_coverage_drop_data.select do |achieved|
45
- achieved.fetch(:max_drop) < achieved.fetch(:drop_percent)
46
- end
47
- end
10
+ private
48
11
 
49
- def compute_coverage_drop_data
50
- maximum_coverage_drop.map do |criterion, percent|
51
- {
52
- criterion: criterion,
53
- max_drop: percent,
54
- drop_percent: drop_percent(criterion)
55
- }
56
- end
12
+ def compute_violations
13
+ CoverageViolations.maximum_drop(result, thresholds)
57
14
  end
58
15
 
59
- # if anyone says "max_coverage_drop 0.000000000000000001" I appologize. Please don't.
60
- MAX_DROP_ACCURACY = 10
61
- def drop_percent(criterion)
62
- drop = last_coverage(criterion) -
63
- SimpleCov.round_coverage(
64
- result.coverage_statistics.fetch(criterion).percent
65
- )
66
-
67
- # floats, I tell ya.
68
- # irb(main):001:0* 80.01 - 80.0
69
- # => 0.010000000000005116
70
- drop.floor(MAX_DROP_ACCURACY)
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)]
71
20
  end
72
21
 
73
- def last_coverage(criterion)
74
- last_coverage_percent = last_run[:result][criterion]
75
-
76
- # fallback for old file format
77
- last_coverage_percent = last_run[:result][:covered_percent] if !last_coverage_percent && criterion == :line
78
-
79
- last_coverage_percent || 0
22
+ def message_for(violation)
23
+ format(
24
+ "%<criterion>s coverage has dropped by %<drop_percent>.2f%% since the last time " \
25
+ "(maximum allowed: %<max_drop>.2f%%).",
26
+ criterion: violation.fetch(:criterion).capitalize,
27
+ drop_percent: violation.fetch(:actual),
28
+ max_drop: violation.fetch(:maximum)
29
+ )
80
30
  end
81
31
  end
82
32
  end
@@ -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