simplecov 1.0.2 → 1.1.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.
Files changed (110) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +65 -1425
  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 +21 -28
  7. data/lib/simplecov/cli/coverage_file.rb +65 -0
  8. data/lib/simplecov/cli/diff.rb +43 -48
  9. data/lib/simplecov/cli/dotfile.rb +12 -6
  10. data/lib/simplecov/cli/merge.rb +12 -10
  11. data/lib/simplecov/cli/open.rb +3 -5
  12. data/lib/simplecov/cli/report.rb +31 -23
  13. data/lib/simplecov/cli/serve/report_preparer.rb +29 -0
  14. data/lib/simplecov/cli/serve/static_file_handler.rb +122 -0
  15. data/lib/simplecov/cli/serve.rb +30 -94
  16. data/lib/simplecov/cli/uncovered.rb +20 -25
  17. data/lib/simplecov/cli.rb +15 -2
  18. data/lib/simplecov/combine/branches_combiner.rb +43 -19
  19. data/lib/simplecov/combine/coverage_accumulator.rb +268 -0
  20. data/lib/simplecov/combine/identity_interner.rb +30 -0
  21. data/lib/simplecov/combine/interned_counts.rb +30 -0
  22. data/lib/simplecov/combine/lines_combiner.rb +48 -20
  23. data/lib/simplecov/combine/methods_combiner.rb +46 -21
  24. data/lib/simplecov/combine/results_combiner.rb +12 -37
  25. data/lib/simplecov/combine.rb +5 -23
  26. data/lib/simplecov/command_guesser.rb +66 -9
  27. data/lib/simplecov/configuration/coverage.rb +12 -15
  28. data/lib/simplecov/configuration/coverage_criteria.rb +34 -38
  29. data/lib/simplecov/configuration/eval_coverage.rb +41 -0
  30. data/lib/simplecov/configuration/filters.rb +11 -44
  31. data/lib/simplecov/configuration/formatting.rb +18 -9
  32. data/lib/simplecov/configuration/groups.rb +42 -0
  33. data/lib/simplecov/configuration/merging.rb +8 -7
  34. data/lib/simplecov/configuration/thresholds.rb +14 -13
  35. data/lib/simplecov/configuration.rb +13 -43
  36. data/lib/simplecov/coverage_json.rb +24 -0
  37. data/lib/simplecov/coverage_statistics.rb +1 -1
  38. data/lib/simplecov/coverage_violations.rb +15 -5
  39. data/lib/simplecov/defaults.rb +7 -3
  40. data/lib/simplecov/directive.rb +1 -1
  41. data/lib/simplecov/exit_codes/check.rb +33 -0
  42. data/lib/simplecov/exit_codes/maximum_coverage_drop_check.rb +11 -20
  43. data/lib/simplecov/exit_codes/maximum_overall_coverage_check.rb +3 -16
  44. data/lib/simplecov/exit_codes/minimum_coverage_by_file_check.rb +14 -23
  45. data/lib/simplecov/exit_codes/minimum_coverage_by_group_check.rb +14 -25
  46. data/lib/simplecov/exit_codes/minimum_overall_coverage_check.rb +4 -17
  47. data/lib/simplecov/exit_codes.rb +1 -0
  48. data/lib/simplecov/exit_handling.rb +11 -41
  49. data/lib/simplecov/file_list.rb +5 -10
  50. data/lib/simplecov/filter.rb +43 -9
  51. data/lib/simplecov/formatter/base.rb +11 -0
  52. data/lib/simplecov/formatter/coverage_json_writer.rb +97 -0
  53. data/lib/simplecov/formatter/html_formatter/public/index.html +35 -5
  54. data/lib/simplecov/formatter/html_formatter/viewer_data_validator.rb +96 -0
  55. data/lib/simplecov/formatter/html_formatter.rb +67 -47
  56. data/lib/simplecov/formatter/json_formatter/errors_formatter.rb +54 -56
  57. data/lib/simplecov/formatter/json_formatter/result_hash_formatter.rb +78 -88
  58. data/lib/simplecov/formatter/json_formatter/source_file_formatter.rb +70 -76
  59. data/lib/simplecov/formatter/json_formatter.rb +5 -48
  60. data/lib/simplecov/formatter/multi_formatter.rb +1 -1
  61. data/lib/simplecov/formatter/simple_formatter.rb +8 -5
  62. data/lib/simplecov/formatter.rb +12 -0
  63. data/lib/simplecov/group_names.rb +32 -0
  64. data/lib/simplecov/last_run.rb +16 -9
  65. data/lib/simplecov/lines_classifier.rb +29 -8
  66. data/lib/simplecov/load_global_config.rb +5 -2
  67. data/lib/simplecov/parallel_adapters/base.rb +17 -0
  68. data/lib/simplecov/parallel_adapters/generic.rb +2 -2
  69. data/lib/simplecov/parallel_adapters/parallel_tests.rb +2 -2
  70. data/lib/simplecov/parallel_coordination.rb +6 -1
  71. data/lib/simplecov/parallel_result_merger.rb +230 -0
  72. data/lib/simplecov/report_deferral.rb +49 -0
  73. data/lib/simplecov/report_stamp.rb +28 -0
  74. data/lib/simplecov/result.rb +40 -10
  75. data/lib/simplecov/result_adapter.rb +48 -16
  76. data/lib/simplecov/result_merger/resultset_file.rb +43 -7
  77. data/lib/simplecov/result_merger/resultset_run_identity.rb +67 -0
  78. data/lib/simplecov/result_merger/resultset_store.rb +15 -12
  79. data/lib/simplecov/result_merger/unloaded_files.rb +103 -0
  80. data/lib/simplecov/result_merger.rb +69 -42
  81. data/lib/simplecov/result_processing.rb +82 -43
  82. data/lib/simplecov/run_identity.rb +77 -0
  83. data/lib/simplecov/simulate_coverage.rb +35 -11
  84. data/lib/simplecov/source_file/method.rb +7 -1
  85. data/lib/simplecov/source_file/ruby_data_parser.rb +25 -3
  86. data/lib/simplecov/source_file/skip_chunks.rb +7 -10
  87. data/lib/simplecov/source_file/source_loader.rb +23 -7
  88. data/lib/simplecov/source_file/statistics.rb +24 -16
  89. data/lib/simplecov/static_coverage_extractor/condition_folding.rb +203 -13
  90. data/lib/simplecov/static_coverage_extractor/location_conventions.rb +19 -30
  91. data/lib/simplecov/static_coverage_extractor/method_collector.rb +7 -0
  92. data/lib/simplecov/static_coverage_extractor/prism_compat.rb +55 -0
  93. data/lib/simplecov/static_coverage_extractor/value_position.rb +6 -14
  94. data/lib/simplecov/static_coverage_extractor/visitor.rb +23 -36
  95. data/lib/simplecov/unloaded_file_injector.rb +75 -0
  96. data/lib/simplecov/version.rb +1 -1
  97. data/lib/simplecov.rb +11 -5
  98. data/schemas/coverage-v1.0.schema.json +2 -2
  99. data/schemas/coverage.schema.json +2 -2
  100. data/sig/simplecov.rbs +206 -67
  101. metadata +28 -14
  102. data/doc/alternate-formatters.md +0 -66
  103. data/doc/commercial-services.md +0 -25
  104. data/doc/editor-integration.md +0 -18
  105. data/lib/simplecov/combine/files_combiner.rb +0 -70
  106. data/lib/simplecov/formatter/html_formatter/public/application.css +0 -1
  107. data/lib/simplecov/formatter/html_formatter/public/application.js +0 -18
  108. data/lib/simplecov/formatter/html_formatter/public/favicon_green.png +0 -0
  109. data/lib/simplecov/formatter/html_formatter/public/favicon_red.png +0 -0
  110. data/lib/simplecov/formatter/html_formatter/public/favicon_yellow.png +0 -0
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "prism_compat"
3
4
  require_relative "condition_folding"
