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
@@ -3,82 +3,81 @@
3
3
  require "fileutils"
4
4
 
5
5
  module SimpleCov
6
- #
7
- # Bundles the configuration options used for SimpleCov. All methods
8
- # defined here are usable from SimpleCov directly. Please check out
9
- # SimpleCov documentation for further info.
10
- #
11
6
  module Configuration
12
- #
13
- # The root for the project. This defaults to the
14
- # current working directory.
15
- #
16
- # Configure with SimpleCov.root('/my/project/path')
17
- #
18
7
  def root(root = nil)
19
- return @root if defined?(@root) && root.nil?
8
+ return @root if instance_variable_defined?(:@root) && root.nil?
20
9
 
10
+ self.root = root
11
+ @root
12
+ end
13
+
14
+ # nil means the working directory, the default the reader would have derived.
15
+ def root=(root)
21
16
  @coverage_path = nil unless @coverage_path_explicit # invalidate cache
22
17
  @root = File.expand_path(root || Dir.getwd)
23
18
  end
24
19
 
25
- #
26
- # The name of the output and cache directory. Defaults to 'coverage'
27
- #
28
- # Configure with SimpleCov.coverage_dir('cov')
29
- #
30
20
  def coverage_dir(dir = nil)
31
- return @coverage_dir if defined?(@coverage_dir) && dir.nil?
21
+ return @coverage_dir if instance_variable_defined?(:@coverage_dir) && dir.nil?
22
+
23
+ self.coverage_dir = dir
24
+ @coverage_dir
25
+ end
32
26
 
27
+ # nil means the default 'coverage', recorded as a derivation rather than a
28
+ # choice.
29
+ def coverage_dir=(dir)
33
30
  @coverage_path = nil unless @coverage_path_explicit # invalidate cache
34
31
  @coverage_dir_explicit = true unless dir.nil?
35
32
  @coverage_dir = dir || "coverage"
36
33
  end
37
34
 
38
35
  #
39
- # Returns the full path to the output directory. By default
40
- # constructed from `SimpleCov.root` + `SimpleCov.coverage_dir`, but
41
- # callers can override with an arbitrary absolute path — handy for
42
- # out-of-tree build directories. See #716.
36
+ # Defaults to `SimpleCov.root` + `SimpleCov.coverage_dir`, but callers can
37
+ # override with an arbitrary absolute path, handy for out-of-tree build
38
+ # directories (#716).
43
39
  #
44
- # Reading is pure: the directory is only created when a path is
45
- # explicitly assigned (the user has signaled they intend to write
46
- # there). The codepaths that actually write into the directory
47
- # (formatters, `LastRun`, `ResultsetStore`) ensure existence
48
- # themselves, so read-only CLI subcommands that interpolate the
49
- # path into status text don't materialize a stray `coverage/`
50
- # directory.
40
+ # Reading is pure: the directory is only created when a path is explicitly
41
+ # assigned. The codepaths that write into it ensure existence themselves,
42
+ # so read-only CLI subcommands that interpolate the path into status text
43
+ # don't materialize a stray `coverage/` directory.
51
44
  #
52
45
  def coverage_path(path = nil)
53
- if path
54
- expanded = File.expand_path(path)
55
- @coverage_path = expanded
56
- @coverage_path_explicit = true
57
- FileUtils.mkdir_p expanded
58
- end
46
+ self.coverage_path = path if path
59
47
 
60
48
  @coverage_path ||= File.expand_path(coverage_dir, root)
61
49
  end
62
50
 
51
+ # Assigning is the signal the caller intends to write there, so the
52
+ # directory is created.
53
+ def coverage_path=(path)
54
+ expanded = File.expand_path(path)
55
+ @coverage_path = expanded
56
+ @coverage_path_explicit = true
57
+ FileUtils.mkdir_p expanded
58
+ end
59
+
63
60
  #
