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,27 +8,22 @@ require_relative "value_position"
|
|
|
8
8
|
|
|
9
9
|
module SimpleCov
|
|
10
10
|
module StaticCoverageExtractor
|
|
11
|
-
# Prism visitor that accumulates branch and method tuples in the
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
|
|
17
|
-
# downstream compares them. Only defined when Prism is loadable;
|
|
18
|
-
# `StaticCoverageExtractor.available?` is the runtime gate.
|
|
19
|
-
class Visitor < ::Prism::Visitor
|
|
20
|
-
# Method tuples and the class/module nesting that names them are
|
|
21
|
-
# collected by this mixin; this class focuses on branch extraction.
|
|
11
|
+
# Prism visitor that accumulates branch and method tuples in the shape
|
|
12
|
+
# Ruby's `Coverage` reports. Tuple ids are sequential across the file like
|
|
13
|
+
# `Coverage`'s, but the numbering order can differ. That's fine: the
|
|
14
|
+
# combiners intern on source span and the report output drops ids, so
|
|
15
|
+
# nothing downstream compares them. Only defined when Prism is loadable.
|
|
16
|
+
class Visitor < Prism::Visitor
|
|
22
17
|
include MethodCollector
|
|
23
|
-
# Source-range resolution, including the per-Ruby-version Coverage
|
|
24
|
-
# conventions. See issue #1226.
|
|
25
18
|
include LocationConventions
|
|
26
|
-
# Which literal `if`/`unless`/ternary conditions the compiler folds
|
|
27
|
-
# away (so we emit no branch for them).
|
|
28
19
|
include ConditionFolding
|
|
29
20
|
|
|
30
21
|
attr_reader :branches, :methods
|
|
31
22
|
|
|
23
|
+
# Prism's Visitor is a stateless dispatch table whose initializer is
|
|
24
|
+
# Object's, so no mutation of the `super` call can be told apart. It
|
|
25
|
+
# stays for the day that stops being true.
|
|
26
|
+
# mutant:disable
|
|
32
27
|
def initialize
|
|
33
28
|
super
|
|
34
29
|
@branches = {}
|
|
@@ -39,32 +34,25 @@ module SimpleCov
|
|
|
39
34
|
@suppress_methods = false
|
|
40
35
|
end
|
|
41
36
|
|
|
42
|
-
#
|
|
43
|
-
#
|
|
44
|
-
#
|
|
45
|
-
# emitting anything. Modern Rubies don't need it (see
|
|
46
|
-
# LocationConventions), so the pass is skipped there.
|
|
37
|
+
# On legacy Rubies the location of an empty branch arm depends on whether
|
|
38
|
+
# its construct is in value (tail) position, so precompute that once for
|
|
39
|
+
# the whole tree before emitting anything.
|
|
47
40
|
def visit_program_node(node)
|
|
48
|
-
# simplecov:disable branch — legacy-only arm; unreachable on the modern dogfood Ruby
|
|
49
41
|
@value_positions = ValuePositions.call(node) if LEGACY_COVERAGE_LOCATIONS
|
|
50
|
-
# simplecov:enable branch
|
|
51
42
|
super
|
|
52
43
|
end
|
|
53
44
|
|
|
54
|
-
# `if` / `unless` / postfix
|
|
55
|
-
#
|
|
56
|
-
#
|
|
57
|
-
#
|
|
58
|
-
#
|
|
59
|
-
# whole condition's range — we do the same.
|
|
45
|
+
# `if` / `unless` / postfix / ternary all parse as IfNode (or UnlessNode).
|
|
46
|
+
# Both carry a `then` arm and an optional `subsequent` (an ElseNode for
|
|
47
|
+
# `else`, another IfNode for `elsif`). When the subsequent is missing,
|
|
48
|
+
# Coverage synthesizes a `:else` arm attributed to the whole condition's
|
|
49
|
+
# range, and so do we.
|
|
60
50
|
#
|
|
61
|
-
# A folded condition emits no tuple, and on modern Rubies only its
|
|
62
|
-
#
|
|
63
|
-
#
|
|
64
|
-
#
|
|
65
|
-
#
|
|
66
|
-
# IfNode emits. On 3.2 the dead arm is visited too, branches only
|
|
67
|
-
# (see visit_folded_arms).
|
|
51
|
+
# A folded condition emits no tuple, and on modern Rubies only its live
|
|
52
|
+
# arm is descended into: the compiler eliminates the dead arm's entire
|
|
53
|
+
# subtree, so a branch or method nested there would be a phantom no
|
|
54
|
+
# loaded run can produce. On 3.2 the dead arm is visited too, branches
|
|
55
|
+
# only.
|
|
68
56
|
def visit_if_node(node)
|
|
69
57
|
verdict = folded_condition(node.predicate)
|
|
70
58
|
return visit_folded_arms(verdict, node.statements, PrismCompat.subsequent(node)) if verdict
|
|
@@ -86,9 +74,8 @@ module SimpleCov
|
|
|
86
74
|
super
|
|
87
75
|
end
|
|
88
76
|
|
|
89
|
-
#
|
|
90
|
-
#
|
|
91
|
-
# Coverage synthesizes one at the case's range.
|
|
77
|
+
# When there's no explicit `else`, Coverage synthesizes one at the case's
|
|
78
|
+
# range.
|
|
92
79
|
def visit_case_node(node)
|
|
93
80
|
emit_case_like(node, :when)
|
|
94
81
|
super
|
|
@@ -99,26 +86,22 @@ module SimpleCov
|
|
|
99
86
|
super
|
|
100
87
|
end
|
|
101
88
|
|
|
102
|
-
# One-line pattern matching: `x => pattern`
|
|
103
|
-
# `
|
|
104
|
-
#
|
|
105
|
-
#
|
|
106
|
-
#
|
|
107
|
-
# `=>` uses the whole expression, `in` uses just the pattern.
|
|
108
|
-
# simplecov:disable branch — legacy-only arms; unreachable on the modern dogfood Ruby
|
|
89
|
+
# One-line pattern matching: `x => pattern` and `x in pattern`. Ruby 3.3's
|
|
90
|
+
# Coverage reports these as a `:case` with an `:in` and an `:else` arm;
|
|
91
|
+
# 3.4 dropped them entirely, so this is legacy-only. The two forms differ
|
|
92
|
+
# only in where Coverage anchors the synthesized `:else`: `=>` uses the
|
|
93
|
+
# whole expression, `in` uses just the pattern.
|
|
109
94
|
def visit_match_required_node(node)
|
|
110
|
-
emit_oneline_pattern(node, node
|
|
95
|
+
emit_oneline_pattern(node, node) if LEGACY_COVERAGE_LOCATIONS
|
|
111
96
|
super
|
|
112
97
|
end
|
|
113
98
|
|
|
114
99
|
def visit_match_predicate_node(node)
|
|
115
|
-
emit_oneline_pattern(node, node.pattern
|
|
100
|
+
emit_oneline_pattern(node, node.pattern) if LEGACY_COVERAGE_LOCATIONS
|
|
116
101
|
super
|
|
117
102
|
end
|
|
118
|
-
# simplecov:enable branch
|
|
119
103
|
|
|
120
|
-
#
|
|
121
|
-
# else (the loop either runs the body or doesn't).
|
|
104
|
+
# A loop gets a single `:body` arm and no synthetic else.
|
|
122
105
|
def visit_while_node(node)
|
|
123
106
|
emit_loop(node, :while)
|
|
124
107
|
super
|
|
@@ -129,11 +112,10 @@ module SimpleCov
|
|
|
129
112
|
super
|
|
130
113
|
end
|
|
131
114
|
|
|
132
|
-
|
|
115
|
+
private
|
|
133
116
|
|
|
134
|
-
# IfNode and UnlessNode share a shape
|
|
135
|
-
#
|
|
136
|
-
# accessors. `if_like_else_location` hides that split.
|
|
117
|
+
# IfNode and UnlessNode share a shape but expose the trailing arm under
|
|
118
|
+
# different accessors, which `if_like_else_location` hides.
|
|
137
119
|
def emit_if_like(node, type)
|
|
138
120
|
then_loc = if_like_then_location(node, type)
|
|
139
121
|
else_loc = if_like_else_location(node, type)
|
|
@@ -151,32 +133,33 @@ module SimpleCov
|
|
|
151
133
|
}
|
|
152
134
|
end
|
|
153
135
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
build_tuple(:
|
|
158
|
-
build_tuple(:else, else_location) => 0
|
|
136
|
+
def emit_oneline_pattern(node, else_span)
|
|
137
|
+
@branches[build_tuple(:case, node)] = {
|
|
138
|
+
build_tuple(:in, node.pattern) => 0,
|
|
139
|
+
build_tuple(:else, else_span) => 0
|
|
159
140
|
}
|
|
160
141
|
end
|
|
161
|
-
# simplecov:enable
|
|
162
142
|
|
|
163
143
|
def emit_case_like(node, when_type)
|
|
164
144
|
arms = node.conditions.to_h do |when_node|
|
|
165
145
|
[build_tuple(when_type, case_arm_location(node, when_node, when_type)), 0]
|
|
166
146
|
end
|
|
167
147
|
arms[build_tuple(:else, else_arm_location(node))] = 0
|
|
168
|
-
@branches[build_tuple(:case, node
|
|
148
|
+
@branches[build_tuple(:case, node)] = arms
|
|
169
149
|
end
|
|
170
150
|
|
|
171
151
|
def emit_loop(node, type)
|
|
172
|
-
cond_tuple = build_tuple(type, node
|
|
152
|
+
cond_tuple = build_tuple(type, node)
|
|
173
153
|
@branches[cond_tuple] = {build_tuple(:body, loop_body_location(node)) => 0}
|
|
174
154
|
end
|
|
175
155
|
|
|
176
|
-
|
|
156
|
+
# `span` is anything that answers the four position accessors: a location
|
|
157
|
+
# from LocationConventions, or the node itself where the range wanted is
|
|
158
|
+
# the node's own.
|
|
159
|
+
def build_tuple(type, span)
|
|
177
160
|
id = @next_id
|
|
178
161
|
@next_id += 1
|
|
179
|
-
[type, id,
|
|
162
|
+
[type, id, span.start_line, span.start_column, span.end_line, span.end_column]
|
|
180
163
|
end
|
|
181
164
|
end
|
|
182
165
|
end
|
|
@@ -4,107 +4,91 @@ begin
|
|
|
4
4
|
require "prism"
|
|
5
5
|
rescue LoadError
|
|
6
6
|
# Prism isn't available on this Ruby (older than 3.3 without the gem).
|
|
7
|
-
# `StaticCoverageExtractor.available?` will return false and callers
|
|
8
|
-
#
|
|
7
|
+
# `StaticCoverageExtractor.available?` will return false and callers fall
|
|
8
|
+
# back to the previous "empty hashes" behavior.
|
|
9
9
|
end
|
|
10
10
|
|
|
11
11
|
module SimpleCov
|
|
12
12
|
# Static enumeration of the branches and methods Ruby's `Coverage` library
|
|
13
|
-
#
|
|
13
|
+
# would have reported if a file had been loaded with `branches: true` /
|
|
14
14
|
# `methods: true`. Used by `SimulateCoverage` to backfill data for files
|
|
15
|
-
# added via `cover` / `track_files` that were never
|
|
16
|
-
#
|
|
17
|
-
# symmetrically with their line coverage
|
|
18
|
-
# totals (see #1059).
|
|
15
|
+
# added via `cover` / `track_files` that were never required during the run,
|
|
16
|
+
# so unloaded files contribute to the branch/method denominators
|
|
17
|
+
# symmetrically with their line coverage (#1059).
|
|
19
18
|
#
|
|
20
|
-
#
|
|
21
|
-
#
|
|
22
|
-
#
|
|
23
|
-
#
|
|
24
|
-
#
|
|
25
|
-
# The emitted shape mirrors `Coverage.result[path]` for the same file:
|
|
26
|
-
# branches are nested as `{condition_tuple => {arm_tuple => 0, ...}}` and
|
|
27
|
-
# methods as `{["ClassName", :name, lines/cols] => 0}`. Position info
|
|
28
|
-
# comes from Prism's reported source locations; it doesn't always match
|
|
29
|
-
# `Coverage`'s byte-for-byte (the two parsers report slightly different
|
|
30
|
-
# column conventions for some constructs), but lines are reliable and
|
|
31
|
-
# downstream consumers that key off line numbers (the HTML formatter,
|
|
32
|
-
# SonarQube, etc.) see the data they expect.
|
|
19
|
+
# The emitted shape mirrors `Coverage.result[path]` for the same file.
|
|
20
|
+
# Position info comes from Prism's reported source locations; it doesn't
|
|
21
|
+
# always match `Coverage`'s byte-for-byte, but lines are reliable and
|
|
22
|
+
# downstream consumers that key off line numbers see the data they expect.
|
|
33
23
|
module StaticCoverageExtractor
|
|
34
|
-
|
|
24
|
+
extend self
|
|
35
25
|
|
|
36
|
-
# simplecov:disable branch
|
|
37
|
-
# The Prism-unavailable arm of this ternary is unreachable when Prism
|
|
38
|
-
# itself IS loadable — i.e., on every engine that exercises the dogfood
|
|
39
|
-
# report. Asserted-on by callers; tested indirectly via the
|
|
40
|
-
# `available?`-returns-false fallback path in SimulateCoverage's spec.
|
|
41
26
|
def available?
|
|
42
|
-
defined?(
|
|
27
|
+
defined?(Prism) ? true : false
|
|
43
28
|
end
|
|
44
|
-
# simplecov:enable branch
|
|
45
29
|
|
|
46
|
-
# Parse `source`
|
|
47
|
-
#
|
|
48
|
-
#
|
|
49
|
-
#
|
|
50
|
-
# extract — fall back to empty hashes."
|
|
30
|
+
# Parse `source` and return `{"branches" => {...}, "methods" => {...}}`
|
|
31
|
+
# matching the shape `Coverage.result[path]` produces. Returns nil on parse
|
|
32
|
+
# failure or when Prism isn't available, which callers treat as "couldn't
|
|
33
|
+
# extract, fall back to empty hashes".
|
|
51
34
|
def call(source)
|
|
52
|
-
# simplecov:disable branch — `then` arm unreachable when Prism IS loadable
|
|
53
35
|
return nil unless available?
|
|
54
36
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
result = ::Prism.parse(source)
|
|
37
|
+
result = Prism.parse(source)
|
|
58
38
|
return nil if result.failure?
|
|
59
39
|
|
|
60
40
|
visitor = Visitor.new
|
|
61
41
|
visitor.visit(result.value)
|
|
62
42
|
{"branches" => visitor.branches, "methods" => visitor.methods}
|
|
63
|
-
rescue
|
|
64
|
-
#
|
|
65
|
-
#
|
|
66
|
-
#
|
|
67
|
-
# the whole report. Defensive; hard to trigger from a real source
|
|
68
|
-
# input that Prism accepts at parse time.
|
|
43
|
+
rescue
|
|
44
|
+
# Parser errors beyond the .failure? check, unsupported AST shapes, or
|
|
45
|
+
# anything else: fall back to empty hashes rather than crashing the whole
|
|
46
|
+
# report.
|
|
69
47
|
nil
|
|
70
|
-
# simplecov:enable line
|
|
71
48
|
end
|
|
72
49
|
|
|
73
|
-
# Summarize a source file's
|
|
74
|
-
# `:eval_generated` filter (
|
|
75
|
-
# SimpleCov.ignore_methods, #1046). Returns a hash:
|
|
50
|
+
# Summarize a source file's real branch and method positions, for the
|
|
51
|
+
# `:eval_generated` filter (#1046). Answers:
|
|
76
52
|
#
|
|
77
53
|
# {
|
|
78
54
|
# branches: Set[start_line, ...], # e.g., [3, 12, 20]
|
|
79
55
|
# methods: Set[[name, start_line], ...] # e.g., [[:foo, 7], [:bar, 13]]
|
|
80
56
|
# }
|
|
81
57
|
#
|
|
82
|
-
# Branch matching is start_line-only rather than by the full tuple.
|
|
83
|
-
#
|
|
84
|
-
#
|
|
85
|
-
#
|
|
86
|
-
#
|
|
87
|
-
# eval-generated one will keep both, which is an acceptable
|
|
88
|
-
# false-negative for an opt-in filter. Method matching uses
|
|
89
|
-
# (name, start_line) since a method name is unique at any line.
|
|
58
|
+
# Branch matching is start_line-only rather than by the full tuple. Static
|
|
59
|
+
# extraction and Coverage can still disagree on a branch's exact column
|
|
60
|
+
# positions, so matching on start_line alone tolerates those differences.
|
|
61
|
+
# Coincidental line-sharing between a real branch and an eval-generated one
|
|
62
|
+
# keeps both, an acceptable false-negative for an opt-in filter.
|
|
90
63
|
#
|
|
91
|
-
# Returns nil when Prism is unavailable or parsing fails, signaling
|
|
92
|
-
#
|
|
64
|
+
# Returns nil when Prism is unavailable or parsing fails, signaling callers
|
|
65
|
+
# to keep every Coverage entry.
|
|
93
66
|
def real_source_positions(source)
|
|
94
67
|
extracted = call(source)
|
|
95
68
|
return nil unless extracted
|
|
96
69
|
|
|
97
70
|
{
|
|
98
|
-
branches: extracted
|
|
99
|
-
methods: extracted
|
|
71
|
+
branches: extracted.fetch("branches").keys.to_set { |tuple| branch_start_line(*tuple) },
|
|
72
|
+
methods: extracted.fetch("methods").keys.to_set { |tuple| method_identity(*tuple) }
|
|
100
73
|
}
|
|
101
74
|
end
|
|
75
|
+
|
|
76
|
+
# Both keys carry their start line third, after the parts that vary between
|
|
77
|
+
# recordings. Read through a parameter list rather than an index, because
|
|
78
|
+
# every spelling of an index answers the same for a tuple of this fixed
|
|
79
|
+
# shape. Binding an argument has only the one spelling.
|
|
80
|
+
def branch_start_line(_type, _id, start_line, *)
|
|
81
|
+
start_line
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def method_identity(_class_name, name, start_line, *)
|
|
85
|
+
[name, start_line]
|
|
86
|
+
end
|
|
102
87
|
end
|
|
103
88
|
end
|
|
104
89
|
|
|
105
90
|
# simplecov:disable branch
|
|
106
|
-
# The `else` arm (Prism missing) is unreachable on engines where the
|
|
107
|
-
#
|
|
108
|
-
# loadable.
|
|
91
|
+
# The `else` arm (Prism missing) is unreachable on engines where the dogfood
|
|
92
|
+
# report runs; the Visitor class only matters when Prism is loadable.
|
|
109
93
|
require_relative "static_coverage_extractor/visitor" if SimpleCov::StaticCoverageExtractor.available?
|
|
110
94
|
# simplecov:enable branch
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SimpleCov
|
|
4
|
+
class TestTracker
|
|
5
|
+
module Accessors
|
|
6
|
+
# Records the block's coverage delta as covered by `test_id`, returning the
|
|
7
|
+
# block's value. This is the seam test frameworks hang per-test tracking on. A
|
|
8
|
+
# no-op, with the block still running, unless `track_tests` is enabled and
|
|
9
|
+
# tracking has started, so callers never guard it.
|
|
10
|
+
def track_test(test_id, &)
|
|
11
|
+
tracker = test_tracker
|
|
12
|
+
return yield unless tracker
|
|
13
|
+
|
|
14
|
+
tracker.track(test_id, &)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def test_tracker
|
|
18
|
+
@test_tracker
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# @api private. Reuses the tracker across repeated `start` calls for the same
|
|
22
|
+
# reason `Coverage` keeps counting across them: restarting must not lose what
|
|
23
|
+
# the process already recorded. The framework hooks are only wired up on the
|
|
24
|
+
# first.
|
|
25
|
+
def start_test_tracking
|
|
26
|
+
validate_test_tracking!
|
|
27
|
+
return unless track_tests? && test_tracker.nil?
|
|
28
|
+
|
|
29
|
+
@test_tracker = TestTracker.new(granularity: track_tests_granularity)
|
|
30
|
+
TestTracker.install_framework_hooks
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SimpleCov
|
|
4
|
+
class TestTracker
|
|
5
|
+
# A one-shot watch that runs a callback the moment a constant of a given name
|
|
6
|
+
# is defined under a host module, via Ruby's `Module#const_added`.
|
|
7
|
+
# `TestTracker` uses it to install the Minitest wrapper in the ordering
|
|
8
|
+
# minitest 6 abandoned plugins for: SimpleCov starts first, as coverage must
|
|
9
|
+
# to see the app load, and minitest is required later, so nothing of
|
|
10
|
+
# minitest's exists yet to hook into.
|
|
11
|
+
#
|
|
12
|
+
# Each watch is its own Module so it can be prepended to the host's singleton
|
|
13
|
+
# class and compose with any `const_added` the host already has. A prepend
|
|
14
|
+
# cannot be removed, so after firing the watch stays in place as a name
|
|
15
|
+
# comparison per constant definition, which is as close to free as Ruby gets.
|
|
16
|
+
class ConstantWatch < Module
|
|
17
|
+
# No `super`: it would forward the implicit block even given explicit
|
|
18
|
+
# arguments, and `Module#initialize` module_evals any block it gets.
|
|
19
|
+
def initialize(name, &on_added)
|
|
20
|
+
@name = name
|
|
21
|
+
@on_added = on_added
|
|
22
|
+
watch = self #: ConstantWatch
|
|
23
|
+
define_method(:const_added) do |added|
|
|
24
|
+
super(added) # steep:ignore UnexpectedPositionalArgument
|
|
25
|
+
watch.notice(added)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# `host.const_added` runs with the host as self, so reaching this watch's
|
|
30
|
+
# state takes the closure. At runtime the `super` inside is the host's next
|
|
31
|
+
# `const_added`; Steep resolves a define_method block's super against the
|
|
32
|
+
# lexically enclosing method instead.
|
|
33
|
+
def attach(host)
|
|
34
|
+
host.singleton_class.prepend(self)
|
|
35
|
+
self
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def notice(added)
|
|
39
|
+
return unless added.equal?(@name)
|
|
40
|
+
|
|
41
|
+
callback = @on_added
|
|
42
|
+
return unless callback
|
|
43
|
+
|
|
44
|
+
@on_added = nil
|
|
45
|
+
callback.call
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SimpleCov
|
|
4
|
+
class TestTracker
|
|
5
|
+
# The peek-diff arithmetic behind the tracker: which in-root lines grew
|
|
6
|
+
# between two coverage snapshots. This is the part a native
|
|
7
|
+
# `Coverage.supported?(:contexts)` engine would replace outright.
|
|
8
|
+
class Delta
|
|
9
|
+
def initialize(root_regex:)
|
|
10
|
+
@root_regex = root_regex
|
|
11
|
+
@in_root = {} #: Hash[String, bool]
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Files outside the root are skipped; a file absent from `before` was loaded
|
|
15
|
+
# by the test itself, so everything it executed is the test's. The root match
|
|
16
|
+
# is memoized, so the regex runs once per file the process ever loads rather
|
|
17
|
+
# than once per file per test.
|
|
18
|
+
def call(before, after)
|
|
19
|
+
changed = {} #: Hash[String, Integer]
|
|
20
|
+
after.each do |path, file_coverage|
|
|
21
|
+
in_root = @in_root.fetch(path) { @in_root[path] = @root_regex.match?(path) }
|
|
22
|
+
next unless in_root
|
|
23
|
+
|
|
24
|
+
bitmap = line_delta(lines_in(before[path]), lines_in(file_coverage))
|
|
25
|
+
changed[path] = bitmap unless bitmap.zero?
|
|
26
|
+
end
|
|
27
|
+
changed
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
# A peek's per-file data is a criteria Hash when Coverage was started with a
|
|
33
|
+
# criteria hash, and a bare Array under the older lines-only form. No lines
|
|
34
|
+
# at all, from a branch-only or method-only start, means nothing to diff.
|
|
35
|
+
def lines_in(file_coverage)
|
|
36
|
+
case file_coverage
|
|
37
|
+
when Hash then file_coverage[:lines]
|
|
38
|
+
when Array then file_coverage
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# The equality check is the overwhelmingly common case, the test never
|
|
43
|
+
# entering this file: an Array compare is fast C, the per-line loop is not.
|
|
44
|
+
#
|
|
45
|
+
# The bitmap is built as a binary string in one pass, so the
|
|
46
|
+
# arbitrary-precision math happens once per file rather than one bignum OR
|
|
47
|
+
# per executed line. The string reads lowest line first and the number wants
|
|
48
|
+
# it the other way round; the leading zero keeps the parse valid for an
|
|
49
|
+
# all-zero delta without changing any value.
|
|
50
|
+
def line_delta(before_lines, after_lines)
|
|
51
|
+
return 0 unless after_lines
|
|
52
|
+
return 0 if before_lines.eql?(after_lines)
|
|
53
|
+
|
|
54
|
+
bits = +""
|
|
55
|
+
after_lines.each_with_index do |count, index|
|
|
56
|
+
bits << (grew?(count, before_lines&.at(index)) ? "1" : "0")
|
|
57
|
+
end
|
|
58
|
+
Integer("0#{bits.reverse}", 2)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# `previous` is nil both for a non-executable line, whose count is nil too,
|
|
62
|
+
# and for a file the test itself loaded, where every executed line is the
|
|
63
|
+
# test's.
|
|
64
|
+
def grew?(count, previous)
|
|
65
|
+
!count.nil? && count > (previous || 0)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SimpleCov
|
|
4
|
+
# The test-framework side of per-test tracking: how the wrapper gets
|
|
5
|
+
# installed into RSpec and Minitest, and how each framework's tests are
|
|
6
|
+
# identified. The recording itself lives in test_tracker.rb.
|
|
7
|
+
class TestTracker
|
|
8
|
+
class << self
|
|
9
|
+
def install_framework_hooks
|
|
10
|
+
install_rspec_hook
|
|
11
|
+
install_minitest_hook_when_loaded
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Wraps every RSpec example in a `track_test` call. Called from
|
|
15
|
+
# `SimpleCov.start_tracking`, where RSpec is already loaded when the suite
|
|
16
|
+
# is an RSpec suite. Installs once per process no matter how often
|
|
17
|
+
# tracking is restarted.
|
|
18
|
+
def install_rspec_hook(rspec = rspec_module)
|
|
19
|
+
return unless rspec
|
|
20
|
+
return if @rspec_hook_installed
|
|
21
|
+
|
|
22
|
+
@rspec_hook_installed = true
|
|
23
|
+
rspec.configure do |config|
|
|
24
|
+
# The block runs instance-eval'd in the example group, so everything it
|
|
25
|
+
# touches must be fully qualified.
|
|
26
|
+
config.around do |example|
|
|
27
|
+
SimpleCov.track_test(TestTracker.rspec_example_id(example)) { example.run }
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# The RSpec this process has, if it has one. A Minitest-only suite has
|
|
33
|
+
# none, and no example can make this process into one: hiding the RSpec
|
|
34
|
+
# constant takes down the very runner asking the question.
|
|
35
|
+
# simplecov:disable branch — the RSpec-less arm is unreachable from an RSpec-driven suite
|
|
36
|
+
# mutant:disable
|
|
37
|
+
def rspec_module
|
|
38
|
+
::RSpec if defined?(::RSpec.configure)
|
|
39
|
+
end
|
|
40
|
+
# simplecov:enable branch
|
|
41
|
+
|
|
42
|
+
# @api private -- test seam, so specs can exercise installation repeatedly
|
|
43
|
+
# without touching the process-wide guard for real.
|
|
44
|
+
def reset_rspec_hook!
|
|
45
|
+
@rspec_hook_installed = false
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Called by the minitest plugin under minitest 5, and by the constant
|
|
49
|
+
# watch below under minitest 6, whose autorun no longer discovers
|
|
50
|
+
# plugins. Prepending a module already in the chain is a no-op, which is
|
|
51
|
+
# the whole of the idempotence.
|
|
52
|
+
def install_minitest_hook(test_case = (Minitest::Test if defined?(Minitest::Test)))
|
|
53
|
+
return if test_case.nil?
|
|
54
|
+
|
|
55
|
+
test_case.prepend(MinitestRun)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Installs the Minitest wrapper now when Minitest is already loaded, and
|
|
59
|
+
# otherwise the moment `Minitest::Test` is defined. The deferred half is
|
|
60
|
+
# what covers minitest 6 under the canonical helper ordering (SimpleCov
|
|
61
|
+
# first, so coverage sees the app load; minitest after), where no plugin
|
|
62
|
+
# ever fires. `root` is Object outside of tests.
|
|
63
|
+
def install_minitest_hook_when_loaded(root = Object)
|
|
64
|
+
minitest = loaded_const(root, :Minitest)
|
|
65
|
+
test_case = minitest && loaded_const(minitest, :Test)
|
|
66
|
+
return install_minitest_hook(test_case) if test_case
|
|
67
|
+
return watch_for_minitest_test(minitest) if minitest
|
|
68
|
+
|
|
69
|
+
ConstantWatch.new(:Minitest) do
|
|
70
|
+
# `loaded_const` rather than the block capturing a module: the module
|
|
71
|
+
# does not exist yet when the watch is armed.
|
|
72
|
+
watch_for_minitest_test(loaded_const(root, :Minitest))
|
|
73
|
+
end.attach(root)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Relative the way RSpec prints it (`./spec/a_spec.rb:12` becomes
|
|
77
|
+
# `spec/a_spec.rb:12`).
|
|
78
|
+
def rspec_example_id(example)
|
|
79
|
+
example.metadata.fetch(:location).delete_prefix("./")
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# An autoload declaration fires `const_added` without the module existing
|
|
83
|
+
# yet; that arrangement is left to the plugin, so nil means nothing to
|
|
84
|
+
# watch.
|
|
85
|
+
def watch_for_minitest_test(minitest)
|
|
86
|
+
return unless minitest
|
|
87
|
+
|
|
88
|
+
ConstantWatch.new(:Test) { install_minitest_hook(loaded_const(minitest, :Test)) }.attach(minitest)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# The constant when it is genuinely loaded, nil when absent or merely
|
|
92
|
+
# declared for autoload: const_get would force such a load, and installing
|
|
93
|
+
# a coverage hook must never be what requires a test framework.
|
|
94
|
+
def loaded_const(mod, name)
|
|
95
|
+
return nil unless mod.const_defined?(name, false)
|
|
96
|
+
return nil if mod.autoload?(name, false)
|
|
97
|
+
|
|
98
|
+
mod.const_get(name)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# A Minitest test's identity: the `path:line` where the test method is
|
|
102
|
+
# defined, relative to `SimpleCov.root`. Falls back to
|
|
103
|
+
# `ClassName#method_name` when the method has no source location.
|
|
104
|
+
def minitest_test_id(test)
|
|
105
|
+
path, line = definition_site(test)
|
|
106
|
+
return "#{test.class}##{test.name}" unless path
|
|
107
|
+
|
|
108
|
+
"#{path.delete_prefix(File.join(SimpleCov.root, ""))}:#{line}"
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# A method defined in C or through an eval without a filename has no site,
|
|
112
|
+
# and a runner exotic enough to name a method it never defined deserves
|
|
113
|
+
# an id over an exception out of the middle of its run.
|
|
114
|
+
def definition_site(test)
|
|
115
|
+
test.method(test.name).source_location
|
|
116
|
+
rescue NameError
|
|
117
|
+
nil
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Prepended to `Minitest::Test` by `install_minitest_hook`, wrapping each
|
|
122
|
+
# test method run, setup and teardown included: a line only a setup block
|
|
123
|
+
# reaches is still this test's line.
|
|
124
|
+
module MinitestRun
|
|
125
|
+
def run
|
|
126
|
+
SimpleCov.track_test(TestTracker.minitest_test_id(self)) { super }
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|