simplecov 0.22.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/LICENSE +1 -1
- data/README.md +1058 -508
- 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 +93 -0
- data/lib/simplecov/cli/diff.rb +152 -0
- data/lib/simplecov/cli/dotfile.rb +105 -0
- data/lib/simplecov/cli/merge.rb +118 -0
- data/lib/simplecov/cli/open.rb +50 -0
- data/lib/simplecov/cli/report.rb +89 -0
- data/lib/simplecov/cli/run.rb +36 -0
- data/lib/simplecov/cli/serve.rb +149 -0
- data/lib/simplecov/cli/uncovered.rb +108 -0
- data/lib/simplecov/cli.rb +150 -0
- data/lib/simplecov/color.rb +79 -0
- data/lib/simplecov/combine/branches_combiner.rb +28 -5
- data/lib/simplecov/combine/files_combiner.rb +47 -1
- data/lib/simplecov/combine/lines_combiner.rb +19 -17
- data/lib/simplecov/combine/methods_combiner.rb +49 -0
- data/lib/simplecov/combine/results_combiner.rb +7 -5
- data/lib/simplecov/command_guesser.rb +46 -32
- data/lib/simplecov/configuration/coverage.rb +175 -0
- data/lib/simplecov/configuration/coverage_criteria.rb +158 -0
- data/lib/simplecov/configuration/filters.rb +197 -0
- data/lib/simplecov/configuration/formatting.rb +129 -0
- data/lib/simplecov/configuration/ignored_entries.rb +63 -0
- data/lib/simplecov/configuration/merging.rb +164 -0
- data/lib/simplecov/configuration/thresholds.rb +178 -0
- data/lib/simplecov/configuration.rb +105 -412
- data/lib/simplecov/coverage_statistics.rb +15 -9
- data/lib/simplecov/coverage_violations.rb +149 -0
- data/lib/simplecov/defaults.rb +29 -22
- data/lib/simplecov/deprecation.rb +45 -0
- data/lib/simplecov/directive.rb +167 -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 +15 -0
- data/lib/simplecov/exit_handling.rb +158 -0
- data/lib/simplecov/file_list.rb +70 -19
- data/lib/simplecov/filter.rb +69 -24
- data/lib/simplecov/formatter/base.rb +102 -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 +85 -0
- data/lib/simplecov/formatter/json_formatter/errors_formatter.rb +90 -0
- data/lib/simplecov/formatter/json_formatter/result_hash_formatter.rb +128 -0
- data/lib/simplecov/formatter/json_formatter/source_file_formatter.rb +99 -0
- data/lib/simplecov/formatter/json_formatter.rb +89 -0
- data/lib/simplecov/formatter/multi_formatter.rb +11 -7
- data/lib/simplecov/formatter/simple_formatter.rb +11 -11
- data/lib/simplecov/formatter.rb +4 -0
- data/lib/simplecov/last_run.rb +10 -3
- data/lib/simplecov/lines_classifier.rb +25 -13
- data/lib/simplecov/load_global_config.rb +9 -4
- data/lib/simplecov/parallel_adapters/base.rb +61 -0
- data/lib/simplecov/parallel_adapters/generic.rb +44 -0
- data/lib/simplecov/parallel_adapters/parallel_tests.rb +93 -0
- data/lib/simplecov/parallel_adapters.rb +83 -0
- data/lib/simplecov/parallel_coordination.rb +129 -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/result/missing_source_files_reporter.rb +49 -0
- data/lib/simplecov/result/source_file_builder.rb +51 -0
- data/lib/simplecov/result.rb +108 -19
- data/lib/simplecov/result_adapter.rb +105 -8
- 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 +59 -91
- data/lib/simplecov/result_processing.rb +179 -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 +9 -4
- 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 +60 -0
- data/lib/simplecov/source_file/ruby_data_parser.rb +88 -0
- data/lib/simplecov/source_file/skip_chunks.rb +75 -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/condition_folding.rb +49 -0
- data/lib/simplecov/static_coverage_extractor/location_conventions.rb +247 -0
- data/lib/simplecov/static_coverage_extractor/method_collector.rb +55 -0
- data/lib/simplecov/static_coverage_extractor/value_position.rb +82 -0
- data/lib/simplecov/static_coverage_extractor/visitor.rb +196 -0
- data/lib/simplecov/static_coverage_extractor.rb +110 -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 +178 -380
- data/lib/simplecov_json_formatter.rb +4 -0
- data/schemas/coverage-v1.0.schema.json +306 -0
- data/schemas/coverage.schema.json +306 -0
- data/sig/simplecov.rbs +1638 -0
- metadata +94 -57
- data/CHANGELOG.md +0 -191
- 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,11 @@ 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
|
-
# Returns all files that are applicable to this result (sans filters!) as instances of
|
|
18
|
+
# Returns all files that are applicable to this result (sans filters!) as instances of
|
|
19
|
+
# SimpleCov::SourceFile. Aliased as :source_files
|
|
16
20
|
attr_reader :files
|
|
17
21
|
alias source_files files
|
|
18
22
|
# Explicitly set the Time this result has been created
|
|
@@ -20,20 +24,47 @@ module SimpleCov
|
|
|
20
24
|
# Explicitly set the command name that was used for this coverage result. Defaults to SimpleCov.command_name
|
|
21
25
|
attr_writer :command_name
|
|
22
26
|
|
|
23
|
-
def_delegators :files, :covered_percent, :covered_percentages, :least_covered_file, :covered_strength,
|
|
27
|
+
def_delegators :files, :covered_percent, :covered_percentages, :least_covered_file, :covered_strength,
|
|
28
|
+
:covered_lines, :missed_lines,
|
|
29
|
+
:total_branches, :covered_branches, :missed_branches,
|
|
30
|
+
:total_methods, :covered_methods, :missed_methods,
|
|
31
|
+
:coverage_statistics, :coverage_statistics_by_file
|
|
24
32
|
def_delegator :files, :lines_of_code, :total_lines
|
|
25
33
|
|
|
34
|
+
# Bundles the filter and grouping configuration a Result applies to its
|
|
35
|
+
# source files after building them. Each field defaults to the SimpleCov
|
|
36
|
+
# singleton's configuration, so ordinary callers never construct one;
|
|
37
|
+
# tests pass a custom instance to opt out of (or extend) the project's
|
|
38
|
+
# filters or groups (e.g. `filters: []` to keep every file). Grouping the
|
|
39
|
+
# three together keeps Result#initialize's parameter list small.
|
|
40
|
+
class FilterConfig
|
|
41
|
+
attr_reader :filters, :cover_filters, :groups
|
|
42
|
+
|
|
43
|
+
def initialize(filters: SimpleCov.filters, cover_filters: SimpleCov.cover_filters, groups: SimpleCov.groups)
|
|
44
|
+
@filters = filters
|
|
45
|
+
@cover_filters = cover_filters
|
|
46
|
+
@groups = groups
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
26
50
|
# Initialize a new SimpleCov::Result from given Coverage.result (a Hash of filenames each containing an array of
|
|
27
|
-
# coverage data)
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
51
|
+
# coverage data).
|
|
52
|
+
#
|
|
53
|
+
# `filter_config` defaults to the SimpleCov singleton's filter / group
|
|
54
|
+
# configuration so existing call sites are unchanged. Pass a custom
|
|
55
|
+
# FilterConfig to opt out — useful for tests that build synthetic Results
|
|
56
|
+
# and don't want the project's filters or groups applied.
|
|
57
|
+
def initialize(original_result, command_name: nil, created_at: nil, not_loaded_files: Set.new,
|
|
58
|
+
report: false, filter_config: FilterConfig.new)
|
|
59
|
+
@original_result = original_result.freeze
|
|
31
60
|
@command_name = command_name
|
|
32
61
|
@created_at = created_at
|
|
33
|
-
@
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
62
|
+
@groups_config = filter_config.groups
|
|
63
|
+
builder = SourceFileBuilder.new(original_result, not_loaded_files: not_loaded_files)
|
|
64
|
+
@files = builder.call
|
|
65
|
+
warn_about_missing_source_files(builder.missing_source_files, original_result.size) if report
|
|
66
|
+
apply_cover_filters!(filter_config.cover_filters)
|
|
67
|
+
apply_filters!(filter_config.filters)
|
|
37
68
|
end
|
|
38
69
|
|
|
39
70
|
# Returns all filenames for source files contained in this result
|
|
@@ -41,14 +72,37 @@ module SimpleCov
|
|
|
41
72
|
files.map(&:filename)
|
|
42
73
|
end
|
|
43
74
|
|
|
44
|
-
# Returns
|
|
75
|
+
# Returns the SimpleCov::SourceFile for the given path, or nil if no
|
|
76
|
+
# matching file is in this result. The path is resolved against
|
|
77
|
+
# SimpleCov.root, so callers can pass either an absolute path or a
|
|
78
|
+
# project-relative one.
|
|
79
|
+
def source_file_for(path)
|
|
80
|
+
target = File.expand_path(path, SimpleCov.root)
|
|
81
|
+
files.find { |file| file.filename == target }
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Returns the {line:/branch:/method:} coverage_statistics hash for the
|
|
85
|
+
# given file path, or nil if no matching source file is in this
|
|
86
|
+
# result. See SimpleCov::Result#source_file_for for path resolution.
|
|
87
|
+
def coverage_for(path)
|
|
88
|
+
source_file_for(path)&.coverage_statistics
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Returns a Hash of groups for this result. Define groups using SimpleCov.group 'Models', 'app/models'
|
|
45
92
|
def groups
|
|
46
|
-
@groups ||= SimpleCov.grouped(files)
|
|
93
|
+
@groups ||= SimpleCov.grouped(files, groups: @groups_config)
|
|
47
94
|
end
|
|
48
95
|
|
|
49
|
-
# Applies the configured SimpleCov.formatter on this result
|
|
96
|
+
# Applies the configured SimpleCov.formatter on this result. Returns
|
|
97
|
+
# nil if formatting has been opted out of (`SimpleCov.formatter false`
|
|
98
|
+
# / `SimpleCov.formatters []`) — the cheap path for non-final
|
|
99
|
+
# processes in a parallel CI run, which only need their
|
|
100
|
+
# `.resultset.json` on disk. See #964.
|
|
50
101
|
def format!
|
|
51
|
-
SimpleCov.formatter
|
|
102
|
+
formatter = SimpleCov.formatter
|
|
103
|
+
return nil if formatter.nil?
|
|
104
|
+
|
|
105
|
+
formatter.new.format(self)
|
|
52
106
|
end
|
|
53
107
|
|
|
54
108
|
# Defines when this result has been created. Defaults to Time.now
|
|
@@ -81,14 +135,49 @@ module SimpleCov
|
|
|
81
135
|
|
|
82
136
|
private
|
|
83
137
|
|
|
138
|
+
def warn_about_missing_source_files(missing, input_size)
|
|
139
|
+
return if missing.empty?
|
|
140
|
+
|
|
141
|
+
# Emit only from the process that writes the final report. The merged
|
|
142
|
+
# result is rebuilt in every parallel worker (each one stores its own
|
|
143
|
+
# slice), so without this gate the warning prints once per worker — this
|
|
144
|
+
# is the same signal SimpleCov uses to pick the process that runs the
|
|
145
|
+
# report and threshold checks. It's intentionally not gated on
|
|
146
|
+
# print_errors: the default at_fork sets print_errors false on workers,
|
|
147
|
+
# and in many parallel runners the final-report process is itself a
|
|
148
|
+
# worker, so a print_errors gate would suppress the one warning we want.
|
|
149
|
+
# See issues #980 and #1171.
|
|
150
|
+
return unless SimpleCov.final_result_process?
|
|
151
|
+
|
|
152
|
+
MissingSourceFilesReporter.new(
|
|
153
|
+
missing,
|
|
154
|
+
input_size: input_size,
|
|
155
|
+
every_entry_dropped: @files.empty? && missing.size == input_size
|
|
156
|
+
).warn!
|
|
157
|
+
end
|
|
158
|
+
|
|
84
159
|
def coverage
|
|
85
|
-
|
|
86
|
-
Hash[keys.zip(original_result.values_at(*keys))]
|
|
160
|
+
original_result.slice(*filenames)
|
|
87
161
|
end
|
|
88
162
|
|
|
89
|
-
# Applies
|
|
90
|
-
|
|
91
|
-
|
|
163
|
+
# Applies the given filter chain to `@files`, dropping each source
|
|
164
|
+
# file that any filter matches.
|
|
165
|
+
def apply_filters!(filters)
|
|
166
|
+
filters.each do |filter|
|
|
167
|
+
@files = SimpleCov::FileList.new(@files.reject { |source_file| filter.matches?(source_file) })
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# When any `cover` matcher is configured, restrict `@files` to source
|
|
172
|
+
# files matching at least one of them. With no cover matchers configured
|
|
173
|
+
# this is a no-op, preserving the historical "everything required, then
|
|
174
|
+
# filtered" universe.
|
|
175
|
+
def apply_cover_filters!(cover_filters)
|
|
176
|
+
return if cover_filters.empty?
|
|
177
|
+
|
|
178
|
+
@files = SimpleCov::FileList.new(
|
|
179
|
+
@files.select { |source_file| cover_filters.any? { |filter| filter.matches?(source_file) } }
|
|
180
|
+
)
|
|
92
181
|
end
|
|
93
182
|
end
|
|
94
183
|
end
|
|
@@ -11,20 +11,117 @@ module SimpleCov
|
|
|
11
11
|
@result = result
|
|
12
12
|
end
|
|
13
13
|
|
|
14
|
-
def self.call(*
|
|
15
|
-
new(*
|
|
14
|
+
def self.call(*)
|
|
15
|
+
new(*).adapt
|
|
16
16
|
end
|
|
17
17
|
|
|
18
18
|
def adapt
|
|
19
19
|
return unless result
|
|
20
20
|
|
|
21
|
-
result.
|
|
22
|
-
|
|
23
|
-
adapted_result.merge!(file_name => {"lines" => cover_statistic})
|
|
24
|
-
else
|
|
25
|
-
adapted_result.merge!(file_name => cover_statistic)
|
|
26
|
-
end
|
|
21
|
+
result.to_h do |file_name, cover_statistic|
|
|
22
|
+
[file_name, adapt_one(file_name, cover_statistic)]
|
|
27
23
|
end
|
|
28
24
|
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
# Pre-0.18 resultsets pointed each filename straight at a line-coverage
|
|
29
|
+
# array; everything since uses the `{lines:, branches:, methods:}`
|
|
30
|
+
# shape. Newer entries also need their methods and branches tables
|
|
31
|
+
# massaged before downstream code reports or merges them.
|
|
32
|
+
def adapt_one(file_name, cover_statistic)
|
|
33
|
+
return {"lines" => cover_statistic} if cover_statistic.is_a?(Array)
|
|
34
|
+
|
|
35
|
+
adapt_oneshot_lines_if_needed(file_name, cover_statistic)
|
|
36
|
+
normalize_method_keys(cover_statistic)
|
|
37
|
+
aggregate_duplicated_branches(cover_statistic)
|
|
38
|
+
cover_statistic
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Normalize memory addresses in method coverage keys so that results
|
|
42
|
+
# from different processes can be merged. Anonymous class names like
|
|
43
|
+
# "#<Class:0x00007ff19ab24790>" get inconsistent addresses across runs.
|
|
44
|
+
# Address widths vary by runtime (32-bit hosts: 8 hex chars; 64-bit
|
|
45
|
+
# CRuby: 16; some JVM/TruffleRuby formats may differ), so match any
|
|
46
|
+
# length of hex digits and collapse to a single placeholder.
|
|
47
|
+
ADDRESS_PATTERN = /0x\h+/
|
|
48
|
+
private_constant :ADDRESS_PATTERN
|
|
49
|
+
|
|
50
|
+
ADDRESS_PLACEHOLDER = "0x0"
|
|
51
|
+
private_constant :ADDRESS_PLACEHOLDER
|
|
52
|
+
|
|
53
|
+
# Strip the `#<Class:Foo>` wrapper Ruby's Coverage adds to singleton-class
|
|
54
|
+
# method keys. `module_function` and class methods get recorded both as
|
|
55
|
+
# singleton (`[#<Class:Foo>, :m, …]`) and instance/module (`[Foo, :m, …]`)
|
|
56
|
+
# entries pointing at the same source location; only one of the two is
|
|
57
|
+
# ever reachable at runtime, so we merge them. Only applies to named
|
|
58
|
+
# constants — anonymous-class addresses like `#<Class:0x0>` are left
|
|
59
|
+
# alone (handled by ADDRESS_PATTERN above).
|
|
60
|
+
SINGLETON_WRAPPER_PATTERN = /\A#<Class:([A-Z_][\w:]*)>\z/
|
|
61
|
+
private_constant :SINGLETON_WRAPPER_PATTERN
|
|
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.
|
|
73
|
+
def normalize_method_keys(cover_statistic)
|
|
74
|
+
methods = cover_statistic[:methods]
|
|
75
|
+
return unless methods
|
|
76
|
+
|
|
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]
|
|
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, {})
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def adapt_oneshot_lines_if_needed(file_name, cover_statistic)
|
|
113
|
+
return unless cover_statistic.key?(:oneshot_lines)
|
|
114
|
+
|
|
115
|
+
oneshot_lines = cover_statistic.delete(:oneshot_lines)
|
|
116
|
+
line_stub = build_line_stub(file_name, oneshot_lines)
|
|
117
|
+
oneshot_lines.each { |covered_line| line_stub[covered_line - 1] = 1 }
|
|
118
|
+
cover_statistic[:lines] = line_stub
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def build_line_stub(file_name, oneshot_lines)
|
|
122
|
+
Coverage.line_stub(file_name)
|
|
123
|
+
rescue Errno::ENOENT, SyntaxError
|
|
124
|
+
Array.new(oneshot_lines.max || 0, nil)
|
|
125
|
+
end
|
|
29
126
|
end
|
|
30
127
|
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,38 @@
|
|
|
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: missing file returns `{}`, an empty
|
|
9
|
+
# or unparseable file warns and returns `{}`, parse success returns
|
|
10
|
+
# the decoded Hash.
|
|
11
|
+
module ResultsetFile
|
|
12
|
+
module_function
|
|
13
|
+
|
|
14
|
+
def parse(path)
|
|
15
|
+
data = read(path)
|
|
16
|
+
decode(data)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def read(path)
|
|
20
|
+
return unless File.exist?(path)
|
|
21
|
+
|
|
22
|
+
data = File.read(path)
|
|
23
|
+
return if data.nil? || data.length < 2
|
|
24
|
+
|
|
25
|
+
data
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def decode(content)
|
|
29
|
+
return {} unless content
|
|
30
|
+
|
|
31
|
+
JSON.parse(content) || {}
|
|
32
|
+
rescue StandardError
|
|
33
|
+
warn "[SimpleCov]: Warning! Parsing JSON content of resultset file failed"
|
|
34
|
+
{}
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "json"
|
|
5
|
+
|
|
6
|
+
module SimpleCov
|
|
7
|
+
module ResultMerger
|
|
8
|
+
# Reads and writes the persistent `.resultset.json` cache, including
|
|
9
|
+
# file-lock synchronization between processes and atomic temp-file
|
|
10
|
+
# renames so concurrent readers don't observe a truncated file.
|
|
11
|
+
module ResultsetStore
|
|
12
|
+
module_function
|
|
13
|
+
|
|
14
|
+
def resultset_path
|
|
15
|
+
File.join(SimpleCov.coverage_path, ".resultset.json")
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def writelock_path
|
|
19
|
+
File.join(SimpleCov.coverage_path, ".resultset.json.lock")
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def write(resultset)
|
|
23
|
+
FileUtils.mkdir_p(SimpleCov.coverage_path)
|
|
24
|
+
temp_path = "#{resultset_path}.#{Process.pid}.tmp"
|
|
25
|
+
File.open(temp_path, "w") { |f| f.puts JSON.pretty_generate(resultset) }
|
|
26
|
+
File.rename(temp_path, resultset_path)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Ensure only one process is reading or writing the resultset at
|
|
30
|
+
# any given time. Reentrant: the lock is acquired once per outer
|
|
31
|
+
# call no matter how deeply nested.
|
|
32
|
+
def synchronize(&)
|
|
33
|
+
return yield if @locked
|
|
34
|
+
|
|
35
|
+
@locked = true
|
|
36
|
+
with_flock(&)
|
|
37
|
+
ensure
|
|
38
|
+
@locked = false
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def with_flock
|
|
42
|
+
FileUtils.mkdir_p(SimpleCov.coverage_path)
|
|
43
|
+
File.open(writelock_path, "w+") do |f|
|
|
44
|
+
f.flock(File::LOCK_EX)
|
|
45
|
+
yield
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -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,57 +41,56 @@ 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
|
+
# Subprocesses merge the resultset too (each forked worker calls
|
|
72
|
+
# `SimpleCov.result` to store its slice), and the default `at_fork`
|
|
73
|
+
# sets `print_errors false` for them. Without this guard the warning
|
|
74
|
+
# is emitted once per worker — N copies of the same message for an
|
|
75
|
+
# N-worker run. Gate on `print_errors` like every other SimpleCov
|
|
76
|
+
# warning so only the reporting process speaks up.
|
|
77
|
+
return unless SimpleCov.print_errors
|
|
78
|
+
|
|
79
|
+
warn "[SimpleCov]: Excluded #{expired_command_names.size} result(s) older than " \
|
|
80
|
+
"merge_timeout (#{SimpleCov.merge_timeout}s) from the merged report: " \
|
|
81
|
+
"#{expired_command_names.sort.join(', ')}. " \
|
|
82
|
+
"Increase SimpleCov.merge_timeout to include them."
|
|
91
83
|
end
|
|
92
84
|
|
|
93
85
|
def create_result(command_names, coverage)
|
|
94
86
|
return nil unless coverage
|
|
95
87
|
|
|
96
88
|
command_name = command_names.reject(&:empty?).sort.join(", ")
|
|
97
|
-
|
|
89
|
+
# The merged result is the authoritative one users actually see, so
|
|
90
|
+
# it's the one that warns about source files dropped because they no
|
|
91
|
+
# longer exist on disk (issue #980). The per-process slices built in
|
|
92
|
+
# `process_coverage_result` stay quiet to avoid one warning per worker.
|
|
93
|
+
SimpleCov::Result.new(coverage, command_name: command_name, report: true)
|
|
98
94
|
end
|
|
99
95
|
|
|
100
96
|
def merge_coverage(*results)
|
|
@@ -104,9 +100,7 @@ module SimpleCov
|
|
|
104
100
|
results.reduce do |(memo_command, memo_coverage), (command, coverage)|
|
|
105
101
|
# timestamp is dropped here, which is intentional (we merge it, it gets a new time stamp as of now)
|
|
106
102
|
merged_coverage = Combine.combine(Combine::ResultsCombiner, memo_coverage, coverage)
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
[merged_command, merged_coverage]
|
|
103
|
+
[memo_command + command, merged_coverage]
|
|
110
104
|
end
|
|
111
105
|
end
|
|
112
106
|
|
|
@@ -115,79 +109,53 @@ module SimpleCov
|
|
|
115
109
|
# SimpleCov::Result with merged coverage data and the command_name
|
|
116
110
|
# for the result consisting of a join on all source result's names
|
|
117
111
|
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
|
-
|
|
112
|
+
command_names, coverage = merge_valid_results(read_resultset)
|
|
123
113
|
create_result(command_names, coverage)
|
|
124
114
|
end
|
|
125
115
|
|
|
126
116
|
def read_resultset
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
read_file(resultset_path)
|
|
130
|
-
end
|
|
131
|
-
|
|
132
|
-
parse_json(resultset_content)
|
|
117
|
+
content = synchronize_resultset { ResultsetFile.read(resultset_path) }
|
|
118
|
+
ResultsetFile.decode(content)
|
|
133
119
|
end
|
|
134
120
|
|
|
135
121
|
# Saves the given SimpleCov::Result in the resultset cache
|
|
136
|
-
def store_result(result)
|
|
122
|
+
def store_result(result) # rubocop:disable Naming/PredicateMethod
|
|
137
123
|
synchronize_resultset do
|
|
138
124
|
# Ensure we have the latest, in case it was already cached
|
|
139
125
|
new_resultset = read_resultset
|
|
140
126
|
|
|
141
127
|
# A single result only ever has one command_name, see `SimpleCov::Result#to_hash`
|
|
142
128
|
command_name, data = result.to_hash.first
|
|
143
|
-
new_resultset[command_name] = data
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
end
|
|
129
|
+
new_resultset[command_name] = merged_entry(new_resultset[command_name], data)
|
|
130
|
+
|
|
131
|
+
ResultsetStore.write(new_resultset)
|
|
147
132
|
end
|
|
148
133
|
true
|
|
149
134
|
end
|
|
150
135
|
|
|
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
|
|
136
|
+
# If an entry with the same command_name was written AFTER our process
|
|
137
|
+
# started, a sibling test runner (typically a subprocess our parent
|
|
138
|
+
# process shelled out to) wrote it. Combine coverage data rather than
|
|
139
|
+
# overwriting, so an empty parent-process result doesn't clobber the
|
|
140
|
+
# subprocess's real data. See https://github.com/simplecov-ruby/simplecov/issues/581.
|
|
141
|
+
def merged_entry(existing, incoming)
|
|
142
|
+
return incoming unless concurrent_runner_entry?(existing)
|
|
167
143
|
|
|
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
|
|
144
|
+
incoming.merge(
|
|
145
|
+
"coverage" => Combine.combine(Combine::ResultsCombiner, existing["coverage"], incoming["coverage"])
|
|
146
|
+
)
|
|
178
147
|
end
|
|
179
148
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
_key, data = result.first
|
|
149
|
+
def concurrent_runner_entry?(entry)
|
|
150
|
+
return false unless entry.is_a?(Hash)
|
|
183
151
|
|
|
184
|
-
|
|
152
|
+
timestamp = entry["timestamp"]
|
|
153
|
+
process_start = SimpleCov.process_start_time
|
|
154
|
+
timestamp && process_start && timestamp.to_i >= process_start.to_i
|
|
185
155
|
end
|
|
186
156
|
|
|
187
|
-
def
|
|
188
|
-
|
|
189
|
-
{"lines" => line_coverage_data}
|
|
190
|
-
end
|
|
157
|
+
def synchronize_resultset(&)
|
|
158
|
+
ResultsetStore.synchronize(&)
|
|
191
159
|
end
|
|
192
160
|
end
|
|
193
161
|
end
|