simplecov 0.22.0 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/LICENSE +1 -1
- data/README.md +71 -916
- data/exe/simplecov +11 -0
- data/lib/minitest/simplecov_plugin.rb +13 -5
- data/lib/simplecov/atomic_file.rb +70 -0
- data/lib/simplecov/autostart.rb +11 -0
- data/lib/simplecov/cli/clean.rb +87 -0
- data/lib/simplecov/cli/command_helpers.rb +55 -0
- data/lib/simplecov/cli/coverage.rb +86 -0
- data/lib/simplecov/cli/coverage_file.rb +65 -0
- data/lib/simplecov/cli/diff.rb +147 -0
- data/lib/simplecov/cli/dotfile.rb +111 -0
- data/lib/simplecov/cli/merge.rb +120 -0
- data/lib/simplecov/cli/open.rb +48 -0
- data/lib/simplecov/cli/report.rb +97 -0
- data/lib/simplecov/cli/run.rb +36 -0
- data/lib/simplecov/cli/serve/report_preparer.rb +29 -0
- data/lib/simplecov/cli/serve/static_file_handler.rb +122 -0
- data/lib/simplecov/cli/serve.rb +85 -0
- data/lib/simplecov/cli/uncovered.rb +103 -0
- data/lib/simplecov/cli.rb +163 -0
- data/lib/simplecov/color.rb +83 -0
- data/lib/simplecov/combine/branches_combiner.rb +54 -7
- data/lib/simplecov/combine/coverage_accumulator.rb +268 -0
- data/lib/simplecov/combine/identity_interner.rb +30 -0
- data/lib/simplecov/combine/interned_counts.rb +30 -0
- data/lib/simplecov/combine/lines_combiner.rb +53 -23
- data/lib/simplecov/combine/methods_combiner.rb +74 -0
- data/lib/simplecov/combine/results_combiner.rb +13 -36
- data/lib/simplecov/combine.rb +5 -23
- data/lib/simplecov/command_guesser.rb +104 -33
- data/lib/simplecov/configuration/coverage.rb +172 -0
- data/lib/simplecov/configuration/coverage_criteria.rb +154 -0
- data/lib/simplecov/configuration/eval_coverage.rb +41 -0
- data/lib/simplecov/configuration/filters.rb +164 -0
- data/lib/simplecov/configuration/formatting.rb +138 -0
- data/lib/simplecov/configuration/groups.rb +42 -0
- data/lib/simplecov/configuration/ignored_entries.rb +63 -0
- data/lib/simplecov/configuration/merging.rb +165 -0
- data/lib/simplecov/configuration/thresholds.rb +179 -0
- data/lib/simplecov/configuration.rb +83 -420
- data/lib/simplecov/coverage_json.rb +24 -0
- data/lib/simplecov/coverage_statistics.rb +16 -10
- data/lib/simplecov/coverage_violations.rb +159 -0
- data/lib/simplecov/defaults.rb +34 -23
- data/lib/simplecov/deprecation.rb +45 -0
- data/lib/simplecov/directive.rb +167 -0
- data/lib/simplecov/exit_codes/check.rb +33 -0
- data/lib/simplecov/exit_codes/exit_code_handling.rb +8 -2
- data/lib/simplecov/exit_codes/maximum_coverage_drop_check.rb +18 -65
- data/lib/simplecov/exit_codes/maximum_overall_coverage_check.rb +32 -0
- data/lib/simplecov/exit_codes/minimum_coverage_by_file_check.rb +17 -36
- data/lib/simplecov/exit_codes/minimum_coverage_by_group_check.rb +30 -0
- data/lib/simplecov/exit_codes/minimum_overall_coverage_check.rb +37 -33
- data/lib/simplecov/exit_codes.rb +20 -0
- data/lib/simplecov/exit_handling.rb +128 -0
- data/lib/simplecov/file_list.rb +70 -24
- data/lib/simplecov/filter.rb +105 -26
- data/lib/simplecov/formatter/base.rb +118 -0
- data/lib/simplecov/formatter/coverage_json_writer.rb +97 -0
- data/lib/simplecov/formatter/html_formatter/public/index.html +87 -0
- data/lib/simplecov/formatter/html_formatter/viewer_data_validator.rb +96 -0
- data/lib/simplecov/formatter/html_formatter.rb +105 -0
- data/lib/simplecov/formatter/json_formatter/errors_formatter.rb +88 -0
- data/lib/simplecov/formatter/json_formatter/result_hash_formatter.rb +118 -0
- data/lib/simplecov/formatter/json_formatter/source_file_formatter.rb +93 -0
- data/lib/simplecov/formatter/json_formatter.rb +46 -0
- data/lib/simplecov/formatter/multi_formatter.rb +12 -8
- data/lib/simplecov/formatter/simple_formatter.rb +14 -11
- data/lib/simplecov/formatter.rb +16 -0
- data/lib/simplecov/group_names.rb +32 -0
- data/lib/simplecov/last_run.rb +18 -4
- data/lib/simplecov/lines_classifier.rb +48 -15
- data/lib/simplecov/load_global_config.rb +12 -4
- data/lib/simplecov/parallel_adapters/base.rb +78 -0
- data/lib/simplecov/parallel_adapters/generic.rb +45 -0
- data/lib/simplecov/parallel_adapters/parallel_tests.rb +93 -0
- data/lib/simplecov/parallel_adapters.rb +85 -0
- data/lib/simplecov/parallel_coordination.rb +134 -0
- data/lib/simplecov/parallel_result_merger.rb +230 -0
- data/lib/simplecov/process.rb +52 -12
- 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/report_deferral.rb +49 -0
- data/lib/simplecov/report_stamp.rb +28 -0
- 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 +145 -26
- data/lib/simplecov/result_adapter.rb +139 -8
- data/lib/simplecov/result_merger/legacy_format_adapter.rb +28 -0
- data/lib/simplecov/result_merger/resultset_file.rb +74 -0
- data/lib/simplecov/result_merger/resultset_run_identity.rb +67 -0
- data/lib/simplecov/result_merger/resultset_store.rb +53 -0
- data/lib/simplecov/result_merger/unloaded_files.rb +103 -0
- data/lib/simplecov/result_merger.rb +109 -114
- data/lib/simplecov/result_processing.rb +218 -0
- data/lib/simplecov/run_identity.rb +77 -0
- data/lib/simplecov/simulate_coverage.rb +83 -12
- 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 +9 -4
- data/lib/simplecov/source_file/line_builder.rb +43 -0
- data/lib/simplecov/source_file/method.rb +58 -0
- data/lib/simplecov/source_file/method_builder.rb +60 -0
- data/lib/simplecov/source_file/ruby_data_parser.rb +110 -0
- data/lib/simplecov/source_file/skip_chunks.rb +72 -0
- data/lib/simplecov/source_file/source_loader.rb +79 -0
- data/lib/simplecov/source_file/statistics.rb +65 -0
- data/lib/simplecov/source_file.rb +66 -232
- data/lib/simplecov/static_coverage_extractor/condition_folding.rb +239 -0
- data/lib/simplecov/static_coverage_extractor/location_conventions.rb +236 -0
- data/lib/simplecov/static_coverage_extractor/method_collector.rb +62 -0
- data/lib/simplecov/static_coverage_extractor/prism_compat.rb +55 -0
- data/lib/simplecov/static_coverage_extractor/value_position.rb +74 -0
- data/lib/simplecov/static_coverage_extractor/visitor.rb +183 -0
- data/lib/simplecov/static_coverage_extractor.rb +110 -0
- data/lib/simplecov/unloaded_file_injector.rb +75 -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 +185 -380
- data/lib/simplecov_json_formatter.rb +4 -0
- data/sig/simplecov.rbs +1777 -0
- metadata +111 -62
- data/CHANGELOG.md +0 -191
- data/doc/alternate-formatters.md +0 -71
- data/doc/commercial-services.md +0 -25
- data/doc/editor-integration.md +0 -18
- data/lib/simplecov/combine/files_combiner.rb +0 -24
- data/lib/simplecov/default_formatter.rb +0 -20
data/lib/simplecov/result.rb
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
require "digest/sha1"
|
|
4
4
|
require "forwardable"
|
|
5
|
+
require_relative "result/missing_source_files_reporter"
|
|
6
|
+
require_relative "result/source_file_builder"
|
|
5
7
|
|
|
6
8
|
module SimpleCov
|
|
7
9
|
#
|
|
@@ -10,9 +12,18 @@ module SimpleCov
|
|
|
10
12
|
#
|
|
11
13
|
class Result
|
|
12
14
|
extend Forwardable
|
|
15
|
+
|
|
13
16
|
# Returns the original Coverage.result used for this instance of SimpleCov::Result
|
|
14
17
|
attr_reader :original_result
|
|
15
|
-
#
|
|
18
|
+
# Every path the producing process was told to track, loaded or not. Carried
|
|
19
|
+
# into the resultset so a merge elsewhere can inject the ones nobody loaded
|
|
20
|
+
# without needing that process's `cover` / `track_files` config. See #1250.
|
|
21
|
+
attr_reader :tracked_files
|
|
22
|
+
# Invocation and top-level worker identities used only for parallel-result
|
|
23
|
+
# coordination. They do not change which fresh suites are merged.
|
|
24
|
+
attr_reader :run_id, :worker_id
|
|
25
|
+
# Returns all files that are applicable to this result (sans filters!) as instances of
|
|
26
|
+
# SimpleCov::SourceFile. Aliased as :source_files
|
|
16
27
|
attr_reader :files
|
|
17
28
|
alias source_files files
|
|
18
29
|
# Explicitly set the Time this result has been created
|
|
@@ -20,20 +31,48 @@ module SimpleCov
|
|
|
20
31
|
# Explicitly set the command name that was used for this coverage result. Defaults to SimpleCov.command_name
|
|
21
32
|
attr_writer :command_name
|
|
22
33
|
|
|
23
|
-
def_delegators :files, :covered_percent, :covered_percentages, :least_covered_file, :covered_strength,
|
|
34
|
+
def_delegators :files, :covered_percent, :covered_percentages, :least_covered_file, :covered_strength,
|
|
35
|
+
:covered_lines, :missed_lines,
|
|
36
|
+
:total_branches, :covered_branches, :missed_branches,
|
|
37
|
+
:total_methods, :covered_methods, :missed_methods,
|
|
38
|
+
:coverage_statistics, :coverage_statistics_by_file
|
|
24
39
|
def_delegator :files, :lines_of_code, :total_lines
|
|
25
40
|
|
|
41
|
+
# Bundles the filter and grouping configuration a Result applies to its
|
|
42
|
+
# source files after building them. Each field defaults to the SimpleCov
|
|
43
|
+
# singleton's configuration, so ordinary callers never construct one;
|
|
44
|
+
# tests pass a custom instance to opt out of (or extend) the project's
|
|
45
|
+
# filters or groups (e.g. `filters: []` to keep every file). Grouping the
|
|
46
|
+
# three together keeps Result#initialize's parameter list small.
|
|
47
|
+
class FilterConfig
|
|
48
|
+
attr_reader :filters, :cover_filters, :groups
|
|
49
|
+
|
|
50
|
+
def initialize(filters: SimpleCov.filters, cover_filters: SimpleCov.cover_filters, groups: SimpleCov.groups)
|
|
51
|
+
@filters = filters
|
|
52
|
+
@cover_filters = cover_filters
|
|
53
|
+
@groups = groups
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
26
57
|
# Initialize a new SimpleCov::Result from given Coverage.result (a Hash of filenames each containing an array of
|
|
27
|
-
# coverage data)
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
58
|
+
# coverage data).
|
|
59
|
+
#
|
|
60
|
+
# `filter_config` defaults to the SimpleCov singleton's filter / group
|
|
61
|
+
# configuration so existing call sites are unchanged. Pass a custom
|
|
62
|
+
# FilterConfig to opt out — useful for tests that build synthetic Results
|
|
63
|
+
# and don't want the project's filters or groups applied.
|
|
64
|
+
def initialize(original_result, command_name: nil, created_at: nil, not_loaded_files: Set.new,
|
|
65
|
+
tracked_files: [], run_id: nil, worker_id: nil, report: false, filter_config: FilterConfig.new)
|
|
66
|
+
@original_result = original_result.freeze
|
|
31
67
|
@command_name = command_name
|
|
32
68
|
@created_at = created_at
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
69
|
+
initialize_coordination_metadata(tracked_files, run_id, worker_id)
|
|
70
|
+
@groups_config = filter_config.groups
|
|
71
|
+
builder = SourceFileBuilder.new(original_result, not_loaded_files: not_loaded_files)
|
|
72
|
+
@files = builder.call
|
|
73
|
+
warn_about_missing_source_files(builder.missing_source_files, original_result.size) if report
|
|
74
|
+
apply_cover_filters!(filter_config.cover_filters)
|
|
75
|
+
apply_filters!(filter_config.filters)
|
|
37
76
|
end
|
|
38
77
|
|
|
39
78
|
# Returns all filenames for source files contained in this result
|
|
@@ -41,14 +80,43 @@ module SimpleCov
|
|
|
41
80
|
files.map(&:filename)
|
|
42
81
|
end
|
|
43
82
|
|
|
44
|
-
# Returns
|
|
83
|
+
# Returns the SimpleCov::SourceFile for the given path, or nil if no
|
|
84
|
+
# matching file is in this result. The path is resolved against
|
|
85
|
+
# SimpleCov.root, so callers can pass either an absolute path or a
|
|
86
|
+
# project-relative one.
|
|
87
|
+
def source_file_for(path)
|
|
88
|
+
target = File.expand_path(path, SimpleCov.root)
|
|
89
|
+
files.find { |file| file.filename == target }
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Returns the {line:/branch:/method:} coverage_statistics hash for the
|
|
93
|
+
# given file path, or nil if no matching source file is in this
|
|
94
|
+
# result. See SimpleCov::Result#source_file_for for path resolution.
|
|
95
|
+
def coverage_for(path)
|
|
96
|
+
source_file_for(path)&.coverage_statistics
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Returns a Hash of groups for this result. Define groups using SimpleCov.group 'Models', 'app/models'
|
|
45
100
|
def groups
|
|
46
|
-
@groups ||= SimpleCov.grouped(files)
|
|
101
|
+
@groups ||= SimpleCov.grouped(files, groups: @groups_config)
|
|
47
102
|
end
|
|
48
103
|
|
|
49
|
-
# Applies the configured SimpleCov.formatter on this result
|
|
104
|
+
# Applies the configured SimpleCov.formatter on this result. Returns
|
|
105
|
+
# nil if formatting has been opted out of (`SimpleCov.formatter false`
|
|
106
|
+
# / `SimpleCov.formatters []`) — the cheap path for non-final
|
|
107
|
+
# processes in a parallel CI run, which only need their
|
|
108
|
+
# `.resultset.json` on disk. See #964.
|
|
50
109
|
def format!
|
|
51
|
-
SimpleCov.formatter
|
|
110
|
+
formatter = SimpleCov.formatter
|
|
111
|
+
return nil if formatter.nil?
|
|
112
|
+
|
|
113
|
+
formatted = Formatter.format(formatter, self)
|
|
114
|
+
# Recorded regardless of how the run ends, so a parent process's
|
|
115
|
+
# clobber-prevention backstop can tell a report was produced even
|
|
116
|
+
# when this run's checks or tests failed (unlike .last_run.json,
|
|
117
|
+
# which only successful runs write).
|
|
118
|
+
SimpleCov::ReportStamp.touch
|
|
119
|
+
formatted
|
|
52
120
|
end
|
|
53
121
|
|
|
54
122
|
# Defines when this result has been created. Defaults to Time.now
|
|
@@ -64,31 +132,82 @@ module SimpleCov
|
|
|
64
132
|
|
|
65
133
|
# Returns a hash representation of this Result that can be used for marshalling it into JSON
|
|
66
134
|
def to_hash
|
|
67
|
-
{
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
135
|
+
data = {"coverage" => coverage, "timestamp" => created_at.to_f} #: Hash[String, untyped]
|
|
136
|
+
data["run_id"] = run_id if run_id
|
|
137
|
+
data["worker_id"] = worker_id if worker_id
|
|
138
|
+
# Omitted when empty so a run that tracks nothing writes the shape it
|
|
139
|
+
# always has, and so the key only appears where it carries information.
|
|
140
|
+
data["tracked_files"] = tracked_files unless tracked_files.empty?
|
|
141
|
+
{command_name => data}
|
|
73
142
|
end
|
|
74
143
|
|
|
75
144
|
# Loads a SimpleCov::Result#to_hash dump
|
|
76
145
|
def self.from_hash(hash)
|
|
77
146
|
hash.map do |command_name, data|
|
|
78
|
-
new(data.fetch("coverage"), command_name: command_name, created_at: Time.at(data["timestamp"])
|
|
147
|
+
new(data.fetch("coverage"), command_name: command_name, created_at: Time.at(data["timestamp"]),
|
|
148
|
+
tracked_files: data["tracked_files"] || [], run_id: data["run_id"],
|
|
149
|
+
worker_id: data["worker_id"])
|
|
79
150
|
end
|
|
80
151
|
end
|
|
81
152
|
|
|
82
153
|
private
|
|
83
154
|
|
|
155
|
+
def initialize_coordination_metadata(tracked_files, run_id, worker_id)
|
|
156
|
+
@tracked_files = tracked_files.to_a
|
|
157
|
+
@run_id = run_id
|
|
158
|
+
@worker_id = worker_id
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def warn_about_missing_source_files(missing, input_size)
|
|
162
|
+
return if missing.empty?
|
|
163
|
+
|
|
164
|
+
# Emit only from the process that writes the final report. The merged
|
|
165
|
+
# result is rebuilt in every parallel worker (each one stores its own
|
|
166
|
+
# slice), so without this gate the warning prints once per worker — this
|
|
167
|
+
# is the same signal SimpleCov uses to pick the process that runs the
|
|
168
|
+
# report and threshold checks. It's intentionally not gated on
|
|
169
|
+
# print_errors: the default at_fork sets print_errors false on workers,
|
|
170
|
+
# and in many parallel runners the final-report process is itself a
|
|
171
|
+
# worker, so a print_errors gate would suppress the one warning we want.
|
|
172
|
+
# See issues #980 and #1171.
|
|
173
|
+
return unless SimpleCov.final_result_process?
|
|
174
|
+
|
|
175
|
+
MissingSourceFilesReporter.new(
|
|
176
|
+
missing,
|
|
177
|
+
input_size: input_size,
|
|
178
|
+
every_entry_dropped: @files.empty? && missing.size == input_size
|
|
179
|
+
).warn!
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# A live result's criterion keys are Symbols (`:lines`, `:branches`),
|
|
183
|
+
# while entries parsed back from `.resultset.json` carry Strings, and
|
|
184
|
+
# the combiners read only String keys. Serialize with String keys so a
|
|
185
|
+
# live result merged against a stored entry contributes its counts
|
|
186
|
+
# instead of being silently dropped for every shared file.
|
|
84
187
|
def coverage
|
|
85
|
-
|
|
86
|
-
|
|
188
|
+
original_result.slice(*filenames).transform_values do |file_coverage|
|
|
189
|
+
file_coverage.transform_keys(&:to_s)
|
|
190
|
+
end
|
|
87
191
|
end
|
|
88
192
|
|
|
89
|
-
# Applies
|
|
90
|
-
|
|
91
|
-
|
|
193
|
+
# Applies the given filter chain to `@files`, dropping each source
|
|
194
|
+
# file that any filter matches.
|
|
195
|
+
def apply_filters!(filters)
|
|
196
|
+
filters.each do |filter|
|
|
197
|
+
@files = SimpleCov::FileList.new(@files.reject { |source_file| filter.matches?(source_file) })
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# When any `cover` matcher is configured, restrict `@files` to source
|
|
202
|
+
# files matching at least one of them. With no cover matchers configured
|
|
203
|
+
# this is a no-op, preserving the historical "everything required, then
|
|
204
|
+
# filtered" universe.
|
|
205
|
+
def apply_cover_filters!(cover_filters)
|
|
206
|
+
return if cover_filters.empty?
|
|
207
|
+
|
|
208
|
+
@files = SimpleCov::FileList.new(
|
|
209
|
+
@files.select { |source_file| cover_filters.any? { |filter| filter.matches?(source_file) } }
|
|
210
|
+
)
|
|
92
211
|
end
|
|
93
212
|
end
|
|
94
213
|
end
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "coverage"
|
|
4
|
+
|
|
3
5
|
module SimpleCov
|
|
4
6
|
#
|
|
5
7
|
# Responsible for adapting the format of the coverage result whether it's default or with statistics
|
|
@@ -11,20 +13,149 @@ module SimpleCov
|
|
|
11
13
|
@result = result
|
|
12
14
|
end
|
|
13
15
|
|
|
14
|
-
def self.call(*
|
|
15
|
-
new(*
|
|
16
|
+
def self.call(*)
|
|
17
|
+
new(*).adapt
|
|
16
18
|
end
|
|
17
19
|
|
|
18
20
|
def adapt
|
|
19
21
|
return unless result
|
|
20
22
|
|
|
21
|
-
result.
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
23
|
+
result.to_h do |file_name, cover_statistic|
|
|
24
|
+
[file_name, adapt_one(file_name, cover_statistic)]
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
# Pre-0.18 resultsets pointed each filename straight at a line-coverage
|
|
31
|
+
# array; everything since uses the `{lines:, branches:, methods:}`
|
|
32
|
+
# shape. Newer entries also need their methods and branches tables
|
|
33
|
+
# massaged before downstream code reports or merges them.
|
|
34
|
+
def adapt_one(file_name, cover_statistic)
|
|
35
|
+
return {"lines" => cover_statistic} if cover_statistic.is_a?(Array)
|
|
36
|
+
|
|
37
|
+
adapt_oneshot_lines_if_needed(file_name, cover_statistic)
|
|
38
|
+
normalize_method_keys(cover_statistic)
|
|
39
|
+
aggregate_duplicated_branches(cover_statistic)
|
|
40
|
+
cover_statistic
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Normalize memory addresses in method coverage keys so that results
|
|
44
|
+
# from different processes can be merged. Anonymous class names like
|
|
45
|
+
# "#<Class:0x00007ff19ab24790>" get inconsistent addresses across runs.
|
|
46
|
+
# Address widths vary by runtime (32-bit hosts: 8 hex chars; 64-bit
|
|
47
|
+
# CRuby: 16; some JVM/TruffleRuby formats may differ), so match any
|
|
48
|
+
# length of hex digits and collapse to a single placeholder.
|
|
49
|
+
ADDRESS_PATTERN = /0x\h+/
|
|
50
|
+
private_constant :ADDRESS_PATTERN
|
|
51
|
+
|
|
52
|
+
ADDRESS_PLACEHOLDER = "0x0"
|
|
53
|
+
private_constant :ADDRESS_PLACEHOLDER
|
|
54
|
+
|
|
55
|
+
# Strip the `#<Class:Foo>` wrapper Ruby's Coverage adds to singleton-class
|
|
56
|
+
# method keys. `module_function` and class methods get recorded both as
|
|
57
|
+
# singleton (`[#<Class:Foo>, :m, …]`) and instance/module (`[Foo, :m, …]`)
|
|
58
|
+
# entries pointing at the same source location; only one of the two is
|
|
59
|
+
# ever reachable at runtime, so we merge them. Only applies to named
|
|
60
|
+
# constants — anonymous-class addresses like `#<Class:0x0>` are left
|
|
61
|
+
# alone (handled by ADDRESS_PATTERN above).
|
|
62
|
+
SINGLETON_WRAPPER_PATTERN = /\A#<Class:([A-Z_][\w:]*)>\z/
|
|
63
|
+
private_constant :SINGLETON_WRAPPER_PATTERN
|
|
64
|
+
|
|
65
|
+
# Ruby's method coverage records one entry per DEFINED METHOD, not per
|
|
66
|
+
# source location: a block handed to `define_method` /
|
|
67
|
+
# `define_singleton_method` from a shared code path yields a separate
|
|
68
|
+
# `[receiver, name, location]` entry for every class it's defined on
|
|
69
|
+
# (a module's `included` hook defining onto each descendant) AND for
|
|
70
|
+
# every name it's defined under (a builder looping `define_method key`
|
|
71
|
+
# over a container), all pointing at the same source. A file-based
|
|
72
|
+
# report can only express "was the method at this location ever
|
|
73
|
+
# executed", so entries are aggregated by location alone, summing
|
|
74
|
+
# hits — otherwise each receiver or name whose generated copy never
|
|
75
|
+
# ran shows as a phantom uncovered method on a line whose line
|
|
76
|
+
# coverage is 100%. Regular `def`s map one location to one name, so
|
|
77
|
+
# they are unaffected. The first entry's (normalized) key is kept for
|
|
78
|
+
# display. See issue #1234.
|
|
79
|
+
def normalize_method_keys(cover_statistic)
|
|
80
|
+
methods = cover_statistic[:methods]
|
|
81
|
+
return unless methods
|
|
82
|
+
|
|
83
|
+
aggregated = {} #: Hash[untyped, [untyped, Integer]]
|
|
84
|
+
methods.each_with_object(aggregated) do |(key, count), memo|
|
|
85
|
+
location = key[2..] #: Array[untyped]
|
|
86
|
+
retained_key, existing = memo[location] || [normalize_method_key(key), 0]
|
|
87
|
+
memo[location] = [retained_key, existing + count]
|
|
27
88
|
end
|
|
89
|
+
cover_statistic[:methods] = aggregated.values.to_h
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def normalize_method_key(key)
|
|
93
|
+
normalized_key = key.dup
|
|
94
|
+
normalized_key[0] = class_display_name(key[0])
|
|
95
|
+
.gsub(ADDRESS_PATTERN, ADDRESS_PLACEHOLDER)
|
|
96
|
+
.sub(SINGLETON_WRAPPER_PATTERN, '\1')
|
|
97
|
+
normalized_key
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Rendering a class name can execute user code: a singleton class's
|
|
101
|
+
# `to_s` renders its attached object via `#inspect`, which a module can
|
|
102
|
+
# shadow with an incompatible signature (Liquid::Utils defines
|
|
103
|
+
# `inspect(value, max_depth = 2)` as a module_function, so rendering
|
|
104
|
+
# `#<Class:Liquid::Utils>` raises ArgumentError). A coverage report
|
|
105
|
+
# must never crash the host suite over that, so on failure rebuild the
|
|
106
|
+
# singleton wrapper from `Module#name` via bound methods (which cannot
|
|
107
|
+
# be shadowed), falling back to the address form, which
|
|
108
|
+
# ADDRESS_PATTERN then normalizes. See issue #1236.
|
|
109
|
+
def class_display_name(klass)
|
|
110
|
+
klass.to_s
|
|
111
|
+
rescue StandardError
|
|
112
|
+
singleton_wrapper_name(klass) || Object.instance_method(:to_s).bind_call(klass)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def singleton_wrapper_name(klass)
|
|
116
|
+
return nil unless klass.is_a?(Class) && klass.singleton_class?
|
|
117
|
+
|
|
118
|
+
attached = klass.attached_object
|
|
119
|
+
# simplecov:disable branch — CRuby only reaches the rescue via a
|
|
120
|
+
# Module/Class attached object (instance singletons render from the
|
|
121
|
+
# class name chain without calling user inspect), so the non-Module
|
|
122
|
+
# arm is defensive.
|
|
123
|
+
name = Module.instance_method(:name).bind_call(attached) if attached.is_a?(Module)
|
|
124
|
+
# simplecov:enable branch
|
|
125
|
+
name && "#<Class:#{name}>"
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Ruby's eval coverage records a fresh set of branch entries for every
|
|
129
|
+
# COMPILE of an eval'd string: a template rendered through multiple view
|
|
130
|
+
# classes (e.g. hanami-view compiles each template once per view) yields
|
|
131
|
+
# several `[:if, id, location]` conditions at identical coordinates in
|
|
132
|
+
# the same file, each counting only the renders that flowed through that
|
|
133
|
+
# compile. Reported as-is they inflate the branch denominator and turn a
|
|
134
|
+
# side covered under a different compile into a phantom miss (issue
|
|
135
|
+
# #1235). Aggregate them by (type, location) — combining a branches hash
|
|
136
|
+
# with an empty one dedups within it, since BranchesCombiner keys arms
|
|
137
|
+
# on location identity. Regular (non-eval) source can never produce two
|
|
138
|
+
# conditions at the same location, so this is a no-op outside eval.
|
|
139
|
+
def aggregate_duplicated_branches(cover_statistic)
|
|
140
|
+
branches = cover_statistic[:branches]
|
|
141
|
+
return unless branches
|
|
142
|
+
|
|
143
|
+
cover_statistic[:branches] = Combine::BranchesCombiner.combine(branches, {})
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def adapt_oneshot_lines_if_needed(file_name, cover_statistic)
|
|
147
|
+
return unless cover_statistic.key?(:oneshot_lines)
|
|
148
|
+
|
|
149
|
+
oneshot_lines = cover_statistic.delete(:oneshot_lines)
|
|
150
|
+
line_stub = build_line_stub(file_name, oneshot_lines)
|
|
151
|
+
oneshot_lines.each { |covered_line| line_stub[covered_line - 1] = 1 }
|
|
152
|
+
cover_statistic[:lines] = line_stub
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def build_line_stub(file_name, oneshot_lines)
|
|
156
|
+
Coverage.line_stub(file_name)
|
|
157
|
+
rescue Errno::ENOENT, SyntaxError
|
|
158
|
+
Array.new(oneshot_lines.max || 0, nil)
|
|
28
159
|
end
|
|
29
160
|
end
|
|
30
161
|
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SimpleCov
|
|
4
|
+
module ResultMerger
|
|
5
|
+
# We changed the format of the raw result data in simplecov, as people
|
|
6
|
+
# are likely to have "old" resultsets lying around (but not too old so
|
|
7
|
+
# that they're still considered we can adapt them). See
|
|
8
|
+
# https://github.com/simplecov-ruby/simplecov/pull/824#issuecomment-576049747
|
|
9
|
+
module LegacyFormatAdapter
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
def call(result)
|
|
13
|
+
pre_0_18?(result) ? upgrade(result) : result
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Pre-0.18 coverage data pointed from file directly to an array of
|
|
17
|
+
# line coverage rather than a `{"lines" => [...]}` hash.
|
|
18
|
+
def pre_0_18?(result)
|
|
19
|
+
_key, data = result.first
|
|
20
|
+
data.is_a?(Array)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def upgrade(result)
|
|
24
|
+
result.transform_values { |line_coverage_data| {"lines" => line_coverage_data} }
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module SimpleCov
|
|
6
|
+
module ResultMerger
|
|
7
|
+
# Read + parse a `.resultset.json` file with the same tolerance the
|
|
8
|
+
# historical `ResultMerger` had: a missing or empty file quietly
|
|
9
|
+
# returns `{}`, an unparseable one warns and returns `{}`, and parse
|
|
10
|
+
# success returns the decoded Hash with any malformed entries
|
|
11
|
+
# warned about and dropped.
|
|
12
|
+
module ResultsetFile
|
|
13
|
+
module_function
|
|
14
|
+
|
|
15
|
+
def parse(path)
|
|
16
|
+
data = read(path)
|
|
17
|
+
decode(data)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# A missing or blank file quietly means "no results yet"; anything
|
|
21
|
+
# else — a 1-byte truncation included — flows through decode so
|
|
22
|
+
# corruption warns instead of silently vanishing (the old
|
|
23
|
+
# `length < 2` check swallowed exactly those). Read first and
|
|
24
|
+
# rescue rather than check exist? (racy against a concurrent
|
|
25
|
+
# clean).
|
|
26
|
+
def read(path)
|
|
27
|
+
data = File.read(path)
|
|
28
|
+
return if data.match?(/\A\s*\z/)
|
|
29
|
+
|
|
30
|
+
data
|
|
31
|
+
rescue Errno::ENOENT
|
|
32
|
+
nil
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def decode(content)
|
|
36
|
+
return {} unless content
|
|
37
|
+
|
|
38
|
+
parsed = JSON.parse(content)
|
|
39
|
+
return {} if parsed.nil? # a "null" file has long meant an empty resultset
|
|
40
|
+
|
|
41
|
+
# Valid JSON need not be an object ("[1,2]" and "\"x\"" parse
|
|
42
|
+
# fine), but everything downstream iterates command => data
|
|
43
|
+
# pairs, so anything else would crash out of the middle of a
|
|
44
|
+
# merge. Same tolerance as a parse failure: warn and move on.
|
|
45
|
+
parsed.is_a?(Hash) ? drop_malformed_entries(parsed) : invalid_resultset
|
|
46
|
+
rescue StandardError
|
|
47
|
+
invalid_resultset
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Each surviving entry must have the shape every consumer relies
|
|
51
|
+
# on: a Hash carrying a Numeric "timestamp" (the merge-timeout
|
|
52
|
+
# check) and a Hash "coverage" (the merge fold). A truncated or
|
|
53
|
+
# hand-edited entry would otherwise crash out of the middle of an
|
|
54
|
+
# at_exit merge, so it gets the same warn-and-move-on treatment
|
|
55
|
+
# as an unparseable file.
|
|
56
|
+
def drop_malformed_entries(resultset)
|
|
57
|
+
malformed, valid = resultset.partition { |_command_name, data| !well_formed_entry?(data) }
|
|
58
|
+
return resultset if malformed.empty?
|
|
59
|
+
|
|
60
|
+
warn "[SimpleCov]: Warning! Ignoring malformed resultset entries: #{malformed.map(&:first).sort.join(', ')}"
|
|
61
|
+
valid.to_h
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def well_formed_entry?(data)
|
|
65
|
+
data.is_a?(Hash) && data["timestamp"].is_a?(Numeric) && data["coverage"].is_a?(Hash)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def invalid_resultset
|
|
69
|
+
warn "[SimpleCov]: Warning! Parsing JSON content of resultset file failed"
|
|
70
|
+
{}
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SimpleCov
|
|
4
|
+
module ResultMerger
|
|
5
|
+
# Run/worker metadata queries used by resultset storage and parallel
|
|
6
|
+
# coordination. Kept separate from the coverage-folding implementation.
|
|
7
|
+
module ResultsetRunIdentity
|
|
8
|
+
# One namespaced identity per distinct worker that wrote to the
|
|
9
|
+
# current run: `[:worker, id]` for entries that carry a worker id,
|
|
10
|
+
# `[:legacy, command_name]` for fresh entries written without one
|
|
11
|
+
# (an old SimpleCov writing to the same resultset). Pairs rather
|
|
12
|
+
# than mangled strings, so a resultset mixing both kinds can never
|
|
13
|
+
# alias a real worker id to a synthesized legacy identity.
|
|
14
|
+
def worker_identities_for_run(results, run_id, started_at)
|
|
15
|
+
results.filter_map do |command_name, data|
|
|
16
|
+
next unless current_run_entry?(data, run_id, started_at)
|
|
17
|
+
|
|
18
|
+
worker_id = data["worker_id"]
|
|
19
|
+
worker_id.to_s.empty? ? [:legacy, command_name] : [:worker, worker_id.to_s]
|
|
20
|
+
end.uniq
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def current_run_entry?(entry, run_id, started_at)
|
|
24
|
+
return false unless entry.is_a?(Hash)
|
|
25
|
+
|
|
26
|
+
entry_run_id = entry["run_id"]
|
|
27
|
+
return fresh_entry?(entry, started_at) unless entry_run_id
|
|
28
|
+
return false unless run_id && entry_run_id == run_id
|
|
29
|
+
return true if SimpleCov::RunIdentity.authoritative?
|
|
30
|
+
|
|
31
|
+
fresh_entry?(entry, started_at)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def fresh_entry?(entry, started_at)
|
|
35
|
+
timestamp = entry["timestamp"]
|
|
36
|
+
timestamp.is_a?(Numeric) && started_at && timestamp >= started_at.to_f
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Whether `entry` was written by a runner concurrent with this
|
|
40
|
+
# process: either it shares our run id (`current_run_entry?`), or it
|
|
41
|
+
# was written strictly after our process started. A mismatched run
|
|
42
|
+
# id must not defeat the timestamp check — a subprocess we shelled
|
|
43
|
+
# out to (fork+exec) generates its own random run id, and its
|
|
44
|
+
# freshly written entry is exactly the data issue #581 protects
|
|
45
|
+
# from being clobbered. Strict id matching is reserved for worker
|
|
46
|
+
# counting (`worker_identities_for_run`), where admitting a stale
|
|
47
|
+
# entry would end a sibling wait early.
|
|
48
|
+
def concurrent_runner_entry?(entry, incoming = nil)
|
|
49
|
+
return false unless entry.is_a?(Hash)
|
|
50
|
+
|
|
51
|
+
incoming_run_id = incoming["run_id"] if incoming.is_a?(Hash)
|
|
52
|
+
started_at = SimpleCov.process_start_time
|
|
53
|
+
return true if current_run_entry?(entry, incoming_run_id, started_at)
|
|
54
|
+
|
|
55
|
+
written_after_start?(entry, started_at)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Strictly after, unlike `fresh_entry?`: an entry stamped at the
|
|
59
|
+
# exact instant we started is treated as leftover from a previous
|
|
60
|
+
# run, so a same-second stale entry is still overwritten.
|
|
61
|
+
def written_after_start?(entry, started_at)
|
|
62
|
+
timestamp = entry["timestamp"]
|
|
63
|
+
timestamp.is_a?(Numeric) && started_at && timestamp > started_at.to_f
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "json"
|
|
5
|
+
require "monitor"
|
|
6
|
+
require_relative "../atomic_file"
|
|
7
|
+
|
|
8
|
+
module SimpleCov
|
|
9
|
+
module ResultMerger
|
|
10
|
+
# Reads and writes the persistent `.resultset.json` cache, including
|
|
11
|
+
# file-lock synchronization between processes and atomic temp-file
|
|
12
|
+
# renames so concurrent readers don't observe a truncated file.
|
|
13
|
+
module ResultsetStore
|
|
14
|
+
LOCK_MONITOR = Monitor.new
|
|
15
|
+
private_constant :LOCK_MONITOR
|
|
16
|
+
|
|
17
|
+
module_function
|
|
18
|
+
|
|
19
|
+
def resultset_path
|
|
20
|
+
File.join(SimpleCov.coverage_path, ".resultset.json")
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def writelock_path
|
|
24
|
+
File.join(SimpleCov.coverage_path, ".resultset.json.lock")
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Compact JSON, not pretty-printed: this is a machine-read cache
|
|
28
|
+
# that every parallel worker rewrites wholesale, and on a large
|
|
29
|
+
# project pretty printing nearly doubles the bytes written, read
|
|
30
|
+
# back, and parsed on each of those store-merge round trips.
|
|
31
|
+
def write(resultset)
|
|
32
|
+
AtomicFile.write(resultset_path, "#{JSON.generate(resultset)}\n")
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Serialize threads before taking the process-wide file lock. Nested
|
|
36
|
+
# calls by the owning thread bypass flock so they cannot deadlock on a
|
|
37
|
+
# second descriptor for the same lock file.
|
|
38
|
+
def synchronize(&)
|
|
39
|
+
return yield if LOCK_MONITOR.mon_owned?
|
|
40
|
+
|
|
41
|
+
LOCK_MONITOR.synchronize { with_flock(&) }
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def with_flock
|
|
45
|
+
FileUtils.mkdir_p(SimpleCov.coverage_path)
|
|
46
|
+
File.open(writelock_path, "w+") do |f|
|
|
47
|
+
f.flock(File::LOCK_EX)
|
|
48
|
+
yield
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|