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,19 +4,21 @@ require "optparse"
4
4
 
5
5
  module SimpleCov
6
6
  module CLI
7
- # Plumbing the subcommands repeat, which their modules `extend`: the
8
- # standard option trio the read-only commands share, the parse
9
- # scaffold around it, the one-line error helper (prefixed with the
10
- # command's own name), and the human-readable stats row `coverage`
11
- # and `report` both print.
12
7
  module CommandHelpers
13
8
  STATS_ROW_FORMAT = " %<label>-7s %<pct>s (%<covered>d / %<total>d)"
14
9
 
15
- # "simplecov merge: ..." derived from the extending module's
16
- # name. The self cast collapses the mixin's view of `self` to the
17
- # Module that extends it, which is what carries `name`.
10
+ # Raised by the shared --help switch; `CLI.dispatch` turns it into the
11
+ # command's own slice of the usage text. The explicit handler also overrides
12
+ # optparse's built-in --help, which would print a bare option summary and
13
+ # exit the process outright from inside the parser.
14
+ class HelpRequested < StandardError; end
15
+
18
16
  def command_name
19
- (_ = self).name.split("::").last.downcase
17
+ name.split("::").last.downcase
18
+ end
19
+
20
+ def one?(count)
21
+ (count - 1).zero?
20
22
  end
21
23
 
22
24
  def error(stderr, message)
@@ -24,23 +26,57 @@ module SimpleCov
24
26
  1
25
27
  end
26
28
 
27
- # The --input / --json / --no-color trio every read-only
28
- # subcommand accepts.
29
+ # `error` returns 1 for an exit status; query helpers need nil so their
30
+ # callers can tell "reported" from a real answer.
31
+ def error_nil(stderr, message)
32
+ error(stderr, message)
33
+ nil
34
+ end
35
+
36
+ def recorded_contexts(document, opts, stderr)
37
+ contexts = document["contexts"]
38
+ if contexts.nil?
39
+ return error_nil(stderr, "no test contexts recorded in #{opts.fetch(:input)}. Enable `track_tests` in " \
40
+ "your `SimpleCov.start` block and rerun the suite to record them")
41
+ end
42
+ return contexts if contexts.instance_of?(Array) && contexts.all?(String)
43
+
44
+ CoverageFile.report_invalid(stderr, command_name, opts.fetch(:input), '"contexts" must be an array of strings')
45
+ end
46
+
29
47
  def common_options(parser, opts)
30
48
  parser.on("--input PATH") { |v| opts[:input] = v }
31
- parser.on("--json") { opts[:json] = true }
32
- parser.on("--no-color") { opts[:no_color] = true }
49
+ parser.on("--json") { opts[:json] = true }
50
+ parser.on("--no-color") { opts[:no_color] = true }
51
+ end
52
+
53
+ def build_parser
54
+ OptionParser.new do |parser|
55
+ yield parser if block_given?
56
+ on_help(parser)
57
+ end
58
+ end
59
+
60
+ # mutant:disable
61
+ # OptionParser answers `-q` as an unambiguous abbreviation of `--quiet`
62
+ # whether or not the short form is declared.
63
+ def quiet_option(parser, opts)
64
+ parser.on("-q", "--quiet") { opts[:quiet] = true }
65
+ end
66
+
67
+ # mutant:disable — optparse matches an abbreviated `-h` against
68
+ # `--help` whether or not the short form is declared, so dropping
69
+ # it changes nothing an example could see. Declared anyway, so the
70
+ # flag appears in the usage text.
71
+ def on_help(parser)
72
+ parser.on("--help", "-h") { raise HelpRequested }
33
73
  end
34
74
 
35
- # The parse scaffold every read-only subcommand repeats: seed the
36
- # shared defaults (plus the command's own), wire up the common
37
- # option trio, yield the parser for command-specific flags, and
38
- # return the parsed opts alongside the positional arguments.
39
75
  def parse_common(args, defaults = {})