64
- # The name of the command (a.k.a. Test Suite) currently running.
65
- # Used for result merging and caching. Auto-detected; set explicitly
66
- # with SimpleCov.command_name("test:units").
61
+ # The name of the command (a.k.a. Test Suite) currently running. Used for
62
+ # result merging and caching. Auto-detected.
67
63
  #
68
64
  def command_name(name = nil)
69
- @command_name = name unless name.nil?
70
- @command_name ||= SimpleCov::CommandGuesser.guess
65
+ self.command_name = name unless name.nil?
66
+ @command_name ||= CommandGuesser.guess
67
+ end
68
+
69
+ def command_name=(name)
70
+ @command_name = name
71
71
  end
72
72
 
73
- # Returns the hash of available profiles
74
73
  def profiles
75
- @profiles ||= SimpleCov::Profiles.new
74
+ @profiles ||= Profiles.new
76
75
  end
77
76
 
78
77
  #
79
- # Allows you to configure simplecov in a block instead of
80
- # prepending SimpleCov to each config method. Parameterized blocks retain
81
- # their caller context and receive this configuration target explicitly.
78
+ # Configure simplecov in a block instead of prepending SimpleCov to each
79
+ # config method. Parameterized blocks retain their caller context and
80
+ # receive this configuration target explicitly.
82
81
  #
83
82
  def configure(&block)
84
83
  raise ArgumentError, "configuration block required" unless block
@@ -91,61 +90,49 @@ module SimpleCov
91
90
  self
92
91
  end
93
92
 
94
- #
95
- # Gets or sets the behavior to process coverage results.
96
- # By default, it stores/merges the current result and formats only
97
- # from the final reporting process.
98
- #
99
93
  def at_exit(&block)
100
94
  @at_exit = block if block
101
95
  configured = @at_exit
102
96
  return configured if configured
103
- return proc {} unless active_session?
97
+ return -> {} unless active_session?
104
98
 
105
- @at_exit = proc do
99
+ @at_exit = lambda do
106
100
  result = SimpleCov.result
107
101
  result.format! if result && SimpleCov.merge_finalization_owner?
108
102
  end
109
103
  end
110
104
 
111
- # Whether SimpleCov has anything to do at exit: the Coverage module
112
- # is actively tracking, or a `@result` has already been assembled
113
- # (e.g. by `SimpleCov.collate`, which never starts Coverage).
105
+ # The Coverage module is actively tracking, or a `@result` has already been
106
+ # assembled (`SimpleCov.collate` never starts Coverage).
114
107
  def active_session?
115
- SimpleCov.result? || (defined?(Coverage) && Coverage.running?)
108
+ !!SimpleCov.result? || coverage_running?
109
+ end
110
+
111
+ # A configuration loaded on its own may have no Coverage to ask, which is
112
+ # not something a suite that measures coverage can be.
113
+ def coverage_running?
114
+ defined?(Coverage) ? Coverage.running? : false
116
115
  end
117
116
 
118
- #
119
- # Gets or sets the behavior to start a new forked Process.
120
- # Defaults to adding " (subprocess: #{serial})" to command_name and
121
- # starting SimpleCov in quiet mode.
122
- #
123
117
  def at_fork(&block)
124
118
  @at_fork = block if block
125
119
  @at_fork ||= lambda { |_pid|
126
- # Needs a name that's unique per worker within a run yet identical
127
- # across runs. Build it from SimpleCov's stable fork serial rather
128
- # than the OS pid: with the pid, every run produced uniquely-named
129
- # results that never overwrote the previous run's, so they piled up
130
- # in .resultset.json until merge_timeout and the merged report's
131
- # file set drifted from run to run. See issue #1171.
120
+ # Needs a name that's unique per worker within a run yet identical across
121
+ # runs. Built from SimpleCov's stable fork serial rather than the OS pid:
122
+ # with the pid, every run produced uniquely-named results that never
123
+ # overwrote the previous run's, so they piled up in .resultset.json
124
+ # until merge_timeout and the merged report's file set drifted from run
125
+ # to run (#1171).
132
126
  SimpleCov.command_name "#{SimpleCov.command_name} (subprocess: #{SimpleCov.subprocess_serial})"
133
- # be quiet, the parent process will use the regular formatter
134
127
  SimpleCov.print_errors false
135
- SimpleCov.formatter SimpleCov::Formatter::SimpleFormatter
128
+ SimpleCov.formatter Formatter::SimpleFormatter
136
129
  SimpleCov.minimum_coverage 0
137
130
  SimpleCov.start
138
131
  }
