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,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleCov
4
+ module CLI
5
+ module Watch
6
+ # The live half of the watch server: an /events endpoint streaming
7
+ # server-sent events to every open report tab, and an index route that
8
+ # appends the reload listener to the artifact on the way out, so the report
9
+ # on disk stays byte-identical to a plain run's.
10
+ class LiveReport
11
+ RELOAD_SCRIPT = "\n<script>new EventSource('/events').onmessage = () => location.reload();</script>\n"
12
+
13
+ def initialize(dir)
14
+ @dir = dir
15
+ @queues = [] #: Array[Thread::Queue]
16
+ @lock = Mutex.new
17
+ end
18
+
19
+ def routes
20
+ page = method(:page)
21
+ {"/events" => method(:stream), "/" => page, "/index.html" => page}
22
+ end
23
+
24
+ def broadcast(event = "reload")
25
+ with_queues { |queues| queues.each { |queue| queue << event } }
26
+ end
27
+
28
+ private
29
+
30
+ # Holds the connection open, forwarding each broadcast as one SSE message,
31
+ # until the tab goes away and the write fails. A closed tab that never sees
32
+ # another broadcast parks its thread on the queue, a leak the dev-server
33
+ # trade accepts.
34
+ def stream(client)
35
+ queue = Queue.new #: Thread::Queue
36
+ begin
37
+ with_queues { |queues| queues << queue }
38
+ forward(client, queue)
39
+ ensure
40
+ with_queues { |queues| queues.delete(queue) }
41
+ end
42
+ end
43
+
44
+ def forward(client, queue)
45
+ client.write("HTTP/1.1 200 OK\r\nContent-Type: text/event-stream\r\n" \
46
+ "Cache-Control: no-cache\r\n\r\n")
47
+ loop { client.write("data: #{next_event(queue)}\n\n") }
48
+ rescue IOError, SystemCallError
49
+ nil
50
+ end
51
+
52
+ # mutant:disable
53
+ def next_event(queue)
54
+ queue.pop
55
+ end
56
+
57
+ def page(client)
58
+ body = File.binread(File.join(@dir, "index.html")) + RELOAD_SCRIPT
59
+ Serve::StaticFileHandler.respond(client, 200, body, "text/html; charset=utf-8")
60
+ rescue SystemCallError
61
+ Serve::StaticFileHandler.respond(client, 404)
62
+ end
63
+
64
+ # mutant:disable
65
+ # Every touch of the queue registry goes through the lock: an accept thread
66
+ # adds and drops its own queue while the session thread walks them all.
67
+ def with_queues
68
+ @lock.synchronize { yield @queues }
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleCov
4
+ module CLI
5
+ module Watch
6
+ class Narrator
7
+ def initialize(stdout, root)
8
+ @stdout = stdout
9
+ @root = root
10
+ end
11
+
12
+ def banner(server, count)
13
+ host = Serve.url_host(server.addr.fetch(3))
14
+ @stdout.puts("watching #{count} file#{"s" unless count.eql?(1)}, " \
15
+ "serving http://#{host}:#{server.addr.fetch(1)}/")
16
+ @stdout.puts("Press Ctrl-C to stop.")
17
+ end
18
+
19
+ def change(changed, plan)
20
+ named = name(changed)
21
+ return @stdout.puts("#{named} changed, no recorded test touches it") unless plan.fetch(:run)
22
+
23
+ @stdout.print("#{named} changed, #{action(plan.fetch(:tests))}...")
24
+ end
25
+
26
+ def result(before, after)
27
+ return @stdout.puts unless after
28
+
29
+ delta = before ? format(" (%+.2f%%)", after - before) : ""
30
+ @stdout.puts(format(" %.2f%%", after) + delta)
31
+ end
32
+
33
+ def failed_refresh
34
+ @stdout.puts(" the report did not regenerate")
35
+ end
36
+
37
+ def stopping
38
+ @stdout.puts("\nsimplecov watch: stopping")
39
+ end
40
+
41
+ private
42
+
43
+ def name(changed)
44
+ relative = changed.map { |path| path.delete_prefix("#{@root}/") }
45
+ named = relative.first
46
+ relative.size.eql?(1) ? named : "#{named} and #{relative.size - 1} more"
47
+ end
48
+
49
+ def action(tests)
50
+ tests ? "running #{tests.size} file#{"s" unless tests.one?}" : "running the full suite"
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleCov
4
+ module CLI
5
+ module Watch
6
+ # Watches a fixed set of paths by polling mtimes, the trade chosen over a
7
+ # filesystem-event dependency for a command that is optional to begin with.
8
+ # Editors that save through a temporary file plus rename still move the final
9
+ # path's mtime, which is all this looks at.
10
+ class Poller
11
+ def initialize
12
+ @snapshot = {} #: Hash[String, untyped]
13
+ end
14
+
15
+ # Replaces the watched set, snapshotting current mtimes. A path with no file
16
+ # snapshots as nil, so appearing counts as a change.
17
+ def watch(paths)
18
+ @snapshot = paths.to_h { |path| [path, stamp(path)] }
19
+ end
20
+
21
+ def size
22
+ @snapshot.size
23
+ end
24
+
25
+ def changes
26
+ changed = [] #: Array[String]
27
+ @snapshot.each_key do |path|
28
+ now = stamp(path)
29
+ next if now.eql?(@snapshot.fetch(path))
30
+
31
+ @snapshot[path] = now
32
+ changed << path
33
+ end
34
+ changed
35
+ end
36
+
37
+ def stamp(path)
38
+ File.mtime(path)
39
+ rescue SystemCallError
40
+ nil
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,134 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleCov
4
+ module CLI
5
+ module Watch
6
+ # One watch session: accept report connections in the background, poll the
7
+ # tracked set for changes, re-run tests on save (selecting over the recorded
8
+ # test map when the report carries one), and push a reload to every open tab
9
+ # when the report regenerates.
10
+ class Session
11
+ # The merge window the child runs get through SIMPLECOV_MERGE_TIMEOUT: a
12
+ # day, so subset re-runs keep merging into a whole report across a long
13
+ # session instead of eroding it after the default ten minutes.
14
+ MERGE_WINDOW = "86400"
15
+
16
+ TOTALS_KEYS = {"line" => "lines", "branch" => "branches", "method" => "methods"}.freeze
17
+
18
+ def initialize(command:, dir:, interval:, stdout:, stderr:)
19
+ @command = command
20
+ @dir = dir
21
+ @interval = interval
22
+ @stderr = stderr
23
+ @root = Dir.pwd
24
+ @narrator = Narrator.new(stdout, @root)
25
+ @live = LiveReport.new(dir)
26
+ @poller = Poller.new
27
+ @document = {} #: Hash[String, untyped]
28
+ end
29
+
30
+ def run(server)
31
+ accept_loop(server)
32
+ run_tests(nil) unless File.exist?(json_path)
33
+ return 1 unless refresh
34
+
35
+ @narrator.banner(server, @poller.size)
36
+ poll_forever
37
+ rescue Interrupt
38
+ @narrator.stopping
39
+ 0
40
+ end
41
+
42
+ def poll_forever
43
+ loop do
44
+ sleep(@interval)
45
+ step
46
+ end
47
+ end
48
+
49
+ def step
50
+ changed = settled_changes
51
+ return if changed.empty?
52
+
53
+ plan = plan_for(changed)
54
+ @narrator.change(changed, plan)
55
+ return unless plan.fetch(:run)
56
+
57
+ before = total_percent
58
+ run_tests(plan.fetch(:tests))
59
+ return @narrator.failed_refresh unless refresh
60
+
61
+ @live.broadcast
62
+ @narrator.result(before, total_percent)
63
+ end
64
+
65
+ private
66
+
67
+ def accept_loop(server)
68
+ Thread.new do
69
+ loop do
70
+ Thread.new(server.accept) do |client|
71
+ Serve::StaticFileHandler.handle_connection(client, @dir, @live.routes)
72
+ end
73
+ end
74
+ rescue IOError, SystemCallError
75
+ nil
76
+ end
77
+ end
78
+
79
+ def plan_for(changed)
80
+ relative = changed.map { |path| path.delete_prefix("#{@root}/") }
81
+ TestPlan.build(relative, @document, root: @root, input: json_path, stderr: @stderr)
82
+ end
83
+
84
+ # The command is the project's own test invocation and must produce the
85
+ # report itself. A selection is appended to it; no selection means the whole
86
+ # suite, which is the command on its own.
87
+ def run_tests(tests)
88
+ env = {"SIMPLECOV_MERGE_TIMEOUT" => MERGE_WINDOW}
89
+ Kernel.system(env, *(tests ? @command + tests : @command))
90
+ end
91
+
92
+ # The regenerated report is both the watch set and the totals the result line
93
+ # diffs against. The previous document is kept when the read fails, so one
94
+ # bad write doesn't blind the session.
95
+ def refresh
96
+ document = CoverageFile.load_document(json_path, command: "watch", stderr: @stderr)
97
+ return false unless document
98
+
99
+ @document = document
100
+ @poller.watch(TestPlan.watched_paths(document, @root))
101
+ document
102
+ end
103
+
104
+ # Editors save in bursts, so after the first detection keep polling until a
105
+ # full interval passes with nothing new.
106
+ def settled_changes
107
+ changed = @poller.changes
108
+ return changed if changed.empty?
109
+
110
+ loop do
111
+ sleep(@interval)
112
+ more = @poller.changes
113
+ break if more.empty?
114
+
115
+ changed |= more
116
+ end
117
+ changed
118
+ end
119
+
120
+ # Follows the report's own primary criterion, falling back to line coverage
121
+ # for documents from before meta carried one.
122
+ def total_percent
123
+ key = TOTALS_KEYS[@document.dig("meta", "primary_coverage")] || "lines"
124
+ value = @document.dig("total", key, "percent")
125
+ (_ = value).to_f if value.is_a?(Numeric)
126
+ end
127
+
128
+ def json_path
129
+ File.join(@dir, "coverage.json")
130
+ end
131
+ end
132
+ end
133
+ end
134
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleCov
4
+ module CLI
5
+ module Watch
6
+ module TestPlan
7
+ extend self
8
+
9
+ # {run:, tests:} where nil tests mean the full command. A report with no map,
10
+ # a malformed one, or any fail-open trigger runs everything; an empty
11
+ # selection runs nothing.
12
+ def build(changed, document, root:, input:, stderr:)
13
+ contexts = document["contexts"]
14
+ return {run: true, tests: nil} unless contexts.instance_of?(Array) && contexts.all?(String)
15
+
16
+ selection = Affected::Selection.build(changed, document, contexts, {input: input}, stderr, root: root)
17
+ return {run: true, tests: nil} if selection.nil? || !selection.fetch(:triggers).empty?
18
+ return {run: false, tests: []} if selection.fetch(:tests).empty?
19
+
20
+ {run: true, tests: selection.fetch(:tests)}
21
+ end
22
+
23
+ # The watch set a report defines: its tracked files plus the files of its
24
+ # recorded tests.
25
+ def watched_paths(document, root)
26
+ none = [] #: Array[String]
27
+ coverage = document["coverage"]
28
+ sources = coverage.instance_of?(Hash) ? coverage.keys : none
29
+ tests = recorded_test_files(document).map { |path| File.expand_path(path, root) }
30
+ (sources + tests).uniq
31
+ end
32
+
33
+ def recorded_test_files(document)
34
+ none = [] #: Array[String]
35
+ contexts = document["contexts"]
36
+ return none unless contexts.instance_of?(Array)
37
+
38
+ contexts.filter_map { |id| Affected::Selection.test_file_of(id.to_s) }
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "optparse"
4
+ require_relative "command_helpers"
5
+ require_relative "open"
6
+ require_relative "serve"
7
+ require_relative "affected"
8
+ require_relative "watch/poller"
9
+ require_relative "watch/live_report"
10
+ require_relative "watch/narrator"
11
+ require_relative "watch/test_plan"
12
+ require_relative "watch/session"
13
+
14
+ module SimpleCov
15
+ module CLI
16
+ # `simplecov watch <command...>`: the coverage inner loop. Serves the report
17
+ # the way `serve` does, polls the tracked files for saves, re-runs the given
18
+ # command on change, and pushes a reload to the open tab over server-sent
19
+ # events when the report regenerates. With a `track_tests` recording in the
20
+ # report, a save re-runs only the tests that touch the changed files, by the
21
+ # same selection walk `simplecov affected` uses.
22
+ module Watch
23
+ extend CommandHelpers
24
+
25
+ extend self
26
+
27
+ def run(args, stdout:, stderr:, **)
28
+ opts, command = parse(args)
29
+ return error(stderr, "missing command to run (e.g. `simplecov watch bundle exec rspec`)") if command.empty?
30
+
31
+ Serve.require_socket
32
+ server = bind(opts, stderr)
33
+ return 1 unless server
34
+
35
+ serve_session(server, command, opts, stdout, stderr)
36
+ end
37
+
38
+ def serve_session(server, command, opts, stdout, stderr)
39
+ launch_browser(server, stderr) if opts.fetch(:open)
40
+ session_for(command, opts, stdout, stderr).run(server)
41
+ ensure
42
+ server.close
43
+ end
44
+
45
+ # Fire-and-forget through the same platform opener `simplecov open` uses,
46
+ # detached so the session never waits on a browser. A platform with no known
47
+ # opener notes the URL instead of failing the watch.
48
+ def launch_browser(server, stderr)
49
+ url = "http://#{Serve.url_host(server.addr.fetch(3))}:#{server.addr.fetch(1)}/"
50
+ opener = Open.browser_opener
51
+ unless opener
52
+ return stderr.puts("simplecov watch: no known browser opener for this platform, open it yourself: #{url}")
53
+ end
54
+
55
+ Process.detach(spawn(*opener, url, out: File::NULL, err: File::NULL))
56
+ end
57
+
58
+ # `order` rather than `parse` stops at the first positional, so the runner
59
+ # command keeps its own flags: `simplecov watch bundle exec rspec --seed 1`
60
+ # passes --seed through untouched.
61
+ def parse(args)
62
+ opts = {port: 0, host: "127.0.0.1", interval: 0.5, open: false} #: Hash[Symbol, untyped]
63
+ rest =
64
+ build_parser do |parser|
65
+ parser.on("--port N", Integer) { |v| opts[:port] = v }
66
+ parser.on("--host HOST") { |v| opts[:host] = v }
67
+ parser.on("--interval SECONDS", Float) { |v| opts[:interval] = v }
68
+ parser.on("--open") { opts[:open] = true }
69
+ end.order(args)
70
+ [opts, rest]
71
+ end
72
+
73
+ def bind(opts, stderr)
74
+ (_ = TCPServer).new(opts.fetch(:host), opts.fetch(:port)) #: TCPServer
75
+ rescue SystemCallError, SocketError => e
76
+ error_nil(stderr, "cannot bind to #{opts.fetch(:host)}:#{opts.fetch(:port)} (#{e})")
77
+ end
78
+
79
+ def session_for(command, opts, stdout, stderr)
80
+ Session.new(command: command, dir: CLI.coverage_dir,
81
+ interval: opts.fetch(:interval), stdout: stdout, stderr: stderr)
82
+ end
83
+ end
84
+ end
85
+ end
data/lib/simplecov/cli.rb CHANGED
@@ -1,50 +1,67 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "optparse"
4
+ require_relative "version"
4
5
  require_relative "color"