40
- opts = {input: SimpleCov::CLI.default_input, json: false, no_color: false} #: Hash[Symbol, untyped]
76
+ opts = {input: CLI.default_input, json: false, no_color: false} #: Hash[Symbol, untyped]
41
77
  opts.merge!(defaults)
42
78
  rest =
43
- OptionParser.new do |parser|
79
+ build_parser do |parser|
44
80
  common_options(parser, opts)
45
81
  yield parser, opts if block_given?
46
82
  end.parse(args)
@@ -0,0 +1,114 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleCov
4
+ module CLI
5
+ module Completions
6
+ # The per-shell renderers. Each receives the same parsed usage data and
7
+ # returns the complete script text. The completions command's own shell-name
8
+ # argument is the one thing the usage tables don't carry, so each renderer
9
+ # wires it in by hand.
10
+ module Scripts
11
+ extend self
12
+
13
+ def fish(commands, options)
14
+ lines = ["# Completions for simplecov. Generated by `simplecov completions fish`."] #: Array[String]
15
+ commands.each do |name, desc|
16
+ lines << "complete -c simplecov -f -n __fish_use_subcommand -a #{name} -d #{fish_quote(desc)}"
17
+ end
18
+ options.each { |name, list| list.each { |option| lines << fish_option(name, option) } }
19
+ lines << "complete -c simplecov -f -n '__fish_seen_subcommand_from completions' -a 'fish bash zsh'"
20
+ lines.join("\n")
21
+ end
22
+
23
+ def fish_option(name, option)
24
+ parts = ["complete -c simplecov -n '__fish_seen_subcommand_from #{name}'"] #: Array[String]
25
+ parts << "-s #{(_ = option.fetch(:short)).delete_prefix("-")}" if option.fetch(:short)
26
+ parts << "-l #{option.fetch(:long).delete_prefix("--")}"
27
+ parts << "-r" if option.fetch(:arg)
28
+ parts << "-d #{fish_quote(option.fetch(:desc))}"
29
+ parts.join(" ")
30
+ end
31
+
32
+ def fish_quote(text)
33
+ "'#{text.gsub("\\") { "\\\\" }.gsub("'") { "\\'" }}'"
34
+ end
35
+
36
+ BASH_TAIL = [
37
+ " esac",
38
+ ' [[ "$cur" == -* ]] && COMPREPLY=( $(compgen -W "$opts" -- "$cur") )',
39
+ "}",
40
+ "complete -o default -F _simplecov simplecov"
41
+ ].freeze
42
+
43
+ def bash(commands, options)
44
+ (bash_head(commands) + bash_cases(options) + BASH_TAIL).join("\n")
45
+ end
46
+
47
+ def bash_head(commands)
48
+ [
49
+ "# Completions for simplecov. Generated by `simplecov completions bash`.",
50
+ "_simplecov() {",
51
+ ' local cur="${COMP_WORDS[COMP_CWORD]}" opts=""',
52
+ ' if [ "$COMP_CWORD" -eq 1 ]; then',
53
+ " COMPREPLY=( $(compgen -W \"#{commands.collect(&:first).join(" ")}\" -- \"$cur\") ); return",
54
+ " fi",
55
+ ' case "${COMP_WORDS[1]}" in'
56
+ ]
57
+ end
58
+
59
+ def bash_cases(options)
60
+ shells = ' completions) COMPREPLY=( $(compgen -W "fish bash zsh -h --help" -- "$cur") ); return;;'
61
+ cases = [shells] #: Array[String]
62
+ options.each do |name, list|
63
+ next if name.eql?("completions")
64
+
65
+ words = list.flat_map { |option| [option.fetch(:short), option.fetch(:long)] }.compact.join(" ")
66
+ cases << " #{name}) opts=\"#{words}\";;"
67
+ end
68
+ cases
69
+ end
70
+
71
+ ZSH_MIDDLE = [
72
+ " )",
73
+ " if (( CURRENT == 2 )); then",
74
+ " _describe -t commands 'simplecov command' commands",
75
+ " return",
76
+ " fi",
77
+ ' case "$words[2]" in'
78
+ ].freeze
79
+
80
+ ZSH_TAIL = [" esac", "}", '_simplecov "$@"'].freeze
81
+
82
+ def zsh(commands, options)
83
+ head = ["#compdef simplecov", "# Completions for simplecov. Generated by `simplecov completions zsh`.",
84
+ "_simplecov() {", " local -a commands", " commands=("]
85
+ rows = commands.collect { |name, desc| " '#{name}:#{zsh_quote(desc)}'" }
86
+ (head + rows + ZSH_MIDDLE + zsh_cases(options) + ZSH_TAIL).join("\n")
87
+ end
88
+
89
+ def zsh_cases(options)
90
+ options.collect do |name, list|
91
+ specs = +""
92
+ specs << "'1:shell:(fish bash zsh)' " if name.eql?("completions")
93
+ list.each { |option| zsh_specs(option).each { |spec| specs << spec << " " } }
94
+ " #{name}) _arguments #{specs.chop};;"
95
+ end
96
+ end
97
+
98
+ # `'--input=[desc]'`: the `=` marks an argument-taking switch, which optparse
99
+ # accepts in that spelling too. Brackets would end the description early, so
100
+ # they are dropped from it.
101
+ def zsh_specs(option)
102
+ desc = "[#{zsh_quote(option.fetch(:desc).delete("[]"))}]"
103
+ specs = [] #: Array[String]
104
+ specs << "'#{option.fetch(:short)}#{desc}'" if option.fetch(:short)
105
+ specs << "'#{option.fetch(:long)}#{"=" if option.fetch(:arg)}#{desc}'"
106
+ end
107
+
108
+ def zsh_quote(text)
109
+ text.gsub("'") { "'\\''" }
110
+ end
111
+ end
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "optparse"
4
+ require_relative "command_helpers"
5
+ require_relative "usage"
6
+ require_relative "completions/scripts"
7
+
8
+ module SimpleCov
9
+ module CLI
10
+ # `simplecov completions <shell>`: completion scripts for fish, bash, and
11
+ # zsh, generated from the usage document rather than a hand-kept table, so a
12
+ # new command or switch shows up in completions the moment it is documented.
13
+ module Completions
14
+ extend CommandHelpers
15
+
16
+ extend self
17
+
18
+ SHELLS = %w[fish bash zsh].freeze
19
+
20
+ # " -q, --quiet Suppress...": optional short, long, optional
21
+ # argument placeholder, then the description after the column gap. A single
22
+ # space never separates the switch from its description, so one non-space
23
+ # token before a two-space run can only be an argument.
24
+ OPTION_ROW = /\A\s*(?:(-\w), )?(--[\w-]+)(?: (\S+))?\s{2,}(\S.*)\z/
25
+ COMMAND_ROW = /\A(\S+)(?: \S+)*?\s{2,}(\S.*)\z/
26
+
27
+ def run(args, stdout:, stderr:)
28
+ shell, = build_parser.parse(args)
29
+ return error(stderr, "missing shell (expected fish, bash, or zsh)") unless shell
30
+ unless SHELLS.include?(shell)
31
+ return error(stderr, "unknown shell #{shell.inspect} (expected fish, bash, or zsh)")
32
+ end
33
+
34
+ stdout.puts(script_for(shell))
35
+ 0
36
+ end
37
+
38
+ def script_for(shell)
39
+ case shell
40
+ when "fish" then Scripts.fish(commands, options_by_command)
41
+ when "bash" then Scripts.bash(commands, options_by_command)
42
+ else Scripts.zsh(commands, options_by_command)
43
+ end
44
+ end
45
+
46
+ def commands
47
+ Usage.text(CLI).split("\n\n").fetch(1).lines.filter_map do |row|
48
+ match = row.strip.match(COMMAND_ROW)
49
+ match && [(_ = match[1]), (_ = match[2])]
50
+ end
51
+ end
52
+
53
+ def options_by_command
54
+ commands.filter_map do |name, _desc|
55
+ list = options_for(name)
56
+ [name, list] unless list.empty?
57
+ end.to_h
58
+ end
59
+
60
+ # `run` takes no options of its own, since everything after it belongs to the
61
+ # command being run, so it must not even offer --help.
62
+ def options_for(command)
63
+ return [] if command.eql?("run")
64
+
65
+ sections = Usage.text(CLI).split("\n\n").select { |section| Usage.section_for?(section, command) }
66
+ options = sections.flat_map { |section| section_options(section) }
67
+ options << {short: "-h", long: "--help", arg: nil, desc: "Show this command's usage"}
68
+ end
69
+
70
+ # Rows that don't parse as options are continuations of the previous row's
71
+ # description, wrapped for the terminal.
72
+ def section_options(section)
73
+ section.lines.each_with_object([]) do |line, options|
74
+ if (match = line.chomp.match(OPTION_ROW))
75
+ options << {short: match[1], long: match[2], arg: match[3], desc: (_ = match[4])}
76
+ elsif (previous = options.last)
77
+ previous[:desc] = "#{previous.fetch(:desc)} #{line.strip}"
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
@@ -6,12 +6,10 @@ require_relative "command_helpers"
6
6
 
