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
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "optparse"
5
+ require "time"
6
+ require_relative "command_helpers"
7
+ require_relative "git"
8
+ require_relative "status/facts"
9
+
10
+ module SimpleCov
11
+ module CLI
12
+ module Status
13
+ extend CommandHelpers
14
+
15
+ extend self
16
+
17
+ def run(args, stdout:, stderr:)
18
+ opts, = parse_common(args)
19
+ document = CoverageFile.load_document(opts.fetch(:input), command: "status", stderr: stderr)
20
+ return 1 unless document
21
+
22
+ facts = Facts.gather(document, CLI.default_resultset)
23
+ opts.fetch(:json) ? stdout.puts(JSON.pretty_generate(facts)) : print_facts(facts, opts, stdout)
24
+ 0
25
+ end
26
+
27
+ def print_facts(facts, opts, stdout)
28
+ stdout.puts("report #{opts.fetch(:input)}")
29
+ report_lines(facts).each { |line| stdout.puts(" #{line}") }
30
+ print_resultset(facts, stdout)
31
+ end
32
+
33
+ def report_lines(facts)
34
+ [
35
+ facts.fetch(:generated_at) && "generated #{facts.fetch(:generated_at)}#{age_suffix(facts.fetch(:age))}",
36
+ provenance_words(facts),
37
+ "commit #{commit_words(facts)}",
38
+ (totals_words(facts.fetch(:totals)) unless facts.fetch(:totals).empty?),
39
+ "tests recorded: #{contexts_words(facts.fetch(:contexts))}"
40
+ ].compact
41
+ end
42
+
43
+ def provenance_words(facts)
44
+ "by simplecov #{facts.fetch(:version)} running #{facts.fetch(:command_name)}"
45
+ end
46
+
47
+ def commit_words(facts)
48
+ commit = facts.fetch(:commit)
49
+ return "not recorded" unless commit.instance_of?(String)
50
+
51
+ "#{commit[0, 7]} (#{distance_words(facts.fetch(:behind))})"
52
+ end
53
+
54
+ def distance_words(behind)
55
+ return "not in this repository's history" unless behind
56
+ return "current HEAD" if behind.zero?
57
+
58
+ "#{behind} commit#{"s" unless one?(behind)} behind HEAD"
59
+ end
60
+
61
+ def totals_words(totals)
62
+ totals.map { |name, percent| "#{name} #{format("%.2f%%", percent)}" }.join(", ")
63
+ end
64
+
65
+ def contexts_words(count)
66
+ count ? "#{count} (track_tests)" : "none (enable track_tests to select and re-run by test)"
67
+ end
68
+
69
+ def print_resultset(facts, stdout)
70
+ entries = facts.fetch(:resultset)
71
+ return stdout.puts("resultset none") unless entries
72
+
73
+ stdout.puts("resultset #{facts.fetch(:resultset_path)}")
74
+ entries.each do |entry|
75
+ age = entry.fetch(:age) ? "#{age_in_words(entry.fetch(:age))} ago" : "age unknown"
76
+ stdout.puts(" #{entry.fetch(:command)}: #{age}")
77
+ end
78
+ end
79
+
80
+ def age_suffix(age)
81
+ " (#{age_in_words(age)} ago)" if age
82
+ end
83
+
84
+ def age_in_words(seconds)
85
+ return "#{seconds.round} seconds" if seconds < 90
86
+ return "#{(seconds / 60.0).round} minutes" if seconds < 5400
87
+ return "#{(seconds / 3600.0).round} hours" if seconds < 129_600
88
+
89
+ "#{(seconds / 86_400.0).round} days"
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleCov
4
+ module CLI
5
+ module Tests
6
+ # The `--redundant` sweep: the contexts whose covered lines other contexts
7
+ # also cover, the tests contributing no coverage of their own. A context is
8
+ # non-redundant exactly when some line is covered by it alone, so per file
9
+ # two bitmaps (lines seen once, lines seen more than once) find the uniquely
10
+ # covered lines and a second pass credits each to its owner.
11
+ module Redundancy
12
+ extend self
13
+
14
+ def redundant_ids(document, contexts, opts, stderr)
15
+ tables = sweep_tables(document, contexts, opts, stderr)
16
+ return unless tables
17
+
18
+ unique = unique_owners(tables, contexts.size)
19
+ contexts.each_index.reject { |index| unique.fetch(index) }.map { |index| contexts.fetch(index) }.sort
20
+ end
21
+
22
+ def unique_owners(tables, context_count)
23
+ unique = Array.new(context_count, false)
24
+ tables.each do |table|
25
+ lone = lone_bits(table)
26
+ table.each { |index, bitmap| unique[index] = true if bitmap.anybits?(lone) }
27
+ end
28
+ unique
29
+ end
30
+
31
+ # A bit enters `once` when brand new and leaves for good when any later
32
+ # bitmap carries it again. The two halves of the update are provably
33
+ # disjoint, so they are summed: for disjoint bits that builds the number OR
34
+ # would, and it leaves no spelling of the combination without a witness.
35
+ def lone_bits(table)
36
+ once = 0
37
+ ever = 0
38
+ table.each_value do |bitmap|
39
+ once = (once & ~bitmap) + (bitmap & ~ever)
40
+ ever |= bitmap
41
+ end
42
+ once
43
+ end
44
+
45
+ # The sweep reads tables no query named, so a malformed one anywhere poisons
46
+ # the whole answer, matching the targeted queries' all-or-nothing tolerance.
47
+ def sweep_tables(document, contexts, opts, stderr)
48
+ coverage = document["coverage"]
49
+ return invalid(opts, stderr, '"coverage" must be an object') unless coverage.is_a?(Hash)
50
+
51
+ tables = [] #: Array[Hash[Integer, Integer]]
52
+ coverage.each do |path, entry|
53
+ table = entry.is_a?(Hash) && swept_table(entry, contexts)
54
+ return invalid(opts, stderr, complaint(path, entry)) unless table
55
+
56
+ tables << table
57
+ end
58
+ tables
59
+ end
60
+
61
+ def swept_table(entry, contexts)
62
+ raw = entry["contexts"] || {}
63
+ Tests.decode_table(raw, contexts.size) if raw.is_a?(Hash)
64
+ end
65
+
66
+ def complaint(path, entry)
67
+ return "entry for #{path} must be an object" unless entry.is_a?(Hash)
68
+
69
+ "entry for #{path} carries a malformed \"contexts\" table"
70
+ end
71
+
72
+ def invalid(opts, stderr, reason)
73
+ CoverageFile.report_invalid(stderr, "tests", opts.fetch(:input), reason)
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,148 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "optparse"
5
+ require_relative "command_helpers"
6
+ require_relative "tests/redundancy"
7
+
8
+ module SimpleCov
9
+ module CLI
10
+ # `simplecov tests [<path>[:<line>]]`: list the recorded tests from a
11
+ # coverage.json written under `track_tests`. Text output is one id per line
12
+ # so the list can feed a test runner directly; empty answers keep stdout
13
+ # empty and note the emptiness on stderr instead.
14
+ module Tests
15
+ extend CommandHelpers
16
+
17
+ extend self
18
+
19
+ def run(args, stdout:, stderr:)
20
+ opts = parse(args)
21
+ return error(stderr, "line number must be positive") if opts.fetch(:line)&.zero?
22
+
23
+ document = CoverageFile.load_document(opts.fetch(:input), command: "tests", stderr: stderr)
24
+ return 1 unless document
25
+
26
+ ids = resolve(document, opts, stderr)
27
+ return 1 unless ids
28
+
29
+ emit(ids, opts, stdout, stderr)
30
+ 0
31
+ end
32
+
33
+ def parse(args)
34
+ opts, rest = parse_common(args) do |parser, options|
35
+ parser.on("--redundant") { options[:redundant] = true }
36
+ end
37
+ opts[:target] = rest.first
38
+ split = split_target(rest.first)
39
+ opts[:path] = split.fetch(:path)
40
+ opts[:line] = split.fetch(:line)
41
+ opts
42
+ end
43
+
44
+ # Only an all-digit tail after the last colon reads as a line number, so
45
+ # Windows drive colons and exotic filenames stay plain paths.
46
+ def split_target(target)
47
+ return {path: nil, line: nil} unless target
48
+
49
+ path, sep, tail = target.rpartition(":")
50
+ return {path: target, line: nil} if sep.empty? || !tail.match?(/\A\d+\z/)
51
+
52
+ {path: path, line: Integer(tail, 10)}
53
+ end
54
+
55
+ def resolve(document, opts, stderr)
56
+ contexts = recorded_contexts(document, opts, stderr)
57
+ return unless contexts
58
+
59
+ opts[:no_recording] = contexts.empty?
60
+ ids = selected_ids(document, contexts, opts, stderr)
61
+ return ids unless ids && opts[:redundant]
62
+
63
+ redundant = Redundancy.redundant_ids(document, contexts, opts, stderr)
64
+ redundant && (ids & redundant)
65
+ end
66
+
67
+ def selected_ids(document, contexts, opts, stderr)
68
+ return contexts.sort unless opts.fetch(:path)
69
+
70
+ entry = locate_entry(document, opts, stderr)
71
+ return unless entry
72
+
73
+ table = context_table(entry, contexts, opts, stderr)
74
+ table && ids_from(table, contexts, opts.fetch(:line))
75
+ end
76
+
77
+ def locate_entry(document, opts, stderr)
78
+ coverage = document["coverage"]
79
+ input = opts.fetch(:input)
80
+ unless coverage.is_a?(Hash)
81
+ return CoverageFile.report_invalid(stderr, "tests", input, '"coverage" must be an object')
82
+ end
83
+
84
+ path = opts.fetch(:path)
85
+ match = CoverageFile.lookup(coverage, path)
86
+ return error_nil(stderr, CoverageFile.not_found_message(coverage, path, input)) unless match
87
+ return match.last if match.last.is_a?(Hash)
88
+
89
+ CoverageFile.report_invalid(stderr, "tests", input, "entry for #{path} must be an object")
90
+ end
91
+
92
+ def context_table(entry, contexts, opts, stderr)
93
+ raw = entry["contexts"] || {}
94
+ table = decode_table(raw, contexts.size) if raw.is_a?(Hash)
95
+ return table if table
96
+
97
+ CoverageFile.report_invalid(stderr, "tests", opts.fetch(:input),
98
+ "entry for #{opts.fetch(:path)} carries a malformed \"contexts\" table")
99
+ end
100
+
101
+ # An absent key is an untouched file; anything malformed, a foreign index or
102
+ # a non-hex bitmap, poisons the whole answer, matching the all-or-nothing
103
+ # tolerance the resultset reader applies.
104
+ def decode_table(raw, context_count)
105
+ table = {} #: Hash[Integer, Integer]
106
+ raw.each do |index, hex|
107
+ return nil unless index.is_a?(String) && index.match?(/\A\d+\z/)
108
+ return nil unless hex.is_a?(String) && hex.match?(/\A\h+\z/)
109
+
110
+ position = Integer(index, 10)
111
+ return nil unless position < context_count
112
+
113
+ table[position] = Integer(hex, 16)
114
+ end
115
+ table
116
+ end
117
+
118
+ def ids_from(table, contexts, line)
119
+ bit = line && (1 << (line - 1))
120
+ table.filter_map { |index, bitmap| contexts.fetch(index) if bit.nil? || bitmap.anybits?(bit) }.sort
121
+ end
122
+
123
+ def emit(ids, opts, stdout, stderr)
124
+ return stdout.puts(JSON.pretty_generate(ids)) if opts.fetch(:json)
125
+ return note_empty(opts, stderr) if ids.empty?
126
+
127
+ ids.each { |id| stdout.puts(id) }
128
+ end
129
+
130
+ def note_empty(opts, stderr)
131
+ stderr.puts("simplecov tests: #{empty_message(opts)}")
132
+ end
133
+
134
+ def empty_message(opts)
135
+ target = opts.fetch(:target)
136
+ if opts[:redundant] && !opts.fetch(:no_recording)
137
+ return "no redundant test covers #{target}" if target
138
+
139
+ "no redundant tests, every recorded test covers at least one line uniquely"
140
+ elsif target
141
+ "no recorded test covers #{target}"
142
+ else
143
+ "no tests recorded"
144
+ end
145
+ end
146
+ end
147
+ end
148
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleCov
4
+ module CLI
5
+ module Uncovered
6
+ module Misses
7
+ extend self
8
+
9
+ def missed_for(payload, criterion)
10
+ case criterion
11
+ when :line then payload["lines"].instance_of?(Array) ? Show::Annotator.missed_lines(payload) : []
12
+ when :branch then collect(payload["branches"])
13
+ else collect(payload["methods"])
14
+ end
15
+ end
16
+
17
+ def collect(items)
18
+ missed = [] #: Array[Integer]
19
+ Show::Annotator.each_missed(items) { |line| missed << line }
20
+ missed.uniq.sort
21
+ end
22
+
23
+ def annotate(stdout, files, criterion, kind)
24
+ root = "#{File.expand_path(SimpleCov.root)}/"
25
+ diagnostics = files.flat_map do |fname, _pct, _covered, _total, missed|
26
+ Annotations.diagnostics(fname.delete_prefix(root), missed, criterion)
27
+ end
28
+ Annotations.emit(stdout, kind, diagnostics)
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -2,37 +2,41 @@
2
2
 
