simplecov 1.1.1 → 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 (194) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +148 -5
  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 +19 -37
  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 +16 -10
  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 +37 -55
  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 +15 -9
  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 -19
  129. data/lib/simplecov/parallel_adapters/parallel_tests.rb +21 -42
  130. data/lib/simplecov/parallel_adapters.rb +24 -55
  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 +56 -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 +34 -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 +57 -113
  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 -5
@@ -3,27 +3,26 @@
3
3
  module SimpleCov
4
4
  class Result
5
5
  # When a resultset references source files that don't exist on the local
6
- # filesystem they're silently dropped which produces an empty `0 / 0
7
- # (100.00%)` report that looks like success but isn't. Emit a single
8
- # warning summarizing the drop and, when every entry was lost, point at
9
- # the typical cause (`SimpleCov.collate` invoked from a machine or path
10
- # different from where the resultsets were generated). See #980.
6
+ # filesystem they're silently dropped, which produces an empty
7
+ # `0 / 0 (100.00%)` report that looks like success but isn't. This emits a
8
+ # single warning summarizing the drop and, when every entry was lost, points
9
+ # at the typical cause: `SimpleCov.collate` invoked from a machine or path
10
+ # different from where the resultsets were generated (#980).
11
11
  class MissingSourceFilesReporter
12
- def initialize(missing_paths, input_size:, every_entry_dropped:)
12
+ def initialize(missing_paths, every_entry_dropped:)
13
13
  @missing_paths = missing_paths
14
- @input_size = input_size
15
14
  @every_entry_dropped = every_entry_dropped
16
15
  end
17
16
 
18
17
  def warn!
19
- warn SimpleCov::Color.colorize(message, :yellow)
18
+ warn Color.colorize(message, :yellow)
20
19
  end
21
20
 
22
21
  def message
23
22
  @every_entry_dropped ? all_missing_warning : partial_missing_warning
24
23
  end
25
24
 
26
- private
25
+ private
27
26
 
28
27
  def all_missing_warning
29
28
  "SimpleCov dropped all #{@missing_paths.size} source file(s) from the result — " \
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleCov
4
+ class Result
5
+ module Serialization
6
+ def to_hash
7
+ data = {"coverage" => coverage, "timestamp" => created_at.to_f} #: Hash[String, untyped]
8
+ data["run_id"] = run_id if run_id
9
+ data["worker_id"] = worker_id if worker_id
10
+ data["tracked_files"] = tracked_files unless tracked_files.empty?
11
+ append_contexts(data)
12
+ {command_name => data}
13
+ end
14
+
15
+ private
16
+
17
+ # Serialized whenever a map was recorded, even an empty one: for the merge,
18
+ # the key's presence is the signal that this result tracked tests, which is
19
+ # what lets it tell "tracked and covered nothing" from "never tracked". The
20
+ # map's files are restricted to this result's post-filter universe.
21
+ def append_contexts(data)
22
+ map = contexts
23
+ return unless map
24
+
25
+ data["contexts"] = map.to_h(only: context_filenames)
26
+ end
27
+
28
+ def context_filenames
29
+ Set.new(filenames)
30
+ end
31
+
32
+ # A live result's criterion keys are Symbols while entries parsed back from
33
+ # `.resultset.json` carry Strings, and the combiners read only String keys.
34
+ # Serializing with String keys is what lets a live result merged against a
35
+ # stored entry contribute its counts instead of being dropped.
36
+ def coverage
37
+ original_result.slice(*filenames).transform_values do |file_coverage|
38
+ file_coverage.transform_keys(&:to_s)
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -2,10 +2,6 @@
2
2
 
3
3
  module SimpleCov
4
4
  class Result
5
- # Constructs `SimpleCov::SourceFile` instances from a raw coverage
6
- # hash, sorts them by filename, and surfaces filenames whose source
7
- # is no longer present on disk so the caller can warn about the
8
- # silent drop (see #980).
9
5
  class SourceFileBuilder