7
7
  module SimpleCov
8
8
  module CLI
9
- # `simplecov coverage <path>` — print per-criterion stats for one
10
- # file from a JSONFormatter coverage.json.
11
9
  module Coverage
12
10
  extend CommandHelpers
13
11
 
14
- module_function
12
+ extend self
15
13
 
16
14
  def run(args, stdout:, stderr:)
17
15
  opts = parse(args, stderr: stderr)
@@ -26,44 +24,43 @@ module SimpleCov
26
24
 
27
25
  def parse(args, stderr:)
28
26
  opts, rest = parse_common(args)
29
- return stderr.puts("simplecov coverage: missing file argument") && nil if rest.empty?
27
+ if rest.empty?
28
+ stderr.puts("simplecov coverage: missing file argument")
29
+ return nil
30
+ end
30
31
 
31
32
  opts[:path] = rest.first
32
33
  opts
33
34
  end
34
35
 
35
36
  def locate_match(opts, stderr)
36
- coverage = CoverageFile.load_coverage(opts[:input], command: "coverage", stderr: stderr)
37
+ coverage = CoverageFile.load_coverage(opts.fetch(:input), command: "coverage", stderr: stderr)
37
38
  return unless coverage
38
39
 
39
- match = lookup(coverage, opts[:path])
40
- if match.nil?
41
- stderr.puts("simplecov coverage: no entry for #{opts[:path]} in #{opts[:input]}")
42
- return nil
43
- end
44
- # A wrong-typed entry used to escape here and crash print_human.
45
- return match if match.last.is_a?(Hash)
40
+ match = lookup(coverage, opts.fetch(:path))
41
+ return report_not_found(stderr, coverage, opts) if match.nil?
42
+ return match if match.last.instance_of?(Hash)
43
+
44
+ CoverageFile.report_invalid(stderr, "coverage", opts.fetch(:input),
45
+ "entry for #{opts.fetch(:path)} must be an object")
46
+ end
46
47
 
