simplecov 0.22.0 → 1.3.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/LICENSE +1 -1
- data/README.md +173 -875
- data/exe/simplecov +8 -0
- data/lib/minitest/simplecov_plugin.rb +18 -7
- data/lib/simplecov/atomic_file.rb +67 -0
- data/lib/simplecov/autostart.rb +9 -0
- 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/annotations.rb +166 -0
- data/lib/simplecov/cli/badge/svg.rb +70 -0
- data/lib/simplecov/cli/badge.rb +62 -0
- data/lib/simplecov/cli/clean.rb +86 -0
- data/lib/simplecov/cli/command_helpers.rb +91 -0
- data/lib/simplecov/cli/completions/scripts.rb +114 -0
- data/lib/simplecov/cli/completions.rb +83 -0
- data/lib/simplecov/cli/coverage.rb +83 -0
- data/lib/simplecov/cli/coverage_file.rb +109 -0
- 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 +115 -0
- data/lib/simplecov/cli/dotfile.rb +156 -0
- 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 +115 -0
- data/lib/simplecov/cli/open.rb +45 -0
- data/lib/simplecov/cli/patch/changed_lines.rb +149 -0
- data/lib/simplecov/cli/patch/output.rb +151 -0
- data/lib/simplecov/cli/patch.rb +175 -0
- data/lib/simplecov/cli/ratchet/output.rb +44 -0
- data/lib/simplecov/cli/ratchet.rb +83 -0
- data/lib/simplecov/cli/report.rb +91 -0
- data/lib/simplecov/cli/run.rb +35 -0
- data/lib/simplecov/cli/serve/report_preparer.rb +30 -0
- data/lib/simplecov/cli/serve/static_file_handler.rb +128 -0
- data/lib/simplecov/cli/serve.rb +88 -0
- 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 +33 -0
- data/lib/simplecov/cli/uncovered.rb +127 -0
- data/lib/simplecov/cli/usage.rb +153 -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 +114 -0
- data/lib/simplecov/color.rb +65 -0
- data/lib/simplecov/combine/branches_combiner.rb +44 -21
- data/lib/simplecov/combine/coverage_accumulator.rb +175 -0
- data/lib/simplecov/combine/identity_interner.rb +27 -0
- data/lib/simplecov/combine/interned_counts.rb +28 -0
- data/lib/simplecov/combine/lines_combiner.rb +35 -29
- data/lib/simplecov/combine/methods_combiner.rb +48 -0
- data/lib/simplecov/combine/results_combiner.rb +9 -49
- data/lib/simplecov/combine.rb +0 -24
- data/lib/simplecov/command_guesser.rb +99 -42
- data/lib/simplecov/configuration/baseline.rb +31 -0
- data/lib/simplecov/configuration/coverage.rb +184 -0
- data/lib/simplecov/configuration/coverage_criteria.rb +143 -0
- data/lib/simplecov/configuration/deprecations.rb +21 -0
- data/lib/simplecov/configuration/eval_coverage.rb +37 -0
- data/lib/simplecov/configuration/filters.rb +131 -0
- data/lib/simplecov/configuration/formatting.rb +129 -0
- data/lib/simplecov/configuration/groups.rb +48 -0
- data/lib/simplecov/configuration/history.rb +44 -0
- data/lib/simplecov/configuration/ignored_entries.rb +72 -0
- data/lib/simplecov/configuration/merging.rb +197 -0
- 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 +150 -0
- data/lib/simplecov/configuration/view_coverage.rb +37 -0
- data/lib/simplecov/configuration.rb +100 -443
- 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 +21 -0
- data/lib/simplecov/coverage_statistics.rb +24 -28
- data/lib/simplecov/coverage_violations.rb +254 -0
- data/lib/simplecov/current_run.rb +61 -0
- data/lib/simplecov/defaults.rb +20 -28
- data/lib/simplecov/deprecation.rb +63 -0
- data/lib/simplecov/directive/erb.rb +44 -0
- data/lib/simplecov/directive/haml.rb +33 -0
- data/lib/simplecov/directive/indented_template.rb +37 -0
- data/lib/simplecov/directive/slim.rb +35 -0
- data/lib/simplecov/directive/template.rb +29 -0
- data/lib/simplecov/directive.rb +152 -0
- data/lib/simplecov/exit_codes/baseline_check.rb +46 -0
- data/lib/simplecov/exit_codes/check.rb +39 -0
- data/lib/simplecov/exit_codes/exit_code_handling.rb +24 -5
- data/lib/simplecov/exit_codes/maximum_coverage_drop_check.rb +17 -67
- 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 +27 -0
- data/lib/simplecov/exit_codes/minimum_coverage_by_file_check.rb +20 -38
- data/lib/simplecov/exit_codes/minimum_coverage_by_group_check.rb +28 -0
- data/lib/simplecov/exit_codes/minimum_overall_coverage_check.rb +37 -34
- data/lib/simplecov/exit_codes.rb +26 -0
- data/lib/simplecov/exit_handling.rb +163 -0
- data/lib/simplecov/file_list.rb +63 -48
- data/lib/simplecov/filter.rb +98 -42
- data/lib/simplecov/formatter/base.rb +100 -0
- data/lib/simplecov/formatter/baseline_formatter.rb +98 -0
- data/lib/simplecov/formatter/coverage_json_writer.rb +95 -0
- data/lib/simplecov/formatter/html_formatter/public/index.html +93 -0
- data/lib/simplecov/formatter/html_formatter/viewer_data_validator.rb +153 -0
- data/lib/simplecov/formatter/html_formatter.rb +88 -0
- data/lib/simplecov/formatter/json_formatter/errors_formatter.rb +122 -0
- data/lib/simplecov/formatter/json_formatter/production_section_formatter.rb +48 -0
- data/lib/simplecov/formatter/json_formatter/result_hash_formatter.rb +132 -0
- data/lib/simplecov/formatter/json_formatter/source_file_formatter.rb +97 -0
- data/lib/simplecov/formatter/json_formatter.rb +40 -0
- data/lib/simplecov/formatter/multi_formatter.rb +8 -10
- data/lib/simplecov/formatter/simple_formatter.rb +12 -15
- data/lib/simplecov/formatter.rb +21 -1
- data/lib/simplecov/group_names.rb +33 -0
- data/lib/simplecov/history.rb +115 -0
- data/lib/simplecov/last_run.rb +16 -5
- data/lib/simplecov/lines_classifier.rb +67 -19
- data/lib/simplecov/load_global_config.rb +12 -4
- data/lib/simplecov/parallel_adapters/base.rb +50 -0
- data/lib/simplecov/parallel_adapters/generic.rb +33 -0
- data/lib/simplecov/parallel_adapters/parallel_tests.rb +72 -0
- data/lib/simplecov/parallel_adapters.rb +54 -0
- data/lib/simplecov/parallel_coordination.rb +124 -0
- data/lib/simplecov/parallel_result_merger/worker_payload.rb +33 -0
- data/lib/simplecov/parallel_result_merger.rb +171 -0
- data/lib/simplecov/process.rb +34 -12
- 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/bundler_filter.rb +1 -1
- data/lib/simplecov/profiles/hidden_filter.rb +1 -1
- data/lib/simplecov/profiles/rails.rb +13 -10
- data/lib/simplecov/profiles/root_filter.rb +2 -5
- data/lib/simplecov/profiles/strict.rb +22 -0
- data/lib/simplecov/profiles/test_frameworks.rb +1 -4
- data/lib/simplecov/profiles.rb +26 -19
- data/lib/simplecov/report_deferral.rb +45 -0
- data/lib/simplecov/report_stamp.rb +25 -0
- data/lib/simplecov/result/filter_config.rb +19 -0
- data/lib/simplecov/result/missing_source_files_reporter.rb +48 -0
- data/lib/simplecov/result/serialization.rb +43 -0
- data/lib/simplecov/result/source_file_builder.rb +46 -0
- data/lib/simplecov/result.rb +123 -47
- data/lib/simplecov/result_adapter.rb +130 -11
- data/lib/simplecov/result_merger/contexts.rb +22 -0
- data/lib/simplecov/result_merger/legacy_format_adapter.rb +24 -0
- data/lib/simplecov/result_merger/resultset_file.rb +67 -0
- data/lib/simplecov/result_merger/resultset_run_identity.rb +65 -0
- data/lib/simplecov/result_merger/resultset_store.rb +62 -0
- data/lib/simplecov/result_merger/unloaded_files.rb +95 -0
- data/lib/simplecov/result_merger.rb +120 -124
- data/lib/simplecov/result_processing.rb +188 -0
- data/lib/simplecov/run_identity.rb +75 -0
- data/lib/simplecov/simulate_coverage.rb +72 -17
- data/lib/simplecov/source_file/branch.rb +8 -35
- data/lib/simplecov/source_file/branch_builder.rb +95 -0
- data/lib/simplecov/source_file/builder_context.rb +28 -0
- data/lib/simplecov/source_file/line.rb +17 -32
- data/lib/simplecov/source_file/line_builder.rb +37 -0
- data/lib/simplecov/source_file/method.rb +54 -0
- data/lib/simplecov/source_file/method_builder.rb +51 -0
- data/lib/simplecov/source_file/ruby_data_parser.rb +103 -0
- data/lib/simplecov/source_file/skip_chunks.rb +72 -0
- data/lib/simplecov/source_file/source_loader.rb +89 -0
- data/lib/simplecov/source_file/statistics.rb +76 -0
- data/lib/simplecov/source_file.rb +64 -260
- data/lib/simplecov/static_coverage_extractor/condition_folding.rb +155 -0
- data/lib/simplecov/static_coverage_extractor/location_conventions.rb +191 -0
- data/lib/simplecov/static_coverage_extractor/method_collector.rb +45 -0
- data/lib/simplecov/static_coverage_extractor/prism_compat.rb +46 -0
- data/lib/simplecov/static_coverage_extractor/value_position.rb +61 -0
- data/lib/simplecov/static_coverage_extractor/visitor.rb +163 -0
- data/lib/simplecov/static_coverage_extractor.rb +78 -0
- 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 +62 -0
- data/lib/simplecov/useless_results_remover.rb +17 -7
- 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 +5 -0
- data/lib/simplecov.rb +143 -394
- data/lib/simplecov_json_formatter.rb +5 -0
- data/man/simplecov.1 +354 -0
- data/sig/simplecov.rbs +1710 -0
- metadata +183 -61
- data/CHANGELOG.md +0 -191
- data/doc/alternate-formatters.md +0 -71
- data/doc/commercial-services.md +0 -25
- data/doc/editor-integration.md +0 -18
- data/lib/simplecov/combine/files_combiner.rb +0 -24
- data/lib/simplecov/default_formatter.rb +0 -20
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "coverage"
|
|
4
|
+
|
|
3
5
|
module SimpleCov
|
|
4
|
-
#
|
|
5
|
-
# Responsible for adapting the format of the coverage result whether it's default or with statistics
|
|
6
|
-
#
|
|
7
6
|
class ResultAdapter
|
|
8
7
|
attr_reader :result
|
|
9
8
|
|
|
@@ -11,20 +10,140 @@ module SimpleCov
|
|
|
11
10
|
@result = result
|
|
12
11
|
end
|
|
13
12
|
|
|
14
|
-
def self.call(*
|
|
15
|
-
new(*
|
|
13
|
+
def self.call(*)
|
|
14
|
+
new(*).adapt
|
|
16
15
|
end
|
|
17
16
|
|
|
18
17
|
def adapt
|
|
19
18
|
return unless result
|
|
20
19
|
|
|
21
|
-
result.
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
20
|
+
result.to_h do |file_name, cover_statistic|
|
|
21
|
+
[file_name, adapt_one(file_name, cover_statistic)]
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
# Pre-0.18 resultsets pointed each filename straight at a line-coverage
|
|
28
|
+
# array; everything since uses the `{lines:, branches:, methods:}` shape.
|
|
29
|
+
def adapt_one(file_name, cover_statistic)
|
|
30
|
+
return {"lines" => cover_statistic} if cover_statistic.instance_of?(Array)
|
|
31
|
+
|
|
32
|
+
adapt_oneshot_lines_if_needed(file_name, cover_statistic)
|
|
33
|
+
normalize_method_keys(cover_statistic)
|
|
34
|
+
aggregate_duplicated_branches(cover_statistic)
|
|
35
|
+
cover_statistic
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Normalizes memory addresses in method coverage keys so results from
|
|
39
|
+
# different processes can be merged: anonymous class names like
|
|
40
|
+
# "#<Class:0x00007ff19ab24790>" get inconsistent addresses across runs, and
|
|
41
|
+
# address widths vary by runtime.
|
|
42
|
+
ADDRESS_PATTERN = /0x\h+/
|
|
43
|
+
private_constant :ADDRESS_PATTERN
|
|
44
|
+
|
|
45
|
+
ADDRESS_PLACEHOLDER = "0x0"
|
|
46
|
+
private_constant :ADDRESS_PLACEHOLDER
|
|
47
|
+
|
|
48
|
+
# Strips the `#<Class:Foo>` wrapper Ruby's Coverage adds to singleton-class
|
|
49
|
+
# method keys. `module_function` and class methods get recorded both as
|
|
50
|
+
# singleton and instance/module entries pointing at the same source
|
|
51
|
+
# location, and only one of the two is ever reachable at runtime. Only
|
|
52
|
+
# named constants: anonymous-class addresses are left to ADDRESS_PATTERN.
|
|
53
|
+
SINGLETON_WRAPPER_PATTERN = /\A#<Class:([A-Z_][\w:]*)>\z/
|
|
54
|
+
private_constant :SINGLETON_WRAPPER_PATTERN
|
|
55
|
+
|
|
56
|
+
# Ruby's method coverage records one entry per defined method, not per
|
|
57
|
+
# source location: a block handed to `define_method` from a shared code
|
|
58
|
+
# path yields a separate `[receiver, name, location]` entry for every class
|
|
59
|
+
# it's defined on and every name it's defined under, all pointing at the
|
|
60
|
+
# same source. A file-based report can only express "was the method at this
|
|
61
|
+
# location ever executed", so entries are aggregated by location alone,
|
|
62
|
+
# summing hits. Otherwise each generated copy that never ran shows as a
|
|
63
|
+
# phantom uncovered method on a line whose line coverage is 100% (#1234).
|
|
64
|
+
# The first entry's normalized key is kept for display.
|
|
65
|
+
def normalize_method_keys(cover_statistic)
|
|
66
|
+
methods = cover_statistic[:methods]
|
|
67
|
+
return unless methods
|
|
68
|
+
|
|
69
|
+
aggregated = {} #: Hash[untyped, [untyped, Integer]]
|
|
70
|
+
methods.each_with_object(aggregated) do |(key, count), memo|
|
|
71
|
+
location = key[2..] #: Array[untyped]
|
|
72
|
+
retained_key, existing = memo[location] || [normalize_method_key(key), 0]
|
|
73
|
+
memo[location] = [retained_key, existing + count]
|
|
27
74
|
end
|
|
75
|
+
cover_statistic[:methods] = aggregated.values.to_h
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def normalize_method_key(key)
|
|
79
|
+
receiver, *rest = key
|
|
80
|
+
normalized_receiver = class_display_name(receiver)
|
|
81
|
+
.gsub(ADDRESS_PATTERN, ADDRESS_PLACEHOLDER)
|
|
82
|
+
.sub(SINGLETON_WRAPPER_PATTERN, '\1')
|
|
83
|
+
[normalized_receiver, *rest]
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Rendering a class name can execute user code: a singleton class's `to_s`
|
|
87
|
+
# renders its attached object via `#inspect`, which a module can shadow
|
|
88
|
+
# with an incompatible signature (Liquid::Utils defines
|
|
89
|
+
# `inspect(value, max_depth = 2)` as a module_function, so rendering
|
|
90
|
+
# `#<Class:Liquid::Utils>` raises ArgumentError). A coverage report must
|
|
91
|
+
# never crash the host suite over that (#1236).
|
|
92
|
+
def class_display_name(klass)
|
|
93
|
+
klass.to_s
|
|
94
|
+
rescue
|
|
95
|
+
singleton_wrapper_name(klass) || Object.instance_method(:to_s).bind_call(klass)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# `singleton_class?` is a Module method, so a receiver that is neither a
|
|
99
|
+
# class nor a module has to be turned away first. Anything that then
|
|
100
|
+
# answers `singleton_class?` truthily is a Class, so `attached_object`
|
|
101
|
+
# applies, reached through a bound method so shadowing cannot divert it.
|
|
102
|
+
def singleton_wrapper_name(klass)
|
|
103
|
+
return nil unless klass.is_a?(Module) && klass.singleton_class?
|
|
104
|
+
|
|
105
|
+
attached = Class.instance_method(:attached_object).bind_call(klass)
|
|
106
|
+
name = Module.instance_method(:name).bind_call(attached) if attached.is_a?(Module)
|
|
107
|
+
name && "#<Class:#{name}>"
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Ruby's eval coverage records a fresh set of branch entries for every
|
|
111
|
+
# compile of an eval'd string: a template rendered through multiple view
|
|
112
|
+
# classes yields several `[:if, id, location]` conditions at identical
|
|
113
|
+
# coordinates, each counting only the renders that flowed through that
|
|
114
|
+
# compile. Reported as-is they inflate the branch denominator and turn a
|
|
115
|
+
# side covered under a different compile into a phantom miss (#1235).
|
|
116
|
+
# Absorbing into an empty table dedups, since BranchesCombiner keys arms on
|
|
117
|
+
# location identity. Regular source can never produce two conditions at the
|
|
118
|
+
# same location, so this is a no-op outside eval.
|
|
119
|
+
def aggregate_duplicated_branches(cover_statistic)
|
|
120
|
+
branches = cover_statistic[:branches]
|
|
121
|
+
return unless branches
|
|
122
|
+
|
|
123
|
+
combiner = Combine::BranchesCombiner
|
|
124
|
+
cover_statistic[:branches] = combiner.materialize(combiner.absorb({}, branches))
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def adapt_oneshot_lines_if_needed(file_name, cover_statistic)
|
|
128
|
+
return unless cover_statistic.key?(:oneshot_lines)
|
|
129
|
+
|
|
130
|
+
oneshot_lines = cover_statistic.delete(:oneshot_lines)
|
|
131
|
+
line_stub = build_line_stub(file_name)
|
|
132
|
+
oneshot_lines.each { |covered_line| line_stub[covered_line - 1] = 1 }
|
|
133
|
+
cover_statistic[:lines] = line_stub
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# A file that has vanished or no longer parses has no stub to build from,
|
|
137
|
+
# and neither does a runtime without `Coverage.line_stub` (TruffleRuby), so
|
|
138
|
+
# start from nothing: the assignments in `adapt_oneshot_lines_if_needed`
|
|
139
|
+
# grow the array to the highest covered line, which is as far as the
|
|
140
|
+
# oneshot data reaches anyway.
|
|
141
|
+
def build_line_stub(file_name)
|
|
142
|
+
return [] unless Coverage.respond_to?(:line_stub)
|
|
143
|
+
|
|
144
|
+
Coverage.line_stub(file_name)
|
|
145
|
+
rescue Errno::ENOENT, SyntaxError
|
|
146
|
+
[]
|
|
28
147
|
end
|
|
29
148
|
end
|
|
30
149
|
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SimpleCov
|
|
4
|
+
module ResultMerger
|
|
5
|
+
module Contexts
|
|
6
|
+
extend self
|
|
7
|
+
|
|
8
|
+
# The same union-or-drop rule the cross-suite merge applies: the union when
|
|
9
|
+
# both sides recorded a map, no map at all when either did not, since a
|
|
10
|
+
# partial map would present one runner's tests as the pair's. `entry` starts
|
|
11
|
+
# as a copy of `incoming`, so the drop has to remove the key rather than
|
|
12
|
+
# merely decline to add it.
|
|
13
|
+
def carry(entry, existing, incoming)
|
|
14
|
+
ours = ContextMap.from_hash(existing["contexts"])
|
|
15
|
+
theirs = ContextMap.from_hash(incoming["contexts"])
|
|
16
|
+
return entry.except("contexts") unless ours && theirs
|
|
17
|
+
|
|
18
|
+
entry.merge("contexts" => ours.absorb(theirs).to_h)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SimpleCov
|
|
4
|
+
module ResultMerger
|
|
5
|
+
module LegacyFormatAdapter
|
|
6
|
+
extend self
|
|
7
|
+
|
|
8
|
+
def call(result)
|
|
9
|
+
pre_0_18?(result) ? upgrade(result) : result
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# Pre-0.18 coverage data pointed from file directly to an array of line
|
|
13
|
+
# coverage rather than a `{"lines" => [...]}` hash.
|
|
14
|
+
def pre_0_18?(result)
|
|
15
|
+
_key, data = result.first
|
|
16
|
+
data.instance_of?(Array)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def upgrade(result)
|
|
20
|
+
result.transform_values { |line_coverage_data| {"lines" => line_coverage_data} }
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module SimpleCov
|
|
6
|
+
module ResultMerger
|
|
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.
|
|
11
|
+
module ResultsetFile
|
|
12
|
+
extend self
|
|
13
|
+
|
|
14
|
+
def parse(path)
|
|
15
|
+
data = read(path)
|
|
16
|
+
decode(data)
|
|
17
|
+
end
|
|
18
|
+
|
|
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.
|
|
23
|
+
def read(path)
|
|
24
|
+
data = File.read(path)
|
|
25
|
+
return if data.match?(/\A\s*\z/)
|
|
26
|
+
|
|
27
|
+
data
|
|
28
|
+
rescue Errno::ENOENT
|
|
29
|
+
nil
|
|
30
|
+
end
|
|
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
|
+
def decode(content)
|
|
36
|
+
return {} unless content
|
|
37
|
+
|
|
38
|
+
parsed = JSON.parse(content)
|
|
39
|
+
return {} if parsed.nil?
|
|
40
|
+
|
|
41
|
+
parsed.instance_of?(Hash) ? drop_malformed_entries(parsed) : invalid_resultset
|
|
42
|
+
rescue
|
|
43
|
+
invalid_resultset
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def drop_malformed_entries(resultset)
|
|
47
|
+
malformed, valid = resultset.partition { |_command_name, data| !well_formed_entry?(data) }
|
|
48
|
+
return resultset if malformed.empty?
|
|
49
|
+
|
|
50
|
+
warn "[SimpleCov]: Warning! Ignoring malformed resultset entries: #{malformed.map(&:first).sort.join(", ")}"
|
|
51
|
+
valid.to_h
|
|
52
|
+
end
|
|
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.
|
|
57
|
+
def well_formed_entry?(data)
|
|
58
|
+
data.instance_of?(Hash) && data["timestamp"].is_a?(Numeric) && data["coverage"].instance_of?(Hash)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def invalid_resultset
|
|
62
|
+
warn "[SimpleCov]: Warning! Parsing JSON content of resultset file failed"
|
|
63
|
+
{}
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SimpleCov
|
|
4
|
+
module ResultMerger
|
|
5
|
+
# Run and worker metadata queries used by resultset storage and parallel
|
|
6
|
+
# coordination.
|
|
7
|
+
module ResultsetRunIdentity
|
|
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.
|
|
13
|
+
def worker_identities_for_run(results, run_id, started_at)
|
|
14
|
+
results.filter_map do |command_name, data|
|
|
15
|
+
next unless current_run_entry?(data, run_id, started_at)
|
|
16
|
+
|
|
17
|
+
worker_id = data["worker_id"]
|
|
18
|
+
worker_id.to_s.empty? ? [:legacy, command_name] : [:worker, worker_id.to_s]
|
|
19
|
+
end.uniq
|
|
20
|
+
end
|
|
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.
|
|
25
|
+
def current_run_entry?(entry, run_id, started_at)
|
|
26
|
+
return false unless entry.instance_of?(Hash)
|
|
27
|
+
|
|
28
|
+
entry_run_id = entry["run_id"]
|
|
29
|
+
return fresh_entry?(entry, started_at) unless entry_run_id
|
|
30
|
+
return false unless entry_run_id.eql?(run_id)
|
|
31
|
+
return true if RunIdentity.authoritative?
|
|
32
|
+
|
|
33
|
+
fresh_entry?(entry, started_at)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def fresh_entry?(entry, started_at)
|
|
37
|
+
timestamp = entry["timestamp"]
|
|
38
|
+
timestamp.is_a?(Numeric) && started_at && timestamp >= started_at.to_f
|
|
39
|
+
end
|
|
40
|
+
|
|
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.
|
|
47
|
+
def concurrent_runner_entry?(entry, incoming = nil)
|
|
48
|
+
return false unless entry.instance_of?(Hash)
|
|
49
|
+
|
|
50
|
+
incoming_run_id = incoming["run_id"] if incoming.instance_of?(Hash)
|
|
51
|
+
started_at = SimpleCov.process_start_time
|
|
52
|
+
return true if current_run_entry?(entry, incoming_run_id, started_at)
|
|
53
|
+
|
|
54
|
+
written_after_start?(entry, started_at)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Strictly after, unlike `fresh_entry?`: an entry stamped at the exact
|
|
58
|
+
# instant we started is treated as leftover from a previous run.
|
|
59
|
+
def written_after_start?(entry, started_at)
|
|
60
|
+
timestamp = entry["timestamp"]
|
|
61
|
+
timestamp.is_a?(Numeric) && started_at && timestamp > started_at.to_f
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "json"
|
|
5
|
+
require "monitor"
|
|
6
|
+
require_relative "../atomic_file"
|
|
7
|
+
|
|
8
|
+
module SimpleCov
|
|
9
|
+
module ResultMerger
|
|
10
|
+
module ResultsetStore
|
|
11
|
+
LOCK_MONITOR = Monitor.new
|
|
12
|
+
private_constant :LOCK_MONITOR
|
|
13
|
+
|
|
14
|
+
extend self
|
|
15
|
+
|
|
16
|
+
def resultset_path
|
|
17
|
+
File.join(SimpleCov.coverage_path, ".resultset.json")
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def writelock_path
|
|
21
|
+
File.join(SimpleCov.coverage_path, ".resultset.json.lock")
|
|
22
|
+
end
|
|
23
|
+
|
|
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.
|
|
28
|
+
def write(resultset)
|
|
29
|
+
AtomicFile.write(resultset_path, "#{JSON.generate(resultset)}\n")
|
|
30
|
+
end
|
|
31
|
+
|
|
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.
|
|
35
|
+
def synchronize(&)
|
|
36
|
+
return yield if LOCK_MONITOR.mon_owned?
|
|
37
|
+
|
|
38
|
+
LOCK_MONITOR.synchronize { with_flock(&) }
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def with_flock(&)
|
|
42
|
+
FileUtils.mkdir_p(SimpleCov.coverage_path)
|
|
43
|
+
holding_writelock(&)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def holding_writelock
|
|
47
|
+
open_file(writelock_path, "w+") do |file|
|
|
48
|
+
file.flock(File::LOCK_EX)
|
|
49
|
+
yield
|
|
50
|
+
end
|
|
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
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,95 @@
|
|
|
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 to
|
|
6
|
+
# inject and which criteria simulated files carry. The mechanism, expanding
|
|
7
|
+
# globs and simulating each file, is `SimpleCov::UnloadedFileInjector`.
|
|
8
|
+
#
|
|
9
|
+
# Injection moved here from the individual processes because only the union
|
|
10
|
+
# of what they all loaded says what was really never loaded. Doing it per
|
|
11
|
+
# process meant N workers simulated the same file up to N times and the
|
|
12
|
+
# merge threw all but one away (#1250).
|
|
13
|
+
module UnloadedFiles
|
|
14
|
+
extend self
|
|
15
|
+
|
|
16
|
+
# A collector for the merge to hand `merge_valid_results`, gathering the
|
|
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.
|
|
20
|
+
def collector(into)
|
|
21
|
+
->(surviving) { into.merge(tracked_in(surviving)) }
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def tracked_in(resultset)
|
|
25
|
+
resultset.each_with_object(Set.new) do |(_command_name, data), set|
|
|
26
|
+
set.merge(Array(data["tracked_files"]))
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Concurrent workers sharing a command name may have been told to track
|
|
31
|
+
# different sets, so keep both rather than letting the later write win.
|
|
32
|
+
def carry_tracked(entry, existing, incoming)
|
|
33
|
+
tracked = Array(existing["tracked_files"]) | Array(incoming["tracked_files"])
|
|
34
|
+
tracked.empty? ? entry : entry.merge("tracked_files" => tracked)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Simulates each tracked file the merged coverage doesn't already carry.
|
|
38
|
+
# The paths come from the resultsets rather than this process's own
|
|
39
|
+
# configuration, which a standalone `collate` would not have. Idempotent,
|
|
40
|
+
# so resultsets that already carry their unloaded files pass through
|
|
41
|
+
# untouched.
|
|
42
|
+
def inject(coverage, tracked_files)
|
|
43
|
+
SimpleCov.inject_unloaded_files(
|
|
44
|
+
coverage, tracked_files.to_a,
|
|
45
|
+
synthesize: carries?(coverage, "branches") || carries?(coverage, "methods"),
|
|
46
|
+
lines: carries?(coverage, "lines")
|
|
47
|
+
)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# A simulated file should have the same shape as the files it is being
|
|
51
|
+
# merged alongside, so the criteria come from the merged data rather than
|
|
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.
|
|
56
|
+
#
|
|
57
|
+
# Falls back to the configuration when there is nothing to be consistent
|
|
58
|
+
# with, which is a merge of resultsets that carried no files.
|
|
59
|
+
def carries?(coverage, criterion)
|
|
60
|
+
return SimpleCov.public_send(CRITERION_PREDICATES.fetch(criterion)) if coverage.empty?
|
|
61
|
+
|
|
62
|
+
coverage.any? { |_filename, file_coverage| file_coverage[criterion] }
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
CRITERION_PREDICATES = {
|
|
66
|
+
"lines" => :line_coverage?,
|
|
67
|
+
"branches" => :branch_coverage?,
|
|
68
|
+
"methods" => :method_coverage?
|
|
69
|
+
}.freeze
|
|
70
|
+
private_constant :CRITERION_PREDICATES
|
|
71
|
+
|
|
72
|
+
# Which files no contributing process ever loaded. Injection reports the
|
|
73
|
+
# ones it added, but a file can also arrive already simulated from a
|
|
74
|
+
# resultset this merge didn't inject into, so the merged line counts are
|
|
75
|
+
# still consulted.
|
|
76
|
+
#
|
|
77
|
+
# A file is judged only when it has a relevant line. A branch-only or
|
|
78
|
+
# method-only run reports no line data at all for the files it loaded,
|
|
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%.
|
|
83
|
+
def never_executed(coverage)
|
|
84
|
+
coverage.each_with_object(Set.new) do |(filename, file_coverage), set|
|
|
85
|
+
counts = Array(file_coverage["lines"]) #: Array[Integer?]
|
|
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?
|
|
89
|
+
|
|
90
|
+
set << filename unless Combine::CoverageAccumulator.executed?(counts)
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|