5
6
  require_relative "cli/dotfile"
7
+ require_relative "cli/badge"
6
8
  require_relative "cli/clean"
9
+ require_relative "cli/completions"
7
10
  require_relative "cli/coverage_file"
8
11
  require_relative "cli/coverage"
12
+ require_relative "cli/dead_code"
9
13
  require_relative "cli/diff"
14
+ require_relative "cli/history"
10
15
  require_relative "cli/merge"
11
16
  require_relative "cli/open"
17
+ require_relative "cli/patch"
18
+ require_relative "cli/ratchet"
12
19
  require_relative "cli/report"
13
20
  require_relative "cli/run"
14
21
  require_relative "cli/serve"
22
+ require_relative "cli/show"
23
+ require_relative "cli/status"
24
+ require_relative "cli/tests"
25
+ require_relative "cli/affected"
15
26
  require_relative "cli/uncovered"
27
+ require_relative "cli/usage"
28
+ require_relative "cli/watch"
16
29
 
17
30
  module SimpleCov
18
- # Lightweight command-line front-end. `run` dispatches a subcommand
19
- # (`coverage`, `report`, `uncovered`, `merge`, `diff`, `open`, etc.)
20
- # see the `usage` text below for the full list, or run `simplecov help`.
21
- #
22
- # Read-only subcommands consume JSONFormatter output (`coverage.json`),
23
- # which the bundled HTMLFormatter already drops alongside the HTML, so
24
- # no runtime hooking is needed for those. Default paths follow the
25
- # project's `.simplecov` `SimpleCov.coverage_dir` setting when one is
26
- # present, so a project that writes its report somewhere other than
27
- # `coverage/` doesn't have to pass `--input` / `--report` every
28
- # invocation.
31
+ # Lightweight command-line front-end. Read-only subcommands consume
32
+ # JSONFormatter output, which the bundled HTMLFormatter already drops
33
+ # alongside the HTML, so no runtime hooking is needed for those. Default
34
+ # paths follow the project's `.simplecov` `coverage_dir` setting when one is
35
+ # present.
29
36
  module CLI