47
- CoverageFile.report_invalid(stderr, "coverage", opts[:input], "entry for #{opts[:path]} must be an object")
48
+ def report_not_found(stderr, coverage, opts)
49
+ message = CoverageFile.not_found_message(coverage, opts.fetch(:path), opts.fetch(:input))
50
+ stderr.puts("simplecov coverage: #{message}")
48
51
  end
49
52
 
50
- # Match either the absolute path, the literal string passed, or
51
- # any coverage entry whose absolute filename ends with "/<path>".
52
- # That covers the three natural ways a user types a path: relative
53
- # to project root ("app/foo.rb"), absolute, or basename-only.
54
53
  def lookup(coverage_hash, path)
55
- absolute = File.expand_path(path)
56
- suffix = "/#{path}"
57
- coverage_hash.find { |fname, _| fname == absolute || fname == path || fname.end_with?(suffix) }
54
+ CoverageFile.lookup(coverage_hash, path)
58
55
  end
59
56
 
60
57
  def emit(match, opts, stdout)
61
58
  filename, payload = match
62
- if opts[:json]
59
+ if opts.fetch(:json)
63
60
  entry = {filename => payload} #: Hash[untyped, untyped]
64
61
  stdout.puts(JSON.pretty_generate(entry))
65
62
  else
