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
|
@@ -15,7 +15,7 @@ module SimpleCov
|
|
|
15
15
|
end
|
|
16
16
|
|
|
17
17
|
def report
|
|
18
|
-
violations.each { |violation|
|
|
18
|
+
violations.each { |violation| ExitCodes.print_error SimpleCov::Color.colorize(message_for(violation), :red) }
|
|
19
19
|
end
|
|
20
20
|
|
|
21
21
|
def exit_code
|
|
@@ -32,7 +32,7 @@ module SimpleCov
|
|
|
32
32
|
end
|
|
33
33
|
|
|
34
34
|
def report_violation(violation)
|
|
35
|
-
|
|
35
|
+
ExitCodes.print_error format(
|
|
36
36
|
"%<criterion>s coverage (%<actual>s) is above the expected maximum coverage (%<expected>.2f%%). " \
|
|
37
37
|
"Time to bump the threshold!",
|
|
38
38
|
criterion: violation.fetch(:criterion).capitalize,
|
|
@@ -17,7 +17,7 @@ module SimpleCov
|
|
|
17
17
|
|
|
18
18
|
def report
|
|
19
19
|
violations.each do |violation|
|
|
20
|
-
|
|
20
|
+
ExitCodes.print_error format(
|
|
21
21
|
"%<criterion>s coverage by file (%<actual>s) is below the expected minimum coverage " \
|
|
22
22
|
"(%<expected>.2f%%) in %<filename>s.",
|
|
23
23
|
criterion: violation.fetch(:criterion).capitalize,
|
|
@@ -16,7 +16,7 @@ module SimpleCov
|
|
|
16
16
|
|
|
17
17
|
def report
|
|
18
18
|
violations.each do |violation|
|
|
19
|
-
|
|
19
|
+
ExitCodes.print_error format(
|
|
20
20
|
"%<criterion>s coverage by group (%<actual>s) is below the expected minimum coverage " \
|
|
21
21
|
"(%<expected>.2f%%) in %<group_name>s.",
|
|
22
22
|
criterion: violation.fetch(:criterion).capitalize,
|
|
@@ -34,7 +34,7 @@ module SimpleCov
|
|
|
34
34
|
def report_violation(violation)
|
|
35
35
|
criterion = violation.fetch(:criterion)
|
|
36
36
|
actual = violation.fetch(:actual)
|
|
37
|
-
|
|
37
|
+
ExitCodes.print_error format(
|
|
38
38
|
"%<criterion>s coverage (%<actual>s) is below the expected minimum coverage (%<expected>.2f%%).",
|
|
39
39
|
criterion: criterion.capitalize,
|
|
40
40
|
actual: SimpleCov::Color.colorize_percent(actual),
|
|
@@ -47,9 +47,9 @@ module SimpleCov
|
|
|
47
47
|
worst = worst_files_for(criterion)
|
|
48
48
|
return if worst.empty?
|
|
49
49
|
|
|
50
|
-
|
|
50
|
+
ExitCodes.print_error " Lowest-coverage files (#{criterion}):"
|
|
51
51
|
worst.each do |path, percent|
|
|
52
|
-
|
|
52
|
+
ExitCodes.print_error format(
|
|
53
53
|
" %<percent>s %<path>s",
|
|
54
54
|
percent: SimpleCov::Color.colorize_percent(percent, format("%6.2f%%", percent)),
|
|
55
55
|
path: path
|
data/lib/simplecov/exit_codes.rb
CHANGED
|
@@ -1,12 +1,24 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module SimpleCov
|
|
4
|
+
# Exit statuses SimpleCov sets when coverage checks fail, and the output
|
|
5
|
+
# helper the enforcement machinery reports through.
|
|
4
6
|
module ExitCodes
|
|
5
7
|
SUCCESS = 0
|
|
6
8
|
EXCEPTION = 1
|
|
7
9
|
MINIMUM_COVERAGE = 2
|
|
8
10
|
MAXIMUM_COVERAGE_DROP = 3
|
|
9
11
|
MAXIMUM_COVERAGE = 4
|
|
12
|
+
|
|
13
|
+
# Threshold-violation reports and exit-status notices are the output of
|
|
14
|
+
# the enforcement feature, not Ruby warnings: routing them through
|
|
15
|
+
# `Kernel#warn` made `-W0` swallow the explanation for a failing exit
|
|
16
|
+
# code, let `Warning.warn` hooks (warning trackers, raise-on-warning
|
|
17
|
+
# test setups) intercept them mid-`at_exit`, and fed colorized text to
|
|
18
|
+
# warning logs. `print_errors false` remains the intended opt-out.
|
|
19
|
+
def self.print_error(message)
|
|
20
|
+
$stderr.puts message # rubocop:disable Style/StderrPuts
|
|
21
|
+
end
|
|
10
22
|
end
|
|
11
23
|
end
|
|
12
24
|
|
|
@@ -56,7 +56,7 @@ module SimpleCov
|
|
|
56
56
|
def warn_about_deferred_report
|
|
57
57
|
return unless print_errors
|
|
58
58
|
|
|
59
|
-
|
|
59
|
+
ExitCodes.print_error SimpleCov::Color.colorize(
|
|
60
60
|
"Skipping SimpleCov report — this process tracked no application code and a newer " \
|
|
61
61
|
"report already exists at #{coverage_path}. This usually means SimpleCov.start ran in a " \
|
|
62
62
|
"parent process (e.g. a Rakefile or Rails' Bundler.require) that shelled out to the test " \
|
|
@@ -97,7 +97,7 @@ module SimpleCov
|
|
|
97
97
|
# @api private
|
|
98
98
|
def exit_and_report_previous_error(exit_status)
|
|
99
99
|
if print_errors
|
|
100
|
-
|
|
100
|
+
ExitCodes.print_error SimpleCov::Color.colorize(
|
|
101
101
|
"Stopped processing SimpleCov as a previous error not related to SimpleCov has been detected",
|
|
102
102
|
:yellow
|
|
103
103
|
)
|
|
@@ -124,7 +124,7 @@ module SimpleCov
|
|
|
124
124
|
return unless exit_status.positive?
|
|
125
125
|
|
|
126
126
|
if print_errors
|
|
127
|
-
|
|
127
|
+
ExitCodes.print_error SimpleCov::Color.colorize(
|
|
128
128
|
"SimpleCov failed with exit #{exit_status} due to a coverage related error", :red
|
|
129
129
|
)
|
|
130
130
|
end
|
data/lib/simplecov/file_list.rb
CHANGED
|
@@ -27,8 +27,8 @@ module SimpleCov
|
|
|
27
27
|
# returns the `{line:, branch:, method:}` Hash; pass a criterion symbol
|
|
28
28
|
# (`:line` / `:branch` / `:method`) to get that one CoverageStatistics.
|
|
29
29
|
def coverage_statistics(criterion = nil)
|
|
30
|
-
@coverage_statistics ||= compute_coverage_statistics
|
|
31
|
-
criterion ?
|
|
30
|
+
stats = (@coverage_statistics ||= compute_coverage_statistics)
|
|
31
|
+
criterion ? stats[criterion] : stats
|
|
32
32
|
end
|
|
33
33
|
|
|
34
34
|
def coverage_statistics_by_file
|
|
@@ -67,7 +67,11 @@ module SimpleCov
|
|
|
67
67
|
|
|
68
68
|
# Finds the least covered file and returns that file's name
|
|
69
69
|
def least_covered_file
|
|
70
|
-
|
|
70
|
+
# `covered_percent` is nil only for an unmeasured criterion, and :line
|
|
71
|
+
# is always measured, so the `|| 0.0` arm never fires at runtime; it
|
|
72
|
+
# (and the cast) exist to satisfy min_by's Comparable requirement.
|
|
73
|
+
least_covered = min_by { |file| file.covered_percent || 0.0 }
|
|
74
|
+
(_ = least_covered).filename
|
|
71
75
|
end
|
|
72
76
|
|
|
73
77
|
# Returns the overall amount of relevant lines of code across all files in this list
|
|
@@ -137,7 +141,10 @@ module SimpleCov
|
|
|
137
141
|
# enabled set so disabled criteria don't surface in totals, JSON,
|
|
138
142
|
# or the HTML report.
|
|
139
143
|
def compute_coverage_statistics_by_file
|
|
140
|
-
seed = enabled_criteria_for_reporting.to_h
|
|
144
|
+
seed = enabled_criteria_for_reporting.to_h do |criterion|
|
|
145
|
+
bucket = [] #: Array[CoverageStatistics]
|
|
146
|
+
[criterion, bucket]
|
|
147
|
+
end
|
|
141
148
|
@files.each_with_object(seed) do |file, together|
|
|
142
149
|
file.coverage_statistics.each do |criterion, stats|
|
|
143
150
|
together[criterion] << stats if together.key?(criterion)
|
|
@@ -153,7 +160,7 @@ module SimpleCov
|
|
|
153
160
|
# criterion is enabled; the JRuby-gated branch/method criteria are
|
|
154
161
|
# reported when they pass their own engine-support check.
|
|
155
162
|
def enabled_criteria_for_reporting
|
|
156
|
-
criteria = []
|
|
163
|
+
criteria = [] #: Array[SimpleCov::criterion]
|
|
157
164
|
criteria << :line if SimpleCov.coverage_criterion_enabled?(:line) ||
|
|
158
165
|
SimpleCov.coverage_criterion_enabled?(:oneshot_line)
|
|
159
166
|
criteria << :branch if SimpleCov.branch_coverage?
|
|
@@ -45,7 +45,8 @@ module SimpleCov
|
|
|
45
45
|
# confusing than the absolute form. See issue #197.
|
|
46
46
|
def displayable_output_path
|
|
47
47
|
directory = relative_or_absolute_output_path
|
|
48
|
-
|
|
48
|
+
entry_point = entry_point_filename
|
|
49
|
+
entry_point ? File.join(directory, entry_point) : directory
|
|
49
50
|
end
|
|
50
51
|
|
|
51
52
|
def relative_or_absolute_output_path
|
|
@@ -31,8 +31,10 @@ module SimpleCov
|
|
|
31
31
|
|
|
32
32
|
copy_static_assets
|
|
33
33
|
# stderr, not stdout: this is a status message, not the program's
|
|
34
|
-
# output. Keeps the line out of pipelines like `rspec -f json`.
|
|
35
|
-
warn
|
|
34
|
+
# output. Keeps the line out of pipelines like `rspec -f json`. And
|
|
35
|
+
# $stderr.puts, not `warn`: a status line should not reach
|
|
36
|
+
# `Warning.warn` hooks or vanish under `-W0` (see #1225).
|
|
37
|
+
$stderr.puts output_message(result) unless @silent # rubocop:disable Style/StderrPuts
|
|
36
38
|
end
|
|
37
39
|
|
|
38
40
|
# Generate HTML from a pre-existing coverage.json file without
|
|
@@ -65,14 +67,18 @@ module SimpleCov
|
|
|
65
67
|
# from opening the existing path for writing.
|
|
66
68
|
def atomic_write(dest, content)
|
|
67
69
|
temp = "#{dest}.#{Process.pid}.#{rand(2**32).to_s(36)}"
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
70
|
+
begin
|
|
71
|
+
File.binwrite(temp, content)
|
|
72
|
+
File.rename(temp, dest)
|
|
73
|
+
ensure
|
|
74
|
+
FileUtils.rm_f(temp)
|
|
75
|
+
end
|
|
72
76
|
end
|
|
73
77
|
|
|
74
78
|
def public_dir
|
|
75
|
-
|
|
79
|
+
# `.to_s` collapses `__dir__`'s nil arm (only possible under eval,
|
|
80
|
+
# which can't happen for a file on disk) so the path is a String.
|
|
81
|
+
File.join(__dir__.to_s, "html_formatter/public/")
|
|
76
82
|
end
|
|
77
83
|
end
|
|
78
84
|
end
|
|
@@ -42,14 +42,20 @@ module SimpleCov
|
|
|
42
42
|
end
|
|
43
43
|
|
|
44
44
|
def record_by_file(violation)
|
|
45
|
-
|
|
45
|
+
by_file = bucket(:minimum_coverage_by_file)
|
|
46
|
+
file_bucket = by_file[violation.fetch(:project_filename)] ||= {} #: Hash[untyped, untyped]
|
|
46
47
|
file_bucket[key_for(violation)] = expected_actual(violation)
|
|
47
48
|
end
|
|
48
49
|
|
|
49
50
|
def format_minimum_by_group
|
|
50
51
|
violations = SimpleCov::CoverageViolations.minimum_by_group(@result, SimpleCov.minimum_coverage_by_group)
|
|
52
|
+
return if violations.empty?
|
|
53
|
+
|
|
54
|
+
# `bucket` lazily creates the errors key, so only touch it when
|
|
55
|
+
# there is a violation to record.
|
|
56
|
+
by_group = bucket(:minimum_coverage_by_group)
|
|
51
57
|
violations.each do |violation|
|
|
52
|
-
group_bucket =
|
|
58
|
+
group_bucket = by_group[violation.fetch(:group_name)] ||= {} #: Hash[untyped, untyped]
|
|
53
59
|
group_bucket[key_for(violation)] = expected_actual(violation)
|
|
54
60
|
end
|
|
55
61
|
end
|
|
@@ -68,7 +74,7 @@ module SimpleCov
|
|
|
68
74
|
end
|
|
69
75
|
|
|
70
76
|
def bucket(name)
|
|
71
|
-
@errors[name] ||= {}
|
|
77
|
+
@errors[name] ||= {} #: Hash[untyped, untyped]
|
|
72
78
|
end
|
|
73
79
|
|
|
74
80
|
def key_for(violation)
|
|
@@ -95,7 +95,7 @@ module SimpleCov
|
|
|
95
95
|
end
|
|
96
96
|
|
|
97
97
|
def format_coverage_statistics(statistics)
|
|
98
|
-
result = {}
|
|
98
|
+
result = {} #: Hash[Symbol, untyped]
|
|
99
99
|
result[:lines] = format_line_statistic(statistics[:line]) if statistics[:line]
|
|
100
100
|
result[:branches] = format_single_statistic(statistics[:branch]) if statistics[:branch]
|
|
101
101
|
result[:methods] = format_single_statistic(statistics[:method]) if statistics[:method]
|
|
@@ -13,7 +13,7 @@ module SimpleCov
|
|
|
13
13
|
end
|
|
14
14
|
|
|
15
15
|
def call
|
|
16
|
-
result = @include_source ? format_source_code : {}
|
|
16
|
+
result = @include_source ? format_source_code : {} #: Hash[Symbol, untyped]
|
|
17
17
|
result.merge!(line_coverage_section) if line_coverage_enabled?
|
|
18
18
|
result.merge!(branch_coverage_section) if SimpleCov.branch_coverage?
|
|
19
19
|
result.merge!(method_coverage_section) if SimpleCov.method_coverage?
|
|
@@ -28,8 +28,10 @@ module SimpleCov
|
|
|
28
28
|
warn_if_concurrent_overwrite(path, result)
|
|
29
29
|
File.write(path, JSON.pretty_generate(self.class.build_hash(result)))
|
|
30
30
|
# stderr, not stdout: this is a status message, not the program's
|
|
31
|
-
# output. Keeps the line out of pipelines like `rspec -f json`.
|
|
32
|
-
warn
|
|
31
|
+
# output. Keeps the line out of pipelines like `rspec -f json`. And
|
|
32
|
+
# $stderr.puts, not `warn`: a status line should not reach
|
|
33
|
+
# `Warning.warn` hooks or vanish under `-W0` (see #1225).
|
|
34
|
+
$stderr.puts output_message(result) unless @silent # rubocop:disable Style/StderrPuts
|
|
33
35
|
end
|
|
34
36
|
|
|
35
37
|
private
|
|
@@ -12,16 +12,21 @@ module SimpleCov
|
|
|
12
12
|
formatters.map do |formatter|
|
|
13
13
|
formatter.new.format(result)
|
|
14
14
|
rescue StandardError => e
|
|
15
|
-
warn("Formatter #{formatter} failed with #{e.class}: #{e.message} (#{e.backtrace.first})")
|
|
15
|
+
warn("Formatter #{formatter} failed with #{e.class}: #{e.message} (#{(_ = e.backtrace).first})")
|
|
16
16
|
nil
|
|
17
17
|
end
|
|
18
18
|
end
|
|
19
19
|
end
|
|
20
20
|
|
|
21
21
|
def self.new(formatters = nil)
|
|
22
|
+
# Normalize eagerly and capture the list in the closure. Array()
|
|
23
|
+
# is pure for every accepted input shape (nil, single formatter,
|
|
24
|
+
# array, or another MultiFormatter class), so this is equivalent
|
|
25
|
+
# to the historical lazy per-instance memoization.
|
|
26
|
+
formatter_list = Array(formatters)
|
|
22
27
|
Class.new do
|
|
23
28
|
define_method :formatters do
|
|
24
|
-
|
|
29
|
+
formatter_list
|
|
25
30
|
end
|
|
26
31
|
include InstanceMethods
|
|
27
32
|
end
|
|
@@ -15,7 +15,9 @@ module SimpleCov
|
|
|
15
15
|
|
|
16
16
|
def format_group(name, files)
|
|
17
17
|
header = "Group: #{name}\n#{'=' * 40}\n"
|
|
18
|
-
|
|
18
|
+
# `covered_percent` is nilable across criteria, but line stats are
|
|
19
|
+
# always measured, so the no-argument call can't return nil here.
|
|
20
|
+
body = files.map { |file| "#{file.filename} (coverage: #{(_ = file.covered_percent).floor(2)}%)\n" }.join
|
|
19
21
|
"#{header}#{body}\n"
|
|
20
22
|
end
|
|
21
23
|
end
|
data/lib/simplecov/process.rb
CHANGED
|
@@ -9,7 +9,24 @@
|
|
|
9
9
|
# correctly with other libraries doing the same — they each prepend
|
|
10
10
|
# their own module and chain via `super`.
|
|
11
11
|
|
|
12
|
+
# Reopened here for the fork-related at_exit plumbing that only matters
|
|
13
|
+
# when subprocess support is loaded.
|
|
12
14
|
module SimpleCov
|
|
15
|
+
class << self
|
|
16
|
+
# Forked children inherit at_exit state that is wrong for them:
|
|
17
|
+
# @at_exit_hook_installed may describe a hook the parent already
|
|
18
|
+
# consumed before forking (Minitest autorun runs the suite inside the
|
|
19
|
+
# parent's at_exit, after SimpleCov's own hook has fired), and
|
|
20
|
+
# external_at_exit may point at a Minitest.after_run deferral that is
|
|
21
|
+
# pid-pinned to the parent. Reset both so the at_fork proc's
|
|
22
|
+
# SimpleCov.start installs a fresh hook that actually fires at child
|
|
23
|
+
# exit. See issue #1227.
|
|
24
|
+
def reset_inherited_at_exit_state!
|
|
25
|
+
@at_exit_hook_installed = false
|
|
26
|
+
self.external_at_exit = false
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
13
30
|
# Prepended onto Process's singleton class so every fork — direct or
|
|
14
31
|
# via Kernel#fork / IO.popen — re-runs SimpleCov's at_fork callback in
|
|
15
32
|
# the child.
|
|
@@ -27,6 +44,11 @@ module SimpleCov
|
|
|
27
44
|
# the user installed, so `final_result_process?` can keep forked
|
|
28
45
|
# workers from each producing the final report. See issue #1171.
|
|
29
46
|
SimpleCov.mark_forked_subprocess!
|
|
47
|
+
# Without this, a child forked under Minitest autorun measures
|
|
48
|
+
# coverage and then silently discards it: every inherited exit
|
|
49
|
+
# route is dead in the child (see the method's comment). A custom
|
|
50
|
+
# at_fork runs afterwards and can still override. See issue #1227.
|
|
51
|
+
SimpleCov.reset_inherited_at_exit_state!
|
|
30
52
|
SimpleCov.at_fork.call(::Process.pid)
|
|
31
53
|
end
|
|
32
54
|
pid
|
|
@@ -27,13 +27,14 @@ module SimpleCov
|
|
|
27
27
|
|
|
28
28
|
# Pre-0.18 resultsets pointed each filename straight at a line-coverage
|
|
29
29
|
# array; everything since uses the `{lines:, branches:, methods:}`
|
|
30
|
-
# shape. Newer entries also need their methods
|
|
31
|
-
# downstream code merges
|
|
30
|
+
# shape. Newer entries also need their methods and branches tables
|
|
31
|
+
# massaged before downstream code reports or merges them.
|
|
32
32
|
def adapt_one(file_name, cover_statistic)
|
|
33
33
|
return {"lines" => cover_statistic} if cover_statistic.is_a?(Array)
|
|
34
34
|
|
|
35
35
|
adapt_oneshot_lines_if_needed(file_name, cover_statistic)
|
|
36
36
|
normalize_method_keys(cover_statistic)
|
|
37
|
+
aggregate_duplicated_branches(cover_statistic)
|
|
37
38
|
cover_statistic
|
|
38
39
|
end
|
|
39
40
|
|
|
@@ -59,19 +60,53 @@ module SimpleCov
|
|
|
59
60
|
SINGLETON_WRAPPER_PATTERN = /\A#<Class:([A-Z_][\w:]*)>\z/
|
|
60
61
|
private_constant :SINGLETON_WRAPPER_PATTERN
|
|
61
62
|
|
|
63
|
+
# Ruby's method coverage records one entry per RECEIVER, not per source
|
|
64
|
+
# location: a block handed to `define_method` / `define_singleton_method`
|
|
65
|
+
# from a shared code path (a module's `included` hook, a builder) yields
|
|
66
|
+
# a separate `[receiver, name, location]` entry for every class it's
|
|
67
|
+
# defined on, all pointing at the same source. A file-based report can
|
|
68
|
+
# only express "was the method at this location ever executed", so
|
|
69
|
+
# entries are aggregated by (name, location), summing hits — otherwise
|
|
70
|
+
# each receiver whose copy never ran shows as a phantom uncovered method
|
|
71
|
+
# on a line whose line coverage is 100% (issue #1234). The first entry's
|
|
72
|
+
# (normalized) receiver is kept for display.
|
|
62
73
|
def normalize_method_keys(cover_statistic)
|
|
63
74
|
methods = cover_statistic[:methods]
|
|
64
75
|
return unless methods
|
|
65
76
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
# Keys may collide after normalization (anonymous classes sharing a
|
|
72
|
-
# method name, or singleton + instance forms of a module_function method).
|
|
73
|
-
normalized[normalized_key] = normalized.fetch(normalized_key, 0) + count
|
|
77
|
+
aggregated = {} #: Hash[untyped, [untyped, Integer]]
|
|
78
|
+
methods.each_with_object(aggregated) do |(key, count), memo|
|
|
79
|
+
identity = key[1..] #: Array[untyped]
|
|
80
|
+
retained_key, existing = memo[identity] || [normalize_method_key(key), 0]
|
|
81
|
+
memo[identity] = [retained_key, existing + count]
|
|
74
82
|
end
|
|
83
|
+
cover_statistic[:methods] = aggregated.values.to_h
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def normalize_method_key(key)
|
|
87
|
+
normalized_key = key.dup
|
|
88
|
+
normalized_key[0] = key[0].to_s
|
|
89
|
+
.gsub(ADDRESS_PATTERN, ADDRESS_PLACEHOLDER)
|
|
90
|
+
.sub(SINGLETON_WRAPPER_PATTERN, '\1')
|
|
91
|
+
normalized_key
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Ruby's eval coverage records a fresh set of branch entries for every
|
|
95
|
+
# COMPILE of an eval'd string: a template rendered through multiple view
|
|
96
|
+
# classes (e.g. hanami-view compiles each template once per view) yields
|
|
97
|
+
# several `[:if, id, location]` conditions at identical coordinates in
|
|
98
|
+
# the same file, each counting only the renders that flowed through that
|
|
99
|
+
# compile. Reported as-is they inflate the branch denominator and turn a
|
|
100
|
+
# side covered under a different compile into a phantom miss (issue
|
|
101
|
+
# #1235). Aggregate them by (type, location) — combining a branches hash
|
|
102
|
+
# with an empty one dedups within it, since BranchesCombiner keys arms
|
|
103
|
+
# on location identity. Regular (non-eval) source can never produce two
|
|
104
|
+
# conditions at the same location, so this is a no-op outside eval.
|
|
105
|
+
def aggregate_duplicated_branches(cover_statistic)
|
|
106
|
+
branches = cover_statistic[:branches]
|
|
107
|
+
return unless branches
|
|
108
|
+
|
|
109
|
+
cover_statistic[:branches] = Combine::BranchesCombiner.combine(branches, {})
|
|
75
110
|
end
|
|
76
111
|
|
|
77
112
|
def adapt_oneshot_lines_if_needed(file_name, cover_statistic)
|
|
@@ -66,7 +66,7 @@ module SimpleCov
|
|
|
66
66
|
|
|
67
67
|
# Applies the configured filters to the given array of SimpleCov::SourceFile items
|
|
68
68
|
def filtered(files)
|
|
69
|
-
result = files.
|
|
69
|
+
result = files.to_a.dup
|
|
70
70
|
filters.each do |filter|
|
|
71
71
|
result = result.reject { |source_file| filter.matches?(source_file) }
|
|
72
72
|
end
|
|
@@ -170,9 +170,9 @@ module SimpleCov
|
|
|
170
170
|
# off); with merging on the merged result reports dropped source files,
|
|
171
171
|
# so the per-process slice stays quiet to avoid one warning per worker.
|
|
172
172
|
def process_coverage_result(report:)
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
result, not_loaded_files = add_not_loaded_files(
|
|
173
|
+
raw = SimpleCov::UselessResultsRemover.call(Coverage.result)
|
|
174
|
+
adapted = SimpleCov::ResultAdapter.call(raw)
|
|
175
|
+
result, not_loaded_files = add_not_loaded_files(adapted)
|
|
176
176
|
@result = SimpleCov::Result.new(result, not_loaded_files: not_loaded_files, report: report)
|
|
177
177
|
end
|
|
178
178
|
end
|
|
@@ -39,8 +39,8 @@ module SimpleCov
|
|
|
39
39
|
source_lines = read_lines(absolute_path)
|
|
40
40
|
lines = coverage_stub(absolute_path, source_lines) ||
|
|
41
41
|
LinesClassifier.new.classify(source_lines)
|
|
42
|
-
|
|
43
|
-
|
|
42
|
+
empty = {"branches" => {}, "methods" => {}} #: Hash[String, Hash[untyped, untyped]]
|
|
43
|
+
synthesized = StaticCoverageExtractor.call(source_lines.join) || empty
|
|
44
44
|
|
|
45
45
|
{
|
|
46
46
|
"lines" => lines,
|
|
@@ -37,12 +37,12 @@ module SimpleCov
|
|
|
37
37
|
|
|
38
38
|
# Returns true if this is a line that should have been covered, but was not
|
|
39
39
|
def missed?
|
|
40
|
-
!never? && !skipped? && coverage.zero?
|
|
40
|
+
!never? && !skipped? && coverage.to_i.zero?
|
|
41
41
|
end
|
|
42
42
|
|
|
43
43
|
# Returns true if this is a line that has been covered
|
|
44
44
|
def covered?
|
|
45
|
-
!never? && !skipped? && coverage.positive?
|
|
45
|
+
!never? && !skipped? && coverage.to_i.positive?
|
|
46
46
|
end
|
|
47
47
|
|
|
48
48
|
# Returns true if this line is not relevant for coverage
|
|
@@ -34,7 +34,7 @@ module SimpleCov
|
|
|
34
34
|
end
|
|
35
35
|
|
|
36
36
|
def lines
|
|
37
|
-
@lines ||= start_line && end_line ? source_file.lines[(start_line - 1)..(end_line - 1)] : []
|
|
37
|
+
@lines ||= start_line && end_line ? source_file.lines[(start_line - 1)..(end_line - 1)] || [] : []
|
|
38
38
|
end
|
|
39
39
|
|
|
40
40
|
# Whether this method's source range intersects the given inclusive line range.
|
|
@@ -12,7 +12,9 @@ module SimpleCov
|
|
|
12
12
|
end
|
|
13
13
|
|
|
14
14
|
def call
|
|
15
|
-
|
|
15
|
+
none = {} #: Hash[untyped, Integer]
|
|
16
|
+
raw_methods = @source_file.coverage_data.fetch("methods", none)
|
|
17
|
+
methods = raw_methods.filter_map do |info, hit_count|
|
|
16
18
|
info = RubyDataParser.call(info)
|
|
17
19
|
next if eval_generated_to_ignore?(info)
|
|
18
20
|
|
|
@@ -14,12 +14,12 @@ module SimpleCov
|
|
|
14
14
|
module_function
|
|
15
15
|
|
|
16
16
|
def call(filename)
|
|
17
|
-
lines = []
|
|
17
|
+
lines = [] #: Array[String]
|
|
18
18
|
# The default encoding is UTF-8
|
|
19
19
|
File.open(filename, "rb:UTF-8") do |file|
|
|
20
20
|
current_line = file.gets
|
|
21
21
|
|
|
22
|
-
if shebang?(current_line)
|
|
22
|
+
if current_line && shebang?(current_line)
|
|
23
23
|
lines << current_line
|
|
24
24
|
current_line = file.gets
|
|
25
25
|
end
|
|
@@ -51,8 +51,8 @@ module SimpleCov
|
|
|
51
51
|
# returns the `{line:, branch:, method:}` Hash; pass a criterion symbol
|
|
52
52
|
# (`:line` / `:branch` / `:method`) to get that one CoverageStatistics.
|
|
53
53
|
def coverage_statistics(criterion = nil)
|
|
54
|
-
@coverage_statistics ||= Statistics.new(self).call
|
|
55
|
-
criterion ?
|
|
54
|
+
stats = (@coverage_statistics ||= Statistics.new(self).call)
|
|
55
|
+
criterion ? stats[criterion] : stats
|
|
56
56
|
end
|
|
57
57
|
|
|
58
58
|
# Returns all source lines for this file as instances of SimpleCov::SourceFile::Line,
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SimpleCov
|
|
4
|
+
module StaticCoverageExtractor
|
|
5
|
+
# Detects the `if` / `unless` / ternary conditions CRuby folds away.
|
|
6
|
+
# When a condition is a statically-known-truthy/falsy literal the
|
|
7
|
+
# compiler eliminates the dead arm and Coverage emits NO branch, so the
|
|
8
|
+
# extractor must not synthesize one either — otherwise the arm is a
|
|
9
|
+
# phantom that no loaded run can ever hit, the same unmergeable-tuple
|
|
10
|
+
# failure mode as #1226 / #1233.
|
|
11
|
+
module ConditionFolding
|
|
12
|
+
# Prism node types for the literals that fold. `while` / `until` do
|
|
13
|
+
# NOT fold (`while true` is a real branch), so only the if-like
|
|
14
|
+
# visitors consult this. Regexp and Range literals are excluded on
|
|
15
|
+
# purpose: as conditions they mean `=~ $_` / flip-flop, which
|
|
16
|
+
# Coverage does branch on.
|
|
17
|
+
STATIC_CONDITION_TYPES = [
|
|
18
|
+
::Prism::IntegerNode, ::Prism::FloatNode, ::Prism::RationalNode,
|
|
19
|
+
::Prism::ImaginaryNode, ::Prism::SymbolNode, ::Prism::StringNode,
|
|
20
|
+
::Prism::TrueNode, ::Prism::FalseNode, ::Prism::NilNode
|
|
21
|
+
].freeze
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
# Parentheses are transparent to the fold (`if (1)` folds like
|
|
26
|
+
# `if 1`), so see through a single parenthesized expression. Compound
|
|
27
|
+
# forms (`!true`, `true || x`) are deliberately not folded: `!` never
|
|
28
|
+
# folds, and `||` / `&&` constant-propagation diverges across Ruby
|
|
29
|
+
# versions, so matching it would trade a rare, version-specific gain
|
|
30
|
+
# for real risk.
|
|
31
|
+
def static_condition?(node)
|
|
32
|
+
node = unwrap_parentheses(node)
|
|
33
|
+
STATIC_CONDITION_TYPES.any? { |type| node.is_a?(type) }
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def unwrap_parentheses(node)
|
|
37
|
+
# @type var current: untyped
|
|
38
|
+
current = node
|
|
39
|
+
while current.is_a?(::Prism::ParenthesesNode)
|
|
40
|
+
body = current.body
|
|
41
|
+
break unless body.is_a?(::Prism::StatementsNode) && body.body.size == 1
|
|
42
|
+
|
|
43
|
+
current = body.body.first
|
|
44
|
+
end
|
|
45
|
+
current
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|