30
37
  COMMANDS = {
31
38
  "coverage" => Coverage,
39
+ "show" => Show,
32
40
  "run" => Run,
33
41
  "open" => Open,
34
42
  "report" => Report,
43
+ "status" => Status,
44
+ "history" => History,
35
45
  "uncovered" => Uncovered,
46
+ "tests" => Tests,
47
+ "affected" => Affected,
36
48
  "merge" => Merge,
37
49
  "diff" => Diff,
50
+ "patch" => Patch,
51
+ "ratchet" => Ratchet,
52
+ "dead-code" => DeadCode,
38
53
  "serve" => Serve,
39
- "clean" => Clean
54
+ "watch" => Watch,
55
+ "badge" => Badge,
56
+ "clean" => Clean,
57
+ "completions" => Completions
40
58
  }.freeze
41
59
 
42
- module_function
60
+ extend self
43
61
 
44
- # Resolved once per process. Walks up from cwd looking for a
45
- # `.simplecov`; if present, the file is loaded with
46
- # `SimpleCov.start` neutered so it can't trigger coverage tracking
47
- # or an at_exit hook just because we asked it for a config value.
62
+ # Resolved once per process, by walking up from cwd for a `.simplecov` and
63
+ # loading it with `SimpleCov.start` neutered so it can't trigger tracking
64
+ # just because we asked it for a config value.
48
65
  def coverage_dir