3
3
  require "json"
4
4
  require "optparse"
5
+ require_relative "annotations"
5
6
  require_relative "command_helpers"
7
+ require_relative "patch/output"
8
+ require_relative "show/annotator"
9
+ require_relative "uncovered/misses"
6
10
 
7
11
  module SimpleCov
8
12
  module CLI
9
- # `simplecov uncovered [--threshold N] [--top N] [--criterion C]` — list
10
- # the lowest-coverage files (by the chosen criterion, ascending), so a
11
- # developer can answer "where should I add tests next?" without
12
- # opening a browser. Reads coverage.json directly.
13
+ # `simplecov uncovered`: the lowest-coverage files by the chosen criterion,
14
+ # ascending, so a developer can answer "where should I add tests next?"
15
+ # without opening a browser.
13
16
  module Uncovered
14
17
  extend CommandHelpers
15
18
 
16
19
  DEFAULT_TOP = 10
17
20
 
18
- module_function
21
+ extend self
19
22
 
20
23
  def run(args, stdout:, stderr:, **)
21
24
  opts = parse(args)
22
- keys = CoverageFile::CRITERIA[opts[:criterion]]
23
- return unknown_criterion(opts[:criterion], stderr) unless keys
25
+ issue = Annotations.issue(opts)
26
+ return error(stderr, issue) if issue
24
27
 
