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
@@ -2,27 +2,21 @@
2
2
 
3
3
  module SimpleCov
4
4
  module Formatter
5
- # Wraps multiple formatters so SimpleCov.formatter can drive several
6
- # output formats (HTML + JSON, etc.) in a single run.
7
5
  class MultiFormatter
8
- # Shared `#format` implementation; included into individual
9
- # MultiFormatter subclasses built by `MultiFormatter.new`.
10
6
  module InstanceMethods
11
7
  def format(result)
12
8
  formatters.map do |formatter|
13
9
  Formatter.format(formatter, result)
14
- rescue StandardError => e
15
- warn("Formatter #{formatter} failed with #{e.class}: #{e.message} (#{(_ = e.backtrace).first})")
16
- nil
10
+ rescue => e
11
+ warn("Formatter #{formatter} failed with #{e.class}: #{e} (#{(_ = e.backtrace).first})")
17
12
  end
18
13
  end
19
14
  end
20
15
 
16
+ # Normalized eagerly and captured in the closure. `Array()` is pure for every
17
+ # accepted input shape, so this is equivalent to the historical lazy
18
+ # per-instance memoization.
21
19
  def self.new(formatters = nil)
22
- # Normalize eagerly and capture the list in the closure. Array()
23
- # is pure for every accepted input shape (nil, single formatter,
24
- # array, or another MultiFormatter class), so this is equivalent
25
- # to the historical lazy per-instance memoization.
26
20
  formatter_list = Array(formatters)
27
21
  Class.new do
28
22
  define_method :formatters do
@@ -2,22 +2,16 @@
2
2
 
3
3
  module SimpleCov
4
4
  module Formatter
5
- #
6
- # A ridiculously simple formatter for SimpleCov results.
7
- #
8
5
  class SimpleFormatter
9
- # Takes a SimpleCov::Result and generates a string out of it
10
6
  def format(result)
11
7
  criterion = SimpleCov.coverage_statistics_key(SimpleCov.primary_coverage)
12
8
  result.groups.map { |name, files| format_group(name, files, criterion) }.join
13
9
  end
14
10
 
15
- private
11
+ private
16
12
 
17
13
  def format_group(name, files, criterion)
18
- header = "Group: #{name}\n#{'=' * 40}\n"
19
- # The configured primary criterion is enabled, so every result file
20
- # carries its corresponding statistic despite the nilable public API.
14
+ header = "Group: #{name}\n#{"=" * 40}\n"
21
15
  body = files.map do |file|
22
16
  "#{file.filename} (coverage: #{(_ = file.covered_percent(criterion)).floor(2)}%)\n"
23
17
  end.join
@@ -1,20 +1,23 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SimpleCov
4
- # Namespace for SimpleCov result formatters. Built-in formatters live
5
- # below this module; custom formatters should respond to `#format(result)`
6
- # and can be wired up via `SimpleCov.formatter=`.
7
- # TODO: Documentation on how to build your own formatters
8
4
  module Formatter
9
- # Formatters can be configured either as classes (instantiated
10
- # fresh for every report) or as ready-built instances the only
11
- # way to reach constructor options like
12
- # `HTMLFormatter.new(silent: true)`. See #1240.
5
+ # Formatters are configured either as classes, instantiated fresh for every
6
+ # report, or as ready-built instances, the only way to reach constructor
7
+ # options like `HTMLFormatter.new(silent: true)` (#1240).
13
8
  def self.instance_for(formatter)
14
- formatter.is_a?(Class) ? formatter.new : formatter
9
+ instantiable?(formatter) ? formatter.new : formatter
15
10
  end
16
11
 
17
- # Normalize a class or instance, then dispatch the result to it.
12
+ # mutant:disable
13
+ # `Class` cannot itself be subclassed, so `instance_of?(Class)` answers for
14
+ # every object exactly as `is_a?(Class)` does: a difference no example can
15
+ # show, which is why it is disabled here rather than pinned by a test.
16
+ def self.instantiable?(formatter)
17
+ formatter.is_a?(Class)
18
+ end
19
+ private_class_method :instantiable?
20
+
18
21
  def self.format(formatter, result)
19
22
  instance_for(formatter).format(result)
20
23
  end
