simplecov 0.22.0 → 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/LICENSE +1 -1
- data/README.md +173 -875
- data/exe/simplecov +8 -0
- data/lib/minitest/simplecov_plugin.rb +18 -7
- data/lib/simplecov/atomic_file.rb +67 -0
- data/lib/simplecov/autostart.rb +9 -0
- 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 +86 -0
- data/lib/simplecov/cli/command_helpers.rb +91 -0
- data/lib/simplecov/cli/completions/scripts.rb +114 -0
- data/lib/simplecov/cli/completions.rb +83 -0
- data/lib/simplecov/cli/coverage.rb +83 -0
- data/lib/simplecov/cli/coverage_file.rb +109 -0
- 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 +115 -0
- data/lib/simplecov/cli/dotfile.rb +156 -0
- 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 +115 -0
- data/lib/simplecov/cli/open.rb +45 -0
- 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 +91 -0
- data/lib/simplecov/cli/run.rb +35 -0
- data/lib/simplecov/cli/serve/report_preparer.rb +30 -0
- data/lib/simplecov/cli/serve/static_file_handler.rb +128 -0
- data/lib/simplecov/cli/serve.rb +88 -0
- 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 +127 -0
- 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 +114 -0
- data/lib/simplecov/color.rb +65 -0
- data/lib/simplecov/combine/branches_combiner.rb +44 -21
- data/lib/simplecov/combine/coverage_accumulator.rb +175 -0
- data/lib/simplecov/combine/identity_interner.rb +27 -0
- data/lib/simplecov/combine/interned_counts.rb +28 -0
- data/lib/simplecov/combine/lines_combiner.rb +35 -29
- data/lib/simplecov/combine/methods_combiner.rb +48 -0
- data/lib/simplecov/combine/results_combiner.rb +9 -49
- data/lib/simplecov/combine.rb +0 -24
- data/lib/simplecov/command_guesser.rb +99 -42
- data/lib/simplecov/configuration/baseline.rb +31 -0
- data/lib/simplecov/configuration/coverage.rb +184 -0
- data/lib/simplecov/configuration/coverage_criteria.rb +143 -0
- data/lib/simplecov/configuration/deprecations.rb +21 -0
- data/lib/simplecov/configuration/eval_coverage.rb +37 -0
- data/lib/simplecov/configuration/filters.rb +131 -0
- data/lib/simplecov/configuration/formatting.rb +129 -0
- data/lib/simplecov/configuration/groups.rb +48 -0
- data/lib/simplecov/configuration/history.rb +44 -0
- data/lib/simplecov/configuration/ignored_entries.rb +72 -0
- data/lib/simplecov/configuration/merging.rb +197 -0
- 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 +150 -0
- data/lib/simplecov/configuration/view_coverage.rb +37 -0
- data/lib/simplecov/configuration.rb +100 -443
- 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 +21 -0
- data/lib/simplecov/coverage_statistics.rb +24 -28
- data/lib/simplecov/coverage_violations.rb +254 -0
- data/lib/simplecov/current_run.rb +61 -0
- data/lib/simplecov/defaults.rb +20 -28
- data/lib/simplecov/deprecation.rb +63 -0
- 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 +152 -0
- data/lib/simplecov/exit_codes/baseline_check.rb +46 -0
- data/lib/simplecov/exit_codes/check.rb +39 -0
- data/lib/simplecov/exit_codes/exit_code_handling.rb +24 -5
- data/lib/simplecov/exit_codes/maximum_coverage_drop_check.rb +17 -67
- 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 +27 -0
- data/lib/simplecov/exit_codes/minimum_coverage_by_file_check.rb +20 -38
- data/lib/simplecov/exit_codes/minimum_coverage_by_group_check.rb +28 -0
- data/lib/simplecov/exit_codes/minimum_overall_coverage_check.rb +37 -34
- data/lib/simplecov/exit_codes.rb +26 -0
- data/lib/simplecov/exit_handling.rb +163 -0
- data/lib/simplecov/file_list.rb +63 -48
- data/lib/simplecov/filter.rb +98 -42
- data/lib/simplecov/formatter/base.rb +100 -0
- data/lib/simplecov/formatter/baseline_formatter.rb +98 -0
- data/lib/simplecov/formatter/coverage_json_writer.rb +95 -0
- data/lib/simplecov/formatter/html_formatter/public/index.html +93 -0
- data/lib/simplecov/formatter/html_formatter/viewer_data_validator.rb +153 -0
- data/lib/simplecov/formatter/html_formatter.rb +88 -0
- data/lib/simplecov/formatter/json_formatter/errors_formatter.rb +122 -0
- data/lib/simplecov/formatter/json_formatter/production_section_formatter.rb +48 -0
- data/lib/simplecov/formatter/json_formatter/result_hash_formatter.rb +132 -0
- data/lib/simplecov/formatter/json_formatter/source_file_formatter.rb +97 -0
- data/lib/simplecov/formatter/json_formatter.rb +40 -0
- data/lib/simplecov/formatter/multi_formatter.rb +8 -10
- data/lib/simplecov/formatter/simple_formatter.rb +12 -15
- data/lib/simplecov/formatter.rb +21 -1
- data/lib/simplecov/group_names.rb +33 -0
- data/lib/simplecov/history.rb +115 -0
- data/lib/simplecov/last_run.rb +16 -5
- data/lib/simplecov/lines_classifier.rb +67 -19
- data/lib/simplecov/load_global_config.rb +12 -4
- data/lib/simplecov/parallel_adapters/base.rb +50 -0
- data/lib/simplecov/parallel_adapters/generic.rb +33 -0
- data/lib/simplecov/parallel_adapters/parallel_tests.rb +72 -0
- data/lib/simplecov/parallel_adapters.rb +54 -0
- data/lib/simplecov/parallel_coordination.rb +124 -0
- data/lib/simplecov/parallel_result_merger/worker_payload.rb +33 -0
- data/lib/simplecov/parallel_result_merger.rb +171 -0
- data/lib/simplecov/process.rb +34 -12
- 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/bundler_filter.rb +1 -1
- data/lib/simplecov/profiles/hidden_filter.rb +1 -1
- data/lib/simplecov/profiles/rails.rb +13 -10
- data/lib/simplecov/profiles/root_filter.rb +2 -5
- data/lib/simplecov/profiles/strict.rb +22 -0
- data/lib/simplecov/profiles/test_frameworks.rb +1 -4
- data/lib/simplecov/profiles.rb +26 -19
- data/lib/simplecov/report_deferral.rb +45 -0
- data/lib/simplecov/report_stamp.rb +25 -0
- data/lib/simplecov/result/filter_config.rb +19 -0
- data/lib/simplecov/result/missing_source_files_reporter.rb +48 -0
- data/lib/simplecov/result/serialization.rb +43 -0
- data/lib/simplecov/result/source_file_builder.rb +46 -0
- data/lib/simplecov/result.rb +123 -47
- data/lib/simplecov/result_adapter.rb +130 -11
- data/lib/simplecov/result_merger/contexts.rb +22 -0
- data/lib/simplecov/result_merger/legacy_format_adapter.rb +24 -0
- data/lib/simplecov/result_merger/resultset_file.rb +67 -0
- data/lib/simplecov/result_merger/resultset_run_identity.rb +65 -0
- data/lib/simplecov/result_merger/resultset_store.rb +62 -0
- data/lib/simplecov/result_merger/unloaded_files.rb +95 -0
- data/lib/simplecov/result_merger.rb +120 -124
- data/lib/simplecov/result_processing.rb +188 -0
- data/lib/simplecov/run_identity.rb +75 -0
- data/lib/simplecov/simulate_coverage.rb +72 -17
- data/lib/simplecov/source_file/branch.rb +8 -35
- data/lib/simplecov/source_file/branch_builder.rb +95 -0
- data/lib/simplecov/source_file/builder_context.rb +28 -0
- data/lib/simplecov/source_file/line.rb +17 -32
- data/lib/simplecov/source_file/line_builder.rb +37 -0
- data/lib/simplecov/source_file/method.rb +54 -0
- data/lib/simplecov/source_file/method_builder.rb +51 -0
- data/lib/simplecov/source_file/ruby_data_parser.rb +103 -0
- data/lib/simplecov/source_file/skip_chunks.rb +72 -0
- data/lib/simplecov/source_file/source_loader.rb +89 -0
- data/lib/simplecov/source_file/statistics.rb +76 -0
- data/lib/simplecov/source_file.rb +64 -260
- data/lib/simplecov/static_coverage_extractor/condition_folding.rb +155 -0
- data/lib/simplecov/static_coverage_extractor/location_conventions.rb +191 -0
- data/lib/simplecov/static_coverage_extractor/method_collector.rb +45 -0
- data/lib/simplecov/static_coverage_extractor/prism_compat.rb +46 -0
- data/lib/simplecov/static_coverage_extractor/value_position.rb +61 -0
- data/lib/simplecov/static_coverage_extractor/visitor.rb +163 -0
- data/lib/simplecov/static_coverage_extractor.rb +78 -0
- 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 +62 -0
- data/lib/simplecov/useless_results_remover.rb +17 -7
- 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 +5 -0
- data/lib/simplecov.rb +143 -394
- data/lib/simplecov_json_formatter.rb +5 -0
- data/man/simplecov.1 +354 -0
- data/sig/simplecov.rbs +1710 -0
- metadata +183 -61
- data/CHANGELOG.md +0 -191
- data/doc/alternate-formatters.md +0 -71
- data/doc/commercial-services.md +0 -25
- data/doc/editor-integration.md +0 -18
- data/lib/simplecov/combine/files_combiner.rb +0 -24
- data/lib/simplecov/default_formatter.rb +0 -20
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest"
|
|
4
|
+
require "json"
|
|
5
|
+
|
|
6
|
+
module SimpleCov
|
|
7
|
+
module CLI
|
|
8
|
+
# The `--annotate KIND` output shared by `uncovered` and `patch`: each command
|
|
9
|
+
# reduces its answer to diagnostics (a path, a contiguous missed line range,
|
|
10
|
+
# and the criterion that missed), and one emitter per CI host renders them in
|
|
11
|
+
# that host's native inline-annotation channel, so a gap surfaces on the diff
|
|
12
|
+
# with no upload step and no extra gem.
|
|
13
|
+
module Annotations
|
|
14
|
+
KINDS = %w[github gitlab rdjson azure teamcity buildkite].freeze
|
|
15
|
+
|
|
16
|
+
MESSAGES = {
|
|
17
|
+
line: "Not covered by tests",
|
|
18
|
+
branch: "Branch not covered by tests",
|
|
19
|
+
method: "Method not covered by tests"
|
|
20
|
+
}.freeze
|
|
21
|
+
|
|
22
|
+
DESCRIPTIONS = {
|
|
23
|
+
line: "Lines the tests never executed",
|
|
24
|
+
branch: "Branches the tests never took",
|
|
25
|
+
method: "Methods the tests never called"
|
|
26
|
+
}.freeze
|
|
27
|
+
|
|
28
|
+
SOURCE = {"name" => "simplecov", "url" => "https://github.com/simplecov-ruby/simplecov"}.freeze
|
|
29
|
+
|
|
30
|
+
# TeamCity's service message grammar and Azure's logging command grammar
|
|
31
|
+
# each reserve a few characters in attribute values.
|
|
32
|
+
TEAMCITY_ESCAPES = {"|" => "||", "'" => "|'", "[" => "|[", "]" => "|]", "\n" => "|n", "\r" => "|r"}.freeze
|
|
33
|
+
AZURE_ESCAPES = {"%" => "%AZP25", ";" => "%3B", "]" => "%5D", "\n" => "%0A", "\r" => "%0D"}.freeze
|
|
34
|
+
|
|
35
|
+
extend self
|
|
36
|
+
|
|
37
|
+
def issue(opts)
|
|
38
|
+
kind = opts.fetch(:annotate)
|
|
39
|
+
return nil unless kind
|
|
40
|
+
return "unknown --annotate #{kind.inspect} (expected #{expected_kinds})" unless KINDS.include?(kind)
|
|
41
|
+
|
|
42
|
+
"cannot combine --annotate with --json" if opts.fetch(:json)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def expected_kinds
|
|
46
|
+
*rest, last = KINDS
|
|
47
|
+
"#{rest.join(", ")}, or #{last}"
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def diagnostics(path, missed, criterion)
|
|
51
|
+
missed.slice_when { |previous, current| current > previous + 1 }.map do |run|
|
|
52
|
+
{path: path, first: run.first, last: run.last, criterion: criterion, message: MESSAGES.fetch(criterion)}
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def emit(stdout, kind, diagnostics)
|
|
57
|
+
case kind
|
|
58
|
+
when "github" then github(stdout, diagnostics)
|
|
59
|
+
when "gitlab" then stdout.puts(JSON.pretty_generate(gitlab(diagnostics)))
|
|
60
|
+
when "rdjson" then stdout.puts(JSON.pretty_generate(rdjson(diagnostics)))
|
|
61
|
+
when "azure" then azure(stdout, diagnostics)
|
|
62
|
+
when "teamcity" then teamcity(stdout, diagnostics)
|
|
63
|
+
else buildkite(stdout, diagnostics)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def github(stdout, diagnostics)
|
|
68
|
+
diagnostics.each do |diagnostic|
|
|
69
|
+
stdout.puts("::warning file=#{diagnostic.fetch(:path)},line=#{diagnostic.fetch(:first)}," \
|
|
70
|
+
"endLine=#{diagnostic.fetch(:last)}::#{diagnostic.fetch(:message)}")
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# GitLab's Code Quality report, a subset of the Code Climate spec, read from
|
|
75
|
+
# a `codequality` artifact for merge request annotations.
|
|
76
|
+
def gitlab(diagnostics)
|
|
77
|
+
diagnostics.map do |diagnostic|
|
|
78
|
+
{
|
|
79
|
+
"description" => diagnostic.fetch(:message),
|
|
80
|
+
"check_name" => "coverage",
|
|
81
|
+
"fingerprint" => fingerprint(diagnostic),
|
|
82
|
+
"severity" => "minor",
|
|
83
|
+
"location" => {
|
|
84
|
+
"path" => diagnostic.fetch(:path),
|
|
85
|
+
"lines" => {"begin" => diagnostic.fetch(:first), "end" => diagnostic.fetch(:last)}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def fingerprint(diagnostic)
|
|
92
|
+
Digest::SHA256.hexdigest("#{diagnostic.fetch(:path)}:#{diagnostic.fetch(:first)}-#{diagnostic.fetch(:last)}:" \
|
|
93
|
+
"#{diagnostic.fetch(:criterion)}")
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# reviewdog's Diagnostic Format, which reviewdog posts to whichever host it
|
|
97
|
+
# runs under.
|
|
98
|
+
def rdjson(diagnostics)
|
|
99
|
+
{"source" => SOURCE, "severity" => "WARNING", "diagnostics" => diagnostics.map { |d| rdjson_diagnostic(d) }}
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def rdjson_diagnostic(diagnostic)
|
|
103
|
+
{
|
|
104
|
+
"message" => diagnostic.fetch(:message),
|
|
105
|
+
"location" => {
|
|
106
|
+
"path" => diagnostic.fetch(:path),
|
|
107
|
+
"range" => {"start" => {"line" => diagnostic.fetch(:first)}, "end" => {"line" => diagnostic.fetch(:last)}}
|
|
108
|
+
},
|
|
109
|
+
"severity" => "WARNING",
|
|
110
|
+
"code" => {"value" => diagnostic.fetch(:criterion).to_s}
|
|
111
|
+
}
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Azure Pipelines logging commands carry one line number, so a range is
|
|
115
|
+
# spelled out in the message.
|
|
116
|
+
def azure(stdout, diagnostics)
|
|
117
|
+
diagnostics.each do |diagnostic|
|
|
118
|
+
stdout.puts("##vso[task.logissue type=warning;sourcepath=#{escape(diagnostic.fetch(:path), AZURE_ESCAPES)};" \
|
|
119
|
+
"linenumber=#{diagnostic.fetch(:first)}]#{described(diagnostic)}")
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# TeamCity's code inspection service messages: each criterion is one
|
|
124
|
+
# inspection type, declared once before its first inspection.
|
|
125
|
+
def teamcity(stdout, diagnostics)
|
|
126
|
+
diagnostics.group_by { |diagnostic| diagnostic.fetch(:criterion) }.each do |criterion, group|
|
|
127
|
+
id = "simplecov.#{criterion}"
|
|
128
|
+
stdout.puts(service_message("inspectionType", id: id, name: MESSAGES.fetch(criterion),
|
|
129
|
+
description: DESCRIPTIONS.fetch(criterion), category: "Code coverage"))
|
|
130
|
+
group.each do |diagnostic|
|
|
131
|
+
stdout.puts(service_message("inspection", typeId: id, message: described(diagnostic),
|
|
132
|
+
file: diagnostic.fetch(:path), line: diagnostic.fetch(:first), SEVERITY: "WARNING"))
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def service_message(name, attributes)
|
|
138
|
+
body = attributes.map { |key, value| "#{key}='#{escape(value.to_s, TEAMCITY_ESCAPES)}'" }.join(" ")
|
|
139
|
+
"##teamcity[#{name} #{body}]"
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Buildkite has no per-line channel; its annotations are Markdown, so this
|
|
143
|
+
# is the body for `buildkite-agent annotate`.
|
|
144
|
+
def buildkite(stdout, diagnostics)
|
|
145
|
+
sections = diagnostics.group_by { |diagnostic| diagnostic.fetch(:message) }.map do |message, group|
|
|
146
|
+
(["#### #{message}"] + group.map { |diagnostic| "- `#{diagnostic.fetch(:path)}:#{range(diagnostic)}`" }).join("\n")
|
|
147
|
+
end
|
|
148
|
+
stdout.puts(sections.join("\n\n")) unless sections.empty?
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def described(diagnostic)
|
|
152
|
+
return diagnostic.fetch(:message) if diagnostic.fetch(:first).equal?(diagnostic.fetch(:last))
|
|
153
|
+
|
|
154
|
+
"#{diagnostic.fetch(:message)} (lines #{range(diagnostic)})"
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def range(diagnostic)
|
|
158
|
+
[diagnostic.fetch(:first), diagnostic.fetch(:last)].uniq.join("-")
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def escape(text, escapes)
|
|
162
|
+
text.gsub(Regexp.union(escapes.keys), escapes)
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SimpleCov
|
|
4
|
+
module CLI
|
|
5
|
+
module Badge
|
|
6
|
+
# Renders the badge SVG in shields.io's flat style: 20px tall with 3px
|
|
7
|
+
# rounded corners, the label on #555 and the value on its ladder color, each
|
|
8
|
+
# text drawn twice for the drop shadow at 10x scale. `textLength` pins the
|
|
9
|
+
# layout, so the estimated segment widths never let glyphs spill.
|
|
10
|
+
module Svg
|
|
11
|
+
extend self
|
|
12
|
+
|
|
13
|
+
COLORS = [[90, "#4c1"], [80, "#97ca00"], [70, "#a4a61d"], [60, "#dfb317"], [50, "#fe7d37"]].freeze
|
|
14
|
+
|
|
15
|
+
def color(percent)
|
|
16
|
+
rung = COLORS.detect { |floor, _color| percent >= floor }
|
|
17
|
+
rung ? rung.fetch(1) : "#e05d44"
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def render(label:, percent:)
|
|
21
|
+
value = format("%.2f%%", percent)
|
|
22
|
+
document(label: escape(label), value: value, fill: color(percent),
|
|
23
|
+
geo: geometry(width(label), width(value)))
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Estimates 11px Verdana; `textLength` absorbs the error.
|
|
27
|
+
def width(text)
|
|
28
|
+
(7 * text.length) + 10
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# The scale(.1) trick draws text at 10x and shrinks it, so the text
|
|
32
|
+
# coordinates live in a 10x space: centers at 10 * (offset + width / 2) and
|
|
33
|
+
# textLength spanning the width less padding.
|
|
34
|
+
def geometry(label_width, value_width)
|
|
35
|
+
{label_width: label_width, value_width: value_width, total: label_width + value_width,
|
|
36
|
+
label_x: 5 * label_width, value_x: (10 * label_width) + (5 * value_width),
|
|
37
|
+
label_span: 10 * (label_width - 10), value_span: 10 * (value_width - 10)}
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def escape(text)
|
|
41
|
+
text.gsub(/[&<>"]/, "&" => "&", "<" => "<", ">" => ">", '"' => """)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def document(label:, value:, fill:, geo:)
|
|
45
|
+
<<~SVG
|
|
46
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="#{geo.fetch(:total)}" height="20" role="img" aria-label="#{label}: #{value}">
|
|
47
|
+
<title>#{label}: #{value}</title>
|
|
48
|
+
<linearGradient id="s" x2="0" y2="100%">
|
|
49
|
+
<stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
|
|
50
|
+
<stop offset="1" stop-opacity=".1"/>
|
|
51
|
+
</linearGradient>
|
|
52
|
+
<clipPath id="r"><rect width="#{geo.fetch(:total)}" height="20" rx="3" fill="#fff"/></clipPath>
|
|
53
|
+
<g clip-path="url(#r)">
|
|
54
|
+
<rect width="#{geo.fetch(:label_width)}" height="20" fill="#555"/>
|
|
55
|
+
<rect x="#{geo.fetch(:label_width)}" width="#{geo.fetch(:value_width)}" height="20" fill="#{fill}"/>
|
|
56
|
+
<rect width="#{geo.fetch(:total)}" height="20" fill="url(#s)"/>
|
|
57
|
+
</g>
|
|
58
|
+
<g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110">
|
|
59
|
+
<text aria-hidden="true" x="#{geo.fetch(:label_x)}" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="#{geo.fetch(:label_span)}">#{label}</text>
|
|
60
|
+
<text x="#{geo.fetch(:label_x)}" y="140" transform="scale(.1)" fill="#fff" textLength="#{geo.fetch(:label_span)}">#{label}</text>
|
|
61
|
+
<text aria-hidden="true" x="#{geo.fetch(:value_x)}" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="#{geo.fetch(:value_span)}">#{value}</text>
|
|
62
|
+
<text x="#{geo.fetch(:value_x)}" y="140" transform="scale(.1)" fill="#fff" textLength="#{geo.fetch(:value_span)}">#{value}</text>
|
|
63
|
+
</g>
|
|
64
|
+
</svg>
|
|
65
|
+
SVG
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "optparse"
|
|
4
|
+
require_relative "command_helpers"
|
|
5
|
+
require_relative "coverage_file"
|
|
6
|
+
require_relative "badge/svg"
|
|
7
|
+
|
|
8
|
+
module SimpleCov
|
|
9
|
+
module CLI
|
|
10
|
+
module Badge
|
|
11
|
+
extend CommandHelpers
|
|
12
|
+
|
|
13
|
+
extend self
|
|
14
|
+
|
|
15
|
+
TOTAL_KEYS = {line: "lines", branch: "branches", method: "methods"}.freeze
|
|
16
|
+
|
|
17
|
+
def run(args, stdout:, stderr:)
|
|
18
|
+
opts = parse(args)
|
|
19
|
+
key = TOTAL_KEYS[opts.fetch(:criterion)]
|
|
20
|
+
unless key
|
|
21
|
+
return error(stderr,
|
|
22
|
+
"unknown --criterion #{opts.fetch(:criterion).inspect} (expected line, branch, or method)")
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
percent = percent_for(opts, key, stderr)
|
|
26
|
+
return 1 unless percent
|
|
27
|
+
|
|
28
|
+
label = opts.fetch(:label) || "#{opts.fetch(:criterion)} coverage"
|
|
29
|
+
emit(Svg.render(label: label, percent: percent), opts, stdout)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def parse(args)
|
|
33
|
+
opts = {input: CLI.default_input, criterion: :line,
|
|
34
|
+
label: nil, output: nil} #: Hash[Symbol, untyped]
|
|
35
|
+
build_parser do |parser|
|
|
36
|
+
parser.on("--input PATH") { |v| opts[:input] = v }
|
|
37
|
+
parser.on("--output PATH") { |v| opts[:output] = v }
|
|
38
|
+
parser.on("--criterion C") { |v| opts[:criterion] = v.to_sym }
|
|
39
|
+
parser.on("--label TEXT") { |v| opts[:label] = v }
|
|
40
|
+
end.parse(args)
|
|
41
|
+
opts
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def percent_for(opts, key, stderr)
|
|
45
|
+
document = CoverageFile.load_document(opts.fetch(:input), command: "badge", stderr: stderr)
|
|
46
|
+
return nil unless document
|
|
47
|
+
|
|
48
|
+
totals = document["total"]
|
|
49
|
+
percent = totals.dig(key, "percent") if totals.is_a?(Hash)
|
|
50
|
+
return percent if percent.is_a?(Numeric)
|
|
51
|
+
|
|
52
|
+
error_nil(stderr, "no #{opts.fetch(:criterion)} totals in #{opts.fetch(:input)} " \
|
|
53
|
+
"(was the run measuring #{opts.fetch(:criterion)} coverage?)")
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def emit(svg, opts, stdout)
|
|
57
|
+
opts.fetch(:output) ? File.write(opts.fetch(:output), svg) : stdout.puts(svg)
|
|
58
|
+
0
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "optparse"
|
|
5
|
+
require_relative "command_helpers"
|
|
6
|
+
|
|
7
|
+
module SimpleCov
|
|
8
|
+
module CLI
|
|
9
|
+
# `simplecov clean [--dry-run]`: remove the coverage report directory.
|
|
10
|
+
# `--dry-run` prints what would be deleted without touching disk.
|
|
11
|
+
module Clean
|
|
12
|
+
extend CommandHelpers
|
|
13
|
+
|
|
14
|
+
extend self
|
|
15
|
+
|
|
16
|
+
def run(args, stdout:, stderr:, **)
|
|
17
|
+
opts = parse(args)
|
|
18
|
+
dir = CLI.coverage_dir
|
|
19
|
+
return error(stderr, "refusing to remove unsafe coverage directory #{dir.inspect}") unless safe_to_remove?(dir)
|
|
20
|
+
return announce(stdout, opts, "#{dir} doesn't exist; nothing to do") || 0 unless File.directory?(dir)
|
|
21
|
+
|
|
22
|
+
sweep(dir, opts, stdout)
|
|
23
|
+
0
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def sweep(dir, opts, stdout)
|
|
27
|
+
if opts.fetch(:dry_run)
|
|
28
|
+
announce(stdout, opts, "would remove #{dir} (#{entry_count(dir)} entries)")
|
|
29
|
+
else
|
|
30
|
+
FileUtils.rm_rf(dir)
|
|
31
|
+
announce(stdout, opts, "removed #{dir}")
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# FNM_DOTMATCH so the dotfiles rm_rf will actually delete count too. The
|
|
36
|
+
# glob still yields the directory's own "." entry, which rm_rf does not
|
|
37
|
+
# remove separately.
|
|
38
|
+
def entry_count(dir)
|
|
39
|
+
Dir.glob("#{dir}/**/*", File::FNM_DOTMATCH).count { |path| !path.end_with?("/.") }
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def announce(stdout, opts, message)
|
|
43
|
+
stdout.puts("simplecov #{command_name}: #{message}") unless opts.fetch(:quiet)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# A filesystem root is its own dirname, on every platform and every Windows
|
|
47
|
+
# drive. The descendant check only protects roots that happen to contain the
|
|
48
|
+
# cwd, which misses other drives, so roots are refused outright.
|
|
49
|
+
def safe_to_remove?(dir)
|
|
50
|
+
target = canonical_path(dir)
|
|
51
|
+
return false if File.dirname(target).eql?(target)
|
|
52
|
+
|
|
53
|
+
protected_paths.none? { |path| path.eql?(target) || descendant_of?(path, target) }
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# The working directory, which a coverage directory must not be or contain.
|
|
57
|
+
# The `.simplecov` project root needs no entry of its own: `Dotfile.find`
|
|
58
|
+
# walks up from the working directory, so any target containing that root
|
|
59
|
+
# contains the working directory too.
|
|
60
|
+
def protected_paths
|
|
61
|
+
[canonical_path(Dir.pwd)]
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Canonical paths only carry a trailing separator on a filesystem root, and
|
|
65
|
+
# roots are refused before this check runs.
|
|
66
|
+
def descendant_of?(path, possible_ancestor)
|
|
67
|
+
path.start_with?("#{possible_ancestor}#{File::SEPARATOR}")
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def canonical_path(path)
|
|
71
|
+
File.realpath(path)
|
|
72
|
+
rescue SystemCallError
|
|
73
|
+
File.expand_path(path)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def parse(args)
|
|
77
|
+
opts = {dry_run: false, quiet: false}
|
|
78
|
+
build_parser do |o|
|
|
79
|
+
o.on("--dry-run") { opts[:dry_run] = true }
|
|
80
|
+
quiet_option(o, opts)
|
|
81
|
+
end.parse(args)
|
|
82
|
+
opts
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "optparse"
|
|
4
|
+
|
|
5
|
+
module SimpleCov
|
|
6
|
+
module CLI
|
|
7
|
+
module CommandHelpers
|
|
8
|
+
STATS_ROW_FORMAT = " %<label>-7s %<pct>s (%<covered>d / %<total>d)"
|
|
9
|
+
|
|
10
|
+
# Raised by the shared --help switch; `CLI.dispatch` turns it into the
|
|
11
|
+
# command's own slice of the usage text. The explicit handler also overrides
|
|
12
|
+
# optparse's built-in --help, which would print a bare option summary and
|
|
13
|
+
# exit the process outright from inside the parser.
|
|
14
|
+
class HelpRequested < StandardError; end
|
|
15
|
+
|
|
16
|
+
def command_name
|
|
17
|
+
name.split("::").last.downcase
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def one?(count)
|
|
21
|
+
(count - 1).zero?
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def error(stderr, message)
|
|
25
|
+
stderr.puts("simplecov #{command_name}: #{message}")
|
|
26
|
+
1
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# `error` returns 1 for an exit status; query helpers need nil so their
|
|
30
|
+
# callers can tell "reported" from a real answer.
|
|
31
|
+
def error_nil(stderr, message)
|
|
32
|
+
error(stderr, message)
|
|
33
|
+
nil
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def recorded_contexts(document, opts, stderr)
|
|
37
|
+
contexts = document["contexts"]
|
|
38
|
+
if contexts.nil?
|
|
39
|
+
return error_nil(stderr, "no test contexts recorded in #{opts.fetch(:input)}. Enable `track_tests` in " \
|
|
40
|
+
"your `SimpleCov.start` block and rerun the suite to record them")
|
|
41
|
+
end
|
|
42
|
+
return contexts if contexts.instance_of?(Array) && contexts.all?(String)
|
|
43
|
+
|
|
44
|
+
CoverageFile.report_invalid(stderr, command_name, opts.fetch(:input), '"contexts" must be an array of strings')
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def common_options(parser, opts)
|
|
48
|
+
parser.on("--input PATH") { |v| opts[:input] = v }
|
|
49
|
+
parser.on("--json") { opts[:json] = true }
|
|
50
|
+
parser.on("--no-color") { opts[:no_color] = true }
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def build_parser
|
|
54
|
+
OptionParser.new do |parser|
|
|
55
|
+
yield parser if block_given?
|
|
56
|
+
on_help(parser)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# mutant:disable
|
|
61
|
+
# OptionParser answers `-q` as an unambiguous abbreviation of `--quiet`
|
|
62
|
+
# whether or not the short form is declared.
|
|
63
|
+
def quiet_option(parser, opts)
|
|
64
|
+
parser.on("-q", "--quiet") { opts[:quiet] = true }
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# mutant:disable — optparse matches an abbreviated `-h` against
|
|
68
|
+
# `--help` whether or not the short form is declared, so dropping
|
|
69
|
+
# it changes nothing an example could see. Declared anyway, so the
|
|
70
|
+
# flag appears in the usage text.
|
|
71
|
+
def on_help(parser)
|
|
72
|
+
parser.on("--help", "-h") { raise HelpRequested }
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def parse_common(args, defaults = {})
|
|
76
|
+
opts = {input: CLI.default_input, json: false, no_color: false} #: Hash[Symbol, untyped]
|
|
77
|
+
opts.merge!(defaults)
|
|
78
|
+
rest =
|
|
79
|
+
build_parser do |parser|
|
|
80
|
+
common_options(parser, opts)
|
|
81
|
+
yield parser, opts if block_given?
|
|
82
|
+
end.parse(args)
|
|
83
|
+
[opts, rest]
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def stats_row(label, percent_text, covered, total)
|
|
87
|
+
format(STATS_ROW_FORMAT, label: "#{label}:", pct: percent_text, covered: covered.to_i, total: total.to_i)
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SimpleCov
|
|
4
|
+
module CLI
|
|
5
|
+
module Completions
|
|
6
|
+
# The per-shell renderers. Each receives the same parsed usage data and
|
|
7
|
+
# returns the complete script text. The completions command's own shell-name
|
|
8
|
+
# argument is the one thing the usage tables don't carry, so each renderer
|
|
9
|
+
# wires it in by hand.
|
|
10
|
+
module Scripts
|
|
11
|
+
extend self
|
|
12
|
+
|
|
13
|
+
def fish(commands, options)
|
|
14
|
+
lines = ["# Completions for simplecov. Generated by `simplecov completions fish`."] #: Array[String]
|
|
15
|
+
commands.each do |name, desc|
|
|
16
|
+
lines << "complete -c simplecov -f -n __fish_use_subcommand -a #{name} -d #{fish_quote(desc)}"
|
|
17
|
+
end
|
|
18
|
+
options.each { |name, list| list.each { |option| lines << fish_option(name, option) } }
|
|
19
|
+
lines << "complete -c simplecov -f -n '__fish_seen_subcommand_from completions' -a 'fish bash zsh'"
|
|
20
|
+
lines.join("\n")
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def fish_option(name, option)
|
|
24
|
+
parts = ["complete -c simplecov -n '__fish_seen_subcommand_from #{name}'"] #: Array[String]
|
|
25
|
+
parts << "-s #{(_ = option.fetch(:short)).delete_prefix("-")}" if option.fetch(:short)
|
|
26
|
+
parts << "-l #{option.fetch(:long).delete_prefix("--")}"
|
|
27
|
+
parts << "-r" if option.fetch(:arg)
|
|
28
|
+
parts << "-d #{fish_quote(option.fetch(:desc))}"
|
|
29
|
+
parts.join(" ")
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def fish_quote(text)
|
|
33
|
+
"'#{text.gsub("\\") { "\\\\" }.gsub("'") { "\\'" }}'"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
BASH_TAIL = [
|
|
37
|
+
" esac",
|
|
38
|
+
' [[ "$cur" == -* ]] && COMPREPLY=( $(compgen -W "$opts" -- "$cur") )',
|
|
39
|
+
"}",
|
|
40
|
+
"complete -o default -F _simplecov simplecov"
|
|
41
|
+
].freeze
|
|
42
|
+
|
|
43
|
+
def bash(commands, options)
|
|
44
|
+
(bash_head(commands) + bash_cases(options) + BASH_TAIL).join("\n")
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def bash_head(commands)
|
|
48
|
+
[
|
|
49
|
+
"# Completions for simplecov. Generated by `simplecov completions bash`.",
|
|
50
|
+
"_simplecov() {",
|
|
51
|
+
' local cur="${COMP_WORDS[COMP_CWORD]}" opts=""',
|
|
52
|
+
' if [ "$COMP_CWORD" -eq 1 ]; then',
|
|
53
|
+
" COMPREPLY=( $(compgen -W \"#{commands.collect(&:first).join(" ")}\" -- \"$cur\") ); return",
|
|
54
|
+
" fi",
|
|
55
|
+
' case "${COMP_WORDS[1]}" in'
|
|
56
|
+
]
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def bash_cases(options)
|
|
60
|
+
shells = ' completions) COMPREPLY=( $(compgen -W "fish bash zsh -h --help" -- "$cur") ); return;;'
|
|
61
|
+
cases = [shells] #: Array[String]
|
|
62
|
+
options.each do |name, list|
|
|
63
|
+
next if name.eql?("completions")
|
|
64
|
+
|
|
65
|
+
words = list.flat_map { |option| [option.fetch(:short), option.fetch(:long)] }.compact.join(" ")
|
|
66
|
+
cases << " #{name}) opts=\"#{words}\";;"
|
|
67
|
+
end
|
|
68
|
+
cases
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
ZSH_MIDDLE = [
|
|
72
|
+
" )",
|
|
73
|
+
" if (( CURRENT == 2 )); then",
|
|
74
|
+
" _describe -t commands 'simplecov command' commands",
|
|
75
|
+
" return",
|
|
76
|
+
" fi",
|
|
77
|
+
' case "$words[2]" in'
|
|
78
|
+
].freeze
|
|
79
|
+
|
|
80
|
+
ZSH_TAIL = [" esac", "}", '_simplecov "$@"'].freeze
|
|
81
|
+
|
|
82
|
+
def zsh(commands, options)
|
|
83
|
+
head = ["#compdef simplecov", "# Completions for simplecov. Generated by `simplecov completions zsh`.",
|
|
84
|
+
"_simplecov() {", " local -a commands", " commands=("]
|
|
85
|
+
rows = commands.collect { |name, desc| " '#{name}:#{zsh_quote(desc)}'" }
|
|
86
|
+
(head + rows + ZSH_MIDDLE + zsh_cases(options) + ZSH_TAIL).join("\n")
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def zsh_cases(options)
|
|
90
|
+
options.collect do |name, list|
|
|
91
|
+
specs = +""
|
|
92
|
+
specs << "'1:shell:(fish bash zsh)' " if name.eql?("completions")
|
|
93
|
+
list.each { |option| zsh_specs(option).each { |spec| specs << spec << " " } }
|
|
94
|
+
" #{name}) _arguments #{specs.chop};;"
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# `'--input=[desc]'`: the `=` marks an argument-taking switch, which optparse
|
|
99
|
+
# accepts in that spelling too. Brackets would end the description early, so
|
|
100
|
+
# they are dropped from it.
|
|
101
|
+
def zsh_specs(option)
|
|
102
|
+
desc = "[#{zsh_quote(option.fetch(:desc).delete("[]"))}]"
|
|
103
|
+
specs = [] #: Array[String]
|
|
104
|
+
specs << "'#{option.fetch(:short)}#{desc}'" if option.fetch(:short)
|
|
105
|
+
specs << "'#{option.fetch(:long)}#{"=" if option.fetch(:arg)}#{desc}'"
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def zsh_quote(text)
|
|
109
|
+
text.gsub("'") { "'\\''" }
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "optparse"
|
|
4
|
+
require_relative "command_helpers"
|
|
5
|
+
require_relative "usage"
|
|
6
|
+
require_relative "completions/scripts"
|
|
7
|
+
|
|
8
|
+
module SimpleCov
|
|
9
|
+
module CLI
|
|
10
|
+
# `simplecov completions <shell>`: completion scripts for fish, bash, and
|
|
11
|
+
# zsh, generated from the usage document rather than a hand-kept table, so a
|
|
12
|
+
# new command or switch shows up in completions the moment it is documented.
|
|
13
|
+
module Completions
|
|
14
|
+
extend CommandHelpers
|
|
15
|
+
|
|
16
|
+
extend self
|
|
17
|
+
|
|
18
|
+
SHELLS = %w[fish bash zsh].freeze
|
|
19
|
+
|
|
20
|
+
# " -q, --quiet Suppress...": optional short, long, optional
|
|
21
|
+
# argument placeholder, then the description after the column gap. A single
|
|
22
|
+
# space never separates the switch from its description, so one non-space
|
|
23
|
+
# token before a two-space run can only be an argument.
|
|
24
|
+
OPTION_ROW = /\A\s*(?:(-\w), )?(--[\w-]+)(?: (\S+))?\s{2,}(\S.*)\z/
|
|
25
|
+
COMMAND_ROW = /\A(\S+)(?: \S+)*?\s{2,}(\S.*)\z/
|
|
26
|
+
|
|
27
|
+
def run(args, stdout:, stderr:)
|
|
28
|
+
shell, = build_parser.parse(args)
|
|
29
|
+
return error(stderr, "missing shell (expected fish, bash, or zsh)") unless shell
|
|
30
|
+
unless SHELLS.include?(shell)
|
|
31
|
+
return error(stderr, "unknown shell #{shell.inspect} (expected fish, bash, or zsh)")
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
stdout.puts(script_for(shell))
|
|
35
|
+
0
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def script_for(shell)
|
|
39
|
+
case shell
|
|
40
|
+
when "fish" then Scripts.fish(commands, options_by_command)
|
|
41
|
+
when "bash" then Scripts.bash(commands, options_by_command)
|
|
42
|
+
else Scripts.zsh(commands, options_by_command)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def commands
|
|
47
|
+
Usage.text(CLI).split("\n\n").fetch(1).lines.filter_map do |row|
|
|
48
|
+
match = row.strip.match(COMMAND_ROW)
|
|
49
|
+
match && [(_ = match[1]), (_ = match[2])]
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def options_by_command
|
|
54
|
+
commands.filter_map do |name, _desc|
|
|
55
|
+
list = options_for(name)
|
|
56
|
+
[name, list] unless list.empty?
|
|
57
|
+
end.to_h
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# `run` takes no options of its own, since everything after it belongs to the
|
|
61
|
+
# command being run, so it must not even offer --help.
|
|
62
|
+
def options_for(command)
|
|
63
|
+
return [] if command.eql?("run")
|
|
64
|
+
|
|
65
|
+
sections = Usage.text(CLI).split("\n\n").select { |section| Usage.section_for?(section, command) }
|
|
66
|
+
options = sections.flat_map { |section| section_options(section) }
|
|
67
|
+
options << {short: "-h", long: "--help", arg: nil, desc: "Show this command's usage"}
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Rows that don't parse as options are continuations of the previous row's
|
|
71
|
+
# description, wrapped for the terminal.
|
|
72
|
+
def section_options(section)
|
|
73
|
+
section.lines.each_with_object([]) do |line, options|
|
|
74
|
+
if (match = line.chomp.match(OPTION_ROW))
|
|
75
|
+
options << {short: match[1], long: match[2], arg: match[3], desc: (_ = match[4])}
|
|
76
|
+
elsif (previous = options.last)
|
|
77
|
+
previous[:desc] = "#{previous.fetch(:desc)} #{line.strip}"
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|