49
66
  @coverage_dir ||= Dotfile.coverage_dir
50
67
  end
@@ -53,14 +70,10 @@ module SimpleCov
53
70
  File.join(coverage_dir, "coverage.json")
54
71
  end
55
72
 
56
- # Resolve "should this subcommand colorize?" once per invocation.
57
- # `--no-color` (opts[:no_color]) is the per-invocation kill-switch;
58
- # otherwise we defer to `SimpleCov::Color.enabled?`, which honors
59
- # `NO_COLOR` / `FORCE_COLOR` and falls back to `stream.tty?`.
60
73
  def color_enabled?(opts, stream)
61
74
  return false if opts[:no_color]
62
75
 
63
- SimpleCov::Color.enabled?(stream)
76
+ Color.enabled?(stream)
64
77
  end
65
78
 
66
79
  def default_report
@@ -71,93 +84,31 @@ module SimpleCov
71
84
  File.join(coverage_dir, ".resultset.json")
72
85
  end
73
86
 
74
- # Returns a process exit status (0 on success, non-zero on error).
75
87
  def run(argv, stdout: $stdout, stderr: $stderr)
76
88
  command, *rest = argv
77
89
  handler = COMMANDS[command]
78
90
  return dispatch(handler, command, rest, stdout: stdout, stderr: stderr) if handler