@@ -24,3 +27,4 @@ end
24
27
  require_relative "formatter/simple_formatter"
25
28
  require_relative "formatter/multi_formatter"
26
29
  require_relative "formatter/json_formatter"
30
+ require_relative "formatter/baseline_formatter"
@@ -1,32 +1,33 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SimpleCov
4
- # Semantic names owned by result processing rather than user configuration.
5
4
  module GroupNames
6
5
  UNGROUPED = "Ungrouped"
7
6
 
8
- module_function
7
+ extend self
9
8
 
10
- # Group names are Hash keys, report labels, and JSON object keys all at
11
- # once, so they are normalized to Strings up front: a Symbol spelling
12
- # (`group :Controllers`) means the String, and anything else has no
13
- # sensible serialized form. Normalizing before `validate!` also keeps
14
- # `group :Ungrouped` from slipping past the reservation below.
9
+ # Group names are Hash keys, report labels, and JSON object keys all at once,
10
+ # so they are normalized to Strings up front: a Symbol spelling means the
11
+ # String, and anything else has no sensible serialized form. Normalizing
12
+ # before `validate!` also keeps `group :Ungrouped` from slipping past the
13
+ # reservation below.
15
14
  def normalize(group_name)
16
15
  case group_name
17
16
  when String then group_name
18
17
  when Symbol then group_name.to_s
19
18
  else
20
- raise SimpleCov::ConfigurationError,
21
- "Group names must be Strings, got #{group_name.inspect} (#{group_name.class})"
19
+ raise ConfigurationError,
20
+ "Group names must be Strings, got #{group_name.inspect} (#{group_name.class})"
22
21
  end
23
22
  end
24
23
 
24
+ # Each name in turn, rather than asking the collection: a String asked
25
+ # whether it "includes" the reserved name answers about its own characters.
25
26
  def validate!(group_names)
26
- return group_names unless group_names.include?(UNGROUPED)
27
+ return group_names unless group_names.any? { |name| name.eql?(UNGROUPED) }
27
28
 
28
- raise SimpleCov::ConfigurationError,
29
- "#{UNGROUPED.inspect} is reserved for files that do not match a configured group"
29
+ raise ConfigurationError,
30
+ "#{UNGROUPED.inspect} is reserved for files that do not match a configured group"
30
31
  end
31
32
  end
32
33
  end
