simplecov 1.1.0 → 1.2.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 (196) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +165 -57
  3. data/exe/simplecov +0 -3
  4. data/lib/minitest/simplecov_plugin.rb +12 -9
  5. data/lib/simplecov/atomic_file.rb +16 -19
  6. data/lib/simplecov/autostart.rb +5 -7
  7. data/lib/simplecov/baseline/parser.rb +58 -0
  8. data/lib/simplecov/baseline.rb +170 -0
  9. data/lib/simplecov/cli/affected/changed_files.rb +57 -0
  10. data/lib/simplecov/cli/affected/selection.rb +117 -0
  11. data/lib/simplecov/cli/affected.rb +148 -0
  12. data/lib/simplecov/cli/badge/svg.rb +70 -0
  13. data/lib/simplecov/cli/badge.rb +62 -0
  14. data/lib/simplecov/cli/clean.rb +23 -24
  15. data/lib/simplecov/cli/command_helpers.rb +55 -19
  16. data/lib/simplecov/cli/completions/scripts.rb +114 -0
  17. data/lib/simplecov/cli/completions.rb +83 -0
  18. data/lib/simplecov/cli/coverage.rb +25 -28
  19. data/lib/simplecov/cli/coverage_file.rb +58 -14
  20. data/lib/simplecov/cli/dead_code/output.rb +100 -0
  21. data/lib/simplecov/cli/dead_code.rb +139 -0
  22. data/lib/simplecov/cli/diff/output.rb +45 -0
  23. data/lib/simplecov/cli/diff.rb +35 -67
  24. data/lib/simplecov/cli/dotfile.rb +83 -38
  25. data/lib/simplecov/cli/git.rb +41 -0
  26. data/lib/simplecov/cli/history/output.rb +135 -0
  27. data/lib/simplecov/cli/history.rb +67 -0
  28. data/lib/simplecov/cli/merge.rb +34 -39
  29. data/lib/simplecov/cli/open.rb +12 -15
  30. data/lib/simplecov/cli/patch/changed_lines.rb +149 -0
  31. data/lib/simplecov/cli/patch/output.rb +135 -0
  32. data/lib/simplecov/cli/patch.rb +169 -0
  33. data/lib/simplecov/cli/ratchet/output.rb +44 -0
  34. data/lib/simplecov/cli/ratchet.rb +83 -0
  35. data/lib/simplecov/cli/report.rb +18 -24
  36. data/lib/simplecov/cli/run.rb +8 -9
  37. data/lib/simplecov/cli/serve/report_preparer.rb +7 -6
  38. data/lib/simplecov/cli/serve/static_file_handler.rb +62 -56
  39. data/lib/simplecov/cli/serve.rb +27 -24
  40. data/lib/simplecov/cli/show/annotator.rb +87 -0
  41. data/lib/simplecov/cli/show/sweep.rb +36 -0
  42. data/lib/simplecov/cli/show.rb +133 -0
  43. data/lib/simplecov/cli/status/facts.rb +87 -0
  44. data/lib/simplecov/cli/status.rb +93 -0
  45. data/lib/simplecov/cli/tests/redundancy.rb +78 -0
  46. data/lib/simplecov/cli/tests.rb +148 -0
  47. data/lib/simplecov/cli/uncovered/misses.rb +37 -0
  48. data/lib/simplecov/cli/uncovered.rb +70 -32
  49. data/lib/simplecov/cli/usage.rb +152 -0
  50. data/lib/simplecov/cli/watch/live_report.rb +73 -0
  51. data/lib/simplecov/cli/watch/narrator.rb +55 -0
  52. data/lib/simplecov/cli/watch/poller.rb +45 -0
  53. data/lib/simplecov/cli/watch/session.rb +134 -0
  54. data/lib/simplecov/cli/watch/test_plan.rb +43 -0
  55. data/lib/simplecov/cli/watch.rb +85 -0
  56. data/lib/simplecov/cli.rb +43 -92
  57. data/lib/simplecov/color.rb +21 -35
  58. data/lib/simplecov/combine/branches_combiner.rb +7 -31
  59. data/lib/simplecov/combine/coverage_accumulator.rb +49 -142
  60. data/lib/simplecov/combine/identity_interner.rb +10 -13
  61. data/lib/simplecov/combine/interned_counts.rb +7 -9
  62. data/lib/simplecov/combine/lines_combiner.rb +20 -44
  63. data/lib/simplecov/combine/methods_combiner.rb +9 -35
  64. data/lib/simplecov/combine/results_combiner.rb +4 -21
  65. data/lib/simplecov/combine.rb +0 -6
  66. data/lib/simplecov/command_guesser.rb +20 -34
  67. data/lib/simplecov/configuration/baseline.rb +31 -0
  68. data/lib/simplecov/configuration/coverage.rb +91 -79
  69. data/lib/simplecov/configuration/coverage_criteria.rb +43 -54
  70. data/lib/simplecov/configuration/deprecations.rb +21 -0
  71. data/lib/simplecov/configuration/eval_coverage.rb +6 -10
  72. data/lib/simplecov/configuration/filters.rb +40 -73
  73. data/lib/simplecov/configuration/formatting.rb +74 -83
  74. data/lib/simplecov/configuration/groups.rb +19 -13
  75. data/lib/simplecov/configuration/history.rb +44 -0
  76. data/lib/simplecov/configuration/ignored_entries.rb +31 -22
  77. data/lib/simplecov/configuration/merging.rb +94 -62
  78. data/lib/simplecov/configuration/missed_caps.rb +51 -0
  79. data/lib/simplecov/configuration/production.rb +23 -0
  80. data/lib/simplecov/configuration/test_tracking.rb +48 -0
  81. data/lib/simplecov/configuration/thresholds.rb +33 -62
  82. data/lib/simplecov/configuration/view_coverage.rb +38 -0
  83. data/lib/simplecov/configuration.rb +70 -76
  84. data/lib/simplecov/configuration_error.rb +5 -0
  85. data/lib/simplecov/context_map/union.rb +81 -0
  86. data/lib/simplecov/context_map.rb +187 -0
  87. data/lib/simplecov/coverage_json.rb +2 -5
  88. data/lib/simplecov/coverage_statistics.rb +14 -24
  89. data/lib/simplecov/coverage_violations.rb +157 -64
  90. data/lib/simplecov/current_run.rb +61 -0
  91. data/lib/simplecov/defaults.rb +4 -23
  92. data/lib/simplecov/deprecation.rb +45 -27
  93. data/lib/simplecov/directive.rb +46 -62
  94. data/lib/simplecov/exit_codes/baseline_check.rb +46 -0
  95. data/lib/simplecov/exit_codes/check.rb +17 -11
  96. data/lib/simplecov/exit_codes/exit_code_handling.rb +21 -8
  97. data/lib/simplecov/exit_codes/maximum_coverage_drop_check.rb +7 -10
  98. data/lib/simplecov/exit_codes/maximum_missed_check.rb +26 -0
  99. data/lib/simplecov/exit_codes/maximum_missed_per_file_check.rb +34 -0
  100. data/lib/simplecov/exit_codes/maximum_overall_coverage_check.rb +7 -12
  101. data/lib/simplecov/exit_codes/minimum_coverage_by_file_check.rb +11 -10
  102. data/lib/simplecov/exit_codes/minimum_coverage_by_group_check.rb +7 -9
  103. data/lib/simplecov/exit_codes/minimum_overall_coverage_check.rb +16 -17
  104. data/lib/simplecov/exit_codes.rb +17 -7
  105. data/lib/simplecov/exit_handling.rb +94 -59
  106. data/lib/simplecov/file_list.rb +20 -51
  107. data/lib/simplecov/filter.rb +43 -66
  108. data/lib/simplecov/formatter/base.rb +38 -51
  109. data/lib/simplecov/formatter/baseline_formatter.rb +98 -0
  110. data/lib/simplecov/formatter/coverage_json_writer.rb +29 -31
  111. data/lib/simplecov/formatter/html_formatter/public/index.html +17 -10
  112. data/lib/simplecov/formatter/html_formatter/viewer_data_validator.rb +74 -17
  113. data/lib/simplecov/formatter/html_formatter.rb +24 -41
  114. data/lib/simplecov/formatter/json_formatter/errors_formatter.rb +50 -16
  115. data/lib/simplecov/formatter/json_formatter/production_section_formatter.rb +48 -0
  116. data/lib/simplecov/formatter/json_formatter/result_hash_formatter.rb +43 -29
  117. data/lib/simplecov/formatter/json_formatter/source_file_formatter.rb +14 -10
  118. data/lib/simplecov/formatter/json_formatter.rb +7 -13
  119. data/lib/simplecov/formatter/multi_formatter.rb +5 -11
  120. data/lib/simplecov/formatter/simple_formatter.rb +2 -8
  121. data/lib/simplecov/formatter.rb +14 -10
  122. data/lib/simplecov/group_names.rb +13 -12
  123. data/lib/simplecov/history.rb +115 -0
  124. data/lib/simplecov/last_run.rb +6 -9
  125. data/lib/simplecov/lines_classifier.rb +31 -16
  126. data/lib/simplecov/load_global_config.rb +7 -7
  127. data/lib/simplecov/parallel_adapters/base.rb +13 -41
  128. data/lib/simplecov/parallel_adapters/generic.rb +7 -18
  129. data/lib/simplecov/parallel_adapters/parallel_tests.rb +21 -42
  130. data/lib/simplecov/parallel_adapters.rb +24 -53
  131. data/lib/simplecov/parallel_coordination.rb +39 -49
  132. data/lib/simplecov/parallel_result_merger/worker_payload.rb +33 -0
  133. data/lib/simplecov/parallel_result_merger.rb +67 -126
  134. data/lib/simplecov/process.rb +14 -32
  135. data/lib/simplecov/production/error.rb +11 -0
  136. data/lib/simplecov/production/file_sink.rb +130 -0
  137. data/lib/simplecov/production.rb +249 -0
  138. data/lib/simplecov/profiles/rails.rb +1 -12
  139. data/lib/simplecov/profiles/root_filter.rb +0 -4
  140. data/lib/simplecov/profiles/strict.rb +9 -19
  141. data/lib/simplecov/profiles.rb +11 -33
  142. data/lib/simplecov/report_deferral.rb +11 -15
  143. data/lib/simplecov/report_stamp.rb +6 -9
  144. data/lib/simplecov/result/filter_config.rb +19 -0
  145. data/lib/simplecov/result/missing_source_files_reporter.rb +8 -9
  146. data/lib/simplecov/result/serialization.rb +43 -0
  147. data/lib/simplecov/result/source_file_builder.rb +7 -12
  148. data/lib/simplecov/result.rb +68 -117
  149. data/lib/simplecov/result_adapter.rb +58 -68
  150. data/lib/simplecov/result_merger/contexts.rb +22 -0
  151. data/lib/simplecov/result_merger/legacy_format_adapter.rb +4 -8
  152. data/lib/simplecov/result_merger/resultset_file.rb +20 -27
  153. data/lib/simplecov/result_merger/resultset_run_identity.rb +23 -25
  154. data/lib/simplecov/result_merger/resultset_store.rb +23 -14
  155. data/lib/simplecov/result_merger/unloaded_files.rb +24 -32
  156. data/lib/simplecov/result_merger.rb +59 -58
  157. data/lib/simplecov/result_processing.rb +74 -119
  158. data/lib/simplecov/run_identity.rb +16 -18
  159. data/lib/simplecov/simulate_coverage.rb +35 -50
  160. data/lib/simplecov/source_file/branch.rb +8 -33
  161. data/lib/simplecov/source_file/branch_builder.rb +28 -47
  162. data/lib/simplecov/source_file/builder_context.rb +6 -13
  163. data/lib/simplecov/source_file/line.rb +13 -33
  164. data/lib/simplecov/source_file/line_builder.rb +9 -15
  165. data/lib/simplecov/source_file/method.rb +3 -7
  166. data/lib/simplecov/source_file/method_builder.rb +6 -15
  167. data/lib/simplecov/source_file/ruby_data_parser.rb +59 -66
  168. data/lib/simplecov/source_file/skip_chunks.rb +19 -28
  169. data/lib/simplecov/source_file/source_loader.rb +45 -35
  170. data/lib/simplecov/source_file/statistics.rb +10 -9
  171. data/lib/simplecov/source_file.rb +21 -51
  172. data/lib/simplecov/static_coverage_extractor/condition_folding.rb +63 -114
  173. data/lib/simplecov/static_coverage_extractor/location_conventions.rb +76 -121
  174. data/lib/simplecov/static_coverage_extractor/method_collector.rb +11 -24
  175. data/lib/simplecov/static_coverage_extractor/prism_compat.rb +15 -24
  176. data/lib/simplecov/static_coverage_extractor/value_position.rb +25 -38
  177. data/lib/simplecov/static_coverage_extractor/visitor.rb +47 -64
  178. data/lib/simplecov/static_coverage_extractor.rb +46 -62
  179. data/lib/simplecov/test_tracker/accessors.rb +34 -0
  180. data/lib/simplecov/test_tracker/constant_watch.rb +49 -0
  181. data/lib/simplecov/test_tracker/delta.rb +69 -0
  182. data/lib/simplecov/test_tracker/framework_hooks.rb +130 -0
  183. data/lib/simplecov/test_tracker.rb +162 -0
  184. data/lib/simplecov/unloaded_file_injector.rb +18 -31
  185. data/lib/simplecov/useless_results_remover.rb +7 -6
  186. data/lib/simplecov/version.rb +1 -1
  187. data/lib/simplecov/view_coverage/template_compiler.rb +83 -0
  188. data/lib/simplecov/view_coverage.rb +55 -0
  189. data/lib/simplecov-html.rb +1 -0
  190. data/lib/simplecov.rb +59 -114
  191. data/lib/simplecov_json_formatter.rb +1 -0
  192. data/man/simplecov.1 +349 -0
  193. data/sig/simplecov.rbs +523 -595
  194. metadata +72 -7
  195. data/schemas/coverage-v1.0.schema.json +0 -306
  196. data/schemas/coverage.schema.json +0 -306