79
91
  return stdout.puts(usage) || 0 if [nil, "help", "--help", "-h"].include?(command)
92
+ return stdout.puts("simplecov #{VERSION}") || 0 if %w[version --version -v].include?(command)
80
93
 
81
94
  stderr.puts("simplecov: unknown command #{command.inspect}", usage)
82
95
  1
83
96
  end
84
97
 
85
- # One rescue covers every subcommand's OptionParser, so a typo'd
86
- # flag or a malformed typed argument becomes a one-line error and
87
- # exit status 1 instead of an unhandled-exception backtrace.
98
+ # One rescue covers every subcommand's OptionParser, so a typo'd flag becomes
99
+ # a one-line error and exit status 1 instead of a backtrace.
88
100
  def dispatch(handler, command, rest, stdout:, stderr:)
89
101
  handler.run(rest, stdout: stdout, stderr: stderr)
102
+ rescue CommandHelpers::HelpRequested
103
+ stdout.puts(Usage.for(self, command))
104
+ 0
90
105
  rescue OptionParser::ParseError => e
91
- stderr.puts("simplecov #{command}: #{e.message} (run `simplecov help` for usage)")
106
+ stderr.puts("simplecov #{command}: #{e} (run `simplecov help` for usage)")
92
107
  1
93
108
  end