139
132
  end
140
133
 
141
- #
142
- # Returns the project name — defaults to the last dirname in
143
- # SimpleCov.root, capitalized with underscores → spaces.
144
- #
134
+ # Only a String renames, and a name already chosen survives anything else.
145
135
  def project_name(new_name = nil)
146
- current = defined?(@project_name) ? @project_name : nil
147
- return current if current && new_name.nil?
148
-
149
136
  @project_name = new_name if new_name.is_a?(String)
150
137
  @project_name ||= File.basename(root).capitalize.tr("_", " ")
151
138
  end
@@ -160,4 +147,11 @@ require_relative "configuration/groups"
160
147
  require_relative "configuration/formatting"
161
148
  require_relative "configuration/ignored_entries"
162
149
  require_relative "configuration/merging"
150
+ require_relative "configuration/test_tracking"
163
151
  require_relative "configuration/thresholds"
152
+ require_relative "configuration/baseline"
153
+ require_relative "configuration/deprecations"
154
+ require_relative "configuration/history"
155
+ require_relative "configuration/missed_caps"
156
+ require_relative "configuration/production"
157
+ require_relative "configuration/view_coverage"
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleCov
4
+ class ConfigurationError < StandardError; end
5
+ end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleCov
4
+ class ContextMap
5
+ # Accumulates the context maps of every result a merge absorbs, under the
6
+ # all-or-nothing rule the resultset format needs: the merged result carries
7
+ # the union of the maps when every merged result recorded one, and no map at
8
+ # all otherwise. Keeping a partial union would present one worker's map as
9
+ # the whole run's, silently omitting the tests of the results that didn't
10
+ # record.
11
+ class Union
12
+ attr_reader :carrying, :entries
13
+
14
+ def initialize
15
+ @map = ContextMap.new
16
+ @complete = true
17
+ @carrying = 0
18
+ @entries = 0
19
+ end
20
+
21
+ def collector
22
+ ->(surviving) { absorb_resultset(surviving) }
23
+ end
24
+
25
+ def absorb_resultset(resultset)
26
+ resultset.each_value { |data| absorb_entry(data) }
27
+ end
28
+
29
+ def absorb_entry(data)
30
+ @entries += 1
31
+ map = ContextMap.from_hash(data["contexts"])
32
+ return @complete = false unless map
33
+
34
+ @carrying += 1
35
+ @map.absorb(map)
36
+ end
37
+
38
+ # The parallel merge builds one union per worker over its slice and ships it
39
+ # back, so the parent combines unions rather than re-reading entries.
40
+ def absorb_union(other)
41
+ @entries += other.entries
42
+ @carrying += other.carrying
43
+ @complete &&= other.complete?
44
+ @map.absorb(other.partial_map)
45
+ self
46
+ end
47
+
48
+ def complete?
49
+ @complete
50
+ end
51
+
52
+ # The union when every absorbed result carried a map, nil otherwise. Dropping
53
+ # warns when the merge mixed results with and without maps, since that
54
+ # usually means `track_tests` is on in some workers and not others; a merge
55
+ # where nothing recorded stays quiet.
56
+ def map
57
+ return @map if @complete
58
+
59
+ warn_about_partial_maps
60
+ end
61
+
62
+ protected
63
+
64
+ # Protected: only another union folding this one in may read it.
65
+ def partial_map
66
+ @map
67
+ end
68
+
69
+ private
70
+
71
+ def warn_about_partial_maps
72
+ return unless @carrying.positive? && SimpleCov.print_errors
73
+
74
+ warn "[SimpleCov]: Dropped the per-test map from the merged result: " \
75
+ "only #{@carrying} of #{@entries} merged results carry one. The map is " \
76
+ "kept only when every merged result records it, so enable `track_tests` " \
77
+ "in every suite that should contribute."
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,187 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleCov
4
+ #
5
+ # Records which context covered which line: a list of context ids and, per
6
+ # source file, a bitmap of covered lines for each context that touched the
7
+ # file. A context is any labeled region of execution, and the vocabulary is
8
+ # deliberately the general one (coverage.py calls the same idea dynamic
9
+ # contexts), so the stored format doesn't bake simplecov's current use into
10
+ # a name.
11
+ #
12
+ # The naive shape of this data, a list of ids on every line, is
13
+ # O(contexts x lines) strings. Context ids are interned instead, and each
14
+ # context's covered lines within a file are a single Integer bitmap with bit
15
+ # N set when line N+1 was executed, which packs a thousand-line file into
16
+ # ~125 bytes and makes the union of two recordings a bitwise OR.
17
+ #
18
+ # Serialized into `.resultset.json` as `{"version" => 1, "contexts" =>
19
+ # [...ids...], "files" => {path => {context index => bitmap as hex}}}`. The
20
+ # format is tolerated, not trusted: `.from_hash` returns nil for anything
21
+ # malformed or future-versioned, which the merge treats the same as a map
22
+ # that was never recorded.
23
+ #
24
+ class ContextMap
25
+ # Bumped on any change an older reader could misread, so `.from_hash` can
26
+ # treat a future format as absent instead of answering from it wrongly.
27
+ VERSION = 1
28
+
29
+ def initialize
30
+ @contexts = [] #: Array[String]
31
+ @indices = {} #: Hash[String, Integer]
32
+ @files = {} #: Hash[String, Hash[Integer, Integer]]
33
+ end
34
+
35
+ # `lines_by_file` maps a source path to the bitmap of lines the context
36
+ # executed there. The id is interned even when the delta is empty: the
37
+ # context ran, and keeping it distinguishes "covered nothing of its own"
38
+ # from "never recorded".
39
+ def record(context_id, lines_by_file)
40
+ index = intern(context_id)
41
+ lines_by_file.each do |path, bitmap|
42
+ next if bitmap.zero?
43
+
44
+ table = (@files[path] ||= {})
45
+ table[index] = (table[index] || 0) | bitmap
46
+ end
47
+ self
48
+ end
49
+
50
+ # Sorted for stable output, since recording order varies between runs and
51
+ # merges. The path is resolved against `SimpleCov.root`, so callers can
52
+ # pass either an absolute or a project-relative one.
53
+ #
54
+ # A line number below 1 needs no guard of its own: it shifts the probe bit
55
+ # right off the end of the bitmap, leaving a mask no line can match.
56
+ def covering(path, line)
57
+ table = @files[File.expand_path(path, SimpleCov.root)]
58
+ return [] unless table
59
+
60
+ bit = 1 << (line - 1)
61
+ table.filter_map { |index, bitmap| @contexts.fetch(index) if bitmap.anybits?(bit) }.sort
62
+ end
63
+
64
+ def contexts
65
+ @contexts.dup
66
+ end
67
+
68
+ def empty?
69
+ @contexts.empty?
70
+ end
71
+
72
+ # Other's contexts are re-interned, so maps recorded by different processes
73
+ # merge by id, and a context both sides saw contributes one entry.
74
+ def absorb(other)
75
+ remap = other.interned_contexts.map { |context_id| intern(context_id) }
76
+ other.file_tables.each do |path, table|
77
+ target = (@files[path] ||= {})
78
+ table.each do |index, bitmap|
79
+ key = remap.fetch(index)
80
+ target[key] = (target[key] || 0) | bitmap
81
+ end
82
+ end
83
+ self
84
+ end
85
+
86
+ # `only:` restricts the files to a given set of paths, so the map follows
87
+ # the same universe as the coverage it sits beside. The context list is
88
+ # never restricted: which contexts ran is true regardless of which files
89
+ # survived filtering.
90
+ def to_h(only: nil)
91
+ # Selected rather than sliced: `only` is a Set, and splatting one into
92
+ # `slice` is a call `to_a` answers for, which makes dropping the `to_a` a
93
+ # mutation nothing can observe.
94
+ tables = only ? @files.select { |path, _table| only.include?(path) } : @files # rubocop:disable Style/HashSlice
95
+ serialized = tables.transform_values { |table| serialize_table(table) }
96
+ {"version" => VERSION, "contexts" => @contexts.dup, "files" => serialized}
97
+ end
98
+
99
+ # Index string => hex bitmap, the wire encoding `to_h` writes.
100
+ # coverage.json shares the resultset's encoding through this, so the format
101
+ # has one owner.
102
+ def serialized_bitmaps_for(path)
103
+ serialize_table(@files[File.expand_path(path, SimpleCov.root)] || {})
104
+ end
105
+
106
+ # @api private
107
+ def intern(context_id)
108
+ @indices[context_id] ||= begin
109
+ @contexts << context_id
110
+ @contexts.size - 1
111
+ end
112
+ end
113
+
114
+ class << self
115
+ # nil when the data is not a well-formed map of this format version.
116
+ # All-or-nothing on purpose: a partially salvaged map would answer
117
+ # `covering` queries with silent gaps, and the merge already drops an
118
+ # absent map consistently.
119
+ def from_hash(data)
120
+ # `eql?` rather than `==`: a version written as 1.0 is not this format,
121
+ # and reading it as if it were is what the version gate exists to stop.
122
+ return nil unless data.instance_of?(Hash) && data["version"].eql?(VERSION)
123
+
124
+ contexts = data["contexts"]
125
+ files = data["files"]
126
+ return nil unless contexts.instance_of?(Array) && contexts.all?(String) && files.instance_of?(Hash)
127
+
128
+ build(contexts, files)
129
+ end
130
+
131
+ private
132
+
133
+ def build(contexts, files)
134
+ map = new
135
+ contexts.each { |context_id| map.intern(context_id) }
136
+ files.each do |path, table|
137
+ return nil unless path.instance_of?(String) && table.instance_of?(Hash)
138
+ return nil unless absorb_table(map, contexts, path, table)
139
+ end
140
+ map
141
+ end
142
+
143
+ def absorb_table(map, contexts, path, table)
144
+ table.all? do |index, encoded|
145
+ position = parse_index(index, contexts.size)
146
+ bitmap = parse_bitmap(encoded)
147
+ map.record(contexts.fetch(position), path => bitmap) if position && bitmap
148
+ end
149
+ end
150
+
151
+ # A serialized context index is a decimal string pointing into the entry's
152
+ # own context list. An out-of-range one makes the whole map malformed.
153
+ def parse_index(index, size)
154
+ return nil unless index.instance_of?(String) && index.match?(/\A\d+\z/)
155
+
156
+ value = index.to_i
157
+ value if value < size
158
+ end
159
+
160
+ def parse_bitmap(encoded)
161
+ return nil unless encoded.instance_of?(String) && encoded.match?(/\A\h+\z/)
162
+
163
+ encoded.to_i(16)
164
+ end
165
+ end
166
+
167
+ private
168
+
169
+ # Sorted by index: recording and merge order vary run to run, and a
170
+ # deterministic serialization keeps stored resultsets diffable.
171
+ def serialize_table(table)
172
+ table.sort.to_h { |index, bitmap| [index.to_s, bitmap.to_s(16)] }
173
+ end
174
+
175
+ protected
176
+
177
+ # Live state, not copies, which is why these are protected rather than
178
+ # public.
179
+ def interned_contexts
180
+ @contexts
181
+ end
182
+
183
+ def file_tables
184
+ @files
185
+ end
186
+ end
187
+ end
@@ -3,9 +3,6 @@
3
3
  require "json"
