simplecov 0.22.0 → 1.1.1
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 +71 -916
- data/exe/simplecov +11 -0
- data/lib/minitest/simplecov_plugin.rb +13 -5
- data/lib/simplecov/atomic_file.rb +70 -0
- data/lib/simplecov/autostart.rb +11 -0
- data/lib/simplecov/cli/clean.rb +87 -0
- data/lib/simplecov/cli/command_helpers.rb +55 -0
- data/lib/simplecov/cli/coverage.rb +86 -0
- data/lib/simplecov/cli/coverage_file.rb +65 -0
- data/lib/simplecov/cli/diff.rb +147 -0
- data/lib/simplecov/cli/dotfile.rb +111 -0
- data/lib/simplecov/cli/merge.rb +120 -0
- data/lib/simplecov/cli/open.rb +48 -0
- data/lib/simplecov/cli/report.rb +97 -0
- data/lib/simplecov/cli/run.rb +36 -0
- data/lib/simplecov/cli/serve/report_preparer.rb +29 -0
- data/lib/simplecov/cli/serve/static_file_handler.rb +122 -0
- data/lib/simplecov/cli/serve.rb +85 -0
- data/lib/simplecov/cli/uncovered.rb +103 -0
- data/lib/simplecov/cli.rb +163 -0
- data/lib/simplecov/color.rb +83 -0
- data/lib/simplecov/combine/branches_combiner.rb +54 -7
- data/lib/simplecov/combine/coverage_accumulator.rb +268 -0
- data/lib/simplecov/combine/identity_interner.rb +30 -0
- data/lib/simplecov/combine/interned_counts.rb +30 -0
- data/lib/simplecov/combine/lines_combiner.rb +53 -23
- data/lib/simplecov/combine/methods_combiner.rb +74 -0
- data/lib/simplecov/combine/results_combiner.rb +13 -36
- data/lib/simplecov/combine.rb +5 -23
- data/lib/simplecov/command_guesser.rb +104 -33
- data/lib/simplecov/configuration/coverage.rb +172 -0
- data/lib/simplecov/configuration/coverage_criteria.rb +154 -0
- data/lib/simplecov/configuration/eval_coverage.rb +41 -0
- data/lib/simplecov/configuration/filters.rb +164 -0
- data/lib/simplecov/configuration/formatting.rb +138 -0
- data/lib/simplecov/configuration/groups.rb +42 -0
- data/lib/simplecov/configuration/ignored_entries.rb +63 -0
- data/lib/simplecov/configuration/merging.rb +165 -0
- data/lib/simplecov/configuration/thresholds.rb +179 -0
- data/lib/simplecov/configuration.rb +83 -420
- data/lib/simplecov/coverage_json.rb +24 -0
- data/lib/simplecov/coverage_statistics.rb +16 -10
- data/lib/simplecov/coverage_violations.rb +159 -0
- data/lib/simplecov/defaults.rb +34 -23
- data/lib/simplecov/deprecation.rb +45 -0
- data/lib/simplecov/directive.rb +167 -0
- data/lib/simplecov/exit_codes/check.rb +33 -0
- data/lib/simplecov/exit_codes/exit_code_handling.rb +8 -2
- data/lib/simplecov/exit_codes/maximum_coverage_drop_check.rb +18 -65
- data/lib/simplecov/exit_codes/maximum_overall_coverage_check.rb +32 -0
- data/lib/simplecov/exit_codes/minimum_coverage_by_file_check.rb +17 -36
- data/lib/simplecov/exit_codes/minimum_coverage_by_group_check.rb +30 -0
- data/lib/simplecov/exit_codes/minimum_overall_coverage_check.rb +37 -33
- data/lib/simplecov/exit_codes.rb +20 -0
- data/lib/simplecov/exit_handling.rb +128 -0
- data/lib/simplecov/file_list.rb +70 -24
- data/lib/simplecov/filter.rb +105 -26
- data/lib/simplecov/formatter/base.rb +118 -0
- data/lib/simplecov/formatter/coverage_json_writer.rb +97 -0
- data/lib/simplecov/formatter/html_formatter/public/index.html +87 -0
- data/lib/simplecov/formatter/html_formatter/viewer_data_validator.rb +96 -0
- data/lib/simplecov/formatter/html_formatter.rb +105 -0
- data/lib/simplecov/formatter/json_formatter/errors_formatter.rb +88 -0
- data/lib/simplecov/formatter/json_formatter/result_hash_formatter.rb +118 -0
- data/lib/simplecov/formatter/json_formatter/source_file_formatter.rb +93 -0
- data/lib/simplecov/formatter/json_formatter.rb +46 -0
- data/lib/simplecov/formatter/multi_formatter.rb +12 -8
- data/lib/simplecov/formatter/simple_formatter.rb +14 -11
- data/lib/simplecov/formatter.rb +16 -0
- data/lib/simplecov/group_names.rb +32 -0
- data/lib/simplecov/last_run.rb +18 -4
- data/lib/simplecov/lines_classifier.rb +48 -15
- data/lib/simplecov/load_global_config.rb +12 -4
- data/lib/simplecov/parallel_adapters/base.rb +78 -0
- data/lib/simplecov/parallel_adapters/generic.rb +45 -0
- data/lib/simplecov/parallel_adapters/parallel_tests.rb +93 -0
- data/lib/simplecov/parallel_adapters.rb +85 -0
- data/lib/simplecov/parallel_coordination.rb +134 -0
- data/lib/simplecov/parallel_result_merger.rb +230 -0
- data/lib/simplecov/process.rb +52 -12
- data/lib/simplecov/profiles/bundler_filter.rb +1 -1
- data/lib/simplecov/profiles/hidden_filter.rb +1 -1
- data/lib/simplecov/profiles/rails.rb +24 -10
- data/lib/simplecov/profiles/root_filter.rb +6 -5
- data/lib/simplecov/profiles/strict.rb +32 -0
- data/lib/simplecov/profiles/test_frameworks.rb +1 -4
- data/lib/simplecov/profiles.rb +32 -3
- data/lib/simplecov/report_deferral.rb +49 -0
- data/lib/simplecov/report_stamp.rb +28 -0
- data/lib/simplecov/result/missing_source_files_reporter.rb +49 -0
- data/lib/simplecov/result/source_file_builder.rb +51 -0
- data/lib/simplecov/result.rb +145 -26
- data/lib/simplecov/result_adapter.rb +139 -8
- data/lib/simplecov/result_merger/legacy_format_adapter.rb +28 -0
- data/lib/simplecov/result_merger/resultset_file.rb +74 -0
- data/lib/simplecov/result_merger/resultset_run_identity.rb +67 -0
- data/lib/simplecov/result_merger/resultset_store.rb +53 -0
- data/lib/simplecov/result_merger/unloaded_files.rb +103 -0
- data/lib/simplecov/result_merger.rb +109 -114
- data/lib/simplecov/result_processing.rb +218 -0
- data/lib/simplecov/run_identity.rb +77 -0
- data/lib/simplecov/simulate_coverage.rb +83 -12
- data/lib/simplecov/source_file/branch.rb +1 -3
- data/lib/simplecov/source_file/branch_builder.rb +114 -0
- data/lib/simplecov/source_file/builder_context.rb +28 -0
- data/lib/simplecov/source_file/line.rb +9 -4
- data/lib/simplecov/source_file/line_builder.rb +43 -0
- data/lib/simplecov/source_file/method.rb +58 -0
- data/lib/simplecov/source_file/method_builder.rb +60 -0
- data/lib/simplecov/source_file/ruby_data_parser.rb +110 -0
- data/lib/simplecov/source_file/skip_chunks.rb +72 -0
- data/lib/simplecov/source_file/source_loader.rb +79 -0
- data/lib/simplecov/source_file/statistics.rb +65 -0
- data/lib/simplecov/source_file.rb +66 -232
- data/lib/simplecov/static_coverage_extractor/condition_folding.rb +239 -0
- data/lib/simplecov/static_coverage_extractor/location_conventions.rb +236 -0
- data/lib/simplecov/static_coverage_extractor/method_collector.rb +62 -0
- data/lib/simplecov/static_coverage_extractor/prism_compat.rb +55 -0
- data/lib/simplecov/static_coverage_extractor/value_position.rb +74 -0
- data/lib/simplecov/static_coverage_extractor/visitor.rb +183 -0
- data/lib/simplecov/static_coverage_extractor.rb +110 -0
- data/lib/simplecov/unloaded_file_injector.rb +75 -0
- data/lib/simplecov/useless_results_remover.rb +16 -7
- data/lib/simplecov/version.rb +1 -1
- data/lib/simplecov-html.rb +4 -0
- data/lib/simplecov.rb +185 -380
- data/lib/simplecov_json_formatter.rb +4 -0
- data/sig/simplecov.rbs +1777 -0
- metadata +111 -62
- 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
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "prism_compat"
|
|
4
|
+
require_relative "condition_folding"
|
|
5
|
+
require_relative "location_conventions"
|
|
6
|
+
require_relative "method_collector"
|
|
7
|
+
require_relative "value_position"
|
|
8
|
+
|
|
9
|
+
module SimpleCov
|
|
10
|
+
module StaticCoverageExtractor
|
|
11
|
+
# Prism visitor that accumulates branch and method tuples in the
|
|
12
|
+
# shape Ruby's `Coverage` reports. Tuple ids are sequential across
|
|
13
|
+
# the file like `Coverage`'s, but the numbering order can differ
|
|
14
|
+
# (e.g. `case`/`when` and chained `&.` are visited in a different
|
|
15
|
+
# order than Coverage numbers them). That's fine: the combiners
|
|
16
|
+
# intern on source span and the report output drops ids, so nothing
|
|
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.
|
|
22
|
+
include MethodCollector
|
|
23
|
+
# Source-range resolution, including the per-Ruby-version Coverage
|
|
24
|
+
# conventions. See issue #1226.
|
|
25
|
+
include LocationConventions
|
|
26
|
+
# Which literal `if`/`unless`/ternary conditions the compiler folds
|
|
27
|
+
# away (so we emit no branch for them).
|
|
28
|
+
include ConditionFolding
|
|
29
|
+
|
|
30
|
+
attr_reader :branches, :methods
|
|
31
|
+
|
|
32
|
+
def initialize
|
|
33
|
+
super
|
|
34
|
+
@branches = {}
|
|
35
|
+
@methods = {}
|
|
36
|
+
@next_id = 0
|
|
37
|
+
@class_stack = []
|
|
38
|
+
@value_positions = nil
|
|
39
|
+
@suppress_methods = false
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Entry point for a parsed file. On legacy Rubies the location of an
|
|
43
|
+
# empty branch arm depends on whether its construct is in value
|
|
44
|
+
# (tail) position, so precompute that once for the whole tree before
|
|
45
|
+
# emitting anything. Modern Rubies don't need it (see
|
|
46
|
+
# LocationConventions), so the pass is skipped there.
|
|
47
|
+
def visit_program_node(node)
|
|
48
|
+
# simplecov:disable branch — legacy-only arm; unreachable on the modern dogfood Ruby
|
|
49
|
+
@value_positions = ValuePositions.call(node) if LEGACY_COVERAGE_LOCATIONS
|
|
50
|
+
# simplecov:enable branch
|
|
51
|
+
super
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# `if` / `unless` / postfix-if / postfix-unless / ternary all parse
|
|
55
|
+
# as IfNode (or UnlessNode). Both carry a `then` arm (the
|
|
56
|
+
# statements body) and an optional `subsequent` (an ElseNode for
|
|
57
|
+
# `else`, another IfNode for `elsif`). When the subsequent is
|
|
58
|
+
# missing, Coverage synthesizes a `:else` arm attributed to the
|
|
59
|
+
# whole condition's range — we do the same.
|
|
60
|
+
#
|
|
61
|
+
# A folded condition emits no tuple, and on modern Rubies only its
|
|
62
|
+
# live arm is descended into: the compiler eliminates the dead
|
|
63
|
+
# arm's entire subtree, so a branch or method nested there would be
|
|
64
|
+
# a phantom no loaded run can produce. A falsy `if`'s elsif chain
|
|
65
|
+
# survives as a plain `if`, which is what visiting the subsequent
|
|
66
|
+
# IfNode emits. On 3.2 the dead arm is visited too, branches only
|
|
67
|
+
# (see visit_folded_arms).
|
|
68
|
+
def visit_if_node(node)
|
|
69
|
+
verdict = folded_condition(node.predicate)
|
|
70
|
+
return visit_folded_arms(verdict, node.statements, PrismCompat.subsequent(node)) if verdict
|
|
71
|
+
|
|
72
|
+
emit_if_like(node, :if)
|
|
73
|
+
super
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def visit_unless_node(node)
|
|
77
|
+
verdict = folded_condition(node.predicate)
|
|
78
|
+
return visit_folded_arms(verdict, PrismCompat.else_clause(node), node.statements) if verdict
|
|
79
|
+
|
|
80
|
+
emit_if_like(node, :unless)
|
|
81
|
+
super
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def visit_call_node(node)
|
|
85
|
+
emit_safe_navigation(node) if node.respond_to?(:safe_navigation?) && node.safe_navigation?
|
|
86
|
+
super
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# `case`/`when` and `case`/`in` (pattern matching) parse as CaseNode
|
|
90
|
+
# and CaseMatchNode respectively. When there's no explicit `else`,
|
|
91
|
+
# Coverage synthesizes one at the case's range.
|
|
92
|
+
def visit_case_node(node)
|
|
93
|
+
emit_case_like(node, :when)
|
|
94
|
+
super
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def visit_case_match_node(node)
|
|
98
|
+
emit_case_like(node, :in)
|
|
99
|
+
super
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# One-line pattern matching: `x => pattern` (MatchRequiredNode) and
|
|
103
|
+
# `x in pattern` (MatchPredicateNode). Ruby 3.3's Coverage reports
|
|
104
|
+
# these as a `:case` with an `:in` and an `:else` arm; 3.4 dropped
|
|
105
|
+
# them entirely (no branch), so this is legacy-only. The two forms
|
|
106
|
+
# differ only in where Coverage anchors the synthesized `:else`:
|
|
107
|
+
# `=>` uses the whole expression, `in` uses just the pattern.
|
|
108
|
+
# simplecov:disable branch — legacy-only arms; unreachable on the modern dogfood Ruby
|
|
109
|
+
def visit_match_required_node(node)
|
|
110
|
+
emit_oneline_pattern(node, node.location) if LEGACY_COVERAGE_LOCATIONS
|
|
111
|
+
super
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def visit_match_predicate_node(node)
|
|
115
|
+
emit_oneline_pattern(node, node.pattern.location) if LEGACY_COVERAGE_LOCATIONS
|
|
116
|
+
super
|
|
117
|
+
end
|
|
118
|
+
# simplecov:enable branch
|
|
119
|
+
|
|
120
|
+
# `while` / `until` loops get a single `:body` arm. No synthetic
|
|
121
|
+
# else (the loop either runs the body or doesn't).
|
|
122
|
+
def visit_while_node(node)
|
|
123
|
+
emit_loop(node, :while)
|
|
124
|
+
super
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def visit_until_node(node)
|
|
128
|
+
emit_loop(node, :until)
|
|
129
|
+
super
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
private
|
|
133
|
+
|
|
134
|
+
# IfNode and UnlessNode share a shape (predicate + then body +
|
|
135
|
+
# optional else/elsif) but expose the trailing arm under different
|
|
136
|
+
# accessors. `if_like_else_location` hides that split.
|
|
137
|
+
def emit_if_like(node, type)
|
|
138
|
+
then_loc = if_like_then_location(node, type)
|
|
139
|
+
else_loc = if_like_else_location(node, type)
|
|
140
|
+
@branches[build_tuple(type, if_like_location(node, type))] = {
|
|
141
|
+
build_tuple(:then, then_loc) => 0,
|
|
142
|
+
build_tuple(:else, else_loc) => 0
|
|
143
|
+
}
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def emit_safe_navigation(node)
|
|
147
|
+
loc = safe_navigation_location(node)
|
|
148
|
+
@branches[build_tuple(:"&.", loc)] = {
|
|
149
|
+
build_tuple(:then, loc) => 0,
|
|
150
|
+
build_tuple(:else, loc) => 0
|
|
151
|
+
}
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# simplecov:disable — legacy-only (3.4 emits no branch for one-line patterns)
|
|
155
|
+
def emit_oneline_pattern(node, else_location)
|
|
156
|
+
@branches[build_tuple(:case, node.location)] = {
|
|
157
|
+
build_tuple(:in, node.pattern.location) => 0,
|
|
158
|
+
build_tuple(:else, else_location) => 0
|
|
159
|
+
}
|
|
160
|
+
end
|
|
161
|
+
# simplecov:enable
|
|
162
|
+
|
|
163
|
+
def emit_case_like(node, when_type)
|
|
164
|
+
arms = node.conditions.to_h do |when_node|
|
|
165
|
+
[build_tuple(when_type, case_arm_location(node, when_node, when_type)), 0]
|
|
166
|
+
end
|
|
167
|
+
arms[build_tuple(:else, else_arm_location(node))] = 0
|
|
168
|
+
@branches[build_tuple(:case, node.location)] = arms
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def emit_loop(node, type)
|
|
172
|
+
cond_tuple = build_tuple(type, node.location)
|
|
173
|
+
@branches[cond_tuple] = {build_tuple(:body, loop_body_location(node)) => 0}
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def build_tuple(type, location)
|
|
177
|
+
id = @next_id
|
|
178
|
+
@next_id += 1
|
|
179
|
+
[type, id, location.start_line, location.start_column, location.end_line, location.end_column]
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
end
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
begin
|
|
4
|
+
require "prism"
|
|
5
|
+
rescue LoadError
|
|
6
|
+
# Prism isn't available on this Ruby (older than 3.3 without the gem).
|
|
7
|
+
# `StaticCoverageExtractor.available?` will return false and callers
|
|
8
|
+
# fall back to the previous "empty hashes" behavior.
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
module SimpleCov
|
|
12
|
+
# Static enumeration of the branches and methods Ruby's `Coverage` library
|
|
13
|
+
# WOULD have reported if a file had been loaded with `branches: true` /
|
|
14
|
+
# `methods: true`. Used by `SimulateCoverage` to backfill data for files
|
|
15
|
+
# added via `cover` / `track_files` that were never `require`'d during the
|
|
16
|
+
# run — so unloaded files contribute to the branch/method denominators
|
|
17
|
+
# symmetrically with their line coverage, instead of vanishing from the
|
|
18
|
+
# totals (see #1059).
|
|
19
|
+
#
|
|
20
|
+
# Implementation uses Prism (stdlib in Ruby 3.3+, gem on older Rubies).
|
|
21
|
+
# When Prism isn't available, `available?` returns false and SimulateCoverage
|
|
22
|
+
# falls back to the previous behavior — older Rubies keep working, just
|
|
23
|
+
# without the synthesized data.
|
|
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.
|
|
33
|
+
module StaticCoverageExtractor
|
|
34
|
+
module_function
|
|
35
|
+
|
|
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
|
+
def available?
|
|
42
|
+
defined?(::Prism) ? true : false
|
|
43
|
+
end
|
|
44
|
+
# simplecov:enable branch
|
|
45
|
+
|
|
46
|
+
# Parse `source` (a string of Ruby) and return a hash of the form
|
|
47
|
+
# `{"branches" => {...}, "methods" => {...}}` matching the shape that
|
|
48
|
+
# `Coverage.result[path]` produces. Returns nil on parse failure or
|
|
49
|
+
# when Prism isn't available; callers should treat that as "couldn't
|
|
50
|
+
# extract — fall back to empty hashes."
|
|
51
|
+
def call(source)
|
|
52
|
+
# simplecov:disable branch — `then` arm unreachable when Prism IS loadable
|
|
53
|
+
return nil unless available?
|
|
54
|
+
|
|
55
|
+
# simplecov:enable branch
|
|
56
|
+
|
|
57
|
+
result = ::Prism.parse(source)
|
|
58
|
+
return nil if result.failure?
|
|
59
|
+
|
|
60
|
+
visitor = Visitor.new
|
|
61
|
+
visitor.visit(result.value)
|
|
62
|
+
{"branches" => visitor.branches, "methods" => visitor.methods}
|
|
63
|
+
rescue StandardError
|
|
64
|
+
# simplecov:disable line
|
|
65
|
+
# Parser errors beyond the .failure? check, unsupported AST shapes,
|
|
66
|
+
# or anything else: fall back to empty hashes rather than crashing
|
|
67
|
+
# the whole report. Defensive; hard to trigger from a real source
|
|
68
|
+
# input that Prism accepts at parse time.
|
|
69
|
+
nil
|
|
70
|
+
# simplecov:enable line
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Summarize a source file's REAL branch and method positions, for the
|
|
74
|
+
# `:eval_generated` filter (SimpleCov.ignore_branches /
|
|
75
|
+
# SimpleCov.ignore_methods, #1046). Returns a hash:
|
|
76
|
+
#
|
|
77
|
+
# {
|
|
78
|
+
# branches: Set[start_line, ...], # e.g., [3, 12, 20]
|
|
79
|
+
# methods: Set[[name, start_line], ...] # e.g., [[:foo, 7], [:bar, 13]]
|
|
80
|
+
# }
|
|
81
|
+
#
|
|
82
|
+
# Branch matching is start_line-only rather than by the full tuple.
|
|
83
|
+
# Static extraction and Coverage can still disagree on a branch's exact
|
|
84
|
+
# column positions (and, for some constructs, its type), so matching on
|
|
85
|
+
# start_line alone is the conservative choice that tolerates those
|
|
86
|
+
# differences. Coincidental line-sharing between a real branch and an
|
|
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.
|
|
90
|
+
#
|
|
91
|
+
# Returns nil when Prism is unavailable or parsing fails, signaling
|
|
92
|
+
# callers to keep every Coverage entry (no false drops).
|
|
93
|
+
def real_source_positions(source)
|
|
94
|
+
extracted = call(source)
|
|
95
|
+
return nil unless extracted
|
|
96
|
+
|
|
97
|
+
{
|
|
98
|
+
branches: extracted["branches"].keys.to_set { |tuple| tuple[2] },
|
|
99
|
+
methods: extracted["methods"].keys.to_set { |tuple| [tuple[1], tuple[2]] }
|
|
100
|
+
}
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# simplecov:disable branch
|
|
106
|
+
# The `else` arm (Prism missing) is unreachable on engines where the
|
|
107
|
+
# dogfood report runs; the Visitor class only matters when Prism is
|
|
108
|
+
# loadable.
|
|
109
|
+
require_relative "static_coverage_extractor/visitor" if SimpleCov::StaticCoverageExtractor.available?
|
|
110
|
+
# simplecov:enable branch
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "simulate_coverage"
|
|
4
|
+
|
|
5
|
+
module SimpleCov
|
|
6
|
+
# Fills in coverage for files that were tracked (via `cover` / `track_files`)
|
|
7
|
+
# but never loaded, so they count toward the denominators instead of being
|
|
8
|
+
# absent from the report entirely.
|
|
9
|
+
#
|
|
10
|
+
# This is the mechanism; the merge-time policy of *when* to inject and
|
|
11
|
+
# which criteria the simulated files carry lives in
|
|
12
|
+
# `ResultMerger::UnloadedFiles` — easy names to confuse.
|
|
13
|
+
#
|
|
14
|
+
# Everything arrives as arguments rather than being read from the SimpleCov
|
|
15
|
+
# singleton. The merge step performs this injection on behalf of the processes
|
|
16
|
+
# that contributed to it, and it does not necessarily share their
|
|
17
|
+
# configuration: a standalone `collate` never ran `SimpleCov.start`, so it has
|
|
18
|
+
# no `cover` glob of its own and takes the tracked paths from the resultsets
|
|
19
|
+
# instead. See #1250.
|
|
20
|
+
module UnloadedFileInjector
|
|
21
|
+
module_function
|
|
22
|
+
|
|
23
|
+
# Expand `globs` into absolute paths, relative to `root` rather than
|
|
24
|
+
# `Dir.pwd` — test runners that chdir (or CI scripts that invoke the suite
|
|
25
|
+
# from a subdirectory) would otherwise silently miss files and produce a
|
|
26
|
+
# different set per environment. See #1106.
|
|
27
|
+
# `reject:` are the producing process's path-decidable filters. Paths its
|
|
28
|
+
# own report would exclude must not be recorded as tracked, or a merge that
|
|
29
|
+
# does not share the configuration would simulate them back in. Before
|
|
30
|
+
# injection moved to the merge the producer filtered them out of its own
|
|
31
|
+
# result and they never reached a resultset. A block filter is handed the
|
|
32
|
+
# source file and may consult coverage that does not exist yet, so those
|
|
33
|
+
# still fall to the merging process's filter chain. See #1250.
|
|
34
|
+
def discover(globs, root:, reject: [])
|
|
35
|
+
paths = globs.compact
|
|
36
|
+
.flat_map { |glob| Dir.glob(glob, base: root) }
|
|
37
|
+
.uniq
|
|
38
|
+
.map { |path| File.expand_path(path, root) }
|
|
39
|
+
return paths if reject.empty?
|
|
40
|
+
|
|
41
|
+
paths.reject { |path| rejected?(path, reject) }
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# `SourceFile` reads source and builds lines lazily, so a path-only filter
|
|
45
|
+
# answers without anything here touching disk.
|
|
46
|
+
def rejected?(path, filters)
|
|
47
|
+
candidate = SourceFile.new(path, NO_COVERAGE_YET, loaded: false)
|
|
48
|
+
filters.any? { |filter| filter.matches?(candidate) }
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
no_coverage_yet = {"lines" => []} #: Hash[String, Array[untyped]]
|
|
52
|
+
NO_COVERAGE_YET = no_coverage_yet.freeze
|
|
53
|
+
private_constant :NO_COVERAGE_YET
|
|
54
|
+
|
|
55
|
+
# Add simulated coverage for every path `coverage` does not already carry.
|
|
56
|
+
# Paths that are present are left alone, whoever put them there, which is
|
|
57
|
+
# what lets this run over resultsets a previous SimpleCov already injected
|
|
58
|
+
# into without double-counting or overwriting real data.
|
|
59
|
+
#
|
|
60
|
+
# Returns the augmented coverage and the set of paths that were added.
|
|
61
|
+
def call(coverage, paths, synthesize:, lines:)
|
|
62
|
+
injected = Set.new
|
|
63
|
+
augmented = coverage.dup
|
|
64
|
+
|
|
65
|
+
paths.each do |path|
|
|
66
|
+
next if augmented.key?(path)
|
|
67
|
+
|
|
68
|
+
augmented[path] = SimulateCoverage.call(path, synthesize: synthesize, lines: lines)
|
|
69
|
+
injected << path
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
[augmented, injected]
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -1,18 +1,27 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module SimpleCov
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
4
|
+
# Drop coverage entries whose paths live outside `SimpleCov.root` so the
|
|
5
|
+
# report only reflects the project's own source. Vendored gems, stdlib
|
|
6
|
+
# files, and anything else that happens to have been touched during the
|
|
7
|
+
# run never make it into the formatted result.
|
|
7
8
|
module UselessResultsRemover
|
|
8
9
|
def self.call(coverage_result)
|
|
9
|
-
coverage_result.select
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
coverage_result.select { |path, _coverage| path.match?(root_regex) }
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# The `/i` flag covers case-insensitive matches on Windows / macOS-HFS+
|
|
14
|
+
# where the on-disk path's case can differ from `SimpleCov.root`'s.
|
|
15
|
+
def self.root_regex
|
|
16
|
+
root = SimpleCov.root
|
|
17
|
+
return @root_regex if root == @root_regex_root
|
|
18
|
+
|
|
19
|
+
@root_regex_root = root
|
|
20
|
+
@root_regex = /\A#{Regexp.escape(root.chomp(File::SEPARATOR) + File::SEPARATOR)}/i
|
|
12
21
|
end
|
|
13
22
|
|
|
14
23
|
def self.root_regx
|
|
15
|
-
|
|
24
|
+
root_regex
|
|
16
25
|
end
|
|
17
26
|
end
|
|
18
27
|
end
|
data/lib/simplecov/version.rb
CHANGED