@@ -0,0 +1,115 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "open3"
5
+ require "time"
6
+ require_relative "atomic_file"
7
+
8
+ module SimpleCov
9
+ # A bounded record of past runs, coverage/.history.json (#1267): one entry
10
+ # per reported run carrying the per-criterion totals, the per-group and
11
+ # per-file percentages, a timestamp, and the branch and commit when the
12
+ # project is a git checkout. It is what turns "did the number move since the
13
+ # very last run" into direction of travel.
14
+ #
15
+ # Entries are capped and written atomically, and a corrupt or foreign file
16
+ # degrades to an empty history with a warning rather than crashing the
17
+ # at_exit hook, the same tolerance `.last_run.json` has. The file is
18
+ # deliberately plain committable JSON.
19
+ module History
20
+ ENVELOPE = "simplecov_history"
21
+ FORMAT_VERSION = 1
22
+
23
+ class << self
24
+ def history_path
25
+ File.join(SimpleCov.coverage_path, ".history.json")
26
+ end
27
+
28
+ def record(result)
29
+ limit = SimpleCov.history_limit
30
+ return if limit.zero?
31
+
32
+ entries = read
33
+ entries << entry_for(result)
34
+ write(entries.last(limit))
35
+ end
36
+
37
+ def read
38
+ return [] unless File.exist?(history_path)
39
+
40
+ content = File.read(history_path)
41
+ return [] if content.match?(/\A\s*\z/)
42
+
43
+ entries = JSON.parse(content).dig(ENVELOPE, "entries")
44
+ entries.instance_of?(Array) ? entries : invalid_history
45
+ rescue JSON::ParserError
46
+ invalid_history
47
+ end
48
+
49
+ # The recorded entries plus `result` as the newest, capped, and without
50
+ # writing anything: what the report artifacts embed, so the embedded history
51
+ # includes the run being reported even though recording happens later.
52
+ def entries_with(result)
53
+ (read << entry_for(result)).last(SimpleCov.history_limit)
54
+ end
55
+
56
+ # A detached HEAD reports its branch as nil rather than the literal "HEAD"
57
+ # most CI checkouts would record.
58
+ def git_info
59
+ branch = git("rev-parse", "--abbrev-ref", "HEAD")
60
+ branch = nil if branch.eql?("HEAD")
61
+ [branch, git("rev-parse", "HEAD")]
62
+ end
63
+
64
+ private
65
+
66
+ def entry_for(result)
67
+ branch, commit = git_info
68
+ {
69
+ # getutc, not utc: the destructive variant would rezone the Time the result
70
+ # still holds.
71
+ "created_at" => result.created_at.getutc.iso8601,
72
+ "branch" => branch,
73
+ "commit" => commit,
74
+ "totals" => measured_percents(result),
75
+ "groups" => result.groups.transform_values { |files| measured_percents(files) },
76
+ "files" => files_percents(result)
77
+ }
78
+ end
79
+
80
+ def measured_percents(stats_source)
81
+ stats_source.coverage_statistics.to_h do |criterion, stats|
82
+ [criterion.to_s, SimpleCov.round_coverage(stats.percent)]
83
+ end
84
+ end
85
+
86
+ def files_percents(result)
87
+ measured = result.coverage_statistics
88
+ initial = {} #: Hash[String, Hash[String, Numeric]]
89
+ result.files.each_with_object(initial) do |file, percents|
90
+ stats = file.coverage_statistics
91
+ percents[file.project_filename] = measured.to_h do |criterion, _run_total|
92
+ [criterion.to_s, SimpleCov.round_coverage(stats.fetch(criterion).percent)]
93
+ end
94
+ end
95
+ end
96
+
97
+ def write(entries)
98
+ payload = {ENVELOPE => {"format_version" => FORMAT_VERSION, "entries" => entries}}
99
+ AtomicFile.write(history_path, "#{JSON.pretty_generate(payload)}\n")
100
+ end
101
+
102
+ def invalid_history
103
+ warn "[SimpleCov]: Warning! Parsing JSON content of .history.json failed, starting a fresh history"
104
+ []
105
+ end
106
+
107
+ def git(*)
108
+ output, status = Open3.capture2e("git", "-C", SimpleCov.root.to_s, *)
109
+ output.rstrip if status.success?
110
+ rescue
111
+ nil
112
+ end
113
+ end
114
+ end
115
+ end
@@ -4,25 +4,23 @@ require "json"
4
4
  require_relative "atomic_file"
5
5
 
6
6
  module SimpleCov
7
- # Reads and writes coverage/.last_run.json — the previous run's coverage
8
- # percentages used by MaximumCoverageDropCheck.
9
7
  module LastRun
10
8
  class << self
11
9
  def last_run_path
12
10
  File.join(SimpleCov.coverage_path, ".last_run.json")
13
11
  end
14
12
 
13
+ # The maximum_coverage_drop check digs into a Hash, so anything else in a
14
+ # corrupt or hand-edited file counts as "no previous run" rather than
15
+ # crashing the at_exit hook.
15
16
  def read
16
17
  return nil unless File.exist?(last_run_path)
17
18
 
18
19
  json = File.read(last_run_path)
19
- return nil if json.strip.empty?
20
+ return nil if json.match?(/\A\s*\z/)
20
21
 
21
22
  parsed = JSON.parse(json, symbolize_names: true)
22
- # The maximum_coverage_drop check digs into a Hash, so anything
23
- # else in a corrupt or hand-edited file counts as "no previous
24
- # run" rather than crashing the at_exit hook.
25
- parsed.is_a?(Hash) ? parsed : invalid_last_run
23
+ parsed.instance_of?(Hash) ? parsed : invalid_last_run
26
24
  rescue JSON::ParserError
27
25
  invalid_last_run
28
26
  end
@@ -31,11 +29,10 @@ module SimpleCov
31
29
  AtomicFile.write(last_run_path, "#{JSON.pretty_generate(json)}\n")
32
30
  end
33
31
 
34
- private
32
+ private
35
33
 
36
34
  def invalid_last_run
37
35
  warn "[SimpleCov]: Warning! Parsing JSON content of .last_run.json failed, ignoring the previous run"