4
5
  require_relative "location_conventions"
5
6
  require_relative "method_collector"
@@ -7,42 +8,13 @@ require_relative "value_position"
7
8
 
8
9
  module SimpleCov
9
10
  module StaticCoverageExtractor
10
- # `Prism::IfNode#subsequent` was renamed from `consequent` in Prism
11
- # 1.3 (Dec 2024). Ruby 3.3's stdlib still ships an older Prism that
12
- # only exposes `consequent`; 3.4+ and any project that's done
13
- # `gem install prism` exposes `subsequent`. Resolve the method name
14
- # ONCE here so the per-node hot path stays branch-free. The
15
- # not-taken arm on whichever Prism version we're on can't be
16
- # exercised by our own dogfood (we only run on one Prism at a time).
17
- # simplecov:disable
18
- IF_NODE_SUBSEQUENT_METHOD =
19
- if ::Prism::IfNode.method_defined?(:subsequent)
20
- :subsequent
21
- else
22
- :consequent
23
- end
24
-
25
- # The same Prism 1.3 rename hit the `else` accessor on `UnlessNode`,
26
- # `CaseNode`, and `CaseMatchNode` (all three: `consequent` ->
27
- # `else_clause`). Ruby 3.3's stdlib Prism (0.19) only exposes
28
- # `consequent`, so reaching for `else_clause` there raised
29
- # NoMethodError inside the extractor — `call` swallowed it and the
30
- # whole file silently fell back to no simulated data for any
31
- # `unless`/`else` or empty-arm `case`. Resolve the name once, like
32
- # IF_NODE_SUBSEQUENT_METHOD. All three nodes renamed together, so one
33
- # constant (probed off CaseNode) covers them.
34
- ELSE_CLAUSE_METHOD =
35
- if ::Prism::CaseNode.method_defined?(:else_clause)
36
- :else_clause
37
- else
38
- :consequent
39
- end
40
- # simplecov:enable
41
-
42
11
  # Prism visitor that accumulates branch and method tuples in the
