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.
Files changed (117) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +56 -1451
  3. data/lib/simplecov/atomic_file.rb +70 -0
  4. data/lib/simplecov/cli/clean.rb +43 -3
  5. data/lib/simplecov/cli/command_helpers.rb +55 -0
  6. data/lib/simplecov/cli/coverage.rb +23 -28
  7. data/lib/simplecov/cli/coverage_file.rb +65 -0
  8. data/lib/simplecov/cli/diff.rb +43 -47
  9. data/lib/simplecov/cli/dotfile.rb +21 -10
  10. data/lib/simplecov/cli/merge.rb +17 -13
  11. data/lib/simplecov/cli/open.rb +3 -5
  12. data/lib/simplecov/cli/report.rb +38 -25
  13. data/lib/simplecov/cli/run.rb +1 -1
  14. data/lib/simplecov/cli/serve/report_preparer.rb +29 -0
  15. data/lib/simplecov/cli/serve/static_file_handler.rb +122 -0
  16. data/lib/simplecov/cli/serve.rb +45 -99
  17. data/lib/simplecov/cli/uncovered.rb +20 -24
  18. data/lib/simplecov/cli.rb +15 -2
  19. data/lib/simplecov/color.rb +10 -1
  20. data/lib/simplecov/combine/branches_combiner.rb +43 -18
  21. data/lib/simplecov/combine/coverage_accumulator.rb +268 -0
  22. data/lib/simplecov/combine/identity_interner.rb +30 -0
  23. data/lib/simplecov/combine/interned_counts.rb +30 -0
  24. data/lib/simplecov/combine/lines_combiner.rb +48 -20
  25. data/lib/simplecov/combine/methods_combiner.rb +53 -5
  26. data/lib/simplecov/combine/results_combiner.rb +12 -36
  27. data/lib/simplecov/combine.rb +5 -23
  28. data/lib/simplecov/command_guesser.rb +66 -9
  29. data/lib/simplecov/configuration/coverage.rb +17 -16
  30. data/lib/simplecov/configuration/coverage_criteria.rb +37 -39
  31. data/lib/simplecov/configuration/eval_coverage.rb +41 -0
  32. data/lib/simplecov/configuration/filters.rb +11 -44
  33. data/lib/simplecov/configuration/formatting.rb +33 -14
  34. data/lib/simplecov/configuration/groups.rb +42 -0
  35. data/lib/simplecov/configuration/merging.rb +8 -7
  36. data/lib/simplecov/configuration/thresholds.rb +27 -22
  37. data/lib/simplecov/configuration.rb +21 -42
  38. data/lib/simplecov/coverage_json.rb +24 -0
  39. data/lib/simplecov/coverage_statistics.rb +5 -2
  40. data/lib/simplecov/coverage_violations.rb +17 -6
  41. data/lib/simplecov/defaults.rb +10 -6
  42. data/lib/simplecov/directive.rb +9 -4
  43. data/lib/simplecov/exit_codes/check.rb +33 -0
  44. data/lib/simplecov/exit_codes/maximum_coverage_drop_check.rb +11 -20
  45. data/lib/simplecov/exit_codes/maximum_overall_coverage_check.rb +4 -17
  46. data/lib/simplecov/exit_codes/minimum_coverage_by_file_check.rb +14 -23
  47. data/lib/simplecov/exit_codes/minimum_coverage_by_group_check.rb +14 -25
  48. data/lib/simplecov/exit_codes/minimum_overall_coverage_check.rb +7 -20
  49. data/lib/simplecov/exit_codes.rb +17 -0
  50. data/lib/simplecov/exit_handling.rb +13 -43
  51. data/lib/simplecov/file_list.rb +14 -12
  52. data/lib/simplecov/filter.rb +43 -9
  53. data/lib/simplecov/formatter/base.rb +18 -1
  54. data/lib/simplecov/formatter/coverage_json_writer.rb +97 -0
  55. data/lib/simplecov/formatter/html_formatter/public/index.html +36 -5
  56. data/lib/simplecov/formatter/html_formatter/viewer_data_validator.rb +96 -0
  57. data/lib/simplecov/formatter/html_formatter.rb +71 -45
  58. data/lib/simplecov/formatter/json_formatter/errors_formatter.rb +54 -50
  59. data/lib/simplecov/formatter/json_formatter/result_hash_formatter.rb +78 -88
  60. data/lib/simplecov/formatter/json_formatter/source_file_formatter.rb +70 -76
  61. data/lib/simplecov/formatter/json_formatter.rb +5 -46
  62. data/lib/simplecov/formatter/multi_formatter.rb +8 -3
  63. data/lib/simplecov/formatter/simple_formatter.rb +8 -3
  64. data/lib/simplecov/formatter.rb +12 -0
  65. data/lib/simplecov/group_names.rb +32 -0
  66. data/lib/simplecov/last_run.rb +16 -9
  67. data/lib/simplecov/lines_classifier.rb +29 -8
  68. data/lib/simplecov/load_global_config.rb +5 -2
  69. data/lib/simplecov/parallel_adapters/base.rb +17 -0
  70. data/lib/simplecov/parallel_adapters/generic.rb +9 -8
  71. data/lib/simplecov/parallel_adapters/parallel_tests.rb +2 -2
  72. data/lib/simplecov/parallel_adapters.rb +4 -2
  73. data/lib/simplecov/parallel_coordination.rb +6 -1
  74. data/lib/simplecov/parallel_result_merger.rb +230 -0
  75. data/lib/simplecov/process.rb +22 -0
  76. data/lib/simplecov/report_deferral.rb +49 -0
  77. data/lib/simplecov/report_stamp.rb +28 -0
  78. data/lib/simplecov/result.rb +40 -10
  79. data/lib/simplecov/result_adapter.rb +79 -10
  80. data/lib/simplecov/result_merger/resultset_file.rb +43 -7
  81. data/lib/simplecov/result_merger/resultset_run_identity.rb +67 -0
  82. data/lib/simplecov/result_merger/resultset_store.rb +15 -12
  83. data/lib/simplecov/result_merger/unloaded_files.rb +103 -0
  84. data/lib/simplecov/result_merger.rb +69 -42
  85. data/lib/simplecov/result_processing.rb +85 -46
  86. data/lib/simplecov/run_identity.rb +77 -0
  87. data/lib/simplecov/simulate_coverage.rb +37 -12
  88. data/lib/simplecov/source_file/line.rb +2 -2
  89. data/lib/simplecov/source_file/method.rb +8 -2
  90. data/lib/simplecov/source_file/method_builder.rb +3 -1
  91. data/lib/simplecov/source_file/ruby_data_parser.rb +25 -3
  92. data/lib/simplecov/source_file/skip_chunks.rb +7 -10
  93. data/lib/simplecov/source_file/source_loader.rb +25 -9
  94. data/lib/simplecov/source_file/statistics.rb +24 -16
  95. data/lib/simplecov/source_file.rb +2 -2
  96. data/lib/simplecov/static_coverage_extractor/condition_folding.rb +239 -0
  97. data/lib/simplecov/static_coverage_extractor/location_conventions.rb +236 -0
  98. data/lib/simplecov/static_coverage_extractor/method_collector.rb +7 -0
  99. data/lib/simplecov/static_coverage_extractor/prism_compat.rb +55 -0
  100. data/lib/simplecov/static_coverage_extractor/value_position.rb +74 -0
  101. data/lib/simplecov/static_coverage_extractor/visitor.rb +74 -62
  102. data/lib/simplecov/unloaded_file_injector.rb +75 -0
  103. data/lib/simplecov/version.rb +1 -1
  104. data/lib/simplecov.rb +37 -17
  105. data/sig/simplecov.rbs +1777 -0
  106. metadata +32 -16
  107. data/doc/alternate-formatters.md +0 -66
  108. data/doc/commercial-services.md +0 -25
  109. data/doc/editor-integration.md +0 -18
  110. data/lib/simplecov/combine/files_combiner.rb +0 -30
  111. data/lib/simplecov/formatter/html_formatter/public/application.css +0 -1
  112. data/lib/simplecov/formatter/html_formatter/public/application.js +0 -18
  113. data/lib/simplecov/formatter/html_formatter/public/favicon_green.png +0 -0
  114. data/lib/simplecov/formatter/html_formatter/public/favicon_red.png +0 -0
  115. data/lib/simplecov/formatter/html_formatter/public/favicon_yellow.png +0 -0
  116. data/schemas/coverage-v1.0.schema.json +0 -306
  117. 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 — `Coverage` uses sequential ids too, so this matches the
