simplecov 0.22.0 → 1.3.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 (210) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +1 -1
  3. data/README.md +173 -875
  4. data/exe/simplecov +8 -0
  5. data/lib/minitest/simplecov_plugin.rb +18 -7
  6. data/lib/simplecov/atomic_file.rb +67 -0
  7. data/lib/simplecov/autostart.rb +9 -0
  8. data/lib/simplecov/baseline/parser.rb +58 -0
  9. data/lib/simplecov/baseline.rb +170 -0
  10. data/lib/simplecov/cli/affected/changed_files.rb +57 -0
  11. data/lib/simplecov/cli/affected/selection.rb +117 -0
  12. data/lib/simplecov/cli/affected.rb +148 -0
  13. data/lib/simplecov/cli/annotations.rb +166 -0
  14. data/lib/simplecov/cli/badge/svg.rb +70 -0
  15. data/lib/simplecov/cli/badge.rb +62 -0
  16. data/lib/simplecov/cli/clean.rb +86 -0
  17. data/lib/simplecov/cli/command_helpers.rb +91 -0
  18. data/lib/simplecov/cli/completions/scripts.rb +114 -0
  19. data/lib/simplecov/cli/completions.rb +83 -0
  20. data/lib/simplecov/cli/coverage.rb +83 -0
  21. data/lib/simplecov/cli/coverage_file.rb +109 -0
  22. data/lib/simplecov/cli/dead_code/output.rb +100 -0
  23. data/lib/simplecov/cli/dead_code.rb +139 -0
  24. data/lib/simplecov/cli/diff/output.rb +45 -0
  25. data/lib/simplecov/cli/diff.rb +115 -0
  26. data/lib/simplecov/cli/dotfile.rb +156 -0
  27. data/lib/simplecov/cli/git.rb +41 -0
  28. data/lib/simplecov/cli/history/output.rb +135 -0
  29. data/lib/simplecov/cli/history.rb +67 -0
  30. data/lib/simplecov/cli/merge.rb +115 -0
  31. data/lib/simplecov/cli/open.rb +45 -0
  32. data/lib/simplecov/cli/patch/changed_lines.rb +149 -0
  33. data/lib/simplecov/cli/patch/output.rb +151 -0
  34. data/lib/simplecov/cli/patch.rb +175 -0
  35. data/lib/simplecov/cli/ratchet/output.rb +44 -0
  36. data/lib/simplecov/cli/ratchet.rb +83 -0
  37. data/lib/simplecov/cli/report.rb +91 -0
  38. data/lib/simplecov/cli/run.rb +35 -0
  39. data/lib/simplecov/cli/serve/report_preparer.rb +30 -0
  40. data/lib/simplecov/cli/serve/static_file_handler.rb +128 -0
  41. data/lib/simplecov/cli/serve.rb +88 -0
  42. data/lib/simplecov/cli/show/annotator.rb +87 -0
  43. data/lib/simplecov/cli/show/sweep.rb +36 -0
  44. data/lib/simplecov/cli/show.rb +133 -0
  45. data/lib/simplecov/cli/status/facts.rb +87 -0
  46. data/lib/simplecov/cli/status.rb +93 -0
  47. data/lib/simplecov/cli/tests/redundancy.rb +78 -0
  48. data/lib/simplecov/cli/tests.rb +148 -0
  49. data/lib/simplecov/cli/uncovered/misses.rb +33 -0
  50. data/lib/simplecov/cli/uncovered.rb +127 -0
  51. data/lib/simplecov/cli/usage.rb +153 -0
  52. data/lib/simplecov/cli/watch/live_report.rb +73 -0
  53. data/lib/simplecov/cli/watch/narrator.rb +55 -0
  54. data/lib/simplecov/cli/watch/poller.rb +45 -0
  55. data/lib/simplecov/cli/watch/session.rb +134 -0
  56. data/lib/simplecov/cli/watch/test_plan.rb +43 -0
  57. data/lib/simplecov/cli/watch.rb +85 -0
  58. data/lib/simplecov/cli.rb +114 -0
  59. data/lib/simplecov/color.rb +65 -0
  60. data/lib/simplecov/combine/branches_combiner.rb +44 -21
  61. data/lib/simplecov/combine/coverage_accumulator.rb +175 -0
  62. data/lib/simplecov/combine/identity_interner.rb +27 -0
  63. data/lib/simplecov/combine/interned_counts.rb +28 -0
  64. data/lib/simplecov/combine/lines_combiner.rb +35 -29
  65. data/lib/simplecov/combine/methods_combiner.rb +48 -0
  66. data/lib/simplecov/combine/results_combiner.rb +9 -49
  67. data/lib/simplecov/combine.rb +0 -24
  68. data/lib/simplecov/command_guesser.rb +99 -42
  69. data/lib/simplecov/configuration/baseline.rb +31 -0
  70. data/lib/simplecov/configuration/coverage.rb +184 -0
  71. data/lib/simplecov/configuration/coverage_criteria.rb +143 -0
  72. data/lib/simplecov/configuration/deprecations.rb +21 -0
  73. data/lib/simplecov/configuration/eval_coverage.rb +37 -0
  74. data/lib/simplecov/configuration/filters.rb +131 -0
  75. data/lib/simplecov/configuration/formatting.rb +129 -0
  76. data/lib/simplecov/configuration/groups.rb +48 -0
  77. data/lib/simplecov/configuration/history.rb +44 -0
  78. data/lib/simplecov/configuration/ignored_entries.rb +72 -0
  79. data/lib/simplecov/configuration/merging.rb +197 -0
  80. data/lib/simplecov/configuration/missed_caps.rb +51 -0
  81. data/lib/simplecov/configuration/production.rb +23 -0
  82. data/lib/simplecov/configuration/test_tracking.rb +48 -0
  83. data/lib/simplecov/configuration/thresholds.rb +150 -0
  84. data/lib/simplecov/configuration/view_coverage.rb +37 -0
  85. data/lib/simplecov/configuration.rb +100 -443
  86. data/lib/simplecov/configuration_error.rb +5 -0
  87. data/lib/simplecov/context_map/union.rb +81 -0
  88. data/lib/simplecov/context_map.rb +187 -0
  89. data/lib/simplecov/coverage_json.rb +21 -0
  90. data/lib/simplecov/coverage_statistics.rb +24 -28
  91. data/lib/simplecov/coverage_violations.rb +254 -0
  92. data/lib/simplecov/current_run.rb +61 -0
  93. data/lib/simplecov/defaults.rb +20 -28
  94. data/lib/simplecov/deprecation.rb +63 -0
  95. data/lib/simplecov/directive/erb.rb +44 -0
  96. data/lib/simplecov/directive/haml.rb +33 -0
  97. data/lib/simplecov/directive/indented_template.rb +37 -0
  98. data/lib/simplecov/directive/slim.rb +35 -0
  99. data/lib/simplecov/directive/template.rb +29 -0
  100. data/lib/simplecov/directive.rb +152 -0
  101. data/lib/simplecov/exit_codes/baseline_check.rb +46 -0
  102. data/lib/simplecov/exit_codes/check.rb +39 -0
  103. data/lib/simplecov/exit_codes/exit_code_handling.rb +24 -5
  104. data/lib/simplecov/exit_codes/maximum_coverage_drop_check.rb +17 -67
  105. data/lib/simplecov/exit_codes/maximum_missed_check.rb +26 -0
  106. data/lib/simplecov/exit_codes/maximum_missed_per_file_check.rb +34 -0
  107. data/lib/simplecov/exit_codes/maximum_overall_coverage_check.rb +27 -0
  108. data/lib/simplecov/exit_codes/minimum_coverage_by_file_check.rb +20 -38
  109. data/lib/simplecov/exit_codes/minimum_coverage_by_group_check.rb +28 -0
  110. data/lib/simplecov/exit_codes/minimum_overall_coverage_check.rb +37 -34
  111. data/lib/simplecov/exit_codes.rb +26 -0
  112. data/lib/simplecov/exit_handling.rb +163 -0
  113. data/lib/simplecov/file_list.rb +63 -48
  114. data/lib/simplecov/filter.rb +98 -42
  115. data/lib/simplecov/formatter/base.rb +100 -0
  116. data/lib/simplecov/formatter/baseline_formatter.rb +98 -0
  117. data/lib/simplecov/formatter/coverage_json_writer.rb +95 -0
  118. data/lib/simplecov/formatter/html_formatter/public/index.html +93 -0
  119. data/lib/simplecov/formatter/html_formatter/viewer_data_validator.rb +153 -0
  120. data/lib/simplecov/formatter/html_formatter.rb +88 -0
  121. data/lib/simplecov/formatter/json_formatter/errors_formatter.rb +122 -0
  122. data/lib/simplecov/formatter/json_formatter/production_section_formatter.rb +48 -0
  123. data/lib/simplecov/formatter/json_formatter/result_hash_formatter.rb +132 -0
  124. data/lib/simplecov/formatter/json_formatter/source_file_formatter.rb +97 -0
  125. data/lib/simplecov/formatter/json_formatter.rb +40 -0
  126. data/lib/simplecov/formatter/multi_formatter.rb +8 -10
  127. data/lib/simplecov/formatter/simple_formatter.rb +12 -15
  128. data/lib/simplecov/formatter.rb +21 -1
  129. data/lib/simplecov/group_names.rb +33 -0
  130. data/lib/simplecov/history.rb +115 -0
  131. data/lib/simplecov/last_run.rb +16 -5
  132. data/lib/simplecov/lines_classifier.rb +67 -19
  133. data/lib/simplecov/load_global_config.rb +12 -4
  134. data/lib/simplecov/parallel_adapters/base.rb +50 -0
  135. data/lib/simplecov/parallel_adapters/generic.rb +33 -0
  136. data/lib/simplecov/parallel_adapters/parallel_tests.rb +72 -0
  137. data/lib/simplecov/parallel_adapters.rb +54 -0
  138. data/lib/simplecov/parallel_coordination.rb +124 -0
  139. data/lib/simplecov/parallel_result_merger/worker_payload.rb +33 -0
  140. data/lib/simplecov/parallel_result_merger.rb +171 -0
  141. data/lib/simplecov/process.rb +34 -12
  142. data/lib/simplecov/production/error.rb +11 -0
  143. data/lib/simplecov/production/file_sink.rb +130 -0
  144. data/lib/simplecov/production.rb +249 -0
  145. data/lib/simplecov/profiles/bundler_filter.rb +1 -1
  146. data/lib/simplecov/profiles/hidden_filter.rb +1 -1
  147. data/lib/simplecov/profiles/rails.rb +13 -10
  148. data/lib/simplecov/profiles/root_filter.rb +2 -5
  149. data/lib/simplecov/profiles/strict.rb +22 -0
  150. data/lib/simplecov/profiles/test_frameworks.rb +1 -4
  151. data/lib/simplecov/profiles.rb +26 -19
  152. data/lib/simplecov/report_deferral.rb +45 -0
  153. data/lib/simplecov/report_stamp.rb +25 -0
  154. data/lib/simplecov/result/filter_config.rb +19 -0
  155. data/lib/simplecov/result/missing_source_files_reporter.rb +48 -0
  156. data/lib/simplecov/result/serialization.rb +43 -0
  157. data/lib/simplecov/result/source_file_builder.rb +46 -0
  158. data/lib/simplecov/result.rb +123 -47
  159. data/lib/simplecov/result_adapter.rb +130 -11
  160. data/lib/simplecov/result_merger/contexts.rb +22 -0
  161. data/lib/simplecov/result_merger/legacy_format_adapter.rb +24 -0
  162. data/lib/simplecov/result_merger/resultset_file.rb +67 -0
  163. data/lib/simplecov/result_merger/resultset_run_identity.rb +65 -0
  164. data/lib/simplecov/result_merger/resultset_store.rb +62 -0
  165. data/lib/simplecov/result_merger/unloaded_files.rb +95 -0
  166. data/lib/simplecov/result_merger.rb +120 -124
  167. data/lib/simplecov/result_processing.rb +188 -0
  168. data/lib/simplecov/run_identity.rb +75 -0
  169. data/lib/simplecov/simulate_coverage.rb +72 -17
  170. data/lib/simplecov/source_file/branch.rb +8 -35
  171. data/lib/simplecov/source_file/branch_builder.rb +95 -0
  172. data/lib/simplecov/source_file/builder_context.rb +28 -0
  173. data/lib/simplecov/source_file/line.rb +17 -32
  174. data/lib/simplecov/source_file/line_builder.rb +37 -0
  175. data/lib/simplecov/source_file/method.rb +54 -0
  176. data/lib/simplecov/source_file/method_builder.rb +51 -0
  177. data/lib/simplecov/source_file/ruby_data_parser.rb +103 -0
  178. data/lib/simplecov/source_file/skip_chunks.rb +72 -0
  179. data/lib/simplecov/source_file/source_loader.rb +89 -0
  180. data/lib/simplecov/source_file/statistics.rb +76 -0
  181. data/lib/simplecov/source_file.rb +64 -260
  182. data/lib/simplecov/static_coverage_extractor/condition_folding.rb +155 -0
  183. data/lib/simplecov/static_coverage_extractor/location_conventions.rb +191 -0
  184. data/lib/simplecov/static_coverage_extractor/method_collector.rb +45 -0
  185. data/lib/simplecov/static_coverage_extractor/prism_compat.rb +46 -0
  186. data/lib/simplecov/static_coverage_extractor/value_position.rb +61 -0
  187. data/lib/simplecov/static_coverage_extractor/visitor.rb +163 -0
  188. data/lib/simplecov/static_coverage_extractor.rb +78 -0
  189. data/lib/simplecov/test_tracker/accessors.rb +34 -0
  190. data/lib/simplecov/test_tracker/constant_watch.rb +49 -0
  191. data/lib/simplecov/test_tracker/delta.rb +69 -0
  192. data/lib/simplecov/test_tracker/framework_hooks.rb +130 -0
  193. data/lib/simplecov/test_tracker.rb +162 -0
  194. data/lib/simplecov/unloaded_file_injector.rb +62 -0
  195. data/lib/simplecov/useless_results_remover.rb +17 -7
  196. data/lib/simplecov/version.rb +1 -1
  197. data/lib/simplecov/view_coverage/template_compiler.rb +83 -0
  198. data/lib/simplecov/view_coverage.rb +55 -0
  199. data/lib/simplecov-html.rb +5 -0
  200. data/lib/simplecov.rb +143 -394
  201. data/lib/simplecov_json_formatter.rb +5 -0
  202. data/man/simplecov.1 +354 -0
  203. data/sig/simplecov.rbs +1710 -0
  204. metadata +183 -61
  205. data/CHANGELOG.md +0 -191
  206. data/doc/alternate-formatters.md +0 -71
  207. data/doc/commercial-services.md +0 -25
  208. data/doc/editor-integration.md +0 -18
  209. data/lib/simplecov/combine/files_combiner.rb +0 -24
  210. data/lib/simplecov/default_formatter.rb +0 -20