28
+ keys = CoverageFile::CRITERIA[opts.fetch(:criterion)]
29
+ return unknown_criterion(opts.fetch(:criterion), stderr) unless keys
30
+
31
+ opts[:missing] = true if opts.fetch(:annotate)
25
32
  report(opts, keys, stdout, stderr)
26
33
  end
27
34
 
28
35
  def report(opts, keys, stdout, stderr)
29
- coverage = CoverageFile.load_coverage(opts[:input], command: "uncovered", stderr: stderr)
36
+ coverage = CoverageFile.load_coverage(opts.fetch(:input), command: "uncovered", stderr: stderr)
30
37
  return 1 unless coverage
31
38
 
32
- files = rank(coverage, opts[:threshold], keys).first(opts[:top])
33
- return stdout.puts(empty_message(opts[:json])) || 0 if files.empty?
34
-
35
- emit(stdout, files, opts)
39
+ emit(stdout, rank(coverage, opts, keys).first(opts.fetch(:top)), opts)
36
40
  0
37
41
  end
38
42
 
@@ -42,22 +46,30 @@ module SimpleCov
42
46
  end
43
47
 
44
48
  def emit(stdout, files, opts)
45
- opts[:json] ? emit_json(stdout, files) : emit_text(stdout, files, SimpleCov::CLI.color_enabled?(opts, stdout))
49
+ kind = opts.fetch(:annotate)
50
+ return Misses.annotate(stdout, files, opts.fetch(:criterion), kind) if kind
51
+ return stdout.puts(empty_message(opts.fetch(:json))) if files.empty?
52
+
53
+ opts.fetch(:json) ? emit_json(stdout, files) : emit_text(stdout, files, CLI.color_enabled?(opts, stdout))
46
54
  end