26
- # conventional shape. Only defined when Prism is loadable;
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 = arm_location(node.statements, node.location)
96
- else_loc = if_like_else_location(node)
97
- @branches[build_tuple(type, node.location)] = {
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.location
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
- # Resolve the source range Coverage attributes to a real-or-synthetic
112
- # `:else` arm of an if-like construct. IfNode uses
113
- # `subsequent` / `consequent` depending on Prism version (resolved
114
- # to `IF_NODE_SUBSEQUENT_METHOD` at load time); UnlessNode uses
115
- # `else_clause`. When neither is present, the synthesized else
116
- # inherits the whole condition's range (matches Coverage's
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
- loc = arm_location(when_node.statements, when_node.location)
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
- body_loc = arm_location(node.statements, node.location)
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SimpleCov
4
- VERSION = "1.0.0"
4
+ VERSION = "1.1.1"
5
5
  end
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
- class << self
12
- CRITERION_TO_RUBY_COVERAGE = {
13
- branch: :branches,
14
- line: :lines,
15
- method: :methods,
16
- oneshot_line: :oneshot_lines
17
- }.freeze
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
- previous = @autoloading_dot_simplecov
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
- defer_to_minitest_after_run if minitest_autorun_pending?
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
- # Trigger adapter selection now so the (possibly lazy) parallel_tests
151
- # gem load happens at start_tracking time rather than mid-suite.
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
- return if org.jruby.RubyInstanceConfig.FULL_TRACE_ENABLED
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 only set by aruba feature tests
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"]