simplecov 1.1.1 → 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 (200) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +148 -5
  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/annotations.rb +166 -0
  13. data/lib/simplecov/cli/badge/svg.rb +70 -0
  14. data/lib/simplecov/cli/badge.rb +62 -0
  15. data/lib/simplecov/cli/clean.rb +23 -24
  16. data/lib/simplecov/cli/command_helpers.rb +55 -19
  17. data/lib/simplecov/cli/completions/scripts.rb +114 -0
  18. data/lib/simplecov/cli/completions.rb +83 -0
  19. data/lib/simplecov/cli/coverage.rb +25 -28
  20. data/lib/simplecov/cli/coverage_file.rb +58 -14
  21. data/lib/simplecov/cli/dead_code/output.rb +100 -0
  22. data/lib/simplecov/cli/dead_code.rb +139 -0
  23. data/lib/simplecov/cli/diff/output.rb +45 -0
  24. data/lib/simplecov/cli/diff.rb +35 -67
  25. data/lib/simplecov/cli/dotfile.rb +83 -38
  26. data/lib/simplecov/cli/git.rb +41 -0
  27. data/lib/simplecov/cli/history/output.rb +135 -0
  28. data/lib/simplecov/cli/history.rb +67 -0
  29. data/lib/simplecov/cli/merge.rb +34 -39
  30. data/lib/simplecov/cli/open.rb +12 -15
  31. data/lib/simplecov/cli/patch/changed_lines.rb +149 -0
  32. data/lib/simplecov/cli/patch/output.rb +151 -0
  33. data/lib/simplecov/cli/patch.rb +175 -0
  34. data/lib/simplecov/cli/ratchet/output.rb +44 -0
  35. data/lib/simplecov/cli/ratchet.rb +83 -0
  36. data/lib/simplecov/cli/report.rb +18 -24
  37. data/lib/simplecov/cli/run.rb +8 -9
  38. data/lib/simplecov/cli/serve/report_preparer.rb +7 -6
  39. data/lib/simplecov/cli/serve/static_file_handler.rb +62 -56
  40. data/lib/simplecov/cli/serve.rb +27 -24
  41. data/lib/simplecov/cli/show/annotator.rb +87 -0
  42. data/lib/simplecov/cli/show/sweep.rb +36 -0
  43. data/lib/simplecov/cli/show.rb +133 -0
  44. data/lib/simplecov/cli/status/facts.rb +87 -0
  45. data/lib/simplecov/cli/status.rb +93 -0
  46. data/lib/simplecov/cli/tests/redundancy.rb +78 -0
  47. data/lib/simplecov/cli/tests.rb +148 -0
  48. data/lib/simplecov/cli/uncovered/misses.rb +33 -0
  49. data/lib/simplecov/cli/uncovered.rb +58 -34
  50. data/lib/simplecov/cli/usage.rb +153 -0
  51. data/lib/simplecov/cli/watch/live_report.rb +73 -0
  52. data/lib/simplecov/cli/watch/narrator.rb +55 -0
  53. data/lib/simplecov/cli/watch/poller.rb +45 -0
  54. data/lib/simplecov/cli/watch/session.rb +134 -0
  55. data/lib/simplecov/cli/watch/test_plan.rb +43 -0
  56. data/lib/simplecov/cli/watch.rb +85 -0
  57. data/lib/simplecov/cli.rb +43 -92
  58. data/lib/simplecov/color.rb +19 -37
  59. data/lib/simplecov/combine/branches_combiner.rb +7 -31
  60. data/lib/simplecov/combine/coverage_accumulator.rb +49 -142
  61. data/lib/simplecov/combine/identity_interner.rb +10 -13
  62. data/lib/simplecov/combine/interned_counts.rb +7 -9
  63. data/lib/simplecov/combine/lines_combiner.rb +20 -44
  64. data/lib/simplecov/combine/methods_combiner.rb +9 -35
  65. data/lib/simplecov/combine/results_combiner.rb +4 -21
  66. data/lib/simplecov/combine.rb +0 -6
  67. data/lib/simplecov/command_guesser.rb +20 -34
  68. data/lib/simplecov/configuration/baseline.rb +31 -0
  69. data/lib/simplecov/configuration/coverage.rb +91 -79
  70. data/lib/simplecov/configuration/coverage_criteria.rb +43 -54
  71. data/lib/simplecov/configuration/deprecations.rb +21 -0
  72. data/lib/simplecov/configuration/eval_coverage.rb +7 -11
  73. data/lib/simplecov/configuration/filters.rb +40 -73
  74. data/lib/simplecov/configuration/formatting.rb +74 -83
  75. data/lib/simplecov/configuration/groups.rb +19 -13
  76. data/lib/simplecov/configuration/history.rb +44 -0
  77. data/lib/simplecov/configuration/ignored_entries.rb +31 -22
  78. data/lib/simplecov/configuration/merging.rb +94 -62
  79. data/lib/simplecov/configuration/missed_caps.rb +51 -0
  80. data/lib/simplecov/configuration/production.rb +23 -0
  81. data/lib/simplecov/configuration/test_tracking.rb +48 -0
  82. data/lib/simplecov/configuration/thresholds.rb +33 -62
  83. data/lib/simplecov/configuration/view_coverage.rb +37 -0
  84. data/lib/simplecov/configuration.rb +70 -76
  85. data/lib/simplecov/configuration_error.rb +5 -0
  86. data/lib/simplecov/context_map/union.rb +81 -0
  87. data/lib/simplecov/context_map.rb +187 -0
  88. data/lib/simplecov/coverage_json.rb +2 -5
  89. data/lib/simplecov/coverage_statistics.rb +14 -24
  90. data/lib/simplecov/coverage_violations.rb +160 -65
  91. data/lib/simplecov/current_run.rb +61 -0
  92. data/lib/simplecov/defaults.rb +4 -23
  93. data/lib/simplecov/deprecation.rb +45 -27
  94. data/lib/simplecov/directive/erb.rb +44 -0
  95. data/lib/simplecov/directive/haml.rb +33 -0
  96. data/lib/simplecov/directive/indented_template.rb +37 -0
  97. data/lib/simplecov/directive/slim.rb +35 -0
  98. data/lib/simplecov/directive/template.rb +29 -0
  99. data/lib/simplecov/directive.rb +47 -62
  100. data/lib/simplecov/exit_codes/baseline_check.rb +46 -0
  101. data/lib/simplecov/exit_codes/check.rb +17 -11
  102. data/lib/simplecov/exit_codes/exit_code_handling.rb +21 -8
  103. data/lib/simplecov/exit_codes/maximum_coverage_drop_check.rb +7 -10
  104. data/lib/simplecov/exit_codes/maximum_missed_check.rb +26 -0
  105. data/lib/simplecov/exit_codes/maximum_missed_per_file_check.rb +34 -0
  106. data/lib/simplecov/exit_codes/maximum_overall_coverage_check.rb +7 -12
  107. data/lib/simplecov/exit_codes/minimum_coverage_by_file_check.rb +11 -10
  108. data/lib/simplecov/exit_codes/minimum_coverage_by_group_check.rb +7 -9
  109. data/lib/simplecov/exit_codes/minimum_overall_coverage_check.rb +16 -17
  110. data/lib/simplecov/exit_codes.rb +16 -10
  111. data/lib/simplecov/exit_handling.rb +94 -59
  112. data/lib/simplecov/file_list.rb +20 -51
  113. data/lib/simplecov/filter.rb +43 -66
  114. data/lib/simplecov/formatter/base.rb +37 -55
  115. data/lib/simplecov/formatter/baseline_formatter.rb +98 -0
  116. data/lib/simplecov/formatter/coverage_json_writer.rb +29 -31
  117. data/lib/simplecov/formatter/html_formatter/public/index.html +15 -9
  118. data/lib/simplecov/formatter/html_formatter/viewer_data_validator.rb +74 -17
  119. data/lib/simplecov/formatter/html_formatter.rb +24 -41
  120. data/lib/simplecov/formatter/json_formatter/errors_formatter.rb +50 -16
  121. data/lib/simplecov/formatter/json_formatter/production_section_formatter.rb +48 -0
  122. data/lib/simplecov/formatter/json_formatter/result_hash_formatter.rb +43 -29
  123. data/lib/simplecov/formatter/json_formatter/source_file_formatter.rb +14 -10
  124. data/lib/simplecov/formatter/json_formatter.rb +7 -13
  125. data/lib/simplecov/formatter/multi_formatter.rb +5 -11
  126. data/lib/simplecov/formatter/simple_formatter.rb +2 -8
  127. data/lib/simplecov/formatter.rb +14 -10
  128. data/lib/simplecov/group_names.rb +13 -12
  129. data/lib/simplecov/history.rb +115 -0
  130. data/lib/simplecov/last_run.rb +6 -9
  131. data/lib/simplecov/lines_classifier.rb +31 -16
  132. data/lib/simplecov/load_global_config.rb +7 -7
  133. data/lib/simplecov/parallel_adapters/base.rb +13 -41
  134. data/lib/simplecov/parallel_adapters/generic.rb +7 -19
  135. data/lib/simplecov/parallel_adapters/parallel_tests.rb +21 -42
  136. data/lib/simplecov/parallel_adapters.rb +24 -55
  137. data/lib/simplecov/parallel_coordination.rb +39 -49
  138. data/lib/simplecov/parallel_result_merger/worker_payload.rb +33 -0
  139. data/lib/simplecov/parallel_result_merger.rb +67 -126
  140. data/lib/simplecov/process.rb +14 -32
  141. data/lib/simplecov/production/error.rb +11 -0
  142. data/lib/simplecov/production/file_sink.rb +130 -0
  143. data/lib/simplecov/production.rb +249 -0
  144. data/lib/simplecov/profiles/rails.rb +4 -15
  145. data/lib/simplecov/profiles/root_filter.rb +0 -4
  146. data/lib/simplecov/profiles/strict.rb +11 -21
  147. data/lib/simplecov/profiles.rb +11 -33
  148. data/lib/simplecov/report_deferral.rb +11 -15
  149. data/lib/simplecov/report_stamp.rb +6 -9
  150. data/lib/simplecov/result/filter_config.rb +19 -0
  151. data/lib/simplecov/result/missing_source_files_reporter.rb +8 -9
  152. data/lib/simplecov/result/serialization.rb +43 -0
  153. data/lib/simplecov/result/source_file_builder.rb +7 -12
  154. data/lib/simplecov/result.rb +74 -117
  155. data/lib/simplecov/result_adapter.rb +56 -68
  156. data/lib/simplecov/result_merger/contexts.rb +22 -0
  157. data/lib/simplecov/result_merger/legacy_format_adapter.rb +4 -8
  158. data/lib/simplecov/result_merger/resultset_file.rb +20 -27
  159. data/lib/simplecov/result_merger/resultset_run_identity.rb +23 -25
  160. data/lib/simplecov/result_merger/resultset_store.rb +23 -14
  161. data/lib/simplecov/result_merger/unloaded_files.rb +24 -32
  162. data/lib/simplecov/result_merger.rb +59 -58
  163. data/lib/simplecov/result_processing.rb +92 -122
  164. data/lib/simplecov/run_identity.rb +16 -18
  165. data/lib/simplecov/simulate_coverage.rb +34 -50
  166. data/lib/simplecov/source_file/branch.rb +8 -33
  167. data/lib/simplecov/source_file/branch_builder.rb +28 -47
  168. data/lib/simplecov/source_file/builder_context.rb +13 -13
  169. data/lib/simplecov/source_file/line.rb +13 -33
  170. data/lib/simplecov/source_file/line_builder.rb +9 -15
  171. data/lib/simplecov/source_file/method.rb +3 -7
  172. data/lib/simplecov/source_file/method_builder.rb +6 -15
  173. data/lib/simplecov/source_file/ruby_data_parser.rb +59 -66
  174. data/lib/simplecov/source_file/skip_chunks.rb +29 -29
  175. data/lib/simplecov/source_file/source_loader.rb +45 -35
  176. data/lib/simplecov/source_file/statistics.rb +20 -9
  177. data/lib/simplecov/source_file.rb +21 -51
  178. data/lib/simplecov/static_coverage_extractor/condition_folding.rb +55 -139
  179. data/lib/simplecov/static_coverage_extractor/location_conventions.rb +76 -121
  180. data/lib/simplecov/static_coverage_extractor/method_collector.rb +9 -26
  181. data/lib/simplecov/static_coverage_extractor/prism_compat.rb +15 -24
  182. data/lib/simplecov/static_coverage_extractor/value_position.rb +25 -38
  183. data/lib/simplecov/static_coverage_extractor/visitor.rb +45 -65
  184. data/lib/simplecov/static_coverage_extractor.rb +43 -75
  185. data/lib/simplecov/test_tracker/accessors.rb +34 -0
  186. data/lib/simplecov/test_tracker/constant_watch.rb +49 -0
  187. data/lib/simplecov/test_tracker/delta.rb +69 -0
  188. data/lib/simplecov/test_tracker/framework_hooks.rb +130 -0
  189. data/lib/simplecov/test_tracker.rb +162 -0
  190. data/lib/simplecov/unloaded_file_injector.rb +18 -31
  191. data/lib/simplecov/useless_results_remover.rb +7 -6
  192. data/lib/simplecov/version.rb +1 -1
  193. data/lib/simplecov/view_coverage/template_compiler.rb +83 -0
  194. data/lib/simplecov/view_coverage.rb +55 -0
  195. data/lib/simplecov-html.rb +1 -0
  196. data/lib/simplecov.rb +57 -113
  197. data/lib/simplecov_json_formatter.rb +1 -0
  198. data/man/simplecov.1 +354 -0
  199. data/sig/simplecov.rbs +528 -595
  200. metadata +79 -6