47
55
 
48
56
  def parse(args)
49
- opts, = parse_common(args, threshold: 100.0, top: DEFAULT_TOP, criterion: :line) do |o, options|
50
- o.on("--threshold N", Float) { |v| options[:threshold] = v }
51
- o.on("--top N", Integer) { |v| options[:top] = validate_top(v) }
52
- o.on("--criterion C") { |v| options[:criterion] = v.to_sym }
53
- end
57
+ opts, = parse_common(args, threshold: 100.0, top: DEFAULT_TOP, criterion: :line,
58
+ missing: false, annotate: nil) { |o, options| own_options(o, options) }
54
59
  opts
55
60
  end
56
61
 
57
- # A negative count would raise from `Array#first`; report it as
58
- # the parse error it is (`--top 0` is allowed and shows nothing).
59
- # OptionParser prepends the offending option's name, so the raise
60
- # carries only the reason.
62
+ def own_options(parser, options)
63
+ parser.on("--threshold N", Float) { |v| options[:threshold] = v }
64
+ parser.on("--top N", Integer) { |v| options[:top] = validate_top(v) }
65
+ parser.on("--criterion C") { |v| options[:criterion] = v.to_sym }
66
+ parser.on("--missing") { options[:missing] = true }
67
+ parser.on("--annotate KIND") { |v| options[:annotate] = v }
68
+ end
69
+
70
+ # A negative count would raise from `Array#first`, so it is reported as the
71
+ # parse error it is. `--top 0` is allowed and shows nothing. OptionParser
72
+ # prepends the offending option's name, so the raise carries only the reason.
61
73
  def validate_top(value)
