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
|
@@ -4,19 +4,21 @@ require "optparse"
|
|
|
4
4
|
|
|
5
5
|
module SimpleCov
|
|
6
6
|
module CLI
|
|
7
|
-
# Plumbing the subcommands repeat, which their modules `extend`: the
|
|
8
|
-
# standard option trio the read-only commands share, the parse
|
|
9
|
-
# scaffold around it, the one-line error helper (prefixed with the
|
|
10
|
-
# command's own name), and the human-readable stats row `coverage`
|
|
11
|
-
# and `report` both print.
|
|
12
7
|
module CommandHelpers
|
|
13
8
|
STATS_ROW_FORMAT = " %<label>-7s %<pct>s (%<covered>d / %<total>d)"
|
|
14
9
|
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
#
|
|
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
|
+
|
|
18
16
|
def command_name
|
|
19
|
-
|
|
17
|
+
name.split("::").last.downcase
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def one?(count)
|
|
21
|
+
(count - 1).zero?
|
|
20
22
|
end
|
|
21
23
|
|
|
22
24
|
def error(stderr, message)
|
|
@@ -24,23 +26,57 @@ module SimpleCov
|
|
|
24
26
|
1
|
|
25
27
|
end
|
|
26
28
|
|
|
27
|
-
#
|
|
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
|
+
|
|
29
47
|
def common_options(parser, opts)
|
|
30
48
|
parser.on("--input PATH") { |v| opts[:input] = v }
|
|
31
|
-
parser.on("--json")
|
|
32
|
-
parser.on("--no-color")
|
|
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 }
|
|
33
73
|
end
|
|
34
74
|
|
|
35
|
-
# The parse scaffold every read-only subcommand repeats: seed the
|
|
36
|
-
# shared defaults (plus the command's own), wire up the common
|
|
37
|
-
# option trio, yield the parser for command-specific flags, and
|
|
38
|
-
# return the parsed opts alongside the positional arguments.
|
|
39
75
|
def parse_common(args, defaults = {})
|
|
40
|
-
opts = {input:
|
|
76
|
+
opts = {input: CLI.default_input, json: false, no_color: false} #: Hash[Symbol, untyped]
|
|
41
77
|
opts.merge!(defaults)
|
|
42
78
|
rest =
|
|
43
|
-
|
|
79
|
+
build_parser do |parser|
|
|
44
80
|
common_options(parser, opts)
|
|
45
81
|
yield parser, opts if block_given?
|
|
46
82
|
end.parse(args)
|
|
@@ -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
|
|
@@ -6,12 +6,10 @@ require_relative "command_helpers"
|
|
|
6
6
|
|
|
7
7
|
module SimpleCov
|
|
8
8
|
module CLI
|
|
9
|
-
# `simplecov coverage <path>` — print per-criterion stats for one
|
|
10
|
-
# file from a JSONFormatter coverage.json.
|
|
11
9
|
module Coverage
|
|
12
10
|
extend CommandHelpers
|
|
13
11
|
|
|
14
|
-
|
|
12
|
+
extend self
|
|
15
13
|
|
|
16
14
|
def run(args, stdout:, stderr:)
|
|
17
15
|
opts = parse(args, stderr: stderr)
|
|
@@ -26,44 +24,43 @@ module SimpleCov
|
|
|
26
24
|
|
|
27
25
|
def parse(args, stderr:)
|
|
28
26
|
opts, rest = parse_common(args)
|
|
29
|
-
|
|
27
|
+
if rest.empty?
|
|
28
|
+
stderr.puts("simplecov coverage: missing file argument")
|
|
29
|
+
return nil
|
|
30
|
+
end
|
|
30
31
|
|
|
31
32
|
opts[:path] = rest.first
|
|
32
33
|
opts
|
|
33
34
|
end
|
|
34
35
|
|
|
35
36
|
def locate_match(opts, stderr)
|
|
36
|
-
coverage = CoverageFile.load_coverage(opts
|
|
37
|
+
coverage = CoverageFile.load_coverage(opts.fetch(:input), command: "coverage", stderr: stderr)
|
|
37
38
|
return unless coverage
|
|
38
39
|
|
|
39
|
-
match = lookup(coverage, opts
|
|
40
|
-
if match.nil?
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
40
|
+
match = lookup(coverage, opts.fetch(:path))
|
|
41
|
+
return report_not_found(stderr, coverage, opts) if match.nil?
|
|
42
|
+
return match if match.last.instance_of?(Hash)
|
|
43
|
+
|
|
44
|
+
CoverageFile.report_invalid(stderr, "coverage", opts.fetch(:input),
|
|
45
|
+
"entry for #{opts.fetch(:path)} must be an object")
|
|
46
|
+
end
|
|
46
47
|
|
|
47
|
-
|
|
48
|
+
def report_not_found(stderr, coverage, opts)
|
|
49
|
+
message = CoverageFile.not_found_message(coverage, opts.fetch(:path), opts.fetch(:input))
|
|
50
|
+
stderr.puts("simplecov coverage: #{message}")
|
|
48
51
|
end
|
|
49
52
|
|
|
50
|
-
# Match either the absolute path, the literal string passed, or
|
|
51
|
-
# any coverage entry whose absolute filename ends with "/<path>".
|
|
52
|
-
# That covers the three natural ways a user types a path: relative
|
|
53
|
-
# to project root ("app/foo.rb"), absolute, or basename-only.
|
|
54
53
|
def lookup(coverage_hash, path)
|
|
55
|
-
|
|
56
|
-
suffix = "/#{path}"
|
|
57
|
-
coverage_hash.find { |fname, _| fname == absolute || fname == path || fname.end_with?(suffix) }
|
|
54
|
+
CoverageFile.lookup(coverage_hash, path)
|
|
58
55
|
end
|
|
59
56
|
|
|
60
57
|
def emit(match, opts, stdout)
|
|
61
58
|
filename, payload = match
|
|
62
|
-
if opts
|
|
59
|
+
if opts.fetch(:json)
|
|
63
60
|
entry = {filename => payload} #: Hash[untyped, untyped]
|
|
64
61
|
stdout.puts(JSON.pretty_generate(entry))
|
|
65
62
|
else
|
|
66
|
-
print_human(filename, payload, stdout,
|
|
63
|
+
print_human(filename, payload, stdout, CLI.color_enabled?(opts, stdout))
|
|
67
64
|
end
|
|
68
65
|
end
|
|
69
66
|
|
|
@@ -73,13 +70,13 @@ module SimpleCov
|
|
|
73
70
|
end
|
|
74
71
|
|
|
75
72
|
def emit_criterion(stdout, payload, criterion, color)
|
|
76
|
-
return unless payload.key?(criterion
|
|
73
|
+
return unless payload.key?(criterion.fetch(:percent))
|
|
77
74
|
|
|
78
|
-
pct = payload
|
|
79
|
-
stdout.puts(stats_row(criterion
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
75
|
+
pct = payload.fetch(criterion.fetch(:percent)).to_f
|
|
76
|
+
stdout.puts(stats_row(criterion.fetch(:label),
|
|
77
|
+
Color.colorize_percent(pct, enabled: color),
|
|
78
|
+
payload[criterion.fetch(:covered)],
|
|
79
|
+
payload[criterion.fetch(:total)]))
|
|
83
80
|
end
|
|
84
81
|
end
|
|
85
82
|
end
|
|
@@ -5,29 +5,76 @@ require_relative "../coverage_json"
|
|
|
5
5
|
module SimpleCov
|
|
6
6
|
module CLI
|
|
7
7
|
# Shared input boundary for commands that consume JSONFormatter's
|
|
8
|
-
# coverage.json. It validates only the stable outer shape so older schema
|
|
8
|
+
# coverage.json. It validates only the stable outer shape, so older schema
|
|
9
9
|
# versions remain readable.
|
|
10
10
|
module CoverageFile
|
|
11
|
-
# The coverage.json fields backing each criterion, keyed by the
|
|
12
|
-
# criterion's singular name. The one table the coverage, uncovered,
|
|
13
|
-
# and diff subcommands all read from, so a schema change lands in
|
|
14
|
-
# exactly one place.
|
|
15
11
|
CRITERIA = {
|
|
16
12
|
line: {label: "Line", percent: "lines_covered_percent",
|
|
17
|
-
covered: "covered_lines", total: "total_lines"},
|
|
13
|
+
covered: "covered_lines", missed: "missed_lines", total: "total_lines"},
|
|
18
14
|
branch: {label: "Branch", percent: "branches_covered_percent",
|
|
19
|
-
covered: "covered_branches", total: "total_branches"},
|
|
15
|
+
covered: "covered_branches", missed: "missed_branches", total: "total_branches"},
|
|
20
16
|
method: {label: "Method", percent: "methods_covered_percent",
|
|
21
|
-
covered: "covered_methods", total: "total_methods"}
|
|
17
|
+
covered: "covered_methods", missed: "missed_methods", total: "total_methods"}
|
|
22
18
|
}.freeze
|
|
23
19
|
|
|
24
|
-
|
|
20
|
+
extend self
|
|
21
|
+
|
|
22
|
+
# An exact match, the absolute path or the literal string passed, wins over
|
|
23
|
+
# a suffix match, so a nested "vendor/lib/foo.rb" can't shadow the real
|
|
24
|
+
# "lib/foo.rb" just by sitting earlier in the report. A subpath can end more
|
|
25
|
+
# than one key, and a collision is left as "not found" for the caller rather
|
|
26
|
+
# than scoring whichever key happens to sit first.
|
|
27
|
+
def lookup(coverage_hash, path)
|
|
28
|
+
absolute = File.expand_path(path)
|
|
29
|
+
return [absolute, coverage_hash.fetch(absolute)] if coverage_hash.key?(absolute)
|
|
30
|
+
return [path, coverage_hash.fetch(path)] if coverage_hash.key?(path)
|
|
31
|
+
|
|
32
|
+
matches = suffix_matches(coverage_hash, path)
|
|
33
|
+
matches.first if matches.one?
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# The suffix's leading slash keeps "foo.rb" from matching "barfoo.rb", and
|
|
37
|
+
# hoisting it keeps the scan from allocating a string per entry.
|
|
38
|
+
def suffix_matches(coverage_hash, path)
|
|
39
|
+
suffix = "/#{path}"
|
|
40
|
+
coverage_hash.select { |fname, _| fname.end_with?(suffix) }
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# The report's entries under exact, realpath-normalized keys, for
|
|
44
|
+
# programmatic resolution where a suffix fallback could bind a path to the
|
|
45
|
+
# wrong entry. The realpath step keeps a symlinked project root from
|
|
46
|
+
# splitting a resolved path apart from the report's keys.
|
|
47
|
+
def exact_index(coverage_hash)
|
|
48
|
+
index = {} #: Hash[String, untyped]
|
|
49
|
+
coverage_hash.each do |key, payload|
|
|
50
|
+
index[key] ||= payload
|
|
51
|
+
index[normalize(key)] ||= payload
|
|
52
|
+
end
|
|
53
|
+
index
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# A key whose file no longer exists keeps its literal spelling.
|
|
57
|
+
def normalize(key)
|
|
58
|
+
File.realdirpath(key)
|
|
59
|
+
rescue SystemCallError
|
|
60
|
+
key
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# An ambiguous subpath names its candidates: "no entry" would send the user
|
|
64
|
+
# hunting for a typo in a path that exists twice.
|
|
65
|
+
def not_found_message(coverage_hash, path, input)
|
|
66
|
+
matches = suffix_matches(coverage_hash, path).keys
|
|
67
|
+
return "no entry for #{path} in #{input}" if matches.size < 2
|
|
68
|
+
|
|
69
|
+
"#{path} matches #{matches.size} files in #{input}: #{matches.sort.join(", ")} " \
|
|
70
|
+
"(use a longer path to pick one)"
|
|
71
|
+
end
|
|
25
72
|
|
|
26
73
|
def load_document(path, command:, stderr:)
|
|
27
|
-
|
|
74
|
+
CoverageJSON.load(path)
|
|
28
75
|
rescue Errno::ENOENT
|
|
29
76
|
report_missing(stderr, command, path)
|
|
30
|
-
rescue
|
|
77
|
+
rescue CoverageJSON::Error => e
|
|
31
78
|
report_invalid(stderr, command, path, e.message)
|
|
32
79
|
rescue SystemCallError => e
|
|
33
80
|
report_unreadable(stderr, command, path, e.message)
|
|
@@ -47,18 +94,15 @@ module SimpleCov
|
|
|
47
94
|
def report_invalid(stderr, command, path, reason)
|
|
48
95
|
detail = reason.lines.first.to_s.strip
|
|
49
96
|
stderr.puts("simplecov #{command}: input file #{path.inspect} isn't valid JSON (#{detail})")
|
|
50
|
-
nil
|
|
51
97
|
end
|
|
52
98
|
|
|
53
99
|
def report_missing(stderr, command, path)
|
|
54
100
|
stderr.puts("simplecov #{command}: #{path} not found")
|
|
55
|
-
nil
|
|
56
101
|
end
|
|
57
102
|
|
|
58
103
|
def report_unreadable(stderr, command, path, reason)
|
|
59
104
|
detail = reason.lines.first.to_s.strip
|
|
60
105
|
stderr.puts("simplecov #{command}: cannot read #{path.inspect} (#{detail})")
|
|
61
|
-
nil
|
|
62
106
|
end
|
|
63
107
|
end
|
|
64
108
|
end
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../patch/output"
|
|
4
|
+
|
|
5
|
+
module SimpleCov
|
|
6
|
+
module CLI
|
|
7
|
+
module DeadCode
|
|
8
|
+
module Output
|
|
9
|
+
extend CommandHelpers
|
|
10
|
+
|
|
11
|
+
extend self
|
|
12
|
+
|
|
13
|
+
def emit(stdout, opts, matrix, production)
|
|
14
|
+
return stdout.puts(JSON.generate(json_payload(matrix, production))) if opts.fetch(:json)
|
|
15
|
+
|
|
16
|
+
stdout.puts("Production coverage: #{opts.fetch(:production)}#{window_suffix(production)}")
|
|
17
|
+
stdout.puts
|
|
18
|
+
last_seen = production.fetch("last_seen")
|
|
19
|
+
opts.fetch(:untested) ? emit_untested(stdout, matrix, last_seen) : emit_dead(stdout, matrix, last_seen)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def emit_dead(stdout, matrix, last_seen)
|
|
23
|
+
rows = section(stdout, "Dead code (not run in production, not covered by tests):", matrix, :dead,
|
|
24
|
+
last_seen)
|
|
25
|
+
rows += section(stdout, "Possibly dead (not run in production, covered only by tests):", matrix,
|
|
26
|
+
:possibly_dead, last_seen)
|
|
27
|
+
return stdout.puts("No dead code found.") if rows.zero?
|
|
28
|
+
|
|
29
|
+
dead = count(matrix, :dead)
|
|
30
|
+
possibly = count(matrix, :possibly_dead)
|
|
31
|
+
stdout.puts("#{pluralize(dead, "dead line")}, #{pluralize(possibly, "possibly dead line")}")
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def emit_untested(stdout, matrix, last_seen)
|
|
35
|
+
rows = section(stdout, "Untested code running in production:", matrix, :untested_in_production, last_seen)
|
|
36
|
+
return stdout.puts("No untested production code found.") if rows.zero?
|
|
37
|
+
|
|
38
|
+
stdout.puts("#{pluralize(count(matrix, :untested_in_production), "untested line")} running in production")
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def section(stdout, heading, matrix, bucket, last_seen)
|
|
42
|
+
rows = matrix.fetch(bucket).sort
|
|
43
|
+
return 0 if rows.empty?
|
|
44
|
+
|
|
45
|
+
stdout.puts(heading)
|
|
46
|
+
rows.each do |file, lines|
|
|
47
|
+
stdout.puts(" #{file}:#{Patch::Output.ranges(lines, ",")}#{markers(matrix, file, last_seen)}")
|
|
48
|
+
end
|
|
49
|
+
stdout.puts
|
|
50
|
+
rows.size
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# The row's parenthesized evidence. "entire file" says every relevant line
|
|
54
|
+
# skipped production; "last run" dates the store's last sighting of the file,
|
|
55
|
+
# which for a dead or possibly dead row means other lines of the file ran
|
|
56
|
+
# then, and its absence means the window never saw the file at all. Both can
|
|
57
|
+
# hold at once.
|
|
58
|
+
def markers(matrix, file, last_seen)
|
|
59
|
+
markers = [] #: Array[String]
|
|
60
|
+
markers << "entire file" if matrix.fetch(:entire).include?(file)
|
|
61
|
+
stamp = last_seen[file]
|
|
62
|
+
markers << "last run #{stamp[0, 10]}" if stamp.instance_of?(String)
|
|
63
|
+
" (#{markers.join(", ")})" unless markers.empty?
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def count(matrix, bucket)
|
|
67
|
+
matrix.fetch(bucket).sum { |_file, lines| lines.size }
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def pluralize(number, noun)
|
|
71
|
+
"#{number} #{noun}#{"s" unless one?(number)}"
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def window_suffix(production)
|
|
75
|
+
started = production["started_at"]
|
|
76
|
+
updated = production["updated_at"]
|
|
77
|
+
" (window #{started} to #{updated})" if started && updated
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def json_payload(matrix, production)
|
|
81
|
+
last_seen = production.fetch("last_seen")
|
|
82
|
+
payload = {window: {started_at: production["started_at"], updated_at: production["updated_at"]}}
|
|
83
|
+
%i[dead possibly_dead untested_in_production].each do |bucket|
|
|
84
|
+
payload[bucket] = matrix.fetch(bucket).sort.map { |file, lines| json_entry(file, lines, last_seen) }
|
|
85
|
+
end
|
|
86
|
+
payload
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# JSON carries the full stamp where the text views print its date, and omits
|
|
90
|
+
# the key for a file the window never saw.
|
|
91
|
+
def json_entry(file, lines, last_seen)
|
|
92
|
+
entry = {file: file, lines: lines} #: Hash[Symbol, untyped]
|
|
93
|
+
stamp = last_seen[file]
|
|
94
|
+
entry[:last_seen] = stamp if stamp.instance_of?(String)
|
|
95
|
+
entry
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|