simplecov 1.0.3 → 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 -1439
- 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 +36 -12
- 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 +6 -3
- 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 +5 -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_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 +6 -2
- 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 +200 -66
- 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
|
@@ -5,9 +5,10 @@ require "json"
|
|
|
5
5
|
module SimpleCov
|
|
6
6
|
module ResultMerger
|
|
7
7
|
# Read + parse a `.resultset.json` file with the same tolerance the
|
|
8
|
-
# historical `ResultMerger` had: missing
|
|
9
|
-
#
|
|
10
|
-
# the decoded Hash
|
|
8
|
+
# historical `ResultMerger` had: a missing or empty file quietly
|
|
9
|
+
# returns `{}`, an unparseable one warns and returns `{}`, and parse
|
|
10
|
+
# success returns the decoded Hash with any malformed entries
|
|
11
|
+
# warned about and dropped.
|
|
11
12
|
module ResultsetFile
|
|
12
13
|
module_function
|
|
13
14
|
|
|
@@ -16,20 +17,55 @@ module SimpleCov
|
|
|
16
17
|
decode(data)
|
|
17
18
|
end
|
|
18
19
|
|
|
20
|
+
# A missing or blank file quietly means "no results yet"; anything
|
|
21
|
+
# else — a 1-byte truncation included — flows through decode so
|
|
22
|
+
# corruption warns instead of silently vanishing (the old
|
|
23
|
+
# `length < 2` check swallowed exactly those). Read first and
|
|
24
|
+
# rescue rather than check exist? (racy against a concurrent
|
|
25
|
+
# clean).
|
|
19
26
|
def read(path)
|
|
20
|
-
return unless File.exist?(path)
|
|
21
|
-
|
|
22
27
|
data = File.read(path)
|
|
23
|
-
return if data.
|
|
28
|
+
return if data.match?(/\A\s*\z/)
|
|
24
29
|
|
|
25
30
|
data
|
|
31
|
+
rescue Errno::ENOENT
|
|
32
|
+
nil
|
|
26
33
|
end
|
|
27
34
|
|
|
28
35
|
def decode(content)
|
|
29
36
|
return {} unless content
|
|
30
37
|
|
|
31
|
-
JSON.parse(content)
|
|
38
|
+
parsed = JSON.parse(content)
|
|
39
|
+
return {} if parsed.nil? # a "null" file has long meant an empty resultset
|
|
40
|
+
|
|
41
|
+
# Valid JSON need not be an object ("[1,2]" and "\"x\"" parse
|
|
42
|
+
# fine), but everything downstream iterates command => data
|
|
43
|
+
# pairs, so anything else would crash out of the middle of a
|
|
44
|
+
# merge. Same tolerance as a parse failure: warn and move on.
|
|
45
|
+
parsed.is_a?(Hash) ? drop_malformed_entries(parsed) : invalid_resultset
|
|
32
46
|
rescue StandardError
|
|
47
|
+
invalid_resultset
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Each surviving entry must have the shape every consumer relies
|
|
51
|
+
# on: a Hash carrying a Numeric "timestamp" (the merge-timeout
|
|
52
|
+
# check) and a Hash "coverage" (the merge fold). A truncated or
|
|
53
|
+
# hand-edited entry would otherwise crash out of the middle of an
|
|
54
|
+
# at_exit merge, so it gets the same warn-and-move-on treatment
|
|
55
|
+
# as an unparseable file.
|
|
56
|
+
def drop_malformed_entries(resultset)
|
|
57
|
+
malformed, valid = resultset.partition { |_command_name, data| !well_formed_entry?(data) }
|
|
58
|
+
return resultset if malformed.empty?
|
|
59
|
+
|
|
60
|
+
warn "[SimpleCov]: Warning! Ignoring malformed resultset entries: #{malformed.map(&:first).sort.join(', ')}"
|
|
61
|
+
valid.to_h
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def well_formed_entry?(data)
|
|
65
|
+
data.is_a?(Hash) && data["timestamp"].is_a?(Numeric) && data["coverage"].is_a?(Hash)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def invalid_resultset
|
|
33
69
|
warn "[SimpleCov]: Warning! Parsing JSON content of resultset file failed"
|
|
34
70
|
{}
|
|
35
71
|
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SimpleCov
|
|
4
|
+
module ResultMerger
|
|
5
|
+
# Run/worker metadata queries used by resultset storage and parallel
|
|
6
|
+
# coordination. Kept separate from the coverage-folding implementation.
|
|
7
|
+
module ResultsetRunIdentity
|
|
8
|
+
# One namespaced identity per distinct worker that wrote to the
|
|
9
|
+
# current run: `[:worker, id]` for entries that carry a worker id,
|
|
10
|
+
# `[:legacy, command_name]` for fresh entries written without one
|
|
11
|
+
# (an old SimpleCov writing to the same resultset). Pairs rather
|
|
12
|
+
# than mangled strings, so a resultset mixing both kinds can never
|
|
13
|
+
# alias a real worker id to a synthesized legacy identity.
|
|
14
|
+
def worker_identities_for_run(results, run_id, started_at)
|
|
15
|
+
results.filter_map do |command_name, data|
|
|
16
|
+
next unless current_run_entry?(data, run_id, started_at)
|
|
17
|
+
|
|
18
|
+
worker_id = data["worker_id"]
|
|
19
|
+
worker_id.to_s.empty? ? [:legacy, command_name] : [:worker, worker_id.to_s]
|
|
20
|
+
end.uniq
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def current_run_entry?(entry, run_id, started_at)
|
|
24
|
+
return false unless entry.is_a?(Hash)
|
|
25
|
+
|
|
26
|
+
entry_run_id = entry["run_id"]
|
|
27
|
+
return fresh_entry?(entry, started_at) unless entry_run_id
|
|
28
|
+
return false unless run_id && entry_run_id == run_id
|
|
29
|
+
return true if SimpleCov::RunIdentity.authoritative?
|
|
30
|
+
|
|
31
|
+
fresh_entry?(entry, started_at)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def fresh_entry?(entry, started_at)
|
|
35
|
+
timestamp = entry["timestamp"]
|
|
36
|
+
timestamp.is_a?(Numeric) && started_at && timestamp >= started_at.to_f
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Whether `entry` was written by a runner concurrent with this
|
|
40
|
+
# process: either it shares our run id (`current_run_entry?`), or it
|
|
41
|
+
# was written strictly after our process started. A mismatched run
|
|
42
|
+
# id must not defeat the timestamp check — a subprocess we shelled
|
|
43
|
+
# out to (fork+exec) generates its own random run id, and its
|
|
44
|
+
# freshly written entry is exactly the data issue #581 protects
|
|
45
|
+
# from being clobbered. Strict id matching is reserved for worker
|
|
46
|
+
# counting (`worker_identities_for_run`), where admitting a stale
|
|
47
|
+
# entry would end a sibling wait early.
|
|
48
|
+
def concurrent_runner_entry?(entry, incoming = nil)
|
|
49
|
+
return false unless entry.is_a?(Hash)
|
|
50
|
+
|
|
51
|
+
incoming_run_id = incoming["run_id"] if incoming.is_a?(Hash)
|
|
52
|
+
started_at = SimpleCov.process_start_time
|
|
53
|
+
return true if current_run_entry?(entry, incoming_run_id, started_at)
|
|
54
|
+
|
|
55
|
+
written_after_start?(entry, started_at)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Strictly after, unlike `fresh_entry?`: an entry stamped at the
|
|
59
|
+
# exact instant we started is treated as leftover from a previous
|
|
60
|
+
# run, so a same-second stale entry is still overwritten.
|
|
61
|
+
def written_after_start?(entry, started_at)
|
|
62
|
+
timestamp = entry["timestamp"]
|
|
63
|
+
timestamp.is_a?(Numeric) && started_at && timestamp > started_at.to_f
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
require "fileutils"
|
|
4
4
|
require "json"
|
|
5
|
+
require "monitor"
|
|
6
|
+
require_relative "../atomic_file"
|
|
5
7
|
|
|
6
8
|
module SimpleCov
|
|
7
9
|
module ResultMerger
|
|
@@ -9,6 +11,9 @@ module SimpleCov
|
|
|
9
11
|
# file-lock synchronization between processes and atomic temp-file
|
|
10
12
|
# renames so concurrent readers don't observe a truncated file.
|
|
11
13
|
module ResultsetStore
|
|
14
|
+
LOCK_MONITOR = Monitor.new
|
|
15
|
+
private_constant :LOCK_MONITOR
|
|
16
|
+
|
|
12
17
|
module_function
|
|
13
18
|
|
|
14
19
|
def resultset_path
|
|
@@ -19,23 +24,21 @@ module SimpleCov
|
|
|
19
24
|
File.join(SimpleCov.coverage_path, ".resultset.json.lock")
|
|
20
25
|
end
|
|
21
26
|
|
|
27
|
+
# Compact JSON, not pretty-printed: this is a machine-read cache
|
|
28
|
+
# that every parallel worker rewrites wholesale, and on a large
|
|
29
|
+
# project pretty printing nearly doubles the bytes written, read
|
|
30
|
+
# back, and parsed on each of those store-merge round trips.
|
|
22
31
|
def write(resultset)
|
|
23
|
-
|
|
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)
|
|
32
|
+
AtomicFile.write(resultset_path, "#{JSON.generate(resultset)}\n")
|
|
27
33
|
end
|
|
28
34
|
|
|
29
|
-
#
|
|
30
|
-
#
|
|
31
|
-
#
|
|
35
|
+
# Serialize threads before taking the process-wide file lock. Nested
|
|
36
|
+
# calls by the owning thread bypass flock so they cannot deadlock on a
|
|
37
|
+
# second descriptor for the same lock file.
|
|
32
38
|
def synchronize(&)
|
|
33
|
-
return yield if
|
|
39
|
+
return yield if LOCK_MONITOR.mon_owned?
|
|
34
40
|
|
|
35
|
-
|
|
36
|
-
with_flock(&)
|
|
37
|
-
ensure
|
|
38
|
-
@locked = false
|
|
41
|
+
LOCK_MONITOR.synchronize { with_flock(&) }
|
|
39
42
|
end
|
|
40
43
|
|
|
41
44
|
def with_flock
|
|
@@ -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
|
|
@@ -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(&)
|