@@ -4,46 +4,27 @@ require "English"
4
4
  require "pathname"
5
5
  require_relative "formatter/html_formatter"
6
6
 
7
- # Default configuration. Profiles autoload on first reference via
8
- # `SimpleCov.profiles.fetch_proc`; the unused ones (e.g. "rails")
9
- # never get required unless a user opts in.
10
7
  SimpleCov.configure do
11
8
  formatter SimpleCov::Formatter::HTMLFormatter
12
9
 
13
10
  load_profile "bundler_filter"
14
11
  load_profile "hidden_filter"
15
- # Exclude files outside of SimpleCov.root. Mirrors the early prune done
16
- # by SimpleCov::UselessResultsRemover so the user-facing filter chain
17
- # honors the same boundary; both share the regex.
18
12
  load_profile "root_filter"
19
- # Exclude test framework directories (`test/`, `spec/`, `features/`,
20
- # `autotest/`). The test suite runs 100% of the test files themselves,
21
- # which inflates totals and obscures the application coverage that
22
- # actually matters. Drop with `remove_filter %r{\A(test|features|spec|autotest)/}`
23
- # if you want test files counted (e.g. to surface dead helpers).
24
13
  load_profile "test_frameworks"
25
14
  end
26
15
 
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.
30
- SimpleCov::CommandGuesser.original_run_command = "#{$PROGRAM_NAME} #{ARGV.join(' ')}"
16
+ # Stashed as early as possible, before rake or test/unit tamper with ARGV
17
+ # (#110). The program name is kept separately as well: the join is lossy for
18
+ # a path containing a space, and the guesser reads the executable from it.
19
+ SimpleCov::CommandGuesser.original_run_command = "#{$PROGRAM_NAME} #{ARGV.join(" ")}"
31
20
  SimpleCov::CommandGuesser.original_program_name = $PROGRAM_NAME