4
4
 
5
5
  module SimpleCov
6
- # Shared parser for coverage.json consumers. It enforces the encoding and
7
- # stable outermost shape while leaving command- or viewer-specific fields to
8
- # their respective callers.
9
6
  module CoverageJSON
10
7
  class Error < StandardError; end
11
8
 
@@ -14,11 +11,11 @@ module SimpleCov
14
11
  raise Error, "input is not valid UTF-8" unless source.valid_encoding?
15
12
 
16
13
  document = JSON.parse(source)
17
- return document if document.is_a?(Hash)
14
+ return document if document.instance_of?(Hash)
18
15
 
19
16
  raise Error, "top-level value must be an object"
20
17
  rescue JSON::ParserError => e
21
- raise Error, e.message
18
+ raise Error, e
22
19
  end
23
20
  end
24
21
  end
@@ -1,22 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SimpleCov
4
- # Holds the individual data of a coverage result.
5
- #
6
- # This is uniform across coverage criteria as they all have:
7
- #
8
- # * total - how many things to cover there are (total relevant loc/branches)
9
- # * covered - how many of the coverables are hit
10
- # * missed - how many of the coverables are missed
11
- # * omitted - how many lines cannot be covered (blank lines/comments); only meaningful for line coverage
12
- # * percent - percentage as covered/missed
13
- # * strength - average hits per coverable unit (will not exist for one shot lines format)
4
+ # One coverage criterion's numbers, uniform across criteria: total, covered,
5
+ # missed, omitted (only meaningful for line coverage), percent, and strength,
6
+ # the average hits per coverable unit, which the oneshot-lines format has no
7
+ # way to produce.
14
8
  class CoverageStatistics