38
- nil
39
36
  end
40
37
  end
41
38
  end
@@ -3,8 +3,8 @@
3
3
  require_relative "directive"
4
4
 
5
5
  module SimpleCov
6
- # Classifies whether lines are relevant for code coverage analysis.
7
- # Comments & whitespace lines, and :nocov: token blocks, are considered not relevant.
6
+ # Classifies whether lines are relevant for code coverage analysis. Comments,
7
+ # whitespace lines, and disabled blocks are considered not relevant.
8
8
  class LinesClassifier
9
9
  RELEVANT = 0
10
10
  NOT_RELEVANT = nil
@@ -27,45 +27,60 @@ module SimpleCov
27
27
  /^(\s*)#(\s*)(:#{SimpleCov.current_nocov_token}:)/o
28
28
  end
29
29
 
30
+ # A marker is itself a comment, so the cheap whitespace-or-comment test gates
31
+ # the token match: a line of real code cannot be a marker, and no longer pays
32
+ # to be checked against one. That matters because this runs per line of every
33
+ # tracked-but-unloaded file, once in every process of a parallel run.
30
34
  def self.no_cov_line?(line)
31
35
  no_cov_line.match?(line)
32
36
  rescue ArgumentError
33
- # E.g., line contains an invalid byte sequence in UTF-8
34
37
  false
35
38
  end
36
39
 
37
40
  def self.whitespace_line?(line)
38
41
  WHITESPACE_OR_COMMENT_LINE.match?(line)
39
42
  rescue ArgumentError
40
- # E.g., line contains an invalid byte sequence in UTF-8
41
43
  false
42
44
  end
43
45
 
46
+ class SkipState
47
+ def initialize
48
+ @skipping = false
49
+ end
50
+
51
+ def skipping?
52
+ @skipping
53
+ end
54
+
55
+ def toggle
56
+ @skipping = !@skipping
57
+ end
58
+ end
59
+
60
+ # The skip state lives in a one-slot box owned by this call rather than in an
61
+ # ivar, so one classifier instance can serve concurrent `classify` calls
62
+ # without the toggles interleaving.
44
63
  def classify(lines)
45
64
  lines = lines.to_a
46
65
  directive_disabled = directive_disabled_line_set(lines)
47
- # The `:nocov:` skip state lives in a one-slot box owned by this
48
- # call, not in an ivar, so one classifier instance can serve
49
- # concurrent `classify` calls without the toggles interleaving.
50
- skip_state = [false]
66
+ skip_state = SkipState.new
51
67
 
52
68
  lines.map.with_index(1) do |line, line_number|
53
69
  classify_line(line, line_number, directive_disabled, skip_state)
54
70
  end
55
71
  end
56
72
 
57
- private
73
+ private
58
74
 
59
- # A `:nocov:` marker is itself a comment, so the cheap
60
- # whitespace-or-comment test can gate the token match: a line of real
61
- # code cannot be a marker, and no longer pays to be checked against
62
- # one. That matters because this runs per line of every
63
- # tracked-but-unloaded file, once in every process of a parallel run.
75
+ # mutant:disable `NOT_RELEVANT` is nil, so naming it and writing
76
+ # the literal are the same value, and so is an `unless` that falls
77
+ # off its end. The name is kept because it says which of the two
78
+ # verdicts a line got, which nil alone does not.
64
79
  def classify_line(line, line_number, directive_disabled, skip_state)
65
80
  if self.class.whitespace_line?(line)
66
- skip_state[0] = !skip_state[0] if self.class.no_cov_line?(line)
81
+ skip_state.toggle if self.class.no_cov_line?(line)
67
82
  NOT_RELEVANT
68
- elsif skip_state[0] || directive_disabled.include?(line_number)
83
+ elsif skip_state.skipping? || directive_disabled.include?(line_number)
69
84
  NOT_RELEVANT
70
85
  else
71
86
  RELEVANT