32
21
 
33
- # Autoload config from ~/.simplecov if present
34
22
  require_relative "load_global_config"
35
23
 
36
- # Autoload config from .simplecov if present
37
- # Recurse upwards until we find .simplecov or reach the root directory
38
-
39
24
  config_path = Pathname.new(SimpleCov.root)
40
25
  loop do
41
26
  filename = config_path.join(".simplecov")
42
27
  if filename.exist?
43
- # `.simplecov` is a configuration file; SimpleCov.start calls inside
44
- # it draw a deprecation warning, and tracking still begins for
45
- # backward compatibility. See `SimpleCov.with_dot_simplecov_autoload`
46
- # and issue #581.
47
28
  SimpleCov.with_dot_simplecov_autoload do
48
29
  load filename.to_s
49
30
  rescue LoadError, StandardError => e
@@ -1,45 +1,63 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "configuration_error"
4
+
3
5
  module SimpleCov
4
- # Emits legacy-API deprecation warnings, deduplicated by the source
5
- # location that triggered them. A deprecated method called in a loop
6
- # or a config block re-evaluated once per parallel worker / spec file
7
- # otherwise repeats the same notice until stderr is unreadable. Keying
8
- # on the caller location collapses those repeats to a single line while
9
- # still warning separately about each distinct call site the user needs
10
- # to fix. See issue #1204.
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).
11
12
  module Deprecation