@@ -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
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "json"
4
+ require_relative "atomic_file"
4
5
 
5
6
  module SimpleCov
6
7
  module LastRun
@@ -9,19 +10,29 @@ module SimpleCov
9
10
  File.join(SimpleCov.coverage_path, ".last_run.json")
10
11
  end
11
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.
12
16
  def read
13
17
  return nil unless File.exist?(last_run_path)
14
18
 
15
19
  json = File.read(last_run_path)
16
- return nil if json.strip.empty?
20
+ return nil if json.match?(/\A\s*\z/)
17
21
 
18
- JSON.parse(json, symbolize_names: true)
22
+ parsed = JSON.parse(json, symbolize_names: true)
23
+ parsed.instance_of?(Hash) ? parsed : invalid_last_run
24
+ rescue JSON::ParserError
25
+ invalid_last_run
19
26
  end
20
27
 
21
28
  def write(json)
22
- File.open(last_run_path, "w+") do |f|
23
- f.puts JSON.pretty_generate(json)
24
- end
29
+ AtomicFile.write(last_run_path, "#{JSON.pretty_generate(json)}\n")
30
+ end
31
+
32
+ private
33
+
34
+ def invalid_last_run
35
+ warn "[SimpleCov]: Warning! Parsing JSON content of .last_run.json failed, ignoring the previous run"
25
36
  end