10
6
  attr_reader :missing_source_files
11
7
 
@@ -16,14 +12,14 @@ module SimpleCov
16
12
  end
17
13
 
18
14
  def call
19
- SimpleCov::FileList.new(
15
+ FileList.new(
20
16
  @original_result
21
17
  .filter_map { |filename, coverage| build_source_file(filename, coverage) }
22
18
  .sort_by(&:filename)
23
19
  )
24
20
  end
25
21
 
26
- private
22
+ private
27
23
 
28
24
  def build_source_file(filename, coverage)
29
25
  unless File.file?(filename)
@@ -31,18 +27,17 @@ module SimpleCov
31
27
  return
32
28
  end
33
29
 
34
- SimpleCov::SourceFile.new(
30
+ SourceFile.new(
35
31
  filename,
36
32
  stringify_outer_keys(coverage),
37
33
  loaded: !@not_loaded_files.include?(filename)
38
34
  )
39
35
  end
40
36
 
41
- # `Coverage.result` returns symbol keys (`:lines`, `:branches`,
42
- # `:methods`); resultsets loaded from disk are already string-keyed.
43
- # SourceFile reads with strings, and handles both Array and
44
- # stringified-Array branch/method keys via `restore_ruby_data_structure`,
45
- # so only the outer hash needs normalizing.
37
+ # `Coverage.result` returns symbol keys; resultsets loaded from disk are
38
+ # already string-keyed. SourceFile reads with strings and handles both Array
39
+ # and stringified-Array branch/method keys itself, so only the outer hash
40
+ # needs normalizing.
46
41
  def stringify_outer_keys(coverage)
47
42
  coverage.transform_keys(&:to_s)
48
43
  end
@@ -2,210 +2,161 @@
2
2
 
3
3
  require "digest/sha1"
4
4
  require "forwardable"
5
+ require_relative "result/filter_config"
5
6
  require_relative "result/missing_source_files_reporter"
7
+ require_relative "result/serialization"
6
8
  require_relative "result/source_file_builder"
7
9
 
8
10
  module SimpleCov
9
11
  #
10
- # A simplecov code coverage result, initialized from the Hash Ruby's built-in coverage
11
- # library generates (Coverage.result).
12
+ # A code coverage result, initialized from the Hash Ruby's built-in coverage
13
+ # library generates.
12
14
  #
13
15
  class Result
14
16
  extend Forwardable
17
+ include Serialization
15
18
 
16
- # Returns the original Coverage.result used for this instance of SimpleCov::Result
17
- attr_reader :original_result
18
- # Every path the producing process was told to track, loaded or not. Carried
19
- # into the resultset so a merge elsewhere can inject the ones nobody loaded
20
- # without needing that process's `cover` / `track_files` config. See #1250.
19
+ attr_reader :original_result, :files
20
+ # Every path the producing process was told to track, loaded or not.
21
+ # Carried into the resultset so a merge elsewhere can inject the ones
22
+ # nobody loaded without needing that process's config (#1250).
21
23
  attr_reader :tracked_files
22
- # Invocation and top-level worker identities used only for parallel-result
23
- # coordination. They do not change which fresh suites are merged.
24
+ # Used only for parallel-result coordination. They do not change which
25
+ # fresh suites are merged.
24
26
  attr_reader :run_id, :worker_id
25
- # Returns all files that are applicable to this result (sans filters!) as instances of
26
- # SimpleCov::SourceFile. Aliased as :source_files
27
- attr_reader :files
28
- alias source_files files
29
- # Explicitly set the Time this result has been created
30
- attr_writer :created_at
31
- # Explicitly set the command name that was used for this coverage result. Defaults to SimpleCov.command_name
32
- attr_writer :command_name
27
+ alias_method :source_files, :files
28
+ # The `ContextMap` recorded under `track_tests`, or nil when this result
29
+ # carries none: tracking was off, or a merge dropped the map because not
30
+ # every merged result recorded one.
31
+ attr_reader :contexts
32
+
33
+ # The distinct run names behind this result, set by a merge so presentation
34
+ # can say "A and N other runs" instead of reading the joined
35
+ # `command_name` aloud in full. Splitting that string back apart would
36
+ # misread a run name that itself contains a comma.
37
+ attr_writer :created_at, :command_name, :command_names
33
38
 
34
39
  def_delegators :files, :covered_percent, :covered_percentages, :least_covered_file, :covered_strength,
35
- :covered_lines, :missed_lines,
36
- :total_branches, :covered_branches, :missed_branches,
37
- :total_methods, :covered_methods, :missed_methods,
38
- :coverage_statistics, :coverage_statistics_by_file
40
+ :covered_lines, :missed_lines,
41
+ :total_branches, :covered_branches, :missed_branches,
42
+ :total_methods, :covered_methods, :missed_methods,
43
+ :coverage_statistics, :coverage_statistics_by_file
39
44
  def_delegator :files, :lines_of_code, :total_lines
40
45
 
41
- # Bundles the filter and grouping configuration a Result applies to its
42
- # source files after building them. Each field defaults to the SimpleCov
43
- # singleton's configuration, so ordinary callers never construct one;
44
- # tests pass a custom instance to opt out of (or extend) the project's
45
- # filters or groups (e.g. `filters: []` to keep every file). Grouping the
46
- # three together keeps Result#initialize's parameter list small.
47
- class FilterConfig
48
- attr_reader :filters, :cover_filters, :groups
49
-
50
- def initialize(filters: SimpleCov.filters, cover_filters: SimpleCov.cover_filters, groups: SimpleCov.groups)
51
- @filters = filters
52
- @cover_filters = cover_filters
53
- @groups = groups
54
- end
55
- end
56
-
57
- # Initialize a new SimpleCov::Result from given Coverage.result (a Hash of filenames each containing an array of
58
- # coverage data).
59
- #
60
46
  # `filter_config` defaults to the SimpleCov singleton's filter / group
61
- # configuration so existing call sites are unchanged. Pass a custom
62
- # FilterConfig to opt out — useful for tests that build synthetic Results
63
- # and don't want the project's filters or groups applied.
47
+ # configuration. Pass a custom FilterConfig to opt out, which is useful for
48
+ # tests that build synthetic Results.
49
+ #
50
+ # `tracked_files` accepts any collection that answers `to_a` (the merge
51
+ # passes a Set), and nil for a run that tracked nothing.
64
52
  def initialize(original_result, command_name: nil, created_at: nil, not_loaded_files: Set.new,
65
- tracked_files: [], run_id: nil, worker_id: nil, report: false, filter_config: FilterConfig.new)
53
+ tracked_files: nil, run_id: nil, worker_id: nil, contexts: nil, report: false,
54
+ filter_config: FilterConfig.new)
66
55
  @original_result = original_result.freeze
67
56
  @command_name = command_name
68
57
  @created_at = created_at
69
- initialize_coordination_metadata(tracked_files, run_id, worker_id)
58
+ initialize_resultset_metadata(tracked_files, run_id, worker_id, contexts)
70
59
  @groups_config = filter_config.groups
71
60
  builder = SourceFileBuilder.new(original_result, not_loaded_files: not_loaded_files)
72
61
  @files = builder.call
73
- warn_about_missing_source_files(builder.missing_source_files, original_result.size) if report
62
+ warn_about_missing_source_files(builder.missing_source_files) if report
74
63
  apply_cover_filters!(filter_config.cover_filters)
75
64
  apply_filters!(filter_config.filters)
76
65
  end
77
66
 
78
- # Returns all filenames for source files contained in this result
79
67
  def filenames
80
68
  files.map(&:filename)
81
69
  end
82
70
 
83
- # Returns the SimpleCov::SourceFile for the given path, or nil if no
84
- # matching file is in this result. The path is resolved against
85
- # SimpleCov.root, so callers can pass either an absolute path or a
86
- # project-relative one.
71
+ # The path is resolved against SimpleCov.root, so callers can pass either
72
+ # an absolute path or a project-relative one.
87
73
  def source_file_for(path)
88
74
  target = File.expand_path(path, SimpleCov.root)
89
- files.find { |file| file.filename == target }
75
+ files.find { |file| file.filename.eql?(target) }
90
76
  end
91
77
 
92
- # Returns the {line:/branch:/method:} coverage_statistics hash for the
93
- # given file path, or nil if no matching source file is in this
94
- # result. See SimpleCov::Result#source_file_for for path resolution.
78
+ # Path resolution as in `source_file_for`.
95
79
  def coverage_for(path)
96
80
  source_file_for(path)&.coverage_statistics
97
81
  end
98
82
 
99
- # Returns a Hash of groups for this result. Define groups using SimpleCov.group 'Models', 'app/models'
100
83
  def groups
101
84
  @groups ||= SimpleCov.grouped(files, groups: @groups_config)
102
85
  end
103
86
 
104
- # Applies the configured SimpleCov.formatter on this result. Returns
105
- # nil if formatting has been opted out of (`SimpleCov.formatter false`
106
- # / `SimpleCov.formatters []`) the cheap path for non-final
107
- # processes in a parallel CI run, which only need their
108
- # `.resultset.json` on disk. See #964.
87
+ # Returns nil if formatting has been opted out of (`SimpleCov.formatter
88
+ # false` / `SimpleCov.formatters []`), the cheap path for non-final
89
+ # processes in a parallel CI run, which only need their `.resultset.json`
90
+ # on disk (#964).
109
91
  def format!
110
92
  formatter = SimpleCov.formatter
111
93
  return nil if formatter.nil?
112
94
 
113
95
  formatted = Formatter.format(formatter, self)
114
96
  # Recorded regardless of how the run ends, so a parent process's
115
- # clobber-prevention backstop can tell a report was produced even
116
- # when this run's checks or tests failed (unlike .last_run.json,
117
- # which only successful runs write).
118
- SimpleCov::ReportStamp.touch
97
+ # clobber-prevention backstop can tell a report was produced even when
98
+ # this run's checks or tests failed, unlike .last_run.json.
99
+ ReportStamp.touch
119
100
  formatted
120
101
  end
121
102
 
122
- # Defines when this result has been created. Defaults to Time.now
123
103
  def created_at
124
104
  @created_at ||= Time.now
125
105
  end
126
106
 
127
- # The command name that launched this result.
128
- # Delegated to SimpleCov.command_name if not set manually
129
107
  def command_name
130
108
  @command_name ||= SimpleCov.command_name
131
109
  end
132
110
 
133
- # Returns a hash representation of this Result that can be used for marshalling it into JSON
134
- def to_hash
135
- data = {"coverage" => coverage, "timestamp" => created_at.to_f} #: Hash[String, untyped]
136
- data["run_id"] = run_id if run_id
137
- data["worker_id"] = worker_id if worker_id
138
- # Omitted when empty so a run that tracks nothing writes the shape it
139
- # always has, and so the key only appears where it carries information.
140
- data["tracked_files"] = tracked_files unless tracked_files.empty?
141
- {command_name => data}
111
+ def command_names
112
+ @command_names ||= [command_name]
142
113
  end
143
114
 
144
- # Loads a SimpleCov::Result#to_hash dump
145
115
  def self.from_hash(hash)
146
116
  hash.map do |command_name, data|
147
- new(data.fetch("coverage"), command_name: command_name, created_at: Time.at(data["timestamp"]),
148
- tracked_files: data["tracked_files"] || [], run_id: data["run_id"],
149
- worker_id: data["worker_id"])
117
+ new(data.fetch("coverage"), command_name: command_name, created_at: Time.at(data.fetch("timestamp")),
118
+ tracked_files: data["tracked_files"], run_id: data["run_id"],
119
+ worker_id: data["worker_id"], contexts: ContextMap.from_hash(data["contexts"]))
150
120
  end
151
121
  end
152
122
 
153
- private
123
+ private
154
124
 
155
- def initialize_coordination_metadata(tracked_files, run_id, worker_id)
125
+ def initialize_resultset_metadata(tracked_files, run_id, worker_id, contexts)
156
126
  @tracked_files = tracked_files.to_a
157
127
  @run_id = run_id
158
128
  @worker_id = worker_id
129
+ @contexts = contexts
159
130
  end
160
131
 
161
- def warn_about_missing_source_files(missing, input_size)
132
+ def warn_about_missing_source_files(missing)
162
133
  return if missing.empty?
163
134
 
164
- # Emit only from the process that writes the final report. The merged
165
- # result is rebuilt in every parallel worker (each one stores its own
166
- # slice), so without this gate the warning prints once per worker this
167
- # is the same signal SimpleCov uses to pick the process that runs the
168
- # report and threshold checks. It's intentionally not gated on
169
- # print_errors: the default at_fork sets print_errors false on workers,
170
- # and in many parallel runners the final-report process is itself a
171
- # worker, so a print_errors gate would suppress the one warning we want.
172
- # See issues #980 and #1171.
135
+ # Emit only from the process that writes the final report: the merged
136
+ # result is rebuilt in every parallel worker, so without this gate the
137
+ # warning prints once per worker. Intentionally not gated on
138
+ # print_errors, because the default at_fork sets print_errors false on
139
+ # workers and in many parallel runners the final-report process is itself
140
+ # a worker (#980, #1171).
173
141
  return unless SimpleCov.final_result_process?
174
142
 
175
- MissingSourceFilesReporter.new(
176
- missing,
177
- input_size: input_size,
178
- every_entry_dropped: @files.empty? && missing.size == input_size
179
- ).warn!
180
- end
181
-
182
- # A live result's criterion keys are Symbols (`:lines`, `:branches`),
183
- # while entries parsed back from `.resultset.json` carry Strings, and
184
- # the combiners read only String keys. Serialize with String keys so a
185
- # live result merged against a stored entry contributes its counts
186
- # instead of being silently dropped for every shared file.
187
- def coverage
188
- original_result.slice(*filenames).transform_values do |file_coverage|
189
- file_coverage.transform_keys(&:to_s)
190
- end
143
+ # Every built file survives (only the missing ones are dropped), so an
144
+ # empty file list is exactly the "nothing was found" case.
145
+ MissingSourceFilesReporter.new(missing, every_entry_dropped: @files.empty?).warn!
191
146
  end
192
147
 
193
- # Applies the given filter chain to `@files`, dropping each source
194
- # file that any filter matches.
195
148
  def apply_filters!(filters)
196
149
  filters.each do |filter|
197
- @files = SimpleCov::FileList.new(@files.reject { |source_file| filter.matches?(source_file) })
150
+ @files = FileList.new(@files.reject { |source_file| filter.matches?(source_file) })
198
151
  end
199
152
  end
200
153
 
201
- # When any `cover` matcher is configured, restrict `@files` to source
202
- # files matching at least one of them. With no cover matchers configured
203
- # this is a no-op, preserving the historical "everything required, then
204
- # filtered" universe.
154
+ # With no cover matchers configured this is a no-op, preserving the
155
+ # historical "everything required, then filtered" universe.
205
156
  def apply_cover_filters!(cover_filters)
206
157
  return if cover_filters.empty?
207
158
 
208
- @files = SimpleCov::FileList.new(
159
+ @files = FileList.new(
209
160
  @files.select { |source_file| cover_filters.any? { |filter| filter.matches?(source_file) } }
210
161
  )
211
162
  end
@@ -3,9 +3,6 @@
3
3
  require "coverage"
4
4
 
5
5
  module SimpleCov
6
- #
7
- # Responsible for adapting the format of the coverage result whether it's default or with statistics
8
- #
9
6
  class ResultAdapter
10
7
  attr_reader :result
11
8
 
@@ -25,14 +22,12 @@ module SimpleCov
25
22
  end
26
23
  end
27
24
 
28
- private
25
+ private
29
26
 
30
27
  # Pre-0.18 resultsets pointed each filename straight at a line-coverage
31
- # array; everything since uses the `{lines:, branches:, methods:}`
32
- # shape. Newer entries also need their methods and branches tables
33
- # massaged before downstream code reports or merges them.
28
+ # array; everything since uses the `{lines:, branches:, methods:}` shape.
34
29
  def adapt_one(file_name, cover_statistic)
35
- return {"lines" => cover_statistic} if cover_statistic.is_a?(Array)
30
+ return {"lines" => cover_statistic} if cover_statistic.instance_of?(Array)
36
31
 
37
32
  adapt_oneshot_lines_if_needed(file_name, cover_statistic)
38
33
  normalize_method_keys(cover_statistic)
@@ -40,42 +35,33 @@ module SimpleCov
40
35
  cover_statistic
41
36
  end
42
37
 
43
- # Normalize memory addresses in method coverage keys so that results
44
- # from different processes can be merged. Anonymous class names like
45
- # "#<Class:0x00007ff19ab24790>" get inconsistent addresses across runs.
46
- # Address widths vary by runtime (32-bit hosts: 8 hex chars; 64-bit
47
- # CRuby: 16; some JVM/TruffleRuby formats may differ), so match any
48
- # length of hex digits and collapse to a single placeholder.
38
+ # Normalizes memory addresses in method coverage keys so results from
39
+ # different processes can be merged: anonymous class names like
40
+ # "#<Class:0x00007ff19ab24790>" get inconsistent addresses across runs, and
41
+ # address widths vary by runtime.
49
42
  ADDRESS_PATTERN = /0x\h+/
50
43
  private_constant :ADDRESS_PATTERN
51
44
 
52
45
  ADDRESS_PLACEHOLDER = "0x0"
53
46
  private_constant :ADDRESS_PLACEHOLDER
54
47
 
55
- # Strip the `#<Class:Foo>` wrapper Ruby's Coverage adds to singleton-class
48
+ # Strips the `#<Class:Foo>` wrapper Ruby's Coverage adds to singleton-class
56
49
  # method keys. `module_function` and class methods get recorded both as
57
- # singleton (`[#<Class:Foo>, :m, …]`) and instance/module (`[Foo, :m, …]`)
58
- # entries pointing at the same source location; only one of the two is
59
- # ever reachable at runtime, so we merge them. Only applies to named
60
- # constants — anonymous-class addresses like `#<Class:0x0>` are left
61
- # alone (handled by ADDRESS_PATTERN above).
50
+ # singleton and instance/module entries pointing at the same source
51
+ # location, and only one of the two is ever reachable at runtime. Only
52
+ # named constants: anonymous-class addresses are left to ADDRESS_PATTERN.
62
53
  SINGLETON_WRAPPER_PATTERN = /\A#<Class:([A-Z_][\w:]*)>\z/
63
54
  private_constant :SINGLETON_WRAPPER_PATTERN
64
55
 
65
- # Ruby's method coverage records one entry per DEFINED METHOD, not per
66
- # source location: a block handed to `define_method` /
67
- # `define_singleton_method` from a shared code path yields a separate
68
- # `[receiver, name, location]` entry for every class it's defined on
69
- # (a module's `included` hook defining onto each descendant) AND for
70
- # every name it's defined under (a builder looping `define_method key`
71
- # over a container), all pointing at the same source. A file-based
72
- # report can only express "was the method at this location ever
73
- # executed", so entries are aggregated by location alone, summing
74
- # hits — otherwise each receiver or name whose generated copy never
75
- # ran shows as a phantom uncovered method on a line whose line
76
- # coverage is 100%. Regular `def`s map one location to one name, so
77
- # they are unaffected. The first entry's (normalized) key is kept for
78
- # display. See issue #1234.
56
+ # Ruby's method coverage records one entry per defined method, not per
57
+ # source location: a block handed to `define_method` from a shared code
58
+ # path yields a separate `[receiver, name, location]` entry for every class
59
+ # it's defined on and every name it's defined under, all pointing at the
60
+ # same source. A file-based report can only express "was the method at this
61
+ # location ever executed", so entries are aggregated by location alone,
62
+ # summing hits. Otherwise each generated copy that never ran shows as a
63
+ # phantom uncovered method on a line whose line coverage is 100% (#1234).
64
+ # The first entry's normalized key is kept for display.
79
65
  def normalize_method_keys(cover_statistic)
80
66
  methods = cover_statistic[:methods]
81
67
  return unless methods
@@ -90,72 +76,74 @@ module SimpleCov
90
76
  end
91
77
 
92
78
  def normalize_method_key(key)
93
- normalized_key = key.dup
94
- normalized_key[0] = class_display_name(key[0])
95
- .gsub(ADDRESS_PATTERN, ADDRESS_PLACEHOLDER)
96
- .sub(SINGLETON_WRAPPER_PATTERN, '\1')
97
- normalized_key
79
+ receiver, *rest = key
80
+ normalized_receiver = class_display_name(receiver)
81
+ .gsub(ADDRESS_PATTERN, ADDRESS_PLACEHOLDER)
82
+ .sub(SINGLETON_WRAPPER_PATTERN, '\1')
83
+ [normalized_receiver, *rest]
98
84
  end
99
85
 
100
- # Rendering a class name can execute user code: a singleton class's
101
- # `to_s` renders its attached object via `#inspect`, which a module can
102
- # shadow with an incompatible signature (Liquid::Utils defines
86
+ # Rendering a class name can execute user code: a singleton class's `to_s`
87
+ # renders its attached object via `#inspect`, which a module can shadow
88
+ # with an incompatible signature (Liquid::Utils defines
103
89
  # `inspect(value, max_depth = 2)` as a module_function, so rendering
104
- # `#<Class:Liquid::Utils>` raises ArgumentError). A coverage report
105
- # must never crash the host suite over that, so on failure rebuild the
106
- # singleton wrapper from `Module#name` via bound methods (which cannot
107
- # be shadowed), falling back to the address form, which
108
- # ADDRESS_PATTERN then normalizes. See issue #1236.
90
+ # `#<Class:Liquid::Utils>` raises ArgumentError). A coverage report must
91
+ # never crash the host suite over that (#1236).
109
92
  def class_display_name(klass)
110
93
  klass.to_s
111
- rescue StandardError
94
+ rescue
112
95
  singleton_wrapper_name(klass) || Object.instance_method(:to_s).bind_call(klass)
113
96
  end
114
97
 
98
+ # `singleton_class?` is a Module method, so a receiver that is neither a
99
+ # class nor a module has to be turned away first. Anything that then
100
+ # answers `singleton_class?` truthily is a Class, so `attached_object`
101
+ # applies, reached through a bound method so shadowing cannot divert it.
115
102
  def singleton_wrapper_name(klass)
116
- return nil unless klass.is_a?(Class) && klass.singleton_class?
103
+ return nil unless klass.is_a?(Module) && klass.singleton_class?
117
104
 
118
- attached = klass.attached_object
119
- # simplecov:disable branch — CRuby only reaches the rescue via a
120
- # Module/Class attached object (instance singletons render from the
121
- # class name chain without calling user inspect), so the non-Module
122
- # arm is defensive.
105
+ attached = Class.instance_method(:attached_object).bind_call(klass)
123
106
  name = Module.instance_method(:name).bind_call(attached) if attached.is_a?(Module)
124
- # simplecov:enable branch
125
107
  name && "#<Class:#{name}>"
126
108
  end
127
109
 
128
110
  # Ruby's eval coverage records a fresh set of branch entries for every
129
- # COMPILE of an eval'd string: a template rendered through multiple view
130
- # classes (e.g. hanami-view compiles each template once per view) yields
131
- # several `[:if, id, location]` conditions at identical coordinates in
132
- # the same file, each counting only the renders that flowed through that
111
+ # compile of an eval'd string: a template rendered through multiple view
112
+ # classes yields several `[:if, id, location]` conditions at identical
113
+ # coordinates, each counting only the renders that flowed through that
133
114
  # compile. Reported as-is they inflate the branch denominator and turn a
134
- # side covered under a different compile into a phantom miss (issue
135
- # #1235). Aggregate them by (type, location) combining a branches hash
136
- # with an empty one dedups within it, since BranchesCombiner keys arms
137
- # on location identity. Regular (non-eval) source can never produce two
138
- # conditions at the same location, so this is a no-op outside eval.
115
+ # side covered under a different compile into a phantom miss (#1235).
116
+ # Absorbing into an empty table dedups, since BranchesCombiner keys arms on
117
+ # location identity. Regular source can never produce two conditions at the
118
+ # same location, so this is a no-op outside eval.
139
119
  def aggregate_duplicated_branches(cover_statistic)
140
120
  branches = cover_statistic[:branches]
141
121
  return unless branches
142
122
 
143
- cover_statistic[:branches] = Combine::BranchesCombiner.combine(branches, {})
123
+ combiner = Combine::BranchesCombiner
124
+ cover_statistic[:branches] = combiner.materialize(combiner.absorb({}, branches))
144
125
  end
145
126
 
146
127
  def adapt_oneshot_lines_if_needed(file_name, cover_statistic)
147
128
  return unless cover_statistic.key?(:oneshot_lines)
148
129
 
149
130
  oneshot_lines = cover_statistic.delete(:oneshot_lines)
150
- line_stub = build_line_stub(file_name, oneshot_lines)
131
+ line_stub = build_line_stub(file_name)
151
132
  oneshot_lines.each { |covered_line| line_stub[covered_line - 1] = 1 }
152
133
  cover_statistic[:lines] = line_stub
153
134
  end
154
135
 
155
- def build_line_stub(file_name, oneshot_lines)
136
+ # A file that has vanished or no longer parses has no stub to build from,
137
+ # and neither does a runtime without `Coverage.line_stub` (TruffleRuby), so
138
+ # start from nothing: the assignments in `adapt_oneshot_lines_if_needed`
139
+ # grow the array to the highest covered line, which is as far as the
140
+ # oneshot data reaches anyway.
141
+ def build_line_stub(file_name)
142
+ return [] unless Coverage.respond_to?(:line_stub)
143
+
156
144
  Coverage.line_stub(file_name)
157
145
  rescue Errno::ENOENT, SyntaxError
158
- Array.new(oneshot_lines.max || 0, nil)
146
+ []
159
147
  end
160
148
  end
161
149
  end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleCov
4
+ module ResultMerger
5
+ module Contexts
6
+ extend self
7
+
8
+ # The same union-or-drop rule the cross-suite merge applies: the union when
9
+ # both sides recorded a map, no map at all when either did not, since a
10
+ # partial map would present one runner's tests as the pair's. `entry` starts
11
+ # as a copy of `incoming`, so the drop has to remove the key rather than
12
+ # merely decline to add it.
13
+ def carry(entry, existing, incoming)
14
+ ours = ContextMap.from_hash(existing["contexts"])
15
+ theirs = ContextMap.from_hash(incoming["contexts"])
16
+ return entry.except("contexts") unless ours && theirs
17
+
18
+ entry.merge("contexts" => ours.absorb(theirs).to_h)
19
+ end
20
+ end
21
+ end
22
+ end