12
- module_function
13
-
14
- # Warn about a deprecated API. `message` is the notice without the
15
- # `[DEPRECATION]` tag or location prefix (both are added here).
16
- #
17
- # `location` defaults to the caller of the deprecated method that
18
- # called us every shipped call site is a one-level alias such as
19
- # `track_files`, so the frame two up is the user code. Pass `location:`
20
- # explicitly when the relevant site isn't that frame (e.g. a source
21
- # file and line discovered while parsing).
22
- # `Array(...)` coerces a missing backtrace (nil) to `[]` so `.first`
23
- # yields nil rather than raising — and, unlike `&.`, adds no branch for
24
- # the unreachable no-caller case to the project's 100% coverage target.
25
- def warn(message, location: Array(Kernel.caller(2..2)).first)
26
- # Key on location when we have one (collapses a deprecated call in a
27
- # loop to a single warning); fall back to the message so a missing
28
- # backtrace never silently swallows every notice.
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
+
29
40
  return unless emitted.add?(location || message)
30
41
 
31
42
  Kernel.warn "#{"#{location}: " if location}[DEPRECATION] #{message}"
32
43
  end
33
44
 
34
- # Already-emitted dedup keys for this process. Parallel workers are
35
- # separate processes with their own set, so each warns at most once.
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.
36
54
  def emitted
