simplecov 1.1.1 → 1.2.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/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 +135 -0
- data/lib/simplecov/cli/patch.rb +169 -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 +37 -0
- data/lib/simplecov/cli/uncovered.rb +70 -32
- data/lib/simplecov/cli/usage.rb +152 -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 +6 -10
- 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 +38 -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 +157 -64
- 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.rb +46 -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 +1 -12
- data/lib/simplecov/profiles/root_filter.rb +0 -4
- data/lib/simplecov/profiles/strict.rb +9 -19
- 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 +68 -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 +74 -119
- 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 +6 -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 +19 -28
- data/lib/simplecov/source_file/source_loader.rb +45 -35
- data/lib/simplecov/source_file/statistics.rb +10 -9
- data/lib/simplecov/source_file.rb +21 -51
- data/lib/simplecov/static_coverage_extractor/condition_folding.rb +63 -114
- data/lib/simplecov/static_coverage_extractor/location_conventions.rb +76 -121
- data/lib/simplecov/static_coverage_extractor/method_collector.rb +11 -24
- 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 +47 -64
- data/lib/simplecov/static_coverage_extractor.rb +46 -62
- 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 +349 -0
- data/sig/simplecov.rbs +523 -595
- metadata +72 -5
|
@@ -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
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "command_helpers"
|
|
4
|
+
require_relative "coverage_file"
|
|
5
|
+
require_relative "../production/file_sink"
|
|
6
|
+
require_relative "dead_code/output"
|
|
7
|
+
|
|
8
|
+
module SimpleCov
|
|
9
|
+
module CLI
|
|
10
|
+
# `simplecov dead-code --production PATH` — cross the coverage a
|
|
11
|
+
# `SimpleCov::Production` sink accumulated with the test report, and
|
|
12
|
+
# answer the question neither answers alone. Per line:
|
|
13
|
+
#
|
|
14
|
+
# production | tests | meaning
|
|
15
|
+
# yes | yes | normal
|
|
16
|
+
# yes | no | untested code real users are running
|
|
17
|
+
# no | yes | possibly dead, a deletion candidate
|
|
18
|
+
# no | no | dead
|
|
19
|
+
#
|
|
20
|
+
# The default view prints the bottom two rows (the deletion candidates);
|
|
21
|
+
# `--untested-in-production` prints the second (the highest-value place to
|
|
22
|
+
# add a test). Coverage over a long enough window is far better evidence
|
|
23
|
+
# for deleting Ruby than any static analysis of it, which is why the header
|
|
24
|
+
# names the window the production data spans.
|
|
25
|
+
module DeadCode
|
|
26
|
+
extend CommandHelpers
|
|
27
|
+
|
|
28
|
+
extend self
|
|
29
|
+
|
|
30
|
+
# The module-name derivation would say "deadcode"; the command is
|
|
31
|
+
# hyphenated.
|
|
32
|
+
def command_name
|
|
33
|
+
"dead-code"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def run(args, stdout:, stderr:, **)
|
|
37
|
+
opts = parse(args, stderr) or return 1
|
|
38
|
+
coverage = CoverageFile.load_coverage(opts.fetch(:input), command: command_name, stderr: stderr) or return 1
|
|
39
|
+
production = load_production(opts.fetch(:production), stderr) or return 1
|
|
40
|
+
|
|
41
|
+
matrix = cross(coverage, production.fetch("coverage"))
|
|
42
|
+
Output.emit(stdout, opts, matrix, production)
|
|
43
|
+
0
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def parse(args, stderr)
|
|
47
|
+
# `production` needs no default: it is filled in below from the project's
|
|
48
|
+
# configured store whenever the flag went unused.
|
|
49
|
+
opts, rest = parse_common(args, untested: false) do |parser, options|
|
|
50
|
+
parser.on("--production PATH") { |v| options[:production] = v }
|
|
51
|
+
parser.on("--untested-in-production") { options[:untested] = true }
|
|
52
|
+
end
|
|
53
|
+
return error_nil(stderr, "unexpected argument #{rest.first.inspect}") unless rest.empty?
|
|
54
|
+
|
|
55
|
+
# The project's configured store fills in for the flag, the way ratchet
|
|
56
|
+
# reads `baseline_file`, so the DSL stays the single source of truth for
|
|
57
|
+
# where production coverage lives.
|
|
58
|
+
opts[:production] ||= Dotfile.production_coverage
|
|
59
|
+
opts.fetch(:production) ? opts : missing_production(stderr)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def missing_production(stderr)
|
|
63
|
+
error_nil(stderr,
|
|
64
|
+
"missing --production PATH (the file a SimpleCov::Production sink wrote, " \
|
|
65
|
+
"or configure SimpleCov.production_coverage in .simplecov)")
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def load_production(path, stderr)
|
|
69
|
+
Production::FileSink.read(path)
|
|
70
|
+
rescue Errno::ENOENT
|
|
71
|
+
error_nil(stderr, "#{path} not found")
|
|
72
|
+
rescue SystemCallError, Production::Error => e
|
|
73
|
+
# The exception stands for its own message in `error`'s interpolation.
|
|
74
|
+
error_nil(stderr, e)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Classifies every relevant line of the report against the production set,
|
|
78
|
+
# and sweeps in production files the report never tracked (nothing tested
|
|
79
|
+
# them, so every recorded line is untested-in-production). Lines the
|
|
80
|
+
# report deems irrelevant or deliberately ignored stay out of every
|
|
81
|
+
# bucket.
|
|
82
|
+
def cross(coverage, production_coverage)
|
|
83
|
+
matrix = {dead: {}, possibly_dead: {}, untested_in_production: {}, entire: Set.new} #: Hash[Symbol, untyped]
|
|
84
|
+
coverage.each do |file, entry|
|
|
85
|
+
lines = entry["lines"]
|
|
86
|
+
classify_file(matrix, file, lines, production_coverage[file] || []) if lines.instance_of?(Array)
|
|
87
|
+
end
|
|
88
|
+
# Files go in unsorted: both the text sections and the JSON payload sort
|
|
89
|
+
# what they print.
|
|
90
|
+
(production_coverage.keys - coverage.keys).each do |file|
|
|
91
|
+
matrix.fetch(:untested_in_production)[file] = production_coverage.fetch(file).sort
|
|
92
|
+
end
|
|
93
|
+
matrix
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# mutant:disable -- the production lines are held as a Set for the cost of
|
|
97
|
+
# asking, and a list answers `include?` the same way, so the conversion
|
|
98
|
+
# has no witness at any call site it could live at.
|
|
99
|
+
def classify_file(matrix, file, lines, production_lines)
|
|
100
|
+
production = production_lines.to_set
|
|
101
|
+
buckets = {dead: [], possibly_dead: [], untested_in_production: []} #: Hash[Symbol, Array[Integer]]
|
|
102
|
+
relevant = 0
|
|
103
|
+
lines.each_with_index do |value, index|
|
|
104
|
+
next unless value.is_a?(Numeric)
|
|
105
|
+
|
|
106
|
+
relevant += 1
|
|
107
|
+
bucket = classify_line(value.positive?, production.include?(index + 1))
|
|
108
|
+
buckets.fetch(bucket) << (index + 1) if bucket
|
|
109
|
+
end
|
|
110
|
+
record(matrix, file, buckets, relevant)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# The matrix's four cells; the yes/yes cell is the one nothing needs
|
|
114
|
+
# reporting about.
|
|
115
|
+
def classify_line(tested, in_production)
|
|
116
|
+
return :untested_in_production if in_production && !tested
|
|
117
|
+
return nil if in_production
|
|
118
|
+
|
|
119
|
+
tested ? :possibly_dead : :dead
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# mutant:disable -- the count of relevant lines is checked before a file is
|
|
123
|
+
# called entirely dead, but a file with no relevant lines lands in no
|
|
124
|
+
# bucket, so the mark is never read back. The guard says what is meant
|
|
125
|
+
# rather than what can be seen.
|
|
126
|
+
def record(matrix, file, buckets, relevant)
|
|
127
|
+
buckets.each { |bucket, found| matrix.fetch(bucket)[file] = found unless found.empty? }
|
|
128
|
+
# Counted down to nothing rather than compared for equality: two counts
|
|
129
|
+
# that are equal are equal through every spelling of the comparison.
|
|
130
|
+
unhit = buckets.fetch(:dead).size + buckets.fetch(:possibly_dead).size
|
|
131
|
+
return unless relevant.positive? && (relevant - unhit).zero?
|
|
132
|
+
|
|
133
|
+
# Every relevant line unhit in production: the deletion candidate is the
|
|
134
|
+
# whole file.
|
|
135
|
+
matrix.fetch(:entire) << file
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|