26
37
  end
27
38
  end
@@ -1,47 +1,95 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module SimpleCov
4
- # Classifies whether lines are relevant for code coverage analysis.
5
- # Comments & whitespace lines, and :nocov: token blocks, are considered not relevant.
3
+ require_relative "directive"
6
4
 
5
+ module SimpleCov
6
+ # Classifies whether lines are relevant for code coverage analysis. Comments,
7
+ # whitespace lines, and disabled blocks are considered not relevant.
7
8
  class LinesClassifier
8
9
  RELEVANT = 0
9
10
  NOT_RELEVANT = nil
10
11
 
11
- WHITESPACE_LINE = /^\s*$/.freeze
12
- COMMENT_LINE = /^\s*#/.freeze
12
+ WHITESPACE_LINE = /^\s*$/
13
+ COMMENT_LINE = /^\s*#/
13
14
  WHITESPACE_OR_COMMENT_LINE = Regexp.union(WHITESPACE_LINE, COMMENT_LINE)
14
15
 
16
+ # The leading `^(\s*)#` anchor is load-bearing beyond matching the marker:
17
+ # `classify_line` only reaches this for lines that already passed
18
+ # `whitespace_line?`, which is sound only while every marker is also a
19
+ # comment. Loosening this to match a trailing `x = 1 # :nocov:` would stop
20
+ # the toggle firing. `lines_classifier_spec.rb` pins the implication.
21
+ #
22
+ # The `/o` flag freezes the interpolated nocov token at this
23
+ # process's first classification; a `nocov_token` configured after
24
+ # coverage has classified a file would be silently ignored. Sound
25
+ # today because configuration always precedes classification.
15
26
  def self.no_cov_line