62
74
  raise OptionParser::InvalidArgument, "must not be negative" if value.negative?
63
75
 
@@ -65,12 +77,18 @@ module SimpleCov
65
77
  end
66
78
 
67
79
  def emit_text(stdout, files, color)
68
- files.each { |fname, pct, covered, total| stdout.puts(format_row(fname, pct, covered, total, color)) }
80
+ files.each do |fname, pct, covered, total, missed|
81
+ row = format_row(fname, pct, covered, total, color)
82
+ row += " missing #{Patch::Output.ranges(missed, ",")}" if missed&.any?
83
+ stdout.puts(row)
84
+ end
69
85
  end
70
86
 
71
87
  def emit_json(stdout, files)
72
- rows = files.map do |fname, pct, covered, total|
73
- {"file" => fname, "percent" => pct, "covered" => covered, "total" => total}
88
+ rows = files.map do |fname, pct, covered, total, missed|
89
+ row = {"file" => fname, "percent" => pct, "covered" => covered, "total" => total}
90
+ row["missing"] = missed if missed
91
+ row
74
92
  end
75
93
  stdout.puts(JSON.pretty_generate(rows))
76
94
  end
@@ -79,24 +97,30 @@ module SimpleCov
79
97
  json ? "[]" : "simplecov uncovered: nothing to report"
80
98
  end
81
99
 
82
- def rank(coverage_hash, threshold, keys)
83
- rows = coverage_hash.filter_map { |fname, payload| row_for(fname, payload, threshold, keys) }
100
+ def rank(coverage_hash, opts, keys)
101
+ rows = coverage_hash.filter_map { |fname, payload| row_for(fname, payload, opts, keys) }
84
102
  rows.sort_by { |_fname, pct, _c, _t| pct }
85
103
  end
86
104
 
87
- def row_for(fname, payload, threshold, keys)
88
- return unless payload.is_a?(Hash) && payload[keys[:total]].to_i.positive?
105
+ def row_for(fname, payload, opts, keys)
106
+ return unless payload.instance_of?(Hash) && payload[keys.fetch(:total)].to_i.positive?
89
107
 
90
- pct = payload[keys[:percent]].to_f
91
- return if pct >= threshold
108
+ pct = payload[keys.fetch(:percent)].to_f
109
+ return if pct >= opts.fetch(:threshold)
110
+
111
+ build_row(fname, pct, payload, opts, keys)
112
+ end
92
113
 
