simplecov 1.1.0 → 1.2.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 +165 -57
- data/exe/simplecov +0 -3
- data/lib/minitest/simplecov_plugin.rb +12 -9
- data/lib/simplecov/atomic_file.rb +16 -19
- data/lib/simplecov/autostart.rb +5 -7
- data/lib/simplecov/baseline/parser.rb +58 -0
- data/lib/simplecov/baseline.rb +170 -0
- data/lib/simplecov/cli/affected/changed_files.rb +57 -0
- data/lib/simplecov/cli/affected/selection.rb +117 -0
- data/lib/simplecov/cli/affected.rb +148 -0
- data/lib/simplecov/cli/badge/svg.rb +70 -0
- data/lib/simplecov/cli/badge.rb +62 -0
- data/lib/simplecov/cli/clean.rb +23 -24
- data/lib/simplecov/cli/command_helpers.rb +55 -19
- data/lib/simplecov/cli/completions/scripts.rb +114 -0
- data/lib/simplecov/cli/completions.rb +83 -0
- data/lib/simplecov/cli/coverage.rb +25 -28
- data/lib/simplecov/cli/coverage_file.rb +58 -14
- data/lib/simplecov/cli/dead_code/output.rb +100 -0
- data/lib/simplecov/cli/dead_code.rb +139 -0
- data/lib/simplecov/cli/diff/output.rb +45 -0
- data/lib/simplecov/cli/diff.rb +35 -67
- data/lib/simplecov/cli/dotfile.rb +83 -38
- data/lib/simplecov/cli/git.rb +41 -0
- data/lib/simplecov/cli/history/output.rb +135 -0
- data/lib/simplecov/cli/history.rb +67 -0
- data/lib/simplecov/cli/merge.rb +34 -39
- data/lib/simplecov/cli/open.rb +12 -15
- data/lib/simplecov/cli/patch/changed_lines.rb +149 -0
- data/lib/simplecov/cli/patch/output.rb +135 -0
- data/lib/simplecov/cli/patch.rb +169 -0
- data/lib/simplecov/cli/ratchet/output.rb +44 -0
- data/lib/simplecov/cli/ratchet.rb +83 -0
- data/lib/simplecov/cli/report.rb +18 -24
- data/lib/simplecov/cli/run.rb +8 -9
- data/lib/simplecov/cli/serve/report_preparer.rb +7 -6
- data/lib/simplecov/cli/serve/static_file_handler.rb +62 -56
- data/lib/simplecov/cli/serve.rb +27 -24
- data/lib/simplecov/cli/show/annotator.rb +87 -0
- data/lib/simplecov/cli/show/sweep.rb +36 -0
- data/lib/simplecov/cli/show.rb +133 -0
- data/lib/simplecov/cli/status/facts.rb +87 -0
- data/lib/simplecov/cli/status.rb +93 -0
- data/lib/simplecov/cli/tests/redundancy.rb +78 -0
- data/lib/simplecov/cli/tests.rb +148 -0
- data/lib/simplecov/cli/uncovered/misses.rb +37 -0
- data/lib/simplecov/cli/uncovered.rb +70 -32
- data/lib/simplecov/cli/usage.rb +152 -0
- data/lib/simplecov/cli/watch/live_report.rb +73 -0
- data/lib/simplecov/cli/watch/narrator.rb +55 -0
- data/lib/simplecov/cli/watch/poller.rb +45 -0
- data/lib/simplecov/cli/watch/session.rb +134 -0
- data/lib/simplecov/cli/watch/test_plan.rb +43 -0
- data/lib/simplecov/cli/watch.rb +85 -0
- data/lib/simplecov/cli.rb +43 -92
- data/lib/simplecov/color.rb +21 -35
- data/lib/simplecov/combine/branches_combiner.rb +7 -31
- data/lib/simplecov/combine/coverage_accumulator.rb +49 -142
- data/lib/simplecov/combine/identity_interner.rb +10 -13
- data/lib/simplecov/combine/interned_counts.rb +7 -9
- data/lib/simplecov/combine/lines_combiner.rb +20 -44
- data/lib/simplecov/combine/methods_combiner.rb +9 -35
- data/lib/simplecov/combine/results_combiner.rb +4 -21
- data/lib/simplecov/combine.rb +0 -6
- data/lib/simplecov/command_guesser.rb +20 -34
- data/lib/simplecov/configuration/baseline.rb +31 -0
- data/lib/simplecov/configuration/coverage.rb +91 -79
- data/lib/simplecov/configuration/coverage_criteria.rb +43 -54
- data/lib/simplecov/configuration/deprecations.rb +21 -0
- data/lib/simplecov/configuration/eval_coverage.rb +6 -10
- data/lib/simplecov/configuration/filters.rb +40 -73
- data/lib/simplecov/configuration/formatting.rb +74 -83
- data/lib/simplecov/configuration/groups.rb +19 -13
- data/lib/simplecov/configuration/history.rb +44 -0
- data/lib/simplecov/configuration/ignored_entries.rb +31 -22
- data/lib/simplecov/configuration/merging.rb +94 -62
- data/lib/simplecov/configuration/missed_caps.rb +51 -0
- data/lib/simplecov/configuration/production.rb +23 -0
- data/lib/simplecov/configuration/test_tracking.rb +48 -0
- data/lib/simplecov/configuration/thresholds.rb +33 -62
- data/lib/simplecov/configuration/view_coverage.rb +38 -0
- data/lib/simplecov/configuration.rb +70 -76
- data/lib/simplecov/configuration_error.rb +5 -0
- data/lib/simplecov/context_map/union.rb +81 -0
- data/lib/simplecov/context_map.rb +187 -0
- data/lib/simplecov/coverage_json.rb +2 -5
- data/lib/simplecov/coverage_statistics.rb +14 -24
- data/lib/simplecov/coverage_violations.rb +157 -64
- data/lib/simplecov/current_run.rb +61 -0
- data/lib/simplecov/defaults.rb +4 -23
- data/lib/simplecov/deprecation.rb +45 -27
- data/lib/simplecov/directive.rb +46 -62
- data/lib/simplecov/exit_codes/baseline_check.rb +46 -0
- data/lib/simplecov/exit_codes/check.rb +17 -11
- data/lib/simplecov/exit_codes/exit_code_handling.rb +21 -8
- data/lib/simplecov/exit_codes/maximum_coverage_drop_check.rb +7 -10
- data/lib/simplecov/exit_codes/maximum_missed_check.rb +26 -0
- data/lib/simplecov/exit_codes/maximum_missed_per_file_check.rb +34 -0
- data/lib/simplecov/exit_codes/maximum_overall_coverage_check.rb +7 -12
- data/lib/simplecov/exit_codes/minimum_coverage_by_file_check.rb +11 -10
- data/lib/simplecov/exit_codes/minimum_coverage_by_group_check.rb +7 -9
- data/lib/simplecov/exit_codes/minimum_overall_coverage_check.rb +16 -17
- data/lib/simplecov/exit_codes.rb +17 -7
- data/lib/simplecov/exit_handling.rb +94 -59
- data/lib/simplecov/file_list.rb +20 -51
- data/lib/simplecov/filter.rb +43 -66
- data/lib/simplecov/formatter/base.rb +38 -51
- data/lib/simplecov/formatter/baseline_formatter.rb +98 -0
- data/lib/simplecov/formatter/coverage_json_writer.rb +29 -31
- data/lib/simplecov/formatter/html_formatter/public/index.html +17 -10
- data/lib/simplecov/formatter/html_formatter/viewer_data_validator.rb +74 -17
- data/lib/simplecov/formatter/html_formatter.rb +24 -41
- data/lib/simplecov/formatter/json_formatter/errors_formatter.rb +50 -16
- data/lib/simplecov/formatter/json_formatter/production_section_formatter.rb +48 -0
- data/lib/simplecov/formatter/json_formatter/result_hash_formatter.rb +43 -29
- data/lib/simplecov/formatter/json_formatter/source_file_formatter.rb +14 -10
- data/lib/simplecov/formatter/json_formatter.rb +7 -13
- data/lib/simplecov/formatter/multi_formatter.rb +5 -11
- data/lib/simplecov/formatter/simple_formatter.rb +2 -8
- data/lib/simplecov/formatter.rb +14 -10
- data/lib/simplecov/group_names.rb +13 -12
- data/lib/simplecov/history.rb +115 -0
- data/lib/simplecov/last_run.rb +6 -9
- data/lib/simplecov/lines_classifier.rb +31 -16
- data/lib/simplecov/load_global_config.rb +7 -7
- data/lib/simplecov/parallel_adapters/base.rb +13 -41
- data/lib/simplecov/parallel_adapters/generic.rb +7 -18
- data/lib/simplecov/parallel_adapters/parallel_tests.rb +21 -42
- data/lib/simplecov/parallel_adapters.rb +24 -53
- data/lib/simplecov/parallel_coordination.rb +39 -49
- data/lib/simplecov/parallel_result_merger/worker_payload.rb +33 -0
- data/lib/simplecov/parallel_result_merger.rb +67 -126
- data/lib/simplecov/process.rb +14 -32
- data/lib/simplecov/production/error.rb +11 -0
- data/lib/simplecov/production/file_sink.rb +130 -0
- data/lib/simplecov/production.rb +249 -0
- data/lib/simplecov/profiles/rails.rb +1 -12
- data/lib/simplecov/profiles/root_filter.rb +0 -4
- data/lib/simplecov/profiles/strict.rb +9 -19
- data/lib/simplecov/profiles.rb +11 -33
- data/lib/simplecov/report_deferral.rb +11 -15
- data/lib/simplecov/report_stamp.rb +6 -9
- data/lib/simplecov/result/filter_config.rb +19 -0
- data/lib/simplecov/result/missing_source_files_reporter.rb +8 -9
- data/lib/simplecov/result/serialization.rb +43 -0
- data/lib/simplecov/result/source_file_builder.rb +7 -12
- data/lib/simplecov/result.rb +68 -117
- data/lib/simplecov/result_adapter.rb +58 -68
- data/lib/simplecov/result_merger/contexts.rb +22 -0
- data/lib/simplecov/result_merger/legacy_format_adapter.rb +4 -8
- data/lib/simplecov/result_merger/resultset_file.rb +20 -27
- data/lib/simplecov/result_merger/resultset_run_identity.rb +23 -25
- data/lib/simplecov/result_merger/resultset_store.rb +23 -14
- data/lib/simplecov/result_merger/unloaded_files.rb +24 -32
- data/lib/simplecov/result_merger.rb +59 -58
- data/lib/simplecov/result_processing.rb +74 -119
- data/lib/simplecov/run_identity.rb +16 -18
- data/lib/simplecov/simulate_coverage.rb +35 -50
- data/lib/simplecov/source_file/branch.rb +8 -33
- data/lib/simplecov/source_file/branch_builder.rb +28 -47
- data/lib/simplecov/source_file/builder_context.rb +6 -13
- data/lib/simplecov/source_file/line.rb +13 -33
- data/lib/simplecov/source_file/line_builder.rb +9 -15
- data/lib/simplecov/source_file/method.rb +3 -7
- data/lib/simplecov/source_file/method_builder.rb +6 -15
- data/lib/simplecov/source_file/ruby_data_parser.rb +59 -66
- data/lib/simplecov/source_file/skip_chunks.rb +19 -28
- data/lib/simplecov/source_file/source_loader.rb +45 -35
- data/lib/simplecov/source_file/statistics.rb +10 -9
- data/lib/simplecov/source_file.rb +21 -51
- data/lib/simplecov/static_coverage_extractor/condition_folding.rb +63 -114
- data/lib/simplecov/static_coverage_extractor/location_conventions.rb +76 -121
- data/lib/simplecov/static_coverage_extractor/method_collector.rb +11 -24
- data/lib/simplecov/static_coverage_extractor/prism_compat.rb +15 -24
- data/lib/simplecov/static_coverage_extractor/value_position.rb +25 -38
- data/lib/simplecov/static_coverage_extractor/visitor.rb +47 -64
- data/lib/simplecov/static_coverage_extractor.rb +46 -62
- data/lib/simplecov/test_tracker/accessors.rb +34 -0
- data/lib/simplecov/test_tracker/constant_watch.rb +49 -0
- data/lib/simplecov/test_tracker/delta.rb +69 -0
- data/lib/simplecov/test_tracker/framework_hooks.rb +130 -0
- data/lib/simplecov/test_tracker.rb +162 -0
- data/lib/simplecov/unloaded_file_injector.rb +18 -31
- data/lib/simplecov/useless_results_remover.rb +7 -6
- data/lib/simplecov/version.rb +1 -1
- data/lib/simplecov/view_coverage/template_compiler.rb +83 -0
- data/lib/simplecov/view_coverage.rb +55 -0
- data/lib/simplecov-html.rb +1 -0
- data/lib/simplecov.rb +59 -114
- data/lib/simplecov_json_formatter.rb +1 -0
- data/man/simplecov.1 +349 -0
- data/sig/simplecov.rbs +523 -595
- metadata +72 -7
- data/schemas/coverage-v1.0.schema.json +0 -306
- data/schemas/coverage.schema.json +0 -306
|
@@ -2,22 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
module SimpleCov
|
|
4
4
|
module ResultMerger
|
|
5
|
-
# We changed the format of the raw result data in simplecov, as people
|
|
6
|
-
# are likely to have "old" resultsets lying around (but not too old so
|
|
7
|
-
# that they're still considered we can adapt them). See
|
|
8
|
-
# https://github.com/simplecov-ruby/simplecov/pull/824#issuecomment-576049747
|
|
9
5
|
module LegacyFormatAdapter
|
|
10
|
-
|
|
6
|
+
extend self
|
|
11
7
|
|
|
12
8
|
def call(result)
|
|
13
9
|
pre_0_18?(result) ? upgrade(result) : result
|
|
14
10
|
end
|
|
15
11
|
|
|
16
|
-
# Pre-0.18 coverage data pointed from file directly to an array of
|
|
17
|
-
#
|
|
12
|
+
# Pre-0.18 coverage data pointed from file directly to an array of line
|
|
13
|
+
# coverage rather than a `{"lines" => [...]}` hash.
|
|
18
14
|
def pre_0_18?(result)
|
|
19
15
|
_key, data = result.first
|
|
20
|
-
data.
|
|
16
|
+
data.instance_of?(Array)
|
|
21
17
|
end
|
|
22
18
|
|
|
23
19
|
def upgrade(result)
|
|
@@ -4,25 +4,22 @@ require "json"
|
|
|
4
4
|
|
|
5
5
|
module SimpleCov
|
|
6
6
|
module ResultMerger
|
|
7
|
-
# Read
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
# warned about and dropped.
|
|
7
|
+
# Read and parse a `.resultset.json` with the same tolerance the historical
|
|
8
|
+
# `ResultMerger` had: a missing or empty file quietly returns `{}`, an
|
|
9
|
+
# unparseable one warns and returns `{}`, and malformed entries inside a
|
|
10
|
+
# valid file are warned about and dropped.
|
|
12
11
|
module ResultsetFile
|
|
13
|
-
|
|
12
|
+
extend self
|
|
14
13
|
|
|
15
14
|
def parse(path)
|
|
16
15
|
data = read(path)
|
|
17
16
|
decode(data)
|
|
18
17
|
end
|
|
19
18
|
|
|
20
|
-
# A missing or blank file quietly means "no results yet"; anything
|
|
21
|
-
#
|
|
22
|
-
#
|
|
23
|
-
#
|
|
24
|
-
# rescue rather than check exist? (racy against a concurrent
|
|
25
|
-
# clean).
|
|
19
|
+
# A missing or blank file quietly means "no results yet"; anything else, a
|
|
20
|
+
# 1-byte truncation included, flows through decode so corruption warns
|
|
21
|
+
# instead of silently vanishing. Read first and rescue rather than check
|
|
22
|
+
# exist?, which is racy against a concurrent clean.
|
|
26
23
|
def read(path)
|
|
27
24
|
data = File.read(path)
|
|
28
25
|
return if data.match?(/\A\s*\z/)
|
|
@@ -32,37 +29,33 @@ module SimpleCov
|
|
|
32
29
|
nil
|
|
33
30
|
end
|
|
34
31
|
|
|
32
|
+
# Valid JSON need not be an object, but everything downstream iterates
|
|
33
|
+
# command => data pairs, so anything else gets the same tolerance as a parse
|
|
34
|
+
# failure. A "null" file has long meant an empty resultset.
|
|
35
35
|
def decode(content)
|
|
36
36
|
return {} unless content
|
|
37
37
|
|
|
38
38
|
parsed = JSON.parse(content)
|
|
39
|
-
return {} if parsed.nil?
|
|
39
|
+
return {} if parsed.nil?
|
|
40
40
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
# pairs, so anything else would crash out of the middle of a
|
|
44
|
-
# merge. Same tolerance as a parse failure: warn and move on.
|
|
45
|
-
parsed.is_a?(Hash) ? drop_malformed_entries(parsed) : invalid_resultset
|
|
46
|
-
rescue StandardError
|
|
41
|
+
parsed.instance_of?(Hash) ? drop_malformed_entries(parsed) : invalid_resultset
|
|
42
|
+
rescue
|
|
47
43
|
invalid_resultset
|
|
48
44
|
end
|
|
49
45
|
|
|
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
46
|
def drop_malformed_entries(resultset)
|
|
57
47
|
malformed, valid = resultset.partition { |_command_name, data| !well_formed_entry?(data) }
|
|
58
48
|
return resultset if malformed.empty?
|
|
59
49
|
|
|
60
|
-
warn "[SimpleCov]: Warning! Ignoring malformed resultset entries: #{malformed.map(&:first).sort.join(
|
|
50
|
+
warn "[SimpleCov]: Warning! Ignoring malformed resultset entries: #{malformed.map(&:first).sort.join(", ")}"
|
|
61
51
|
valid.to_h
|
|
62
52
|
end
|
|
63
53
|
|
|
54
|
+
# Every consumer relies on a Hash carrying a Numeric "timestamp" and a Hash
|
|
55
|
+
# "coverage". A truncated or hand-edited entry would otherwise crash out of
|
|
56
|
+
# the middle of an at_exit merge.
|
|
64
57
|
def well_formed_entry?(data)
|
|
65
|
-
data.
|
|
58
|
+
data.instance_of?(Hash) && data["timestamp"].is_a?(Numeric) && data["coverage"].instance_of?(Hash)
|
|
66
59
|
end
|
|
67
60
|
|
|
68
61
|
def invalid_resultset
|
|
@@ -2,15 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
module SimpleCov
|
|
4
4
|
module ResultMerger
|
|
5
|
-
# Run
|
|
6
|
-
# coordination.
|
|
5
|
+
# Run and worker metadata queries used by resultset storage and parallel
|
|
6
|
+
# coordination.
|
|
7
7
|
module ResultsetRunIdentity
|
|
8
|
-
# One namespaced identity per distinct worker that wrote to the
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
# alias a real worker id to a synthesized legacy identity.
|
|
8
|
+
# One namespaced identity per distinct worker that wrote to the current run:
|
|
9
|
+
# `[:worker, id]` for entries carrying a worker id, `[:legacy, command_name]`
|
|
10
|
+
# for fresh entries written without one. Pairs rather than mangled strings,
|
|
11
|
+
# so a resultset mixing both kinds can never alias a real worker id to a
|
|
12
|
+
# synthesized legacy identity.
|
|
14
13
|
def worker_identities_for_run(results, run_id, started_at)
|
|
15
14
|
results.filter_map do |command_name, data|
|
|
16
15
|
next unless current_run_entry?(data, run_id, started_at)
|
|
@@ -20,13 +19,16 @@ module SimpleCov
|
|
|
20
19
|
end.uniq
|
|
21
20
|
end
|
|
22
21
|
|
|
22
|
+
# `eql?` rather than `==`: run ids are strings, and value equality already
|
|
23
|
+
# answers false for the nil run id a caller with no identity of its own
|
|
24
|
+
# passes.
|
|
23
25
|
def current_run_entry?(entry, run_id, started_at)
|
|
24
|
-
return false unless entry.
|
|
26
|
+
return false unless entry.instance_of?(Hash)
|
|
25
27
|
|
|
26
28
|
entry_run_id = entry["run_id"]
|
|
27
29
|
return fresh_entry?(entry, started_at) unless entry_run_id
|
|
28
|
-
return false unless
|
|
29
|
-
return true if
|
|
30
|
+
return false unless entry_run_id.eql?(run_id)
|
|
31
|
+
return true if RunIdentity.authoritative?
|
|
30
32
|
|
|
31
33
|
fresh_entry?(entry, started_at)
|
|
32
34
|
end
|
|
@@ -36,28 +38,24 @@ module SimpleCov
|
|
|
36
38
|
timestamp.is_a?(Numeric) && started_at && timestamp >= started_at.to_f
|
|
37
39
|
end
|
|
38
40
|
|
|
39
|
-
#
|
|
40
|
-
# process
|
|
41
|
-
#
|
|
42
|
-
#
|
|
43
|
-
#
|
|
44
|
-
#
|
|
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.
|
|
41
|
+
# Either the entry shares our run id, or it was written strictly after our
|
|
42
|
+
# process started. A mismatched run id must not defeat the timestamp check: a
|
|
43
|
+
# subprocess we shelled out to generates its own random run id, and its
|
|
44
|
+
# freshly written entry is exactly the data #581 protects from being
|
|
45
|
+
# clobbered. Strict id matching is reserved for worker counting, where
|
|
46
|
+
# admitting a stale entry would end a sibling wait early.
|
|
48
47
|
def concurrent_runner_entry?(entry, incoming = nil)
|
|
49
|
-
return false unless entry.
|
|
48
|
+
return false unless entry.instance_of?(Hash)
|
|
50
49
|
|
|
51
|
-
incoming_run_id = incoming["run_id"] if incoming.
|
|
50
|
+
incoming_run_id = incoming["run_id"] if incoming.instance_of?(Hash)
|
|
52
51
|
started_at = SimpleCov.process_start_time
|
|
53
52
|
return true if current_run_entry?(entry, incoming_run_id, started_at)
|
|
54
53
|
|
|
55
54
|
written_after_start?(entry, started_at)
|
|
56
55
|
end
|
|
57
56
|
|
|
58
|
-
# Strictly after, unlike `fresh_entry?`: an entry stamped at the
|
|
59
|
-
#
|
|
60
|
-
# run, so a same-second stale entry is still overwritten.
|
|
57
|
+
# Strictly after, unlike `fresh_entry?`: an entry stamped at the exact
|
|
58
|
+
# instant we started is treated as leftover from a previous run.
|
|
61
59
|
def written_after_start?(entry, started_at)
|
|
62
60
|
timestamp = entry["timestamp"]
|
|
63
61
|
timestamp.is_a?(Numeric) && started_at && timestamp > started_at.to_f
|
|
@@ -7,14 +7,11 @@ require_relative "../atomic_file"
|
|
|
7
7
|
|
|
8
8
|
module SimpleCov
|
|
9
9
|
module ResultMerger
|
|
10
|
-
# Reads and writes the persistent `.resultset.json` cache, including
|
|
11
|
-
# file-lock synchronization between processes and atomic temp-file
|
|
12
|
-
# renames so concurrent readers don't observe a truncated file.
|
|
13
10
|
module ResultsetStore
|
|
14
11
|
LOCK_MONITOR = Monitor.new
|
|
15
12
|
private_constant :LOCK_MONITOR
|
|
16
13
|
|
|
17
|
-
|
|
14
|
+
extend self
|
|
18
15
|
|
|
19
16
|
def resultset_path
|
|
20
17
|
File.join(SimpleCov.coverage_path, ".resultset.json")
|
|
@@ -24,30 +21,42 @@ module SimpleCov
|
|
|
24
21
|
File.join(SimpleCov.coverage_path, ".resultset.json.lock")
|
|
25
22
|
end
|
|
26
23
|
|
|
27
|
-
# Compact JSON, not pretty-printed: this is a machine-read cache
|
|
28
|
-
#
|
|
29
|
-
#
|
|
30
|
-
#
|
|
24
|
+
# Compact JSON, not pretty-printed: this is a machine-read cache that every
|
|
25
|
+
# parallel worker rewrites wholesale, and on a large project pretty printing
|
|
26
|
+
# nearly doubles the bytes written, read back, and parsed on each of those
|
|
27
|
+
# store-merge round trips.
|
|
31
28
|
def write(resultset)
|
|
32
29
|
AtomicFile.write(resultset_path, "#{JSON.generate(resultset)}\n")
|
|
33
30
|
end
|
|
34
31
|
|
|
35
|
-
#
|
|
36
|
-
# calls by the owning thread bypass flock so they cannot deadlock on a
|
|
37
|
-
#
|
|
32
|
+
# Threads are serialized before the process-wide file lock is taken. Nested
|
|
33
|
+
# calls by the owning thread bypass flock so they cannot deadlock on a second
|
|
34
|
+
# descriptor for the same lock file.
|
|
38
35
|
def synchronize(&)
|
|
39
36
|
return yield if LOCK_MONITOR.mon_owned?
|
|
40
37
|
|
|
41
38
|
LOCK_MONITOR.synchronize { with_flock(&) }
|
|
42
39
|
end
|
|
43
40
|
|
|
44
|
-
def with_flock
|
|
41
|
+
def with_flock(&)
|
|
45
42
|
FileUtils.mkdir_p(SimpleCov.coverage_path)
|
|
46
|
-
|
|
47
|
-
|
|
43
|
+
holding_writelock(&)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def holding_writelock
|
|
47
|
+
open_file(writelock_path, "w+") do |file|
|
|
48
|
+
file.flock(File::LOCK_EX)
|
|
48
49
|
yield
|
|
49
50
|
end
|
|
50
51
|
end
|
|
52
|
+
|
|
53
|
+
# mutant:disable — Ruby 4.0 removed Kernel#open's leading-pipe
|
|
54
|
+
# command mode, so no test can tell `File.open` from `open` here
|
|
55
|
+
# any more. The explicit receiver is kept: on older rubies it is
|
|
56
|
+
# what refuses to run a lock path as a command.
|
|
57
|
+
def open_file(name, mode, &)
|
|
58
|
+
File.open(name, mode, &)
|
|
59
|
+
end
|
|
51
60
|
end
|
|
52
61
|
end
|
|
53
62
|
end
|
|
@@ -2,25 +2,21 @@
|
|
|
2
2
|
|
|
3
3
|
module SimpleCov
|
|
4
4
|
module ResultMerger
|
|
5
|
-
# The merge step's half of unloaded-file handling: the policy of when
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
# `SimpleCov::UnloadedFileInjector`, an easy name to confuse with this
|
|
9
|
-
# one.
|
|
5
|
+
# The merge step's half of unloaded-file handling: the policy of when to
|
|
6
|
+
# inject and which criteria simulated files carry. The mechanism, expanding
|
|
7
|
+
# globs and simulating each file, is `SimpleCov::UnloadedFileInjector`.
|
|
10
8
|
#
|
|
11
9
|
# Injection moved here from the individual processes because only the union
|
|
12
10
|
# of what they all loaded says what was really never loaded. Doing it per
|
|
13
11
|
# process meant N workers simulated the same file up to N times and the
|
|
14
|
-
# merge threw all but one away
|
|
12
|
+
# merge threw all but one away (#1250).
|
|
15
13
|
module UnloadedFiles
|
|
16
|
-
|
|
14
|
+
extend self
|
|
17
15
|
|
|
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
16
|
# A collector for the merge to hand `merge_valid_results`, gathering the
|
|
23
17
|
# tracked paths of every resultset that survives the merge timeout.
|
|
18
|
+
# Absent from resultsets written before this was recorded, in which case
|
|
19
|
+
# those processes injected their own unloaded files already.
|
|
24
20
|
def collector(into)
|
|
25
21
|
->(surviving) { into.merge(tracked_in(surviving)) }
|
|
26
22
|
end
|
|
@@ -38,12 +34,11 @@ module SimpleCov
|
|
|
38
34
|
tracked.empty? ? entry : entry.merge("tracked_files" => tracked)
|
|
39
35
|
end
|
|
40
36
|
|
|
41
|
-
#
|
|
37
|
+
# Simulates each tracked file the merged coverage doesn't already carry.
|
|
42
38
|
# The paths come from the resultsets rather than this process's own
|
|
43
|
-
# configuration, which a standalone `collate` would not have
|
|
44
|
-
#
|
|
45
|
-
#
|
|
46
|
-
# that already carry their unloaded files pass through untouched.
|
|
39
|
+
# configuration, which a standalone `collate` would not have. Idempotent,
|
|
40
|
+
# so resultsets that already carry their unloaded files pass through
|
|
41
|
+
# untouched.
|
|
47
42
|
def inject(coverage, tracked_files)
|
|
48
43
|
SimpleCov.inject_unloaded_files(
|
|
49
44
|
coverage, tracked_files.to_a,
|
|
@@ -54,10 +49,10 @@ module SimpleCov
|
|
|
54
49
|
|
|
55
50
|
# A simulated file should have the same shape as the files it is being
|
|
56
51
|
# merged alongside, so the criteria come from the merged data rather than
|
|
57
|
-
# from this process's configuration, which is not necessarily
|
|
58
|
-
#
|
|
59
|
-
#
|
|
60
|
-
#
|
|
52
|
+
# from this process's configuration, which is not necessarily what any
|
|
53
|
+
# contributing process measured under: `simplecov merge` never ran
|
|
54
|
+
# `SimpleCov.start` at all. Giving injected files fewer tables than their
|
|
55
|
+
# neighbours is what inflates the percentage #1059 fixed.
|
|
61
56
|
#
|
|
62
57
|
# Falls back to the configuration when there is nothing to be consistent
|
|
63
58
|
# with, which is a merge of resultsets that carried no files.
|
|
@@ -77,25 +72,22 @@ module SimpleCov
|
|
|
77
72
|
# Which files no contributing process ever loaded. Injection reports the
|
|
78
73
|
# ones it added, but a file can also arrive already simulated from a
|
|
79
74
|
# resultset this merge didn't inject into, so the merged line counts are
|
|
80
|
-
# still consulted
|
|
81
|
-
# `Combine::CoverageAccumulator` reconciles synthesized tuples on.
|
|
75
|
+
# still consulted.
|
|
82
76
|
#
|
|
83
77
|
# A file is judged only when it has a relevant line. A branch-only or
|
|
84
78
|
# method-only run reports no line data at all for the files it loaded,
|
|
85
|
-
# and a loaded file with no executable lines
|
|
86
|
-
#
|
|
87
|
-
#
|
|
88
|
-
#
|
|
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.
|
|
79
|
+
# and a loaded file with no executable lines reports every line as `nil`.
|
|
80
|
+
# Either would otherwise read as "never executed" and mark a genuinely
|
|
81
|
+
# loaded file not loaded, turning its branch and method coverage into
|
|
82
|
+
# #902's 0%.
|
|
93
83
|
def never_executed(coverage)
|
|
94
84
|
coverage.each_with_object(Set.new) do |(filename, file_coverage), set|
|
|
95
85
|
counts = Array(file_coverage["lines"]) #: Array[Integer?]
|
|
96
|
-
|
|
86
|
+
# A line count is an Integer or nil, and every Integer is truthy, so a
|
|
87
|
+
# truthy element is exactly a relevant line.
|
|
88
|
+
next unless counts.any?
|
|
97
89
|
|
|
98
|
-
set << filename unless Combine::CoverageAccumulator.executed?(
|
|
90
|
+
set << filename unless Combine::CoverageAccumulator.executed?(counts)
|
|
99
91
|
end
|
|
100
92
|
end
|
|
101
93
|
end
|
|
@@ -4,13 +4,13 @@ require_relative "result_merger/legacy_format_adapter"
|
|
|
4
4
|
require_relative "result_merger/resultset_file"
|
|
5
5
|
require_relative "result_merger/resultset_run_identity"
|
|
6
6
|
require_relative "result_merger/resultset_store"
|
|
7
|
+
require_relative "result_merger/contexts"
|
|
7
8
|
require_relative "result_merger/unloaded_files"
|
|
8
9
|
|
|
9
10
|
module SimpleCov
|
|
10
11
|
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
# upon multiple test suites.
|
|
12
|
+
# Caching, loading and merging `SimpleCov::Result`s into a single result for
|
|
13
|
+
# coverage analysis based upon multiple test suites.
|
|
14
14
|
#
|
|
15
15
|
module ResultMerger
|
|
16
16
|
extend ResultsetRunIdentity
|
|
@@ -27,40 +27,40 @@ module SimpleCov
|
|
|
27
27
|
end
|
|
28
28
|
|
|
29
29
|
def merge_results(*file_paths, ignore_timeout: false)
|
|
30
|
-
# Tracked paths are collected as each resultset is parsed,
|
|
31
|
-
# still read and discarded one at a time
|
|
30
|
+
# Tracked paths and test maps are collected as each resultset is parsed,
|
|
31
|
+
# so files are still read and discarded one at a time (#1250).
|
|
32
32
|
tracked_files = Set.new
|
|
33
|
+
context_maps = ContextMap::Union.new
|
|
33
34
|
command_names, coverage = absorb_results(file_paths, ignore_timeout: ignore_timeout,
|
|
34
|
-
|
|
35
|
-
create_result(command_names, coverage, tracked_files: tracked_files)
|
|
35
|
+
&entry_collector(tracked_files, context_maps))
|
|
36
|
+
create_result(command_names, coverage, tracked_files: tracked_files, contexts: context_maps.map)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# One block over the entries each resultset contributes to the merge,
|
|
40
|
+
# feeding every merge-side accumulator in a single pass: the tracked
|
|
41
|
+
# paths and the per-test map union (#1250).
|
|
42
|
+
def entry_collector(tracked_files, context_maps)
|
|
43
|
+
collectors = [UnloadedFiles.collector(tracked_files), context_maps.collector]
|
|
44
|
+
->(surviving) { collectors.each { |collector| collector.call(surviving) } }
|
|
36
45
|
end
|
|
37
46
|
|
|
38
47
|
#
|
|
39
48
|
# Reads every resultset and folds it into one merged coverage, stopping
|
|
40
49
|
# short of building a `SimpleCov::Result`.
|
|
41
50
|
#
|
|
42
|
-
#
|
|
43
|
-
#
|
|
44
|
-
#
|
|
45
|
-
#
|
|
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.
|
|
51
|
+
# Files are only read in and parsed one at a time on purpose. In big CI
|
|
52
|
+
# setups you might deal with 100s of jobs each producing megabytes of
|
|
53
|
+
# data, and reading them all in easily produces gigabytes of memory
|
|
54
|
+
# consumption. For the same reason a `Result` is only created at the end.
|
|
50
55
|
#
|
|
51
56
|
# One accumulator absorbs the whole run, rather than folding each file
|
|
52
|
-
# into the merged-so-far pairwise: the pairwise form rebuilt every
|
|
53
|
-
#
|
|
54
|
-
#
|
|
55
|
-
# Absorbing is still one resultset at a time, so the memory ceiling
|
|
56
|
-
# above is unchanged.
|
|
57
|
+
# into the merged-so-far pairwise: the pairwise form rebuilt every file's
|
|
58
|
+
# coverage once per resultset, which is what made merging a large
|
|
59
|
+
# parallel run's results the dominant cost of `collate`.
|
|
57
60
|
#
|
|
58
61
|
# `file_paths` is only ever iterated, so a caller that wants to observe
|
|
59
|
-
# the merge as it goes can hand in any Enumerable
|
|
60
|
-
# passes an Enumerator that reports progress
|
|
61
|
-
# this loop and risk timing something other than what ships.
|
|
62
|
-
#
|
|
63
|
-
# @return [Array] the command names and the merged coverage
|
|
62
|
+
# the merge as it goes can hand in any Enumerable. `benchmarks/collate`
|
|
63
|
+
# passes an Enumerator that reports progress.
|
|
64
64
|
#
|
|
65
65
|
def absorb_results(file_paths, ignore_timeout: false, &on_parse)
|
|
66
66
|
Combine::CoverageAccumulator.fold(
|
|
@@ -84,7 +84,6 @@ module SimpleCov
|
|
|
84
84
|
[[command_name], LegacyFormatAdapter.call(data.fetch("coverage"))]
|
|
85
85
|
end
|
|
86
86
|
|
|
87
|
-
# one file itself _might_ include multiple test runs
|
|
88
87
|
merge_coverage(*command_plus_coverage)
|
|
89
88
|
end
|
|
90
89
|
|
|
@@ -101,50 +100,54 @@ module SimpleCov
|
|
|
101
100
|
end
|
|
102
101
|
|
|
103
102
|
def warn_about_expired_results(expired_command_names)
|
|
104
|
-
#
|
|
105
|
-
#
|
|
106
|
-
# their own slice; the selected final process performs this merge.
|
|
103
|
+
# Ordinary parallel workers only store their own slice; the selected
|
|
104
|
+
# final process performs this merge.
|
|
107
105
|
return unless SimpleCov.print_errors
|
|
108
106
|
|
|
109
107
|
warn "[SimpleCov]: Excluded #{expired_command_names.size} result(s) older than " \
|
|
110
108
|
"merge_timeout (#{SimpleCov.merge_timeout}s) from the merged report: " \
|
|
111
|
-
"#{expired_command_names.sort.join(
|
|
109
|
+
"#{expired_command_names.sort.join(", ")}. " \
|
|
112
110
|
"Increase SimpleCov.merge_timeout to include them."
|
|
113
111
|
end
|
|
114
112
|
|
|
115
|
-
|
|
113
|
+
# `tracked_files:` has no default: only the caller knows what the
|
|
114
|
+
# contributing processes were told to track, and quietly reading an
|
|
115
|
+
# omission as "nothing was tracked" would drop every never-loaded
|
|
116
|
+
# file from the merged report.
|
|
117
|
+
def create_result(command_names, coverage, tracked_files:, contexts: nil)
|
|
116
118
|
return nil unless coverage
|
|
117
119
|
|
|
118
|
-
|
|
120
|
+
# Deduped: a CI matrix collates one resultset per worker, and joining
|
|
121
|
+
# every run's name verbatim rendered "RSpec" a hundred times over in the
|
|
122
|
+
# report footer (#1284).
|
|
123
|
+
distinct_names = command_names.reject(&:empty?).uniq.sort
|
|
119
124
|
coverage, injected = UnloadedFiles.inject(coverage, tracked_files)
|
|
120
|
-
# The merged result is the authoritative one users actually see, so
|
|
121
|
-
#
|
|
122
|
-
#
|
|
123
|
-
#
|
|
124
|
-
|
|
125
|
+
# The merged result is the authoritative one users actually see, so it's
|
|
126
|
+
# the one that warns about source files dropped because they no longer
|
|
127
|
+
# exist on disk (#980). The per-process slices stay quiet to avoid one
|
|
128
|
+
# warning per worker.
|
|
129
|
+
Result.new(
|
|
125
130
|
coverage,
|
|
126
|
-
command_name:
|
|
131
|
+
command_name: distinct_names.join(", "), contexts: contexts, report: true,
|
|
127
132
|
not_loaded_files: UnloadedFiles.never_executed(coverage) | injected,
|
|
128
|
-
tracked_files: tracked_files
|
|
129
|
-
|
|
130
|
-
)
|
|
133
|
+
tracked_files: tracked_files
|
|
134
|
+
).tap { |result| result.command_names = distinct_names }
|
|
131
135
|
end
|
|
132
136
|
|
|
133
137
|
def merge_coverage(*results)
|
|
134
138
|
return [[""], nil] if results.empty?
|
|
135
|
-
return results.first if results.size == 1
|
|
136
139
|
|
|
137
|
-
|
|
140
|
+
# Destructured rather than counted: the single-result fast path then
|
|
141
|
+
# hands back the very result it checked for, not a second lookup.
|
|
142
|
+
only, *rest = results
|
|
143
|
+
rest.empty? ? only : Combine::CoverageAccumulator.fold(results)
|
|
138
144
|
end
|
|
139
145
|
|
|
140
|
-
#
|
|
141
|
-
# Gets all SimpleCov::Results stored in resultset, merges them and produces a new
|
|
142
|
-
# SimpleCov::Result with merged coverage data and the command_name
|
|
143
|
-
# for the result consisting of a join on all source result's names
|
|
144
146
|
def merged_result
|
|
145
147
|
tracked_files = Set.new
|
|
146
|
-
|
|
147
|
-
|
|
148
|
+
context_maps = ContextMap::Union.new
|
|
149
|
+
command_names, coverage = merge_valid_results(read_resultset, &entry_collector(tracked_files, context_maps))
|
|
150
|
+
create_result(command_names, coverage, tracked_files: tracked_files, contexts: context_maps.map)
|
|
148
151
|
end
|
|
149
152
|
|
|
150
153
|
def read_resultset
|
|
@@ -152,13 +155,11 @@ module SimpleCov
|
|
|
152
155
|
ResultsetFile.decode(content)
|
|
153
156
|
end
|
|
154
157
|
|
|
155
|
-
|
|
156
|
-
def store_result(result) # rubocop:disable Naming/PredicateMethod
|
|
158
|
+
def store_result(result)
|
|
157
159
|
synchronize_resultset do
|
|
158
|
-
# Ensure we have the latest, in case it was already cached
|
|
159
160
|
new_resultset = read_resultset
|
|
160
161
|
|
|
161
|
-
# A single result only ever has one command_name, see `
|
|
162
|
+
# A single result only ever has one command_name, see `Result#to_hash`.
|
|
162
163
|
command_name, data = result.to_hash.first
|
|
163
164
|
new_resultset[command_name] = merged_entry(new_resultset[command_name], data)
|
|
164
165
|
|
|
@@ -168,16 +169,16 @@ module SimpleCov
|
|
|
168
169
|
end
|
|
169
170
|
|
|
170
171
|
# If an entry with the same command_name was written AFTER our process
|
|
171
|
-
# started, a sibling test runner
|
|
172
|
-
#
|
|
173
|
-
#
|
|
174
|
-
# subprocess's real data. See https://github.com/simplecov-ruby/simplecov/issues/581.
|
|
172
|
+
# started, a sibling test runner wrote it. Combine coverage data rather
|
|
173
|
+
# than overwriting, so an empty parent-process result doesn't clobber the
|
|
174
|
+
# subprocess's real data (#581).
|
|
175
175
|
def merged_entry(existing, incoming)
|
|
176
176
|
return incoming unless concurrent_runner_entry?(existing, incoming)
|
|
177
177
|
|
|
178
178
|
merged = incoming.merge(
|
|
179
|
-
"coverage" => Combine::ResultsCombiner.combine(existing
|
|
179
|
+
"coverage" => Combine::ResultsCombiner.combine(existing.fetch("coverage"), incoming.fetch("coverage"))
|
|
180
180
|
)
|
|
181
|
+
merged = Contexts.carry(merged, existing, incoming)
|
|
181
182
|
UnloadedFiles.carry_tracked(merged, existing, incoming)
|
|
182
183
|
end
|
|
183
184
|
|