16
- /^(\s*)#(\s*)(:#{SimpleCov.nocov_token}:)/o
27
+ /^(\s*)#(\s*)(:#{SimpleCov.current_nocov_token}:)/o
17
28
  end
18
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.
19
34
  def self.no_cov_line?(line)
20
35
  no_cov_line.match?(line)
21
36
  rescue ArgumentError
22
- # E.g., line contains an invalid byte sequence in UTF-8
23
37
  false
24
38
  end
25
39
 
26
40
  def self.whitespace_line?(line)
27
41
  WHITESPACE_OR_COMMENT_LINE.match?(line)
28
42
  rescue ArgumentError
29
- # E.g., line contains an invalid byte sequence in UTF-8
30
43
  false
31
44
  end
32
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.
33
63
  def classify(lines)
34
- skipping = false
35
-
36
- lines.map do |line|
37
- if self.class.no_cov_line?(line)
38
- skipping = !skipping
39
- NOT_RELEVANT
40
- elsif skipping || self.class.whitespace_line?(line)
41
- NOT_RELEVANT
42
- else
43
- RELEVANT
44
- end
64
+ lines = lines.to_a
65
+ directive_disabled = directive_disabled_line_set(lines)
66
+ skip_state = SkipState.new
67
+
68
+ lines.map.with_index(1) do |line, line_number|
69
+ classify_line(line, line_number, directive_disabled, skip_state)
70
+ end
71
+ end
72
+
73
+ private
74
+
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.
79
+ def classify_line(line, line_number, directive_disabled, skip_state)
80
+ if self.class.whitespace_line?(line)
81
+ skip_state.toggle if self.class.no_cov_line?(line)
82
+ NOT_RELEVANT
83
+ elsif skip_state.skipping? || directive_disabled.include?(line_number)
84
+ NOT_RELEVANT
85
+ else
86
+ RELEVANT
87
+ end
88
+ end
89
+
90
+ def directive_disabled_line_set(lines)
91
+ Directive.disabled_ranges(lines).fetch(:line).each_with_object(Set.new) do |range, set|
92
+ range.each { |line_number| set.add(line_number) }
45
93
  end
