simplecov 1.0.2 → 1.1.0
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/README.md +65 -1425
- data/lib/simplecov/atomic_file.rb +70 -0
- data/lib/simplecov/cli/clean.rb +43 -3
- data/lib/simplecov/cli/command_helpers.rb +55 -0
- data/lib/simplecov/cli/coverage.rb +21 -28
- data/lib/simplecov/cli/coverage_file.rb +65 -0
- data/lib/simplecov/cli/diff.rb +43 -48
- data/lib/simplecov/cli/dotfile.rb +12 -6
- data/lib/simplecov/cli/merge.rb +12 -10
- data/lib/simplecov/cli/open.rb +3 -5
- data/lib/simplecov/cli/report.rb +31 -23
- 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 +30 -94
- data/lib/simplecov/cli/uncovered.rb +20 -25
- data/lib/simplecov/cli.rb +15 -2
- data/lib/simplecov/combine/branches_combiner.rb +43 -19
- 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 +48 -20
- data/lib/simplecov/combine/methods_combiner.rb +46 -21
- data/lib/simplecov/combine/results_combiner.rb +12 -37
- data/lib/simplecov/combine.rb +5 -23
- data/lib/simplecov/command_guesser.rb +66 -9
- data/lib/simplecov/configuration/coverage.rb +12 -15
- data/lib/simplecov/configuration/coverage_criteria.rb +34 -38
- data/lib/simplecov/configuration/eval_coverage.rb +41 -0
- data/lib/simplecov/configuration/filters.rb +11 -44
- data/lib/simplecov/configuration/formatting.rb +18 -9
- data/lib/simplecov/configuration/groups.rb +42 -0
- data/lib/simplecov/configuration/merging.rb +8 -7
- data/lib/simplecov/configuration/thresholds.rb +14 -13
- data/lib/simplecov/configuration.rb +13 -43
- data/lib/simplecov/coverage_json.rb +24 -0
- data/lib/simplecov/coverage_statistics.rb +1 -1
- data/lib/simplecov/coverage_violations.rb +15 -5
- data/lib/simplecov/defaults.rb +7 -3
- data/lib/simplecov/directive.rb +1 -1
- data/lib/simplecov/exit_codes/check.rb +33 -0
- data/lib/simplecov/exit_codes/maximum_coverage_drop_check.rb +11 -20
- data/lib/simplecov/exit_codes/maximum_overall_coverage_check.rb +3 -16
- data/lib/simplecov/exit_codes/minimum_coverage_by_file_check.rb +14 -23
- data/lib/simplecov/exit_codes/minimum_coverage_by_group_check.rb +14 -25
- data/lib/simplecov/exit_codes/minimum_overall_coverage_check.rb +4 -17
- data/lib/simplecov/exit_codes.rb +1 -0
- data/lib/simplecov/exit_handling.rb +11 -41
- data/lib/simplecov/file_list.rb +5 -10
- data/lib/simplecov/filter.rb +43 -9
- data/lib/simplecov/formatter/base.rb +11 -0
- data/lib/simplecov/formatter/coverage_json_writer.rb +97 -0
- data/lib/simplecov/formatter/html_formatter/public/index.html +35 -5
- data/lib/simplecov/formatter/html_formatter/viewer_data_validator.rb +96 -0
- data/lib/simplecov/formatter/html_formatter.rb +67 -47
- data/lib/simplecov/formatter/json_formatter/errors_formatter.rb +54 -56
- data/lib/simplecov/formatter/json_formatter/result_hash_formatter.rb +78 -88
- data/lib/simplecov/formatter/json_formatter/source_file_formatter.rb +70 -76
- data/lib/simplecov/formatter/json_formatter.rb +5 -48
- data/lib/simplecov/formatter/multi_formatter.rb +1 -1
- data/lib/simplecov/formatter/simple_formatter.rb +8 -5
- data/lib/simplecov/formatter.rb +12 -0
- data/lib/simplecov/group_names.rb +32 -0
- data/lib/simplecov/last_run.rb +16 -9
- data/lib/simplecov/lines_classifier.rb +29 -8
- data/lib/simplecov/load_global_config.rb +5 -2
- data/lib/simplecov/parallel_adapters/base.rb +17 -0
- data/lib/simplecov/parallel_adapters/generic.rb +2 -2
- data/lib/simplecov/parallel_adapters/parallel_tests.rb +2 -2
- data/lib/simplecov/parallel_coordination.rb +6 -1
- data/lib/simplecov/parallel_result_merger.rb +230 -0
- data/lib/simplecov/report_deferral.rb +49 -0
- data/lib/simplecov/report_stamp.rb +28 -0
- data/lib/simplecov/result.rb +40 -10
- data/lib/simplecov/result_adapter.rb +48 -16
- data/lib/simplecov/result_merger/resultset_file.rb +43 -7
- data/lib/simplecov/result_merger/resultset_run_identity.rb +67 -0
- data/lib/simplecov/result_merger/resultset_store.rb +15 -12
- data/lib/simplecov/result_merger/unloaded_files.rb +103 -0
- data/lib/simplecov/result_merger.rb +69 -42
- data/lib/simplecov/result_processing.rb +82 -43
- data/lib/simplecov/run_identity.rb +77 -0
- data/lib/simplecov/simulate_coverage.rb +35 -11
- data/lib/simplecov/source_file/method.rb +7 -1
- data/lib/simplecov/source_file/ruby_data_parser.rb +25 -3
- data/lib/simplecov/source_file/skip_chunks.rb +7 -10
- data/lib/simplecov/source_file/source_loader.rb +23 -7
- data/lib/simplecov/source_file/statistics.rb +24 -16
- data/lib/simplecov/static_coverage_extractor/condition_folding.rb +203 -13
- data/lib/simplecov/static_coverage_extractor/location_conventions.rb +19 -30
- data/lib/simplecov/static_coverage_extractor/method_collector.rb +7 -0
- data/lib/simplecov/static_coverage_extractor/prism_compat.rb +55 -0
- data/lib/simplecov/static_coverage_extractor/value_position.rb +6 -14
- data/lib/simplecov/static_coverage_extractor/visitor.rb +23 -36
- data/lib/simplecov/unloaded_file_injector.rb +75 -0
- data/lib/simplecov/version.rb +1 -1
- data/lib/simplecov.rb +11 -5
- data/schemas/coverage-v1.0.schema.json +2 -2
- data/schemas/coverage.schema.json +2 -2
- data/sig/simplecov.rbs +206 -67
- metadata +28 -14
- data/doc/alternate-formatters.md +0 -66
- data/doc/commercial-services.md +0 -25
- data/doc/editor-integration.md +0 -18
- data/lib/simplecov/combine/files_combiner.rb +0 -70
- data/lib/simplecov/formatter/html_formatter/public/application.css +0 -1
- data/lib/simplecov/formatter/html_formatter/public/application.js +0 -18
- 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
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "result_merger/legacy_format_adapter"
|
|
4
4
|
require_relative "result_merger/resultset_file"
|
|
5
|
+
require_relative "result_merger/resultset_run_identity"
|
|
5
6
|
require_relative "result_merger/resultset_store"
|
|
7
|
+
require_relative "result_merger/unloaded_files"
|
|
6
8
|
|
|
7
9
|
module SimpleCov
|
|
8
10
|
#
|
|
@@ -11,6 +13,8 @@ module SimpleCov
|
|
|
11
13
|
# upon multiple test suites.
|
|
12
14
|
#
|
|
13
15
|
module ResultMerger
|
|
16
|
+
extend ResultsetRunIdentity
|
|
17
|
+
|
|
14
18
|
class << self
|
|
15
19
|
def resultset_path
|
|
16
20
|
ResultsetStore.resultset_path
|
|
@@ -23,29 +27,58 @@ module SimpleCov
|
|
|
23
27
|
end
|
|
24
28
|
|
|
25
29
|
def merge_results(*file_paths, ignore_timeout: false)
|
|
26
|
-
#
|
|
27
|
-
#
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
# even more data especially when it also reads in all source files.
|
|
34
|
-
initial_memo = valid_results(file_paths.shift, ignore_timeout: ignore_timeout)
|
|
35
|
-
|
|
36
|
-
command_names, coverage = file_paths.reduce(initial_memo) do |memo, file_path|
|
|
37
|
-
merge_coverage(memo, valid_results(file_path, ignore_timeout: ignore_timeout))
|
|
38
|
-
end
|
|
30
|
+
# Tracked paths are collected as each resultset is parsed, so files are
|
|
31
|
+
# still read and discarded one at a time. See #1250.
|
|
32
|
+
tracked_files = Set.new
|
|
33
|
+
command_names, coverage = absorb_results(file_paths, ignore_timeout: ignore_timeout,
|
|
34
|
+
&UnloadedFiles.collector(tracked_files))
|
|
35
|
+
create_result(command_names, coverage, tracked_files: tracked_files)
|
|
36
|
+
end
|
|
39
37
|
|
|
40
|
-
|
|
38
|
+
#
|
|
39
|
+
# Reads every resultset and folds it into one merged coverage, stopping
|
|
40
|
+
# short of building a `SimpleCov::Result`.
|
|
41
|
+
#
|
|
42
|
+
# It is intentional here that files are only read in and parsed one at a time.
|
|
43
|
+
#
|
|
44
|
+
# In big CI setups you might deal with 100s of CI jobs and each one producing Megabytes
|
|
45
|
+
# of data. Reading them all in easily produces Gigabytes of memory consumption which
|
|
46
|
+
# we want to avoid.
|
|
47
|
+
#
|
|
48
|
+
# For similar reasons a SimpleCov::Result is only created in the end as that'd create
|
|
49
|
+
# even more data especially when it also reads in all source files.
|
|
50
|
+
#
|
|
51
|
+
# One accumulator absorbs the whole run, rather than folding each file
|
|
52
|
+
# into the merged-so-far pairwise: the pairwise form rebuilt every
|
|
53
|
+
# file's coverage once per resultset, which is what made merging a
|
|
54
|
+
# large parallel run's results the dominant cost of `collate`.
|
|
55
|
+
# Absorbing is still one resultset at a time, so the memory ceiling
|
|
56
|
+
# above is unchanged.
|
|
57
|
+
#
|
|
58
|
+
# `file_paths` is only ever iterated, so a caller that wants to observe
|
|
59
|
+
# the merge as it goes can hand in any Enumerable — `benchmarks/collate`
|
|
60
|
+
# passes an Enumerator that reports progress — rather than reimplement
|
|
61
|
+
# this loop and risk timing something other than what ships.
|
|
62
|
+
#
|
|
63
|
+
# @return [Array] the command names and the merged coverage
|
|
64
|
+
#
|
|
65
|
+
def absorb_results(file_paths, ignore_timeout: false, &on_parse)
|
|
66
|
+
Combine::CoverageAccumulator.fold(
|
|
67
|
+
file_paths.lazy.map { |file_path| valid_results(file_path, ignore_timeout: ignore_timeout, &on_parse) }
|
|
68
|
+
)
|
|
41
69
|
end
|
|
42
70
|
|
|
43
|
-
|
|
44
|
-
|
|
71
|
+
# Yields the surviving entries before they are reduced.
|
|
72
|
+
def valid_results(file_path, ignore_timeout: false, &on_parse)
|
|
73
|
+
merge_valid_results(ResultsetFile.parse(file_path), ignore_timeout: ignore_timeout, &on_parse)
|
|
45
74
|
end
|
|
46
75
|
|
|
76
|
+
# Yields the entries that survived the merge timeout, so a caller that
|
|
77
|
+
# wants to observe what a resultset carried sees only what is being
|
|
78
|
+
# merged. An expired entry contributes nothing, tracked paths included.
|
|
47
79
|
def merge_valid_results(results, ignore_timeout: false)
|
|
48
80
|
results = drop_expired_results(results) unless ignore_timeout
|
|
81
|
+
yield results if block_given?
|
|
49
82
|
|
|
50
83
|
command_plus_coverage = results.map do |command_name, data|
|
|
51
84
|
[[command_name], LegacyFormatAdapter.call(data.fetch("coverage"))]
|
|
@@ -68,12 +101,9 @@ module SimpleCov
|
|
|
68
101
|
end
|
|
69
102
|
|
|
70
103
|
def warn_about_expired_results(expired_command_names)
|
|
71
|
-
#
|
|
72
|
-
#
|
|
73
|
-
#
|
|
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.
|
|
104
|
+
# Respect quiet configurations and keep this consistent with every
|
|
105
|
+
# other SimpleCov diagnostic. Ordinary parallel workers only store
|
|
106
|
+
# their own slice; the selected final process performs this merge.
|
|
77
107
|
return unless SimpleCov.print_errors
|
|
78
108
|
|
|
79
109
|
warn "[SimpleCov]: Excluded #{expired_command_names.size} result(s) older than " \
|
|
@@ -82,26 +112,29 @@ module SimpleCov
|
|
|
82
112
|
"Increase SimpleCov.merge_timeout to include them."
|
|
83
113
|
end
|
|
84
114
|
|
|
85
|
-
def create_result(command_names, coverage)
|
|
115
|
+
def create_result(command_names, coverage, tracked_files: Set.new)
|
|
86
116
|
return nil unless coverage
|
|
87
117
|
|
|
88
118
|
command_name = command_names.reject(&:empty?).sort.join(", ")
|
|
119
|
+
coverage, injected = UnloadedFiles.inject(coverage, tracked_files)
|
|
89
120
|
# The merged result is the authoritative one users actually see, so
|
|
90
121
|
# it's the one that warns about source files dropped because they no
|
|
91
122
|
# longer exist on disk (issue #980). The per-process slices built in
|
|
92
123
|
# `process_coverage_result` stay quiet to avoid one warning per worker.
|
|
93
|
-
SimpleCov::Result.new(
|
|
124
|
+
SimpleCov::Result.new(
|
|
125
|
+
coverage,
|
|
126
|
+
command_name: command_name,
|
|
127
|
+
not_loaded_files: UnloadedFiles.never_executed(coverage) | injected,
|
|
128
|
+
tracked_files: tracked_files.to_a,
|
|
129
|
+
report: true
|
|
130
|
+
)
|
|
94
131
|
end
|
|
95
132
|
|
|
96
133
|
def merge_coverage(*results)
|
|
97
134
|
return [[""], nil] if results.empty?
|
|
98
135
|
return results.first if results.size == 1
|
|
99
136
|
|
|
100
|
-
|
|
101
|
-
# timestamp is dropped here, which is intentional (we merge it, it gets a new time stamp as of now)
|
|
102
|
-
merged_coverage = Combine.combine(Combine::ResultsCombiner, memo_coverage, coverage)
|
|
103
|
-
[memo_command + command, merged_coverage]
|
|
104
|
-
end
|
|
137
|
+
Combine::CoverageAccumulator.fold(results)
|
|
105
138
|
end
|
|
106
139
|
|
|
107
140
|
#
|
|
@@ -109,8 +142,9 @@ module SimpleCov
|
|
|
109
142
|
# SimpleCov::Result with merged coverage data and the command_name
|
|
110
143
|
# for the result consisting of a join on all source result's names
|
|
111
144
|
def merged_result
|
|
112
|
-
|
|
113
|
-
|
|
145
|
+
tracked_files = Set.new
|
|
146
|
+
command_names, coverage = merge_valid_results(read_resultset, &UnloadedFiles.collector(tracked_files))
|
|
147
|
+
create_result(command_names, coverage, tracked_files: tracked_files)
|
|
114
148
|
end
|
|
115
149
|
|
|
116
150
|
def read_resultset
|
|
@@ -139,19 +173,12 @@ module SimpleCov
|
|
|
139
173
|
# overwriting, so an empty parent-process result doesn't clobber the
|
|
140
174
|
# subprocess's real data. See https://github.com/simplecov-ruby/simplecov/issues/581.
|
|
141
175
|
def merged_entry(existing, incoming)
|
|
142
|
-
return incoming unless concurrent_runner_entry?(existing)
|
|
176
|
+
return incoming unless concurrent_runner_entry?(existing, incoming)
|
|
143
177
|
|
|
144
|
-
incoming.merge(
|
|
145
|
-
"coverage" => Combine.combine(
|
|
178
|
+
merged = incoming.merge(
|
|
179
|
+
"coverage" => Combine::ResultsCombiner.combine(existing["coverage"], incoming["coverage"])
|
|
146
180
|
)
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
def concurrent_runner_entry?(entry)
|
|
150
|
-
return false unless entry.is_a?(Hash)
|
|
151
|
-
|
|
152
|
-
timestamp = entry["timestamp"]
|
|
153
|
-
process_start = SimpleCov.process_start_time
|
|
154
|
-
timestamp && process_start && timestamp.to_i >= process_start.to_i
|
|
181
|
+
UnloadedFiles.carry_tracked(merged, existing, incoming)
|
|
155
182
|
end
|
|
156
183
|
|
|
157
184
|
def synchronize_resultset(&)
|
|
@@ -13,13 +13,27 @@ module SimpleCov
|
|
|
13
13
|
# so all results in all files specified will be merged. Pass
|
|
14
14
|
# `ignore_timeout: false` to honor it.
|
|
15
15
|
#
|
|
16
|
-
|
|
16
|
+
# `processes:` above 1 fans the merge out across that many forked worker
|
|
17
|
+
# processes, for a collate big enough that reading and parsing the
|
|
18
|
+
# resultsets dominates it. The report is identical either way, not merely
|
|
19
|
+
# equivalent — the workers visit the resultsets in the order the
|
|
20
|
+
# single-process merge visits them — and one process never forks at all.
|
|
21
|
+
# The count is deliberately not clamped to the machine's core count nor
|
|
22
|
+
# gated on some minimum number of resultsets: how many processes a collate
|
|
23
|
+
# job can afford is the caller's call, not SimpleCov's. Anything below 1 is
|
|
24
|
+
# taken as 1, so a count computed from arithmetic that can reach zero needs
|
|
25
|
+
# no guarding. Defaults to `SIMPLECOV_CONCURRENCY`, or 1 when that is
|
|
26
|
+
# unset. See `SimpleCov::ParallelResultMerger`.
|
|
27
|
+
#
|
|
28
|
+
def collate(result_filenames, profile = nil, processes: ENV.fetch("SIMPLECOV_CONCURRENCY", 1).to_i,
|
|
29
|
+
ignore_timeout: true, &)
|
|
17
30
|
raise ArgumentError, "There are no reports to be merged" if result_filenames.empty?
|
|
18
31
|
|
|
19
32
|
initial_setup(profile, &)
|
|
20
33
|
|
|
21
34
|
# Use the ResultMerger to produce a single, merged result, ready to use.
|
|
22
|
-
@result =
|
|
35
|
+
@result = ParallelResultMerger.merge_and_store(*result_filenames, processes: [1, processes].max,
|
|
36
|
+
ignore_timeout: ignore_timeout)
|
|
23
37
|
|
|
24
38
|
@collating_result = true
|
|
25
39
|
run_exit_tasks!
|
|
@@ -28,24 +42,22 @@ module SimpleCov
|
|
|
28
42
|
end
|
|
29
43
|
|
|
30
44
|
#
|
|
31
|
-
# Returns the result for the current coverage run
|
|
32
|
-
#
|
|
45
|
+
# Returns the result for the current coverage run. With merging enabled,
|
|
46
|
+
# every process stores its own slice, but only the finalization owner reads
|
|
47
|
+
# and caches the merged result.
|
|
33
48
|
#
|
|
34
49
|
def result
|
|
35
50
|
return @result if result?
|
|
36
51
|
|
|
37
52
|
use_merging = merging
|
|
38
53
|
|
|
39
|
-
|
|
40
|
-
# step, so this per-process result is the final one and reports any
|
|
41
|
-
# dropped source files; otherwise the merged result does the reporting.
|
|
42
|
-
process_coverage_result(report: !use_merging) if defined?(Coverage) && Coverage.running?
|
|
54
|
+
collect_own_coverage(standalone: !use_merging)
|
|
43
55
|
|
|
44
56
|
# If we're using merging of results, store the current result
|
|
45
57
|
# first (if there is one), then merge the results and return those
|
|
46
58
|
if use_merging
|
|
47
59
|
SimpleCov::ResultMerger.store_result(@result) if result?
|
|
48
|
-
return @result unless
|
|
60
|
+
return @result unless merge_finalization_owner?
|
|
49
61
|
|
|
50
62
|
wait_for_other_processes
|
|
51
63
|
@result = SimpleCov::ResultMerger.merged_result
|
|
@@ -54,6 +66,16 @@ module SimpleCov
|
|
|
54
66
|
@result
|
|
55
67
|
end
|
|
56
68
|
|
|
69
|
+
# Build this process's slice of the coverage result. `standalone` is true
|
|
70
|
+
# when no merge step follows, which makes this the final result: it is the
|
|
71
|
+
# one that reports dropped source files, and the one that injects unloaded
|
|
72
|
+
# files. When a merge does follow, both jobs belong to the merged result.
|
|
73
|
+
def collect_own_coverage(standalone:)
|
|
74
|
+
return unless defined?(Coverage) && Coverage.running?
|
|
75
|
+
|
|
76
|
+
process_coverage_result(report: standalone, inject_unloaded: standalone)
|
|
77
|
+
end
|
|
78
|
+
|
|
57
79
|
# Returns nil if the result has not been computed, otherwise the result.
|
|
58
80
|
def result?
|
|
59
81
|
defined?(@result) && @result
|
|
@@ -79,7 +101,7 @@ module SimpleCov
|
|
|
79
101
|
# construction). Files matched by no group fall into the implicit
|
|
80
102
|
# "Ungrouped" bucket.
|
|
81
103
|
def grouped(files, groups: SimpleCov.groups)
|
|
82
|
-
return {} if groups.empty?
|
|
104
|
+
return {} if GroupNames.validate!(groups.keys).empty?
|
|
83
105
|
|
|
84
106
|
grouped = groups.transform_values do |filter|
|
|
85
107
|
SimpleCov::FileList.new(files.select { |source_file| filter.matches?(source_file) })
|
|
@@ -87,7 +109,7 @@ module SimpleCov
|
|
|
87
109
|
|
|
88
110
|
in_group = grouped_file_set(grouped)
|
|
89
111
|
ungrouped = files.reject { |source_file| in_group.include?(source_file) }
|
|
90
|
-
grouped[
|
|
112
|
+
grouped[GroupNames::UNGROUPED] = SimpleCov::FileList.new(ungrouped) if ungrouped.any?
|
|
91
113
|
|
|
92
114
|
grouped
|
|
93
115
|
end
|
|
@@ -115,6 +137,29 @@ module SimpleCov
|
|
|
115
137
|
coverage.floor(2)
|
|
116
138
|
end
|
|
117
139
|
|
|
140
|
+
# Add simulated coverage for each of `candidate_paths` the result doesn't
|
|
141
|
+
# already carry, and return it with the set of paths added.
|
|
142
|
+
#
|
|
143
|
+
# @api private — the seam `SimpleCov::ResultMerger` injects through. Public
|
|
144
|
+
# only because the merge runs in another object, on behalf of processes
|
|
145
|
+
# whose configuration it may not share, so it supplies the paths itself.
|
|
146
|
+
def inject_unloaded_files(result, candidate_paths, synthesize: nil, lines: nil)
|
|
147
|
+
return [result, Set.new] if candidate_paths.empty?
|
|
148
|
+
|
|
149
|
+
# Synthesizing branch and method tuples means parsing every tracked file
|
|
150
|
+
# that wasn't loaded, which is about half the cost of simulating one.
|
|
151
|
+
# Nothing reads those tuples when neither criterion is enabled —
|
|
152
|
+
# `Combine::CoverageAccumulator` only combines them per criterion, and the
|
|
153
|
+
# statistics drop them the same way — so skip the parse. The same goes
|
|
154
|
+
# for line data, which a branch-only or method-only run neither reports
|
|
155
|
+
# nor receives from `Coverage` for the files it loaded. See #1250.
|
|
156
|
+
UnloadedFileInjector.call(
|
|
157
|
+
result, candidate_paths,
|
|
158
|
+
synthesize: synthesize.nil? ? branch_coverage? || method_coverage? : synthesize,
|
|
159
|
+
lines: lines.nil? ? line_coverage? : lines
|
|
160
|
+
)
|
|
161
|
+
end
|
|
162
|
+
|
|
118
163
|
private
|
|
119
164
|
|
|
120
165
|
def initial_setup(profile, &block)
|
|
@@ -126,14 +171,15 @@ module SimpleCov
|
|
|
126
171
|
grouped.values.each_with_object(Set.new) { |file_list, set| set.merge(file_list) }
|
|
127
172
|
end
|
|
128
173
|
|
|
129
|
-
#
|
|
130
|
-
#
|
|
131
|
-
#
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
174
|
+
# Every path this process was told to track, whether or not it loaded them.
|
|
175
|
+
# Recorded on the result (and from there into the resultset) so that a merge
|
|
176
|
+
# in another process can inject the ones nobody loaded without needing this
|
|
177
|
+
# process's `cover` / `track_files` configuration. A standalone `collate`
|
|
178
|
+
# never ran `SimpleCov.start` and so has none of its own. See #1250.
|
|
179
|
+
def tracked_file_paths
|
|
180
|
+
UnloadedFileInjector.discover(
|
|
181
|
+
unloaded_file_discovery_globs, root: root, reject: filters.select(&:path_only?)
|
|
182
|
+
)
|
|
137
183
|
end
|
|
138
184
|
|
|
139
185
|
# Globs to expand on disk when injecting unloaded files into the
|
|
@@ -144,36 +190,29 @@ module SimpleCov
|
|
|
144
190
|
[tracked_files, *cover_globs].compact
|
|
145
191
|
end
|
|
146
192
|
|
|
147
|
-
# Expand the given globs relative to SimpleCov.root, not Dir.pwd —
|
|
148
|
-
# test runners that chdir (or CI scripts that invoke the suite
|
|
149
|
-
# from a subdir) would otherwise silently miss the unloaded-file
|
|
150
|
-
# injection and produce a different file set per environment. See
|
|
151
|
-
# issue #1106.
|
|
152
|
-
def discover_unloaded_paths(globs)
|
|
153
|
-
globs.flat_map { |glob| Dir.glob(glob, base: root) }.uniq
|
|
154
|
-
end
|
|
155
|
-
|
|
156
|
-
def inject_unloaded_files(result, candidate_paths)
|
|
157
|
-
not_loaded_files = candidate_paths.each_with_object(Set.new) do |file, set|
|
|
158
|
-
absolute_path = File.expand_path(file, root)
|
|
159
|
-
next if result.key?(absolute_path)
|
|
160
|
-
|
|
161
|
-
result[absolute_path] = SimulateCoverage.call(absolute_path)
|
|
162
|
-
set << absolute_path
|
|
163
|
-
end
|
|
164
|
-
|
|
165
|
-
[result, not_loaded_files]
|
|
166
|
-
end
|
|
167
|
-
|
|
168
193
|
# Run all the steps that handle processing the raw coverage result.
|
|
169
194
|
# `report:` is true only when this slice is the final result (merging
|
|
170
195
|
# off); with merging on the merged result reports dropped source files,
|
|
171
196
|
# so the per-process slice stays quiet to avoid one warning per worker.
|
|
172
|
-
|
|
197
|
+
#
|
|
198
|
+
# `inject_unloaded:` is likewise false when a merge step follows. Only the
|
|
199
|
+
# union of every process's loaded files says what was really never loaded,
|
|
200
|
+
# so injecting here means each worker simulates nearly the whole project
|
|
201
|
+
# and all but one of those passes is merged away. See #1250.
|
|
202
|
+
def process_coverage_result(report:, inject_unloaded: true)
|
|
173
203
|
raw = SimpleCov::UselessResultsRemover.call(Coverage.result)
|
|
174
204
|
adapted = SimpleCov::ResultAdapter.call(raw)
|
|
175
|
-
|
|
176
|
-
|
|
205
|
+
# What this process was told to track and did not load. Subtracting what
|
|
206
|
+
# it loaded matters because `Result#to_hash` serializes coverage after
|
|
207
|
+
# filtering: a file this process loaded and then filtered out would
|
|
208
|
+
# otherwise be recorded as tracked while absent from the stored coverage,
|
|
209
|
+
# and the merge would simulate it back in as never-loaded. See #1250.
|
|
210
|
+
tracked = tracked_file_paths - adapted.keys
|
|
211
|
+
result, not_loaded = inject_unloaded ? inject_unloaded_files(adapted, tracked) : [adapted, Set.new]
|
|
212
|
+
@result = SimpleCov::Result.new(
|
|
213
|
+
result, not_loaded_files: not_loaded, tracked_files: tracked,
|
|
214
|
+
run_id: SimpleCov.run_id, worker_id: SimpleCov.worker_id, report: report
|
|
215
|
+
)
|
|
177
216
|
end
|
|
178
217
|
end
|
|
179
218
|
end
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "securerandom"
|
|
4
|
+
|
|
5
|
+
module SimpleCov
|
|
6
|
+
# Identifies one test invocation and each top-level parallel worker within
|
|
7
|
+
# it. Forked subprocesses inherit both values from their parent worker.
|
|
8
|
+
module RunIdentity
|
|
9
|
+
module_function
|
|
10
|
+
|
|
11
|
+
# Returns the run id together with its provenance: `true` when the id
|
|
12
|
+
# alone proves same-run membership (explicitly configured, derived from
|
|
13
|
+
# parallel_tests' per-invocation pid file, or freshly random), `false`
|
|
14
|
+
# when it was inferred from the parent pid, which consecutive runs
|
|
15
|
+
# launched by the same long-lived parent process share.
|
|
16
|
+
def generate
|
|
17
|
+
explicit = ENV.fetch("SIMPLECOV_RUN_ID", nil)
|
|
18
|
+
return [explicit, true] if explicit && !explicit.empty?
|
|
19
|
+
|
|
20
|
+
pid_file = ENV.fetch("PARALLEL_PID_FILE", nil)
|
|
21
|
+
return ["parallel-tests:#{File.basename(pid_file)}", true] if pid_file && !pid_file.empty?
|
|
22
|
+
|
|
23
|
+
return ["parallel-parent:#{Process.ppid}", false] if SimpleCov::ParallelAdapters.current
|
|
24
|
+
|
|
25
|
+
[SecureRandom.uuid, true]
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def worker_id
|
|
29
|
+
explicit = ENV.fetch("SIMPLECOV_WORKER_ID", nil)
|
|
30
|
+
return explicit if explicit && !explicit.empty?
|
|
31
|
+
|
|
32
|
+
number = ENV.fetch("TEST_ENV_NUMBER", nil)
|
|
33
|
+
return number.empty? ? "1" : number if number
|
|
34
|
+
|
|
35
|
+
Process.pid.to_s
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Whether the current run id alone proves same-run membership. Decided
|
|
39
|
+
# where the id is generated, never re-inferred from the id's shape, so
|
|
40
|
+
# an explicit SIMPLECOV_RUN_ID that happens to look like an inferred
|
|
41
|
+
# one is still trusted.
|
|
42
|
+
def authoritative?
|
|
43
|
+
materialize_current
|
|
44
|
+
@authoritative
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def current
|
|
48
|
+
materialize_current
|
|
49
|
+
@current
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def materialize_current
|
|
53
|
+
@current, @authoritative = generate unless defined?(@current)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def current_worker_id
|
|
57
|
+
@current_worker_id ||= worker_id
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def prepare
|
|
61
|
+
SimpleCov::ParallelAdapters.current
|
|
62
|
+
SimpleCov.run_id
|
|
63
|
+
SimpleCov.worker_id
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Mixed into SimpleCov after ParallelAdapters has loaded.
|
|
67
|
+
module Accessors
|
|
68
|
+
def run_id
|
|
69
|
+
SimpleCov::RunIdentity.current
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def worker_id
|
|
73
|
+
SimpleCov::RunIdentity.current_worker_id
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -33,25 +33,46 @@ module SimpleCov
|
|
|
33
33
|
# can't be parsed, fall back to the old empty hashes — old behavior,
|
|
34
34
|
# old tradeoff.
|
|
35
35
|
#
|
|
36
|
+
# Pass `synthesize: false` to skip the static analysis and return the
|
|
37
|
+
# empty hashes directly. Callers use it when neither branch nor method
|
|
38
|
+
# coverage is enabled, since nothing will read the tuples and the Prism
|
|
39
|
+
# parse is about half the cost of simulating a file. See #1250.
|
|
40
|
+
#
|
|
41
|
+
# Pass `lines: false` to omit the `"lines"` key entirely, mirroring what
|
|
42
|
+
# `Coverage.result` reports for a file loaded under a branch-only or
|
|
43
|
+
# method-only run. Emitting zeroed lines there would make a simulated
|
|
44
|
+
# file indistinguishable from one a sibling process actually loaded once
|
|
45
|
+
# the two are merged.
|
|
46
|
+
#
|
|
36
47
|
# @return [Hash]
|
|
37
48
|
#
|
|
38
|
-
def call(absolute_path)
|
|
49
|
+
def call(absolute_path, synthesize: true, lines: true)
|
|
39
50
|
source_lines = read_lines(absolute_path)
|
|
40
|
-
|
|
41
|
-
|
|
51
|
+
simulated = synthesized_tuples(source_lines, synthesize)
|
|
52
|
+
return simulated unless lines
|
|
53
|
+
|
|
54
|
+
classified = coverage_stub(absolute_path, source_lines) ||
|
|
55
|
+
LinesClassifier.new.classify(source_lines)
|
|
56
|
+
{"lines" => classified}.merge(simulated)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# The branch and method tuples for a file, or empty hashes when the static
|
|
60
|
+
# analysis is skipped (nothing enabled reads them) or unavailable (no Prism,
|
|
61
|
+
# or the file doesn't parse).
|
|
62
|
+
def synthesized_tuples(source_lines, synthesize)
|
|
42
63
|
empty = {"branches" => {}, "methods" => {}} #: Hash[String, Hash[untyped, untyped]]
|
|
43
|
-
synthesized = StaticCoverageExtractor.call(source_lines.join) || empty
|
|
64
|
+
synthesized = (StaticCoverageExtractor.call(source_lines.join) if synthesize) || empty
|
|
44
65
|
|
|
45
|
-
{
|
|
46
|
-
"lines" => lines,
|
|
47
|
-
"branches" => synthesized["branches"],
|
|
48
|
-
"methods" => synthesized["methods"]
|
|
49
|
-
}
|
|
66
|
+
{"branches" => synthesized["branches"], "methods" => synthesized["methods"]}
|
|
50
67
|
end
|
|
51
68
|
|
|
69
|
+
# SystemCallError, not just ENOENT: a `track_files` glob can sweep
|
|
70
|
+
# up an unreadable file or a directory named like a Ruby file
|
|
71
|
+
# (EACCES, EISDIR), and simulation must degrade to "empty file"
|
|
72
|
+
# rather than crash the merge or report step.
|
|
52
73
|
def read_lines(path)
|
|
53
74
|
File.readlines(path)
|
|
54
|
-
rescue
|
|
75
|
+
rescue SystemCallError
|
|
55
76
|
[]
|
|
56
77
|
end
|
|
57
78
|
|
|
@@ -68,7 +89,10 @@ module SimpleCov
|
|
|
68
89
|
classifier_output = LinesClassifier.new.classify(source_lines)
|
|
69
90
|
stub.each_index { |idx| stub[idx] = nil if classifier_output[idx].nil? }
|
|
70
91
|
stub
|
|
71
|
-
rescue
|
|
92
|
+
rescue SystemCallError, SyntaxError
|
|
93
|
+
# SystemCallError for the same reason as read_lines above:
|
|
94
|
+
# line_stub reads the file itself, so EACCES/EISDIR surface here
|
|
95
|
+
# too.
|
|
72
96
|
nil
|
|
73
97
|
end
|
|
74
98
|
end
|
|
@@ -18,10 +18,16 @@ module SimpleCov
|
|
|
18
18
|
!skipped? && coverage.positive?
|
|
19
19
|
end
|
|
20
20
|
|
|
21
|
+
# Criterion-level skips (nocov chunks, `# simplecov:disable` /
|
|
22
|
+
# `# simplecov:disable method` regions) arrive via `skipped!` from
|
|
23
|
+
# MethodBuilder. Deliberately NOT derived from the lines' skip
|
|
24
|
+
# state: a line-only directive around a def must not remove the
|
|
25
|
+
# method from method totals. Without line info there is nothing to
|
|
26
|
+
# report against, so such a method stays skipped.
|
|
21
27
|
def skipped?
|
|
22
28
|
return @skipped if defined?(@skipped)
|
|
23
29
|
|
|
24
|
-
@skipped = lines.
|
|
30
|
+
@skipped = lines.empty?
|
|
25
31
|
end
|
|
26
32
|
|
|
27
33
|
# Flag the method as skipped directly, without going through its lines.
|
|
@@ -15,10 +15,28 @@ module SimpleCov
|
|
|
15
15
|
|
|
16
16
|
# Tests use the real data structures (except for integration tests)
|
|
17
17
|
# so no need to put them through here.
|
|
18
|
+
#
|
|
19
|
+
# String parses are memoized: `Combine::BranchesCombiner` and
|
|
20
|
+
# `Combine::MethodsCombiner` derive a merge identity from every key of
|
|
21
|
+
# both sides on every pairwise merge, so collating N resultsets parses
|
|
22
|
+
# each key string N-1 times — and Ripper dominates the wall time of a
|
|
23
|
+
# large collate.
|
|
24
|
+
# Key strings repeat across folds and within report building, while the
|
|
25
|
+
# set of unique keys is bounded by the project's branch and method
|
|
26
|
+
# count, so a permanent cache stays small. Cached arrays are frozen
|
|
27
|
+
# element-wise (String class names included, not just the tuple):
|
|
28
|
+
# every caller destructures without mutating, and sharing one array
|
|
29
|
+
# across callers must stay that way. The cache key needs no such
|
|
30
|
+
# care — `Hash#[]=` dups and freezes String keys on its own.
|
|
18
31
|
def call(structure)
|
|
19
32
|
return structure if structure.is_a?(Array)
|
|
20
33
|
|
|
21
|
-
|
|
34
|
+
string = structure.to_s
|
|
35
|
+
parse_cache[string] ||= parse_array_string(string).each(&:freeze).freeze
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def parse_cache
|
|
39
|
+
@parse_cache ||= {} #: Hash[String, untyped]
|
|
22
40
|
end
|
|
23
41
|
|
|
24
42
|
# Parse a string like '[:if, 0, 3, 4, 3, 21]' or
|
|
@@ -79,9 +97,13 @@ module SimpleCov
|
|
|
79
97
|
# Method coverage keys can contain inspect-format class references
|
|
80
98
|
# like `#<Class:Foo>` or `#<Class:0x...>`, which aren't valid Ruby
|
|
81
99
|
# syntax. Wrap them in quotes so Ripper can parse the surrounding
|
|
82
|
-
# array literal; downstream we treat them as opaque strings.
|
|
100
|
+
# array literal; downstream we treat them as opaque strings. The
|
|
101
|
+
# pattern recurses because singleton methods on instances nest one
|
|
102
|
+
# inspect segment inside another (`#<Class:#<Object:0x...>>`), and
|
|
103
|
+
# stopping at the first `>` leaves a dangling `>` that fails both
|
|
104
|
+
# Ripper passes and crashes the merge.
|
|
83
105
|
def quote_inspected_class_segments(str)
|
|
84
|
-
str.gsub(/#<[
|
|
106
|
+
str.gsub(/#<(?:[^<>]|\g<0>)*>/) { |segment| %("#{segment.gsub('"', '\\"')}") }
|
|
85
107
|
end
|
|
86
108
|
end
|
|
87
109
|
end
|
|
@@ -22,19 +22,16 @@ module SimpleCov
|
|
|
22
22
|
@src = src
|
|
23
23
|
end
|
|
24
24
|
|
|
25
|
-
#
|
|
26
|
-
#
|
|
27
|
-
#
|
|
25
|
+
# Every criterion honors the deprecated all-criteria `# :nocov:`
|
|
26
|
+
# chunks plus its own per-criterion directive ranges. Methods
|
|
27
|
+
# included: they carry a source range, and nocov has always meant
|
|
28
|
+
# "exclude everything here".
|
|
28
29
|
def for(criterion)
|
|
29
|
-
|
|
30
|
-
directive_chunks.fetch(:method)
|
|
31
|
-
else
|
|
32
|
-
nocov_chunks + directive_chunks.fetch(criterion)
|
|
33
|
-
end
|
|
30
|
+
nocov_chunks + directive_chunks.fetch(criterion)
|
|
34
31
|
end
|
|
35
32
|
|
|
36
|
-
#
|
|
37
|
-
#
|
|
33
|
+
# Ranges of 1-based line numbers (see `with_index(1)` below);
|
|
34
|
+
# consumers subtract 1 to index into the zero-based lines array.
|
|
38
35
|
def nocov_chunks
|
|
39
36
|
@nocov_chunks ||= build_nocov_chunks
|
|
40
37
|
end
|