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,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,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
|
|
@@ -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)
|