93
- [fname, pct, payload[keys[:covered]].to_i, payload[keys[:total]].to_i]
114
+ def build_row(fname, pct, payload, opts, keys)
115
+ row = [fname, pct, payload[keys.fetch(:covered)].to_i, payload.fetch(keys.fetch(:total)).to_i]
116
+ row << Misses.missed_for(payload, opts.fetch(:criterion)) if opts.fetch(:missing)
117
+ row
94
118
  end
95
119
 
96
120
  def format_row(fname, pct, covered, total, color)
97
121
  format("%<pct>s %<covered>d/%<total>d %<fname>s",
98
- pct: SimpleCov::Color.colorize_percent(pct, format("%6.2f%%", pct), enabled: color),
99
- covered: covered, total: total, fname: fname)
122
+ pct: Color.colorize_percent(pct, format("%6.2f%%", pct), enabled: color),
123
+ covered: covered, total: total, fname: fname)
100
124
  end
101
125
  end
102
126
  end
@@ -0,0 +1,153 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleCov
4
+ module CLI
5
+ module Usage
6
+ extend self
7
+
8
+ def for(cli, command)
9
+ sections = text(cli).split("\n\n")
10
+ row = sections.fetch(1).lines.find { |line| line.match?(/\A #{Regexp.escape(command)}[ \[]/) }
11
+ options = sections.select { |section| section_for?(section, command) }
12
+ ["Usage: simplecov #{command} [options]", row&.strip, *options].compact.join("\n\n")
13
+ end
14
+
15
+ def section_for?(section, command)
16
+ header = heading_of(section)
17
+ header.end_with?("options:") && header.delete_suffix("options:").split(%r{[\s/]+}).include?(command)
18
+ end
19
+
20
+ def heading_of(section)
21
+ section.lines.first.to_s.strip
22
+ end
23
+
24
+ # A method rather than a constant so its default paths resolve at call time
25
+ # against the active `.simplecov`.
26
+ def text(cli)
27
+ <<~USAGE
28
+ Usage: simplecov <command> [options]
29
+
30
+ Commands:
31
+ run <command...> Execute <command> with SimpleCov pre-loaded
32
+ (works without a test_helper hook)
33
+ coverage <path> Print coverage stats for the given file
34
+ show <path> Print the file's source annotated with hit counts and misses
35
+ report Print the overall summary and group totals
36
+ status Report freshness: age, commit distance, recorded tests
37
+ history Print the recorded coverage trend, a sparkline per criterion
38
+ badge Render the coverage percent as an SVG badge
39
+ uncovered List the lowest-coverage files
40
+ tests [<path>[:<line>]] List recorded tests for a file or line (needs track_tests)
41
+ affected List test files touching code changed since a git ref (needs track_tests)
42
+ merge <files...> Merge multiple .resultset.json files
43
+ diff <baseline> Show per-file coverage delta vs baseline
44
+ patch Show coverage of the lines a change touched
45
+ ratchet Rewrite the per-file coverage baseline, only ever tightening
46
+ dead-code Cross production coverage with the report to find dead code
47
+ open Open the HTML report in the default browser
48
+ serve Serve the coverage report over HTTP
49
+ watch <command...> Re-run <command> on save and live-reload the served report
50
+ clean Remove the coverage report directory
51
+ completions <shell> Emit the completion script for fish, bash, or zsh
52
+ help Show this message
53
+ version Print the version (also --version / -v)
54
+
55
+ Every command answers `--help` / `-h`. Default paths follow a project's
56
+ `.simplecov` SimpleCov.coverage_dir (#{cli.coverage_dir} for this run).
57
+
58
+ coverage / show / report / status / uncovered / tests / affected / diff / patch / ratchet / dead-code options:
59
+ --input PATH Read from PATH instead of #{cli.default_input}
60
+ --no-color Disable colorized percentages (also honors NO_COLOR / FORCE_COLOR env)
61
+
62
+ coverage options:
63
+ --json Print the file's JSON entry verbatim
64
+
65
+ show options:
66
+ --uncovered-only Print only <path>:<ranges> of the missed lines
67
+ --json Emit path, missed lines, per-line hits, and markers as JSON
68
+
69
+ report options:
70
+ --json Emit totals and group sections as JSON
71
+
72
+ status options:
73
+ --json Emit the freshness facts as a JSON object
74
+
75
+ history options:
76
+ --input PATH Read from PATH instead of the coverage directory's .history.json
77
+ --file PATH Follow one file's trajectory instead of the totals
78
+ --json Emit the entries (or the file's trajectory) as JSON
79
+ --no-color Disable colorized deltas (also honors NO_COLOR / FORCE_COLOR env)
80
+
81
+ badge options:
82
+ --input PATH Read from PATH instead of #{cli.default_input}
83
+ --output PATH Write the badge to PATH instead of stdout
84
+ --criterion C line, branch, or method (default: line)
85
+ --label TEXT The badge's left-side text (default: the criterion's, like "line coverage")
86
+
87
+ uncovered options:
88
+ --threshold N Only show files below N% coverage
89
+ --top N Show at most N files (default: 10)
90
+ --criterion C line, branch, or method (default: line)
91
+ --missing Append the missed line ranges to each row
92
+ --annotate KIND Emit CI annotations instead of rows: github, gitlab, rdjson, azure, teamcity, or buildkite
93
+ --json Emit results as a JSON array (for CI)
94
+
95
+ tests options:
96
+ --redundant List only tests whose covered lines other tests also cover
97
+ --json Emit the test ids as a JSON array
98
+
99
+ affected options:
100
+ --base REF Diff against the merge base of REF and HEAD (default: origin's HEAD, else main)
101
+ --run <command...> Run <command> (all args after --run) with the selection appended, or bare on a full-suite fallback, exiting with the command's status
102
+ --json Emit {full_suite, triggers, tests} as a JSON object
103
+
104
+ merge options:
105
+ --output PATH Write merged resultset to PATH (default: #{cli.default_resultset})
106
+ --honor-timeout Drop entries older than merge_timeout
107
+ --dry-run Print what would be written without writing
108
+ -q, --quiet Suppress the success status line
109
+
110
+ diff options:
111
+ --fail-on-drop Exit non-zero when any file's coverage dropped vs baseline (deletions don't count)
112
+ --json Emit results as a JSON array (for CI)
113
+ --threshold N Only show files whose absolute delta in any criterion is at least N%
114
+
115
+ patch options:
116
+ --base REF Diff against the merge-base of REF for the touched lines (default: origin's HEAD, else main, or in CI the PR's target branch)
117
+ --minimum N Exit non-zero when patch coverage on any measured criterion is below N%
118
+ --find-renames Follow a renamed file instead of counting the moved file as all-new
119
+ --annotate KIND Emit CI annotations instead of rows: github, gitlab, rdjson, azure, teamcity, or buildkite
120
+ --json Emit results as a JSON array (for CI)
121
+
122
+ dead-code options:
123
+ --production PATH The production coverage file a SimpleCov::Production sink wrote (default: the project's `production_coverage`, required when none is configured)
124
+ --untested-in-production Print code production runs that no test covers, instead of the dead rows
125
+ --json Emit every category (dead, possibly dead, untested in production) as JSON
126
+
127
+ ratchet options:
128
+ --baseline PATH The baseline file to rewrite (default: the project's `baseline_file`, .simplecov_baseline.yml)
129
+ --init Regenerate from scratch: add entries for new files and reset every floor to the current state
130
+ --dry-run Print what would change without writing
131
+ --json Emit the summary (tightened / pruned / regressed entries) as JSON
132
+
133
+ open options:
134
+ --report PATH Open PATH instead of #{cli.default_report}
135
+
136
+ serve options:
137
+ --port N Bind to port N (default: random open port)
138
+ --host HOST Bind to HOST (default: 127.0.0.1)
139
+
140
+ watch options:
141
+ --port N Bind to port N (default: random open port)
142
+ --host HOST Bind to HOST (default: 127.0.0.1)
143
+ --interval SECONDS Poll the tracked files this often (default: 0.5)
144
+ --open Open the served report in the default browser
145
+
146
+ clean options:
147
+ --dry-run Print what would be removed without deleting
148
+ -q, --quiet Suppress status lines
149
+ USAGE
150
+ end
151
+ end
152
+ end
153
+ end