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.
- checksums.yaml +4 -4
- data/README.md +148 -5
- data/exe/simplecov +0 -3
- data/lib/minitest/simplecov_plugin.rb +12 -9
- data/lib/simplecov/atomic_file.rb +16 -19
- data/lib/simplecov/autostart.rb +5 -7
- data/lib/simplecov/baseline/parser.rb +58 -0
- data/lib/simplecov/baseline.rb +170 -0
- data/lib/simplecov/cli/affected/changed_files.rb +57 -0
- data/lib/simplecov/cli/affected/selection.rb +117 -0
- data/lib/simplecov/cli/affected.rb +148 -0
- data/lib/simplecov/cli/annotations.rb +166 -0
- data/lib/simplecov/cli/badge/svg.rb +70 -0
- data/lib/simplecov/cli/badge.rb +62 -0
- data/lib/simplecov/cli/clean.rb +23 -24
- data/lib/simplecov/cli/command_helpers.rb +55 -19
- data/lib/simplecov/cli/completions/scripts.rb +114 -0
- data/lib/simplecov/cli/completions.rb +83 -0
- data/lib/simplecov/cli/coverage.rb +25 -28
- data/lib/simplecov/cli/coverage_file.rb +58 -14
- data/lib/simplecov/cli/dead_code/output.rb +100 -0
- data/lib/simplecov/cli/dead_code.rb +139 -0
- data/lib/simplecov/cli/diff/output.rb +45 -0
- data/lib/simplecov/cli/diff.rb +35 -67
- data/lib/simplecov/cli/dotfile.rb +83 -38
- data/lib/simplecov/cli/git.rb +41 -0
- data/lib/simplecov/cli/history/output.rb +135 -0
- data/lib/simplecov/cli/history.rb +67 -0
- data/lib/simplecov/cli/merge.rb +34 -39
- data/lib/simplecov/cli/open.rb +12 -15
- data/lib/simplecov/cli/patch/changed_lines.rb +149 -0
- data/lib/simplecov/cli/patch/output.rb +151 -0
- data/lib/simplecov/cli/patch.rb +175 -0
- data/lib/simplecov/cli/ratchet/output.rb +44 -0
- data/lib/simplecov/cli/ratchet.rb +83 -0
- data/lib/simplecov/cli/report.rb +18 -24
- data/lib/simplecov/cli/run.rb +8 -9
- data/lib/simplecov/cli/serve/report_preparer.rb +7 -6
- data/lib/simplecov/cli/serve/static_file_handler.rb +62 -56
- data/lib/simplecov/cli/serve.rb +27 -24
- data/lib/simplecov/cli/show/annotator.rb +87 -0
- data/lib/simplecov/cli/show/sweep.rb +36 -0
- data/lib/simplecov/cli/show.rb +133 -0
- data/lib/simplecov/cli/status/facts.rb +87 -0
- data/lib/simplecov/cli/status.rb +93 -0
- data/lib/simplecov/cli/tests/redundancy.rb +78 -0
- data/lib/simplecov/cli/tests.rb +148 -0
- data/lib/simplecov/cli/uncovered/misses.rb +33 -0
- data/lib/simplecov/cli/uncovered.rb +58 -34
- data/lib/simplecov/cli/usage.rb +153 -0
- data/lib/simplecov/cli/watch/live_report.rb +73 -0
- data/lib/simplecov/cli/watch/narrator.rb +55 -0
- data/lib/simplecov/cli/watch/poller.rb +45 -0
- data/lib/simplecov/cli/watch/session.rb +134 -0
- data/lib/simplecov/cli/watch/test_plan.rb +43 -0
- data/lib/simplecov/cli/watch.rb +85 -0
- data/lib/simplecov/cli.rb +43 -92
- data/lib/simplecov/color.rb +19 -37
- data/lib/simplecov/combine/branches_combiner.rb +7 -31
- data/lib/simplecov/combine/coverage_accumulator.rb +49 -142
- data/lib/simplecov/combine/identity_interner.rb +10 -13
- data/lib/simplecov/combine/interned_counts.rb +7 -9
- data/lib/simplecov/combine/lines_combiner.rb +20 -44
- data/lib/simplecov/combine/methods_combiner.rb +9 -35
- data/lib/simplecov/combine/results_combiner.rb +4 -21
- data/lib/simplecov/combine.rb +0 -6
- data/lib/simplecov/command_guesser.rb +20 -34
- data/lib/simplecov/configuration/baseline.rb +31 -0
- data/lib/simplecov/configuration/coverage.rb +91 -79
- data/lib/simplecov/configuration/coverage_criteria.rb +43 -54
- data/lib/simplecov/configuration/deprecations.rb +21 -0
- data/lib/simplecov/configuration/eval_coverage.rb +7 -11
- data/lib/simplecov/configuration/filters.rb +40 -73
- data/lib/simplecov/configuration/formatting.rb +74 -83
- data/lib/simplecov/configuration/groups.rb +19 -13
- data/lib/simplecov/configuration/history.rb +44 -0
- data/lib/simplecov/configuration/ignored_entries.rb +31 -22
- data/lib/simplecov/configuration/merging.rb +94 -62
- data/lib/simplecov/configuration/missed_caps.rb +51 -0
- data/lib/simplecov/configuration/production.rb +23 -0
- data/lib/simplecov/configuration/test_tracking.rb +48 -0
- data/lib/simplecov/configuration/thresholds.rb +33 -62
- data/lib/simplecov/configuration/view_coverage.rb +37 -0
- data/lib/simplecov/configuration.rb +70 -76
- data/lib/simplecov/configuration_error.rb +5 -0
- data/lib/simplecov/context_map/union.rb +81 -0
- data/lib/simplecov/context_map.rb +187 -0
- data/lib/simplecov/coverage_json.rb +2 -5
- data/lib/simplecov/coverage_statistics.rb +14 -24
- data/lib/simplecov/coverage_violations.rb +160 -65
- data/lib/simplecov/current_run.rb +61 -0
- data/lib/simplecov/defaults.rb +4 -23
- data/lib/simplecov/deprecation.rb +45 -27
- data/lib/simplecov/directive/erb.rb +44 -0
- data/lib/simplecov/directive/haml.rb +33 -0
- data/lib/simplecov/directive/indented_template.rb +37 -0
- data/lib/simplecov/directive/slim.rb +35 -0
- data/lib/simplecov/directive/template.rb +29 -0
- data/lib/simplecov/directive.rb +47 -62
- data/lib/simplecov/exit_codes/baseline_check.rb +46 -0
- data/lib/simplecov/exit_codes/check.rb +17 -11
- data/lib/simplecov/exit_codes/exit_code_handling.rb +21 -8
- data/lib/simplecov/exit_codes/maximum_coverage_drop_check.rb +7 -10
- data/lib/simplecov/exit_codes/maximum_missed_check.rb +26 -0
- data/lib/simplecov/exit_codes/maximum_missed_per_file_check.rb +34 -0
- data/lib/simplecov/exit_codes/maximum_overall_coverage_check.rb +7 -12
- data/lib/simplecov/exit_codes/minimum_coverage_by_file_check.rb +11 -10
- data/lib/simplecov/exit_codes/minimum_coverage_by_group_check.rb +7 -9
- data/lib/simplecov/exit_codes/minimum_overall_coverage_check.rb +16 -17
- data/lib/simplecov/exit_codes.rb +16 -10
- data/lib/simplecov/exit_handling.rb +94 -59
- data/lib/simplecov/file_list.rb +20 -51
- data/lib/simplecov/filter.rb +43 -66
- data/lib/simplecov/formatter/base.rb +37 -55
- data/lib/simplecov/formatter/baseline_formatter.rb +98 -0
- data/lib/simplecov/formatter/coverage_json_writer.rb +29 -31
- data/lib/simplecov/formatter/html_formatter/public/index.html +15 -9
- data/lib/simplecov/formatter/html_formatter/viewer_data_validator.rb +74 -17
- data/lib/simplecov/formatter/html_formatter.rb +24 -41
- data/lib/simplecov/formatter/json_formatter/errors_formatter.rb +50 -16
- data/lib/simplecov/formatter/json_formatter/production_section_formatter.rb +48 -0
- data/lib/simplecov/formatter/json_formatter/result_hash_formatter.rb +43 -29
- data/lib/simplecov/formatter/json_formatter/source_file_formatter.rb +14 -10
- data/lib/simplecov/formatter/json_formatter.rb +7 -13
- data/lib/simplecov/formatter/multi_formatter.rb +5 -11
- data/lib/simplecov/formatter/simple_formatter.rb +2 -8
- data/lib/simplecov/formatter.rb +14 -10
- data/lib/simplecov/group_names.rb +13 -12
- data/lib/simplecov/history.rb +115 -0
- data/lib/simplecov/last_run.rb +6 -9
- data/lib/simplecov/lines_classifier.rb +31 -16
- data/lib/simplecov/load_global_config.rb +7 -7
- data/lib/simplecov/parallel_adapters/base.rb +13 -41
- data/lib/simplecov/parallel_adapters/generic.rb +7 -19
- data/lib/simplecov/parallel_adapters/parallel_tests.rb +21 -42
- data/lib/simplecov/parallel_adapters.rb +24 -55
- data/lib/simplecov/parallel_coordination.rb +39 -49
- data/lib/simplecov/parallel_result_merger/worker_payload.rb +33 -0
- data/lib/simplecov/parallel_result_merger.rb +67 -126
- data/lib/simplecov/process.rb +14 -32
- data/lib/simplecov/production/error.rb +11 -0
- data/lib/simplecov/production/file_sink.rb +130 -0
- data/lib/simplecov/production.rb +249 -0
- data/lib/simplecov/profiles/rails.rb +4 -15
- data/lib/simplecov/profiles/root_filter.rb +0 -4
- data/lib/simplecov/profiles/strict.rb +11 -21
- data/lib/simplecov/profiles.rb +11 -33
- data/lib/simplecov/report_deferral.rb +11 -15
- data/lib/simplecov/report_stamp.rb +6 -9
- data/lib/simplecov/result/filter_config.rb +19 -0
- data/lib/simplecov/result/missing_source_files_reporter.rb +8 -9
- data/lib/simplecov/result/serialization.rb +43 -0
- data/lib/simplecov/result/source_file_builder.rb +7 -12
- data/lib/simplecov/result.rb +74 -117
- data/lib/simplecov/result_adapter.rb +56 -68
- data/lib/simplecov/result_merger/contexts.rb +22 -0
- data/lib/simplecov/result_merger/legacy_format_adapter.rb +4 -8
- data/lib/simplecov/result_merger/resultset_file.rb +20 -27
- data/lib/simplecov/result_merger/resultset_run_identity.rb +23 -25
- data/lib/simplecov/result_merger/resultset_store.rb +23 -14
- data/lib/simplecov/result_merger/unloaded_files.rb +24 -32
- data/lib/simplecov/result_merger.rb +59 -58
- data/lib/simplecov/result_processing.rb +92 -122
- data/lib/simplecov/run_identity.rb +16 -18
- data/lib/simplecov/simulate_coverage.rb +34 -50
- data/lib/simplecov/source_file/branch.rb +8 -33
- data/lib/simplecov/source_file/branch_builder.rb +28 -47
- data/lib/simplecov/source_file/builder_context.rb +13 -13
- data/lib/simplecov/source_file/line.rb +13 -33
- data/lib/simplecov/source_file/line_builder.rb +9 -15
- data/lib/simplecov/source_file/method.rb +3 -7
- data/lib/simplecov/source_file/method_builder.rb +6 -15
- data/lib/simplecov/source_file/ruby_data_parser.rb +59 -66
- data/lib/simplecov/source_file/skip_chunks.rb +29 -29
- data/lib/simplecov/source_file/source_loader.rb +45 -35
- data/lib/simplecov/source_file/statistics.rb +20 -9
- data/lib/simplecov/source_file.rb +21 -51
- data/lib/simplecov/static_coverage_extractor/condition_folding.rb +55 -139
- data/lib/simplecov/static_coverage_extractor/location_conventions.rb +76 -121
- data/lib/simplecov/static_coverage_extractor/method_collector.rb +9 -26
- data/lib/simplecov/static_coverage_extractor/prism_compat.rb +15 -24
- data/lib/simplecov/static_coverage_extractor/value_position.rb +25 -38
- data/lib/simplecov/static_coverage_extractor/visitor.rb +45 -65
- data/lib/simplecov/static_coverage_extractor.rb +43 -75
- data/lib/simplecov/test_tracker/accessors.rb +34 -0
- data/lib/simplecov/test_tracker/constant_watch.rb +49 -0
- data/lib/simplecov/test_tracker/delta.rb +69 -0
- data/lib/simplecov/test_tracker/framework_hooks.rb +130 -0
- data/lib/simplecov/test_tracker.rb +162 -0
- data/lib/simplecov/unloaded_file_injector.rb +18 -31
- data/lib/simplecov/useless_results_remover.rb +7 -6
- data/lib/simplecov/version.rb +1 -1
- data/lib/simplecov/view_coverage/template_compiler.rb +83 -0
- data/lib/simplecov/view_coverage.rb +55 -0
- data/lib/simplecov-html.rb +1 -0
- data/lib/simplecov.rb +57 -113
- data/lib/simplecov_json_formatter.rb +1 -0
- data/man/simplecov.1 +354 -0
- data/sig/simplecov.rbs +528 -595
- 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.
|
|
19
|
-
#
|
|
20
|
-
#
|
|
21
|
-
#
|
|
22
|
-
#
|
|
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
|
-
"
|
|
54
|
+
"watch" => Watch,
|
|
55
|
+
"badge" => Badge,
|
|
56
|
+
"clean" => Clean,
|
|
57
|
+
"completions" => Completions
|
|
40
58
|
}.freeze
|
|
41
59
|
|
|
42
|
-
|
|
60
|
+
extend self
|
|
43
61
|
|
|
44
|
-
# Resolved once per process
|
|
45
|
-
#
|
|
46
|
-
#
|
|
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
|
-
|
|
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
|
-
#
|
|
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
|
|
106
|
+
stderr.puts("simplecov #{command}: #{e} (run `simplecov help` for usage)")
|
|
92
107
|
1
|
|
93
108
|
end
|
|
94
109
|
|
|
95
110
|
def usage
|
|
96
|
-
|
|
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
|