66
- print_human(filename, payload, stdout, SimpleCov::CLI.color_enabled?(opts, stdout))
63
+ print_human(filename, payload, stdout, CLI.color_enabled?(opts, stdout))
67
64
  end
68
65
  end
69
66
 
@@ -73,13 +70,13 @@ module SimpleCov
73
70
  end
74
71
 
75
72
  def emit_criterion(stdout, payload, criterion, color)
76
- return unless payload.key?(criterion[:percent])
73
+ return unless payload.key?(criterion.fetch(:percent))
77
74
 
78
- pct = payload[criterion[:percent]].to_f
79
- stdout.puts(stats_row(criterion[:label],
80
- SimpleCov::Color.colorize_percent(pct, enabled: color),
81
- payload[criterion[:covered]],
82
- payload[criterion[:total]]))
75
+ pct = payload.fetch(criterion.fetch(:percent)).to_f
76
+ stdout.puts(stats_row(criterion.fetch(:label),
77
+ Color.colorize_percent(pct, enabled: color),
78
+ payload[criterion.fetch(:covered)],
79
+ payload[criterion.fetch(:total)]))
83
80
  end
84
81
  end
85
82
  end
@@ -5,29 +5,76 @@ require_relative "../coverage_json"
5
5
  module SimpleCov
6
6
  module CLI
7
7
  # Shared input boundary for commands that consume JSONFormatter's
8
- # coverage.json. It validates only the stable outer shape so older schema
8
+ # coverage.json. It validates only the stable outer shape, so older schema
9
9
  # versions remain readable.
10
10
  module CoverageFile
11
- # The coverage.json fields backing each criterion, keyed by the
12
- # criterion's singular name. The one table the coverage, uncovered,
13
- # and diff subcommands all read from, so a schema change lands in
14
- # exactly one place.
15
11
  CRITERIA = {
16
12
  line: {label: "Line", percent: "lines_covered_percent",
17
- covered: "covered_lines", total: "total_lines"},
13
+ covered: "covered_lines", missed: "missed_lines", total: "total_lines"},
18
14
  branch: {label: "Branch", percent: "branches_covered_percent",
19
- covered: "covered_branches", total: "total_branches"},
15
+ covered: "covered_branches", missed: "missed_branches", total: "total_branches"},
20
16
  method: {label: "Method", percent: "methods_covered_percent",
21
- covered: "covered_methods", total: "total_methods"}
17
+ covered: "covered_methods", missed: "missed_methods", total: "total_methods"}
22
18
  }.freeze
23
19
 