@@ -1,12 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # `~/.simplecov` was historically resolved via a three-step fallback chain
4
- # (HOME, then `Etc.getpwuid.dir`, then `~$USER`) for hostile container
5
- # environments circa 2017. Modern CRuby/JRuby/TruffleRuby all set HOME
6
- # reliably, so trust it and skip silently when it isn't there. An ENV
7
- # check rather than Dir.home: JRuby raises from Dir.home with HOME
8
- # unset, and `File.expand_path("~")` raises for a set-but-empty HOME
9
- # (some container/CI images), which must not break `require "simplecov"`.
3
+ # `~/.simplecov` was historically resolved through a three-step fallback chain
4
+ # for hostile container environments circa 2017. Modern CRuby and JRuby both set
5
+ # HOME reliably, so trust it and skip silently when it isn't there. An ENV check
6
+ # rather than `Dir.home`: JRuby raises from `Dir.home` with HOME unset, and
7
+ # `File.expand_path("~")` raises for a set-but-empty HOME, which must not break
8
+ # `require "simplecov"`.
9
+
10
10
  unless ENV.fetch("HOME", "").empty?
11
11
  # simplecov:disable — only fires when ~/.simplecov exists, which is
12
12
  # developer-machine-dependent (we can't rely on it for the dogfood).
@@ -3,73 +3,45 @@
3
3
  module SimpleCov
4
4
  module ParallelAdapters
5
5
  # Default no-op implementations for a parallel-test-runner adapter.
6
- # Real adapters subclass and override what they need; everything else
7
- # falls back to "behave like a single-process run."
8
- #
9
- # Adapters are classes (used as singletons, never instantiated) — they
10
- # answer a small fixed set of questions about whether THIS worker
11
- # process is the one that should do final-result work, and provide an
12
- # optional hook for waiting on sibling workers.
13
- #
14
- # @see SimpleCov::ParallelAdapters for the registry and selection.
6
+ # Everything not overridden falls back to behaving like a single-process
7
+ # run. Adapters are classes, used as singletons and never instantiated.
15
8
  class Base
16
9
  class << self
17
- # Should this adapter be selected for the current process? Adapters
18
- # are tried in registration order; the first one whose `active?`
19
- # returns true is chosen. Inactive adapters return `false`.
20
10
  def active?
21
11
  false
22
12
  end
23
13
 
24
- # Among the parallel workers in this run, should THIS worker do
25
- # the final-result work (wait for siblings, merge resultsets,
26
- # run threshold checks, format the report)? Default is `true`
27
- # for the single-process case.
28
14
  def first_worker?
29
15
  true
30
16
  end
31
17
 
32
- # Optional: block until sibling workers have finished writing
33
- # their resultsets. An adapter that wraps a parallel-test runner
34
- # with a native synchronization primitive (e.g., `parallel_tests`'s
35
- # `wait_for_other_processes_to_finish`) implements this for
36
- # lower latency; otherwise SimpleCov polls the resultset cache
37
- # as a fallback (see `SimpleCov.wait_for_parallel_results`).
38
18
  def wait_for_siblings
39
- # No-op default; polling fallback handles correctness.
40
19
  end
41
20
 
42
- # Does `wait_for_siblings` block until every sibling PROCESS has
43
- # exited (so no further resultset can appear)? When true, the
44
- # reporting worker can accept a settled resultset count below
45
- # `expected_worker_count` as final instead of waiting out the whole
46
- # `parallel_wait_timeout` for workers that produced no coverage.
47
- # Defaults to false (no native wait; the poll is the only signal).
21
+ # Whether `wait_for_siblings` blocks until every sibling process has exited,
22
+ # so no further resultset can appear. When true, the reporting worker can
23
+ # accept a settled resultset count below `expected_worker_count` as final
24
+ # instead of waiting out the whole `parallel_wait_timeout`.
48
25
  def native_wait?
49
26
  false
50
27
  end
51
28
 
52
- # How many parallel workers are participating in this run. Used
53
- # by the polling fallback to know how many resultset entries to
54
- # expect. Defaults to 1 (single-process).
55
29
  def expected_worker_count
56
30
  1
57
31
  end
58
32
 
59
- # The user's explicit `SimpleCov.parallel_tests false` opt-out,
60
- # which every adapter's `active?` honors before its own
61
- # detection.
33
+ # mutant:disable `false` is a singleton, so comparing against
34
+ # it answers the same through ==, eql? and equal?, and no
35
+ # example can tell the three apart.
62
36
  def forced_off?
63
37
  SimpleCov.parallel_tests == false
64
38
  end