37
55
  @emitted ||= Set.new
38
56
  end
39
57
 
40
- # @api private — reset emitted state between tests.
41
58
  def reset!
42
59
  @emitted = Set.new
60
+ @mode = :warn
43
61
  end
44
62
  end
45
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
@@ -1,44 +1,27 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "ripper"
4
+ require_relative "directive/template"
4
5
 
5
6
  module SimpleCov
6
- # Parses `# simplecov:disable` / `# simplecov:enable` directive comments.
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).
7
11
  #
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.
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.
35
18
  #
36
19
  # Comment extraction goes through `Ripper.lex` so directive markers inside
37
20
  # string literals or heredocs are correctly ignored.
38
21
  class Directive
39
22
  CATEGORIES = %i[line branch method].freeze
40
23
 
41
- CATEGORY_PATTERN = "(?:#{CATEGORIES.join('|')})".freeze
24
+ CATEGORY_PATTERN = "(?:#{CATEGORIES.join("|")})".freeze
42
25
  CATEGORIES_PATTERN = "(?:#{CATEGORY_PATTERN}(?:\\s*,\\s*#{CATEGORY_PATTERN})*)".freeze
43
26
  PATTERN = /
44
27
  \#\s*simplecov\s*:\s*
@@ -50,11 +33,9 @@ module SimpleCov
50
33
 
51
34
  attr_reader :line_number, :mode, :categories
52
35
 
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
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)
58
39
  ranges = CATEGORIES.to_h do |category|
59
40
  empty = [] # : Array[Range[Integer]]
60
41
  [category, empty]
@@ -62,14 +43,14 @@ module SimpleCov
62
43
  open_starts = {} # : Hash[Symbol, Integer]
63
44
 
64
45
  directives_in(lines).each { |directive| directive.apply(ranges, open_starts) }
65
- open_starts.each { |category, start| ranges[category] << (start..lines.size) }
46
+ open_starts.each { |category, start| ranges.fetch(category) << (start..lines.size) }
66
47
 
67
48
  ranges
68
49
  end
69
50
 
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.
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.
73
54
  def self.directives_in(lines)
