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,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
|
|
10
|
-
#
|
|
11
|
-
#
|
|
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
|
-
|
|
21
|
+
extend self
|
|
19
22
|
|
|
20
23
|
def run(args, stdout:, stderr:, **)
|
|
21
24
|
opts = parse(args)
|
|
22
|
-
|
|
23
|
-
return
|
|
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
|
|
36
|
+
coverage = CoverageFile.load_coverage(opts.fetch(:input), command: "uncovered", stderr: stderr)
|
|
30
37
|
return 1 unless coverage
|
|
31
38
|
|
|
32
|
-
|
|
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
|
-
|
|
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
|
|
50
|
-
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
|
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,
|
|
83
|
-
rows = coverage_hash.filter_map { |fname, payload| row_for(fname, payload,
|
|
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,
|
|
88
|
-
return unless payload.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
99
|
-
|
|
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
|