@@ -1,158 +1,111 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Result-building façade: turns the raw `Coverage.result` hash into a
4
- # `SimpleCov::Result`, applies filters and groups, drives merging
5
- # across test suites via `SimpleCov::ResultMerger`, and exposes the
6
- # `collate` entry point for stitching disparate resultsets together.
7
3
  module SimpleCov
8
4
  class << self
5
+ # By default `collate` ignores the merge_timeout so all results in all
6
+ # files specified will be merged. Pass `ignore_timeout: false` to honor it.
9
7
  #
10
- # Collate a series of SimpleCov result files into a single SimpleCov output.
11
- #
12
- # See README for usage. By default `collate` ignores the merge_timeout
13
- # so all results in all files specified will be merged. Pass
14
- # `ignore_timeout: false` to honor it.
15
- #
16
- # `processes:` above 1 fans the merge out across that many forked worker
17
- # processes, for a collate big enough that reading and parsing the
18
- # resultsets dominates it. The report is identical either way, not merely
19
- # equivalent — the workers visit the resultsets in the order the
20
- # single-process merge visits them — and one process never forks at all.
21
- # The count is deliberately not clamped to the machine's core count nor
22
- # gated on some minimum number of resultsets: how many processes a collate
23
- # job can afford is the caller's call, not SimpleCov's. Anything below 1 is
24
- # taken as 1, so a count computed from arithmetic that can reach zero needs
25
- # no guarding. Defaults to `SIMPLECOV_CONCURRENCY`, or 1 when that is
26
- # unset. See `SimpleCov::ParallelResultMerger`.
27
- #
28
- def collate(result_filenames, profile = nil, processes: ENV.fetch("SIMPLECOV_CONCURRENCY", 1).to_i,
29
- ignore_timeout: true, &)
8
+ # `processes:` above 1 fans the merge out across that many forked workers.
9
+ # The report is identical either way, not merely equivalent. The count is
10
+ # deliberately not clamped to the machine's core count nor gated on a
11
+ # minimum number of resultsets: what a collate job can afford is the
12
+ # caller's call. Anything below 1 is taken as 1, so an unset
13
+ # `SIMPLECOV_CONCURRENCY` needs no default of its own.
14
+ def collate(result_filenames, profile = nil, processes: ENV["SIMPLECOV_CONCURRENCY"].to_i,
15
+ ignore_timeout: true, &)
30
16
  raise ArgumentError, "There are no reports to be merged" if result_filenames.empty?