24
- module_function
20
+ extend self
21
+
22
+ # An exact match, the absolute path or the literal string passed, wins over
23
+ # a suffix match, so a nested "vendor/lib/foo.rb" can't shadow the real
24
+ # "lib/foo.rb" just by sitting earlier in the report. A subpath can end more
25
+ # than one key, and a collision is left as "not found" for the caller rather
26
+ # than scoring whichever key happens to sit first.
27
+ def lookup(coverage_hash, path)
28
+ absolute = File.expand_path(path)
29
+ return [absolute, coverage_hash.fetch(absolute)] if coverage_hash.key?(absolute)
30
+ return [path, coverage_hash.fetch(path)] if coverage_hash.key?(path)
31
+
32
+ matches = suffix_matches(coverage_hash, path)
33
+ matches.first if matches.one?
34
+ end
35
+
36
+ # The suffix's leading slash keeps "foo.rb" from matching "barfoo.rb", and
37
+ # hoisting it keeps the scan from allocating a string per entry.
38
+ def suffix_matches(coverage_hash, path)
39
+ suffix = "/#{path}"
40
+ coverage_hash.select { |fname, _| fname.end_with?(suffix) }
41
+ end
42
+
43
+ # The report's entries under exact, realpath-normalized keys, for
44
+ # programmatic resolution where a suffix fallback could bind a path to the
45
+ # wrong entry. The realpath step keeps a symlinked project root from
46
+ # splitting a resolved path apart from the report's keys.
47
+ def exact_index(coverage_hash)
48
+ index = {} #: Hash[String, untyped]
49
+ coverage_hash.each do |key, payload|
50
+ index[key] ||= payload
51
+ index[normalize(key)] ||= payload
52
+ end
53
+ index
54
+ end
55
+
56
+ # A key whose file no longer exists keeps its literal spelling.
57
+ def normalize(key)
58
+ File.realdirpath(key)
59
+ rescue SystemCallError
60
+ key
61
+ end
62
+
63
+ # An ambiguous subpath names its candidates: "no entry" would send the user
64
+ # hunting for a typo in a path that exists twice.
65
+ def not_found_message(coverage_hash, path, input)
66
+ matches = suffix_matches(coverage_hash, path).keys
67
+ return "no entry for #{path} in #{input}" if matches.size < 2
68
+
69
+ "#{path} matches #{matches.size} files in #{input}: #{matches.sort.join(", ")} " \
70
+ "(use a longer path to pick one)"
71
+ end
25
72
 
26
73
  def load_document(path, command:, stderr:)
27
- SimpleCov::CoverageJSON.load(path)
74
+ CoverageJSON.load(path)
28
75
  rescue Errno::ENOENT
29
76
  report_missing(stderr, command, path)
30
- rescue SimpleCov::CoverageJSON::Error => e
77
+ rescue CoverageJSON::Error => e
31
78
  report_invalid(stderr, command, path, e.message)
32
79
  rescue SystemCallError => e
33
80
  report_unreadable(stderr, command, path, e.message)
@@ -47,18 +94,15 @@ module SimpleCov
47
94
  def report_invalid(stderr, command, path, reason)
48
95
  detail = reason.lines.first.to_s.strip
49
96
  stderr.puts("simplecov #{command}: input file #{path.inspect} isn't valid JSON (#{detail})")
50
- nil
51
97
  end
52
98
 
53
99
  def report_missing(stderr, command, path)
54
100
  stderr.puts("simplecov #{command}: #{path} not found")
55
- nil
56
101
  end
57
102
 
58
103
  def report_unreadable(stderr, command, path, reason)
59
104
  detail = reason.lines.first.to_s.strip
60
105
  stderr.puts("simplecov #{command}: cannot read #{path.inspect} (#{detail})")
61
- nil
62
106
  end
63
107
  end
