simplecov 0.22.0 → 1.0.0.rc1
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/CHANGELOG.md +81 -1
- data/LICENSE +1 -1
- data/README.md +1009 -511
- data/doc/alternate-formatters.md +0 -5
- data/doc/commercial-services.md +5 -5
- data/exe/simplecov +11 -0
- data/lib/minitest/simplecov_plugin.rb +13 -5
- data/lib/simplecov/autostart.rb +11 -0
- data/lib/simplecov/cli/clean.rb +47 -0
- data/lib/simplecov/cli/coverage.rb +91 -0
- data/lib/simplecov/cli/diff.rb +151 -0
- data/lib/simplecov/cli/dotfile.rb +100 -0
- data/lib/simplecov/cli/merge.rb +116 -0
- data/lib/simplecov/cli/open.rb +50 -0
- data/lib/simplecov/cli/report.rb +84 -0
- data/lib/simplecov/cli/run.rb +36 -0
- data/lib/simplecov/cli/serve.rb +139 -0
- data/lib/simplecov/cli/uncovered.rb +107 -0
- data/lib/simplecov/cli.rb +150 -0
- data/lib/simplecov/color.rb +74 -0
- data/lib/simplecov/combine/branches_combiner.rb +3 -2
- data/lib/simplecov/combine/files_combiner.rb +7 -1
- data/lib/simplecov/combine/lines_combiner.rb +19 -17
- data/lib/simplecov/combine/methods_combiner.rb +26 -0
- data/lib/simplecov/combine/results_combiner.rb +5 -4
- data/lib/simplecov/command_guesser.rb +46 -32
- data/lib/simplecov/configuration/coverage.rb +171 -0
- data/lib/simplecov/configuration/coverage_criteria.rb +156 -0
- data/lib/simplecov/configuration/filters.rb +195 -0
- data/lib/simplecov/configuration/formatting.rb +119 -0
- data/lib/simplecov/configuration/ignored_entries.rb +63 -0
- data/lib/simplecov/configuration/merging.rb +74 -0
- data/lib/simplecov/configuration/thresholds.rb +174 -0
- data/lib/simplecov/configuration.rb +79 -405
- data/lib/simplecov/coverage_statistics.rb +12 -9
- data/lib/simplecov/coverage_violations.rb +148 -0
- data/lib/simplecov/defaults.rb +27 -20
- data/lib/simplecov/directive.rb +162 -0
- data/lib/simplecov/exit_codes/exit_code_handling.rb +8 -2
- data/lib/simplecov/exit_codes/maximum_coverage_drop_check.rb +19 -57
- data/lib/simplecov/exit_codes/maximum_overall_coverage_check.rb +45 -0
- data/lib/simplecov/exit_codes/minimum_coverage_by_file_check.rb +17 -27
- data/lib/simplecov/exit_codes/minimum_coverage_by_group_check.rb +41 -0
- data/lib/simplecov/exit_codes/minimum_overall_coverage_check.rb +38 -21
- data/lib/simplecov/exit_codes.rb +3 -0
- data/lib/simplecov/exit_handling.rb +158 -0
- data/lib/simplecov/file_list.rb +61 -17
- data/lib/simplecov/filter.rb +69 -24
- data/lib/simplecov/formatter/base.rb +101 -0
- data/lib/simplecov/formatter/html_formatter/public/application.css +1 -0
- data/lib/simplecov/formatter/html_formatter/public/application.js +18 -0
- data/lib/simplecov/formatter/html_formatter/public/favicon_green.png +0 -0
- data/lib/simplecov/formatter/html_formatter/public/favicon_red.png +0 -0
- data/lib/simplecov/formatter/html_formatter/public/favicon_yellow.png +0 -0
- data/lib/simplecov/formatter/html_formatter/public/index.html +56 -0
- data/lib/simplecov/formatter/html_formatter.rb +79 -0
- data/lib/simplecov/formatter/json_formatter/errors_formatter.rb +84 -0
- data/lib/simplecov/formatter/json_formatter/result_hash_formatter.rb +127 -0
- data/lib/simplecov/formatter/json_formatter/source_file_formatter.rb +99 -0
- data/lib/simplecov/formatter/json_formatter.rb +77 -0
- data/lib/simplecov/formatter/multi_formatter.rb +4 -5
- data/lib/simplecov/formatter/simple_formatter.rb +9 -11
- data/lib/simplecov/formatter.rb +4 -0
- data/lib/simplecov/last_run.rb +10 -3
- data/lib/simplecov/lines_classifier.rb +26 -13
- data/lib/simplecov/load_global_config.rb +9 -4
- data/lib/simplecov/parallel_adapters/base.rb +51 -0
- data/lib/simplecov/parallel_adapters/generic.rb +42 -0
- data/lib/simplecov/parallel_adapters/parallel_tests.rb +77 -0
- data/lib/simplecov/parallel_adapters.rb +83 -0
- data/lib/simplecov/parallel_coordination.rb +95 -0
- data/lib/simplecov/process.rb +20 -14
- data/lib/simplecov/profiles/bundler_filter.rb +1 -1
- data/lib/simplecov/profiles/hidden_filter.rb +1 -1
- data/lib/simplecov/profiles/rails.rb +24 -10
- data/lib/simplecov/profiles/root_filter.rb +6 -5
- data/lib/simplecov/profiles/strict.rb +32 -0
- data/lib/simplecov/profiles/test_frameworks.rb +1 -4
- data/lib/simplecov/profiles.rb +32 -3
- data/lib/simplecov/result/missing_source_files_reporter.rb +49 -0
- data/lib/simplecov/result/source_file_builder.rb +51 -0
- data/lib/simplecov/result.rb +97 -19
- data/lib/simplecov/result_adapter.rb +68 -6
- data/lib/simplecov/result_merger/legacy_format_adapter.rb +28 -0
- data/lib/simplecov/result_merger/resultset_file.rb +38 -0
- data/lib/simplecov/result_merger/resultset_store.rb +50 -0
- data/lib/simplecov/result_merger.rb +46 -90
- data/lib/simplecov/result_processing.rb +162 -0
- data/lib/simplecov/simulate_coverage.rb +54 -8
- data/lib/simplecov/source_file/branch.rb +1 -3
- data/lib/simplecov/source_file/branch_builder.rb +114 -0
- data/lib/simplecov/source_file/builder_context.rb +28 -0
- data/lib/simplecov/source_file/line.rb +7 -2
- data/lib/simplecov/source_file/line_builder.rb +43 -0
- data/lib/simplecov/source_file/method.rb +52 -0
- data/lib/simplecov/source_file/method_builder.rb +58 -0
- data/lib/simplecov/source_file/ruby_data_parser.rb +88 -0
- data/lib/simplecov/source_file/skip_chunks.rb +77 -0
- data/lib/simplecov/source_file/source_loader.rb +63 -0
- data/lib/simplecov/source_file/statistics.rb +57 -0
- data/lib/simplecov/source_file.rb +66 -232
- data/lib/simplecov/static_coverage_extractor/visitor.rb +193 -0
- data/lib/simplecov/static_coverage_extractor.rb +111 -0
- data/lib/simplecov/useless_results_remover.rb +16 -7
- data/lib/simplecov/version.rb +1 -1
- data/lib/simplecov-html.rb +4 -0
- data/lib/simplecov.rb +131 -377
- data/lib/simplecov_json_formatter.rb +4 -0
- data/schemas/coverage-v1.0.schema.json +300 -0
- data/schemas/coverage.schema.json +300 -0
- metadata +88 -56
- data/lib/simplecov/default_formatter.rb +0 -20
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
require_relative "result_merger/legacy_format_adapter"
|
|
4
|
+
require_relative "result_merger/resultset_file"
|
|
5
|
+
require_relative "result_merger/resultset_store"
|
|
4
6
|
|
|
5
7
|
module SimpleCov
|
|
6
8
|
#
|
|
@@ -10,13 +12,8 @@ module SimpleCov
|
|
|
10
12
|
#
|
|
11
13
|
module ResultMerger
|
|
12
14
|
class << self
|
|
13
|
-
# The path to the .resultset.json cache file
|
|
14
15
|
def resultset_path
|
|
15
|
-
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
def resultset_writelock
|
|
19
|
-
File.join(SimpleCov.coverage_path, ".resultset.json.lock")
|
|
16
|
+
ResultsetStore.resultset_path
|
|
20
17
|
end
|
|
21
18
|
|
|
22
19
|
def merge_and_store(*file_paths, ignore_timeout: false)
|
|
@@ -44,50 +41,37 @@ module SimpleCov
|
|
|
44
41
|
end
|
|
45
42
|
|
|
46
43
|
def valid_results(file_path, ignore_timeout: false)
|
|
47
|
-
|
|
48
|
-
merge_valid_results(results, ignore_timeout: ignore_timeout)
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
def parse_file(path)
|
|
52
|
-
data = read_file(path)
|
|
53
|
-
parse_json(data)
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
def read_file(path)
|
|
57
|
-
return unless File.exist?(path)
|
|
58
|
-
|
|
59
|
-
data = File.read(path)
|
|
60
|
-
return if data.nil? || data.length < 2
|
|
61
|
-
|
|
62
|
-
data
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
def parse_json(content)
|
|
66
|
-
return {} unless content
|
|
67
|
-
|
|
68
|
-
JSON.parse(content) || {}
|
|
69
|
-
rescue StandardError
|
|
70
|
-
warn "[SimpleCov]: Warning! Parsing JSON content of resultset file failed"
|
|
71
|
-
{}
|
|
44
|
+
merge_valid_results(ResultsetFile.parse(file_path), ignore_timeout: ignore_timeout)
|
|
72
45
|
end
|
|
73
46
|
|
|
74
47
|
def merge_valid_results(results, ignore_timeout: false)
|
|
75
|
-
results = results
|
|
48
|
+
results = drop_expired_results(results) unless ignore_timeout
|
|
76
49
|
|
|
77
50
|
command_plus_coverage = results.map do |command_name, data|
|
|
78
|
-
[[command_name],
|
|
51
|
+
[[command_name], LegacyFormatAdapter.call(data.fetch("coverage"))]
|
|
79
52
|
end
|
|
80
53
|
|
|
81
54
|
# one file itself _might_ include multiple test runs
|
|
82
55
|
merge_coverage(*command_plus_coverage)
|
|
83
56
|
end
|
|
84
57
|
|
|
58
|
+
def drop_expired_results(results)
|
|
59
|
+
fresh, expired = results.partition { |_command_name, data| within_merge_timeout?(data) }
|
|
60
|
+
return results if expired.empty?
|
|
61
|
+
|
|
62
|
+
warn_about_expired_results(expired.map(&:first))
|
|
63
|
+
fresh.to_h
|
|
64
|
+
end
|
|
65
|
+
|
|
85
66
|
def within_merge_timeout?(data)
|
|
86
|
-
|
|
67
|
+
(Time.now - Time.at(data.fetch("timestamp"))) < SimpleCov.merge_timeout
|
|
87
68
|
end
|
|
88
69
|
|
|
89
|
-
def
|
|
90
|
-
|
|
70
|
+
def warn_about_expired_results(expired_command_names)
|
|
71
|
+
warn "[SimpleCov]: Excluded #{expired_command_names.size} result(s) older than " \
|
|
72
|
+
"merge_timeout (#{SimpleCov.merge_timeout}s) from the merged report: " \
|
|
73
|
+
"#{expired_command_names.sort.join(', ')}. " \
|
|
74
|
+
"Increase SimpleCov.merge_timeout to include them."
|
|
91
75
|
end
|
|
92
76
|
|
|
93
77
|
def create_result(command_names, coverage)
|
|
@@ -104,9 +88,7 @@ module SimpleCov
|
|
|
104
88
|
results.reduce do |(memo_command, memo_coverage), (command, coverage)|
|
|
105
89
|
# timestamp is dropped here, which is intentional (we merge it, it gets a new time stamp as of now)
|
|
106
90
|
merged_coverage = Combine.combine(Combine::ResultsCombiner, memo_coverage, coverage)
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
[merged_command, merged_coverage]
|
|
91
|
+
[memo_command + command, merged_coverage]
|
|
110
92
|
end
|
|
111
93
|
end
|
|
112
94
|
|
|
@@ -115,79 +97,53 @@ module SimpleCov
|
|
|
115
97
|
# SimpleCov::Result with merged coverage data and the command_name
|
|
116
98
|
# for the result consisting of a join on all source result's names
|
|
117
99
|
def merged_result
|
|
118
|
-
|
|
119
|
-
# it's more involved to make syre `synchronize_resultset` is only used around reading
|
|
120
|
-
resultset_hash = read_resultset
|
|
121
|
-
command_names, coverage = merge_valid_results(resultset_hash)
|
|
122
|
-
|
|
100
|
+
command_names, coverage = merge_valid_results(read_resultset)
|
|
123
101
|
create_result(command_names, coverage)
|
|
124
102
|
end
|
|
125
103
|
|
|
126
104
|
def read_resultset
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
read_file(resultset_path)
|
|
130
|
-
end
|
|
131
|
-
|
|
132
|
-
parse_json(resultset_content)
|
|
105
|
+
content = synchronize_resultset { ResultsetFile.read(resultset_path) }
|
|
106
|
+
ResultsetFile.decode(content)
|
|
133
107
|
end
|
|
134
108
|
|
|
135
109
|
# Saves the given SimpleCov::Result in the resultset cache
|
|
136
|
-
def store_result(result)
|
|
110
|
+
def store_result(result) # rubocop:disable Naming/PredicateMethod
|
|
137
111
|
synchronize_resultset do
|
|
138
112
|
# Ensure we have the latest, in case it was already cached
|
|
139
113
|
new_resultset = read_resultset
|
|
140
114
|
|
|
141
115
|
# A single result only ever has one command_name, see `SimpleCov::Result#to_hash`
|
|
142
116
|
command_name, data = result.to_hash.first
|
|
143
|
-
new_resultset[command_name] = data
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
end
|
|
117
|
+
new_resultset[command_name] = merged_entry(new_resultset[command_name], data)
|
|
118
|
+
|
|
119
|
+
ResultsetStore.write(new_resultset)
|
|
147
120
|
end
|
|
148
121
|
true
|
|
149
122
|
end
|
|
150
123
|
|
|
151
|
-
#
|
|
152
|
-
#
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
@resultset_locked = true
|
|
159
|
-
File.open(resultset_writelock, "w+") do |f|
|
|
160
|
-
f.flock(File::LOCK_EX)
|
|
161
|
-
yield
|
|
162
|
-
end
|
|
163
|
-
ensure
|
|
164
|
-
@resultset_locked = false
|
|
165
|
-
end
|
|
166
|
-
end
|
|
124
|
+
# If an entry with the same command_name was written AFTER our process
|
|
125
|
+
# started, a sibling test runner (typically a subprocess our parent
|
|
126
|
+
# process shelled out to) wrote it. Combine coverage data rather than
|
|
127
|
+
# overwriting, so an empty parent-process result doesn't clobber the
|
|
128
|
+
# subprocess's real data. See https://github.com/simplecov-ruby/simplecov/issues/581.
|
|
129
|
+
def merged_entry(existing, incoming)
|
|
130
|
+
return incoming unless concurrent_runner_entry?(existing)
|
|
167
131
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
# See https://github.com/simplecov-ruby/simplecov/pull/824#issuecomment-576049747
|
|
172
|
-
def adapt_result(result)
|
|
173
|
-
if pre_simplecov_0_18_result?(result)
|
|
174
|
-
adapt_pre_simplecov_0_18_result(result)
|
|
175
|
-
else
|
|
176
|
-
result
|
|
177
|
-
end
|
|
132
|
+
incoming.merge(
|
|
133
|
+
"coverage" => Combine.combine(Combine::ResultsCombiner, existing["coverage"], incoming["coverage"])
|
|
134
|
+
)
|
|
178
135
|
end
|
|
179
136
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
_key, data = result.first
|
|
137
|
+
def concurrent_runner_entry?(entry)
|
|
138
|
+
return false unless entry.is_a?(Hash)
|
|
183
139
|
|
|
184
|
-
|
|
140
|
+
timestamp = entry["timestamp"]
|
|
141
|
+
process_start = SimpleCov.process_start_time
|
|
142
|
+
timestamp && process_start && timestamp.to_i >= process_start.to_i
|
|
185
143
|
end
|
|
186
144
|
|
|
187
|
-
def
|
|
188
|
-
|
|
189
|
-
{"lines" => line_coverage_data}
|
|
190
|
-
end
|
|
145
|
+
def synchronize_resultset(&)
|
|
146
|
+
ResultsetStore.synchronize(&)
|
|
191
147
|
end
|
|
192
148
|
end
|
|
193
149
|
end
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Result-building façade: turns the raw `Coverage.result` hash into a
|
|
4
|
+
# `SimpleCov::Result`, applies filters and groups, drives merging
|
|
5
|
+
# across test suites via `SimpleCov::ResultMerger`, and exposes the
|
|
6
|
+
# `collate` entry point for stitching disparate resultsets together.
|
|
7
|
+
module SimpleCov
|
|
8
|
+
class << self
|
|
9
|
+
#
|
|
10
|
+
# Collate a series of SimpleCov result files into a single SimpleCov output.
|
|
11
|
+
#
|
|
12
|
+
# See README for usage. By default `collate` ignores the merge_timeout
|
|
13
|
+
# so all results in all files specified will be merged. Pass
|
|
14
|
+
# `ignore_timeout: false` to honor it.
|
|
15
|
+
#
|
|
16
|
+
def collate(result_filenames, profile = nil, ignore_timeout: true, &block)
|
|
17
|
+
raise ArgumentError, "There are no reports to be merged" if result_filenames.empty?
|
|
18
|
+
|
|
19
|
+
initial_setup(profile, &block)
|
|
20
|
+
|
|
21
|
+
# Use the ResultMerger to produce a single, merged result, ready to use.
|
|
22
|
+
@result = ResultMerger.merge_and_store(*result_filenames, ignore_timeout: ignore_timeout)
|
|
23
|
+
|
|
24
|
+
run_exit_tasks!
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
#
|
|
28
|
+
# Returns the result for the current coverage run, merging it across test suites
|
|
29
|
+
# from cache using SimpleCov::ResultMerger if use_merging is activated (default)
|
|
30
|
+
#
|
|
31
|
+
def result
|
|
32
|
+
return @result if result?
|
|
33
|
+
|
|
34
|
+
# Collect our coverage result
|
|
35
|
+
process_coverage_result if defined?(Coverage) && Coverage.running?
|
|
36
|
+
|
|
37
|
+
# If we're using merging of results, store the current result
|
|
38
|
+
# first (if there is one), then merge the results and return those
|
|
39
|
+
if merging
|
|
40
|
+
wait_for_other_processes
|
|
41
|
+
SimpleCov::ResultMerger.store_result(@result) if result?
|
|
42
|
+
@result = SimpleCov::ResultMerger.merged_result
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
@result
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Returns nil if the result has not been computed, otherwise the result.
|
|
49
|
+
def result?
|
|
50
|
+
defined?(@result) && @result
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Applies the configured filters to the given array of SimpleCov::SourceFile items
|
|
54
|
+
def filtered(files)
|
|
55
|
+
result = files.clone
|
|
56
|
+
filters.each do |filter|
|
|
57
|
+
result = result.reject { |source_file| filter.matches?(source_file) }
|
|
58
|
+
end
|
|
59
|
+
SimpleCov::FileList.new result
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Bin the given source files by group filter. `groups:` defaults to
|
|
63
|
+
# `SimpleCov.groups`; pass a Hash explicitly to bin against a
|
|
64
|
+
# different group config (e.g., the snapshot a Result captured at
|
|
65
|
+
# construction). Files matched by no group fall into the implicit
|
|
66
|
+
# "Ungrouped" bucket.
|
|
67
|
+
def grouped(files, groups: SimpleCov.groups)
|
|
68
|
+
return {} if groups.empty?
|
|
69
|
+
|
|
70
|
+
grouped = groups.transform_values do |filter|
|
|
71
|
+
SimpleCov::FileList.new(files.select { |source_file| filter.matches?(source_file) })
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
in_group = grouped_file_set(grouped)
|
|
75
|
+
ungrouped = files.reject { |source_file| in_group.include?(source_file) }
|
|
76
|
+
grouped["Ungrouped"] = SimpleCov::FileList.new(ungrouped) if ungrouped.any?
|
|
77
|
+
|
|
78
|
+
grouped
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Applies the profile of given name on SimpleCov configuration
|
|
82
|
+
def load_profile(name)
|
|
83
|
+
profiles.load(name)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Clear out the previously cached .result. Primarily useful in testing.
|
|
87
|
+
def clear_result
|
|
88
|
+
@result = nil
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# @api private — persist the per-criterion coverage percentages
|
|
92
|
+
# rounded down (see #679) so the next run can compute drift.
|
|
93
|
+
def write_last_run(result)
|
|
94
|
+
SimpleCov::LastRun.write(
|
|
95
|
+
result: result.coverage_statistics.transform_values { |stats| round_coverage(stats.percent) }
|
|
96
|
+
)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# @api private — round down to two decimals to be extra strict.
|
|
100
|
+
def round_coverage(coverage)
|
|
101
|
+
coverage.floor(2)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
private
|
|
105
|
+
|
|
106
|
+
def initial_setup(profile, &block)
|
|
107
|
+
load_profile(profile) if profile
|
|
108
|
+
configure(&block) if block
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def grouped_file_set(grouped)
|
|
112
|
+
grouped.values.each_with_object(Set.new) { |file_list, set| set.merge(file_list) }
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Finds files that were to be tracked but were not loaded, and
|
|
116
|
+
# initializes their line-by-line coverage to zero (or nil for
|
|
117
|
+
# comments / whitespace).
|
|
118
|
+
def add_not_loaded_files(result)
|
|
119
|
+
globs = unloaded_file_discovery_globs
|
|
120
|
+
return [result, Set.new] if globs.empty?
|
|
121
|
+
|
|
122
|
+
inject_unloaded_files(result.dup, discover_unloaded_paths(globs))
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Globs to expand on disk when injecting unloaded files into the
|
|
126
|
+
# result. Combines the legacy `track_files` glob (additive only)
|
|
127
|
+
# with every string glob declared via `cover` (also restrictive,
|
|
128
|
+
# but the restriction lives in `Result#apply_cover_filters!`).
|
|
129
|
+
def unloaded_file_discovery_globs
|
|
130
|
+
[tracked_files, *cover_globs].compact
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Expand the given globs relative to SimpleCov.root, not Dir.pwd —
|
|
134
|
+
# test runners that chdir (or CI scripts that invoke the suite
|
|
135
|
+
# from a subdir) would otherwise silently miss the unloaded-file
|
|
136
|
+
# injection and produce a different file set per environment. See
|
|
137
|
+
# issue #1106.
|
|
138
|
+
def discover_unloaded_paths(globs)
|
|
139
|
+
globs.flat_map { |glob| Dir.glob(glob, base: root) }.uniq
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def inject_unloaded_files(result, candidate_paths)
|
|
143
|
+
not_loaded_files = candidate_paths.each_with_object(Set.new) do |file, set|
|
|
144
|
+
absolute_path = File.expand_path(file, root)
|
|
145
|
+
next if result.key?(absolute_path)
|
|
146
|
+
|
|
147
|
+
result[absolute_path] = SimulateCoverage.call(absolute_path)
|
|
148
|
+
set << absolute_path
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
[result, not_loaded_files]
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# Run all the steps that handle processing the raw coverage result.
|
|
155
|
+
def process_coverage_result
|
|
156
|
+
@result = SimpleCov::UselessResultsRemover.call(Coverage.result)
|
|
157
|
+
@result = SimpleCov::ResultAdapter.call(@result)
|
|
158
|
+
result, not_loaded_files = add_not_loaded_files(@result)
|
|
159
|
+
@result = SimpleCov::Result.new(result, not_loaded_files: not_loaded_files)
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "static_coverage_extractor"
|
|
4
|
+
|
|
3
5
|
module SimpleCov
|
|
4
6
|
#
|
|
5
7
|
# Responsible for producing file coverage metrics.
|
|
@@ -8,22 +10,66 @@ module SimpleCov
|
|
|
8
10
|
module_function
|
|
9
11
|
|
|
10
12
|
#
|
|
11
|
-
# Simulate
|
|
12
|
-
#
|
|
13
|
+
# Simulate a file coverage report for a file that was tracked but never
|
|
14
|
+
# required. Returns the same hash shape as `Coverage.result` (lines,
|
|
15
|
+
# branches, methods).
|
|
16
|
+
#
|
|
17
|
+
# The line classification comes from `Coverage.line_stub` — the same
|
|
18
|
+
# classification the runtime would have produced if the file had been
|
|
19
|
+
# required — overlaid with SimpleCov's `# :nocov:` toggles and
|
|
20
|
+
# `# simplecov:disable line` directive ranges, which `Coverage` doesn't
|
|
21
|
+
# know about. This keeps "relevant lines" identical whether a file was
|
|
22
|
+
# loaded or just tracked, fixing the multi-line statement discrepancy
|
|
23
|
+
# in https://github.com/simplecov-ruby/simplecov/issues/654.
|
|
13
24
|
#
|
|
14
|
-
#
|
|
25
|
+
# Branches and methods are enumerated by static analysis (via
|
|
26
|
+
# `StaticCoverageExtractor`, which uses Prism). Earlier behavior left
|
|
27
|
+
# both as empty hashes, which made unloaded files invisible to the
|
|
28
|
+
# branch/method denominators while their lines DID count — so a
|
|
29
|
+
# `track_files`/`cover` glob that picked up files without specs
|
|
30
|
+
# silently inflated branch% relative to line%. See
|
|
31
|
+
# https://github.com/simplecov-ruby/simplecov/issues/1059. When Prism
|
|
32
|
+
# isn't loadable (Ruby < 3.3 without the prism gem) or the file
|
|
33
|
+
# can't be parsed, fall back to the old empty hashes — old behavior,
|
|
34
|
+
# old tradeoff.
|
|
15
35
|
#
|
|
16
36
|
# @return [Hash]
|
|
17
37
|
#
|
|
18
38
|
def call(absolute_path)
|
|
19
|
-
|
|
39
|
+
source_lines = read_lines(absolute_path)
|
|
40
|
+
lines = coverage_stub(absolute_path, source_lines) ||
|
|
41
|
+
LinesClassifier.new.classify(source_lines)
|
|
42
|
+
synthesized = StaticCoverageExtractor.call(source_lines.join) ||
|
|
43
|
+
{"branches" => {}, "methods" => {}}
|
|
20
44
|
|
|
21
45
|
{
|
|
22
|
-
"lines" =>
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
"branches" => {}
|
|
46
|
+
"lines" => lines,
|
|
47
|
+
"branches" => synthesized["branches"],
|
|
48
|
+
"methods" => synthesized["methods"]
|
|
26
49
|
}
|
|
27
50
|
end
|
|
51
|
+
|
|
52
|
+
def read_lines(path)
|
|
53
|
+
File.readlines(path)
|
|
54
|
+
rescue Errno::ENOENT
|
|
55
|
+
[]
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Combine `Coverage.line_stub` (which gets multi-line statements right)
|
|
59
|
+
# with `LinesClassifier` (which knows about `# :nocov:` toggles and
|
|
60
|
+
# `# simplecov:disable line` ranges). Returns nil — and the caller
|
|
61
|
+
# falls back to `LinesClassifier` alone — when `Coverage` can't read
|
|
62
|
+
# or parse the file, or when the runtime doesn't expose `line_stub`
|
|
63
|
+
# (JRuby and TruffleRuby).
|
|
64
|
+
def coverage_stub(path, source_lines)
|
|
65
|
+
return nil unless Coverage.respond_to?(:line_stub)
|
|
66
|
+
|
|
67
|
+
stub = Coverage.line_stub(path)
|
|
68
|
+
classifier_output = LinesClassifier.new.classify(source_lines)
|
|
69
|
+
stub.each_index { |idx| stub[idx] = nil if classifier_output[idx].nil? }
|
|
70
|
+
stub
|
|
71
|
+
rescue Errno::ENOENT, SyntaxError
|
|
72
|
+
nil
|
|
73
|
+
end
|
|
28
74
|
end
|
|
29
75
|
end
|
|
@@ -8,7 +8,6 @@ module SimpleCov
|
|
|
8
8
|
class Branch
|
|
9
9
|
attr_reader :start_line, :end_line, :coverage, :type
|
|
10
10
|
|
|
11
|
-
# rubocop:disable Metrics/ParameterLists
|
|
12
11
|
def initialize(start_line:, end_line:, coverage:, inline:, type:)
|
|
13
12
|
@start_line = start_line
|
|
14
13
|
@end_line = end_line
|
|
@@ -17,7 +16,6 @@ module SimpleCov
|
|
|
17
16
|
@type = type
|
|
18
17
|
@skipped = false
|
|
19
18
|
end
|
|
20
|
-
# rubocop:enable Metrics/ParameterLists
|
|
21
19
|
|
|
22
20
|
def inline?
|
|
23
21
|
@inline
|
|
@@ -33,7 +31,7 @@ module SimpleCov
|
|
|
33
31
|
end
|
|
34
32
|
|
|
35
33
|
#
|
|
36
|
-
# Check if
|
|
34
|
+
# Check if branch missed or not
|
|
37
35
|
#
|
|
38
36
|
# @return [Boolean]
|
|
39
37
|
#
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SimpleCov
|
|
4
|
+
class SourceFile
|
|
5
|
+
# Builds the `SourceFile::Branch` objects for a source file from
|
|
6
|
+
# the raw branch data Ruby's Coverage library reports. Applies the
|
|
7
|
+
# `ignore_branches :eval_generated` / `:implicit_else` filters and
|
|
8
|
+
# marks branches inside `# simplecov:disable` / `# :nocov:` chunks
|
|
9
|
+
# as skipped.
|
|
10
|
+
class BranchBuilder
|
|
11
|
+
def initialize(source_file)
|
|
12
|
+
@source_file = source_file
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def call
|
|
16
|
+
coverage_branch_data = @source_file.coverage_data["branches"] || {}
|
|
17
|
+
branches = coverage_branch_data.flat_map do |condition, coverage_branches|
|
|
18
|
+
next [] if eval_generated_condition_to_ignore?(condition)
|
|
19
|
+
|
|
20
|
+
build_branches_from(condition, coverage_branches)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
process_skipped(branches)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
# Detect a Coverage-reported branch condition that originates from
|
|
29
|
+
# `eval`/`module_eval`/`class_eval`/`instance_eval` rather than from
|
|
30
|
+
# the file's literal source. Coverage attributes such branches to the
|
|
31
|
+
# caller's `__FILE__`/`__LINE__`, so a Rails `delegate :foo, to: :bar`
|
|
32
|
+
# call surfaces inside the source file as if there were branches at
|
|
33
|
+
# the `delegate` line. Prism never sees those branches in the static
|
|
34
|
+
# source, so a condition whose start_line isn't in the real-source
|
|
35
|
+
# branch set must be eval-generated. Only consulted when the user has
|
|
36
|
+
# opted in via `SimpleCov.ignore_branches :eval_generated`. See #1046.
|
|
37
|
+
def eval_generated_condition_to_ignore?(condition)
|
|
38
|
+
return false unless SimpleCov.ignored_branch?(:eval_generated)
|
|
39
|
+
|
|
40
|
+
positions = @source_file.real_source_positions
|
|
41
|
+
# simplecov:disable branch — nil branch fires only when Prism is unavailable
|
|
42
|
+
return false unless positions
|
|
43
|
+
|
|
44
|
+
# simplecov:enable branch
|
|
45
|
+
|
|
46
|
+
_type, _id, start_line, * = RubyDataParser.call(condition)
|
|
47
|
+
!positions[:branches].include?(start_line)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def build_branches_from(condition, branches)
|
|
51
|
+
# the format handed in from the coverage data is like this:
|
|
52
|
+
#
|
|
53
|
+
# [:then, 4, 6, 6, 6, 10]
|
|
54
|
+
#
|
|
55
|
+
# which is [type, id, start_line, start_col, end_line, end_col]
|
|
56
|
+
_condition_type, _condition_id, *condition_range = RubyDataParser.call(condition)
|
|
57
|
+
|
|
58
|
+
branches.filter_map do |branch_data, hit_count|
|
|
59
|
+
build_branch(RubyDataParser.call(branch_data), hit_count, condition_range)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def build_branch(branch_data, hit_count, condition_range)
|
|
64
|
+
type, _id, start_line, start_col, end_line, end_col = branch_data
|
|
65
|
+
return nil if implicit_else_to_ignore?(type, [start_line, start_col, end_line, end_col], condition_range)
|
|
66
|
+
|
|
67
|
+
SourceFile::Branch.new(
|
|
68
|
+
start_line: start_line,
|
|
69
|
+
end_line: end_line,
|
|
70
|
+
coverage: hit_count,
|
|
71
|
+
inline: start_line == condition_range.first,
|
|
72
|
+
type: type
|
|
73
|
+
)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Detect synthetic `:else` branches that Ruby's Coverage library reports
|
|
77
|
+
# for constructs with no literal `else` keyword in source (`case/in` /
|
|
78
|
+
# `case/when` without else, `||=`, `&&=`, `if`/`unless` without else,
|
|
79
|
+
# and the postfix `return if cond` shape). The signal is structural:
|
|
80
|
+
# a synthetic else reuses its parent condition's *full source range*
|
|
81
|
+
# (start_line, start_col, end_line, end_col all identical), while an
|
|
82
|
+
# explicit `else` arm carries a narrower range — its own keyword/body
|
|
83
|
+
# position rather than the whole conditional. Comparing the full range
|
|
84
|
+
# (not just `start_line`) is what distinguishes a ternary's explicit
|
|
85
|
+
# else on the same line as the condition — `arg == 42 ? :yes : :no`,
|
|
86
|
+
# where the else's columns differ from the parent's — from a postfix
|
|
87
|
+
# `return if cond` where the synthetic else inherits the full range.
|
|
88
|
+
# Only consulted when the user has opted in via
|
|
89
|
+
# `SimpleCov.ignore_branches :implicit_else`. See #1033.
|
|
90
|
+
def implicit_else_to_ignore?(type, branch_range, condition_range)
|
|
91
|
+
return false unless type == :else
|
|
92
|
+
return false unless SimpleCov.ignored_branch?(:implicit_else)
|
|
93
|
+
|
|
94
|
+
branch_range == condition_range
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def process_skipped(branches)
|
|
98
|
+
chunks = @source_file.skip_chunks_for(:branch)
|
|
99
|
+
return branches if chunks.empty?
|
|
100
|
+
|
|
101
|
+
# A non-inline branch's source range starts on its arm body (e.g. the
|
|
102
|
+
# `:yes` line of `if cond / :yes / else / :no / end`), but `report_line`
|
|
103
|
+
# is the condition line above it — that's where the user sees the
|
|
104
|
+
# branch in the report and where they would naturally place an inline
|
|
105
|
+
# `# simplecov:disable branch` directive. Honour both.
|
|
106
|
+
branches.each do |branch|
|
|
107
|
+
branch.skipped! if chunks.any? { |chunk| branch.overlaps_with?(chunk) || chunk.include?(branch.report_line) }
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
branches
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SimpleCov
|
|
4
|
+
class SourceFile
|
|
5
|
+
# Helpers shared by the per-criterion builders (LineBuilder /
|
|
6
|
+
# BranchBuilder / MethodBuilder). Mixed into SourceFile so each
|
|
7
|
+
# builder can ask the file for its skip-chunk ranges and Prism-derived
|
|
8
|
+
# real source positions without duplicating the memoization.
|
|
9
|
+
module BuilderContext
|
|
10
|
+
# Skip-chunk lookup for the named criterion (`:line`, `:branch`,
|
|
11
|
+
# `:method`).
|
|
12
|
+
def skip_chunks_for(criterion)
|
|
13
|
+
(@skip_chunks ||= SkipChunks.new(filename, src)).for(criterion)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Memoized set of real source positions (branch start lines, method
|
|
17
|
+
# name+line pairs) extracted via Prism. Returns nil when Prism is
|
|
18
|
+
# unavailable or parsing fails, signaling callers to keep every
|
|
19
|
+
# Coverage entry (no false drops). The `defined?` guard preserves a
|
|
20
|
+
# nil memoization across calls.
|
|
21
|
+
def real_source_positions
|
|
22
|
+
return @real_source_positions if defined?(@real_source_positions)
|
|
23
|
+
|
|
24
|
+
@real_source_positions = StaticCoverageExtractor.real_source_positions(src.join)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -25,7 +25,9 @@ module SimpleCov
|
|
|
25
25
|
def initialize(src, line_number, coverage)
|
|
26
26
|
raise ArgumentError, "Only String accepted for source" unless src.is_a?(String)
|
|
27
27
|
raise ArgumentError, "Only Integer accepted for line_number" unless line_number.is_a?(Integer)
|
|
28
|
-
|
|
28
|
+
unless coverage.is_a?(Integer) || coverage.nil?
|
|
29
|
+
raise ArgumentError, "Only Integer and nil accepted for coverage"
|
|
30
|
+
end
|
|
29
31
|
|
|
30
32
|
@src = src
|
|
31
33
|
@line_number = line_number
|
|
@@ -65,7 +67,10 @@ module SimpleCov
|
|
|
65
67
|
return "skipped" if skipped?
|
|
66
68
|
return "never" if never?
|
|
67
69
|
return "missed" if missed?
|
|
68
|
-
|
|
70
|
+
|
|
71
|
+
# simplecov:disable — defensive: covered? is the only state left after the three above
|
|
72
|
+
"covered" if covered?
|
|
73
|
+
# simplecov:enable
|
|
69
74
|
end
|
|
70
75
|
end
|
|
71
76
|
end
|