46
94
  end
47
95
  end
@@ -1,8 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "etc"
4
- home_dir = (ENV["HOME"] && File.expand_path("~")) || Etc.getpwuid.dir || (ENV["USER"] && File.expand_path("~#{ENV['USER']}"))
5
- if home_dir
6
- global_config_path = File.join(home_dir, ".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
+ unless ENV.fetch("HOME", "").empty?
11
+ # simplecov:disable — only fires when ~/.simplecov exists, which is
12
+ # developer-machine-dependent (we can't rely on it for the dogfood).
13
+ global_config_path = File.join(File.expand_path("~"), ".simplecov")
7
14
  load global_config_path if File.exist?(global_config_path)
15
+ # simplecov:enable
8
16
  end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleCov
4
+ module ParallelAdapters
5
+ # Default no-op implementations for a parallel-test-runner adapter.
6
+ # Everything not overridden falls back to behaving like a single-process
7
+ # run. Adapters are classes, used as singletons and never instantiated.
8
+ class Base
9
+ class << self
10
+ def active?
11
+ false
12
+ end
13
+
14
+ def first_worker?
15
+ true
16
+ end
17
+
18
+ def wait_for_siblings
19
+ end
20
+
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`.
25
+ def native_wait?
26
+ false
27
+ end
28
+
29
+ def expected_worker_count
30
+ 1
31
+ end
32
+
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.
36
+ def forced_off?
37
+ SimpleCov.parallel_tests == false
38
+ end
39
+
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.
43
+ def parallel_test_groups_count
44
+ count = Integer(ENV.fetch("PARALLEL_TEST_GROUPS", nil), exception: false)
45
+ count&.positive? ? count : 1
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module SimpleCov
6
+ module ParallelAdapters
7
+ # Catch-all adapter for parallel test runners that follow the
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.
12
+ class GenericAdapter < Base
13
+ class << self
14
+ def active?
15
+ return false if forced_off?
16
+
17
+ ENV.key?("TEST_ENV_NUMBER")
18
+ end
19
+
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.
23
+ def first_worker?
24
+ ["", "1"].include?(ENV.fetch("TEST_ENV_NUMBER", nil))
25
+ end
26
+
27
+ def expected_worker_count
28
+ parallel_test_groups_count
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module SimpleCov
6
+ module ParallelAdapters
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.
13
+ class ParallelTestsAdapter < Base
14
+ class << self
15
+ def active?
16
+ return false if forced_off?
17
+
18
+ ensure_loaded
19
+ !!(defined?(::ParallelTests) && native_parallel_tests_environment?)
20
+ end
21
+
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.
28
+ def first_worker?
29
+ ParallelTests.first_process?
30
+ end
31
+
32
+ def wait_for_siblings
33
+ return unless native_parallel_tests_environment?
34
+
35
+ ParallelTests.wait_for_other_processes_to_finish
36
+ end
37
+
38
+ def native_wait?
39
+ native_parallel_tests_environment?
40
+ end
41
+
42
+ def expected_worker_count
43
+ parallel_test_groups_count
44
+ end
45
+
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).
51
+ def ensure_loaded
52
+ return if defined?(::ParallelTests)
53
+ return if forced_off?
54
+ return unless SimpleCov.parallel_tests || env_suggests_parallel_tests?
55
+
56
+ # simplecov:disable — only fires under a real parallel_tests setup
57
+ require "parallel_tests"
58
+ rescue LoadError
59
+ # simplecov:enable
60
+ end
61
+
62
+ def env_suggests_parallel_tests?
63
+ ENV.key?("TEST_ENV_NUMBER") && ENV.key?("PARALLEL_TEST_GROUPS")
64
+ end
65
+
66
+ def native_parallel_tests_environment?
67
+ ENV.key?("TEST_ENV_NUMBER") && ENV.key?("PARALLEL_PID_FILE")
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "parallel_adapters/base"
4
+ require_relative "parallel_adapters/parallel_tests"
5
+ require_relative "parallel_adapters/generic"
6
+
7
+ module SimpleCov
8
+ # Registry and selection for parallel-test-runner adapters. An adapter
9
+ # answers a small fixed set of questions on SimpleCov's behalf: `active?`,
10
+ # `first_worker?`, `wait_for_siblings`, and `expected_worker_count`.
11
+ #
12
+ # `Base` provides safe no-op defaults. `ParallelTestsAdapter` wraps the
13
+ # grosser/parallel_tests gem; `GenericAdapter` is env-var-only detection for
14
+ # runners that follow the `TEST_ENV_NUMBER` convention without shipping a
15
+ # Ruby API (#1065, #1156).
16
+ #
17
+ # Users register their own with `ParallelAdapters.register MyAdapter`.
18
+ # Subclass `Base` to inherit the no-op defaults: the contract methods are
19
+ # class methods, so plain inheritance is what carries them through and
20
+ # `extend Base` won't pick them up.
21
+ module ParallelAdapters
22
+ extend self
23
+
24
+ # ParallelTestsAdapter first (most specific: it uses the gem's own API when
25
+ # the gem is loaded), then GenericAdapter as the env-var fallback.
26
+ # User-registered adapters are prepended.
27
+ def adapters
28
+ @adapters ||= [ParallelTestsAdapter, GenericAdapter]
29
+ end
30
+
31
+ # Newly registered adapters are inserted at the front of the selection
32
+ # list, so a custom adapter for a specific runner takes precedence over the
33
+ # built-ins.
34
+ def register(adapter)
35
+ reset_current!
36
+ adapters.unshift(adapter) unless adapters.include?(adapter)
37
+ adapter
38
+ end
39
+
40
+ # The first registered adapter whose `active?` answers true, or nil when
41
+ # none is, in which case the caller treats the process as single-worker.
42
+ def current
43
+ return @current if instance_variable_defined?(:@current)
44
+
45
+ @current = adapters.find(&:active?)
46
+ end
47
+
48
+ # Primarily for tests that mutate env vars between examples; production
49
+ # runs are single-shot.
50
+ def reset_current!
51
+ remove_instance_variable(:@current) if instance_variable_defined?(:@current)
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,124 @@
1
+ # frozen_string_literal: true
2
+
3
+ # simplecov:disable
4
+ # Methods below only fire under a parallel test runner; not reachable
5
+ # from a single-process rspec run. Cucumber's test_projects exercise
6
+ # the parallel_tests integration end-to-end in subprocesses, but
7
+ # those subprocesses don't merge their Coverage data back into the
8
+ # parent this dogfood report measures.
9
+ module SimpleCov
10
+ class << self
11
+ # @api private
12
+ def final_result_process?
13
+ adapter = ParallelAdapters.current
14
+ # A subprocess forked while coverage was running is never the final
15
+ # reporter: the process that spawned it merges every slice and produces
16
+ # the report. Without this, fork-based runners that don't set
17
+ # TEST_ENV_NUMBER (Minitest's `parallelize`) have every worker produce
18
+ # the final report and its warnings (#1171).
19
+ return !forked_subprocess? unless adapter
20
+
21
+ adapter.first_worker?
22
+ end
23
+
24
+ # @api private
25
+ def wait_for_other_processes
26
+ adapter = ParallelAdapters.current
27
+ return unless adapter && final_result_process?
28
+
29
+ # Adapters that wrap a runner with a real "wait" primitive implement
30
+ # this; the rest no-op and rely on the polling fallback below.
31
+ adapter.wait_for_siblings
32
+
33
+ # The native wait can return before sibling at_exit handlers finish
34
+ # writing resultsets, and adapters without a native wait have nothing
35
+ # else. Either way, poll the resultset cache until all expected workers
36
+ # have reported or a timeout is reached. The outcome is captured so
37
+ # threshold checks can be suppressed against a partial total.
38
+ @parallel_results_complete =
39
+ wait_for_parallel_results(adapter.expected_worker_count, native_wait: adapter.native_wait?)
40
+ end
41
+
42
+ # @api private -- true when every sibling reported its resultset before the
43
+ # wait deadline. Defaults to true outside a parallel run.
44
+ def parallel_results_complete?
45
+ instance_variable_defined?(:@parallel_results_complete) ? @parallel_results_complete : true
46
+ end
47
+
48
+ # @api private -- seconds the resultset count must hold steady, after a
49
+ # native wait, before we accept fewer than `expected` workers as final.
50
+ PARALLEL_RESULTS_SETTLE = 0.5
51
+ private_constant :PARALLEL_RESULTS_SETTLE
52
+
53
+ # @api private -- true when the reporting worker has every resultset it's
54
+ # going to get, false on timeout. Single-process runs short-circuit.
55
+ #
56
+ # Normally we poll until `expected` workers have reported or
57
+ # `SimpleCov.parallel_wait_timeout` elapses. When a native wait already
58
+ # confirmed every sibling process exited, no further resultset will appear,
59
+ # so a count below `expected` just means some workers produced none, and
60
+ # once the count holds steady for `PARALLEL_RESULTS_SETTLE` we accept it as
61
+ # final. Without a native wait we can't tell an idle worker from a slow
62
+ # one, so we keep waiting the full timeout.
63
+ def wait_for_parallel_results(expected, native_wait: false)
64
+ return true unless expected > 1 # simplecov:disable branch — only false in real parallel runs
65
+
66
+ deadline = monotonic_time + parallel_wait_timeout
67
+ tracker = {count: 0, since: monotonic_time}
68
+ loop do
69
+ seen = current_parallel_worker_count
70
+ return true if seen >= expected
71
+ return true if native_wait && resultset_count_settled?(tracker, seen)
72
+ return false if parallel_wait_timed_out?(deadline, expected, seen)
73
+
74
+ sleep 0.1
75
+ end
76
+ end
77
+
78
+ def current_parallel_worker_count
79
+ resultset = ResultMerger.read_resultset
80
+ ResultMerger.worker_identities_for_run(resultset, run_id, process_start_time).size
81
+ end
82
+
83
+ # `tracker` carries the last count and the time it last changed across poll
84
+ # iterations. Answered through an if/else rather than an early return
85
+ # because falling through after a climb would read the clock a second time
86
+ # and answer the same `false` anyway.
87
+ def resultset_count_settled?(tracker, count)
88
+ if count > tracker.fetch(:count)
89
+ tracker[:count] = count
90
+ tracker[:since] = monotonic_time
91
+ false
92
+ else
93
+ count.positive? && (monotonic_time - tracker.fetch(:since)) >= PARALLEL_RESULTS_SETTLE
94
+ end
95
+ end
96
+
97
+ def monotonic_time
98
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
99
+ end
100
+
101
+ # @api private -- warns on the first timeout so the user knows the merged
102
+ # total is partial.
103
+ def parallel_wait_timed_out?(deadline, expected, seen)
104
+ return false unless monotonic_time > deadline
105
+
106
+ warn_about_incomplete_parallel_results(expected, seen)
107
+ true
108
+ end
109
+
110
+ # @api private
111
+ def warn_about_incomplete_parallel_results(expected, seen)
112
+ return unless print_errors
113
+
114
+ warn Color.colorize(
115
+ "Only #{seen} of #{expected} parallel-test workers reported within " \
116
+ "#{parallel_wait_timeout}s, so coverage totals are partial and minimum / " \
117
+ "maximum coverage checks are skipped for this run. Increase " \
118
+ "SimpleCov.parallel_wait_timeout if a worker routinely needs longer.",
119
+ :yellow
120
+ )
121
+ end
122
+ end
123
+ end
124
+ # simplecov:enable
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleCov
4
+ module ParallelResultMerger
5
+ # What a worker ships back over its pipe: the `[command_names, coverage]` pair
6
+ # its slice folded to, the tracked paths the slice carried, and the slice's
7
+ # context-map union. Building and folding live together so the pipe format
8
+ # has one owner. The parent needs the union across every worker of the
9
+ # tracked paths, to know what nothing loaded, and of the maps, to judge the
10
+ # all-or-nothing rule over the whole run rather than per slice.
11
+ module WorkerPayload
12
+ extend self
13
+
14
+ def build(chunk, ignore_timeout:)
15
+ tracked_files = Set.new
16
+ context_maps = ContextMap::Union.new
17
+ pair = ResultMerger.absorb_results(chunk, ignore_timeout: ignore_timeout,
18
+ &ResultMerger.entry_collector(tracked_files, context_maps))
19
+ [pair, tracked_files.to_a, context_maps]
20
+ end
21
+
22
+ def absorb(payload, tracked_files, context_maps)
23
+ _pair, tracked, maps = payload
24
+ tracked_files.merge(tracked)
25
+ context_maps.absorb_union(maps)
26
+ end
27
+
28
+ def pair(payload)
29
+ payload.first
30
+ end
31
+ end
32
+ end
33
+ end