31
17
 
32
18
  initial_setup(profile, &)
33
19
 
34
- # Use the ResultMerger to produce a single, merged result, ready to use.
35
- @result = ParallelResultMerger.merge_and_store(*result_filenames, processes: [1, processes].max,
36
- ignore_timeout: ignore_timeout)
20
+ current_run.result = ParallelResultMerger.merge_and_store(*result_filenames, processes: [1, processes].max,
21
+ ignore_timeout: ignore_timeout)
37
22
 
38
- @collating_result = true
23
+ current_run.collating_result = true
39
24
  run_exit_tasks!
40
25
  ensure
41
- @collating_result = false
26
+ current_run.collating_result = false
42
27
  end
43
28
 
44
- #
45
- # Returns the result for the current coverage run. With merging enabled,
46
- # every process stores its own slice, but only the finalization owner reads
47
- # and caches the merged result.
48
- #
49
29
  def result
50
- return @result if result?
30
+ return current_run.result if result?
51
31
 
52
- use_merging = merging
53
-
54
- collect_own_coverage(standalone: !use_merging)
55
-
56
- # If we're using merging of results, store the current result
57
- # first (if there is one), then merge the results and return those
58
- if use_merging
59
- SimpleCov::ResultMerger.store_result(@result) if result?
60
- return @result unless merge_finalization_owner?
32
+ merge = merging
33
+ collect_own_coverage(standalone: !merge)
34
+ merge_own_slice if merge
35
+ current_run.result
36
+ end
61
37
 
