simplecov 1.1.1 → 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 +148 -5
- 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 +19 -37
- 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 +16 -10
- 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 +37 -55
- 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 +15 -9
- 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 -19
- data/lib/simplecov/parallel_adapters/parallel_tests.rb +21 -42
- data/lib/simplecov/parallel_adapters.rb +24 -55
- 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 +56 -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 +34 -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 +57 -113
- data/lib/simplecov_json_formatter.rb +1 -0
- data/man/simplecov.1 +349 -0
- data/sig/simplecov.rbs +523 -595
- metadata +72 -5
|
@@ -8,52 +8,22 @@ module SimpleCov
|
|
|
8
8
|
module Combine
|
|
9
9
|
#
|
|
10
10
|
# Folds any number of resultsets' coverage into one, absorbing them one
|
|
11
|
-
# at a time
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
# the entire accumulated structure on every one of its N-1 steps: a
|
|
15
|
-
# fresh outer file hash, a fresh lines array for every file, and a fresh
|
|
16
|
-
# branch / method table for every file whose keys were re-interned from
|
|
17
|
-
# their tuples each time. On a 160-worker run over ~1,800 files that is
|
|
18
|
-
# ~290,000 whole-file rebuilds to produce ~1,800 files of output.
|
|
19
|
-
#
|
|
20
|
-
# The accumulated side is private to the fold, so it can be updated in
|
|
21
|
-
# place instead, and the interned tables only have to be turned back into
|
|
22
|
-
# tuple-keyed hashes once, at the end.
|
|
23
|
-
#
|
|
24
|
-
# Resultsets are absorbed one at a time rather than taken as a list, so
|
|
25
|
-
# `ResultMerger.merge_results` can keep reading and discarding them one
|
|
26
|
-
# file at a time — reading 100s of CI jobs' worth of coverage into memory
|
|
27
|
-
# at once is what that method is careful not to do.
|
|
11
|
+
# at a time so that a caller reading resultsets off disk never holds more
|
|
12
|
+
# than one in memory, and so the accumulated side can be updated in place
|
|
13
|
+
# rather than rebuilt once per resultset.
|
|
28
14
|
#
|
|
29
15
|
class CoverageAccumulator
|
|
30
|
-
# A file some process actually loaded has at least one executed line;
|
|
31
|
-
#
|
|
32
|
-
# the signal the merge reconciles synthesized tuples on, and the one
|
|
33
|
-
# `ResultMerger` re-derives a merged result's not-loaded set from, so it
|
|
34
|
-
# lives here rather than being spelled out at each site.
|
|
16
|
+
# A file some process actually loaded has at least one executed line; a
|
|
17
|
+
# simulated (never-loaded) file's lines are all `nil` or `0`.
|
|
35
18
|
#
|
|
36
|
-
# `Array()` plus the `Numeric` test rather than
|
|
37
|
-
#
|
|
38
|
-
#
|
|
39
|
-
# SimpleCov version, or hand-edited, can carry anything under
|
|
40
|
-
# "lines" — a Hash coerces to pairs, a String to itself. Keeping a
|
|
41
|
-
# malformed entry to a wrong answer instead of a NoMethodError out
|
|
42
|
-
# of the middle of a merge is the point.
|
|
19
|
+
# `Array()` plus the `Numeric` test rather than `any?(&:positive?)`
|
|
20
|
+
# because this reads straight off a parsed resultset, which is external
|
|
21
|
+
# input and can carry anything under "lines".
|
|
43
22
|
def self.executed?(lines)
|
|
44
23
|
counts = Array(lines) #: Array[untyped]
|
|
45
24
|
counts.any? { |count| count.is_a?(Numeric) && count.positive? }
|
|
46
25
|
end
|
|
47
26
|
|
|
48
|
-
#
|
|
49
|
-
# Folds `[command_names, coverage]` pairs into one merged coverage.
|
|
50
|
-
#
|
|
51
|
-
# `pairs` is only ever iterated, so a caller that reads resultsets off
|
|
52
|
-
# disk can hand in a lazy enumerable and never hold more than one in
|
|
53
|
-
# memory — which is what `ResultMerger.merge_results` is careful about.
|
|
54
|
-
#
|
|
55
|
-
# @return [Array] the concatenated command names and the merged coverage
|
|
56
|
-
#
|
|
57
27
|
def self.fold(pairs)
|
|
58
28
|
accumulator = new
|
|
59
29
|
command_names = [] #: Array[String]
|
|
@@ -67,8 +37,6 @@ module SimpleCov
|
|
|
67
37
|
end
|
|
68
38
|
|
|
69
39
|
def initialize
|
|
70
|
-
@files = {} #: Hash[String, untyped]
|
|
71
|
-
@absorbed = false
|
|
72
40
|
# Whether a criterion is enabled can't change mid-fold, so read it once
|
|
73
41
|
# here rather than once per file. `branch_coverage?` reaches
|
|
74
42
|
# `Coverage.supported?`, and a large parallel run merges thousands of
|
|
@@ -77,48 +45,31 @@ module SimpleCov
|
|
|
77
45
|
@method_coverage = SimpleCov.method_coverage?
|
|
78
46
|
end
|
|
79
47
|
|
|
80
|
-
#
|
|
81
|
-
# Folds one resultset's coverage (filename => per-file coverage) into
|
|
82
|
-
# the accumulator. A `nil` coverage is ignored — a resultset that
|
|
83
|
-
# carried nothing contributes nothing.
|
|
84
|
-
#
|
|
85
|
-
# @return [CoverageAccumulator] self, so absorbs can be chained
|
|
86
|
-
#
|
|
87
48
|
def absorb(coverage)
|
|
88
49
|
return self unless coverage
|
|
89
50
|
|
|
90
|
-
|
|
51
|
+
files = (@files ||= {}) #: Hash[String, untyped]
|
|
91
52
|
coverage.each do |filename, file_coverage|
|
|
92
|
-
|
|
53
|
+
files[filename] = merge_file(files[filename], file_coverage)
|
|
93
54
|
end
|
|
94
55
|
|
|
95
56
|
self
|
|
96
57
|
end
|
|
97
58
|
|
|
98
|
-
#
|
|
99
|
-
#
|
|
100
|
-
#
|
|
101
|
-
# nothing", and only the former means there is no report to build.
|
|
102
|
-
#
|
|
103
|
-
# A file only one resultset carried is returned exactly as it came in,
|
|
104
|
-
# untouched: with nothing to merge it into, copying it would only cost
|
|
105
|
-
# memory.
|
|
106
|
-
#
|
|
107
|
-
# @return [Hash, nil]
|
|
108
|
-
#
|
|
59
|
+
# `nil` until the first absorb, so callers can tell "no results" apart
|
|
60
|
+
# from "results that cover nothing": only the former means there is no
|
|
61
|
+
# report to build.
|
|
109
62
|
def result
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
63
|
+
@files&.transform_values do |entry|
|
|
64
|
+
case entry
|
|
65
|
+
when MergedFile then entry.to_h
|
|
66
|
+
else entry
|
|
67
|
+
end
|
|
114
68
|
end
|
|
115
69
|
end
|
|
116
70
|
|
|
117
|
-
|
|
71
|
+
private
|
|
118
72
|
|
|
119
|
-
# The first sighting of a file is kept as-is; the second promotes it to
|
|
120
|
-
# a `MergedFile` that owns its state, and every later one folds into
|
|
121
|
-
# that same object.
|
|
122
73
|
def merge_file(existing, file_coverage)
|
|
123
74
|
case existing
|
|
124
75
|
when nil then file_coverage
|
|
@@ -128,34 +79,25 @@ module SimpleCov
|
|
|
128
79
|
end
|
|
129
80
|
|
|
130
81
|
#
|
|
131
|
-
# One file's accumulated coverage
|
|
132
|
-
#
|
|
82
|
+
# One file's accumulated coverage.
|
|
83
|
+
#
|
|
84
|
+
# A criterion is carried for this file when the data carries it, not when
|
|
85
|
+
# this process happens to measure it: `simplecov merge` never ran
|
|
86
|
+
# SimpleCov.start at all, and dropping a table it did not ask for would
|
|
87
|
+
# lose branch data the producers did measure. A nil table means nobody
|
|
88
|
+
# measured that criterion, an empty one means it was measured and the
|
|
89
|
+
# file has none.
|
|
133
90
|
#
|
|
134
91
|
class MergedFile
|
|
135
|
-
# A criterion is carried for this file when the data carries it, not
|
|
136
|
-
# when this process happens to measure it. A merge runs on behalf of
|
|
137
|
-
# the processes that produced the resultsets and does not necessarily
|
|
138
|
-
# share their configuration: `simplecov merge` never ran SimpleCov.start
|
|
139
|
-
# at all, and dropping a table it did not ask for would lose branch data
|
|
140
|
-
# the producers did measure. A nil table means nobody measured that
|
|
141
|
-
# criterion; an empty one means it was measured and the file has none.
|
|
142
92
|
def initialize(coverage, branches:, methods:)
|
|
143
93
|
@branch_coverage = branches
|
|
144
94
|
@method_coverage = methods
|
|
145
|
-
@lines = coverage["lines"]
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
# actually carries methods.
|
|
150
|
-
@branches = BranchesCombiner.absorb({}, coverage["branches"]) if branches || coverage["branches"]
|
|
151
|
-
@methods = MethodsCombiner.absorb(nil, coverage["methods"]) if methods || coverage["methods"]
|
|
95
|
+
@lines = LinesCombiner.merge_into(nil, coverage["lines"])
|
|
96
|
+
branches_table = coverage["branches"]
|
|
97
|
+
@branches = BranchesCombiner.absorb(new_table, branches_table) if branches || branches_table
|
|
98
|
+
@methods = MethodsCombiner.absorb(nil, coverage["methods"])
|
|
152
99
|
end
|
|
153
100
|
|
|
154
|
-
#
|
|
155
|
-
# Folds one more resultset's coverage for this file in.
|
|
156
|
-
#
|
|
157
|
-
# @return [MergedFile] self
|
|
158
|
-
#
|
|
159
101
|
def absorb(coverage)
|
|
160
102
|
reconcile_synthesized(coverage)
|
|
161
103
|
@lines = LinesCombiner.merge_into(@lines, coverage["lines"])
|
|
@@ -169,33 +111,26 @@ module SimpleCov
|
|
|
169
111
|
merged
|
|
170
112
|
end
|
|
171
113
|
|
|
172
|
-
|
|
114
|
+
private
|
|
173
115
|
|
|
174
116
|
def new_table
|
|
175
117
|
{} #: Hash[untyped, untyped]
|
|
176
118
|
end
|
|
177
119
|
|
|
178
120
|
# When exactly one side of the merge was actually executed, its branch
|
|
179
|
-
# and method tuples are authoritative and the other side's are
|
|
180
|
-
#
|
|
181
|
-
#
|
|
182
|
-
#
|
|
183
|
-
#
|
|
184
|
-
#
|
|
185
|
-
# inflation for files no process loaded, rather than a false miss on a
|
|
186
|
-
# covered file. Lines are never dropped: a simulated file's line shape
|
|
187
|
-
# is correct and carries the unloaded-file denominator (#1059).
|
|
188
|
-
#
|
|
189
|
-
# The accumulated side's executed-ness is re-read on every absorb
|
|
190
|
-
# because folding in an executed resultset can flip it, exactly as it
|
|
191
|
-
# would have flipped between two steps of the pairwise fold.
|
|
121
|
+
# and method tuples are authoritative and the other side's are dropped.
|
|
122
|
+
# SimulateCoverage synthesizes those tuples statically, so a location
|
|
123
|
+
# that drifts from what Coverage emits would otherwise be unioned in by
|
|
124
|
+
# position and survive as a phantom, permanently-missed branch (#1233).
|
|
125
|
+
# Lines are never dropped: a simulated file's line shape is correct and
|
|
126
|
+
# carries the unloaded-file denominator (#1059).
|
|
192
127
|
def reconcile_synthesized(coverage)
|
|
193
|
-
|
|
128
|
+
incoming_lines = coverage["lines"]
|
|
129
|
+
return absorb_tuples(coverage) unless lines_measured?(@lines) && lines_measured?(incoming_lines)
|
|
194
130
|
|
|
195
|
-
|
|
196
|
-
incoming_executed = executed?(coverage["lines"])
|
|
131
|
+
incoming_executed = executed?(incoming_lines)
|
|
197
132
|
|
|
198
|
-
if
|
|
133
|
+
if executed?(@lines).equal?(incoming_executed)
|
|
199
134
|
absorb_tuples(coverage)
|
|
200
135
|
elsif incoming_executed
|
|
201
136
|
replace_tuples(coverage)
|
|
@@ -204,58 +139,30 @@ module SimpleCov
|
|
|
204
139
|
end
|
|
205
140
|
end
|
|
206
141
|
|
|
207
|
-
# Both sides agree on whether they ran: union their tuples. A criterion
|
|
208
|
-
# only the incoming side carries comes into play here, because its
|
|
209
|
-
# tuples survive. It deliberately does not come into play on the path
|
|
210
|
-
# that drops them: promoting there would advertise an empty table
|
|
211
|
-
# sourced from data this merge just discarded, which claims the file
|
|
212
|
-
# has no branches when what is true is that nobody measured them.
|
|
213
142
|
def absorb_tuples(coverage)
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
@methods = MethodsCombiner.absorb(@methods, coverage["methods"])
|
|
143
|
+
branches_table = coverage["branches"]
|
|
144
|
+
@branches = BranchesCombiner.absorb(@branches || new_table, branches_table) if branches_table
|
|
145
|
+
@methods = MethodsCombiner.absorb(@methods, coverage["methods"])
|
|
217
146
|
end
|
|
218
147
|
|
|
219
|
-
# Only the accumulated side ran, so the incoming tuples are synthesized
|
|
220
|
-
# and contribute nothing. When the discarded side carried a methods
|
|
221
|
-
# table, a measuring process still gets one — the empty table stands in
|
|
222
|
-
# for the blanked side, which is what a pair against a blank one used
|
|
223
|
-
# to produce for a file that has no methods yet. When no side has
|
|
224
|
-
# carried methods at all, the table stays nil (the rule `initialize`
|
|
225
|
-
# documents), the same answer the union path gives.
|
|
226
148
|
def drop_incoming_tuples(coverage)
|
|
227
149
|
@methods ||= {} if @method_coverage && coverage["methods"] #: Hash[untyped, untyped]
|
|
228
150
|
end
|
|
229
151
|
|
|
230
|
-
# Only the incoming side ran, so what's accumulated is synthesized:
|
|
231
|
-
# start its tuples over from the executed side's. The authoritative
|
|
232
|
-
# side decides which criteria the file carries, not just their tuples.
|
|
233
|
-
# Keeping a table in play that only the dropped side carried would make
|
|
234
|
-
# the answer depend on which side was seen first.
|
|
235
152
|
def replace_tuples(coverage)
|
|
236
153
|
@branches = authoritative_table(BranchesCombiner, coverage["branches"], @branch_coverage)
|
|
237
|
-
# Methods stay nil until some resultset carries them (see
|
|
238
|
-
# `initialize`), so a measuring process keeps an empty table only
|
|
239
|
-
# when one did.
|
|
240
154
|
@methods = authoritative_table(MethodsCombiner, coverage["methods"], @method_coverage && !@methods.nil?)
|
|
241
155
|
end
|
|
242
156
|
|
|
243
|
-
# The executed side's tuples when it has them. Otherwise `keep_empty`
|
|
244
|
-
# decides whether the criterion survives as an empty table, which is
|
|
245
|
-
# what keeps `SourceFile` able to tell "no branches here" from
|
|
246
|
-
# "branches were not measured".
|
|
247
157
|
def authoritative_table(combiner, table, keep_empty)
|
|
248
158
|
return combiner.absorb(new_table, table) if table
|
|
249
159
|
|
|
250
|
-
|
|
160
|
+
new_table if keep_empty
|
|
251
161
|
end
|
|
252
162
|
|
|
253
|
-
#
|
|
254
|
-
#
|
|
255
|
-
|
|
256
|
-
# tuples. Unknown means union, which is already what happens when
|
|
257
|
-
# neither side carries lines.
|
|
258
|
-
def judgeable?(lines)
|
|
163
|
+
# Absent lines do not mean the side never ran: a branch-only or
|
|
164
|
+
# method-only run omits them even for the files it loaded.
|
|
165
|
+
def lines_measured?(lines)
|
|
259
166
|
!Array(lines).empty?
|
|
260
167
|
end
|
|
261
168
|
|
|
@@ -2,21 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
module SimpleCov
|
|
4
4
|
module Combine
|
|
5
|
+
# Builds the raw-key => interned-identity caches the tuple combiners keep. A
|
|
6
|
+
# key is mapped on first sight and kept: the pairwise fold would otherwise
|
|
7
|
+
# re-derive the accumulator's identities on every one of its merges, always
|
|
8
|
+
# to the same answer.
|
|
5
9
|
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
# Identities exist only to be `combine`'s hash keys, and `Array#hash` is
|
|
13
|
-
# not memoized, so keying on the identity tuple itself would rehash its
|
|
14
|
-
# elements for every key of both sides of every merge. An Integer hashes
|
|
15
|
-
# as an immediate. Two keys share an id exactly when their identities are
|
|
16
|
-
# equal, so the string and array forms of one key still merge together.
|
|
17
|
-
#
|
|
10
|
+
# Identities exist only to be hash keys, and `Array#hash` is not memoized, so
|
|
11
|
+
# keying on the identity tuple itself would rehash its elements for every key
|
|
12
|
+
# of both sides of every merge. An Integer hashes as an immediate. Two keys
|
|
13
|
+
# share an id exactly when their identities are equal, so the string and
|
|
14
|
+
# array forms of one key still merge together.
|
|
18
15
|
module IdentityInterner
|
|
19
|
-
|
|
16
|
+
extend self
|
|
20
17
|
|
|
21
18
|
def build
|
|
22
19
|
ids = {} #: Hash[::Array[untyped], Integer]
|
|
@@ -2,17 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
module SimpleCov
|
|
4
4
|
module Combine
|
|
5
|
-
# The inner merge loop the branches and methods combiners share:
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
# still holds.
|
|
5
|
+
# The inner merge loop the branches and methods combiners share: folds
|
|
6
|
+
# `source`'s tuple => count pairs into `target`, an interned table keyed by
|
|
7
|
+
# each tuple's merge identity and holding `[raw_key, count]` pairs. The
|
|
8
|
+
# first-seen raw key is kept for display; counts sum. `target` and its pairs
|
|
9
|
+
# are always built by this loop, so updating them in place can't reach data a
|
|
10
|
+
# caller still holds.
|
|
12
11
|
module InternedCounts
|
|
13
|
-
|
|
12
|
+
extend self
|
|
14
13
|
|
|
15
|
-
# @return [Hash] `target`
|
|
16
14
|
def absorb_counts(target, source, identities)
|
|
17
15
|
source.each do |key, count|
|
|
18
16
|
interned = identities[key]
|
|
@@ -2,72 +2,48 @@
|
|
|
2
2
|
|
|
3
3
|
module SimpleCov
|
|
4
4
|
module Combine
|
|
5
|
-
#
|
|
6
|
-
# Combine two different lines coverage results on same file
|
|
7
|
-
#
|
|
8
|
-
# Should be called through `CoverageAccumulator`.
|
|
9
5
|
module LinesCombiner
|
|
10
|
-
|
|
6
|
+
extend self
|
|
11
7
|
|
|
12
|
-
# Folds `source` into `target` rather than building a third array
|
|
13
|
-
#
|
|
14
|
-
# reference
|
|
15
|
-
# hash being passed into a second merge) would see it change.
|
|
16
|
-
# `source` is never touched.
|
|
8
|
+
# Folds `source` into `target` rather than building a third array, so only
|
|
9
|
+
# for a `target` the caller owns outright: a caller still holding a
|
|
10
|
+
# reference would see it change. `source` is never touched.
|
|
17
11
|
#
|
|
18
|
-
# Two runs of the same source file should agree on which lines
|
|
19
|
-
#
|
|
20
|
-
#
|
|
21
|
-
#
|
|
22
|
-
# which would silently drop an uncovered line from the
|
|
23
|
-
# denominator and inflate the percentage:
|
|
12
|
+
# Two runs of the same source file should agree on which lines are
|
|
13
|
+
# coverage-relevant (`nil` for comments, `0`+ for executable). When they
|
|
14
|
+
# don't, "relevant on either side" wins rather than masking a real `0` as
|
|
15
|
+
# `nil`, which would drop an uncovered line from the denominator:
|
|
24
16
|
#
|
|
25
|
-
#
|
|
26
|
-
#
|
|
27
|
-
#
|
|
28
|
-
#
|
|
29
|
-
# The loop is written out rather than dispatching a block per
|
|
30
|
-
# element because this is the innermost loop of a merge: a
|
|
31
|
-
# 160-worker run over ~1,800 files folds tens of millions of line
|
|
32
|
-
# counts through it, and a call per count is a measurable share of
|
|
33
|
-
# that.
|
|
34
|
-
#
|
|
35
|
-
# @return [Array] the array to keep — `target` itself once there is one
|
|
17
|
+
# nil + nil = nil
|
|
18
|
+
# nil + int = int (preserves a relevant-but-uncovered 0)
|
|
19
|
+
# int + int = int (sum)
|
|
36
20
|
def merge_into(target, source)
|
|
37
21
|
return target unless source
|
|
38
22
|
return source.dup unless target
|
|
39
23
|
|
|
40
24
|
size = source.size
|
|
41
|
-
target.
|
|
25
|
+
target.fill(nil, target.size, size - target.size)
|
|
42
26
|
sum_into(target, source, size)
|
|
43
27
|
end
|
|
44
28
|
|
|
45
|
-
#
|
|
29
|
+
# The loop is written out rather than dispatching a block per element
|
|
30
|
+
# because this is the innermost loop of a merge: a 160-worker run over
|
|
31
|
+
# ~1,800 files folds tens of millions of line counts through it.
|
|
46
32
|
#
|
|
47
|
-
#
|
|
48
|
-
# hand-edited or foreign resultset
|
|
49
|
-
#
|
|
50
|
-
#
|
|
51
|
-
# the block dispatch this loop exists to avoid, and the well-formed
|
|
52
|
-
# fast path stays branch-for-branch what it was.
|
|
33
|
+
# Both sides go through `to_i`, which is what keeps the merge total over
|
|
34
|
+
# external input: a count a hand-edited or foreign resultset wrote as "3" or
|
|
35
|
+
# as a JSON float merges to a wrong answer instead of raising out of the
|
|
36
|
+
# middle of a merge. On the accumulated side it doubles as the nil arm.
|
|
53
37
|
def sum_into(target, source, size)
|
|
54
38
|
index = 0
|
|
55
39
|
while index < size
|
|
56
40
|
if (value = source[index])
|
|
57
|
-
|
|
58
|
-
existing = target[index]
|
|
59
|
-
target[index] = existing.is_a?(Integer) ? existing + value : coerce_add(existing, value)
|
|
41
|
+
target[index] = target.at(index).to_i + value.to_i
|
|
60
42
|
end
|
|
61
43
|
index += 1
|
|
62
44
|
end
|
|
63
45
|
target
|
|
64
46
|
end
|
|
65
|
-
|
|
66
|
-
# The rare arm: `existing` is nil (line not yet relevant in the
|
|
67
|
-
# target) or malformed external input.
|
|
68
|
-
def coerce_add(existing, value)
|
|
69
|
-
existing.nil? ? value : existing.to_i + value
|
|
70
|
-
end
|
|
71
47
|
end
|
|
72
48
|
end
|
|
73
49
|
end
|
|
@@ -6,44 +6,24 @@ require_relative "interned_counts"
|
|
|
6
6
|
|
|
7
7
|
module SimpleCov
|
|
8
8
|
module Combine
|
|
9
|
-
#
|
|
10
|
-
# Combine different method coverage results on a single file.
|
|
11
|
-
#
|
|
12
|
-
# Should be called through `CoverageAccumulator`.
|
|
13
9
|
module MethodsCombiner
|
|
14
|
-
|
|
10
|
+
extend self
|
|
15
11
|
|
|
16
|
-
#
|
|
17
|
-
# Return merged methods or the existing methods if other is missing.
|
|
18
|
-
#
|
|
19
12
|
# Method coverage maps `[class, name, start_line, start_col, end_line,
|
|
20
|
-
# end_col]` keys to hit counts. Keys are matched on their
|
|
21
|
-
#
|
|
22
|
-
#
|
|
23
|
-
#
|
|
24
|
-
#
|
|
25
|
-
#
|
|
26
|
-
# full key would keep both, letting a never-called copy's 0 shadow a
|
|
27
|
-
# covered method after merge (issue #1234). Combining sums the hit
|
|
28
|
-
# counts for matching methods and preserves methods that only appear
|
|
29
|
-
# in one result.
|
|
30
|
-
#
|
|
31
|
-
# @return [Hash]
|
|
32
|
-
#
|
|
13
|
+
# end_col]` keys to hit counts. Keys are matched on their source identity,
|
|
14
|
+
# the location alone, because Ruby records one entry per defined method: the
|
|
15
|
+
# same `define_method` block defined onto different classes, or under
|
|
16
|
+
# different names, in different processes arrives with different receivers
|
|
17
|
+
# for the same source method, and matching on the full key would keep both,
|
|
18
|
+
# letting a never-called copy's 0 shadow a covered method (#1234).
|
|
33
19
|
def combine(coverage_a, coverage_b)
|
|
34
20
|
merged = absorb({}, coverage_a) #: Hash[untyped, [untyped, Integer]]
|
|
35
21
|
materialize(absorb(merged, coverage_b))
|
|
36
22
|
end
|
|
37
23
|
|
|
38
|
-
# Folds `coverage` into `target`, an interned table keyed by method
|
|
39
|
-
# source identity. See `BranchesCombiner.absorb` for why a fold keeps
|
|
40
|
-
# its accumulator interned.
|
|
41
|
-
#
|
|
42
24
|
# A `nil` `target` stays `nil` until some resultset actually carries
|
|
43
|
-
# methods, so a merge can tell "no method data anywhere" apart from
|
|
44
|
-
#
|
|
45
|
-
#
|
|
46
|
-
# @return [Hash, nil] `target`, created on the first coverage seen
|
|
25
|
+
# methods, so a merge can tell "no method data anywhere" apart from "method
|
|
26
|
+
# data that covers nothing".
|
|
47
27
|
def absorb(target, coverage)
|
|
48
28
|
return target unless coverage
|
|
49
29
|
|
|
@@ -51,16 +31,10 @@ module SimpleCov
|
|
|
51
31
|
InternedCounts.absorb_counts(target, coverage, identities)
|
|
52
32
|
end
|
|
53
33
|
|
|
54
|
-
# Turns an interned table back into the tuple-keyed hash the rest of
|
|
55
|
-
# SimpleCov reads. Done once, at the end of a fold.
|
|
56
|
-
#
|
|
57
|
-
# @return [Hash]
|
|
58
34
|
def materialize(target)
|
|
59
35
|
target.values.to_h
|
|
60
36
|
end
|
|
61
37
|
|
|
62
|
-
# Bounded by the project's method count, like `RubyDataParser`'s parse
|
|
63
|
-
# cache.
|
|
64
38
|
def identities
|
|
65
39
|
@identities ||= IdentityInterner.build { |key| source_identity(key) }
|
|
66
40
|
end
|
|
@@ -4,29 +4,12 @@ require_relative "coverage_accumulator"
|
|
|
4
4
|
|
|
5
5
|
module SimpleCov
|
|
6
6
|
module Combine
|
|
7
|
-
# There might be reports from different kinds of tests,
|
|
8
|
-
# e.g. RSpec and Cucumber. We need to combine their results
|
|
9
|
-
# into unified one. This class does that.
|
|
10
|
-
# To unite the results on file basis, it leverages
|
|
11
|
-
# the combine of lines and branches inside each file within given results.
|
|
12
7
|
module ResultsCombiner
|
|
13
|
-
|
|
8
|
+
extend self
|
|
14
9
|
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
# ==> CoverageAccumulator::MergedFile: hold one file's merged coverage.
|
|
19
|
-
# ===> LinesCombiner: combine lines results.
|
|
20
|
-
# ===> BranchesCombiner: combine branches results.
|
|
21
|
-
# ===> MethodsCombiner: combine methods results.
|
|
22
|
-
#
|
|
23
|
-
# Callers that read their results one at a time (`ResultMerger`, to
|
|
24
|
-
# keep a big CI run's resultsets out of memory all at once) should
|
|
25
|
-
# drive a `CoverageAccumulator` directly instead of collecting the
|
|
26
|
-
# results to pass here.
|
|
27
|
-
#
|
|
28
|
-
# @return [Hash]
|
|
29
|
-
#
|
|
10
|
+
# Callers that read their results one at a time, to keep a big CI run's
|
|
11
|
+
# resultsets out of memory at once, should drive a `CoverageAccumulator`
|
|
12
|
+
# directly instead of collecting the results to pass here.
|
|
30
13
|
def combine(*results)
|
|
31
14
|
accumulator = CoverageAccumulator.new
|
|
32
15
|
results.each { |result| accumulator.absorb(result) }
|
data/lib/simplecov/combine.rb
CHANGED
|
@@ -1,12 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module SimpleCov
|
|
4
|
-
# Namespace for the pieces that combine coverage results.
|
|
5
|
-
#
|
|
6
|
-
# `CoverageAccumulator` is the entry point: it folds any number of
|
|
7
|
-
# resultsets together, delegating one file's lines, branches and methods to
|
|
8
|
-
# the combiner modules alongside it.
|
|
9
|
-
#
|
|
10
4
|
module Combine
|
|
11
5
|
end
|
|
12
6
|
end
|