65
39
 
66
- # Shared implementation of the PARALLEL_TEST_GROUPS convention
67
- # both built-in adapters follow for `expected_worker_count`.
68
- # Unset, empty, non-numeric, and non-positive values all mean 1:
69
- # an unparseable value must not yield 0 workers, which would end
70
- # the sibling wait before it started.
40
+ # Unset, empty, non-numeric, and non-positive values all mean 1: an
41
+ # unparseable value must not yield 0 workers, which would end the sibling
42
+ # wait before it started.
71
43
  def parallel_test_groups_count
72
- count = Integer(ENV.fetch("PARALLEL_TEST_GROUPS", ""), exception: false)
44
+ count = Integer(ENV.fetch("PARALLEL_TEST_GROUPS", nil), exception: false)
73
45
  count&.positive? ? count : 1
74
46
  end
75
47
  end
@@ -5,21 +5,10 @@ require_relative "base"
5
5
  module SimpleCov
6
6
  module ParallelAdapters
7
7
  # Catch-all adapter for parallel test runners that follow the
8
- # `TEST_ENV_NUMBER` / `PARALLEL_TEST_GROUPS` env-var convention but
9
- # don't ship a Ruby API for SimpleCov to hook (parallel_rspec,
10
- # knapsack-style splitters, custom CI sharding scripts). Activates
11
- # when `TEST_ENV_NUMBER` is set; doesn't require any specific gem to
12
- # be loaded.
13
- #
14
- # Heuristic for `first_worker?`: the worker whose `TEST_ENV_NUMBER`
15
- # is `""` (parallel_tests/parallel_rspec convention) or `"1"`
16
- # (zero-based runners that start at 1). Any other value is treated
17
- # as a non-first worker.
18
- #
19
- # `wait_for_siblings` is inherited from Base as a no-op — without a
20
- # runner-provided API the only synchronization available is polling
21
- # the resultset cache, which `SimpleCov.wait_for_parallel_results`
22
- # does after the no-op returns.
8
+ # `TEST_ENV_NUMBER` / `PARALLEL_TEST_GROUPS` env-var convention but ship no
9
+ # Ruby API for SimpleCov to hook. `wait_for_siblings` is inherited from Base
10
+ # as a no-op: without a runner-provided API the only synchronization
11
+ # available is polling the resultset cache.
23
12
  class GenericAdapter < Base
24
13
  class << self
25
14
  def active?
@@ -28,9 +17,9 @@ module SimpleCov
28
17
  ENV.key?("TEST_ENV_NUMBER")
29
18
  end
30
19
 
31
- # parallel_tests sets the first worker's TEST_ENV_NUMBER to "";
32
- # parallel_rspec inherits that. Runners that number from 1 use
33
- # "1" for the first worker. Both shapes match.
20
+ # parallel_tests sets the first worker's TEST_ENV_NUMBER to "", and runners
21
+ # that number from 1 use "1". Both shapes match; any other value is a
22
+ # non-first worker.
34
23
  def first_worker?
35
24
  ["", "1"].include?(ENV.fetch("TEST_ENV_NUMBER", nil))
36
25
  end
@@ -4,47 +4,37 @@ require_relative "base"
4
4
 
5
5
  module SimpleCov
6
6
  module ParallelAdapters
7
- # Adapter for [grosser/parallel_tests](https://github.com/grosser/parallel_tests).
8
- # This is the historical default SimpleCov has special-cased
9
- # parallel_tests since 0.18 — and remains the most precise option for
10
- # projects on it. Detection requires the full native coordination
11
- # contract: the `ParallelTests` constant has been loaded,
12
- # `TEST_ENV_NUMBER` is set, and `PARALLEL_PID_FILE` is set. The pid-file
13
- # path is required because the native wait API reads it with `ENV.fetch`.
14
- # When a runner only provides the env-var convention, GenericAdapter is
15
- # the correct coordination path.
7
+ # Adapter for grosser/parallel_tests, the historical default and still the
8
+ # most precise option for projects on it. Detection requires the full native
9
+ # coordination contract: the `ParallelTests` constant loaded,
10
+ # `TEST_ENV_NUMBER` set, and `PARALLEL_PID_FILE` set, because the native
11
+ # wait API reads the pid-file path with `ENV.fetch`. A runner that only
12
+ # provides the env-var convention belongs to GenericAdapter.
16
13
  class ParallelTestsAdapter < Base