15
9
  attr_reader :total, :covered, :missed, :omitted, :strength, :percent
16
10
 
17
- # Seed for the reduce in `.from`: covered, missed, omitted, strength.
18
11
  ZERO_STATS = [0, 0, 0, 0.0].freeze #: [Integer, Integer, Integer, Float]
19
12
 
13
+ # Strength is remultiplied by loc because files have different strengths and
14
+ # line counts, giving them a different weight in the total.
20
15
  def self.from(coverage_statistics)
21
16
  sum_covered, sum_missed, sum_omitted, sum_total_strength =
22
17
  coverage_statistics.reduce(ZERO_STATS) do |(covered, missed, omitted, total_strength), stats|
@@ -24,8 +19,6 @@ module SimpleCov
24
19
  covered + stats.covered,
25
20
  missed + stats.missed,
26
21
  omitted + stats.omitted,
27
- # gotta remultiply with loc because files have different strength and loc
28
- # giving them a different "weight" in total
29
22
  total_strength + (stats.strength * stats.total)
30
23
  ]
31
24
  end
@@ -33,19 +26,16 @@ module SimpleCov
33
26
  new(covered: sum_covered, missed: sum_missed, omitted: sum_omitted, total_strength: sum_total_strength)
34
27
  end
35
28
 
36
- # Requires only covered, missed and strength to be initialized.
37
- #
38
- # Other values are computed by this class.
39
- def initialize(covered:, missed:, omitted: 0, total_strength: 0.0, percent: nil)
40
- @covered = covered
41
- @missed = missed
42
- @omitted = omitted
43
- @total = covered + missed
44
- @percent = percent || compute_percent(covered, missed, total)
29
+ def initialize(covered:, missed:, omitted: 0, total_strength: 0, percent: nil)
30
+ @covered = covered
31
+ @missed = missed
32
+ @omitted = omitted
33
+ @total = covered + missed
34
+ @percent = percent || compute_percent(covered, missed, total)
45
35
  @strength = compute_strength(total_strength, total)
46
36
  end
47
37
 
48
- private
38
+ private
49
39
 
50
40
  def compute_percent(covered, missed, total)
51
41
  return 100.0 if missed.zero?
@@ -56,7 +46,7 @@ module SimpleCov
56
46
  def compute_strength(total_strength, total)
57
47
  return 0.0 if total.zero?
58
48
 
59
- total_strength.to_f / total
49
+ total_strength.fdiv(total)
60
50
  end
61
51
  end
62
52
  end