simplecov 0.22.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/LICENSE +1 -1
- data/README.md +173 -875
- data/exe/simplecov +8 -0
- data/lib/minitest/simplecov_plugin.rb +18 -7
- data/lib/simplecov/atomic_file.rb +67 -0
- data/lib/simplecov/autostart.rb +9 -0
- data/lib/simplecov/baseline/parser.rb +58 -0
- data/lib/simplecov/baseline.rb +170 -0
- data/lib/simplecov/cli/affected/changed_files.rb +57 -0
- data/lib/simplecov/cli/affected/selection.rb +117 -0
- data/lib/simplecov/cli/affected.rb +148 -0
- data/lib/simplecov/cli/annotations.rb +166 -0
- data/lib/simplecov/cli/badge/svg.rb +70 -0
- data/lib/simplecov/cli/badge.rb +62 -0
- data/lib/simplecov/cli/clean.rb +86 -0
- data/lib/simplecov/cli/command_helpers.rb +91 -0
- data/lib/simplecov/cli/completions/scripts.rb +114 -0
- data/lib/simplecov/cli/completions.rb +83 -0
- data/lib/simplecov/cli/coverage.rb +83 -0
- data/lib/simplecov/cli/coverage_file.rb +109 -0
- data/lib/simplecov/cli/dead_code/output.rb +100 -0
- data/lib/simplecov/cli/dead_code.rb +139 -0
- data/lib/simplecov/cli/diff/output.rb +45 -0
- data/lib/simplecov/cli/diff.rb +115 -0
- data/lib/simplecov/cli/dotfile.rb +156 -0
- data/lib/simplecov/cli/git.rb +41 -0
- data/lib/simplecov/cli/history/output.rb +135 -0
- data/lib/simplecov/cli/history.rb +67 -0
- data/lib/simplecov/cli/merge.rb +115 -0
- data/lib/simplecov/cli/open.rb +45 -0
- data/lib/simplecov/cli/patch/changed_lines.rb +149 -0
- data/lib/simplecov/cli/patch/output.rb +151 -0
- data/lib/simplecov/cli/patch.rb +175 -0
- data/lib/simplecov/cli/ratchet/output.rb +44 -0
- data/lib/simplecov/cli/ratchet.rb +83 -0
- data/lib/simplecov/cli/report.rb +91 -0
- data/lib/simplecov/cli/run.rb +35 -0
- data/lib/simplecov/cli/serve/report_preparer.rb +30 -0
- data/lib/simplecov/cli/serve/static_file_handler.rb +128 -0
- data/lib/simplecov/cli/serve.rb +88 -0
- data/lib/simplecov/cli/show/annotator.rb +87 -0
- data/lib/simplecov/cli/show/sweep.rb +36 -0
- data/lib/simplecov/cli/show.rb +133 -0
- data/lib/simplecov/cli/status/facts.rb +87 -0
- data/lib/simplecov/cli/status.rb +93 -0
- data/lib/simplecov/cli/tests/redundancy.rb +78 -0
- data/lib/simplecov/cli/tests.rb +148 -0
- data/lib/simplecov/cli/uncovered/misses.rb +33 -0
- data/lib/simplecov/cli/uncovered.rb +127 -0
- data/lib/simplecov/cli/usage.rb +153 -0
- data/lib/simplecov/cli/watch/live_report.rb +73 -0
- data/lib/simplecov/cli/watch/narrator.rb +55 -0
- data/lib/simplecov/cli/watch/poller.rb +45 -0
- data/lib/simplecov/cli/watch/session.rb +134 -0
- data/lib/simplecov/cli/watch/test_plan.rb +43 -0
- data/lib/simplecov/cli/watch.rb +85 -0
- data/lib/simplecov/cli.rb +114 -0
- data/lib/simplecov/color.rb +65 -0
- data/lib/simplecov/combine/branches_combiner.rb +44 -21
- data/lib/simplecov/combine/coverage_accumulator.rb +175 -0
- data/lib/simplecov/combine/identity_interner.rb +27 -0
- data/lib/simplecov/combine/interned_counts.rb +28 -0
- data/lib/simplecov/combine/lines_combiner.rb +35 -29
- data/lib/simplecov/combine/methods_combiner.rb +48 -0
- data/lib/simplecov/combine/results_combiner.rb +9 -49
- data/lib/simplecov/combine.rb +0 -24
- data/lib/simplecov/command_guesser.rb +99 -42
- data/lib/simplecov/configuration/baseline.rb +31 -0
- data/lib/simplecov/configuration/coverage.rb +184 -0
- data/lib/simplecov/configuration/coverage_criteria.rb +143 -0
- data/lib/simplecov/configuration/deprecations.rb +21 -0
- data/lib/simplecov/configuration/eval_coverage.rb +37 -0
- data/lib/simplecov/configuration/filters.rb +131 -0
- data/lib/simplecov/configuration/formatting.rb +129 -0
- data/lib/simplecov/configuration/groups.rb +48 -0
- data/lib/simplecov/configuration/history.rb +44 -0
- data/lib/simplecov/configuration/ignored_entries.rb +72 -0
- data/lib/simplecov/configuration/merging.rb +197 -0
- data/lib/simplecov/configuration/missed_caps.rb +51 -0
- data/lib/simplecov/configuration/production.rb +23 -0
- data/lib/simplecov/configuration/test_tracking.rb +48 -0
- data/lib/simplecov/configuration/thresholds.rb +150 -0
- data/lib/simplecov/configuration/view_coverage.rb +37 -0
- data/lib/simplecov/configuration.rb +100 -443
- data/lib/simplecov/configuration_error.rb +5 -0
- data/lib/simplecov/context_map/union.rb +81 -0
- data/lib/simplecov/context_map.rb +187 -0
- data/lib/simplecov/coverage_json.rb +21 -0
- data/lib/simplecov/coverage_statistics.rb +24 -28
- data/lib/simplecov/coverage_violations.rb +254 -0
- data/lib/simplecov/current_run.rb +61 -0
- data/lib/simplecov/defaults.rb +20 -28
- data/lib/simplecov/deprecation.rb +63 -0
- data/lib/simplecov/directive/erb.rb +44 -0
- data/lib/simplecov/directive/haml.rb +33 -0
- data/lib/simplecov/directive/indented_template.rb +37 -0
- data/lib/simplecov/directive/slim.rb +35 -0
- data/lib/simplecov/directive/template.rb +29 -0
- data/lib/simplecov/directive.rb +152 -0
- data/lib/simplecov/exit_codes/baseline_check.rb +46 -0
- data/lib/simplecov/exit_codes/check.rb +39 -0
- data/lib/simplecov/exit_codes/exit_code_handling.rb +24 -5
- data/lib/simplecov/exit_codes/maximum_coverage_drop_check.rb +17 -67
- data/lib/simplecov/exit_codes/maximum_missed_check.rb +26 -0
- data/lib/simplecov/exit_codes/maximum_missed_per_file_check.rb +34 -0
- data/lib/simplecov/exit_codes/maximum_overall_coverage_check.rb +27 -0
- data/lib/simplecov/exit_codes/minimum_coverage_by_file_check.rb +20 -38
- data/lib/simplecov/exit_codes/minimum_coverage_by_group_check.rb +28 -0
- data/lib/simplecov/exit_codes/minimum_overall_coverage_check.rb +37 -34
- data/lib/simplecov/exit_codes.rb +26 -0
- data/lib/simplecov/exit_handling.rb +163 -0
- data/lib/simplecov/file_list.rb +63 -48
- data/lib/simplecov/filter.rb +98 -42
- data/lib/simplecov/formatter/base.rb +100 -0
- data/lib/simplecov/formatter/baseline_formatter.rb +98 -0
- data/lib/simplecov/formatter/coverage_json_writer.rb +95 -0
- data/lib/simplecov/formatter/html_formatter/public/index.html +93 -0
- data/lib/simplecov/formatter/html_formatter/viewer_data_validator.rb +153 -0
- data/lib/simplecov/formatter/html_formatter.rb +88 -0
- data/lib/simplecov/formatter/json_formatter/errors_formatter.rb +122 -0
- data/lib/simplecov/formatter/json_formatter/production_section_formatter.rb +48 -0
- data/lib/simplecov/formatter/json_formatter/result_hash_formatter.rb +132 -0
- data/lib/simplecov/formatter/json_formatter/source_file_formatter.rb +97 -0
- data/lib/simplecov/formatter/json_formatter.rb +40 -0
- data/lib/simplecov/formatter/multi_formatter.rb +8 -10
- data/lib/simplecov/formatter/simple_formatter.rb +12 -15
- data/lib/simplecov/formatter.rb +21 -1
- data/lib/simplecov/group_names.rb +33 -0
- data/lib/simplecov/history.rb +115 -0
- data/lib/simplecov/last_run.rb +16 -5
- data/lib/simplecov/lines_classifier.rb +67 -19
- data/lib/simplecov/load_global_config.rb +12 -4
- data/lib/simplecov/parallel_adapters/base.rb +50 -0
- data/lib/simplecov/parallel_adapters/generic.rb +33 -0
- data/lib/simplecov/parallel_adapters/parallel_tests.rb +72 -0
- data/lib/simplecov/parallel_adapters.rb +54 -0
- data/lib/simplecov/parallel_coordination.rb +124 -0
- data/lib/simplecov/parallel_result_merger/worker_payload.rb +33 -0
- data/lib/simplecov/parallel_result_merger.rb +171 -0
- data/lib/simplecov/process.rb +34 -12
- data/lib/simplecov/production/error.rb +11 -0
- data/lib/simplecov/production/file_sink.rb +130 -0
- data/lib/simplecov/production.rb +249 -0
- data/lib/simplecov/profiles/bundler_filter.rb +1 -1
- data/lib/simplecov/profiles/hidden_filter.rb +1 -1
- data/lib/simplecov/profiles/rails.rb +13 -10
- data/lib/simplecov/profiles/root_filter.rb +2 -5
- data/lib/simplecov/profiles/strict.rb +22 -0
- data/lib/simplecov/profiles/test_frameworks.rb +1 -4
- data/lib/simplecov/profiles.rb +26 -19
- data/lib/simplecov/report_deferral.rb +45 -0
- data/lib/simplecov/report_stamp.rb +25 -0
- data/lib/simplecov/result/filter_config.rb +19 -0
- data/lib/simplecov/result/missing_source_files_reporter.rb +48 -0
- data/lib/simplecov/result/serialization.rb +43 -0
- data/lib/simplecov/result/source_file_builder.rb +46 -0
- data/lib/simplecov/result.rb +123 -47
- data/lib/simplecov/result_adapter.rb +130 -11
- data/lib/simplecov/result_merger/contexts.rb +22 -0
- data/lib/simplecov/result_merger/legacy_format_adapter.rb +24 -0
- data/lib/simplecov/result_merger/resultset_file.rb +67 -0
- data/lib/simplecov/result_merger/resultset_run_identity.rb +65 -0
- data/lib/simplecov/result_merger/resultset_store.rb +62 -0
- data/lib/simplecov/result_merger/unloaded_files.rb +95 -0
- data/lib/simplecov/result_merger.rb +120 -124
- data/lib/simplecov/result_processing.rb +188 -0
- data/lib/simplecov/run_identity.rb +75 -0
- data/lib/simplecov/simulate_coverage.rb +72 -17
- data/lib/simplecov/source_file/branch.rb +8 -35
- data/lib/simplecov/source_file/branch_builder.rb +95 -0
- data/lib/simplecov/source_file/builder_context.rb +28 -0
- data/lib/simplecov/source_file/line.rb +17 -32
- data/lib/simplecov/source_file/line_builder.rb +37 -0
- data/lib/simplecov/source_file/method.rb +54 -0
- data/lib/simplecov/source_file/method_builder.rb +51 -0
- data/lib/simplecov/source_file/ruby_data_parser.rb +103 -0
- data/lib/simplecov/source_file/skip_chunks.rb +72 -0
- data/lib/simplecov/source_file/source_loader.rb +89 -0
- data/lib/simplecov/source_file/statistics.rb +76 -0
- data/lib/simplecov/source_file.rb +64 -260
- data/lib/simplecov/static_coverage_extractor/condition_folding.rb +155 -0
- data/lib/simplecov/static_coverage_extractor/location_conventions.rb +191 -0
- data/lib/simplecov/static_coverage_extractor/method_collector.rb +45 -0
- data/lib/simplecov/static_coverage_extractor/prism_compat.rb +46 -0
- data/lib/simplecov/static_coverage_extractor/value_position.rb +61 -0
- data/lib/simplecov/static_coverage_extractor/visitor.rb +163 -0
- data/lib/simplecov/static_coverage_extractor.rb +78 -0
- data/lib/simplecov/test_tracker/accessors.rb +34 -0
- data/lib/simplecov/test_tracker/constant_watch.rb +49 -0
- data/lib/simplecov/test_tracker/delta.rb +69 -0
- data/lib/simplecov/test_tracker/framework_hooks.rb +130 -0
- data/lib/simplecov/test_tracker.rb +162 -0
- data/lib/simplecov/unloaded_file_injector.rb +62 -0
- data/lib/simplecov/useless_results_remover.rb +17 -7
- data/lib/simplecov/version.rb +1 -1
- data/lib/simplecov/view_coverage/template_compiler.rb +83 -0
- data/lib/simplecov/view_coverage.rb +55 -0
- data/lib/simplecov-html.rb +5 -0
- data/lib/simplecov.rb +143 -394
- data/lib/simplecov_json_formatter.rb +5 -0
- data/man/simplecov.1 +354 -0
- data/sig/simplecov.rbs +1710 -0
- metadata +183 -61
- data/CHANGELOG.md +0 -191
- data/doc/alternate-formatters.md +0 -71
- data/doc/commercial-services.md +0 -25
- data/doc/editor-integration.md +0 -18
- data/lib/simplecov/combine/files_combiner.rb +0 -24
- data/lib/simplecov/default_formatter.rb +0 -20
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SimpleCov
|
|
4
|
+
module CLI
|
|
5
|
+
module Status
|
|
6
|
+
module Facts
|
|
7
|
+
extend self
|
|
8
|
+
|
|
9
|
+
def gather(document, resultset_path)
|
|
10
|
+
contexts = document["contexts"]
|
|
11
|
+
meta_facts(document).merge(
|
|
12
|
+
totals: totals(document["total"]),
|
|
13
|
+
contexts: (contexts.size if contexts.instance_of?(Array)),
|
|
14
|
+
resultset_path: resultset_path, resultset: readable_resultset(resultset_path)
|
|
15
|
+
)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def meta_facts(document)
|
|
19
|
+
none = {} #: Hash[String, untyped]
|
|
20
|
+
recorded = document["meta"]
|
|
21
|
+
meta = recorded.instance_of?(Hash) ? recorded : none
|
|
22
|
+
generated = parse_time(meta["timestamp"])
|
|
23
|
+
{
|
|
24
|
+
generated_at: meta["timestamp"], age: generated && whole_seconds(Time.now - generated),
|
|
25
|
+
version: meta["simplecov_version"], command_name: meta["command_name"],
|
|
26
|
+
commit: meta["commit"], behind: behind(meta["commit"])
|
|
27
|
+
}
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def age_of(timestamp)
|
|
31
|
+
whole_seconds(Process.clock_gettime(Process::CLOCK_REALTIME) - timestamp)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def whole_seconds(elapsed)
|
|
35
|
+
elapsed.truncate
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def parse_time(value)
|
|
39
|
+
Time.iso8601(value) if value.instance_of?(String)
|
|
40
|
+
rescue ArgumentError
|
|
41
|
+
nil
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Commits between the report's commit and HEAD: 0 at HEAD, nil when
|
|
45
|
+
# unanswerable, meaning no commit recorded, no git, or a commit this
|
|
46
|
+
# repository has never seen.
|
|
47
|
+
def behind(commit)
|
|
48
|
+
return nil unless commit.instance_of?(String)
|
|
49
|
+
|
|
50
|
+
stdout, _detail, success = Git.capture("rev-list", "--count", "#{commit}..HEAD")
|
|
51
|
+
commit_count(_ = stdout) if success
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# `Integer` ignores surrounding whitespace on its own, so git's trailing
|
|
55
|
+
# newline needs no trimming. The base is spelled out because bare `Integer`
|
|
56
|
+
# reads a leading zero as octal.
|
|
57
|
+
def commit_count(output)
|
|
58
|
+
Integer(output, 10)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def totals(total)
|
|
62
|
+
return {} unless total.instance_of?(Hash)
|
|
63
|
+
|
|
64
|
+
rows = {"line" => "lines", "branch" => "branches", "method" => "methods"}.filter_map do |name, key|
|
|
65
|
+
percent = total.dig(key, "percent")
|
|
66
|
+
[name, percent] if percent.is_a?(Numeric)
|
|
67
|
+
end
|
|
68
|
+
rows.to_h
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# An unreadable resultset answers nothing rather than raising: its
|
|
72
|
+
# absence is a fact, not an error.
|
|
73
|
+
def readable_resultset(path)
|
|
74
|
+
parsed = JSON.parse(File.read(path))
|
|
75
|
+
return nil unless parsed.instance_of?(Hash)
|
|
76
|
+
|
|
77
|
+
parsed.map do |command, data|
|
|
78
|
+
timestamp = data["timestamp"] if data.instance_of?(Hash)
|
|
79
|
+
{command: command, age: (age_of(timestamp) if timestamp.is_a?(Numeric))}
|
|
80
|
+
end
|
|
81
|
+
rescue SystemCallError, JSON::ParserError
|
|
82
|
+
nil
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "optparse"
|
|
5
|
+
require "time"
|
|
6
|
+
require_relative "command_helpers"
|
|
7
|
+
require_relative "git"
|
|
8
|
+
require_relative "status/facts"
|
|
9
|
+
|
|
10
|
+
module SimpleCov
|
|
11
|
+
module CLI
|
|
12
|
+
module Status
|
|
13
|
+
extend CommandHelpers
|
|
14
|
+
|
|
15
|
+
extend self
|
|
16
|
+
|
|
17
|
+
def run(args, stdout:, stderr:)
|
|
18
|
+
opts, = parse_common(args)
|
|
19
|
+
document = CoverageFile.load_document(opts.fetch(:input), command: "status", stderr: stderr)
|
|
20
|
+
return 1 unless document
|
|
21
|
+
|
|
22
|
+
facts = Facts.gather(document, CLI.default_resultset)
|
|
23
|
+
opts.fetch(:json) ? stdout.puts(JSON.pretty_generate(facts)) : print_facts(facts, opts, stdout)
|
|
24
|
+
0
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def print_facts(facts, opts, stdout)
|
|
28
|
+
stdout.puts("report #{opts.fetch(:input)}")
|
|
29
|
+
report_lines(facts).each { |line| stdout.puts(" #{line}") }
|
|
30
|
+
print_resultset(facts, stdout)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def report_lines(facts)
|
|
34
|
+
[
|
|
35
|
+
facts.fetch(:generated_at) && "generated #{facts.fetch(:generated_at)}#{age_suffix(facts.fetch(:age))}",
|
|
36
|
+
provenance_words(facts),
|
|
37
|
+
"commit #{commit_words(facts)}",
|
|
38
|
+
(totals_words(facts.fetch(:totals)) unless facts.fetch(:totals).empty?),
|
|
39
|
+
"tests recorded: #{contexts_words(facts.fetch(:contexts))}"
|
|
40
|
+
].compact
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def provenance_words(facts)
|
|
44
|
+
"by simplecov #{facts.fetch(:version)} running #{facts.fetch(:command_name)}"
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def commit_words(facts)
|
|
48
|
+
commit = facts.fetch(:commit)
|
|
49
|
+
return "not recorded" unless commit.instance_of?(String)
|
|
50
|
+
|
|
51
|
+
"#{commit[0, 7]} (#{distance_words(facts.fetch(:behind))})"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def distance_words(behind)
|
|
55
|
+
return "not in this repository's history" unless behind
|
|
56
|
+
return "current HEAD" if behind.zero?
|
|
57
|
+
|
|
58
|
+
"#{behind} commit#{"s" unless one?(behind)} behind HEAD"
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def totals_words(totals)
|
|
62
|
+
totals.map { |name, percent| "#{name} #{format("%.2f%%", percent)}" }.join(", ")
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def contexts_words(count)
|
|
66
|
+
count ? "#{count} (track_tests)" : "none (enable track_tests to select and re-run by test)"
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def print_resultset(facts, stdout)
|
|
70
|
+
entries = facts.fetch(:resultset)
|
|
71
|
+
return stdout.puts("resultset none") unless entries
|
|
72
|
+
|
|
73
|
+
stdout.puts("resultset #{facts.fetch(:resultset_path)}")
|
|
74
|
+
entries.each do |entry|
|
|
75
|
+
age = entry.fetch(:age) ? "#{age_in_words(entry.fetch(:age))} ago" : "age unknown"
|
|
76
|
+
stdout.puts(" #{entry.fetch(:command)}: #{age}")
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def age_suffix(age)
|
|
81
|
+
" (#{age_in_words(age)} ago)" if age
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def age_in_words(seconds)
|
|
85
|
+
return "#{seconds.round} seconds" if seconds < 90
|
|
86
|
+
return "#{(seconds / 60.0).round} minutes" if seconds < 5400
|
|
87
|
+
return "#{(seconds / 3600.0).round} hours" if seconds < 129_600
|
|
88
|
+
|
|
89
|
+
"#{(seconds / 86_400.0).round} days"
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SimpleCov
|
|
4
|
+
module CLI
|
|
5
|
+
module Tests
|
|
6
|
+
# The `--redundant` sweep: the contexts whose covered lines other contexts
|
|
7
|
+
# also cover, the tests contributing no coverage of their own. A context is
|
|
8
|
+
# non-redundant exactly when some line is covered by it alone, so per file
|
|
9
|
+
# two bitmaps (lines seen once, lines seen more than once) find the uniquely
|
|
10
|
+
# covered lines and a second pass credits each to its owner.
|
|
11
|
+
module Redundancy
|
|
12
|
+
extend self
|
|
13
|
+
|
|
14
|
+
def redundant_ids(document, contexts, opts, stderr)
|
|
15
|
+
tables = sweep_tables(document, contexts, opts, stderr)
|
|
16
|
+
return unless tables
|
|
17
|
+
|
|
18
|
+
unique = unique_owners(tables, contexts.size)
|
|
19
|
+
contexts.each_index.reject { |index| unique.fetch(index) }.map { |index| contexts.fetch(index) }.sort
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def unique_owners(tables, context_count)
|
|
23
|
+
unique = Array.new(context_count, false)
|
|
24
|
+
tables.each do |table|
|
|
25
|
+
lone = lone_bits(table)
|
|
26
|
+
table.each { |index, bitmap| unique[index] = true if bitmap.anybits?(lone) }
|
|
27
|
+
end
|
|
28
|
+
unique
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# A bit enters `once` when brand new and leaves for good when any later
|
|
32
|
+
# bitmap carries it again. The two halves of the update are provably
|
|
33
|
+
# disjoint, so they are summed: for disjoint bits that builds the number OR
|
|
34
|
+
# would, and it leaves no spelling of the combination without a witness.
|
|
35
|
+
def lone_bits(table)
|
|
36
|
+
once = 0
|
|
37
|
+
ever = 0
|
|
38
|
+
table.each_value do |bitmap|
|
|
39
|
+
once = (once & ~bitmap) + (bitmap & ~ever)
|
|
40
|
+
ever |= bitmap
|
|
41
|
+
end
|
|
42
|
+
once
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# The sweep reads tables no query named, so a malformed one anywhere poisons
|
|
46
|
+
# the whole answer, matching the targeted queries' all-or-nothing tolerance.
|
|
47
|
+
def sweep_tables(document, contexts, opts, stderr)
|
|
48
|
+
coverage = document["coverage"]
|
|
49
|
+
return invalid(opts, stderr, '"coverage" must be an object') unless coverage.is_a?(Hash)
|
|
50
|
+
|
|
51
|
+
tables = [] #: Array[Hash[Integer, Integer]]
|
|
52
|
+
coverage.each do |path, entry|
|
|
53
|
+
table = entry.is_a?(Hash) && swept_table(entry, contexts)
|
|
54
|
+
return invalid(opts, stderr, complaint(path, entry)) unless table
|
|
55
|
+
|
|
56
|
+
tables << table
|
|
57
|
+
end
|
|
58
|
+
tables
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def swept_table(entry, contexts)
|
|
62
|
+
raw = entry["contexts"] || {}
|
|
63
|
+
Tests.decode_table(raw, contexts.size) if raw.is_a?(Hash)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def complaint(path, entry)
|
|
67
|
+
return "entry for #{path} must be an object" unless entry.is_a?(Hash)
|
|
68
|
+
|
|
69
|
+
"entry for #{path} carries a malformed \"contexts\" table"
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def invalid(opts, stderr, reason)
|
|
73
|
+
CoverageFile.report_invalid(stderr, "tests", opts.fetch(:input), reason)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
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/redundancy"
|
|
7
|
+
|
|
8
|
+
module SimpleCov
|
|
9
|
+
module CLI
|
|
10
|
+
# `simplecov tests [<path>[:<line>]]`: list the recorded tests from a
|
|
11
|
+
# coverage.json written under `track_tests`. Text output is one id per line
|
|
12
|
+
# so the list can feed a test runner directly; empty answers keep stdout
|
|
13
|
+
# empty and note the emptiness on stderr instead.
|
|
14
|
+
module Tests
|
|
15
|
+
extend CommandHelpers
|
|
16
|
+
|
|
17
|
+
extend self
|
|
18
|
+
|
|
19
|
+
def run(args, stdout:, stderr:)
|
|
20
|
+
opts = parse(args)
|
|
21
|
+
return error(stderr, "line number must be positive") if opts.fetch(:line)&.zero?
|
|
22
|
+
|
|
23
|
+
document = CoverageFile.load_document(opts.fetch(:input), command: "tests", stderr: stderr)
|
|
24
|
+
return 1 unless document
|
|
25
|
+
|
|
26
|
+
ids = resolve(document, opts, stderr)
|
|
27
|
+
return 1 unless ids
|
|
28
|
+
|
|
29
|
+
emit(ids, opts, stdout, stderr)
|
|
30
|
+
0
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def parse(args)
|
|
34
|
+
opts, rest = parse_common(args) do |parser, options|
|
|
35
|
+
parser.on("--redundant") { options[:redundant] = true }
|
|
36
|
+
end
|
|
37
|
+
opts[:target] = rest.first
|
|
38
|
+
split = split_target(rest.first)
|
|
39
|
+
opts[:path] = split.fetch(:path)
|
|
40
|
+
opts[:line] = split.fetch(:line)
|
|
41
|
+
opts
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Only an all-digit tail after the last colon reads as a line number, so
|
|
45
|
+
# Windows drive colons and exotic filenames stay plain paths.
|
|
46
|
+
def split_target(target)
|
|
47
|
+
return {path: nil, line: nil} unless target
|
|
48
|
+
|
|
49
|
+
path, sep, tail = target.rpartition(":")
|
|
50
|
+
return {path: target, line: nil} if sep.empty? || !tail.match?(/\A\d+\z/)
|
|
51
|
+
|
|
52
|
+
{path: path, line: Integer(tail, 10)}
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def resolve(document, opts, stderr)
|
|
56
|
+
contexts = recorded_contexts(document, opts, stderr)
|
|
57
|
+
return unless contexts
|
|
58
|
+
|
|
59
|
+
opts[:no_recording] = contexts.empty?
|
|
60
|
+
ids = selected_ids(document, contexts, opts, stderr)
|
|
61
|
+
return ids unless ids && opts[:redundant]
|
|
62
|
+
|
|
63
|
+
redundant = Redundancy.redundant_ids(document, contexts, opts, stderr)
|
|
64
|
+
redundant && (ids & redundant)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def selected_ids(document, contexts, opts, stderr)
|
|
68
|
+
return contexts.sort unless opts.fetch(:path)
|
|
69
|
+
|
|
70
|
+
entry = locate_entry(document, opts, stderr)
|
|
71
|
+
return unless entry
|
|
72
|
+
|
|
73
|
+
table = context_table(entry, contexts, opts, stderr)
|
|
74
|
+
table && ids_from(table, contexts, opts.fetch(:line))
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def locate_entry(document, opts, stderr)
|
|
78
|
+
coverage = document["coverage"]
|
|
79
|
+
input = opts.fetch(:input)
|
|
80
|
+
unless coverage.is_a?(Hash)
|
|
81
|
+
return CoverageFile.report_invalid(stderr, "tests", input, '"coverage" must be an object')
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
path = opts.fetch(:path)
|
|
85
|
+
match = CoverageFile.lookup(coverage, path)
|
|
86
|
+
return error_nil(stderr, CoverageFile.not_found_message(coverage, path, input)) unless match
|
|
87
|
+
return match.last if match.last.is_a?(Hash)
|
|
88
|
+
|
|
89
|
+
CoverageFile.report_invalid(stderr, "tests", input, "entry for #{path} must be an object")
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def context_table(entry, contexts, opts, stderr)
|
|
93
|
+
raw = entry["contexts"] || {}
|
|
94
|
+
table = decode_table(raw, contexts.size) if raw.is_a?(Hash)
|
|
95
|
+
return table if table
|
|
96
|
+
|
|
97
|
+
CoverageFile.report_invalid(stderr, "tests", opts.fetch(:input),
|
|
98
|
+
"entry for #{opts.fetch(:path)} carries a malformed \"contexts\" table")
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# An absent key is an untouched file; anything malformed, a foreign index or
|
|
102
|
+
# a non-hex bitmap, poisons the whole answer, matching the all-or-nothing
|
|
103
|
+
# tolerance the resultset reader applies.
|
|
104
|
+
def decode_table(raw, context_count)
|
|
105
|
+
table = {} #: Hash[Integer, Integer]
|
|
106
|
+
raw.each do |index, hex|
|
|
107
|
+
return nil unless index.is_a?(String) && index.match?(/\A\d+\z/)
|
|
108
|
+
return nil unless hex.is_a?(String) && hex.match?(/\A\h+\z/)
|
|
109
|
+
|
|
110
|
+
position = Integer(index, 10)
|
|
111
|
+
return nil unless position < context_count
|
|
112
|
+
|
|
113
|
+
table[position] = Integer(hex, 16)
|
|
114
|
+
end
|
|
115
|
+
table
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def ids_from(table, contexts, line)
|
|
119
|
+
bit = line && (1 << (line - 1))
|
|
120
|
+
table.filter_map { |index, bitmap| contexts.fetch(index) if bit.nil? || bitmap.anybits?(bit) }.sort
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def emit(ids, opts, stdout, stderr)
|
|
124
|
+
return stdout.puts(JSON.pretty_generate(ids)) if opts.fetch(:json)
|
|
125
|
+
return note_empty(opts, stderr) if ids.empty?
|
|
126
|
+
|
|
127
|
+
ids.each { |id| stdout.puts(id) }
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def note_empty(opts, stderr)
|
|
131
|
+
stderr.puts("simplecov tests: #{empty_message(opts)}")
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def empty_message(opts)
|
|
135
|
+
target = opts.fetch(:target)
|
|
136
|
+
if opts[:redundant] && !opts.fetch(:no_recording)
|
|
137
|
+
return "no redundant test covers #{target}" if target
|
|
138
|
+
|
|
139
|
+
"no redundant tests, every recorded test covers at least one line uniquely"
|
|
140
|
+
elsif target
|
|
141
|
+
"no recorded test covers #{target}"
|
|
142
|
+
else
|
|
143
|
+
"no tests recorded"
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SimpleCov
|
|
4
|
+
module CLI
|
|
5
|
+
module Uncovered
|
|
6
|
+
module Misses
|
|
7
|
+
extend self
|
|
8
|
+
|
|
9
|
+
def missed_for(payload, criterion)
|
|
10
|
+
case criterion
|
|
11
|
+
when :line then payload["lines"].instance_of?(Array) ? Show::Annotator.missed_lines(payload) : []
|
|
12
|
+
when :branch then collect(payload["branches"])
|
|
13
|
+
else collect(payload["methods"])
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def collect(items)
|
|
18
|
+
missed = [] #: Array[Integer]
|
|
19
|
+
Show::Annotator.each_missed(items) { |line| missed << line }
|
|
20
|
+
missed.uniq.sort
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def annotate(stdout, files, criterion, kind)
|
|
24
|
+
root = "#{File.expand_path(SimpleCov.root)}/"
|
|
25
|
+
diagnostics = files.flat_map do |fname, _pct, _covered, _total, missed|
|
|
26
|
+
Annotations.diagnostics(fname.delete_prefix(root), missed, criterion)
|
|
27
|
+
end
|
|
28
|
+
Annotations.emit(stdout, kind, diagnostics)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "optparse"
|
|
5
|
+
require_relative "annotations"
|
|
6
|
+
require_relative "command_helpers"
|
|
7
|
+
require_relative "patch/output"
|
|
8
|
+
require_relative "show/annotator"
|
|
9
|
+
require_relative "uncovered/misses"
|
|
10
|
+
|
|
11
|
+
module SimpleCov
|
|
12
|
+
module CLI
|
|
13
|
+
# `simplecov uncovered`: the lowest-coverage files by the chosen criterion,
|
|
14
|
+
# ascending, so a developer can answer "where should I add tests next?"
|
|
15
|
+
# without opening a browser.
|
|
16
|
+
module Uncovered
|
|
17
|
+
extend CommandHelpers
|
|
18
|
+
|
|
19
|
+
DEFAULT_TOP = 10
|
|
20
|
+
|
|
21
|
+
extend self
|
|
22
|
+
|
|
23
|
+
def run(args, stdout:, stderr:, **)
|
|
24
|
+
opts = parse(args)
|
|
25
|
+
issue = Annotations.issue(opts)
|
|
26
|
+
return error(stderr, issue) if issue
|
|
27
|
+
|
|
28
|
+
keys = CoverageFile::CRITERIA[opts.fetch(:criterion)]
|
|
29
|
+
return unknown_criterion(opts.fetch(:criterion), stderr) unless keys
|
|
30
|
+
|
|
31
|
+
opts[:missing] = true if opts.fetch(:annotate)
|
|
32
|
+
report(opts, keys, stdout, stderr)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def report(opts, keys, stdout, stderr)
|
|
36
|
+
coverage = CoverageFile.load_coverage(opts.fetch(:input), command: "uncovered", stderr: stderr)
|
|
37
|
+
return 1 unless coverage
|
|
38
|
+
|
|
39
|
+
emit(stdout, rank(coverage, opts, keys).first(opts.fetch(:top)), opts)
|
|
40
|
+
0
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def unknown_criterion(criterion, stderr)
|
|
44
|
+
stderr.puts("simplecov uncovered: unknown --criterion #{criterion.inspect} (expected line, branch, or method)")
|
|
45
|
+
1
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def emit(stdout, files, opts)
|
|
49
|
+
kind = opts.fetch(:annotate)
|
|
50
|
+
return Misses.annotate(stdout, files, opts.fetch(:criterion), kind) if kind
|
|
51
|
+
return stdout.puts(empty_message(opts.fetch(:json))) if files.empty?
|
|
52
|
+
|
|
53
|
+
opts.fetch(:json) ? emit_json(stdout, files) : emit_text(stdout, files, CLI.color_enabled?(opts, stdout))
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def parse(args)
|
|
57
|
+
opts, = parse_common(args, threshold: 100.0, top: DEFAULT_TOP, criterion: :line,
|
|
58
|
+
missing: false, annotate: nil) { |o, options| own_options(o, options) }
|
|
59
|
+
opts
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def own_options(parser, options)
|
|
63
|
+
parser.on("--threshold N", Float) { |v| options[:threshold] = v }
|
|
64
|
+
parser.on("--top N", Integer) { |v| options[:top] = validate_top(v) }
|
|
65
|
+
parser.on("--criterion C") { |v| options[:criterion] = v.to_sym }
|
|
66
|
+
parser.on("--missing") { options[:missing] = true }
|
|
67
|
+
parser.on("--annotate KIND") { |v| options[:annotate] = v }
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# A negative count would raise from `Array#first`, so it is reported as the
|
|
71
|
+
# parse error it is. `--top 0` is allowed and shows nothing. OptionParser
|
|
72
|
+
# prepends the offending option's name, so the raise carries only the reason.
|
|
73
|
+
def validate_top(value)
|
|
74
|
+
raise OptionParser::InvalidArgument, "must not be negative" if value.negative?
|
|
75
|
+
|
|
76
|
+
value
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def emit_text(stdout, files, color)
|
|
80
|
+
files.each do |fname, pct, covered, total, missed|
|
|
81
|
+
row = format_row(fname, pct, covered, total, color)
|
|
82
|
+
row += " missing #{Patch::Output.ranges(missed, ",")}" if missed&.any?
|
|
83
|
+
stdout.puts(row)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def emit_json(stdout, files)
|
|
88
|
+
rows = files.map do |fname, pct, covered, total, missed|
|
|
89
|
+
row = {"file" => fname, "percent" => pct, "covered" => covered, "total" => total}
|
|
90
|
+
row["missing"] = missed if missed
|
|
91
|
+
row
|
|
92
|
+
end
|
|
93
|
+
stdout.puts(JSON.pretty_generate(rows))
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def empty_message(json)
|
|
97
|
+
json ? "[]" : "simplecov uncovered: nothing to report"
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def rank(coverage_hash, opts, keys)
|
|
101
|
+
rows = coverage_hash.filter_map { |fname, payload| row_for(fname, payload, opts, keys) }
|
|
102
|
+
rows.sort_by { |_fname, pct, _c, _t| pct }
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def row_for(fname, payload, opts, keys)
|
|
106
|
+
return unless payload.instance_of?(Hash) && payload[keys.fetch(:total)].to_i.positive?
|
|
107
|
+
|
|
108
|
+
pct = payload[keys.fetch(:percent)].to_f
|
|
109
|
+
return if pct >= opts.fetch(:threshold)
|
|
110
|
+
|
|
111
|
+
build_row(fname, pct, payload, opts, keys)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def build_row(fname, pct, payload, opts, keys)
|
|
115
|
+
row = [fname, pct, payload[keys.fetch(:covered)].to_i, payload.fetch(keys.fetch(:total)).to_i]
|
|
116
|
+
row << Misses.missed_for(payload, opts.fetch(:criterion)) if opts.fetch(:missing)
|
|
117
|
+
row
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def format_row(fname, pct, covered, total, color)
|
|
121
|
+
format("%<pct>s %<covered>d/%<total>d %<fname>s",
|
|
122
|
+
pct: Color.colorize_percent(pct, format("%6.2f%%", pct), enabled: color),
|
|
123
|
+
covered: covered, total: total, fname: fname)
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|