17
14
  class << self
18
15
  def active?
19
16
  return false if forced_off?
20
17
 
21
18
  ensure_loaded
22
- # !! to coerce `defined?` (returns nil or "constant") to a proper bool.
23
19
  !!(defined?(::ParallelTests) && native_parallel_tests_environment?)
24
20
  end
25
21
 
26
- # Pick the *first* started process to do the final-result work,
27
- # not the last. The parallel_tests README recommends
28
- # `first_process?` for "do something once after every worker
29
- # finishes" hooks, so user code that has its own
30
- # `wait_for_other_processes_to_finish` in an `RSpec.after(:suite)`
31
- # overwhelmingly waits in the first process — picking the same
32
- # side avoids the cross-process deadlock #922 reported. Also
33
- # handles `PARALLEL_TEST_GROUPS=1` naturally (the only worker's
34
- # `TEST_ENV_NUMBER` is "" and `first_process?` tests for that
35
- # empty string).
22
+ # The first started process, not the last. The parallel_tests README
23
+ # recommends `first_process?` for "do something once after every worker
24
+ # finishes" hooks, so user code with its own
25
+ # `wait_for_other_processes_to_finish` overwhelmingly waits in the first
26
+ # process, and picking the same side avoids the cross-process deadlock #922
27
+ # reported.
36
28
  def first_worker?
37
- ::ParallelTests.first_process?
29
+ ParallelTests.first_process?
38
30
  end
39
31
 
40
32
  def wait_for_siblings
41
33
  return unless native_parallel_tests_environment?
42
34
 
43
- ::ParallelTests.wait_for_other_processes_to_finish
35
+ ParallelTests.wait_for_other_processes_to_finish
44
36
  end
45
37
 
46
- # The native wait blocks until every sibling process exits, but only
47
- # when the pid-file contract is present (see `wait_for_siblings`).
48
38
  def native_wait?
49
39
  native_parallel_tests_environment?
50
40
  end
@@ -53,30 +43,19 @@ module SimpleCov
53
43
  parallel_test_groups_count
54
44
  end
55
45
 
56
- # Auto-require `parallel_tests` when it's installed AND the env
57
- # vars it sets are present, so callers can rely on
58
- # `defined?(::ParallelTests)` downstream. parallel_tests is an
59
- # optional dependency (see https://github.com/grosser/parallel_tests/issues/772),
60
- # and `TEST_ENV_NUMBER` / `PARALLEL_TEST_GROUPS` are commonly set
61
- # for other reasons (custom subprocess coordination, CI sharding,
62
- # the parallel_rspec gem which intentionally mirrors the env-var
63
- # convention), so a missing gem is treated as "user isn't using
64
- # parallel_tests" — silently skip and let GenericAdapter handle
65
- # it. Users who want to override the auto-detect can set
66
- # `SimpleCov.parallel_tests true` (force on) or `false` (force
67
- # off). See #1018.
46
+ # parallel_tests is an optional dependency, and `TEST_ENV_NUMBER` /
47
+ # `PARALLEL_TEST_GROUPS` are commonly set for other reasons, so a missing
48
+ # gem is treated as "user isn't using parallel_tests": stay quiet and let
49
+ # GenericAdapter handle it. Warning here regressed users who use those env
50
+ # vars for their own subprocess coordination (#1018).
68
51
  def ensure_loaded
69
- return if defined?(::ParallelTests) # simplecov:disable — only true after a previous load
70
- return if SimpleCov.parallel_tests == false # simplecov:disable — only fires when user opts out
71
- # simplecov:disable — env-var-only path
52
+ return if defined?(::ParallelTests)
53
+ return if forced_off?
72
54
  return unless SimpleCov.parallel_tests || env_suggests_parallel_tests?
73
55
 
74
56
  # simplecov:disable — only fires under a real parallel_tests setup
75
57
  require "parallel_tests"
76
58
  rescue LoadError
77
- # Gem isn't installed; stay quiet — warning here regressed
78
- # users who use those env vars for their own subprocess
79
- # coordination.
80
59
  # simplecov:enable
81
60
  end
82
61