simplecov 1.1.1 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/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/annotations.rb +166 -0
- data/lib/simplecov/cli/badge/svg.rb +70 -0
- data/lib/simplecov/cli/badge.rb +62 -0
- data/lib/simplecov/cli/clean.rb +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 +151 -0
- data/lib/simplecov/cli/patch.rb +175 -0
- data/lib/simplecov/cli/ratchet/output.rb +44 -0
- data/lib/simplecov/cli/ratchet.rb +83 -0
- data/lib/simplecov/cli/report.rb +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 +33 -0
- data/lib/simplecov/cli/uncovered.rb +58 -34
- data/lib/simplecov/cli/usage.rb +153 -0
- data/lib/simplecov/cli/watch/live_report.rb +73 -0
- data/lib/simplecov/cli/watch/narrator.rb +55 -0
- data/lib/simplecov/cli/watch/poller.rb +45 -0
- data/lib/simplecov/cli/watch/session.rb +134 -0
- data/lib/simplecov/cli/watch/test_plan.rb +43 -0
- data/lib/simplecov/cli/watch.rb +85 -0
- data/lib/simplecov/cli.rb +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 +7 -11
- 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 +37 -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 +160 -65
- 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/erb.rb +44 -0
- data/lib/simplecov/directive/haml.rb +33 -0
- data/lib/simplecov/directive/indented_template.rb +37 -0
- data/lib/simplecov/directive/slim.rb +35 -0
- data/lib/simplecov/directive/template.rb +29 -0
- data/lib/simplecov/directive.rb +47 -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 +4 -15
- data/lib/simplecov/profiles/root_filter.rb +0 -4
- data/lib/simplecov/profiles/strict.rb +11 -21
- 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 +74 -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 +92 -122
- 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 +13 -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 +29 -29
- data/lib/simplecov/source_file/source_loader.rb +45 -35
- data/lib/simplecov/source_file/statistics.rb +20 -9
- data/lib/simplecov/source_file.rb +21 -51
- data/lib/simplecov/static_coverage_extractor/condition_folding.rb +55 -139
- data/lib/simplecov/static_coverage_extractor/location_conventions.rb +76 -121
- data/lib/simplecov/static_coverage_extractor/method_collector.rb +9 -26
- 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 +45 -65
- data/lib/simplecov/static_coverage_extractor.rb +43 -75
- 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 +354 -0
- data/sig/simplecov.rbs +528 -595
- metadata +79 -6
|
@@ -2,15 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
module SimpleCov
|
|
4
4
|
module StaticCoverageExtractor
|
|
5
|
-
# Visitor mixin that collects method tuples and tracks the lexical
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
# method-collection concern separate from branch extraction.
|
|
5
|
+
# Visitor mixin that collects method tuples and tracks the lexical class /
|
|
6
|
+
# module nesting that names them, in the shape Ruby's `Coverage` reports
|
|
7
|
+
# methods. Module and Class are both namespaces here, since `Coverage` reports
|
|
8
|
+
# both as the constant.
|
|
10
9
|
module MethodCollector
|
|
11
|
-
# Track class/module nesting so method tuples carry the lexical
|
|
12
|
-
# class name. Module + Class are both treated as namespaces here
|
|
13
|
-
# since `Coverage` reports both as the constant.
|
|
14
10
|
def visit_class_node(node)
|
|
15
11
|
with_class(constant_name(node.constant_path)) { super }
|
|
16
12
|
end
|
|
@@ -19,37 +15,24 @@ module SimpleCov
|
|
|
19
15
|
with_class(constant_name(node.constant_path)) { super }
|
|
20
16
|
end
|
|
21
17
|
|
|
22
|
-
# `def name(...)` and `def self.name(...)` both produce DefNode.
|
|
23
|
-
#
|
|
24
|
-
#
|
|
25
|
-
# Suppression covers 3.2's folded dead arms, where nested branches
|
|
26
|
-
# stay instrumented but a `def` never registers (see
|
|
27
|
-
# Visitor#visit_dead_arm).
|
|
18
|
+
# `def name(...)` and `def self.name(...)` both produce DefNode. The class
|
|
19
|
+
# context is the surrounding lexical class or module, or `Object` at the top
|
|
20
|
+
# level, matching `Coverage`'s convention.
|
|
28
21
|
def visit_def_node(node)
|
|
29
|
-
# simplecov:disable branch — suppression is reachable on 3.2 only
|
|
30
|
-
return super if @suppress_methods
|
|
31
|
-
|
|
32
|
-
# simplecov:enable branch
|
|
33
|
-
loc = node.location
|
|
34
22
|
class_name = @class_stack.last || "Object"
|
|
35
|
-
key = [class_name, node.name,
|
|
23
|
+
key = [class_name, node.name, node.start_line, node.start_column, node.end_line, node.end_column]
|
|
36
24
|
@methods[key] = 0
|
|
37
25
|
super
|
|
38
26
|
end
|
|
39
27
|
|
|
40
|
-
|
|
28
|
+
private
|
|
41
29
|
|
|
42
|
-
# Render a constant path (e.g., `Foo::Bar`) as its source-form
|
|
43
|
-
# string. Defensive nil / to_s fallbacks: ClassNode and ModuleNode
|
|
44
|
-
# always carry a constant_path in practice.
|
|
45
|
-
# simplecov:disable
|
|
46
30
|
def constant_name(node)
|
|
47
31
|
return "<anonymous>" if node.nil?
|
|
48
32
|
return node.slice if node.respond_to?(:slice)
|
|
49
33
|
|
|
50
34
|
node.to_s
|
|
51
35
|
end
|
|
52
|
-
# simplecov:enable
|
|
53
36
|
|
|
54
37
|
def with_class(name)
|
|
55
38
|
@class_stack.push(name)
|
|
@@ -2,51 +2,42 @@
|
|
|
2
2
|
|
|
3
3
|
module SimpleCov
|
|
4
4
|
module StaticCoverageExtractor
|
|
5
|
-
# The Prism 1.3
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
# back to no simulated data (see the 1.0.2 audit).
|
|
5
|
+
# The Prism 1.3 accessor renames for the trailing clause of conditional
|
|
6
|
+
# nodes, resolved once at load so the per-node hot paths stay branch-free.
|
|
7
|
+
# Ruby 3.3's stdlib Prism predates the renames; 3.4+ and any installed prism
|
|
8
|
+
# gem post-date them, and reaching for the modern name on the old one raised
|
|
9
|
+
# NoMethodError inside the extractor, which `call` swallowed, silently
|
|
10
|
+
# falling back to no simulated data.
|
|
12
11
|
#
|
|
13
12
|
# This lives in its own file, required by every consumer, because the
|
|
14
|
-
# constants are referenced from several extractor files and defining
|
|
15
|
-
#
|
|
16
|
-
#
|
|
13
|
+
# constants are referenced from several extractor files and defining them
|
|
14
|
+
# after those files were loaded worked only while the references happened at
|
|
15
|
+
# call time: a load-order trap for the next editor.
|
|
17
16
|
module PrismCompat
|
|
18
|
-
|
|
17
|
+
extend self
|
|
19
18
|
|
|
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
19
|
# simplecov:disable
|
|
24
20
|
IF_NODE_SUBSEQUENT_METHOD =
|
|
25
|
-
if
|
|
21
|
+
if Prism::IfNode.method_defined?(:subsequent)
|
|
26
22
|
:subsequent
|
|
27
23
|
else
|
|
28
24
|
:consequent
|
|
29
25
|
end
|
|
30
26
|
|
|
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
27
|
ELSE_CLAUSE_METHOD =
|
|
36
|
-
if
|
|
28
|
+
if Prism::CaseNode.method_defined?(:else_clause)
|
|
37
29
|
:else_clause
|
|
38
30
|
else
|
|
39
31
|
:consequent
|
|
40
32
|
end
|
|
41
33
|
# simplecov:enable
|
|
42
34
|
|
|
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
35
|
def subsequent(node)
|
|
47
|
-
node.
|
|
36
|
+
node.instance_of?(Prism::IfNode) ? node.public_send(IF_NODE_SUBSEQUENT_METHOD) : else_clause(node)
|
|
48
37
|
end
|
|
49
38
|
|
|
39
|
+
# The rename hit `UnlessNode`, `CaseNode`, and `CaseMatchNode` together, so
|
|
40
|
+
# one constant probed off CaseNode covers all three.
|
|
50
41
|
def else_clause(node)
|
|
51
42
|
node.public_send(ELSE_CLAUSE_METHOD)
|
|
52
43
|
end
|
|
@@ -5,33 +5,24 @@ require_relative "prism_compat"
|
|
|
5
5
|
module SimpleCov
|
|
6
6
|
module StaticCoverageExtractor
|
|
7
7
|
# Ruby 3.3 value-position analysis for the extractor's legacy branch
|
|
8
|
-
# conventions (
|
|
8
|
+
# conventions (#1233). On 3.3 the source range Coverage assigns to an empty
|
|
9
|
+
# branch arm depends on whether its construct is in value position, where its
|
|
10
|
+
# result is the method's return value, or void position, where the result is
|
|
11
|
+
# discarded. Value position keeps the whole-construct range; void collapses
|
|
12
|
+
# the arm to a point at its header's end. Ruby 3.4 dropped the distinction.
|
|
9
13
|
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
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.
|
|
14
|
+
# "Value position" here is strictly method-return (tail) position. It reaches
|
|
15
|
+
# a node only through statement tails and `if`/`unless`/`when` arms.
|
|
16
|
+
# Assignments, blocks, lambdas, method arguments, `case/in` arms, and loop
|
|
17
|
+
# bodies all discard it, so `tail_children` names the constructs that forward
|
|
18
|
+
# tail position and everything else falls through to the void default.
|
|
24
19
|
module ValuePositions
|
|
25
|
-
|
|
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.
|
|
20
|
+
extend self
|
|
32
21
|
|
|
33
|
-
# An identity set
|
|
34
|
-
#
|
|
22
|
+
# An identity set of the Prism nodes Coverage treats as being in value
|
|
23
|
+
# position. This pass runs natively only on legacy Rubies; elsewhere the
|
|
24
|
+
# specs drive it directly, pinned by the differential tuple-equivalence spec
|
|
25
|
+
# against real Coverage on 3.3.
|
|
35
26
|
def call(root)
|
|
36
27
|
positions = {} #: Hash[untyped, bool]
|
|
37
28
|
positions.compare_by_identity
|
|
@@ -40,7 +31,7 @@ module SimpleCov
|
|
|
40
31
|
end
|
|
41
32
|
|
|
42
33
|
def mark(node, in_value, positions)
|
|
43
|
-
return unless node.is_a?(
|
|
34
|
+
return unless node.is_a?(Prism::Node)
|
|
44
35
|
|
|
45
36
|
positions[node] = true if in_value
|
|
46
37
|
children = tail_children(node, in_value)
|
|
@@ -49,26 +40,22 @@ module SimpleCov
|
|
|
49
40
|
end
|
|
50
41
|
end
|
|
51
42
|
|
|
52
|
-
#
|
|
53
|
-
#
|
|
54
|
-
#
|
|
55
|
-
#
|
|
43
|
+
# A method body is a tail context even when the `def` itself is not, since
|
|
44
|
+
# the method still returns its last expression, so it is included regardless
|
|
45
|
+
# of `in_value`. `case/in` is intentionally absent: its `in` arms and `else`
|
|
46
|
+
# both discard tail position.
|
|
56
47
|
def tail_children(node, in_value)
|
|
57
|
-
|
|
58
|
-
return [node.body] if node.is_a?(::Prism::DefNode)
|
|
48
|
+
return [node.body] if node.instance_of?(Prism::DefNode)
|
|
59
49
|
return [] unless in_value
|
|
60
50
|
|
|
61
|
-
# `case/in` (CaseMatchNode) is intentionally not a tail construct:
|
|
62
|
-
# its `in` arms and `else` both discard tail position.
|
|
63
51
|
case node
|
|
64
|
-
when
|
|
65
|
-
when
|
|
66
|
-
when
|
|
67
|
-
when
|
|
52
|
+
when Prism::StatementsNode then [node.body.last]
|
|
53
|
+
when Prism::IfNode, Prism::UnlessNode then [node.statements, PrismCompat.subsequent(node)]
|
|
54
|
+
when Prism::CaseNode then [*node.conditions, PrismCompat.else_clause(node)]
|
|
55
|
+
when Prism::ElseNode, Prism::WhenNode, Prism::BeginNode, Prism::ProgramNode then [node.statements]
|
|
68
56
|
else []
|
|
69
57
|
end
|
|
70
58
|
end
|
|
71
|
-
# simplecov:enable
|
|
72
59
|
end
|
|
73
60
|
end
|
|
74
61
|
end
|
|
@@ -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.
|
|
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 = {}
|
|
@@ -36,35 +31,25 @@ module SimpleCov
|
|
|
36
31
|
@next_id = 0
|
|
37
32
|
@class_stack = []
|
|
38
33
|
@value_positions = nil
|
|
39
|
-
@suppress_methods = false
|
|
40
34
|
end
|
|
41
35
|
|
|
42
|
-
#
|
|
43
|
-
#
|
|
44
|
-
#
|
|
45
|
-
# emitting anything. Modern Rubies don't need it (see
|
|
46
|
-
# LocationConventions), so the pass is skipped there.
|
|
36
|
+
# On legacy Rubies the location of an empty branch arm depends on whether
|
|
37
|
+
# its construct is in value (tail) position, so precompute that once for
|
|
38
|
+
# the whole tree before emitting anything.
|
|
47
39
|
def visit_program_node(node)
|
|
48
|
-
# simplecov:disable branch — legacy-only arm; unreachable on the modern dogfood Ruby
|
|
49
40
|
@value_positions = ValuePositions.call(node) if LEGACY_COVERAGE_LOCATIONS
|
|
50
|
-
# simplecov:enable branch
|
|
51
41
|
super
|
|
52
42
|
end
|
|
53
43
|
|
|
54
|
-
# `if` / `unless` / postfix
|
|
55
|
-
#
|
|
56
|
-
#
|
|
57
|
-
#
|
|
58
|
-
#
|
|
59
|
-
# whole condition's range — we do the same.
|
|
44
|
+
# `if` / `unless` / postfix / ternary all parse as IfNode (or UnlessNode).
|
|
45
|
+
# Both carry a `then` arm and an optional `subsequent` (an ElseNode for
|
|
46
|
+
# `else`, another IfNode for `elsif`). When the subsequent is missing,
|
|
47
|
+
# Coverage synthesizes a `:else` arm attributed to the whole condition's
|
|
48
|
+
# range, and so do we.
|
|
60
49
|
#
|
|
61
|
-
# A folded condition emits no tuple, and
|
|
62
|
-
#
|
|
63
|
-
#
|
|
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).
|
|
50
|
+
# A folded condition emits no tuple, and only its live arm is descended
|
|
51
|
+
# into: the compiler eliminates the dead arm's entire subtree, so a branch
|
|
52
|
+
# or method nested there would be a phantom no loaded run can produce.
|
|
68
53
|
def visit_if_node(node)
|
|
69
54
|
verdict = folded_condition(node.predicate)
|
|
70
55
|
return visit_folded_arms(verdict, node.statements, PrismCompat.subsequent(node)) if verdict
|
|
@@ -86,9 +71,8 @@ module SimpleCov
|
|
|
86
71
|
super
|
|
87
72
|
end
|
|
88
73
|
|
|
89
|
-
#
|
|
90
|
-
#
|
|
91
|
-
# Coverage synthesizes one at the case's range.
|
|
74
|
+
# When there's no explicit `else`, Coverage synthesizes one at the case's
|
|
75
|
+
# range.
|
|
92
76
|
def visit_case_node(node)
|
|
93
77
|
emit_case_like(node, :when)
|
|
94
78
|
super
|
|
@@ -99,26 +83,22 @@ module SimpleCov
|
|
|
99
83
|
super
|
|
100
84
|
end
|
|
101
85
|
|
|
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
|
|
86
|
+
# One-line pattern matching: `x => pattern` and `x in pattern`. Ruby 3.3's
|
|
87
|
+
# Coverage reports these as a `:case` with an `:in` and an `:else` arm;
|
|
88
|
+
# 3.4 dropped them entirely, so this is legacy-only. The two forms differ
|
|
89
|
+
# only in where Coverage anchors the synthesized `:else`: `=>` uses the
|
|
90
|
+
# whole expression, `in` uses just the pattern.
|
|
109
91
|
def visit_match_required_node(node)
|
|
110
|
-
emit_oneline_pattern(node, node
|
|
92
|
+
emit_oneline_pattern(node, node) if LEGACY_COVERAGE_LOCATIONS
|
|
111
93
|
super
|
|
112
94
|
end
|
|
113
95
|
|
|
114
96
|
def visit_match_predicate_node(node)
|
|
115
|
-
emit_oneline_pattern(node, node.pattern
|
|
97
|
+
emit_oneline_pattern(node, node.pattern) if LEGACY_COVERAGE_LOCATIONS
|
|
116
98
|
super
|
|
117
99
|
end
|
|
118
|
-
# simplecov:enable branch
|
|
119
100
|
|
|
120
|
-
#
|
|
121
|
-
# else (the loop either runs the body or doesn't).
|
|
101
|
+
# A loop gets a single `:body` arm and no synthetic else.
|
|
122
102
|
def visit_while_node(node)
|
|
123
103
|
emit_loop(node, :while)
|
|
124
104
|
super
|
|
@@ -129,11 +109,10 @@ module SimpleCov
|
|
|
129
109
|
super
|
|
130
110
|
end
|
|
131
111
|
|
|
132
|
-
|
|
112
|
+
private
|
|
133
113
|
|
|
134
|
-
# IfNode and UnlessNode share a shape
|
|
135
|
-
#
|
|
136
|
-
# accessors. `if_like_else_location` hides that split.
|
|
114
|
+
# IfNode and UnlessNode share a shape but expose the trailing arm under
|
|
115
|
+
# different accessors, which `if_like_else_location` hides.
|
|
137
116
|
def emit_if_like(node, type)
|
|
138
117
|
then_loc = if_like_then_location(node, type)
|
|
139
118
|
else_loc = if_like_else_location(node, type)
|
|
@@ -151,32 +130,33 @@ module SimpleCov
|
|
|
151
130
|
}
|
|
152
131
|
end
|
|
153
132
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
build_tuple(:
|
|
158
|
-
build_tuple(:else, else_location) => 0
|
|
133
|
+
def emit_oneline_pattern(node, else_span)
|
|
134
|
+
@branches[build_tuple(:case, node)] = {
|
|
135
|
+
build_tuple(:in, node.pattern) => 0,
|
|
136
|
+
build_tuple(:else, else_span) => 0
|
|
159
137
|
}
|
|
160
138
|
end
|
|
161
|
-
# simplecov:enable
|
|
162
139
|
|
|
163
140
|
def emit_case_like(node, when_type)
|
|
164
141
|
arms = node.conditions.to_h do |when_node|
|
|
165
142
|
[build_tuple(when_type, case_arm_location(node, when_node, when_type)), 0]
|
|
166
143
|
end
|
|
167
144
|
arms[build_tuple(:else, else_arm_location(node))] = 0
|
|
168
|
-
@branches[build_tuple(:case, node
|
|
145
|
+
@branches[build_tuple(:case, node)] = arms
|
|
169
146
|
end
|
|
170
147
|
|
|
171
148
|
def emit_loop(node, type)
|
|
172
|
-
cond_tuple = build_tuple(type, node
|
|
149
|
+
cond_tuple = build_tuple(type, node)
|
|
173
150
|
@branches[cond_tuple] = {build_tuple(:body, loop_body_location(node)) => 0}
|
|
174
151
|
end
|
|
175
152
|
|
|
176
|
-
|
|
153
|
+
# `span` is anything that answers the four position accessors: a location
|
|
154
|
+
# from LocationConventions, or the node itself where the range wanted is
|
|
155
|
+
# the node's own.
|
|
156
|
+
def build_tuple(type, span)
|
|
177
157
|
id = @next_id
|
|
178
158
|
@next_id += 1
|
|
179
|
-
[type, id,
|
|
159
|
+
[type, id, span.start_line, span.start_column, span.end_line, span.end_column]
|
|
180
160
|
end
|
|
181
161
|
end
|
|
182
162
|
end
|
|
@@ -1,110 +1,78 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
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
|
|
3
|
+
require "prism"
|
|
10
4
|
|
|
11
5
|
module SimpleCov
|
|
12
6
|
# Static enumeration of the branches and methods Ruby's `Coverage` library
|
|
13
|
-
#
|
|
7
|
+
# would have reported if a file had been loaded with `branches: true` /
|
|
14
8
|
# `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).
|
|
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.
|
|
9
|
+
# added via `cover` / `track_files` that were never required during the run,
|
|
10
|
+
# so unloaded files contribute to the branch/method denominators
|
|
11
|
+
# symmetrically with their line coverage (#1059).
|
|
24
12
|
#
|
|
25
|
-
# The emitted shape mirrors `Coverage.result[path]` for the same file
|
|
26
|
-
#
|
|
27
|
-
#
|
|
28
|
-
#
|
|
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.
|
|
13
|
+
# The emitted shape mirrors `Coverage.result[path]` for the same file.
|
|
14
|
+
# Position info comes from Prism's reported source locations; it doesn't
|
|
15
|
+
# always match `Coverage`'s byte-for-byte, but lines are reliable and
|
|
16
|
+
# downstream consumers that key off line numbers see the data they expect.
|
|
33
17
|
module StaticCoverageExtractor
|
|
34
|
-
|
|
18
|
+
extend self
|
|
35
19
|
|
|
36
|
-
#
|
|
37
|
-
#
|
|
38
|
-
#
|
|
39
|
-
#
|
|
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."
|
|
20
|
+
# Parse `source` and return `{"branches" => {...}, "methods" => {...}}`
|
|
21
|
+
# matching the shape `Coverage.result[path]` produces. Returns nil on parse
|
|
22
|
+
# failure, which callers treat as "couldn't extract, fall back to empty
|
|
23
|
+
# hashes".
|
|
51
24
|
def call(source)
|
|
52
|
-
|
|
53
|
-
return nil unless available?
|
|
54
|
-
|
|
55
|
-
# simplecov:enable branch
|
|
56
|
-
|
|
57
|
-
result = ::Prism.parse(source)
|
|
25
|
+
result = Prism.parse(source)
|
|
58
26
|
return nil if result.failure?
|
|
59
27
|
|
|
60
28
|
visitor = Visitor.new
|
|
61
29
|
visitor.visit(result.value)
|
|
62
30
|
{"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.
|
|
31
|
+
rescue
|
|
32
|
+
# Parser errors beyond the .failure? check, unsupported AST shapes, or
|
|
33
|
+
# anything else: fall back to empty hashes rather than crashing the whole
|
|
34
|
+
# report.
|
|
69
35
|
nil
|
|
70
|
-
# simplecov:enable line
|
|
71
36
|
end
|
|
72
37
|
|
|
73
|
-
# Summarize a source file's
|
|
74
|
-
# `:eval_generated` filter (
|
|
75
|
-
# SimpleCov.ignore_methods, #1046). Returns a hash:
|
|
38
|
+
# Summarize a source file's real branch and method positions, for the
|
|
39
|
+
# `:eval_generated` filter (#1046). Answers:
|
|
76
40
|
#
|
|
77
41
|
# {
|
|
78
42
|
# branches: Set[start_line, ...], # e.g., [3, 12, 20]
|
|
79
43
|
# methods: Set[[name, start_line], ...] # e.g., [[:foo, 7], [:bar, 13]]
|
|
80
44
|
# }
|
|
81
45
|
#
|
|
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.
|
|
46
|
+
# Branch matching is start_line-only rather than by the full tuple. Static
|
|
47
|
+
# extraction and Coverage can still disagree on a branch's exact column
|
|
48
|
+
# positions, so matching on start_line alone tolerates those differences.
|
|
49
|
+
# Coincidental line-sharing between a real branch and an eval-generated one
|
|
50
|
+
# keeps both, an acceptable false-negative for an opt-in filter.
|
|
90
51
|
#
|
|
91
|
-
# Returns nil when
|
|
92
|
-
#
|
|
52
|
+
# Returns nil when parsing fails, signaling callers to keep every Coverage
|
|
53
|
+
# entry.
|
|
93
54
|
def real_source_positions(source)
|
|
94
55
|
extracted = call(source)
|
|
95
56
|
return nil unless extracted
|
|
96
57
|
|
|
97
58
|
{
|
|
98
|
-
branches: extracted
|
|
99
|
-
methods: extracted
|
|
59
|
+
branches: extracted.fetch("branches").keys.to_set { |tuple| branch_start_line(*tuple) },
|
|
60
|
+
methods: extracted.fetch("methods").keys.to_set { |tuple| method_identity(*tuple) }
|
|
100
61
|
}
|
|
101
62
|
end
|
|
63
|
+
|
|
64
|
+
# Both keys carry their start line third, after the parts that vary between
|
|
65
|
+
# recordings. Read through a parameter list rather than an index, because
|
|
66
|
+
# every spelling of an index answers the same for a tuple of this fixed
|
|
67
|
+
# shape. Binding an argument has only the one spelling.
|
|
68
|
+
def branch_start_line(_type, _id, start_line, *)
|
|
69
|
+
start_line
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def method_identity(_class_name, name, start_line, *)
|
|
73
|
+
[name, start_line]
|
|
74
|
+
end
|
|
102
75
|
end
|
|
103
76
|
end
|
|
104
77
|
|
|
105
|
-
|
|
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
|
|
78
|
+
require_relative "static_coverage_extractor/visitor"
|
|
@@ -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
|