simplecov 1.0.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/README.md +56 -1451
- data/lib/simplecov/atomic_file.rb +70 -0
- data/lib/simplecov/cli/clean.rb +43 -3
- data/lib/simplecov/cli/command_helpers.rb +55 -0
- data/lib/simplecov/cli/coverage.rb +23 -28
- data/lib/simplecov/cli/coverage_file.rb +65 -0
- data/lib/simplecov/cli/diff.rb +43 -47
- data/lib/simplecov/cli/dotfile.rb +21 -10
- data/lib/simplecov/cli/merge.rb +17 -13
- data/lib/simplecov/cli/open.rb +3 -5
- data/lib/simplecov/cli/report.rb +38 -25
- data/lib/simplecov/cli/run.rb +1 -1
- 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 +45 -99
- data/lib/simplecov/cli/uncovered.rb +20 -24
- data/lib/simplecov/cli.rb +15 -2
- data/lib/simplecov/color.rb +10 -1
- data/lib/simplecov/combine/branches_combiner.rb +43 -18
- 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 +48 -20
- data/lib/simplecov/combine/methods_combiner.rb +53 -5
- data/lib/simplecov/combine/results_combiner.rb +12 -36
- data/lib/simplecov/combine.rb +5 -23
- data/lib/simplecov/command_guesser.rb +66 -9
- data/lib/simplecov/configuration/coverage.rb +17 -16
- data/lib/simplecov/configuration/coverage_criteria.rb +37 -39
- data/lib/simplecov/configuration/eval_coverage.rb +41 -0
- data/lib/simplecov/configuration/filters.rb +11 -44
- data/lib/simplecov/configuration/formatting.rb +33 -14
- data/lib/simplecov/configuration/groups.rb +42 -0
- data/lib/simplecov/configuration/merging.rb +8 -7
- data/lib/simplecov/configuration/thresholds.rb +27 -22
- data/lib/simplecov/configuration.rb +21 -42
- data/lib/simplecov/coverage_json.rb +24 -0
- data/lib/simplecov/coverage_statistics.rb +5 -2
- data/lib/simplecov/coverage_violations.rb +17 -6
- data/lib/simplecov/defaults.rb +10 -6
- data/lib/simplecov/directive.rb +9 -4
- data/lib/simplecov/exit_codes/check.rb +33 -0
- data/lib/simplecov/exit_codes/maximum_coverage_drop_check.rb +11 -20
- data/lib/simplecov/exit_codes/maximum_overall_coverage_check.rb +4 -17
- data/lib/simplecov/exit_codes/minimum_coverage_by_file_check.rb +14 -23
- data/lib/simplecov/exit_codes/minimum_coverage_by_group_check.rb +14 -25
- data/lib/simplecov/exit_codes/minimum_overall_coverage_check.rb +7 -20
- data/lib/simplecov/exit_codes.rb +17 -0
- data/lib/simplecov/exit_handling.rb +13 -43
- data/lib/simplecov/file_list.rb +14 -12
- data/lib/simplecov/filter.rb +43 -9
- data/lib/simplecov/formatter/base.rb +18 -1
- data/lib/simplecov/formatter/coverage_json_writer.rb +97 -0
- data/lib/simplecov/formatter/html_formatter/public/index.html +36 -5
- data/lib/simplecov/formatter/html_formatter/viewer_data_validator.rb +96 -0
- data/lib/simplecov/formatter/html_formatter.rb +71 -45
- data/lib/simplecov/formatter/json_formatter/errors_formatter.rb +54 -50
- data/lib/simplecov/formatter/json_formatter/result_hash_formatter.rb +78 -88
- data/lib/simplecov/formatter/json_formatter/source_file_formatter.rb +70 -76
- data/lib/simplecov/formatter/json_formatter.rb +5 -46
- data/lib/simplecov/formatter/multi_formatter.rb +8 -3
- data/lib/simplecov/formatter/simple_formatter.rb +8 -3
- data/lib/simplecov/formatter.rb +12 -0
- data/lib/simplecov/group_names.rb +32 -0
- data/lib/simplecov/last_run.rb +16 -9
- data/lib/simplecov/lines_classifier.rb +29 -8
- data/lib/simplecov/load_global_config.rb +5 -2
- data/lib/simplecov/parallel_adapters/base.rb +17 -0
- data/lib/simplecov/parallel_adapters/generic.rb +9 -8
- data/lib/simplecov/parallel_adapters/parallel_tests.rb +2 -2
- data/lib/simplecov/parallel_adapters.rb +4 -2
- data/lib/simplecov/parallel_coordination.rb +6 -1
- data/lib/simplecov/parallel_result_merger.rb +230 -0
- data/lib/simplecov/process.rb +22 -0
- data/lib/simplecov/report_deferral.rb +49 -0
- data/lib/simplecov/report_stamp.rb +28 -0
- data/lib/simplecov/result.rb +40 -10
- data/lib/simplecov/result_adapter.rb +79 -10
- data/lib/simplecov/result_merger/resultset_file.rb +43 -7
- data/lib/simplecov/result_merger/resultset_run_identity.rb +67 -0
- data/lib/simplecov/result_merger/resultset_store.rb +15 -12
- data/lib/simplecov/result_merger/unloaded_files.rb +103 -0
- data/lib/simplecov/result_merger.rb +69 -42
- data/lib/simplecov/result_processing.rb +85 -46
- data/lib/simplecov/run_identity.rb +77 -0
- data/lib/simplecov/simulate_coverage.rb +37 -12
- data/lib/simplecov/source_file/line.rb +2 -2
- data/lib/simplecov/source_file/method.rb +8 -2
- data/lib/simplecov/source_file/method_builder.rb +3 -1
- data/lib/simplecov/source_file/ruby_data_parser.rb +25 -3
- data/lib/simplecov/source_file/skip_chunks.rb +7 -10
- data/lib/simplecov/source_file/source_loader.rb +25 -9
- data/lib/simplecov/source_file/statistics.rb +24 -16
- data/lib/simplecov/source_file.rb +2 -2
- 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 +7 -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 +74 -62
- data/lib/simplecov/unloaded_file_injector.rb +75 -0
- data/lib/simplecov/version.rb +1 -1
- data/lib/simplecov.rb +37 -17
- data/sig/simplecov.rbs +1777 -0
- metadata +32 -16
- data/doc/alternate-formatters.md +0 -66
- data/doc/commercial-services.md +0 -25
- data/doc/editor-integration.md +0 -18
- data/lib/simplecov/combine/files_combiner.rb +0 -30
- data/lib/simplecov/formatter/html_formatter/public/application.css +0 -1
- data/lib/simplecov/formatter/html_formatter/public/application.js +0 -18
- data/lib/simplecov/formatter/html_formatter/public/favicon_green.png +0 -0
- data/lib/simplecov/formatter/html_formatter/public/favicon_red.png +0 -0
- data/lib/simplecov/formatter/html_formatter/public/favicon_yellow.png +0 -0
- data/schemas/coverage-v1.0.schema.json +0 -306
- data/schemas/coverage.schema.json +0 -306
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SimpleCov
|
|
4
|
+
module StaticCoverageExtractor
|
|
5
|
+
# The Prism 1.3 (Dec 2024) accessor renames for the trailing clause of
|
|
6
|
+
# conditional nodes, resolved ONCE at load so the per-node hot paths
|
|
7
|
+
# stay branch-free. Ruby 3.3's stdlib Prism (0.19) predates the
|
|
8
|
+
# renames; 3.4+ and any installed prism gem post-dates them. Reaching
|
|
9
|
+
# for the modern name on 0.19 raised NoMethodError inside the
|
|
10
|
+
# extractor — `call` swallowed it and the whole file silently fell
|
|
11
|
+
# back to no simulated data (see the 1.0.2 audit).
|
|
12
|
+
#
|
|
13
|
+
# This lives in its own file, required by every consumer, because the
|
|
14
|
+
# constants are referenced from several extractor files and defining
|
|
15
|
+
# them after those files were loaded worked only while the references
|
|
16
|
+
# happened at call time — a load-order trap for the next editor.
|
|
17
|
+
module PrismCompat
|
|
18
|
+
module_function
|
|
19
|
+
|
|
20
|
+
# `Prism::IfNode#subsequent` was renamed from `consequent`. The
|
|
21
|
+
# not-taken arm on whichever Prism version we're on can't be
|
|
22
|
+
# exercised by our own dogfood (we only run on one Prism at a time).
|
|
23
|
+
# simplecov:disable
|
|
24
|
+
IF_NODE_SUBSEQUENT_METHOD =
|
|
25
|
+
if ::Prism::IfNode.method_defined?(:subsequent)
|
|
26
|
+
:subsequent
|
|
27
|
+
else
|
|
28
|
+
:consequent
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# The same rename hit the `else` accessor on `UnlessNode`,
|
|
32
|
+
# `CaseNode`, and `CaseMatchNode` (all three: `consequent` ->
|
|
33
|
+
# `else_clause`). All three renamed together, so one constant
|
|
34
|
+
# (probed off CaseNode) covers them.
|
|
35
|
+
ELSE_CLAUSE_METHOD =
|
|
36
|
+
if ::Prism::CaseNode.method_defined?(:else_clause)
|
|
37
|
+
:else_clause
|
|
38
|
+
else
|
|
39
|
+
:consequent
|
|
40
|
+
end
|
|
41
|
+
# simplecov:enable
|
|
42
|
+
|
|
43
|
+
# The `else`/`elsif` clause of an if-like node (an ElseNode, or a
|
|
44
|
+
# nested IfNode for `elsif`), or the `else` clause of anything else
|
|
45
|
+
# that has one, under whichever accessor this Prism exposes.
|
|
46
|
+
def subsequent(node)
|
|
47
|
+
node.is_a?(::Prism::IfNode) ? node.public_send(IF_NODE_SUBSEQUENT_METHOD) : else_clause(node)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def else_clause(node)
|
|
51
|
+
node.public_send(ELSE_CLAUSE_METHOD)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "prism_compat"
|
|
4
|
+
|
|
5
|
+
module SimpleCov
|
|
6
|
+
module StaticCoverageExtractor
|
|
7
|
+
# Ruby 3.3 value-position analysis for the extractor's legacy branch
|
|
8
|
+
# conventions (see LocationConventions and the #1233 audit).
|
|
9
|
+
#
|
|
10
|
+
# On Ruby 3.3, the source range Coverage assigns to an EMPTY branch arm
|
|
11
|
+
# depends on whether its construct is in value position — its result is
|
|
12
|
+
# the method's return value — or void position, where the result is
|
|
13
|
+
# discarded. Value position keeps the whole-construct range; void
|
|
14
|
+
# collapses the arm to a point at its header's end. Ruby 3.4 dropped the
|
|
15
|
+
# distinction, so this pass only runs on legacy Rubies.
|
|
16
|
+
#
|
|
17
|
+
# "Value position" here is narrower than general value-use: it is
|
|
18
|
+
# strictly method-return (tail) position. It reaches a node only through
|
|
19
|
+
# statement tails and `if`/`unless`/`when` arms. Assignments, blocks,
|
|
20
|
+
# lambdas, method arguments, `case/in` arms, and loop bodies all discard
|
|
21
|
+
# it (Coverage treats their empty arms as void). So `tail_children`
|
|
22
|
+
# names the constructs that forward tail position and everything else
|
|
23
|
+
# falls through to the void default.
|
|
24
|
+
module ValuePositions
|
|
25
|
+
module_function
|
|
26
|
+
|
|
27
|
+
# simplecov:disable
|
|
28
|
+
# This whole pass runs only on legacy Rubies (the modern dogfood
|
|
29
|
+
# never calls it), so its lines can't be covered on the CI Ruby that
|
|
30
|
+
# enforces 100%. Behavior is pinned instead by the differential
|
|
31
|
+
# tuple-equivalence spec, which runs against real Coverage on 3.3.
|
|
32
|
+
|
|
33
|
+
# An identity set (a `compare_by_identity` Hash used as a set) of the
|
|
34
|
+
# Prism nodes Coverage treats as being in value position.
|
|
35
|
+
def call(root)
|
|
36
|
+
positions = {} #: Hash[untyped, bool]
|
|
37
|
+
positions.compare_by_identity
|
|
38
|
+
mark(root, true, positions)
|
|
39
|
+
positions
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def mark(node, in_value, positions)
|
|
43
|
+
return unless node.is_a?(::Prism::Node)
|
|
44
|
+
|
|
45
|
+
positions[node] = true if in_value
|
|
46
|
+
children = tail_children(node, in_value)
|
|
47
|
+
node.compact_child_nodes.each do |child|
|
|
48
|
+
mark(child, children.any? { |c| c.equal?(child) }, positions)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# The children of `node` that inherit its tail position; empty for the
|
|
53
|
+
# void default. A method body is a tail context even when the `def`
|
|
54
|
+
# itself is not (the method still returns its last expression), so it
|
|
55
|
+
# is included regardless of `in_value`.
|
|
56
|
+
def tail_children(node, in_value)
|
|
57
|
+
# A method body is a tail context even when the `def` is not.
|
|
58
|
+
return [node.body] if node.is_a?(::Prism::DefNode)
|
|
59
|
+
return [] unless in_value
|
|
60
|
+
|
|
61
|
+
# `case/in` (CaseMatchNode) is intentionally not a tail construct:
|
|
62
|
+
# its `in` arms and `else` both discard tail position.
|
|
63
|
+
case node
|
|
64
|
+
when ::Prism::StatementsNode then [node.body.last]
|
|
65
|
+
when ::Prism::IfNode, ::Prism::UnlessNode then [node.statements, PrismCompat.subsequent(node)]
|
|
66
|
+
when ::Prism::CaseNode then [*node.conditions, PrismCompat.else_clause(node)]
|
|
67
|
+
when ::Prism::ElseNode, ::Prism::WhenNode, ::Prism::BeginNode, ::Prism::ProgramNode then [node.statements]
|
|
68
|
+
else []
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
# simplecov:enable
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -1,34 +1,31 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "prism_compat"
|
|
4
|
+
require_relative "condition_folding"
|
|
5
|
+
require_relative "location_conventions"
|
|
3
6
|
require_relative "method_collector"
|
|
7
|
+
require_relative "value_position"
|
|
4
8
|
|
|
5
9
|
module SimpleCov
|
|
6
10
|
module StaticCoverageExtractor
|
|
7
|
-
# `Prism::IfNode#subsequent` was renamed from `consequent` in Prism
|
|
8
|
-
# 1.3 (Dec 2024). Ruby 3.3's stdlib still ships an older Prism that
|
|
9
|
-
# only exposes `consequent`; 3.4+ and any project that's done
|
|
10
|
-
# `gem install prism` exposes `subsequent`. Resolve the method name
|
|
11
|
-
# ONCE here so the per-node hot path stays branch-free. The
|
|
12
|
-
# not-taken arm on whichever Prism version we're on can't be
|
|
13
|
-
# exercised by our own dogfood (we only run on one Prism at a time).
|
|
14
|
-
# simplecov:disable
|
|
15
|
-
IF_NODE_SUBSEQUENT_METHOD =
|
|
16
|
-
if ::Prism::IfNode.method_defined?(:subsequent)
|
|
17
|
-
:subsequent
|
|
18
|
-
else
|
|
19
|
-
:consequent
|
|
20
|
-
end
|
|
21
|
-
# simplecov:enable
|
|
22
|
-
|
|
23
11
|
# Prism visitor that accumulates branch and method tuples in the
|
|
24
12
|
# shape Ruby's `Coverage` reports. Tuple ids are sequential across
|
|
25
|
-
# the file
|
|
26
|
-
#
|
|
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;
|
|
27
18
|
# `StaticCoverageExtractor.available?` is the runtime gate.
|
|
28
19
|
class Visitor < ::Prism::Visitor
|
|
29
20
|
# Method tuples and the class/module nesting that names them are
|
|
30
21
|
# collected by this mixin; this class focuses on branch extraction.
|
|
31
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
|
|
32
29
|
|
|
33
30
|
attr_reader :branches, :methods
|
|
34
31
|
|
|
@@ -38,6 +35,20 @@ module SimpleCov
|
|
|
38
35
|
@methods = {}
|
|
39
36
|
@next_id = 0
|
|
40
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
|
|
41
52
|
end
|
|
42
53
|
|
|
43
54
|
# `if` / `unless` / postfix-if / postfix-unless / ternary all parse
|
|
@@ -46,12 +57,26 @@ module SimpleCov
|
|
|
46
57
|
# `else`, another IfNode for `elsif`). When the subsequent is
|
|
47
58
|
# missing, Coverage synthesizes a `:else` arm attributed to the
|
|
48
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).
|
|
49
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
|
+
|
|
50
72
|
emit_if_like(node, :if)
|
|
51
73
|
super
|
|
52
74
|
end
|
|
53
75
|
|
|
54
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
|
+
|
|
55
80
|
emit_if_like(node, :unless)
|
|
56
81
|
super
|
|
57
82
|
end
|
|
@@ -74,6 +99,24 @@ module SimpleCov
|
|
|
74
99
|
super
|
|
75
100
|
end
|
|
76
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
|
+
|
|
77
120
|
# `while` / `until` loops get a single `:body` arm. No synthetic
|
|
78
121
|
# else (the loop either runs the body or doesn't).
|
|
79
122
|
def visit_while_node(node)
|
|
@@ -92,75 +135,44 @@ module SimpleCov
|
|
|
92
135
|
# optional else/elsif) but expose the trailing arm under different
|
|
93
136
|
# accessors. `if_like_else_location` hides that split.
|
|
94
137
|
def emit_if_like(node, type)
|
|
95
|
-
then_loc =
|
|
96
|
-
else_loc = if_like_else_location(node)
|
|
97
|
-
@branches[build_tuple(type, node
|
|
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))] = {
|
|
98
141
|
build_tuple(:then, then_loc) => 0,
|
|
99
142
|
build_tuple(:else, else_loc) => 0
|
|
100
143
|
}
|
|
101
144
|
end
|
|
102
145
|
|
|
103
146
|
def emit_safe_navigation(node)
|
|
104
|
-
loc = node
|
|
147
|
+
loc = safe_navigation_location(node)
|
|
105
148
|
@branches[build_tuple(:"&.", loc)] = {
|
|
106
149
|
build_tuple(:then, loc) => 0,
|
|
107
150
|
build_tuple(:else, loc) => 0
|
|
108
151
|
}
|
|
109
152
|
end
|
|
110
153
|
|
|
111
|
-
#
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
# convention).
|
|
118
|
-
def if_like_else_location(node)
|
|
119
|
-
sub = node.is_a?(::Prism::IfNode) ? node.public_send(IF_NODE_SUBSEQUENT_METHOD) : node.else_clause
|
|
120
|
-
return node.location unless sub
|
|
121
|
-
|
|
122
|
-
arm_location(else_body_of(sub), sub.location)
|
|
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
|
+
}
|
|
123
160
|
end
|
|
161
|
+
# simplecov:enable
|
|
124
162
|
|
|
125
163
|
def emit_case_like(node, when_type)
|
|
126
164
|
arms = node.conditions.to_h do |when_node|
|
|
127
|
-
|
|
128
|
-
[build_tuple(when_type, loc), 0]
|
|
165
|
+
[build_tuple(when_type, case_arm_location(node, when_node, when_type)), 0]
|
|
129
166
|
end
|
|
130
167
|
arms[build_tuple(:else, else_arm_location(node))] = 0
|
|
131
168
|
@branches[build_tuple(:case, node.location)] = arms
|
|
132
169
|
end
|
|
133
170
|
|
|
134
|
-
# Resolve the source range Coverage attributes to a synthetic-or-real
|
|
135
|
-
# `:else` arm of a case construct: the body of an explicit else,
|
|
136
|
-
# or the case's full range when no else is present.
|
|
137
|
-
def else_arm_location(node)
|
|
138
|
-
return node.location unless node.else_clause
|
|
139
|
-
|
|
140
|
-
arm_location(else_body_of(node.else_clause), node.else_clause.location)
|
|
141
|
-
end
|
|
142
|
-
|
|
143
171
|
def emit_loop(node, type)
|
|
144
172
|
cond_tuple = build_tuple(type, node.location)
|
|
145
|
-
|
|
146
|
-
@branches[cond_tuple] = {build_tuple(:body, body_loc) => 0}
|
|
173
|
+
@branches[cond_tuple] = {build_tuple(:body, loop_body_location(node)) => 0}
|
|
147
174
|
end
|
|
148
175
|
|
|
149
|
-
# Body location for an arm. Prism's `statements` is a StatementsNode
|
|
150
|
-
# whose span covers the contained expressions; fall back to the
|
|
151
|
-
# parent when the arm body is empty (e.g., `if cond then end`).
|
|
152
|
-
def arm_location(statements, fallback_location)
|
|
153
|
-
statements&.location || fallback_location
|
|
154
|
-
end
|
|
155
|
-
|
|
156
|
-
# simplecov:disable branch
|
|
157
|
-
# The `else_node` fallback is defensive: every Prism node passed
|
|
158
|
-
# in here in practice responds to `:statements`.
|
|
159
|
-
def else_body_of(else_node)
|
|
160
|
-
else_node.respond_to?(:statements) ? else_node.statements : else_node
|
|
161
|
-
end
|
|
162
|
-
# simplecov:enable branch
|
|
163
|
-
|
|
164
176
|
def build_tuple(type, location)
|
|
165
177
|
id = @next_id
|
|
166
178
|
@next_id += 1
|
|
@@ -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
|
data/lib/simplecov/version.rb
CHANGED
data/lib/simplecov.rb
CHANGED
|
@@ -8,14 +8,18 @@ module SimpleCov
|
|
|
8
8
|
# every coverage criterion has been disabled.
|
|
9
9
|
class ConfigurationError < StandardError; end
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
11
|
+
# Maps SimpleCov's criterion names to the keys `Coverage.start` expects.
|
|
12
|
+
# Lives at module scope (not inside `class << self`) so it can be
|
|
13
|
+
# declared in the RBS signatures; lexical scoping keeps every existing
|
|
14
|
+
# reference inside the singleton class working.
|
|
15
|
+
CRITERION_TO_RUBY_COVERAGE = {
|
|
16
|
+
branch: :branches,
|
|
17
|
+
line: :lines,
|
|
18
|
+
method: :methods,
|
|
19
|
+
oneshot_line: :oneshot_lines
|
|
20
|
+
}.freeze
|
|
18
21
|
|
|
22
|
+
class << self
|
|
19
23
|
attr_accessor :pid
|
|
20
24
|
# When this process started tracking coverage. Captured by SimpleCov.start
|
|
21
25
|
# so JSONFormatter can detect when an existing coverage.json was written
|
|
@@ -93,10 +97,13 @@ module SimpleCov
|
|
|
93
97
|
# the warning is the cue to move `SimpleCov.start` into a test helper.
|
|
94
98
|
# See #581.
|
|
95
99
|
def with_dot_simplecov_autoload
|
|
96
|
-
|
|
100
|
+
# Read in the ensure clause, where flow analysis cannot see the
|
|
101
|
+
# assignment above; anchor the type here.
|
|
102
|
+
previous = @autoloading_dot_simplecov # : bool?
|
|
97
103
|
@autoloading_dot_simplecov = true
|
|
98
104
|
yield
|
|
99
105
|
ensure
|
|
106
|
+
# @type var previous: bool?
|
|
100
107
|
@autoloading_dot_simplecov = previous
|
|
101
108
|
end
|
|
102
109
|
|
|
@@ -123,7 +130,11 @@ module SimpleCov
|
|
|
123
130
|
return if @at_exit_hook_installed
|
|
124
131
|
|
|
125
132
|
@at_exit_hook_installed = true
|
|
126
|
-
|
|
133
|
+
# Never defer in a forked child: Minitest pins its after_run at_exit
|
|
134
|
+
# to the pid that armed autorun, so the deferral target can't fire
|
|
135
|
+
# there and the child's resultset would be silently dropped. See
|
|
136
|
+
# issue #1227.
|
|
137
|
+
defer_to_minitest_after_run if minitest_autorun_pending? && !forked_subprocess?
|
|
127
138
|
Kernel.at_exit do
|
|
128
139
|
next if SimpleCov.external_at_exit?
|
|
129
140
|
|
|
@@ -147,10 +158,8 @@ module SimpleCov
|
|
|
147
158
|
::Process.respond_to?(:_fork)
|
|
148
159
|
# simplecov:enable
|
|
149
160
|
|
|
150
|
-
#
|
|
151
|
-
|
|
152
|
-
# `current` is memoized; subsequent calls are cheap.
|
|
153
|
-
SimpleCov::ParallelAdapters.current
|
|
161
|
+
# Select the parallel adapter and generate identities before any forks.
|
|
162
|
+
SimpleCov::RunIdentity.prepare
|
|
154
163
|
|
|
155
164
|
@result = nil
|
|
156
165
|
self.pid = Process.pid
|
|
@@ -173,7 +182,7 @@ module SimpleCov
|
|
|
173
182
|
|
|
174
183
|
start_arguments[:eval] = true if coverage_for_eval_enabled?
|
|
175
184
|
|
|
176
|
-
Coverage.start(start_arguments) unless Coverage.running?
|
|
185
|
+
Coverage.start(**start_arguments) unless Coverage.running?
|
|
177
186
|
end
|
|
178
187
|
|
|
179
188
|
# `Rake::TestTask` runs `ruby -e 'require "minitest/autorun"; ...'`,
|
|
@@ -203,7 +212,9 @@ module SimpleCov
|
|
|
203
212
|
return unless defined?(JRUBY_VERSION) && defined?(JRuby) # simplecov:disable — JRuby-only branch
|
|
204
213
|
|
|
205
214
|
# simplecov:disable — JRuby-only branches; unreachable from CRuby
|
|
206
|
-
|
|
215
|
+
# `org` is JRuby's Java-package entry point; it does not exist on
|
|
216
|
+
# CRuby, so no RBS declaration can be truthful here.
|
|
217
|
+
return if org.jruby.RubyInstanceConfig.FULL_TRACE_ENABLED # steep:ignore NoMethod
|
|
207
218
|
|
|
208
219
|
warn 'Coverage may be inaccurate; set the "--debug" command line option, ' \
|
|
209
220
|
'or do JRUBY_OPTS="--debug" ' \
|
|
@@ -217,6 +228,7 @@ end
|
|
|
217
228
|
require "forwardable"
|
|
218
229
|
require_relative "simplecov/color"
|
|
219
230
|
require_relative "simplecov/deprecation"
|
|
231
|
+
require_relative "simplecov/group_names"
|
|
220
232
|
require_relative "simplecov/configuration"
|
|
221
233
|
SimpleCov.extend SimpleCov::Configuration
|
|
222
234
|
require_relative "simplecov/coverage_statistics"
|
|
@@ -232,24 +244,32 @@ require_relative "simplecov/result"
|
|
|
232
244
|
require_relative "simplecov/filter"
|
|
233
245
|
require_relative "simplecov/formatter"
|
|
234
246
|
require_relative "simplecov/last_run"
|
|
247
|
+
require_relative "simplecov/report_stamp"
|
|
235
248
|
require_relative "simplecov/lines_classifier"
|
|
236
249
|
require_relative "simplecov/result_merger"
|
|
250
|
+
require_relative "simplecov/parallel_result_merger"
|
|
237
251
|
require_relative "simplecov/parallel_adapters"
|
|
252
|
+
require_relative "simplecov/run_identity"
|
|
253
|
+
SimpleCov.extend SimpleCov::RunIdentity::Accessors
|
|
238
254
|
require_relative "simplecov/command_guesser"
|
|
239
255
|
require_relative "simplecov/version"
|
|
240
256
|
require_relative "simplecov/result_adapter"
|
|
241
257
|
require_relative "simplecov/combine"
|
|
258
|
+
require_relative "simplecov/combine/identity_interner"
|
|
242
259
|
require_relative "simplecov/combine/branches_combiner"
|
|
243
260
|
require_relative "simplecov/combine/methods_combiner"
|
|
244
|
-
require_relative "simplecov/combine/files_combiner"
|
|
245
261
|
require_relative "simplecov/combine/lines_combiner"
|
|
262
|
+
require_relative "simplecov/combine/coverage_accumulator"
|
|
246
263
|
require_relative "simplecov/combine/results_combiner"
|
|
247
264
|
require_relative "simplecov/useless_results_remover"
|
|
248
265
|
require_relative "simplecov/simulate_coverage"
|
|
266
|
+
require_relative "simplecov/unloaded_file_injector"
|
|
249
267
|
require_relative "simplecov/result_processing"
|
|
250
268
|
require_relative "simplecov/exit_handling"
|
|
269
|
+
require_relative "simplecov/report_deferral"
|
|
251
270
|
require_relative "simplecov/parallel_coordination"
|
|
252
271
|
|
|
253
272
|
# Load default config
|
|
254
|
-
# simplecov:disable — env-var
|
|
273
|
+
# simplecov:disable — env-var never set in the dogfooded test process
|
|
274
|
+
# (sandbox fixture subprocesses set it via simplecov/no_defaults)
|
|
255
275
|
require_relative "simplecov/defaults" unless ENV["SIMPLECOV_NO_DEFAULTS"]
|