43
12
  # shape Ruby's `Coverage` reports. Tuple ids are sequential across
44
- # the file `Coverage` uses sequential ids too, so this matches the
45
- # 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;
46
18
  # `StaticCoverageExtractor.available?` is the runtime gate.
47
19
  class Visitor < ::Prism::Visitor
48
20
  # Method tuples and the class/module nesting that names them are
@@ -64,6 +36,7 @@ module SimpleCov
64
36
  @next_id = 0
65
37
  @class_stack = []
66
38
  @value_positions = nil
39
+ @suppress_methods = false
67
40
  end
68
41
 
69
42
  # Entry point for a parsed file. On legacy Rubies the location of an
@@ -84,13 +57,27 @@ module SimpleCov
84
57
  # `else`, another IfNode for `elsif`). When the subsequent is
85
58
  # missing, Coverage synthesizes a `:else` arm attributed to the
86
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).
87
68
  def visit_if_node(node)
88
- emit_if_like(node, :if) unless static_condition?(node.predicate)
69
+ verdict = folded_condition(node.predicate)
70
+ return visit_folded_arms(verdict, node.statements, PrismCompat.subsequent(node)) if verdict
71
+
72
+ emit_if_like(node, :if)
89
73
  super
90
74
  end
91
75
 
92
76
  def visit_unless_node(node)
93
- emit_if_like(node, :unless) unless static_condition?(node.predicate)
77
+ verdict = folded_condition(node.predicate)
78
+ return visit_folded_arms(verdict, PrismCompat.else_clause(node), node.statements) if verdict
79
+
80
+ emit_if_like(node, :unless)
94
81
  super
95
82
  end
96
83
 
@@ -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.2"
4
+ VERSION = "1.1.0"
5
5
  end
data/lib/simplecov.rb CHANGED
@@ -158,10 +158,8 @@ module SimpleCov
158
158
  ::Process.respond_to?(:_fork)
159
159
  # simplecov:enable
160
160
 
161
- # Trigger adapter selection now so the (possibly lazy) parallel_tests
162
- # gem load happens at start_tracking time rather than mid-suite.
163
- # `current` is memoized; subsequent calls are cheap.
164
- SimpleCov::ParallelAdapters.current
161
+ # Select the parallel adapter and generate identities before any forks.
162
+ SimpleCov::RunIdentity.prepare
165
163
 
166
164
  @result = nil
167
165
  self.pid = Process.pid
@@ -230,6 +228,7 @@ end
230
228
  require "forwardable"
231
229
  require_relative "simplecov/color"
232
230
  require_relative "simplecov/deprecation"