62
- wait_for_other_processes
63
- @result = SimpleCov::ResultMerger.merged_result
64
- end
38
+ def merge_own_slice
39
+ ResultMerger.store_result(result) if result?
40
+ return unless merge_finalization_owner?
65
41
 
66
- @result
42
+ wait_for_other_processes
43
+ current_run.result = ResultMerger.merged_result
67
44
  end
68
45
 
69
- # Build this process's slice of the coverage result. `standalone` is true
70
- # when no merge step follows, which makes this the final result: it is the
71
- # one that reports dropped source files, and the one that injects unloaded
72
- # files. When a merge does follow, both jobs belong to the merged result.
46
+ # `standalone` is true when no merge step follows, which makes this the
47
+ # final result: the one that reports dropped source files and injects
48
+ # unloaded files. When a merge does follow, both jobs belong to the merge.
73
49
  def collect_own_coverage(standalone:)
74
50
  return unless defined?(Coverage) && Coverage.running?
75
51
 
76
52
  process_coverage_result(report: standalone, inject_unloaded: standalone)
77
53
  end
78
54
 
79
- # Returns nil if the result has not been computed, otherwise the result.
80
- def result?
81
- defined?(@result) && @result
82
- end
83
-
84
- # @api private — true while `SimpleCov.collate` is running its finalizer.
85
- def collating_result?
86
- defined?(@collating_result) && @collating_result
87
- end
88
-
89
- # Applies the configured filters to the given array of SimpleCov::SourceFile items
90
55
  def filtered(files)
91
- result = files.to_a.dup
56
+ result = files.to_a
92
57
  filters.each do |filter|
93
58
  result = result.reject { |source_file| filter.matches?(source_file) }
94
59
  end
95
- SimpleCov::FileList.new result
60
+ FileList.new result
96
61
  end
97
62
 
98
- # Bin the given source files by group filter. `groups:` defaults to
99
- # `SimpleCov.groups`; pass a Hash explicitly to bin against a
100
- # different group config (e.g., the snapshot a Result captured at
101
- # construction). Files matched by no group fall into the implicit
102
- # "Ungrouped" bucket.
103
- def grouped(files, groups: SimpleCov.groups)
63
+ # Files matched by no group fall into the implicit "Ungrouped" bucket.
64
+ def grouped(files, groups: default_groups)
104
65
  return {} if GroupNames.validate!(groups.keys).empty?
105
66
 
106
67
  grouped = groups.transform_values do |filter|
107
- SimpleCov::FileList.new(files.select { |source_file| filter.matches?(source_file) })
68
+ FileList.new(files.select { |source_file| filter.matches?(source_file) })
108
69
  end
109
70
 
