simplecov 1.1.1 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +148 -5
- data/exe/simplecov +0 -3
- data/lib/minitest/simplecov_plugin.rb +12 -9
- data/lib/simplecov/atomic_file.rb +16 -19
- data/lib/simplecov/autostart.rb +5 -7
- data/lib/simplecov/baseline/parser.rb +58 -0
- data/lib/simplecov/baseline.rb +170 -0
- data/lib/simplecov/cli/affected/changed_files.rb +57 -0
- data/lib/simplecov/cli/affected/selection.rb +117 -0
- data/lib/simplecov/cli/affected.rb +148 -0
- data/lib/simplecov/cli/annotations.rb +166 -0
- data/lib/simplecov/cli/badge/svg.rb +70 -0
- data/lib/simplecov/cli/badge.rb +62 -0
- data/lib/simplecov/cli/clean.rb +23 -24
- data/lib/simplecov/cli/command_helpers.rb +55 -19
- data/lib/simplecov/cli/completions/scripts.rb +114 -0
- data/lib/simplecov/cli/completions.rb +83 -0
- data/lib/simplecov/cli/coverage.rb +25 -28
- data/lib/simplecov/cli/coverage_file.rb +58 -14
- data/lib/simplecov/cli/dead_code/output.rb +100 -0
- data/lib/simplecov/cli/dead_code.rb +139 -0
- data/lib/simplecov/cli/diff/output.rb +45 -0
- data/lib/simplecov/cli/diff.rb +35 -67
- data/lib/simplecov/cli/dotfile.rb +83 -38
- data/lib/simplecov/cli/git.rb +41 -0
- data/lib/simplecov/cli/history/output.rb +135 -0
- data/lib/simplecov/cli/history.rb +67 -0
- data/lib/simplecov/cli/merge.rb +34 -39
- data/lib/simplecov/cli/open.rb +12 -15
- data/lib/simplecov/cli/patch/changed_lines.rb +149 -0
- data/lib/simplecov/cli/patch/output.rb +151 -0
- data/lib/simplecov/cli/patch.rb +175 -0
- data/lib/simplecov/cli/ratchet/output.rb +44 -0
- data/lib/simplecov/cli/ratchet.rb +83 -0
- data/lib/simplecov/cli/report.rb +18 -24
- data/lib/simplecov/cli/run.rb +8 -9
- data/lib/simplecov/cli/serve/report_preparer.rb +7 -6
- data/lib/simplecov/cli/serve/static_file_handler.rb +62 -56
- data/lib/simplecov/cli/serve.rb +27 -24
- data/lib/simplecov/cli/show/annotator.rb +87 -0
- data/lib/simplecov/cli/show/sweep.rb +36 -0
- data/lib/simplecov/cli/show.rb +133 -0
- data/lib/simplecov/cli/status/facts.rb +87 -0
- data/lib/simplecov/cli/status.rb +93 -0
- data/lib/simplecov/cli/tests/redundancy.rb +78 -0
- data/lib/simplecov/cli/tests.rb +148 -0
- data/lib/simplecov/cli/uncovered/misses.rb +33 -0
- data/lib/simplecov/cli/uncovered.rb +58 -34
- data/lib/simplecov/cli/usage.rb +153 -0
- data/lib/simplecov/cli/watch/live_report.rb +73 -0
- data/lib/simplecov/cli/watch/narrator.rb +55 -0
- data/lib/simplecov/cli/watch/poller.rb +45 -0
- data/lib/simplecov/cli/watch/session.rb +134 -0
- data/lib/simplecov/cli/watch/test_plan.rb +43 -0
- data/lib/simplecov/cli/watch.rb +85 -0
- data/lib/simplecov/cli.rb +43 -92
- data/lib/simplecov/color.rb +19 -37
- data/lib/simplecov/combine/branches_combiner.rb +7 -31
- data/lib/simplecov/combine/coverage_accumulator.rb +49 -142
- data/lib/simplecov/combine/identity_interner.rb +10 -13
- data/lib/simplecov/combine/interned_counts.rb +7 -9
- data/lib/simplecov/combine/lines_combiner.rb +20 -44
- data/lib/simplecov/combine/methods_combiner.rb +9 -35
- data/lib/simplecov/combine/results_combiner.rb +4 -21
- data/lib/simplecov/combine.rb +0 -6
- data/lib/simplecov/command_guesser.rb +20 -34
- data/lib/simplecov/configuration/baseline.rb +31 -0
- data/lib/simplecov/configuration/coverage.rb +91 -79
- data/lib/simplecov/configuration/coverage_criteria.rb +43 -54
- data/lib/simplecov/configuration/deprecations.rb +21 -0
- data/lib/simplecov/configuration/eval_coverage.rb +7 -11
- data/lib/simplecov/configuration/filters.rb +40 -73
- data/lib/simplecov/configuration/formatting.rb +74 -83
- data/lib/simplecov/configuration/groups.rb +19 -13
- data/lib/simplecov/configuration/history.rb +44 -0
- data/lib/simplecov/configuration/ignored_entries.rb +31 -22
- data/lib/simplecov/configuration/merging.rb +94 -62
- data/lib/simplecov/configuration/missed_caps.rb +51 -0
- data/lib/simplecov/configuration/production.rb +23 -0
- data/lib/simplecov/configuration/test_tracking.rb +48 -0
- data/lib/simplecov/configuration/thresholds.rb +33 -62
- data/lib/simplecov/configuration/view_coverage.rb +37 -0
- data/lib/simplecov/configuration.rb +70 -76
- data/lib/simplecov/configuration_error.rb +5 -0
- data/lib/simplecov/context_map/union.rb +81 -0
- data/lib/simplecov/context_map.rb +187 -0
- data/lib/simplecov/coverage_json.rb +2 -5
- data/lib/simplecov/coverage_statistics.rb +14 -24
- data/lib/simplecov/coverage_violations.rb +160 -65
- data/lib/simplecov/current_run.rb +61 -0
- data/lib/simplecov/defaults.rb +4 -23
- data/lib/simplecov/deprecation.rb +45 -27
- data/lib/simplecov/directive/erb.rb +44 -0
- data/lib/simplecov/directive/haml.rb +33 -0
- data/lib/simplecov/directive/indented_template.rb +37 -0
- data/lib/simplecov/directive/slim.rb +35 -0
- data/lib/simplecov/directive/template.rb +29 -0
- data/lib/simplecov/directive.rb +47 -62
- data/lib/simplecov/exit_codes/baseline_check.rb +46 -0
- data/lib/simplecov/exit_codes/check.rb +17 -11
- data/lib/simplecov/exit_codes/exit_code_handling.rb +21 -8
- data/lib/simplecov/exit_codes/maximum_coverage_drop_check.rb +7 -10
- data/lib/simplecov/exit_codes/maximum_missed_check.rb +26 -0
- data/lib/simplecov/exit_codes/maximum_missed_per_file_check.rb +34 -0
- data/lib/simplecov/exit_codes/maximum_overall_coverage_check.rb +7 -12
- data/lib/simplecov/exit_codes/minimum_coverage_by_file_check.rb +11 -10
- data/lib/simplecov/exit_codes/minimum_coverage_by_group_check.rb +7 -9
- data/lib/simplecov/exit_codes/minimum_overall_coverage_check.rb +16 -17
- data/lib/simplecov/exit_codes.rb +16 -10
- data/lib/simplecov/exit_handling.rb +94 -59
- data/lib/simplecov/file_list.rb +20 -51
- data/lib/simplecov/filter.rb +43 -66
- data/lib/simplecov/formatter/base.rb +37 -55
- data/lib/simplecov/formatter/baseline_formatter.rb +98 -0
- data/lib/simplecov/formatter/coverage_json_writer.rb +29 -31
- data/lib/simplecov/formatter/html_formatter/public/index.html +15 -9
- data/lib/simplecov/formatter/html_formatter/viewer_data_validator.rb +74 -17
- data/lib/simplecov/formatter/html_formatter.rb +24 -41
- data/lib/simplecov/formatter/json_formatter/errors_formatter.rb +50 -16
- data/lib/simplecov/formatter/json_formatter/production_section_formatter.rb +48 -0
- data/lib/simplecov/formatter/json_formatter/result_hash_formatter.rb +43 -29
- data/lib/simplecov/formatter/json_formatter/source_file_formatter.rb +14 -10
- data/lib/simplecov/formatter/json_formatter.rb +7 -13
- data/lib/simplecov/formatter/multi_formatter.rb +5 -11
- data/lib/simplecov/formatter/simple_formatter.rb +2 -8
- data/lib/simplecov/formatter.rb +14 -10
- data/lib/simplecov/group_names.rb +13 -12
- data/lib/simplecov/history.rb +115 -0
- data/lib/simplecov/last_run.rb +6 -9
- data/lib/simplecov/lines_classifier.rb +31 -16
- data/lib/simplecov/load_global_config.rb +7 -7
- data/lib/simplecov/parallel_adapters/base.rb +13 -41
- data/lib/simplecov/parallel_adapters/generic.rb +7 -19
- data/lib/simplecov/parallel_adapters/parallel_tests.rb +21 -42
- data/lib/simplecov/parallel_adapters.rb +24 -55
- data/lib/simplecov/parallel_coordination.rb +39 -49
- data/lib/simplecov/parallel_result_merger/worker_payload.rb +33 -0
- data/lib/simplecov/parallel_result_merger.rb +67 -126
- data/lib/simplecov/process.rb +14 -32
- data/lib/simplecov/production/error.rb +11 -0
- data/lib/simplecov/production/file_sink.rb +130 -0
- data/lib/simplecov/production.rb +249 -0
- data/lib/simplecov/profiles/rails.rb +4 -15
- data/lib/simplecov/profiles/root_filter.rb +0 -4
- data/lib/simplecov/profiles/strict.rb +11 -21
- data/lib/simplecov/profiles.rb +11 -33
- data/lib/simplecov/report_deferral.rb +11 -15
- data/lib/simplecov/report_stamp.rb +6 -9
- data/lib/simplecov/result/filter_config.rb +19 -0
- data/lib/simplecov/result/missing_source_files_reporter.rb +8 -9
- data/lib/simplecov/result/serialization.rb +43 -0
- data/lib/simplecov/result/source_file_builder.rb +7 -12
- data/lib/simplecov/result.rb +74 -117
- data/lib/simplecov/result_adapter.rb +56 -68
- data/lib/simplecov/result_merger/contexts.rb +22 -0
- data/lib/simplecov/result_merger/legacy_format_adapter.rb +4 -8
- data/lib/simplecov/result_merger/resultset_file.rb +20 -27
- data/lib/simplecov/result_merger/resultset_run_identity.rb +23 -25
- data/lib/simplecov/result_merger/resultset_store.rb +23 -14
- data/lib/simplecov/result_merger/unloaded_files.rb +24 -32
- data/lib/simplecov/result_merger.rb +59 -58
- data/lib/simplecov/result_processing.rb +92 -122
- data/lib/simplecov/run_identity.rb +16 -18
- data/lib/simplecov/simulate_coverage.rb +34 -50
- data/lib/simplecov/source_file/branch.rb +8 -33
- data/lib/simplecov/source_file/branch_builder.rb +28 -47
- data/lib/simplecov/source_file/builder_context.rb +13 -13
- data/lib/simplecov/source_file/line.rb +13 -33
- data/lib/simplecov/source_file/line_builder.rb +9 -15
- data/lib/simplecov/source_file/method.rb +3 -7
- data/lib/simplecov/source_file/method_builder.rb +6 -15
- data/lib/simplecov/source_file/ruby_data_parser.rb +59 -66
- data/lib/simplecov/source_file/skip_chunks.rb +29 -29
- data/lib/simplecov/source_file/source_loader.rb +45 -35
- data/lib/simplecov/source_file/statistics.rb +20 -9
- data/lib/simplecov/source_file.rb +21 -51
- data/lib/simplecov/static_coverage_extractor/condition_folding.rb +55 -139
- data/lib/simplecov/static_coverage_extractor/location_conventions.rb +76 -121
- data/lib/simplecov/static_coverage_extractor/method_collector.rb +9 -26
- data/lib/simplecov/static_coverage_extractor/prism_compat.rb +15 -24
- data/lib/simplecov/static_coverage_extractor/value_position.rb +25 -38
- data/lib/simplecov/static_coverage_extractor/visitor.rb +45 -65
- data/lib/simplecov/static_coverage_extractor.rb +43 -75
- data/lib/simplecov/test_tracker/accessors.rb +34 -0
- data/lib/simplecov/test_tracker/constant_watch.rb +49 -0
- data/lib/simplecov/test_tracker/delta.rb +69 -0
- data/lib/simplecov/test_tracker/framework_hooks.rb +130 -0
- data/lib/simplecov/test_tracker.rb +162 -0
- data/lib/simplecov/unloaded_file_injector.rb +18 -31
- data/lib/simplecov/useless_results_remover.rb +7 -6
- data/lib/simplecov/version.rb +1 -1
- data/lib/simplecov/view_coverage/template_compiler.rb +83 -0
- data/lib/simplecov/view_coverage.rb +55 -0
- data/lib/simplecov-html.rb +1 -0
- data/lib/simplecov.rb +57 -113
- data/lib/simplecov_json_formatter.rb +1 -0
- data/man/simplecov.1 +354 -0
- data/sig/simplecov.rbs +528 -595
- metadata +79 -6
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SimpleCov
|
|
4
|
+
module CLI
|
|
5
|
+
module Affected
|
|
6
|
+
module Selection
|
|
7
|
+
TEST_FILE_NAME = /\A(?:test_.*|.*_(?:test|spec))\.rb\z/
|
|
8
|
+
|
|
9
|
+
extend self
|
|
10
|
+
|
|
11
|
+
def build(changed, document, contexts, opts, stderr, root:)
|
|
12
|
+
none = {} #: Hash[String, untyped]
|
|
13
|
+
coverage = document.fetch("coverage", none)
|
|
14
|
+
unless coverage.instance_of?(Hash)
|
|
15
|
+
return CoverageFile.report_invalid(stderr, "affected", opts.fetch(:input), '"coverage" must be an object')
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
state = initial_state(changed, contexts, coverage, opts, stderr, root: root)
|
|
19
|
+
return nil unless changed.all? { |path| classify(path, state) }
|
|
20
|
+
|
|
21
|
+
{tests: state.fetch(:tests).uniq.sort, triggers: state.fetch(:triggers)}
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def initial_state(changed, contexts, coverage, opts, stderr, root:)
|
|
25
|
+
{
|
|
26
|
+
tests: [], triggers: [], changed: changed, contexts: contexts,
|
|
27
|
+
known: contexts.each_with_object(Set.new) { |id, set| set << test_file_of(id) },
|
|
28
|
+
index: CoverageFile.exact_index(coverage), root: root, opts: opts, stderr: stderr
|
|
29
|
+
} #: Hash[Symbol, untyped]
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# A recorded test file selects itself, a file the report covers selects the
|
|
33
|
+
# files of the tests touching it, a test-looking file the map has never seen
|
|
34
|
+
# always runs, and anything else is a trigger. Changed paths are exact
|
|
35
|
+
# root-relative names, so they resolve against the report exactly: a
|
|
36
|
+
# lookalike entry elsewhere must not stand in for a file it doesn't carry.
|
|
37
|
+
def classify(path, state)
|
|
38
|
+
entry = entry_for(path, state)
|
|
39
|
+
if state.fetch(:known).include?(path)
|
|
40
|
+
select_test_file(path, state)
|
|
41
|
+
elsif entry
|
|
42
|
+
select_covering(path, entry, state)
|
|
43
|
+
elsif TEST_FILE_NAME.match?(File.basename(path))
|
|
44
|
+
select_test_file(path, state, note_deleted: false)
|
|
45
|
+
else
|
|
46
|
+
note_untracked(path, state)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def note_untracked(path, state)
|
|
51
|
+
state.fetch(:triggers) << "#{path} changed but #{state.fetch(:opts).fetch(:input)} has no data for it"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# A changed test file runs, unless the change is its deletion: its tests are
|
|
55
|
+
# gone, which removes them from the answer without distrusting the map.
|
|
56
|
+
def select_test_file(path, state, note_deleted: true)
|
|
57
|
+
if on_disk?(path, state)
|
|
58
|
+
state.fetch(:tests) << path
|
|
59
|
+
elsif note_deleted
|
|
60
|
+
state.fetch(:stderr).puts("simplecov affected: skipping deleted test file #{path}")
|
|
61
|
+
end
|
|
62
|
+
path
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Existence judged at the repository root the paths are relative to, not the
|
|
66
|
+
# cwd, so a subdirectory run answers the same.
|
|
67
|
+
def on_disk?(path, state)
|
|
68
|
+
File.exist?(File.expand_path(path, state.fetch(:root)))
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def entry_for(path, state)
|
|
72
|
+
state.fetch(:index)[File.expand_path(path, state.fetch(:root))] || state.fetch(:index)[path]
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def select_covering(path, entry, state)
|
|
76
|
+
table = covering_table(path, entry, state)
|
|
77
|
+
table&.each_key { |index| select_context(state.fetch(:contexts).fetch(index), path, state) }
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def covering_table(path, entry, state)
|
|
81
|
+
unless entry.instance_of?(Hash)
|
|
82
|
+
return CoverageFile.report_invalid(state.fetch(:stderr), "affected", state.fetch(:opts).fetch(:input),
|
|
83
|
+
"entry for #{path} must be an object")
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
raw = entry["contexts"] || {}
|
|
87
|
+
table = raw.instance_of?(Hash) && Tests.decode_table(raw, state.fetch(:contexts).size)
|
|
88
|
+
return table if table
|
|
89
|
+
|
|
90
|
+
CoverageFile.report_invalid(state.fetch(:stderr), "affected", state.fetch(:opts).fetch(:input),
|
|
91
|
+
"entry for #{path} carries a malformed \"contexts\" table")
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Selected via its file, or a staleness trigger when it has no file to
|
|
95
|
+
# select: a locationless id, or a file that exists neither on disk nor in
|
|
96
|
+
# the change.
|
|
97
|
+
def select_context(id, path, state)
|
|
98
|
+
file = test_file_of(id)
|
|
99
|
+
if file.nil?
|
|
100
|
+
state.fetch(:triggers) << "recorded test #{id} touches #{path} but has no file location"
|
|
101
|
+
elsif on_disk?(file, state)
|
|
102
|
+
state.fetch(:tests) << file
|
|
103
|
+
elsif !state.fetch(:changed).include?(file)
|
|
104
|
+
state.fetch(:triggers) << "recorded test file #{file} no longer exists"
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# "spec/a_spec.rb:12" and the :file granularity's bare "spec/a_spec.rb" both
|
|
109
|
+
# name a file, while a locationless fallback id names none.
|
|
110
|
+
def test_file_of(id)
|
|
111
|
+
base = id.sub(/:\d+\z/, "")
|
|
112
|
+
base if base.include?("/") || base.end_with?(".rb")
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "optparse"
|
|
5
|
+
require_relative "command_helpers"
|
|
6
|
+
require_relative "tests"
|
|
7
|
+
require_relative "affected/changed_files"
|
|
8
|
+
require_relative "affected/selection"
|
|
9
|
+
|
|
10
|
+
module SimpleCov
|
|
11
|
+
module CLI
|
|
12
|
+
# `simplecov affected`: select the test files whose recorded tests touch the
|
|
13
|
+
# code changed since a git base ref, from a coverage.json written under
|
|
14
|
+
# `track_tests`. The change is everything between the merge base of `--base`
|
|
15
|
+
# and the working tree, plus untracked files.
|
|
16
|
+
#
|
|
17
|
+
# The set intersection is the easy half; the hard half is knowing when to
|
|
18
|
+
# distrust the map, because a test map is stale the moment something changes
|
|
19
|
+
# that no test mentions by name. Any changed file outside the tracked set
|
|
20
|
+
# fails open to the full suite, out loud: each trigger is named on stderr and
|
|
21
|
+
# stdout stays empty, which a bare runner reads as "run everything". Test
|
|
22
|
+
# files always select themselves, recorded or not.
|
|
23
|
+
module Affected
|
|
24
|
+
extend CommandHelpers
|
|
25
|
+
|
|
26
|
+
extend self
|
|
27
|
+
|
|
28
|
+
def run(args, stdout:, stderr:)
|
|
29
|
+
opts = parse(args)
|
|
30
|
+
issue = precheck(opts)
|
|
31
|
+
return error(stderr, issue) if issue
|
|
32
|
+
|
|
33
|
+
document = CoverageFile.load_document(opts.fetch(:input), command: "affected", stderr: stderr)
|
|
34
|
+
return 1 unless document
|
|
35
|
+
|
|
36
|
+
contexts = recorded_contexts(document, opts, stderr)
|
|
37
|
+
return 1 unless contexts
|
|
38
|
+
|
|
39
|
+
diffed = ChangedFiles.call(opts[:base] ||= Git.default_base, stderr)
|
|
40
|
+
return 1 unless diffed
|
|
41
|
+
|
|
42
|
+
respond(diffed, document, contexts, opts, stdout: stdout, stderr: stderr)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def parse(args)
|
|
46
|
+
head, runner = split_runner(args)
|
|
47
|
+
opts, rest = parse_common(head, base: nil, run: runner) do |parser, into|
|
|
48
|
+
parser.on("--base REF") { |v| into[:base] = v }
|
|
49
|
+
end
|
|
50
|
+
opts[:rest] = rest
|
|
51
|
+
opts
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Everything after `--run` is the runner command, verbatim, so the command's
|
|
55
|
+
# own flags never collide with ours.
|
|
56
|
+
def split_runner(args)
|
|
57
|
+
index = args.index("--run")
|
|
58
|
+
return [args, nil] unless index
|
|
59
|
+
|
|
60
|
+
[args.take(index), args.drop(index + 1)]
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# A stray positional looks exactly like a forgotten `--base` ref, and
|
|
64
|
+
# silently dropping it would select tests for the wrong change.
|
|
65
|
+
def precheck(opts)
|
|
66
|
+
stray = opts.fetch(:rest).first
|
|
67
|
+
return "unexpected argument #{stray.inspect} (did you mean `--base #{stray}`?)" if stray
|
|
68
|
+
return "missing command after --run" if opts.fetch(:run) && opts.fetch(:run).empty?
|
|
69
|
+
|
|
70
|
+
"--run and --json cannot be combined" if opts.fetch(:run) && opts.fetch(:json)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def respond(diffed, document, contexts, opts, stdout:, stderr:)
|
|
74
|
+
opts[:root] = diffed.fetch(:root)
|
|
75
|
+
return no_changes(opts, stdout, stderr) if diffed.fetch(:changed).empty?
|
|
76
|
+
|
|
77
|
+
selection = Selection.build(diffed.fetch(:changed), document, contexts, opts, stderr, root: diffed.fetch(:root))
|
|
78
|
+
return 1 unless selection
|
|
79
|
+
|
|
80
|
+
deliver(selection, opts, stdout, stderr)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def no_changes(opts, stdout, stderr)
|
|
84
|
+
stderr.puts("simplecov affected: no changes against #{opts.fetch(:base)}")
|
|
85
|
+
empty = {tests: [], triggers: []} #: Hash[Symbol, Array[String]]
|
|
86
|
+
emit_json(empty, false, stdout) if opts.fetch(:json)
|
|
87
|
+
0
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def deliver(selection, opts, stdout, stderr)
|
|
91
|
+
full = !selection.fetch(:triggers).empty?
|
|
92
|
+
selection.fetch(:triggers).each { |trigger| stderr.puts("simplecov affected: #{trigger}") }
|
|
93
|
+
stderr.puts("simplecov affected: falling back to the full suite") if full
|
|
94
|
+
return emit_json(selection, full, stdout) if opts.fetch(:json)
|
|
95
|
+
return execute(selection, full, opts, stderr) if opts.fetch(:run)
|
|
96
|
+
|
|
97
|
+
emit_text(selection, full, stdout, stderr)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def emit_json(selection, full, stdout)
|
|
101
|
+
none = [] #: Array[String]
|
|
102
|
+
stdout.puts(JSON.pretty_generate("full_suite" => full, "triggers" => selection.fetch(:triggers),
|
|
103
|
+
"tests" => full ? none : selection.fetch(:tests)))
|
|
104
|
+
0
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Stdout carries the selected files and nothing else, so the list can feed a
|
|
108
|
+
# runner directly, and a full-suite fallback prints nothing, which
|
|
109
|
+
# `rspec $(simplecov affected)` reads as "run everything".
|
|
110
|
+
def emit_text(selection, full, stdout, stderr)
|
|
111
|
+
return 0 if full
|
|
112
|
+
|
|
113
|
+
tests = selection.fetch(:tests)
|
|
114
|
+
if tests.empty?
|
|
115
|
+
note_untouched(stderr)
|
|
116
|
+
else
|
|
117
|
+
tests.each { |file| stdout.puts(file) }
|
|
118
|
+
0
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def note_untouched(stderr)
|
|
123
|
+
stderr.puts("simplecov affected: no recorded test touches the changed code")
|
|
124
|
+
0
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def execute(selection, full, opts, stderr)
|
|
128
|
+
return run_command(opts.fetch(:run), opts.fetch(:root), stderr) if full
|
|
129
|
+
return note_untouched(stderr) if selection.fetch(:tests).empty?
|
|
130
|
+
|
|
131
|
+
count = selection.fetch(:tests).size
|
|
132
|
+
stderr.puts("simplecov affected: running #{count} test file#{"s" unless count.eql?(1)}")
|
|
133
|
+
run_command(opts.fetch(:run) + selection.fetch(:tests), opts.fetch(:root), stderr)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# The selection's paths are relative to the repository root, so the runner
|
|
137
|
+
# starts there. The command is named explicitly in the failure because not
|
|
138
|
+
# every engine's exception message carries it.
|
|
139
|
+
def run_command(command, root, stderr)
|
|
140
|
+
_, status = Process.wait2(spawn(*command, chdir: root))
|
|
141
|
+
status.exitstatus || 1
|
|
142
|
+
rescue SystemCallError => e
|
|
143
|
+
stderr.puts("simplecov affected: cannot run #{command.first.inspect} (#{e})")
|
|
144
|
+
127
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
@@ -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
|
data/lib/simplecov/cli/clean.rb
CHANGED
|
@@ -1,22 +1,21 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "fileutils"
|
|
3
4
|
require "optparse"
|
|
4
5
|
require_relative "command_helpers"
|
|
5
6
|
|
|
6
7
|
module SimpleCov
|
|
7
8
|
module CLI
|
|
8
|
-
# `simplecov clean [--dry-run]
|
|
9
|
-
#
|
|
10
|
-
# `--dry-run` flag prints what would be deleted without touching
|
|
11
|
-
# disk, for when you're not sure what's in there.
|
|
9
|
+
# `simplecov clean [--dry-run]`: remove the coverage report directory.
|
|
10
|
+
# `--dry-run` prints what would be deleted without touching disk.
|
|
12
11
|
module Clean
|
|
13
12
|
extend CommandHelpers
|
|
14
13
|
|
|
15
|
-
|
|
14
|
+
extend self
|
|
16
15
|
|
|
17
16
|
def run(args, stdout:, stderr:, **)
|
|
18
17
|
opts = parse(args)
|
|
19
|
-
dir =
|
|
18
|
+
dir = CLI.coverage_dir
|
|
20
19
|
return error(stderr, "refusing to remove unsafe coverage directory #{dir.inspect}") unless safe_to_remove?(dir)
|
|
21
20
|
return announce(stdout, opts, "#{dir} doesn't exist; nothing to do") || 0 unless File.directory?(dir)
|
|
22
21
|
|
|
@@ -25,45 +24,45 @@ module SimpleCov
|
|
|
25
24
|
end
|
|
26
25
|
|
|
27
26
|
def sweep(dir, opts, stdout)
|
|
28
|
-
if opts
|
|
27
|
+
if opts.fetch(:dry_run)
|
|
29
28
|
announce(stdout, opts, "would remove #{dir} (#{entry_count(dir)} entries)")
|
|
30
29
|
else
|
|
31
|
-
require "fileutils"
|
|
32
30
|
FileUtils.rm_rf(dir)
|
|
33
31
|
announce(stdout, opts, "removed #{dir}")
|
|
34
32
|
end
|
|
35
33
|
end
|
|
36
34
|
|
|
37
|
-
# FNM_DOTMATCH so the dotfiles rm_rf will actually delete
|
|
38
|
-
#
|
|
39
|
-
# yields the directory's own "." entry, which rm_rf does not
|
|
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
|
|
40
37
|
# remove separately.
|
|
41
38
|
def entry_count(dir)
|
|
42
39
|
Dir.glob("#{dir}/**/*", File::FNM_DOTMATCH).count { |path| !path.end_with?("/.") }
|
|
43
40
|
end
|
|
44
41
|
|
|
45
42
|
def announce(stdout, opts, message)
|
|
46
|
-
stdout.puts("simplecov #{command_name}: #{message}") unless opts
|
|
43
|
+
stdout.puts("simplecov #{command_name}: #{message}") unless opts.fetch(:quiet)
|
|
47
44
|
end
|
|
48
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
49
|
def safe_to_remove?(dir)
|
|
50
50
|
target = canonical_path(dir)
|
|
51
|
-
|
|
52
|
-
# roots like "D:/" on Windows). The descendant check below only
|
|
53
|
-
# protects roots that happen to contain the cwd or project root,
|
|
54
|
-
# which misses other drives, so refuse roots outright.
|
|
55
|
-
return false if File.dirname(target) == target
|
|
51
|
+
return false if File.dirname(target).eql?(target)
|
|
56
52
|
|
|
57
|
-
protected_paths.none? { |path| path
|
|
53
|
+
protected_paths.none? { |path| path.eql?(target) || descendant_of?(path, target) }
|
|
58
54
|
end
|
|
59
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
60
|
def protected_paths
|
|
61
|
-
|
|
62
|
-
[Dir.pwd, (File.dirname(project_root) if project_root)].compact.map { |path| canonical_path(path) }.uniq
|
|
61
|
+
[canonical_path(Dir.pwd)]
|
|
63
62
|
end
|
|
64
63
|
|
|
65
|
-
# Canonical paths only carry a trailing separator on a filesystem
|
|
66
|
-
#
|
|
64
|
+
# Canonical paths only carry a trailing separator on a filesystem root, and
|
|
65
|
+
# roots are refused before this check runs.
|
|
67
66
|
def descendant_of?(path, possible_ancestor)
|
|
68
67
|
path.start_with?("#{possible_ancestor}#{File::SEPARATOR}")
|
|
69
68
|
end
|
|
@@ -76,9 +75,9 @@ module SimpleCov
|
|
|
76
75
|
|
|
77
76
|
def parse(args)
|
|
78
77
|
opts = {dry_run: false, quiet: false}
|
|
79
|
-
|
|
78
|
+
build_parser do |o|
|
|
80
79
|
o.on("--dry-run") { opts[:dry_run] = true }
|
|
81
|
-
o
|
|
80
|
+
quiet_option(o, opts)
|
|
82
81
|
end.parse(args)
|
|
83
82
|
opts
|
|
84
83
|
end
|