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
data/lib/simplecov/cli/merge.rb
CHANGED
|
@@ -7,59 +7,56 @@ require_relative "command_helpers"
|
|
|
7
7
|
|
|
8
8
|
module SimpleCov
|
|
9
9
|
module CLI
|
|
10
|
-
# `simplecov merge <files
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
# every project. Requires the full simplecov library to be on the
|
|
14
|
-
# load path; lazy-required so the read-only subcommands above don't
|
|
15
|
-
# pay for ResultMerger (and its Coverage runtime guard).
|
|
10
|
+
# `simplecov merge <files...>`: wraps `SimpleCov::ResultMerger` so a CI
|
|
11
|
+
# matrix that produces one .resultset.json per worker can stitch them
|
|
12
|
+
# together from the shell instead of dropping a Rake task into every project.
|
|
16
13
|
module Merge
|
|
17
14
|
extend CommandHelpers
|
|
18
15
|
|
|
19
|
-
|
|
16
|
+
extend self
|
|
20
17
|
|
|
21
18
|
def run(args, stdout:, stderr:, **)
|
|
22
19
|
opts = parse(args)
|
|
23
|
-
return error(stderr, "missing input files") if opts
|
|
24
|
-
return 1 unless valid_inputs?(opts[:files], stderr)
|
|
20
|
+
return error(stderr, "missing input files") if opts.fetch(:files).empty?
|
|
25
21
|
|
|
26
|
-
|
|
27
|
-
|
|
22
|
+
parsed = parse_inputs(opts.fetch(:files), stderr)
|
|
23
|
+
return 1 unless parsed
|
|
24
|
+
|
|
25
|
+
warn_about_duplicate_command_names(parsed, stderr)
|
|
26
|
+
result = result_merger.merge_results(*opts.fetch(:files), ignore_timeout: !opts.fetch(:honor_timeout))
|
|
28
27
|
return error(stderr, "no mergeable results in input files") unless result
|
|
29
28
|
|
|
30
29
|
commit(opts, result, stdout)
|
|
31
30
|
0
|
|
32
31
|
end
|
|
33
32
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
33
|
+
# Loaded here rather than at the top of the file, so the read-only
|
|
34
|
+
# subcommands never pay for ResultMerger and its Coverage runtime guard.
|
|
35
|
+
def result_merger
|
|
36
|
+
require "simplecov"
|
|
37
|
+
ResultMerger
|
|
38
38
|
end
|
|
39
39
|
|
|
40
|
-
def
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
true
|
|
40
|
+
def commit(opts, result, stdout)
|
|
41
|
+
verb = opts.fetch(:dry_run) ? "would write" : "wrote"
|
|
42
|
+
write(opts.fetch(:output), result) unless opts.fetch(:dry_run)
|
|
43
|
+
stdout.puts("simplecov merge: #{verb} #{opts.fetch(:output)}") unless opts.fetch(:quiet)
|
|
45
44
|
end
|
|
46
45
|
|
|
47
46
|
def parse(args)
|
|
48
|
-
opts = {output:
|
|
47
|
+
opts = {output: CLI.default_resultset, honor_timeout: false, dry_run: false, quiet: false}
|
|
49
48
|
files =
|
|
50
|
-
|
|
49
|
+
build_parser do |o|
|
|
51
50
|
o.on("--output PATH") { |v| opts[:output] = v }
|
|
52
51
|
o.on("--honor-timeout") { opts[:honor_timeout] = true }
|
|
53
52
|
o.on("--dry-run") { opts[:dry_run] = true }
|
|
54
|
-
o
|
|
53
|
+
quiet_option(o, opts)
|
|
55
54
|
end.parse(args)
|
|
56
55
|
opts.merge(files: files)
|
|
57
56
|
end
|
|
58
57
|
|
|
59
|
-
#
|
|
60
|
-
#
|
|
61
|
-
# generic "no mergeable results" into a message that points at
|
|
62
|
-
# the specific input causing the failure.
|
|
58
|
+
# Validating every input up front turns ResultMerger's generic "no mergeable
|
|
59
|
+
# results" into a message that points at the specific input causing it.
|
|
63
60
|
def parse_inputs(files, stderr)
|
|
64
61
|
parsed = {} #: Hash[String, Hash[String, untyped]]
|
|
65
62
|
files.each_with_object(parsed) do |path, memo|
|
|
@@ -69,31 +66,29 @@ module SimpleCov
|
|
|
69
66
|
end
|
|
70
67
|
end
|
|
71
68
|
|
|
72
|
-
# Read first
|
|
73
|
-
#
|
|
74
|
-
#
|
|
69
|
+
# Read first and classify by the exception: an exist?-then-read pair is racy,
|
|
70
|
+
# and rescuing only ENOENT crashed with a backtrace on a directory or an
|
|
71
|
+
# unreadable file.
|
|
75
72
|
def parse_input(path, stderr)
|
|
76
73
|
data = JSON.parse(File.read(path))
|
|
77
|
-
return data if data.
|
|
74
|
+
return data if data.instance_of?(Hash) && !data.empty?
|
|
78
75
|
|
|
79
76
|
parse_input_error(stderr, path, "has no resultset entries")
|
|
80
77
|
rescue Errno::ENOENT
|
|
81
78
|
parse_input_error(stderr, path, "not found")
|
|
82
79
|
rescue JSON::ParserError => e
|
|
83
|
-
parse_input_error(stderr, path, "isn't valid JSON (#{e
|
|
80
|
+
parse_input_error(stderr, path, "isn't valid JSON (#{e})")
|
|
84
81
|
rescue SystemCallError => e
|
|
85
|
-
parse_input_error(stderr, path, "cannot be read (#{e.message.lines.first.to_s.
|
|
82
|
+
parse_input_error(stderr, path, "cannot be read (#{e.message.lines.first.to_s.rstrip})")
|
|
86
83
|
end
|
|
87
84
|
|
|
88
85
|
def parse_input_error(stderr, path, reason)
|
|
89
86
|
stderr.puts("simplecov merge: input file #{path.inspect} #{reason}")
|
|
90
|
-
nil
|
|
91
87
|
end
|
|
92
88
|
|
|
93
|
-
#
|
|
94
|
-
#
|
|
95
|
-
#
|
|
96
|
-
# operator can rename the workers or accept the merge knowingly.
|
|
89
|
+
# ResultMerger folds entries that share a command_name together with
|
|
90
|
+
# last-write-wins on the timestamp, which is easy to mistake for "no merge
|
|
91
|
+
# happened", so the overlap is surfaced.
|
|
97
92
|
def warn_about_duplicate_command_names(parsed, stderr)
|
|
98
93
|
files_per_command = {} #: Hash[String, Array[String]]
|
|
99
94
|
parsed.each do |path, data|
|
|
@@ -108,7 +103,7 @@ module SimpleCov
|
|
|
108
103
|
|
|
109
104
|
def duplicate_warning(command_name, paths)
|
|
110
105
|
"simplecov merge: warning — command_name #{command_name.inspect} " \
|
|
111
|
-
"appears in #{paths.size} input files (#{paths.join(
|
|
106
|
+
"appears in #{paths.size} input files (#{paths.join(", ")}); " \
|
|
112
107
|
"entries will be merged"
|
|
113
108
|
end
|
|
114
109
|
|
data/lib/simplecov/cli/open.rb
CHANGED
|
@@ -5,42 +5,39 @@ require_relative "command_helpers"
|
|
|
5
5
|
|
|
6
6
|
module SimpleCov
|
|
7
7
|
module CLI
|
|
8
|
-
# `simplecov open [--report PATH]` — open the HTML report in the
|
|
9
|
-
# platform's default browser. Tiny QoL wrapper around `xdg-open` /
|
|
10
|
-
# `open` / `start` so users don't have to type a file:// URL.
|
|
11
8
|
module Open
|
|
12
9
|
extend CommandHelpers
|
|
13
10
|
|
|
14
|
-
|
|
11
|
+
extend self
|
|
15
12
|
|
|
16
13
|
def run(args, stderr:, **)
|
|
17
14
|
path = parse(args)
|
|
18
15
|
return error(stderr, "#{path} not found") unless File.exist?(path)
|
|
19
16
|
|
|
20
17
|
opener = browser_opener
|
|
21
|
-
return error(stderr, "no known opener for #{RbConfig::CONFIG
|
|
18
|
+
return error(stderr, "no known opener for #{RbConfig::CONFIG.fetch("host_os")}") unless opener
|
|
22
19
|
|
|
23
20
|
system(*opener, path) ? 0 : 1
|
|
24
21
|
end
|
|
25
22
|
|
|
26
23
|
def parse(args)
|
|
27
|
-
path =
|
|
28
|
-
|
|
24
|
+
path = CLI.default_report
|
|
25
|
+
build_parser do |o|
|
|
29
26
|
o.on("--report PATH") { |v| path = v }
|
|
30
27
|
end.parse(args)
|
|
31
28
|
path
|
|
32
29
|
end
|
|
33
30
|
|
|
34
|
-
#
|
|
35
|
-
#
|
|
36
|
-
#
|
|
37
|
-
#
|
|
38
|
-
#
|
|
31
|
+
# The argv for the platform's "open this file" command, or nil if the host OS
|
|
32
|
+
# isn't recognized. On Windows, `start` is a cmd.exe builtin rather than an
|
|
33
|
+
# executable, so it routes through `cmd /c`; the empty string is the
|
|
34
|
+
# window-title positional `start` takes before the path, so a quoted path
|
|
35
|
+
# isn't mis-parsed as the title.
|
|
39
36
|
def browser_opener
|
|
40
|
-
case RbConfig::CONFIG
|
|
41
|
-
when /darwin/
|
|
37
|
+
case RbConfig::CONFIG.fetch("host_os")
|
|
38
|
+
when /darwin/ then ["open"]
|
|
42
39
|
when /mswin|mingw|cygwin/ then ["cmd", "/c", "start", ""]
|
|
43
|
-
when /linux|bsd|solaris/
|
|
40
|
+
when /linux|bsd|solaris/ then ["xdg-open"]
|
|
44
41
|
end
|
|
45
42
|
end
|
|
46
43
|
end
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../git"
|
|
4
|
+
|
|
5
|
+
module SimpleCov
|
|
6
|
+
module CLI
|
|
7
|
+
module Patch
|
|
8
|
+
# Run `git diff --unified=0` against a base and parse the result into
|
|
9
|
+
# `{root:, changes: {path => [added line numbers] | :all}}`, where
|
|
10
|
+
# `root` is the repository toplevel the paths are relative to and
|
|
11
|
+
# `:all` marks an untracked file, whose every line is new.
|
|
12
|
+
module ChangedLines
|
|
13
|
+
# A `git diff --unified=0` hunk header. Only the new-file side is
|
|
14
|
+
# captured: removed lines cannot be covered.
|
|
15
|
+
HUNK_HEADER = /\A@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@/
|
|
16
|
+
|
|
17
|
+
UNQUOTE = {
|
|
18
|
+
"\\" => "\\", '"' => '"', "n" => "\n", "t" => "\t", "r" => "\r",
|
|
19
|
+
"a" => "\a", "b" => "\b", "f" => "\f", "v" => "\v"
|
|
20
|
+
}.freeze
|
|
21
|
+
|
|
22
|
+
extend self
|
|
23
|
+
|
|
24
|
+
# nil signals "could not diff" (already reported); empty changes are a
|
|
25
|
+
# valid result meaning the change touched no lines at all. Everything
|
|
26
|
+
# runs against the repository toplevel rather than the cwd, because
|
|
27
|
+
# `--relative` from a subdirectory would exclude every change outside
|
|
28
|
+
# that subdirectory from a `--minimum` gate.
|
|
29
|
+
def call(base, find_renames:, stderr:)
|
|
30
|
+
root = git_toplevel
|
|
31
|
+
return report_git_error(stderr, base, nil) unless root
|
|
32
|
+
|
|
33
|
+
output, detail = git_diff(root, base, find_renames: find_renames)
|
|
34
|
+
return report_git_error(stderr, base, detail) unless output
|
|
35
|
+
|
|
36
|
+
# `scrub` keeps a non-UTF-8 content line from blowing up the regexp
|
|
37
|
+
# parse; hunk headers are pure ASCII.
|
|
38
|
+
changes = parse_diff(output.scrub) #: Hash[String, untyped]
|
|
39
|
+
untracked_files(root).each { |path| changes[path] = :all }
|
|
40
|
+
{root: root, changes: changes}
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def git_toplevel
|
|
44
|
+
Git.toplevel
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# `--merge-base <base>` diffs the merge base of <base> and the working
|
|
48
|
+
# tree against the working tree: the change reads as its own work even
|
|
49
|
+
# when <base> has moved on independently, and uncommitted edits count
|
|
50
|
+
# too. The rest pins the output against a user's git config so it can't
|
|
51
|
+
# skew the numbers, run code, or throw off the parse: `--no-ext-diff` /
|
|
52
|
+
# `--no-textconv` (either runs a configured command, and the latter also
|
|
53
|
+
# renumbers lines), `--no-color`, `core.quotePath=false` (emit non-ASCII
|
|
54
|
+
# paths literally, so they still match report keys),
|
|
55
|
+
# `--inter-hunk-context=0` (never merge hunks over unchanged lines,
|
|
56
|
+
# which would score those lines as touched), fixed `a/`/`b/` prefixes
|
|
57
|
+
# (so the `diff_path` strip can't be fooled by `diff.noprefix`), and
|
|
58
|
+
# `--no-renames` unless asked (so a moved file reads as all-new).
|
|
59
|
+
# stderr is kept out of the parsed stream and its first line returned as
|
|
60
|
+
# the failure detail, so "unknown revision" reaches the user.
|
|
61
|
+
def git_diff(root, base, find_renames:)
|
|
62
|
+
# Refusing an option-like ref keeps a `--base` value from being read by
|
|
63
|
+
# git as an option instead of a revision (`--output=FILE` writes to
|
|
64
|
+
# disk, `--line-prefix=` empties the diff so a `--minimum` gate passes).
|
|
65
|
+
return [nil, "a ref cannot begin with \"-\""] if Git.option_like_ref?(base)
|
|
66
|
+
|
|
67
|
+
argv = ["-C", root, "-c", "core.quotePath=false", "diff", "--unified=0",
|
|
68
|
+
"--no-color", "--no-ext-diff", "--no-textconv", "--inter-hunk-context=0",
|
|
69
|
+
"--dst-prefix=b/",
|
|
70
|
+
find_renames ? "--find-renames" : "--no-renames", "--merge-base", base, "--"]
|
|
71
|
+
stdout, detail, success = Git.capture(*argv)
|
|
72
|
+
[(stdout if success), detail]
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# A brand-new file that was never `git add`ed appears in no diff, but its
|
|
76
|
+
# lines are this change's lines all the same, and skipping it would pass
|
|
77
|
+
# a `--minimum` gate over entirely unscored new code. A failure here
|
|
78
|
+
# contributes nothing rather than failing the run.
|
|
79
|
+
def untracked_files(root)
|
|
80
|
+
stdout, _detail, success = Git.capture("-C", root, "ls-files", "--others", "--exclude-standard", "-z")
|
|
81
|
+
success ? (_ = stdout).scrub.split("\0") : []
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def report_git_error(stderr, base, detail)
|
|
85
|
+
reason = detail.to_s.empty? ? "is this a git working tree, and does the ref exist?" : detail
|
|
86
|
+
stderr.puts("simplecov patch: could not run `git diff` against #{base.inspect} (#{reason})")
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Split into per-file sections first so a file's `+++` header, its first,
|
|
90
|
+
# before any hunk, is what names the path. Inside a hunk an added line is
|
|
91
|
+
# itself `+`-prefixed, so a touched line whose own text begins with
|
|
92
|
+
# `++ ` renders as `+++ ...`; reading only the section's first `+++`
|
|
93
|
+
# keeps that content line from standing in as the header.
|
|
94
|
+
def parse_diff(output)
|
|
95
|
+
changes = {} #: Hash[String, Array[Integer]]
|
|
96
|
+
sections = output.split(/^(?=diff --git )/)
|
|
97
|
+
sections.each do |section|
|
|
98
|
+
path = section_path(section) or next
|
|
99
|
+
section.each_line do |line|
|
|
100
|
+
match = HUNK_HEADER.match(line)
|
|
101
|
+
record_hunk(changes, path, match) if match
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
changes
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# `+++ /dev/null` (a deletion) yields nil.
|
|
108
|
+
def section_path(section)
|
|
109
|
+
header = section[/^\+\+\+ .+/]
|
|
110
|
+
header && diff_path(header)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def record_hunk(changes, path, match)
|
|
114
|
+
start = Integer(match[1])
|
|
115
|
+
count = match[2] ? Integer(match[2]) : 1
|
|
116
|
+
return if count.zero? # a pure-deletion hunk adds nothing
|
|
117
|
+
|
|
118
|
+
(changes[path] ||= []).concat((start...(start + count)).to_a)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def diff_path(line)
|
|
122
|
+
raw = unquote(line[4..].to_s.chomp)
|
|
123
|
+
# git's own literal token for an absent side, not this host's null
|
|
124
|
+
# device, so File::NULL (which is "NUL" on Windows) would be wrong.
|
|
125
|
+
return nil if raw.eql?("/dev/null") # rubocop:disable Style/FileNull
|
|
126
|
+
|
|
127
|
+
raw.sub(%r{\A[ab]/}, "")
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# A path carrying a quote, backslash, or control character is emitted as
|
|
131
|
+
# `"b/lib/we\"ird.rb"` even under `core.quotePath=false` (which only
|
|
132
|
+
# covers non-ASCII), and left quoted it matches no coverage key.
|
|
133
|
+
def unquote(raw)
|
|
134
|
+
return raw unless raw.length >= 2 && raw.start_with?('"') && raw.end_with?('"')
|
|
135
|
+
|
|
136
|
+
# Unescaped as raw bytes: an octal escape is a bare byte, and mixing one
|
|
137
|
+
# into a UTF-8 string would raise mid-gsub.
|
|
138
|
+
unquoted = (_ = raw.b[1..-2]).gsub(/\\(?:[0-7]{1,3}|.)/) { |escape| unescape(escape) }
|
|
139
|
+
unquoted.force_encoding(raw.encoding).scrub
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def unescape(escape)
|
|
143
|
+
body = _ = escape[1..]
|
|
144
|
+
body.match?(/\A[0-7]/) ? Integer(body, 8).chr : UNQUOTE.fetch(body, body)
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module SimpleCov
|
|
6
|
+
module CLI
|
|
7
|
+
module Patch
|
|
8
|
+
module Output
|
|
9
|
+
OPTIONAL_CRITERIA = %i[branch method].freeze
|
|
10
|
+
|
|
11
|
+
extend self
|
|
12
|
+
|
|
13
|
+
def emit(stdout, rows, opts)
|
|
14
|
+
if opts.fetch(:json)
|
|
15
|
+
stdout.puts(JSON.pretty_generate(json_rows(rows)))
|
|
16
|
+
else
|
|
17
|
+
emit_text(stdout, rows, CLI.color_enabled?(opts, stdout))
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def emit_text(stdout, rows, color)
|
|
22
|
+
return stdout.puts("simplecov patch: no coverable lines changed") if rows.empty?
|
|
23
|
+
|
|
24
|
+
rows.sort_by! { |row| [pct(row.fetch(:line)), row.fetch(:file)] }
|
|
25
|
+
rows.each { |row| stdout.puts(format_row(row, color)) }
|
|
26
|
+
stdout.puts(format_total(rows, color))
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def format_row(row, color)
|
|
30
|
+
line = " #{criterion_cells(row, color).join(" ")} #{row.fetch(:file)}"
|
|
31
|
+
note = missing_note(row)
|
|
32
|
+
note.empty? ? line : "#{line} #{note}"
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def criterion_cells(row, color)
|
|
36
|
+
cells = [criterion_cell("lines", row.fetch(:line), color)]
|
|
37
|
+
cells << criterion_cell("branches", row.fetch(:branch), color) if measured?(row.fetch(:branch))
|
|
38
|
+
cells << criterion_cell("methods", row.fetch(:method), color) if measured?(row.fetch(:method))
|
|
39
|
+
cells
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# A "lines" cell always, "branches" and "methods" cells only when the
|
|
43
|
+
# touched lines actually carried one: a hollow 0/0 cell is noise.
|
|
44
|
+
def criterion_cell(label, stats, color)
|
|
45
|
+
percent = pct(stats)
|
|
46
|
+
cell = Color.colorize(format("%6.2f%%", percent), (percent >= 100) ? :green : :red, enabled: color)
|
|
47
|
+
"#{cell} (#{stats.fetch(:covered)}/#{stats.fetch(:relevant)}) #{label}"
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def missing_note(row)
|
|
51
|
+
parts = [] #: Array[String]
|
|
52
|
+
parts << "missing #{ranges(row.fetch(:line).fetch(:missing))}" if row.fetch(:line).fetch(:missing).any?
|
|
53
|
+
OPTIONAL_CRITERIA.each do |criterion|
|
|
54
|
+
stats = row.fetch(criterion)
|
|
55
|
+
parts << "#{criterion} #{ranges(stats.fetch(:missing))}" if measured?(stats) && stats.fetch(:missing).any?
|
|
56
|
+
end
|
|
57
|
+
parts.join(" ")
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def format_total(rows, color)
|
|
61
|
+
totals = {line: sum_stats(rows, :line), branch: sum_stats(rows, :branch), method: sum_stats(rows, :method)}
|
|
62
|
+
" Patch coverage: #{criterion_cells(totals, color).join(", ")}"
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def json_rows(rows)
|
|
66
|
+
rows.map do |row|
|
|
67
|
+
data = {file: row.fetch(:file), line: row.fetch(:line).merge(percent: pct(row.fetch(:line)))}
|
|
68
|
+
OPTIONAL_CRITERIA.each do |criterion|
|
|
69
|
+
stats = row.fetch(criterion)
|
|
70
|
+
data[criterion] = stats.merge(percent: pct(stats)) if measured?(stats)
|
|
71
|
+
end
|
|
72
|
+
data
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Branch and method stats are nil when the report never measured them, and
|
|
77
|
+
# 0/0 when the change touched none.
|
|
78
|
+
def measured?(stats)
|
|
79
|
+
stats.is_a?(Hash) && stats.fetch(:relevant).positive?
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Collapses a sorted line list into ranges: [41, 42, 43, 47] -> "41-43, 47".
|
|
83
|
+
# The show subcommand borrows this with a bare-comma separator for its
|
|
84
|
+
# greppable `path:40,52-58,71` form.
|
|
85
|
+
def ranges(numbers, separator = ", ")
|
|
86
|
+
numbers.slice_when { |prev, curr| curr > prev + 1 }.each_with_object(+"") do |run, out|
|
|
87
|
+
out << separator unless out.empty?
|
|
88
|
+
out << (run.one? ? only(run) : span(run))
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# mutant:disable
|
|
93
|
+
def only(run)
|
|
94
|
+
format("%<only>d", only: run.first)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def span(run)
|
|
98
|
+
format("%<from>d-%<to>d", from: run.first, to: run.last)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def sum_stats(rows, criterion)
|
|
102
|
+
stats = rows.filter_map { |row| row.fetch(criterion) }
|
|
103
|
+
{covered: stats.sum { |stat| stat.fetch(:covered) }, relevant: stats.sum { |stat| stat.fetch(:relevant) }}
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def pct(stats)
|
|
107
|
+
relevant = stats.fetch(:relevant)
|
|
108
|
+
return 100.0 if relevant.zero?
|
|
109
|
+
|
|
110
|
+
(stats.fetch(:covered).to_f / relevant * 100).round(2)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# No --minimum is report-only. With one, every measured criterion must clear
|
|
114
|
+
# the floor, and `short?` never fails an unmeasured one.
|
|
115
|
+
def gate(rows, minimum)
|
|
116
|
+
return 0 unless minimum
|
|
117
|
+
|
|
118
|
+
below = %i[line branch method].any? { |criterion| short?(sum_stats(rows, criterion), minimum) }
|
|
119
|
+
below ? 1 : 0
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Cross-multiplied rather than compared as percentages so neither rounding
|
|
123
|
+
# nor float division can shift the verdict at the boundary: reading the
|
|
124
|
+
# displayed `pct` would round a 19_999/20_000 patch up to 100 and pass it
|
|
125
|
+
# against `--minimum 100`, while dividing makes 23/40 compute as 57.4999...
|
|
126
|
+
# and fail an exactly-57.5% patch. The minimum becomes an exact Rational
|
|
127
|
+
# from its own text, and `covered * 100` is an integer, so the comparison is
|
|
128
|
+
# exact. A criterion with nothing relevant never falls short.
|
|
129
|
+
def short?(stats, minimum)
|
|
130
|
+
stats.fetch(:covered) * 100 < Rational(minimum.to_s) * stats.fetch(:relevant)
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "command_helpers"
|
|
4
|
+
require_relative "patch/changed_lines"
|
|
5
|
+
require_relative "patch/output"
|
|
6
|
+
|
|
7
|
+
module SimpleCov
|
|
8
|
+
module CLI
|
|
9
|
+
# `simplecov patch [--base REF]`: line coverage over only the lines a
|
|
10
|
+
# change touched. Where `diff` asks "did the number move" and needs a
|
|
11
|
+
# baseline artifact, `patch` asks the question a reviewer actually asks: is
|
|
12
|
+
# the code in this change tested? A project sitting at 40% cannot move its
|
|
13
|
+
# global number in one pull request, but it can insist that every line it
|
|
14
|
+
# adds is covered.
|
|
15
|
+
#
|
|
16
|
+
# Only files the report already carries are scored: a changed file
|
|
17
|
+
# SimpleCov never tracked is out of scope, and a line `LinesClassifier`
|
|
18
|
+
# considers never relevant stays out of the denominator the same way it
|
|
19
|
+
# stays out of the file total.
|
|
20
|
+
#
|
|
21
|
+
module Patch
|
|
22
|
+
extend CommandHelpers
|
|
23
|
+
|
|
24
|
+
extend self
|
|
25
|
+
|
|
26
|
+
def run(args, stdout:, stderr:, **)
|
|
27
|
+
opts = parse(args, stderr)
|
|
28
|
+
return 1 unless opts
|
|
29
|
+
|
|
30
|
+
# An omitted --base resolves through origin's HEAD, so master and trunk
|
|
31
|
+
# repositories work bare; in CI, pass the pull request's target branch
|
|
32
|
+
# explicitly.
|
|
33
|
+
opts[:base] ||= Git.default_base
|
|
34
|
+
diffed = ChangedLines.call(opts.fetch(:base), find_renames: opts.fetch(:find_renames), stderr: stderr)
|
|
35
|
+
return 1 unless diffed
|
|
36
|
+
|
|
37
|
+
rows = compute_rows(opts.fetch(:coverage), diffed, stderr)
|
|
38
|
+
Output.emit(stdout, rows, opts)
|
|
39
|
+
Output.gate(rows, opts.fetch(:minimum))
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def parse(args, stderr)
|
|
43
|
+
# No `base:` default: the run fills it in from the repository when the
|
|
44
|
+
# option is left out.
|
|
45
|
+
opts, rest = parse_common(args, find_renames: false, minimum: nil) do |parser, options|
|
|
46
|
+
parser.on("--base REF") { |v| options[:base] = v }
|
|
47
|
+
parser.on("--minimum N", Float) { |v| options[:minimum] = v }
|
|
48
|
+
parser.on("--find-renames") { options[:find_renames] = true }
|
|
49
|
+
end
|
|
50
|
+
return unless positional_ok?(rest, stderr)
|
|
51
|
+
|
|
52
|
+
opts[:coverage] = CoverageFile.load_coverage(opts.fetch(:input), command: "patch", stderr: stderr) or return nil
|
|
53
|
+
opts
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# A stray positional looks exactly like a ref, so a forgotten `--base`
|
|
57
|
+
# (`simplecov patch feature-x`) would otherwise diff against the default
|
|
58
|
+
# and gate the wrong change in silence.
|
|
59
|
+
def positional_ok?(rest, stderr)
|
|
60
|
+
return true if rest.empty?
|
|
61
|
+
|
|
62
|
+
error(stderr, "unexpected argument #{rest.first.inspect} (did you mean `--base #{rest.first}`?)")
|
|
63
|
+
false
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Everything below reads values that came out of a coverage.json, which
|
|
67
|
+
# carries plain objects, arrays and integers and never a subclass of one,
|
|
68
|
+
# so the shape checks ask about the class itself.
|
|
69
|
+
|
|
70
|
+
# Diff paths are exact root-relative names, so they resolve exactly against
|
|
71
|
+
# the report. The suffix fallback `CoverageFile.lookup` offers interactive
|
|
72
|
+
# commands could only ever bind a changed file the report doesn't carry to
|
|
73
|
+
# some other file's hits and score the wrong entry.
|
|
74
|
+
def compute_rows(coverage, diffed, stderr)
|
|
75
|
+
index = CoverageFile.exact_index(coverage)
|
|
76
|
+
diffed.fetch(:changes).filter_map do |path, lines|
|
|
77
|
+
payload = index[File.expand_path(path, diffed.fetch(:root))] || index[path]
|
|
78
|
+
next unless payload.instance_of?(Hash) # file the report doesn't carry -> out of scope
|
|
79
|
+
|
|
80
|
+
changed = changed_for(lines, payload)
|
|
81
|
+
warn_stale(path, payload, changed, stderr)
|
|
82
|
+
row = build_row(path, payload, changed)
|
|
83
|
+
row if scored?(row) # nothing coverable changed in this file
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# An untracked file appears in no diff, so `:all` stands in for its line
|
|
88
|
+
# numbers: every line the report knows is this change's work.
|
|
89
|
+
def changed_for(lines, payload)
|
|
90
|
+
return lines.uniq unless lines.equal?(:all)
|
|
91
|
+
|
|
92
|
+
hits = payload["lines"]
|
|
93
|
+
hits.instance_of?(Array) ? (1..hits.length).to_a : []
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# A changed line past the end of the report's lines array reads as
|
|
97
|
+
# never-relevant and silently drops out of the denominator, which is right
|
|
98
|
+
# for a fresh report and wrong for a stale one, so say which is likelier
|
|
99
|
+
# out loud instead of letting a `--minimum` gate pass vacuously.
|
|
100
|
+
def warn_stale(path, payload, changed, stderr)
|
|
101
|
+
hits = payload["lines"]
|
|
102
|
+
return unless hits.instance_of?(Array)
|
|
103
|
+
return if changed.empty? || changed.max <= hits.length
|
|
104
|
+
|
|
105
|
+
stderr.puts("simplecov patch: #{path} changed beyond the #{hits.length}-line entry in the " \
|
|
106
|
+
"report (is the report stale? regenerate it and rerun)")
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def build_row(path, payload, changed)
|
|
110
|
+
{
|
|
111
|
+
file: path,
|
|
112
|
+
line: line_stats(payload["lines"], changed),
|
|
113
|
+
branch: entry_stats(payload["branches"], changed),
|
|
114
|
+
method: entry_stats(payload["methods"], changed)
|
|
115
|
+
}
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# A line counts only where the report gives it an Integer hit count; a
|
|
119
|
+
# never-relevant line (nil) or a `:nocov:` line ("ignored") stays out of
|
|
120
|
+
# the denominator, the same rule the file total uses.
|
|
121
|
+
def line_stats(hits, changed)
|
|
122
|
+
covered = 0
|
|
123
|
+
missing = [] #: Array[Integer]
|
|
124
|
+
return {covered: covered, relevant: 0, missing: missing} unless hits.instance_of?(Array)
|
|
125
|
+
|
|
126
|
+
changed.each do |number|
|
|
127
|
+
hit = hits.at(number - 1)
|
|
128
|
+
# nil / "ignored" (nocov) / any non-Integer -> never relevant, skipped.
|
|
129
|
+
next unless hit.instance_of?(Integer)
|
|
130
|
+
|
|
131
|
+
hit.positive? ? (covered += 1) : (missing << number)
|
|
132
|
+
end
|
|
133
|
+
{covered: covered, relevant: covered + missing.size, missing: missing.sort}
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Branches and methods share the report shape (a reported line and an
|
|
137
|
+
# integer hit count), so one scorer serves both. nil when the report
|
|
138
|
+
# carries no data for the criterion, so the output and gate skip it
|
|
139
|
+
# rather than reporting a hollow 0/0. A miss is recorded at the reported
|
|
140
|
+
# line so the note points where the source is.
|
|
141
|
+
def entry_stats(entries, changed)
|
|
142
|
+
return nil unless entries.instance_of?(Array)
|
|
143
|
+
|
|
144
|
+
covered = 0
|
|
145
|
+
missing = [] #: Array[Integer]
|
|
146
|
+
each_touched(entries, changed) do |line, hits|
|
|
147
|
+
hits.positive? ? (covered += 1) : (missing << line)
|
|
148
|
+
end
|
|
149
|
+
{covered: covered, relevant: covered + missing.size, missing: missing.uniq.sort}
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# "ignored" (nocov) entries and entries off the change are skipped.
|
|
153
|
+
def each_touched(entries, changed)
|
|
154
|
+
entries.each do |entry|
|
|
155
|
+
next unless entry.instance_of?(Hash)
|
|
156
|
+
|
|
157
|
+
line = entry["report_line"] || entry["start_line"]
|
|
158
|
+
hits = entry["coverage"]
|
|
159
|
+
yield line, hits if changed.include?(line) && hits.instance_of?(Integer)
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def scored?(row)
|
|
164
|
+
row.fetch(:line).fetch(:relevant).positive? ||
|
|
165
|
+
Output.measured?(row.fetch(:branch)) || Output.measured?(row.fetch(:method))
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
end
|