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
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SimpleCov
|
|
4
|
+
module ResultMerger
|
|
5
|
+
# The merge step's half of unloaded-file handling: the policy of when
|
|
6
|
+
# to inject and which criteria simulated files carry. The mechanism —
|
|
7
|
+
# expanding globs and simulating each file — is
|
|
8
|
+
# `SimpleCov::UnloadedFileInjector`, an easy name to confuse with this
|
|
9
|
+
# one.
|
|
10
|
+
#
|
|
11
|
+
# Injection moved here from the individual processes because only the union
|
|
12
|
+
# of what they all loaded says what was really never loaded. Doing it per
|
|
13
|
+
# process meant N workers simulated the same file up to N times and the
|
|
14
|
+
# merge threw all but one away. See #1250.
|
|
15
|
+
module UnloadedFiles
|
|
16
|
+
module_function
|
|
17
|
+
|
|
18
|
+
# The union of what every contributing process was told to track. Absent
|
|
19
|
+
# from resultsets written before this was recorded, in which case those
|
|
20
|
+
# processes injected their own unloaded files and the data is already in
|
|
21
|
+
# the coverage hash.
|
|
22
|
+
# A collector for the merge to hand `merge_valid_results`, gathering the
|
|
23
|
+
# tracked paths of every resultset that survives the merge timeout.
|
|
24
|
+
def collector(into)
|
|
25
|
+
->(surviving) { into.merge(tracked_in(surviving)) }
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def tracked_in(resultset)
|
|
29
|
+
resultset.each_with_object(Set.new) do |(_command_name, data), set|
|
|
30
|
+
set.merge(Array(data["tracked_files"]))
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Concurrent workers sharing a command name may have been told to track
|
|
35
|
+
# different sets, so keep both rather than letting the later write win.
|
|
36
|
+
def carry_tracked(entry, existing, incoming)
|
|
37
|
+
tracked = Array(existing["tracked_files"]) | Array(incoming["tracked_files"])
|
|
38
|
+
tracked.empty? ? entry : entry.merge("tracked_files" => tracked)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Simulate each tracked file the merged coverage doesn't already carry.
|
|
42
|
+
# The paths come from the resultsets rather than this process's own
|
|
43
|
+
# configuration, which a standalone `collate` would not have: it never ran
|
|
44
|
+
# `SimpleCov.start`, so it has no `cover` glob to expand. Idempotent, so
|
|
45
|
+
# resultsets from an older SimpleCov (or from a `merging false` process)
|
|
46
|
+
# that already carry their unloaded files pass through untouched.
|
|
47
|
+
def inject(coverage, tracked_files)
|
|
48
|
+
SimpleCov.inject_unloaded_files(
|
|
49
|
+
coverage, tracked_files.to_a,
|
|
50
|
+
synthesize: carries?(coverage, "branches") || carries?(coverage, "methods"),
|
|
51
|
+
lines: carries?(coverage, "lines")
|
|
52
|
+
)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# A simulated file should have the same shape as the files it is being
|
|
56
|
+
# merged alongside, so the criteria come from the merged data rather than
|
|
57
|
+
# from this process's configuration, which is not necessarily the
|
|
58
|
+
# configuration any contributing process measured under: `simplecov merge`
|
|
59
|
+
# never ran `SimpleCov.start` at all. Giving injected files fewer tables
|
|
60
|
+
# than their neighbours is what inflates the percentage #1059 fixed.
|
|
61
|
+
#
|
|
62
|
+
# Falls back to the configuration when there is nothing to be consistent
|
|
63
|
+
# with, which is a merge of resultsets that carried no files.
|
|
64
|
+
def carries?(coverage, criterion)
|
|
65
|
+
return SimpleCov.public_send(CRITERION_PREDICATES.fetch(criterion)) if coverage.empty?
|
|
66
|
+
|
|
67
|
+
coverage.any? { |_filename, file_coverage| file_coverage[criterion] }
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
CRITERION_PREDICATES = {
|
|
71
|
+
"lines" => :line_coverage?,
|
|
72
|
+
"branches" => :branch_coverage?,
|
|
73
|
+
"methods" => :method_coverage?
|
|
74
|
+
}.freeze
|
|
75
|
+
private_constant :CRITERION_PREDICATES
|
|
76
|
+
|
|
77
|
+
# Which files no contributing process ever loaded. Injection reports the
|
|
78
|
+
# ones it added, but a file can also arrive already simulated from a
|
|
79
|
+
# resultset this merge didn't inject into, so the merged line counts are
|
|
80
|
+
# still consulted, using the same signal
|
|
81
|
+
# `Combine::CoverageAccumulator` reconciles synthesized tuples on.
|
|
82
|
+
#
|
|
83
|
+
# A file is judged only when it has a relevant line. A branch-only or
|
|
84
|
+
# method-only run reports no line data at all for the files it loaded,
|
|
85
|
+
# and a loaded file with no executable lines (a comment-only constants
|
|
86
|
+
# stub, say) reports every line as `nil` — either would otherwise read
|
|
87
|
+
# as "never executed" and mark a genuinely loaded file not loaded,
|
|
88
|
+
# turning its branch and method coverage into #902's 0%.
|
|
89
|
+
# `SimulateCoverage` omits lines under those criteria too and gives a
|
|
90
|
+
# simulated file a `0` on every relevant line, so genuinely unloaded
|
|
91
|
+
# files still carry judgeable data, and a simulated file with none is
|
|
92
|
+
# already flagged by injection reporting it.
|
|
93
|
+
def never_executed(coverage)
|
|
94
|
+
coverage.each_with_object(Set.new) do |(filename, file_coverage), set|
|
|
95
|
+
counts = Array(file_coverage["lines"]) #: Array[Integer?]
|
|
96
|
+
next unless counts.any? { |count| !count.nil? }
|
|
97
|
+
|
|
98
|
+
set << filename unless Combine::CoverageAccumulator.executed?(file_coverage["lines"])
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
@@ -1,6 +1,10 @@
|
|
|
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_run_identity"
|
|
6
|
+
require_relative "result_merger/resultset_store"
|
|
7
|
+
require_relative "result_merger/unloaded_files"
|
|
4
8
|
|
|
5
9
|
module SimpleCov
|
|
6
10
|
#
|
|
@@ -9,14 +13,11 @@ module SimpleCov
|
|
|
9
13
|
# upon multiple test suites.
|
|
10
14
|
#
|
|
11
15
|
module ResultMerger
|
|
16
|
+
extend ResultsetRunIdentity
|
|
17
|
+
|
|
12
18
|
class << self
|
|
13
|
-
# The path to the .resultset.json cache file
|
|
14
19
|
def resultset_path
|
|
15
|
-
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
def resultset_writelock
|
|
19
|
-
File.join(SimpleCov.coverage_path, ".resultset.json.lock")
|
|
20
|
+
ResultsetStore.resultset_path
|
|
20
21
|
end
|
|
21
22
|
|
|
22
23
|
def merge_and_store(*file_paths, ignore_timeout: false)
|
|
@@ -26,88 +27,114 @@ module SimpleCov
|
|
|
26
27
|
end
|
|
27
28
|
|
|
28
29
|
def merge_results(*file_paths, ignore_timeout: false)
|
|
29
|
-
#
|
|
30
|
-
#
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
# For similar reasons a SimpleCov::Result is only created in the end as that'd create
|
|
36
|
-
# even more data especially when it also reads in all source files.
|
|
37
|
-
initial_memo = valid_results(file_paths.shift, ignore_timeout: ignore_timeout)
|
|
38
|
-
|
|
39
|
-
command_names, coverage = file_paths.reduce(initial_memo) do |memo, file_path|
|
|
40
|
-
merge_coverage(memo, valid_results(file_path, ignore_timeout: ignore_timeout))
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
create_result(command_names, coverage)
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
def valid_results(file_path, ignore_timeout: false)
|
|
47
|
-
results = parse_file(file_path)
|
|
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)
|
|
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)
|
|
54
36
|
end
|
|
55
37
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
+
)
|
|
63
69
|
end
|
|
64
70
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
JSON.parse(content) || {}
|
|
69
|
-
rescue StandardError
|
|
70
|
-
warn "[SimpleCov]: Warning! Parsing JSON content of resultset file failed"
|
|
71
|
-
{}
|
|
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)
|
|
72
74
|
end
|
|
73
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.
|
|
74
79
|
def merge_valid_results(results, ignore_timeout: false)
|
|
75
|
-
results = results
|
|
80
|
+
results = drop_expired_results(results) unless ignore_timeout
|
|
81
|
+
yield results if block_given?
|
|
76
82
|
|
|
77
83
|
command_plus_coverage = results.map do |command_name, data|
|
|
78
|
-
[[command_name],
|
|
84
|
+
[[command_name], LegacyFormatAdapter.call(data.fetch("coverage"))]
|
|
79
85
|
end
|
|
80
86
|
|
|
81
87
|
# one file itself _might_ include multiple test runs
|
|
82
88
|
merge_coverage(*command_plus_coverage)
|
|
83
89
|
end
|
|
84
90
|
|
|
91
|
+
def drop_expired_results(results)
|
|
92
|
+
fresh, expired = results.partition { |_command_name, data| within_merge_timeout?(data) }
|
|
93
|
+
return results if expired.empty?
|
|
94
|
+
|
|
95
|
+
warn_about_expired_results(expired.map(&:first))
|
|
96
|
+
fresh.to_h
|
|
97
|
+
end
|
|
98
|
+
|
|
85
99
|
def within_merge_timeout?(data)
|
|
86
|
-
|
|
100
|
+
(Time.now - Time.at(data.fetch("timestamp"))) < SimpleCov.merge_timeout
|
|
87
101
|
end
|
|
88
102
|
|
|
89
|
-
def
|
|
90
|
-
|
|
103
|
+
def warn_about_expired_results(expired_command_names)
|
|
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.
|
|
107
|
+
return unless SimpleCov.print_errors
|
|
108
|
+
|
|
109
|
+
warn "[SimpleCov]: Excluded #{expired_command_names.size} result(s) older than " \
|
|
110
|
+
"merge_timeout (#{SimpleCov.merge_timeout}s) from the merged report: " \
|
|
111
|
+
"#{expired_command_names.sort.join(', ')}. " \
|
|
112
|
+
"Increase SimpleCov.merge_timeout to include them."
|
|
91
113
|
end
|
|
92
114
|
|
|
93
|
-
def create_result(command_names, coverage)
|
|
115
|
+
def create_result(command_names, coverage, tracked_files: Set.new)
|
|
94
116
|
return nil unless coverage
|
|
95
117
|
|
|
96
118
|
command_name = command_names.reject(&:empty?).sort.join(", ")
|
|
97
|
-
|
|
119
|
+
coverage, injected = UnloadedFiles.inject(coverage, tracked_files)
|
|
120
|
+
# The merged result is the authoritative one users actually see, so
|
|
121
|
+
# it's the one that warns about source files dropped because they no
|
|
122
|
+
# longer exist on disk (issue #980). The per-process slices built in
|
|
123
|
+
# `process_coverage_result` stay quiet to avoid one warning per worker.
|
|
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
|
+
)
|
|
98
131
|
end
|
|
99
132
|
|
|
100
133
|
def merge_coverage(*results)
|
|
101
134
|
return [[""], nil] if results.empty?
|
|
102
135
|
return results.first if results.size == 1
|
|
103
136
|
|
|
104
|
-
|
|
105
|
-
# timestamp is dropped here, which is intentional (we merge it, it gets a new time stamp as of now)
|
|
106
|
-
merged_coverage = Combine.combine(Combine::ResultsCombiner, memo_coverage, coverage)
|
|
107
|
-
merged_command = memo_command + command
|
|
108
|
-
|
|
109
|
-
[merged_command, merged_coverage]
|
|
110
|
-
end
|
|
137
|
+
Combine::CoverageAccumulator.fold(results)
|
|
111
138
|
end
|
|
112
139
|
|
|
113
140
|
#
|
|
@@ -115,79 +142,47 @@ module SimpleCov
|
|
|
115
142
|
# SimpleCov::Result with merged coverage data and the command_name
|
|
116
143
|
# for the result consisting of a join on all source result's names
|
|
117
144
|
def merged_result
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
command_names, coverage = merge_valid_results(resultset_hash)
|
|
122
|
-
|
|
123
|
-
create_result(command_names, coverage)
|
|
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)
|
|
124
148
|
end
|
|
125
149
|
|
|
126
150
|
def read_resultset
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
read_file(resultset_path)
|
|
130
|
-
end
|
|
131
|
-
|
|
132
|
-
parse_json(resultset_content)
|
|
151
|
+
content = synchronize_resultset { ResultsetFile.read(resultset_path) }
|
|
152
|
+
ResultsetFile.decode(content)
|
|
133
153
|
end
|
|
134
154
|
|
|
135
155
|
# Saves the given SimpleCov::Result in the resultset cache
|
|
136
|
-
def store_result(result)
|
|
156
|
+
def store_result(result) # rubocop:disable Naming/PredicateMethod
|
|
137
157
|
synchronize_resultset do
|
|
138
158
|
# Ensure we have the latest, in case it was already cached
|
|
139
159
|
new_resultset = read_resultset
|
|
140
160
|
|
|
141
161
|
# A single result only ever has one command_name, see `SimpleCov::Result#to_hash`
|
|
142
162
|
command_name, data = result.to_hash.first
|
|
143
|
-
new_resultset[command_name] = data
|
|
144
|
-
File.open(resultset_path, "w+") do |f_|
|
|
145
|
-
f_.puts JSON.pretty_generate(new_resultset)
|
|
146
|
-
end
|
|
147
|
-
end
|
|
148
|
-
true
|
|
149
|
-
end
|
|
163
|
+
new_resultset[command_name] = merged_entry(new_resultset[command_name], data)
|
|
150
164
|
|
|
151
|
-
|
|
152
|
-
# given time
|
|
153
|
-
def synchronize_resultset
|
|
154
|
-
# make it reentrant
|
|
155
|
-
return yield if defined?(@resultset_locked) && @resultset_locked
|
|
156
|
-
|
|
157
|
-
begin
|
|
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
|
|
167
|
-
|
|
168
|
-
# We changed the format of the raw result data in simplecov, as people are likely
|
|
169
|
-
# to have "old" resultsets lying around (but not too old so that they're still
|
|
170
|
-
# considered we can adapt them).
|
|
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
|
|
165
|
+
ResultsetStore.write(new_resultset)
|
|
177
166
|
end
|
|
167
|
+
true
|
|
178
168
|
end
|
|
179
169
|
|
|
180
|
-
#
|
|
181
|
-
|
|
182
|
-
|
|
170
|
+
# If an entry with the same command_name was written AFTER our process
|
|
171
|
+
# started, a sibling test runner (typically a subprocess our parent
|
|
172
|
+
# process shelled out to) wrote it. Combine coverage data rather than
|
|
173
|
+
# overwriting, so an empty parent-process result doesn't clobber the
|
|
174
|
+
# subprocess's real data. See https://github.com/simplecov-ruby/simplecov/issues/581.
|
|
175
|
+
def merged_entry(existing, incoming)
|
|
176
|
+
return incoming unless concurrent_runner_entry?(existing, incoming)
|
|
183
177
|
|
|
184
|
-
|
|
178
|
+
merged = incoming.merge(
|
|
179
|
+
"coverage" => Combine::ResultsCombiner.combine(existing["coverage"], incoming["coverage"])
|
|
180
|
+
)
|
|
181
|
+
UnloadedFiles.carry_tracked(merged, existing, incoming)
|
|
185
182
|
end
|
|
186
183
|
|
|
187
|
-
def
|
|
188
|
-
|
|
189
|
-
{"lines" => line_coverage_data}
|
|
190
|
-
end
|
|
184
|
+
def synchronize_resultset(&)
|
|
185
|
+
ResultsetStore.synchronize(&)
|
|
191
186
|
end
|
|
192
187
|
end
|
|
193
188
|
end
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Result-building façade: turns the raw `Coverage.result` hash into a
|
|
4
|
+
# `SimpleCov::Result`, applies filters and groups, drives merging
|
|
5
|
+
# across test suites via `SimpleCov::ResultMerger`, and exposes the
|
|
6
|
+
# `collate` entry point for stitching disparate resultsets together.
|
|
7
|
+
module SimpleCov
|
|
8
|
+
class << self
|
|
9
|
+
#
|
|
10
|
+
# Collate a series of SimpleCov result files into a single SimpleCov output.
|
|
11
|
+
#
|
|
12
|
+
# See README for usage. By default `collate` ignores the merge_timeout
|
|
13
|
+
# so all results in all files specified will be merged. Pass
|
|
14
|
+
# `ignore_timeout: false` to honor it.
|
|
15
|
+
#
|
|
16
|
+
# `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, &)
|
|
30
|
+
raise ArgumentError, "There are no reports to be merged" if result_filenames.empty?
|
|
31
|
+
|
|
32
|
+
initial_setup(profile, &)
|
|
33
|
+
|
|
34
|
+
# Use the ResultMerger to produce a single, merged result, ready to use.
|
|
35
|
+
@result = ParallelResultMerger.merge_and_store(*result_filenames, processes: [1, processes].max,
|
|
36
|
+
ignore_timeout: ignore_timeout)
|
|
37
|
+
|
|
38
|
+
@collating_result = true
|
|
39
|
+
run_exit_tasks!
|
|
40
|
+
ensure
|
|
41
|
+
@collating_result = false
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
#
|
|
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.
|
|
48
|
+
#
|
|
49
|
+
def result
|
|
50
|
+
return @result if result?
|
|
51
|
+
|
|
52
|
+
use_merging = merging
|
|
53
|
+
|
|
54
|
+
collect_own_coverage(standalone: !use_merging)
|
|
55
|
+
|
|
56
|
+
# If we're using merging of results, store the current result
|
|
57
|
+
# first (if there is one), then merge the results and return those
|
|
58
|
+
if use_merging
|
|
59
|
+
SimpleCov::ResultMerger.store_result(@result) if result?
|
|
60
|
+
return @result unless merge_finalization_owner?
|
|
61
|
+
|
|
62
|
+
wait_for_other_processes
|
|
63
|
+
@result = SimpleCov::ResultMerger.merged_result
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
@result
|
|
67
|
+
end
|
|
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
|
+
|
|
79
|
+
# Returns nil if the result has not been computed, otherwise the result.
|
|
80
|
+
def result?
|
|
81
|
+
defined?(@result) && @result
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# @api private — true while `SimpleCov.collate` is running its finalizer.
|
|
85
|
+
def collating_result?
|
|
86
|
+
defined?(@collating_result) && @collating_result
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Applies the configured filters to the given array of SimpleCov::SourceFile items
|
|
90
|
+
def filtered(files)
|
|
91
|
+
result = files.to_a.dup
|
|
92
|
+
filters.each do |filter|
|
|
93
|
+
result = result.reject { |source_file| filter.matches?(source_file) }
|
|
94
|
+
end
|
|
95
|
+
SimpleCov::FileList.new result
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Bin the given source files by group filter. `groups:` defaults to
|
|
99
|
+
# `SimpleCov.groups`; pass a Hash explicitly to bin against a
|
|
100
|
+
# different group config (e.g., the snapshot a Result captured at
|
|
101
|
+
# construction). Files matched by no group fall into the implicit
|
|
102
|
+
# "Ungrouped" bucket.
|
|
103
|
+
def grouped(files, groups: SimpleCov.groups)
|
|
104
|
+
return {} if GroupNames.validate!(groups.keys).empty?
|
|
105
|
+
|
|
106
|
+
grouped = groups.transform_values do |filter|
|
|
107
|
+
SimpleCov::FileList.new(files.select { |source_file| filter.matches?(source_file) })
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
in_group = grouped_file_set(grouped)
|
|
111
|
+
ungrouped = files.reject { |source_file| in_group.include?(source_file) }
|
|
112
|
+
grouped[GroupNames::UNGROUPED] = SimpleCov::FileList.new(ungrouped) if ungrouped.any?
|
|
113
|
+
|
|
114
|
+
grouped
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Applies the profile of given name on SimpleCov configuration
|
|
118
|
+
def load_profile(name)
|
|
119
|
+
profiles.load(name)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Clear out the previously cached .result. Primarily useful in testing.
|
|
123
|
+
def clear_result
|
|
124
|
+
@result = nil
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# @api private — persist the per-criterion coverage percentages
|
|
128
|
+
# rounded down (see #679) so the next run can compute drift.
|
|
129
|
+
def write_last_run(result)
|
|
130
|
+
SimpleCov::LastRun.write(
|
|
131
|
+
result: result.coverage_statistics.transform_values { |stats| round_coverage(stats.percent) }
|
|
132
|
+
)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# @api private — round down to two decimals to be extra strict.
|
|
136
|
+
def round_coverage(coverage)
|
|
137
|
+
coverage.floor(2)
|
|
138
|
+
end
|
|
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
|
+
|
|
163
|
+
private
|
|
164
|
+
|
|
165
|
+
def initial_setup(profile, &block)
|
|
166
|
+
load_profile(profile) if profile
|
|
167
|
+
configure(&block) if block
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def grouped_file_set(grouped)
|
|
171
|
+
grouped.values.each_with_object(Set.new) { |file_list, set| set.merge(file_list) }
|
|
172
|
+
end
|
|
173
|
+
|
|
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
|
+
)
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# Globs to expand on disk when injecting unloaded files into the
|
|
186
|
+
# result. Combines the legacy `track_files` glob (additive only)
|
|
187
|
+
# with every string glob declared via `cover` (also restrictive,
|
|
188
|
+
# but the restriction lives in `Result#apply_cover_filters!`).
|
|
189
|
+
def unloaded_file_discovery_globs
|
|
190
|
+
[tracked_files, *cover_globs].compact
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# Run all the steps that handle processing the raw coverage result.
|
|
194
|
+
# `report:` is true only when this slice is the final result (merging
|
|
195
|
+
# off); with merging on the merged result reports dropped source files,
|
|
196
|
+
# so the per-process slice stays quiet to avoid one warning per worker.
|
|
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)
|
|
203
|
+
raw = SimpleCov::UselessResultsRemover.call(Coverage.result)
|
|
204
|
+
adapted = SimpleCov::ResultAdapter.call(raw)
|
|
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
|
+
)
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
end
|