94
109
 
95
110
  def usage
96
- <<~USAGE
97
- Usage: simplecov <command> [options]
98
-
99
- Commands:
100
- run <command...> Execute <command> with simplecov pre-loaded
101
- (so a coverage report is generated even
102
- when the project has no test_helper hook)
103
- coverage <path> Print coverage stats for the given file
104
- report Print the overall summary and group totals
105
- uncovered List the lowest-coverage files
106
- merge <files...> Merge multiple .resultset.json files
107
- diff <baseline> Show per-file coverage delta vs baseline
108
- open Open the HTML report in the default browser
109
- serve Serve the coverage report over HTTP
110
- clean Remove the coverage report directory
111
- help Show this message
112
-
113
- Default paths follow SimpleCov.coverage_dir from a project's
114
- `.simplecov` when one is present (#{coverage_dir} for this run).
115
-
116
- coverage / report / uncovered / diff options:
117
- --input PATH Read from PATH instead of #{default_input}
118
- --no-color Disable colorized percentages
119
- (also honors NO_COLOR / FORCE_COLOR env)
120
-
121
- coverage options:
122
- --json Print the file's JSON entry verbatim
123
-
124
- report options:
125
- --json Emit totals and group sections as JSON
126
-
127
- uncovered options:
128
- --threshold N Only show files below N% coverage
129
- --top N Show at most N files (default: 10)
130
- --criterion C line, branch, or method (default: line)
131
- --json Emit results as a JSON array (for CI)
132
-
133
- merge options:
134
- --output PATH Write merged resultset to PATH
135
- (default: #{default_resultset})
136
- --honor-timeout Drop entries older than merge_timeout
137
- --dry-run Print what would be written without
138
- actually writing
139
- -q, --quiet Suppress the success status line
140
-
141
- diff options:
142
- --fail-on-drop Exit non-zero when any file's coverage
143
- dropped vs the baseline (deleted
144
- files don't count as drops)
145
- --json Emit results as a JSON array (for CI)
146
- --threshold N Only show files whose absolute delta
147
- in any criterion is at least N%
148
-
149
- open options:
150
- --report PATH Open PATH instead of #{default_report}
151
-
152
- serve options:
153
- --port N Bind to port N (default: random open port)
154
- --host HOST Bind to HOST (default: 127.0.0.1)
155
-
156
- clean options:
157
- --dry-run Print what would be removed without
158
- deleting anything
159
- -q, --quiet Suppress status lines
160
- USAGE
111
+ Usage.text(self)
161
112
  end
162
113
  end
163
114
  end