110
- in_group = grouped_file_set(grouped)
71
+ in_group = grouped_file_set(grouped)
111
72
  ungrouped = files.reject { |source_file| in_group.include?(source_file) }
112
- grouped[GroupNames::UNGROUPED] = SimpleCov::FileList.new(ungrouped) if ungrouped.any?
73
+ grouped[GroupNames::UNGROUPED] = FileList.new(ungrouped) if ungrouped.any?
113
74
 
114
75
  grouped
115
76
  end
116
77
 
117
- # Applies the profile of given name on SimpleCov configuration
118
78
  def load_profile(name)
119
79
  profiles.load(name)
120
80
  end
121
81
 
122
- # Clear out the previously cached .result. Primarily useful in testing.
123
82
  def clear_result
124
- @result = nil
83
+ current_run.result = nil
125
84
  end
126
85
 
127
- # @api private persist the per-criterion coverage percentages
128
- # rounded down (see #679) so the next run can compute drift.
86
+ # @api private -- floored (#679) so the next run can compute drift.
129
87
  def write_last_run(result)
130
- SimpleCov::LastRun.write(
88
+ LastRun.write(
131
89
  result: result.coverage_statistics.transform_values { |stats| round_coverage(stats.percent) }
132
90
  )
133
91
  end
134
92
 
135
- # @api private round down to two decimals to be extra strict.
93
+ # @api private -- floored, to be extra strict.
136
94
  def round_coverage(coverage)
137
95
  coverage.floor(2)
138
96
  end
139
97
 
140
- # Add simulated coverage for each of `candidate_paths` the result doesn't
141
- # already carry, and return it with the set of paths added.
142
- #
143
- # @api private — the seam `SimpleCov::ResultMerger` injects through. Public
98
+ # @api private -- the seam `SimpleCov::ResultMerger` injects through. Public
144
99
  # only because the merge runs in another object, on behalf of processes
145
100
  # whose configuration it may not share, so it supplies the paths itself.
146
101
  def inject_unloaded_files(result, candidate_paths, synthesize: nil, lines: nil)
147
102
  return [result, Set.new] if candidate_paths.empty?
148
103
 
149
104
  # Synthesizing branch and method tuples means parsing every tracked file
150
- # that wasn't loaded, which is about half the cost of simulating one.
151
- # Nothing reads those tuples when neither criterion is enabled
152
- # `Combine::CoverageAccumulator` only combines them per criterion, and the
153
- # statistics drop them the same way so skip the parse. The same goes
154
- # for line data, which a branch-only or method-only run neither reports
155
- # nor receives from `Coverage` for the files it loaded. See #1250.
105
+ # that wasn't loaded, about half the cost of simulating one, and nothing
106
+ # reads those tuples when neither criterion is enabled. The same goes for
107
+ # line data, which a branch-only or method-only run neither reports nor
108
+ # receives from `Coverage` for the files it loaded (#1250).
156
109
  UnloadedFileInjector.call(
157
110
  result, candidate_paths,
158
111
  synthesize: synthesize.nil? ? branch_coverage? || method_coverage? : synthesize,
@@ -160,7 +113,7 @@ module SimpleCov
160
113
  )
161
114
  end
162
115
 
163
- private
116
+ private
164
117
 
165
118
  def initial_setup(profile, &block)
166
119
  load_profile(profile) if profile
@@ -172,46 +125,48 @@ module SimpleCov
172
125
  end
173
126
 
174
127
  # Every path this process was told to track, whether or not it loaded them.
175
- # Recorded on the result (and from there into the resultset) so that a merge
176
- # in another process can inject the ones nobody loaded without needing this
177
- # process's `cover` / `track_files` configuration. A standalone `collate`
178
- # never ran `SimpleCov.start` and so has none of its own. See #1250.
128
+ # Recorded on the result so a merge in another process can inject the ones
129
+ # nobody loaded without needing this process's `cover` / `track_files`
130
+ # configuration. A standalone `collate` never ran `SimpleCov.start` (#1250).
179
131
  def tracked_file_paths
180
132
  UnloadedFileInjector.discover(
181
133
  unloaded_file_discovery_globs, root: root, reject: filters.select(&:path_only?)
182
134
  )
183
135
  end
184
136
 
185
- # Globs to expand on disk when injecting unloaded files into the
186
- # result. Combines the legacy `track_files` glob (additive only)
187
- # with every string glob declared via `cover` (also restrictive,
188
- # but the restriction lives in `Result#apply_cover_filters!`).
137
+ # The legacy `track_files` glob (additive only) plus every string glob
138
+ # declared via `cover` (also restrictive, but the restriction lives in
139
+ # `Result#apply_cover_filters!`).
189
140
  def unloaded_file_discovery_globs
190
141
  [tracked_files, *cover_globs].compact
191
142
  end
192
143
 
193
- # Run all the steps that handle processing the raw coverage result.
194
- # `report:` is true only when this slice is the final result (merging
195
- # off); with merging on the merged result reports dropped source files,
196
- # so the per-process slice stays quiet to avoid one warning per worker.
197
- #
198
- # `inject_unloaded:` is likewise false when a merge step follows. Only the
199
- # union of every process's loaded files says what was really never loaded,
200
- # so injecting here means each worker simulates nearly the whole project
201
- # and all but one of those passes is merged away. See #1250.
144
+ # `report:` and `inject_unloaded:` are false when a merge step follows.
145
+ # Only the union of every process's loaded files says what was really never
146
+ # loaded, and the merged result is the one that reports dropped source
147
+ # files, so a per-process slice would warn once per worker and simulate
148
+ # nearly the whole project only to have it merged away (#1250).
202
149
  def process_coverage_result(report:, inject_unloaded: true)
203
- raw = SimpleCov::UselessResultsRemover.call(Coverage.result)
204
- adapted = SimpleCov::ResultAdapter.call(raw)
205
- # What this process was told to track and did not load. Subtracting what
206
- # it loaded matters because `Result#to_hash` serializes coverage after
207
- # filtering: a file this process loaded and then filtered out would
208
- # otherwise be recorded as tracked while absent from the stored coverage,
209
- # and the merge would simulate it back in as never-loaded. See #1250.
150
+ # Templates nobody rendered are compiled into the running Coverage so they
151
+ # arrive at 0% below rather than being absent. Needs a live Coverage and
152
+ # ActionView, which rules out the merge point that does the same job for
153
+ # unloaded `.rb` files.
154
+ ViewCoverage.compile_unrendered
155
+ raw = UselessResultsRemover.call(Coverage.result)
156
+ adapted = ResultAdapter.call(raw)
157
+ # `Result#to_hash` serializes coverage after filtering, so a file this
158
+ # process loaded and then filtered out would otherwise be recorded as
159
+ # tracked while absent from the stored coverage, and the merge would
160
+ # simulate it back in as never-loaded (#1250).
210
161
  tracked = tracked_file_paths - adapted.keys
211
162
  result, not_loaded = inject_unloaded ? inject_unloaded_files(adapted, tracked) : [adapted, Set.new]
212
- @result = SimpleCov::Result.new(
213
- result, not_loaded_files: not_loaded, tracked_files: tracked,
214
- run_id: SimpleCov.run_id, worker_id: SimpleCov.worker_id, report: report
163
+ current_run.result = build_result(raw, result, not_loaded: not_loaded, tracked: tracked, report: report)
164
+ end
165
+
166
+ def build_result(raw, coverage, not_loaded:, tracked:, report:)
167
+ Result.new(
168
+ coverage, not_loaded_files: not_loaded, tracked_files: tracked, run_id: run_id,
169
+ worker_id: worker_id, contexts: test_tracker&.recorded_map(closing: raw), report: report
215
170
  )
216
171
  end
217
172
  end
@@ -3,16 +3,16 @@
3
3
  require "securerandom"
4
4
 
5
5
  module SimpleCov
6
- # Identifies one test invocation and each top-level parallel worker within
7
- # it. Forked subprocesses inherit both values from their parent worker.
6
+ # Identifies one test invocation and each top-level parallel worker within it.
7
+ # Forked subprocesses inherit both values from their parent worker.
8
8
  module RunIdentity
9
- module_function
9
+ extend self
10
10
 
11
- # Returns the run id together with its provenance: `true` when the id
12
- # alone proves same-run membership (explicitly configured, derived from
13
- # parallel_tests' per-invocation pid file, or freshly random), `false`
14
- # when it was inferred from the parent pid, which consecutive runs
15
- # launched by the same long-lived parent process share.
11
+ # Answers the run id together with its provenance: true when the id alone
12
+ # proves same-run membership (explicitly configured, derived from
13
+ # parallel_tests' per-invocation pid file, or freshly random), false when it
14
+ # was inferred from the parent pid, which consecutive runs launched by the
15
+ # same long-lived parent process share.
16
16
  def generate
17
17
  explicit = ENV.fetch("SIMPLECOV_RUN_ID", nil)
18
18
  return [explicit, true] if explicit && !explicit.empty?
@@ -20,7 +20,7 @@ module SimpleCov
20
20
  pid_file = ENV.fetch("PARALLEL_PID_FILE", nil)
21
21
  return ["parallel-tests:#{File.basename(pid_file)}", true] if pid_file && !pid_file.empty?
22
22
 
23
- return ["parallel-parent:#{Process.ppid}", false] if SimpleCov::ParallelAdapters.current
23
+ return ["parallel-parent:#{Process.ppid}", false] if ParallelAdapters.current
24
24
 
25
25
  [SecureRandom.uuid, true]
26
26
  end
@@ -35,10 +35,9 @@ module SimpleCov
35
35
  Process.pid.to_s
36
36
  end
37
37
 
38
- # Whether the current run id alone proves same-run membership. Decided
39
- # where the id is generated, never re-inferred from the id's shape, so
40
- # an explicit SIMPLECOV_RUN_ID that happens to look like an inferred
41
- # one is still trusted.
38
+ # Decided where the id is generated, never re-inferred from the id's shape,
39
+ # so an explicit SIMPLECOV_RUN_ID that happens to look like an inferred one
40
+ # is still trusted.
42
41
  def authoritative?
43
42
  materialize_current
44
43
  @authoritative
@@ -50,7 +49,7 @@ module SimpleCov
50
49
  end
51
50
 
52
51
  def materialize_current
53
- @current, @authoritative = generate unless defined?(@current)
52
+ @current, @authoritative = generate unless instance_variable_defined?(:@current)
54
53
  end
55
54
 
56
55
  def current_worker_id
@@ -58,19 +57,18 @@ module SimpleCov
58
57
  end
59
58
 
60
59
  def prepare
61
- SimpleCov::ParallelAdapters.current
60
+ ParallelAdapters.current
62
61
  SimpleCov.run_id
63
62
  SimpleCov.worker_id
64
63
  end
65
64
 
66
- # Mixed into SimpleCov after ParallelAdapters has loaded.
67
65
  module Accessors
68
66
  def run_id
69
- SimpleCov::RunIdentity.current
67
+ RunIdentity.current
70
68
  end
71
69
 
72
70
  def worker_id
73
- SimpleCov::RunIdentity.current_worker_id
71
+ RunIdentity.current_worker_id
74
72
  end
75
73
  end
76
74
  end
@@ -1,50 +1,39 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "coverage"
3
4
  require_relative "static_coverage_extractor"
4
5
 
5
6
  module SimpleCov
6
- #
7
- # Responsible for producing file coverage metrics.
8
- #
9
7
  module SimulateCoverage
10
- module_function
8
+ extend self
11
9
 
12
10
  #
13
11
  # Simulate a file coverage report for a file that was tracked but never
14
- # required. Returns the same hash shape as `Coverage.result` (lines,
15
- # branches, methods).
12
+ # required, in the same hash shape as `Coverage.result`.
16
13
  #
17
- # The line classification comes from `Coverage.line_stub` the same
14
+ # The line classification comes from `Coverage.line_stub`, the same
18
15
  # classification the runtime would have produced if the file had been
19
- # required overlaid with SimpleCov's `# :nocov:` toggles and
16
+ # required, overlaid with SimpleCov's `# :nocov:` toggles and
20
17
  # `# simplecov:disable line` directive ranges, which `Coverage` doesn't
21
18
  # know about. This keeps "relevant lines" identical whether a file was
22
- # loaded or just tracked, fixing the multi-line statement discrepancy
23
- # in https://github.com/simplecov-ruby/simplecov/issues/654.
19
+ # loaded or just tracked (#654).
24
20
  #
25
- # Branches and methods are enumerated by static analysis (via
26
- # `StaticCoverageExtractor`, which uses Prism). Earlier behavior left
27
- # both as empty hashes, which made unloaded files invisible to the
28
- # branch/method denominators while their lines DID count so a
29
- # `track_files`/`cover` glob that picked up files without specs
30
- # silently inflated branch% relative to line%. See
31
- # https://github.com/simplecov-ruby/simplecov/issues/1059. When Prism
32
- # isn't loadable (Ruby < 3.3 without the prism gem) or the file
33
- # can't be parsed, fall back to the old empty hashes — old behavior,
34
- # old tradeoff.
21
+ # Branches and methods are enumerated by static analysis. Earlier behavior
22
+ # left both as empty hashes, which made unloaded files invisible to the
23
+ # branch/method denominators while their lines did count, so a glob that
24
+ # picked up files without specs silently inflated branch% relative to
25
+ # line% (#1059). When Prism isn't loadable or the file can't be parsed,
26
+ # fall back to the old empty hashes.
35
27
  #
36
- # Pass `synthesize: false` to skip the static analysis and return the
37
- # empty hashes directly. Callers use it when neither branch nor method
38
- # coverage is enabled, since nothing will read the tuples and the Prism
39
- # parse is about half the cost of simulating a file. See #1250.
28
+ # Pass `synthesize: false` to skip the static analysis. Callers use it when
29
+ # neither branch nor method coverage is enabled, since nothing will read
30
+ # the tuples and the Prism parse is about half the cost of simulating a
31
+ # file (#1250).
40
32
  #
41
33
  # Pass `lines: false` to omit the `"lines"` key entirely, mirroring what
42
34
  # `Coverage.result` reports for a file loaded under a branch-only or
43
- # method-only run. Emitting zeroed lines there would make a simulated
44
- # file indistinguishable from one a sibling process actually loaded once
45
- # the two are merged.
46
- #
47
- # @return [Hash]
35
+ # method-only run. Emitting zeroed lines there would make a simulated file
36
+ # indistinguishable from one a sibling process actually loaded.
48
37
  #
49
38
  def call(absolute_path, synthesize: true, lines: true)
50
39
  source_lines = read_lines(absolute_path)
@@ -52,47 +41,43 @@ module SimpleCov
52
41
  return simulated unless lines
53
42
 
54
43
  classified = coverage_stub(absolute_path, source_lines) ||
55
- LinesClassifier.new.classify(source_lines)
44
+ LinesClassifier.new.classify(source_lines)
56
45
  {"lines" => classified}.merge(simulated)
57
46
  end
58
47
 
59
- # The branch and method tuples for a file, or empty hashes when the static
60
- # analysis is skipped (nothing enabled reads them) or unavailable (no Prism,
61
- # or the file doesn't parse).
48
+ # Empty hashes when the static analysis is skipped (nothing enabled reads
49
+ # them) or unavailable (no Prism, or the file doesn't parse).
62
50
  def synthesized_tuples(source_lines, synthesize)
63
51
  empty = {"branches" => {}, "methods" => {}} #: Hash[String, Hash[untyped, untyped]]
64
52
  synthesized = (StaticCoverageExtractor.call(source_lines.join) if synthesize) || empty
65
53
 
66
- {"branches" => synthesized["branches"], "methods" => synthesized["methods"]}
54
+ {"branches" => synthesized.fetch("branches"), "methods" => synthesized.fetch("methods")}
67
55
  end
68
56
 
69
- # SystemCallError, not just ENOENT: a `track_files` glob can sweep
70
- # up an unreadable file or a directory named like a Ruby file
71
- # (EACCES, EISDIR), and simulation must degrade to "empty file"
72
- # rather than crash the merge or report step.
57
+ # SystemCallError, not just ENOENT: a `track_files` glob can sweep up an
58
+ # unreadable file or a directory named like a Ruby file, and simulation must
59
+ # degrade to "empty file" rather than crash the merge or report step.
73
60
  def read_lines(path)
74
61
  File.readlines(path)
75
62
  rescue SystemCallError
76
63
  []
77
64
  end
78
65
 
79
- # Combine `Coverage.line_stub` (which gets multi-line statements right)
66
+ # Combines `Coverage.line_stub` (which gets multi-line statements right)
80
67
  # with `LinesClassifier` (which knows about `# :nocov:` toggles and
81
- # `# simplecov:disable line` ranges). Returns nil and the caller
82
- # falls back to `LinesClassifier` alone when `Coverage` can't read
83
- # or parse the file, or when the runtime doesn't expose `line_stub`
84
- # (JRuby and TruffleRuby).
68
+ # `# simplecov:disable line` ranges). Answers nil when `Coverage` can't
69
+ # read or parse the file, or the runtime doesn't expose `line_stub`.
85
70
  def coverage_stub(path, source_lines)
86
71
  return nil unless Coverage.respond_to?(:line_stub)
87
72
 
88
- stub = Coverage.line_stub(path)
89
- classifier_output = LinesClassifier.new.classify(source_lines)
90
- stub.each_index { |idx| stub[idx] = nil if classifier_output[idx].nil? }
91
- stub
73
+ # Paired off rather than indexed: the two lists describe the same file
74
+ # line for line. A line the classifier has no verdict for keeps the nil
75
+ # it would have been set to.
76
+ relevance = LinesClassifier.new.classify(source_lines)
77
+ Coverage.line_stub(path).zip(relevance).map { |hits, relevant| hits if relevant }
92
78
  rescue SystemCallError, SyntaxError
93
- # SystemCallError for the same reason as read_lines above:
94
- # line_stub reads the file itself, so EACCES/EISDIR surface here
95
- # too.
79
+ # SystemCallError for the same reason as read_lines: line_stub reads the
80
+ # file itself, so EACCES/EISDIR surface here too.
96
81
  nil
97
82
  end
98
83
  end
@@ -2,51 +2,33 @@
2
2
 
3
3
  module SimpleCov
4
4
  class SourceFile
5
- #
6
- # Representing single branch that has been detected in coverage report.
7
- # Give us support methods that handle needed calculations.
8
5
  class Branch
9
6
  attr_reader :start_line, :end_line, :coverage, :type
10
7
 
11
8
  def initialize(start_line:, end_line:, coverage:, inline:, type:)
12
9
  @start_line = start_line
13
- @end_line = end_line
14
- @coverage = coverage
15
- @inline = inline
16
- @type = type
17
- @skipped = false
10
+ @end_line = end_line
11
+ @coverage = coverage
12
+ @inline = inline
13
+ @type = type
14
+ @skipped = false
18
15
  end
19
16
 
20
17
  def inline?
21
18
  @inline
22
19
  end
23
20
 
24
- #
25
- # Return true if there is relevant count defined > 0
26
- #
27
- # @return [Boolean]
28
- #
29
21
  def covered?
30
22
  !skipped? && coverage.positive?
31
23
  end
32
24
 
33
- #
34
- # Check if branch missed or not
35
- #
36
- # @return [Boolean]
37
- #
38
25
  def missed?
39
26
  !skipped? && coverage.zero?
40
27
  end
41
28
 
42
- # The line on which we want to report the coverage
43
- #
44
- # Usually we choose the line above the start of the branch (so that it shows up
45
- # at if/else) because that
46
- # * highlights the condition
47
- # * makes it distinguishable if the first line of the branch is an inline branch
48
- # (see the nested_branches fixture)
49
- #
29
+ # Usually the line above the start of the branch, so that it shows up at the
30
+ # if/else. That highlights the condition, and makes it distinguishable when
31
+ # the first line of the branch is itself an inline branch.
50
32
  def report_line
51
33
  if inline?
52
34
  start_line
@@ -55,12 +37,10 @@ module SimpleCov
55
37
  end
56
38
  end
57
39
 
58
- # Flags the branch as skipped
59
40
  def skipped!
60
41
  @skipped = true
61
42
  end
62
43
 
63
- # Returns true if the branch was marked skipped by virtue of nocov comments.
64
44
  def skipped?
65
45
  @skipped
66
46
  end
@@ -69,11 +49,6 @@ module SimpleCov
69
49
  start_line <= line_range.end && end_line >= line_range.begin
70
50
  end
71
51
 
72
- #
73
- # Return array with coverage count and badge
74
- #
75
- # @return [Array]
76
- #
77
52
  def report
78
53
  [type, coverage]
79
54
  end