231
+ require_relative "simplecov/group_names"
233
232
  require_relative "simplecov/configuration"
234
233
  SimpleCov.extend SimpleCov::Configuration
235
234
  require_relative "simplecov/coverage_statistics"
@@ -245,22 +244,29 @@ require_relative "simplecov/result"
245
244
  require_relative "simplecov/filter"
246
245
  require_relative "simplecov/formatter"
247
246
  require_relative "simplecov/last_run"
247
+ require_relative "simplecov/report_stamp"
248
248
  require_relative "simplecov/lines_classifier"
249
249
  require_relative "simplecov/result_merger"
250
+ require_relative "simplecov/parallel_result_merger"
250
251
  require_relative "simplecov/parallel_adapters"
252
+ require_relative "simplecov/run_identity"
253
+ SimpleCov.extend SimpleCov::RunIdentity::Accessors
251
254
  require_relative "simplecov/command_guesser"
252
255
  require_relative "simplecov/version"
253
256
  require_relative "simplecov/result_adapter"
254
257
  require_relative "simplecov/combine"
258
+ require_relative "simplecov/combine/identity_interner"
255
259
  require_relative "simplecov/combine/branches_combiner"
256
260
  require_relative "simplecov/combine/methods_combiner"
257
- require_relative "simplecov/combine/files_combiner"
258
261
  require_relative "simplecov/combine/lines_combiner"
262
+ require_relative "simplecov/combine/coverage_accumulator"
259
263
  require_relative "simplecov/combine/results_combiner"
260
264
  require_relative "simplecov/useless_results_remover"
261
265
  require_relative "simplecov/simulate_coverage"
266
+ require_relative "simplecov/unloaded_file_injector"
262
267
  require_relative "simplecov/result_processing"
263
268
  require_relative "simplecov/exit_handling"
269
+ require_relative "simplecov/report_deferral"
264
270
  require_relative "simplecov/parallel_coordination"
265
271
 
266
272
  # Load default config
@@ -254,7 +254,7 @@
254
254
  },
255
255
  "total": {"type": "integer", "minimum": 0, "description": "Executable lines: `covered + missed`. Does not include `omitted`."},
256
256
  "percent": {"type": "number"},
257
- "strength": {"type": "number", "description": "Average number of executions across covered lines (hits per covered line)."}
257
+ "strength": {"type": "number", "description": "Average executions per coverable line: the sum of hit counts divided by `covered + missed`."}
258
258
  }
259
259
  },
260
260
  "coverage_statistic": {
@@ -266,7 +266,7 @@
266
266
  "missed": {"type": "integer", "minimum": 0},
267
267
  "total": {"type": "integer", "minimum": 0, "description": "Total branches or methods: `covered + missed`."},
268
268
  "percent": {"type": "number"},
269
- "strength": {"type": "number", "description": "Average number of executions across covered branches or methods (hits per covered item)."}
269
+ "strength": {"type": "number", "description": "Average executions per coverable branch or method: the sum of hit counts divided by `covered + missed`."}
270
270
  }
271
271
  },
272
272
  "line_coverage": {
@@ -254,7 +254,7 @@
254
254
  },
255
255
  "total": {"type": "integer", "minimum": 0, "description": "Executable lines: `covered + missed`. Does not include `omitted`."},
256
256
  "percent": {"type": "number"},
257
- "strength": {"type": "number", "description": "Average number of executions across covered lines (hits per covered line)."}
257
+ "strength": {"type": "number", "description": "Average executions per coverable line: the sum of hit counts divided by `covered + missed`."}
258
258
  }
259
259
  },
260
260
  "coverage_statistic": {
@@ -266,7 +266,7 @@
266
266
  "missed": {"type": "integer", "minimum": 0},
267
267
  "total": {"type": "integer", "minimum": 0, "description": "Total branches or methods: `covered + missed`."},
268
268
  "percent": {"type": "number"},
269
- "strength": {"type": "number", "description": "Average number of executions across covered branches or methods (hits per covered item)."}
269
+ "strength": {"type": "number", "description": "Average executions per coverable branch or method: the sum of hit counts divided by `covered + missed`."}
270
270
  }
271
271
  },
272
272
  "line_coverage": {