simplecov 1.0.0 → 1.0.2
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/lib/simplecov/cli/coverage.rb +5 -3
- data/lib/simplecov/cli/diff.rb +2 -1
- data/lib/simplecov/cli/dotfile.rb +9 -4
- data/lib/simplecov/cli/merge.rb +5 -3
- data/lib/simplecov/cli/report.rb +11 -6
- data/lib/simplecov/cli/run.rb +1 -1
- data/lib/simplecov/cli/serve.rb +17 -7
- data/lib/simplecov/cli/uncovered.rb +2 -1
- data/lib/simplecov/color.rb +6 -1
- data/lib/simplecov/combine/branches_combiner.rb +2 -1
- data/lib/simplecov/combine/files_combiner.rb +42 -2
- data/lib/simplecov/combine/methods_combiner.rb +27 -4
- data/lib/simplecov/combine/results_combiner.rb +2 -1
- data/lib/simplecov/configuration/coverage.rb +6 -2
- data/lib/simplecov/configuration/coverage_criteria.rb +3 -1
- data/lib/simplecov/configuration/formatting.rb +17 -7
- data/lib/simplecov/configuration/thresholds.rb +13 -9
- data/lib/simplecov/configuration.rb +22 -13
- data/lib/simplecov/coverage_statistics.rb +4 -1
- data/lib/simplecov/coverage_violations.rb +2 -1
- data/lib/simplecov/defaults.rb +3 -3
- data/lib/simplecov/directive.rb +8 -3
- data/lib/simplecov/exit_codes/maximum_coverage_drop_check.rb +1 -1
- data/lib/simplecov/exit_codes/maximum_overall_coverage_check.rb +1 -1
- data/lib/simplecov/exit_codes/minimum_coverage_by_file_check.rb +1 -1
- data/lib/simplecov/exit_codes/minimum_coverage_by_group_check.rb +1 -1
- data/lib/simplecov/exit_codes/minimum_overall_coverage_check.rb +3 -3
- data/lib/simplecov/exit_codes.rb +12 -0
- data/lib/simplecov/exit_handling.rb +3 -3
- data/lib/simplecov/file_list.rb +12 -5
- data/lib/simplecov/formatter/base.rb +2 -1
- data/lib/simplecov/formatter/html_formatter.rb +13 -7
- data/lib/simplecov/formatter/json_formatter/errors_formatter.rb +9 -3
- data/lib/simplecov/formatter/json_formatter/result_hash_formatter.rb +1 -1
- data/lib/simplecov/formatter/json_formatter/source_file_formatter.rb +1 -1
- data/lib/simplecov/formatter/json_formatter.rb +4 -2
- data/lib/simplecov/formatter/multi_formatter.rb +7 -2
- data/lib/simplecov/formatter/simple_formatter.rb +3 -1
- data/lib/simplecov/process.rb +22 -0
- data/lib/simplecov/result_adapter.rb +45 -10
- data/lib/simplecov/result_processing.rb +4 -4
- data/lib/simplecov/simulate_coverage.rb +2 -2
- data/lib/simplecov/source_file/line.rb +2 -2
- data/lib/simplecov/source_file/method.rb +1 -1
- data/lib/simplecov/source_file/method_builder.rb +3 -1
- data/lib/simplecov/source_file/source_loader.rb +2 -2
- data/lib/simplecov/source_file.rb +2 -2
- data/lib/simplecov/static_coverage_extractor/condition_folding.rb +49 -0
- data/lib/simplecov/static_coverage_extractor/location_conventions.rb +247 -0
- data/lib/simplecov/static_coverage_extractor/value_position.rb +82 -0
- data/lib/simplecov/static_coverage_extractor/visitor.rb +71 -46
- data/lib/simplecov/version.rb +1 -1
- data/lib/simplecov.rb +24 -11
- data/sig/simplecov.rbs +1638 -0
- metadata +7 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1f159cd5427690b20f11c1e4a3f1a90c23c16f5f65e2043205bf132a5abc0218
|
|
4
|
+
data.tar.gz: d5a28a2c529ff2b59652bd0c1ba1f9d649c7c24fd02521905c6d2cbe6b1eeae3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 792c138d327b5d305ebe273dfd5a97d70eb8104102557d5ae6452a26d5cc64e1db7720c3de5e2c556a78b22561c53778579909ddef7f4615e1c447f041fa921c
|
|
7
|
+
data.tar.gz: 9f6479ceabbd5f6fc24585a2134a0f4e1270dbd988922d4d387e5695055b811655999b3d95c2a9b31c960cf0c22aa884d5bf94eae93ca6fb97868bbec0de3aab
|
|
@@ -28,7 +28,7 @@ module SimpleCov
|
|
|
28
28
|
end
|
|
29
29
|
|
|
30
30
|
def parse(args, stderr:)
|
|
31
|
-
opts = {input: SimpleCov::CLI.default_input, json: false, no_color: false}
|
|
31
|
+
opts = {input: SimpleCov::CLI.default_input, json: false, no_color: false} #: Hash[Symbol, untyped]
|
|
32
32
|
rest =
|
|
33
33
|
OptionParser.new do |o|
|
|
34
34
|
o.on("--input PATH") { |v| opts[:input] = v }
|
|
@@ -45,7 +45,8 @@ module SimpleCov
|
|
|
45
45
|
return stderr.puts("simplecov coverage: #{opts[:input]} not found") && nil unless File.exist?(opts[:input])
|
|
46
46
|
|
|
47
47
|
data = JSON.parse(File.read(opts[:input]))
|
|
48
|
-
|
|
48
|
+
none = {} #: Hash[String, untyped]
|
|
49
|
+
match = lookup(data.fetch("coverage", none), opts[:path])
|
|
49
50
|
return match if match
|
|
50
51
|
|
|
51
52
|
stderr.puts("simplecov coverage: no entry for #{opts[:path]} in #{opts[:input]}")
|
|
@@ -65,7 +66,8 @@ module SimpleCov
|
|
|
65
66
|
def emit(match, opts, stdout)
|
|
66
67
|
filename, payload = match
|
|
67
68
|
if opts[:json]
|
|
68
|
-
|
|
69
|
+
entry = {filename => payload} #: Hash[untyped, untyped]
|
|
70
|
+
stdout.puts(JSON.pretty_generate(entry))
|
|
69
71
|
else
|
|
70
72
|
print_human(filename, payload, stdout, SimpleCov::CLI.color_enabled?(opts, stdout))
|
|
71
73
|
end
|
data/lib/simplecov/cli/diff.rb
CHANGED
|
@@ -68,7 +68,8 @@ module SimpleCov
|
|
|
68
68
|
end
|
|
69
69
|
|
|
70
70
|
def load_coverage(path, stderr)
|
|
71
|
-
|
|
71
|
+
no_coverage = {} #: Hash[String, untyped]
|
|
72
|
+
return normalize_keys(JSON.parse(File.read(path)).fetch("coverage", no_coverage)) if File.exist?(path)
|
|
72
73
|
|
|
73
74
|
stderr.puts("simplecov diff: #{path} not found")
|
|
74
75
|
nil
|
|
@@ -57,8 +57,8 @@ module SimpleCov
|
|
|
57
57
|
end
|
|
58
58
|
|
|
59
59
|
def with_simplecov_loaded
|
|
60
|
-
previous_no_defaults = ENV.fetch("SIMPLECOV_NO_DEFAULTS", nil)
|
|
61
|
-
previous_cli = ENV.fetch("SIMPLECOV_CLI", nil)
|
|
60
|
+
previous_no_defaults = ENV.fetch("SIMPLECOV_NO_DEFAULTS", nil) #: String?
|
|
61
|
+
previous_cli = ENV.fetch("SIMPLECOV_CLI", nil) #: String?
|
|
62
62
|
ENV["SIMPLECOV_NO_DEFAULTS"] = "1"
|
|
63
63
|
# SIMPLECOV_CLI lets a project's `.simplecov` opt some config into
|
|
64
64
|
# CLI-only behavior — e.g. simplecov itself sets `coverage_dir`
|
|
@@ -67,6 +67,8 @@ module SimpleCov
|
|
|
67
67
|
require "simplecov"
|
|
68
68
|
yield
|
|
69
69
|
ensure
|
|
70
|
+
# @type var previous_no_defaults: String?
|
|
71
|
+
# @type var previous_cli: String?
|
|
70
72
|
ENV["SIMPLECOV_NO_DEFAULTS"] = previous_no_defaults
|
|
71
73
|
ENV["SIMPLECOV_CLI"] = previous_cli
|
|
72
74
|
end
|
|
@@ -79,20 +81,23 @@ module SimpleCov
|
|
|
79
81
|
def load_with_start_neutered(path)
|
|
80
82
|
klass = SimpleCov.singleton_class
|
|
81
83
|
names = %i[start_tracking install_at_exit_hook]
|
|
82
|
-
stash = names.to_h { |name| [name, klass.instance_method(name)] }
|
|
84
|
+
stash = names.to_h { |name| [name, klass.instance_method(name)] } #: Hash[Symbol, UnboundMethod]
|
|
83
85
|
# define_method over an existing method emits a "method redefined"
|
|
84
86
|
# warning under $VERBOSE; the override and restore are intentional.
|
|
85
87
|
silence_verbose { names.each { |name| klass.define_method(name) { nil } } }
|
|
86
88
|
load path
|
|
87
89
|
ensure
|
|
90
|
+
# @type var stash: Hash[Symbol, UnboundMethod]
|
|
91
|
+
# @type var klass: Class
|
|
88
92
|
silence_verbose { stash.each { |name, method| klass.define_method(name, method) } }
|
|
89
93
|
end
|
|
90
94
|
|
|
91
95
|
def silence_verbose
|
|
92
|
-
previous = $VERBOSE
|
|
96
|
+
previous = $VERBOSE #: bool?
|
|
93
97
|
$VERBOSE = nil
|
|
94
98
|
yield
|
|
95
99
|
ensure
|
|
100
|
+
# @type var previous: bool?
|
|
96
101
|
$VERBOSE = previous
|
|
97
102
|
end
|
|
98
103
|
end
|
data/lib/simplecov/cli/merge.rb
CHANGED
|
@@ -57,7 +57,8 @@ module SimpleCov
|
|
|
57
57
|
# generic "no mergeable results" into a message that points at
|
|
58
58
|
# the specific input causing the failure.
|
|
59
59
|
def parse_inputs(files, stderr)
|
|
60
|
-
|
|
60
|
+
parsed = {} #: Hash[String, Hash[String, untyped]]
|
|
61
|
+
files.each_with_object(parsed) do |path, memo|
|
|
61
62
|
data = parse_input(path, stderr) or return nil
|
|
62
63
|
|
|
63
64
|
memo[path] = data
|
|
@@ -85,8 +86,9 @@ module SimpleCov
|
|
|
85
86
|
# mistake for "no merge happened." Surface the overlap so the
|
|
86
87
|
# operator can rename the workers or accept the merge knowingly.
|
|
87
88
|
def warn_about_duplicate_command_names(parsed, stderr)
|
|
88
|
-
files_per_command =
|
|
89
|
-
|
|
89
|
+
files_per_command = {} #: Hash[String, Array[String]]
|
|
90
|
+
parsed.each do |path, data|
|
|
91
|
+
data.each_key { |command_name| (files_per_command[command_name] ||= []) << path }
|
|
90
92
|
end
|
|
91
93
|
files_per_command.each do |command_name, paths|
|
|
92
94
|
next if paths.size < 2
|
data/lib/simplecov/cli/report.rb
CHANGED
|
@@ -45,8 +45,9 @@ module SimpleCov
|
|
|
45
45
|
end
|
|
46
46
|
|
|
47
47
|
def emit_text(stdout, data, color)
|
|
48
|
-
|
|
49
|
-
data.fetch("
|
|
48
|
+
none = {} #: Hash[String, untyped]
|
|
49
|
+
emit_totals(stdout, "All Files", data.fetch("total", none), color)
|
|
50
|
+
data.fetch("groups", none).each { |name, group| emit_totals(stdout, name, group, color) }
|
|
50
51
|
end
|
|
51
52
|
|
|
52
53
|
def emit_totals(stdout, label, totals, color)
|
|
@@ -66,17 +67,21 @@ module SimpleCov
|
|
|
66
67
|
end
|
|
67
68
|
|
|
68
69
|
def emit_json(stdout, data)
|
|
69
|
-
|
|
70
|
-
data.fetch("
|
|
70
|
+
none = {} #: Hash[String, untyped]
|
|
71
|
+
payload = {"All Files" => collect_section(data.fetch("total", none))}
|
|
72
|
+
data.fetch("groups", none).each { |name, group| payload[name] = collect_section(group) }
|
|
71
73
|
stdout.puts(JSON.pretty_generate(payload))
|
|
72
74
|
end
|
|
73
75
|
|
|
74
76
|
def collect_section(totals)
|
|
75
|
-
|
|
77
|
+
sections = {} #: Hash[String, untyped]
|
|
78
|
+
SECTIONS.each_with_object(sections) do |(_, key), out|
|
|
76
79
|
section = totals[key]
|
|
77
80
|
next unless section.is_a?(Hash) && section["total"].to_i.positive?
|
|
78
81
|
|
|
79
|
-
out[key] = {
|
|
82
|
+
out[key.to_s] = {
|
|
83
|
+
"percent" => section["percent"], "covered" => section["covered"], "total" => section["total"]
|
|
84
|
+
}
|
|
80
85
|
end
|
|
81
86
|
end
|
|
82
87
|
end
|
data/lib/simplecov/cli/run.rb
CHANGED
|
@@ -8,7 +8,7 @@ module SimpleCov
|
|
|
8
8
|
# without a test_helper that already calls SimpleCov.start (e.g.
|
|
9
9
|
# plain `bundle exec rake test` on an unconfigured library).
|
|
10
10
|
module Run
|
|
11
|
-
AUTOSTART = File.expand_path("../autostart", __dir__)
|
|
11
|
+
AUTOSTART = File.expand_path("../autostart", __dir__.to_s)
|
|
12
12
|
|
|
13
13
|
module_function
|
|
14
14
|
|
data/lib/simplecov/cli/serve.rb
CHANGED
|
@@ -36,16 +36,26 @@ module SimpleCov
|
|
|
36
36
|
return error(stderr, "#{dir} doesn't exist; run your test suite first") unless File.directory?(dir)
|
|
37
37
|
|
|
38
38
|
require "socket"
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
39
|
+
with_server(opts) do |server|
|
|
40
|
+
announce(stdout, server, dir)
|
|
41
|
+
serve_loop(server, dir, stdout)
|
|
42
|
+
0
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def with_server(opts)
|
|
47
|
+
# The receiver cast works around an rbs stdlib gap: TCPSocket's
|
|
48
|
+
# explicit `self.new` shadows TCPServer#initialize's (host, port) form.
|
|
49
|
+
server = (_ = TCPServer).new(opts[:host], opts[:port]) #: TCPServer
|
|
50
|
+
begin
|
|
51
|
+
yield server
|
|
52
|
+
ensure
|
|
53
|
+
server.close
|
|
54
|
+
end
|
|
45
55
|
end
|
|
46
56
|
|
|
47
57
|
def parse(args)
|
|
48
|
-
opts = {port: 0, host: "127.0.0.1"}
|
|
58
|
+
opts = {port: 0, host: "127.0.0.1"} #: Hash[Symbol, untyped]
|
|
49
59
|
OptionParser.new do |o|
|
|
50
60
|
o.on("--port N", Integer) { |v| opts[:port] = v }
|
|
51
61
|
o.on("--host HOST") { |v| opts[:host] = v }
|
|
@@ -33,7 +33,8 @@ module SimpleCov
|
|
|
33
33
|
def report(opts, keys, stdout, stderr)
|
|
34
34
|
return 1 unless (data = Report.load_data(opts[:input], stderr))
|
|
35
35
|
|
|
36
|
-
|
|
36
|
+
none = {} #: Hash[String, untyped]
|
|
37
|
+
files = rank(data.fetch("coverage", none), opts[:threshold], keys).first(opts[:top])
|
|
37
38
|
return stdout.puts(empty_message(opts[:json])) || 0 if files.empty?
|
|
38
39
|
|
|
39
40
|
emit(stdout, files, opts)
|
data/lib/simplecov/color.rb
CHANGED
|
@@ -34,7 +34,12 @@ module SimpleCov
|
|
|
34
34
|
# doesn't get ANSI sequences. See the module-level comment for
|
|
35
35
|
# precedence.
|
|
36
36
|
def enabled?(stream = $stderr)
|
|
37
|
-
|
|
37
|
+
# `SimpleCov.color` only exists once the full library is loaded.
|
|
38
|
+
# The standalone CLI (`exe/simplecov`) loads `simplecov/color`
|
|
39
|
+
# without `simplecov` itself to stay lightweight, so treat a
|
|
40
|
+
# missing config the same as its `:auto` default: fall through to
|
|
41
|
+
# the env vars and tty check below.
|
|
42
|
+
config = SimpleCov.color if SimpleCov.respond_to?(:color)
|
|
38
43
|
return config if [true, false].include?(config)
|
|
39
44
|
return false if env_set?("NO_COLOR")
|
|
40
45
|
return true if env_set?("FORCE_COLOR")
|
|
@@ -24,7 +24,8 @@ module SimpleCov
|
|
|
24
24
|
# @return [Hash]
|
|
25
25
|
#
|
|
26
26
|
def combine(coverage_a, coverage_b)
|
|
27
|
-
|
|
27
|
+
merged = {} #: Hash[untyped, [untyped, Hash[untyped, untyped]]]
|
|
28
|
+
combined = [coverage_a, coverage_b].each_with_object(merged) do |coverage, memo|
|
|
28
29
|
coverage.each do |condition, branches_inside|
|
|
29
30
|
condition_key = tuple_identity(condition)
|
|
30
31
|
condition_tuple, merged_branches = memo[condition_key] ||= [condition, {}]
|
|
@@ -9,22 +9,62 @@ module SimpleCov
|
|
|
9
9
|
module FilesCombiner
|
|
10
10
|
module_function
|
|
11
11
|
|
|
12
|
+
empty_table = {} #: Hash[untyped, untyped]
|
|
13
|
+
EMPTY_TABLE = empty_table.freeze
|
|
14
|
+
private_constant :EMPTY_TABLE
|
|
15
|
+
|
|
16
|
+
# Branch/method tuples drawn from a simulated (never-loaded) file
|
|
17
|
+
# when the other side of the merge was actually executed.
|
|
18
|
+
NO_SYNTHESIZED = {"branches" => EMPTY_TABLE, "methods" => EMPTY_TABLE}.freeze
|
|
19
|
+
|
|
12
20
|
#
|
|
13
21
|
# Combines the results for 2 coverages of a file.
|
|
14
22
|
#
|
|
15
23
|
# @return [Hash]
|
|
16
24
|
#
|
|
17
25
|
def combine(coverage_a, coverage_b)
|
|
26
|
+
source_a, source_b = reconcile_synthesized(coverage_a, coverage_b)
|
|
27
|
+
|
|
18
28
|
combination = {"lines" => Combine.combine(LinesCombiner, coverage_a["lines"], coverage_b["lines"])}
|
|
19
29
|
if SimpleCov.branch_coverage?
|
|
20
|
-
combined_branches = Combine.combine(BranchesCombiner,
|
|
30
|
+
combined_branches = Combine.combine(BranchesCombiner, source_a["branches"], source_b["branches"])
|
|
21
31
|
combination["branches"] = combined_branches || {}
|
|
22
32
|
end
|
|
23
33
|
if SimpleCov.method_coverage?
|
|
24
|
-
combination["methods"] = Combine.combine(MethodsCombiner,
|
|
34
|
+
combination["methods"] = Combine.combine(MethodsCombiner, source_a["methods"], source_b["methods"])
|
|
25
35
|
end
|
|
26
36
|
combination
|
|
27
37
|
end
|
|
38
|
+
|
|
39
|
+
# When exactly one side of the merge was actually executed, its branch
|
|
40
|
+
# and method tuples are authoritative and the other side's are dropped.
|
|
41
|
+
# A simulated entry (SimulateCoverage backfills tracked-but-unloaded
|
|
42
|
+
# files) synthesizes those tuples statically, so a location that drifts
|
|
43
|
+
# from what Coverage emits would otherwise be unioned in by position
|
|
44
|
+
# and survive as a phantom, permanently-missed branch (see #1233). This
|
|
45
|
+
# contains any such drift to denominator inflation for files no process
|
|
46
|
+
# loaded, rather than a false miss on a covered file. Lines are never
|
|
47
|
+
# dropped: a simulated file's line shape is correct and carries the
|
|
48
|
+
# unloaded-file denominator (#1059).
|
|
49
|
+
#
|
|
50
|
+
# Returns the two coverages to draw branch/method tuples from, blanking
|
|
51
|
+
# the non-executed side only when the other side was executed. When
|
|
52
|
+
# both sides were executed (two real runs) or neither was (all
|
|
53
|
+
# simulated), both are returned unchanged and combine normally.
|
|
54
|
+
def reconcile_synthesized(coverage_a, coverage_b)
|
|
55
|
+
executed_a = executed?(coverage_a)
|
|
56
|
+
executed_b = executed?(coverage_b)
|
|
57
|
+
return [coverage_a, coverage_b] if executed_a == executed_b
|
|
58
|
+
|
|
59
|
+
executed_a ? [coverage_a, NO_SYNTHESIZED] : [NO_SYNTHESIZED, coverage_b]
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# A file some process actually loaded has at least one executed line;
|
|
63
|
+
# a simulated (never-loaded) file's lines are all `nil` or `0`.
|
|
64
|
+
def executed?(coverage)
|
|
65
|
+
lines = Array(coverage["lines"]) #: Array[Integer?]
|
|
66
|
+
lines.any? { |count| count&.positive? }
|
|
67
|
+
end
|
|
28
68
|
end
|
|
29
69
|
end
|
|
30
70
|
end
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "../source_file/ruby_data_parser"
|
|
4
|
+
|
|
3
5
|
module SimpleCov
|
|
4
6
|
module Combine
|
|
5
7
|
#
|
|
@@ -12,14 +14,35 @@ module SimpleCov
|
|
|
12
14
|
#
|
|
13
15
|
# Return merged methods or the existing methods if other is missing.
|
|
14
16
|
#
|
|
15
|
-
# Method coverage
|
|
16
|
-
#
|
|
17
|
-
#
|
|
17
|
+
# Method coverage maps `[class, name, start_line, start_col, end_line,
|
|
18
|
+
# end_col]` keys to hit counts. Keys are matched on their SOURCE
|
|
19
|
+
# identity — (name, location), ignoring the class element — because
|
|
20
|
+
# Ruby records one entry per receiver: the same `define_method` block
|
|
21
|
+
# defined onto different classes in different processes arrives with
|
|
22
|
+
# different (normalized) receivers for the same source method, and
|
|
23
|
+
# matching on the full key would keep both, letting a never-called
|
|
24
|
+
# receiver's 0 shadow a covered method after merge (issue #1234).
|
|
25
|
+
# Combining sums the hit counts for matching methods and preserves
|
|
26
|
+
# methods that only appear in one result.
|
|
18
27
|
#
|
|
19
28
|
# @return [Hash]
|
|
20
29
|
#
|
|
21
30
|
def combine(coverage_a, coverage_b)
|
|
22
|
-
|
|
31
|
+
merged = {} #: Hash[untyped, [untyped, Integer]]
|
|
32
|
+
[coverage_a, coverage_b].each_with_object(merged) do |coverage, memo|
|
|
33
|
+
coverage.each do |key, count|
|
|
34
|
+
method_key = source_identity(key)
|
|
35
|
+
retained_key, existing = memo[method_key] || [key, 0]
|
|
36
|
+
memo[method_key] = [retained_key, existing + count]
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
merged.values.to_h
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def source_identity(key)
|
|
44
|
+
_class_name, *identity = SourceFile::RubyDataParser.call(key)
|
|
45
|
+
identity
|
|
23
46
|
end
|
|
24
47
|
end
|
|
25
48
|
end
|
|
@@ -21,7 +21,8 @@ module SimpleCov
|
|
|
21
21
|
# @return [Hash]
|
|
22
22
|
#
|
|
23
23
|
def combine(*results)
|
|
24
|
-
|
|
24
|
+
initial = {} #: Hash[untyped, untyped]
|
|
25
|
+
results.reduce(initial) do |combined_results, next_result|
|
|
25
26
|
combine_result_sets(combined_results, next_result)
|
|
26
27
|
end
|
|
27
28
|
end
|
|
@@ -42,7 +42,9 @@ module SimpleCov
|
|
|
42
42
|
#
|
|
43
43
|
def coverage(criterion, primary: false, enabled: true, oneshot: false, **thresholds, &block)
|
|
44
44
|
criterion = enable_coverage_criterion(criterion, enabled: enabled, oneshot: oneshot)
|
|
45
|
-
primary_coverage
|
|
45
|
+
# The cast admits :eval, which primary_coverage rejects at runtime
|
|
46
|
+
# (it is enable-only and never in the enabled-criteria set).
|
|
47
|
+
primary_coverage(_ = criterion) if primary
|
|
46
48
|
|
|
47
49
|
configurator = CoverageCriterion.new(self, criterion)
|
|
48
50
|
apply_threshold_options(configurator, thresholds)
|
|
@@ -164,7 +166,9 @@ module SimpleCov
|
|
|
164
166
|
|
|
165
167
|
# Make this criterion the report's primary (leading) criterion.
|
|
166
168
|
def primary
|
|
167
|
-
@
|
|
169
|
+
# @criterion is Symbol-wide because this receiver is also built for
|
|
170
|
+
# :eval; primary_coverage validates at runtime.
|
|
171
|
+
@config.primary_coverage(_ = @criterion)
|
|
168
172
|
end
|
|
169
173
|
end
|
|
170
174
|
end
|
|
@@ -134,7 +134,9 @@ module SimpleCov
|
|
|
134
134
|
def default_primary_coverage
|
|
135
135
|
return DEFAULT_COVERAGE_CRITERION if coverage_criterion_enabled?(DEFAULT_COVERAGE_CRITERION)
|
|
136
136
|
|
|
137
|
-
|
|
137
|
+
# Set#first types as nilable, but an empty criteria set is rejected at
|
|
138
|
+
# start_tracking before any caller can observe a nil here.
|
|
139
|
+
_ = coverage_criteria.first
|
|
138
140
|
end
|
|
139
141
|
|
|
140
142
|
def raise_if_criterion_disabled(criterion)
|
|
@@ -17,22 +17,32 @@ module SimpleCov
|
|
|
17
17
|
# overhead.
|
|
18
18
|
#
|
|
19
19
|
def formatter(formatter = :__no_arg__)
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
case formatter
|
|
21
|
+
when :__no_arg__
|
|
22
|
+
@formatter
|
|
23
|
+
else
|
|
24
|
+
@formatter = formatter || nil # normalize `false` to `nil`
|
|
25
|
+
end
|
|
23
26
|
end
|
|
24
27
|
|
|
25
28
|
# Sets the configured formatters. Pass `[]` to opt out of
|
|
26
29
|
# formatting entirely; see `formatter` for the rationale.
|
|
27
30
|
def formatters(formatters = :__no_arg__)
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
31
|
+
case formatters
|
|
32
|
+
when :__no_arg__
|
|
33
|
+
configured = formatter
|
|
34
|
+
configured ? [configured] : []
|
|
35
|
+
else
|
|
36
|
+
self.formatters = formatters
|
|
37
|
+
formatters
|
|
38
|
+
end
|
|
32
39
|
end
|
|
33
40
|
|
|
34
41
|
# Sets the configured formatters. Equivalent to `formatters [...]`.
|
|
42
|
+
# Accepts a single formatter as well as an Array, matching the pre-1.0 behavior
|
|
43
|
+
# where `MultiFormatter.new` normalized its input.
|
|
35
44
|
def formatters=(formatters)
|
|
45
|
+
formatters = Array(formatters)
|
|
36
46
|
@formatter = formatters.empty? ? nil : SimpleCov::Formatter::MultiFormatter.new(formatters)
|
|
37
47
|
end
|
|
38
48
|
|
|
@@ -123,7 +123,11 @@ module SimpleCov
|
|
|
123
123
|
# so downstream code only has one shape to handle.
|
|
124
124
|
def partition_per_file_thresholds(coverage)
|
|
125
125
|
coverage.each_key { |key| validate_per_file_key(key) }
|
|
126
|
-
|
|
126
|
+
pairs = coverage.partition { |key, _| key.is_a?(Symbol) }
|
|
127
|
+
# The assertions restate what the partition predicate guarantees:
|
|
128
|
+
# Symbol keys carry per-criterion Numeric defaults, the rest are paths.
|
|
129
|
+
defaults = pairs[0].to_h #: coverage_thresholds
|
|
130
|
+
raw = pairs[1].to_h #: Hash[String | Regexp, Numeric | coverage_thresholds]
|
|
127
131
|
overrides = raw.transform_values { |value| value.is_a?(Numeric) ? {primary_coverage => value} : value }
|
|
128
132
|
[defaults, overrides]
|
|
129
133
|
end
|
|
@@ -143,11 +147,11 @@ module SimpleCov
|
|
|
143
147
|
# `minimum_coverage_by_file` argument so the deprecation warning can be
|
|
144
148
|
# copy-pasted verbatim into the user's config.
|
|
145
149
|
def per_file_coverage_replacement(defaults, overrides)
|
|
146
|
-
by_criterion =
|
|
147
|
-
defaults.each { |criterion, percent| by_criterion[criterion] << "minimum_per_file #{percent}" }
|
|
150
|
+
by_criterion = {} #: Hash[Symbol, Array[String]]
|
|
151
|
+
defaults.each { |criterion, percent| (by_criterion[criterion] ||= []) << "minimum_per_file #{percent}" }
|
|
148
152
|
overrides.each do |target, criteria|
|
|
149
153
|
criteria.each do |criterion, percent|
|
|
150
|
-
by_criterion[criterion] << "minimum_per_file #{percent}, only: #{target.inspect}"
|
|
154
|
+
(by_criterion[criterion] ||= []) << "minimum_per_file #{percent}, only: #{target.inspect}"
|
|
151
155
|
end
|
|
152
156
|
end
|
|
153
157
|
render_coverage_blocks(by_criterion)
|
|
@@ -155,11 +159,11 @@ module SimpleCov
|
|
|
155
159
|
|
|
156
160
|
# Same, for a (deprecated) `minimum_coverage_by_group` argument.
|
|
157
161
|
def per_group_coverage_replacement(coverage)
|
|
158
|
-
by_criterion =
|
|
159
|
-
coverage.each do |group_name,
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
by_criterion[criterion] << "minimum_per_group #{percent}, only: #{group_name.inspect}"
|
|
162
|
+
by_criterion = {} #: Hash[Symbol, Array[String]]
|
|
163
|
+
coverage.each do |group_name, thresholds|
|
|
164
|
+
normalized = (thresholds.is_a?(Numeric) ? {primary_coverage => thresholds} : thresholds) #: coverage_thresholds
|
|
165
|
+
normalized.each do |criterion, percent|
|
|
166
|
+
(by_criterion[criterion] ||= []) << "minimum_per_group #{percent}, only: #{group_name.inspect}"
|
|
163
167
|
end
|
|
164
168
|
end
|
|
165
169
|
render_coverage_blocks(by_criterion)
|
|
@@ -51,9 +51,10 @@ module SimpleCov
|
|
|
51
51
|
#
|
|
52
52
|
def coverage_path(path = nil)
|
|
53
53
|
if path
|
|
54
|
-
|
|
54
|
+
expanded = File.expand_path(path)
|
|
55
|
+
@coverage_path = expanded
|
|
55
56
|
@coverage_path_explicit = true
|
|
56
|
-
FileUtils.mkdir_p
|
|
57
|
+
FileUtils.mkdir_p expanded
|
|
57
58
|
end
|
|
58
59
|
|
|
59
60
|
@coverage_path ||= File.expand_path(coverage_dir, root)
|
|
@@ -65,9 +66,8 @@ module SimpleCov
|
|
|
65
66
|
# with SimpleCov.command_name("test:units").
|
|
66
67
|
#
|
|
67
68
|
def command_name(name = nil)
|
|
68
|
-
@
|
|
69
|
-
@
|
|
70
|
-
@name
|
|
69
|
+
@command_name = name unless name.nil?
|
|
70
|
+
@command_name ||= SimpleCov::CommandGuesser.guess
|
|
71
71
|
end
|
|
72
72
|
|
|
73
73
|
# Returns the hash of available profiles
|
|
@@ -80,17 +80,24 @@ module SimpleCov
|
|
|
80
80
|
# prepending SimpleCov to each config method.
|
|
81
81
|
#
|
|
82
82
|
def configure(&block)
|
|
83
|
-
|
|
83
|
+
# Both locals are read in the ensure clause, where flow analysis
|
|
84
|
+
# cannot see the assignments below; anchor their types up front.
|
|
85
|
+
saved = nil # : Hash[Symbol, untyped]?
|
|
86
|
+
block_context = block.binding.receiver # : untyped
|
|
84
87
|
|
|
85
|
-
# If the block was defined in our own context, instance_exec is
|
|
86
|
-
|
|
88
|
+
# If the block was defined in our own context, instance_exec is
|
|
89
|
+
# sufficient. The `_ =` erases the block's self-binding, which RBS
|
|
90
|
+
# can express but `instance_exec`'s core signature cannot accept.
|
|
91
|
+
return instance_exec(&(_ = block)) if equal?(block_context)
|
|
87
92
|
|
|
88
93
|
# Copy the caller's instance variables in so that references like @filter
|
|
89
94
|
# inside the block resolve to the caller's values, not ours.
|
|
90
95
|
saved = swap_ivars_from(block_context)
|
|
91
|
-
instance_exec(&block)
|
|
96
|
+
instance_exec(&(_ = block))
|
|
92
97
|
ensure
|
|
93
|
-
|
|
98
|
+
# @type var block_context: untyped
|
|
99
|
+
# @type var saved: Hash[Symbol, untyped]?
|
|
100
|
+
restore_ivars(block_context, saved) if saved
|
|
94
101
|
end
|
|
95
102
|
|
|
96
103
|
#
|
|
@@ -100,7 +107,8 @@ module SimpleCov
|
|
|
100
107
|
#
|
|
101
108
|
def at_exit(&block)
|
|
102
109
|
@at_exit = block if block
|
|
103
|
-
|
|
110
|
+
configured = @at_exit
|
|
111
|
+
return configured if configured
|
|
104
112
|
return proc {} unless active_session?
|
|
105
113
|
|
|
106
114
|
@at_exit = proc do
|
|
@@ -144,7 +152,8 @@ module SimpleCov
|
|
|
144
152
|
# SimpleCov.root, capitalized with underscores → spaces.
|
|
145
153
|
#
|
|
146
154
|
def project_name(new_name = nil)
|
|
147
|
-
|
|
155
|
+
current = defined?(@project_name) ? @project_name : nil
|
|
156
|
+
return current if current && new_name.nil?
|
|
148
157
|
|
|
149
158
|
@project_name = new_name if new_name.is_a?(String)
|
|
150
159
|
@project_name ||= File.basename(root).capitalize.tr("_", " ")
|
|
@@ -156,7 +165,7 @@ module SimpleCov
|
|
|
156
165
|
# of ours that would be clobbered. Returns the saved values for
|
|
157
166
|
# later restoration.
|
|
158
167
|
def swap_ivars_from(block_context)
|
|
159
|
-
saved = {}
|
|
168
|
+
saved = {} # : Hash[Symbol, untyped]
|
|
160
169
|
our_ivars = instance_variables
|
|
161
170
|
block_context.instance_variables.each do |ivar|
|
|
162
171
|
saved[ivar] = instance_variable_get(ivar) if our_ivars.include?(ivar)
|
|
@@ -14,9 +14,12 @@ module SimpleCov
|
|
|
14
14
|
class CoverageStatistics
|
|
15
15
|
attr_reader :total, :covered, :missed, :omitted, :strength, :percent
|
|
16
16
|
|
|
17
|
+
# Seed for the reduce in `.from`: covered, missed, omitted, strength.
|
|
18
|
+
ZERO_STATS = [0, 0, 0, 0.0].freeze #: [Integer, Integer, Integer, Float]
|
|
19
|
+
|
|
17
20
|
def self.from(coverage_statistics)
|
|
18
21
|
sum_covered, sum_missed, sum_omitted, sum_total_strength =
|
|
19
|
-
coverage_statistics.reduce(
|
|
22
|
+
coverage_statistics.reduce(ZERO_STATS) do |(covered, missed, omitted, total_strength), stats|
|
|
20
23
|
[
|
|
21
24
|
covered + stats.covered,
|
|
22
25
|
missed + stats.missed,
|
|
@@ -46,7 +46,8 @@ module SimpleCov
|
|
|
46
46
|
def minimum_by_group(result, thresholds)
|
|
47
47
|
thresholds.flat_map do |group_name, minimums|
|
|
48
48
|
group = lookup_group(result, group_name)
|
|
49
|
-
|
|
49
|
+
none = [] # : Array[Hash[Symbol, untyped]]
|
|
50
|
+
group ? group_minimum_violations(group_name, group, minimums) : none
|
|
50
51
|
end
|
|
51
52
|
end
|
|
52
53
|
|
data/lib/simplecov/defaults.rb
CHANGED
|
@@ -41,12 +41,12 @@ loop do
|
|
|
41
41
|
# it are intercepted and converted to configuration, with a deprecation
|
|
42
42
|
# warning. See `SimpleCov.with_dot_simplecov_autoload` and issue #581.
|
|
43
43
|
SimpleCov.with_dot_simplecov_autoload do
|
|
44
|
-
load filename
|
|
45
|
-
rescue LoadError, StandardError
|
|
44
|
+
load filename.to_s
|
|
45
|
+
rescue LoadError, StandardError => e
|
|
46
46
|
# simplecov:disable — only fires when .simplecov is unreadable
|
|
47
47
|
# or raises during load
|
|
48
48
|
warn "Warning: Error occurred while trying to load #{filename}. " \
|
|
49
|
-
"Error message: #{
|
|
49
|
+
"Error message: #{e.message}"
|
|
50
50
|
# simplecov:enable
|
|
51
51
|
end
|
|
52
52
|
break
|
data/lib/simplecov/directive.rb
CHANGED
|
@@ -55,8 +55,11 @@ module SimpleCov
|
|
|
55
55
|
# An unclosed `disable` block extends to the end of the file.
|
|
56
56
|
def self.disabled_ranges(src_lines)
|
|
57
57
|
lines = src_lines.to_a
|
|
58
|
-
ranges = CATEGORIES.to_h
|
|
59
|
-
|
|
58
|
+
ranges = CATEGORIES.to_h do |category|
|
|
59
|
+
empty = [] # : Array[Range[Integer]]
|
|
60
|
+
[category, empty]
|
|
61
|
+
end
|
|
62
|
+
open_starts = {} # : Hash[Symbol, Integer]
|
|
60
63
|
|
|
61
64
|
directives_in(lines).each { |directive| directive.apply(ranges, open_starts) }
|
|
62
65
|
open_starts.each { |category, start| ranges[category] << (start..lines.size) }
|
|
@@ -91,7 +94,9 @@ module SimpleCov
|
|
|
91
94
|
|
|
92
95
|
new(
|
|
93
96
|
line_number: line_number,
|
|
94
|
-
|
|
97
|
+
# `to_s` is a no-op: the pattern requires the `mode` group, so it
|
|
98
|
+
# is present whenever `match` is; it also satisfies the type checker.
|
|
99
|
+
mode: match[:mode].to_s.to_sym,
|
|
95
100
|
categories: parse_categories(match[:categories]),
|
|
96
101
|
inline: inline?(lines, line_number, column + match.begin(0))
|
|
97
102
|
)
|