74
55
  return [] unless source_might_contain_directive?(lines)
75
56
 
@@ -78,13 +59,13 @@ module SimpleCov
78
59
  end
79
60
  end
80
61
 
81
- # Cheap pre-check so we don't tokenize files that obviously can't contain
82
- # a directive.
62
+ # Cheap pre-check, so files that obviously can't contain a directive are
63
+ # never tokenized.
83
64
  def self.source_might_contain_directive?(lines)
84
65
  lines.any? do |line|
85
66
  line.include?("simplecov")
86
67
  rescue ArgumentError, EncodingError
87
- false # simplecov:disable — defensive guard for invalid byte sequences in source
68
+ false
88
69
  end
89
70
  end
90
71
 
@@ -94,72 +75,76 @@ module SimpleCov
94
75
 
95
76
  new(
96
77
  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,
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,
100
82
  categories: parse_categories(match[:categories]),
101
83
  inline: inline?(lines, line_number, column + match.begin(0))
102
84
  )
103
85
  rescue ArgumentError, EncodingError
104
- # E.g., comment text contains an invalid byte sequence in UTF-8.
86
+ # The comment text contains an invalid byte sequence, say.
105
87
  nil
106
88
  end
107
89
 
90
+ # The bare form targets every category, and answers the frozen constant
91
+ # itself: a directive's categories are only ever iterated.
108
92
  def self.parse_categories(text)
109
- return CATEGORIES.dup if text.nil?
93
+ return CATEGORIES if text.nil?
110
94
 
111
95
  text.split(/\s*,\s*/).map(&:to_sym)
112
96
  end
113
97
 
114
- # Whether the directive sits after non-whitespace content on its line.
115
98
  # `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`).
99
+ # adjusted for any prefix that may precede it within the comment token.
118
100
  def self.inline?(lines, line_number, column)
119
- line = lines[line_number - 1].to_s
120
- !line.byteslice(0, column).to_s.strip.empty?
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?
121
107
  rescue ArgumentError, EncodingError
122
- false # simplecov:disable — defensive guard for invalid byte sequences
108
+ false
123
109
  end
124
110
 
125
111
  def self.comments_in(lines)
126
112
  source = lines.map { |line| line.end_with?("\n") ? line : "#{line}\n" }.join
127
113
  Ripper.lex(source).filter_map do |(line_number, column), type, text|
128
- [line_number, column, text] if type == :on_comment
114
+ [line_number, column, text] if type.equal?(:on_comment)
129
115
  end
130
116
  rescue ArgumentError, EncodingError
131
- [] # simplecov:disable — Ripper.lex can raise on invalid byte sequences
117
+ []
132
118
  end
133
119
 
134
120
  private_class_method :directives_in, :source_might_contain_directive?,
135
- :parse_comment, :parse_categories, :inline?, :comments_in
121
+ :parse_comment, :parse_categories, :inline?, :comments_in
136
122
 
137
123
  def initialize(line_number:, mode:, categories:, inline:)
138
124
  @line_number = line_number
139
- @mode = mode
140
- @categories = categories
141
- @inline = inline
125
+ @mode = mode
126
+ @categories = categories
127
+ @inline = inline
142
128
  end
143
129
 
144
130
  def disabled?
145
- mode == :disable
131
+ mode.equal?(:disable)
146
132
  end
147
133
 
148
134
  def inline?
149
135
  @inline
150
136
  end
151
137
 
152
- # Apply this directive's effect to the in-flight per-category state.
153
138
  # Inline directives mark just their line; block disables open a region;
154
139
  # block enables close one. Re-opening an already-open block is a no-op.
155
140
  def apply(ranges, open_starts)
156
141
  categories.each do |category|
157
142
  if inline?
158
- ranges[category] << (line_number..line_number) if disabled?
143
+ ranges.fetch(category) << (line_number..line_number) if disabled?
159
144
  elsif disabled?
160
145
  open_starts[category] ||= line_number
161
146
  elsif (start = open_starts.delete(category))
162
- ranges[category] << (start..line_number)
147
+ ranges.fetch(category) << (start..line_number)
163
148
  end
164
149
  end
165
150
  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