64
108
  end
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../patch/output"
4
+
5
+ module SimpleCov
6
+ module CLI
7
+ module DeadCode
8
+ module Output
9
+ extend CommandHelpers
10
+
11
+ extend self
12
+
13
+ def emit(stdout, opts, matrix, production)
14
+ return stdout.puts(JSON.generate(json_payload(matrix, production))) if opts.fetch(:json)
15
+
16
+ stdout.puts("Production coverage: #{opts.fetch(:production)}#{window_suffix(production)}")
17
+ stdout.puts
18
+ last_seen = production.fetch("last_seen")
19
+ opts.fetch(:untested) ? emit_untested(stdout, matrix, last_seen) : emit_dead(stdout, matrix, last_seen)
20
+ end
21
+
22
+ def emit_dead(stdout, matrix, last_seen)
23
+ rows = section(stdout, "Dead code (not run in production, not covered by tests):", matrix, :dead,
24
+ last_seen)
25
+ rows += section(stdout, "Possibly dead (not run in production, covered only by tests):", matrix,
26
+ :possibly_dead, last_seen)
27
+ return stdout.puts("No dead code found.") if rows.zero?
28
+
29
+ dead = count(matrix, :dead)
30
+ possibly = count(matrix, :possibly_dead)
31
+ stdout.puts("#{pluralize(dead, "dead line")}, #{pluralize(possibly, "possibly dead line")}")
32
+ end
33
+
34
+ def emit_untested(stdout, matrix, last_seen)
35
+ rows = section(stdout, "Untested code running in production:", matrix, :untested_in_production, last_seen)
36
+ return stdout.puts("No untested production code found.") if rows.zero?
37
+
38
+ stdout.puts("#{pluralize(count(matrix, :untested_in_production), "untested line")} running in production")
39
+ end
40
+
41
+ def section(stdout, heading, matrix, bucket, last_seen)
42
+ rows = matrix.fetch(bucket).sort
43
+ return 0 if rows.empty?
44
+
45
+ stdout.puts(heading)
46
+ rows.each do |file, lines|
47
+ stdout.puts(" #{file}:#{Patch::Output.ranges(lines, ",")}#{markers(matrix, file, last_seen)}")
48
+ end
49
+ stdout.puts
50
+ rows.size
51
+ end
52
+
53
+ # The row's parenthesized evidence. "entire file" says every relevant line
54
+ # skipped production; "last run" dates the store's last sighting of the file,
55
+ # which for a dead or possibly dead row means other lines of the file ran
56
+ # then, and its absence means the window never saw the file at all. Both can
57
+ # hold at once.
58
+ def markers(matrix, file, last_seen)
59
+ markers = [] #: Array[String]
60
+ markers << "entire file" if matrix.fetch(:entire).include?(file)
61
+ stamp = last_seen[file]
62
+ markers << "last run #{stamp[0, 10]}" if stamp.instance_of?(String)
63
+ " (#{markers.join(", ")})" unless markers.empty?
64
+ end
65
+
66
+ def count(matrix, bucket)
67
+ matrix.fetch(bucket).sum { |_file, lines| lines.size }
68
+ end
69
+
70
+ def pluralize(number, noun)
71
+ "#{number} #{noun}#{"s" unless one?(number)}"
72
+ end
73
+
74
+ def window_suffix(production)
75
+ started = production["started_at"]
76
+ updated = production["updated_at"]
77
+ " (window #{started} to #{updated})" if started && updated
78
+ end
79
+
80
+ def json_payload(matrix, production)
81
+ last_seen = production.fetch("last_seen")
82
+ payload = {window: {started_at: production["started_at"], updated_at: production["updated_at"]}}
83
+ %i[dead possibly_dead untested_in_production].each do |bucket|
84
+ payload[bucket] = matrix.fetch(bucket).sort.map { |file, lines| json_entry(file, lines, last_seen) }
85
+ end
86
+ payload
87
+ end
88
+
89
+ # JSON carries the full stamp where the text views print its date, and omits
90
+ # the key for a file the window never saw.
91
+ def json_entry(file, lines, last_seen)
92
+ entry = {file: file, lines: lines} #: Hash[Symbol, untyped]
93
+ stamp = last_seen[file]
94
+ entry[:last_seen] = stamp if stamp.instance_of?(String)
95
+ entry
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end