simplecov 0.22.0 → 1.1.1

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 (137) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +1 -1
  3. data/README.md +71 -916
  4. data/exe/simplecov +11 -0
  5. data/lib/minitest/simplecov_plugin.rb +13 -5
  6. data/lib/simplecov/atomic_file.rb +70 -0
  7. data/lib/simplecov/autostart.rb +11 -0
  8. data/lib/simplecov/cli/clean.rb +87 -0
  9. data/lib/simplecov/cli/command_helpers.rb +55 -0
  10. data/lib/simplecov/cli/coverage.rb +86 -0
  11. data/lib/simplecov/cli/coverage_file.rb +65 -0
  12. data/lib/simplecov/cli/diff.rb +147 -0
  13. data/lib/simplecov/cli/dotfile.rb +111 -0
  14. data/lib/simplecov/cli/merge.rb +120 -0
  15. data/lib/simplecov/cli/open.rb +48 -0
  16. data/lib/simplecov/cli/report.rb +97 -0
  17. data/lib/simplecov/cli/run.rb +36 -0
  18. data/lib/simplecov/cli/serve/report_preparer.rb +29 -0
  19. data/lib/simplecov/cli/serve/static_file_handler.rb +122 -0
  20. data/lib/simplecov/cli/serve.rb +85 -0
  21. data/lib/simplecov/cli/uncovered.rb +103 -0
  22. data/lib/simplecov/cli.rb +163 -0
  23. data/lib/simplecov/color.rb +83 -0
  24. data/lib/simplecov/combine/branches_combiner.rb +54 -7
  25. data/lib/simplecov/combine/coverage_accumulator.rb +268 -0
  26. data/lib/simplecov/combine/identity_interner.rb +30 -0
  27. data/lib/simplecov/combine/interned_counts.rb +30 -0
  28. data/lib/simplecov/combine/lines_combiner.rb +53 -23
  29. data/lib/simplecov/combine/methods_combiner.rb +74 -0
  30. data/lib/simplecov/combine/results_combiner.rb +13 -36
  31. data/lib/simplecov/combine.rb +5 -23
  32. data/lib/simplecov/command_guesser.rb +104 -33
  33. data/lib/simplecov/configuration/coverage.rb +172 -0
  34. data/lib/simplecov/configuration/coverage_criteria.rb +154 -0
  35. data/lib/simplecov/configuration/eval_coverage.rb +41 -0
  36. data/lib/simplecov/configuration/filters.rb +164 -0
  37. data/lib/simplecov/configuration/formatting.rb +138 -0
  38. data/lib/simplecov/configuration/groups.rb +42 -0
  39. data/lib/simplecov/configuration/ignored_entries.rb +63 -0
  40. data/lib/simplecov/configuration/merging.rb +165 -0
  41. data/lib/simplecov/configuration/thresholds.rb +179 -0
  42. data/lib/simplecov/configuration.rb +83 -420
  43. data/lib/simplecov/coverage_json.rb +24 -0
  44. data/lib/simplecov/coverage_statistics.rb +16 -10
  45. data/lib/simplecov/coverage_violations.rb +159 -0
  46. data/lib/simplecov/defaults.rb +34 -23
  47. data/lib/simplecov/deprecation.rb +45 -0
  48. data/lib/simplecov/directive.rb +167 -0
  49. data/lib/simplecov/exit_codes/check.rb +33 -0
  50. data/lib/simplecov/exit_codes/exit_code_handling.rb +8 -2
  51. data/lib/simplecov/exit_codes/maximum_coverage_drop_check.rb +18 -65
  52. data/lib/simplecov/exit_codes/maximum_overall_coverage_check.rb +32 -0
  53. data/lib/simplecov/exit_codes/minimum_coverage_by_file_check.rb +17 -36
  54. data/lib/simplecov/exit_codes/minimum_coverage_by_group_check.rb +30 -0
  55. data/lib/simplecov/exit_codes/minimum_overall_coverage_check.rb +37 -33
  56. data/lib/simplecov/exit_codes.rb +20 -0
  57. data/lib/simplecov/exit_handling.rb +128 -0
  58. data/lib/simplecov/file_list.rb +70 -24
  59. data/lib/simplecov/filter.rb +105 -26
  60. data/lib/simplecov/formatter/base.rb +118 -0
  61. data/lib/simplecov/formatter/coverage_json_writer.rb +97 -0
  62. data/lib/simplecov/formatter/html_formatter/public/index.html +87 -0
  63. data/lib/simplecov/formatter/html_formatter/viewer_data_validator.rb +96 -0
  64. data/lib/simplecov/formatter/html_formatter.rb +105 -0
  65. data/lib/simplecov/formatter/json_formatter/errors_formatter.rb +88 -0
  66. data/lib/simplecov/formatter/json_formatter/result_hash_formatter.rb +118 -0
  67. data/lib/simplecov/formatter/json_formatter/source_file_formatter.rb +93 -0
  68. data/lib/simplecov/formatter/json_formatter.rb +46 -0
  69. data/lib/simplecov/formatter/multi_formatter.rb +12 -8
  70. data/lib/simplecov/formatter/simple_formatter.rb +14 -11
  71. data/lib/simplecov/formatter.rb +16 -0
  72. data/lib/simplecov/group_names.rb +32 -0
  73. data/lib/simplecov/last_run.rb +18 -4
  74. data/lib/simplecov/lines_classifier.rb +48 -15
  75. data/lib/simplecov/load_global_config.rb +12 -4
  76. data/lib/simplecov/parallel_adapters/base.rb +78 -0
  77. data/lib/simplecov/parallel_adapters/generic.rb +45 -0
  78. data/lib/simplecov/parallel_adapters/parallel_tests.rb +93 -0
  79. data/lib/simplecov/parallel_adapters.rb +85 -0
  80. data/lib/simplecov/parallel_coordination.rb +134 -0
  81. data/lib/simplecov/parallel_result_merger.rb +230 -0
  82. data/lib/simplecov/process.rb +52 -12
  83. data/lib/simplecov/profiles/bundler_filter.rb +1 -1
  84. data/lib/simplecov/profiles/hidden_filter.rb +1 -1
  85. data/lib/simplecov/profiles/rails.rb +24 -10
  86. data/lib/simplecov/profiles/root_filter.rb +6 -5
  87. data/lib/simplecov/profiles/strict.rb +32 -0
  88. data/lib/simplecov/profiles/test_frameworks.rb +1 -4
  89. data/lib/simplecov/profiles.rb +32 -3
  90. data/lib/simplecov/report_deferral.rb +49 -0
  91. data/lib/simplecov/report_stamp.rb +28 -0
  92. data/lib/simplecov/result/missing_source_files_reporter.rb +49 -0
  93. data/lib/simplecov/result/source_file_builder.rb +51 -0
  94. data/lib/simplecov/result.rb +145 -26
  95. data/lib/simplecov/result_adapter.rb +139 -8
  96. data/lib/simplecov/result_merger/legacy_format_adapter.rb +28 -0
  97. data/lib/simplecov/result_merger/resultset_file.rb +74 -0
  98. data/lib/simplecov/result_merger/resultset_run_identity.rb +67 -0
  99. data/lib/simplecov/result_merger/resultset_store.rb +53 -0
  100. data/lib/simplecov/result_merger/unloaded_files.rb +103 -0
  101. data/lib/simplecov/result_merger.rb +109 -114
  102. data/lib/simplecov/result_processing.rb +218 -0
  103. data/lib/simplecov/run_identity.rb +77 -0
  104. data/lib/simplecov/simulate_coverage.rb +83 -12
  105. data/lib/simplecov/source_file/branch.rb +1 -3
  106. data/lib/simplecov/source_file/branch_builder.rb +114 -0
  107. data/lib/simplecov/source_file/builder_context.rb +28 -0
  108. data/lib/simplecov/source_file/line.rb +9 -4
  109. data/lib/simplecov/source_file/line_builder.rb +43 -0
  110. data/lib/simplecov/source_file/method.rb +58 -0
  111. data/lib/simplecov/source_file/method_builder.rb +60 -0
  112. data/lib/simplecov/source_file/ruby_data_parser.rb +110 -0
  113. data/lib/simplecov/source_file/skip_chunks.rb +72 -0
  114. data/lib/simplecov/source_file/source_loader.rb +79 -0
  115. data/lib/simplecov/source_file/statistics.rb +65 -0
  116. data/lib/simplecov/source_file.rb +66 -232
  117. data/lib/simplecov/static_coverage_extractor/condition_folding.rb +239 -0
  118. data/lib/simplecov/static_coverage_extractor/location_conventions.rb +236 -0
  119. data/lib/simplecov/static_coverage_extractor/method_collector.rb +62 -0
  120. data/lib/simplecov/static_coverage_extractor/prism_compat.rb +55 -0
  121. data/lib/simplecov/static_coverage_extractor/value_position.rb +74 -0
  122. data/lib/simplecov/static_coverage_extractor/visitor.rb +183 -0
  123. data/lib/simplecov/static_coverage_extractor.rb +110 -0
  124. data/lib/simplecov/unloaded_file_injector.rb +75 -0
  125. data/lib/simplecov/useless_results_remover.rb +16 -7
  126. data/lib/simplecov/version.rb +1 -1
  127. data/lib/simplecov-html.rb +4 -0
  128. data/lib/simplecov.rb +185 -380
  129. data/lib/simplecov_json_formatter.rb +4 -0
  130. data/sig/simplecov.rbs +1777 -0
  131. metadata +111 -62
  132. data/CHANGELOG.md +0 -191
  133. data/doc/alternate-formatters.md +0 -71
  134. data/doc/commercial-services.md +0 -25
  135. data/doc/editor-integration.md +0 -18
  136. data/lib/simplecov/combine/files_combiner.rb +0 -24
  137. data/lib/simplecov/default_formatter.rb +0 -20
@@ -1,19 +1,30 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "directive"
4
+
3
5
  module SimpleCov
4
6
  # Classifies whether lines are relevant for code coverage analysis.
5
7
  # Comments & whitespace lines, and :nocov: token blocks, are considered not relevant.
6
-
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
 
19
30
  def self.no_cov_line?(line)
@@ -31,17 +42,39 @@ module SimpleCov
31
42
  end
32
43
 
33
44
  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
45
+ lines = lines.to_a
46
+ 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]
51
+
52
+ lines.map.with_index(1) do |line, line_number|
53
+ classify_line(line, line_number, directive_disabled, skip_state)
54
+ end
55
+ end
56
+
57
+ private
58
+
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.
64
+ def classify_line(line, line_number, directive_disabled, skip_state)
65
+ if self.class.whitespace_line?(line)
66
+ skip_state[0] = !skip_state[0] if self.class.no_cov_line?(line)
67
+ NOT_RELEVANT
68
+ elsif skip_state[0] || directive_disabled.include?(line_number)
69
+ NOT_RELEVANT
70
+ else
71
+ RELEVANT
72
+ end
73
+ end
74
+
75
+ def directive_disabled_line_set(lines)
76
+ Directive.disabled_ranges(lines).fetch(:line).each_with_object(Set.new) do |range, set|
77
+ range.each { |line_number| set.add(line_number) }
45
78
  end
46
79
  end
47
80
  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 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"`.
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,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleCov
4
+ module ParallelAdapters
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.
15
+ class Base
16
+ 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
+ def active?
21
+ false
22
+ end
23
+
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
+ def first_worker?
29
+ true
30
+ end
31
+
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
+ def wait_for_siblings
39
+ # No-op default; polling fallback handles correctness.
40
+ end
41
+
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).
48
+ def native_wait?
49
+ false
50
+ end
51
+
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
+ def expected_worker_count
56
+ 1
57
+ end
58
+
59
+ # The user's explicit `SimpleCov.parallel_tests false` opt-out,
60
+ # which every adapter's `active?` honors before its own
61
+ # detection.
62
+ def forced_off?
63
+ SimpleCov.parallel_tests == false
64
+ end
65
+
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.
71
+ def parallel_test_groups_count
72
+ count = Integer(ENV.fetch("PARALLEL_TEST_GROUPS", ""), exception: false)
73
+ count&.positive? ? count : 1
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,45 @@
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
9
+ # don't ship a Ruby API for SimpleCov to hook (parallel_rspec,
10
+ # rspec-conductor since 1.0.7, knapsack-style splitters, custom CI
11
+ # sharding scripts). Activates when `TEST_ENV_NUMBER` is set;
12
+ # doesn't require any specific gem to be loaded.
13
+ #
14
+ # Heuristic for `first_worker?`: the worker whose `TEST_ENV_NUMBER`
15
+ # is `""` (parallel_tests/parallel_rspec convention, rspec-conductor's
16
+ # default) or `"1"` (runners that number from 1, like rspec-conductor
17
+ # with `--first-is-1`). Any other value is treated as a non-first
18
+ # worker.
19
+ #
20
+ # `wait_for_siblings` is inherited from Base as a no-op — without a
21
+ # runner-provided API the only synchronization available is polling
22
+ # the resultset cache, which `SimpleCov.wait_for_parallel_results`
23
+ # does after the no-op returns.
24
+ class GenericAdapter < Base
25
+ class << self
26
+ def active?
27
+ return false if forced_off?
28
+
29
+ ENV.key?("TEST_ENV_NUMBER")
30
+ end
31
+
32
+ # parallel_tests sets the first worker's TEST_ENV_NUMBER to "";
33
+ # parallel_rspec inherits that. Runners that number from 1 use
34
+ # "1" for the first worker. Both shapes match.
35
+ def first_worker?
36
+ ["", "1"].include?(ENV.fetch("TEST_ENV_NUMBER", nil))
37
+ end
38
+
39
+ def expected_worker_count
40
+ parallel_test_groups_count
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module SimpleCov
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.
16
+ class ParallelTestsAdapter < Base
17
+ class << self
18
+ def active?
19
+ return false if forced_off?
20
+
21
+ ensure_loaded
22
+ # !! to coerce `defined?` (returns nil or "constant") to a proper bool.
23
+ !!(defined?(::ParallelTests) && native_parallel_tests_environment?)
24
+ end
25
+
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).
36
+ def first_worker?
37
+ ::ParallelTests.first_process?
38
+ end
39
+
40
+ def wait_for_siblings
41
+ return unless native_parallel_tests_environment?
42
+
43
+ ::ParallelTests.wait_for_other_processes_to_finish
44
+ end
45
+
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
+ def native_wait?
49
+ native_parallel_tests_environment?
50
+ end
51
+
52
+ def expected_worker_count
53
+ parallel_test_groups_count
54
+ end
55
+
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.
68
+ 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
72
+ return unless SimpleCov.parallel_tests || env_suggests_parallel_tests?
73
+
74
+ # simplecov:disable — only fires under a real parallel_tests setup
75
+ require "parallel_tests"
76
+ 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
+ # simplecov:enable
81
+ end
82
+
83
+ def env_suggests_parallel_tests?
84
+ ENV.key?("TEST_ENV_NUMBER") && ENV.key?("PARALLEL_TEST_GROUPS")
85
+ end
86
+
87
+ def native_parallel_tests_environment?
88
+ ENV.key?("TEST_ENV_NUMBER") && ENV.key?("PARALLEL_PID_FILE")
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,85 @@
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 + selection for parallel-test-runner adapters. An adapter
9
+ # answers a small fixed set of questions on SimpleCov's behalf:
10
+ #
11
+ # - `active?` — are WE the runner in charge for this process?
12
+ # - `first_worker?` — should this process do the final-result work?
13
+ # - `wait_for_siblings` — block until siblings finish (optional)
14
+ # - `expected_worker_count` — how many workers total
15
+ #
16
+ # `SimpleCov::ParallelAdapters::Base` provides safe no-op defaults; two
17
+ # adapters ship out of the box:
18
+ #
19
+ # - `ParallelTestsAdapter` — wraps the grosser/parallel_tests gem
20
+ # (precise sync + first-process detection via the gem's own API).
21
+ # - `GenericAdapter` — env-var-only detection for runners that follow
22
+ # the parallel_tests `TEST_ENV_NUMBER` convention but don't ship a
23
+ # Ruby API (parallel_rspec, rspec-conductor 1.0.7+, custom CI
24
+ # sharding, knapsack-style splitters). See
25
+ # https://github.com/simplecov-ruby/simplecov/issues/1065 and
26
+ # https://github.com/simplecov-ruby/simplecov/issues/1156.
27
+ #
28
+ # Users can plug in additional adapters:
29
+ #
30
+ # SimpleCov::ParallelAdapters.register MyRunnerAdapter
31
+ #
32
+ # An adapter just needs to be a class responding to the four methods
33
+ # above. Subclass `SimpleCov::ParallelAdapters::Base` to inherit the
34
+ # no-op defaults and override only what you need (the contract methods
35
+ # are defined as class methods, so plain inheritance is what carries
36
+ # them through; `extend Base` won't pick them up).
37
+ module ParallelAdapters
38
+ module_function
39
+
40
+ # Adapters in selection order. ParallelTestsAdapter first (most
41
+ # specific — uses the gem's own API when the gem is loaded); then
42
+ # GenericAdapter as the env-var fallback. User-registered adapters
43
+ # are prepended (#register puts new entries at the front) so
44
+ # downstream code can override the built-ins by registering a more
45
+ # specific match.
46
+ def adapters
47
+ @adapters ||= [ParallelTestsAdapter, GenericAdapter]
48
+ end
49
+
50
+ # Register a custom adapter. Newly registered adapters are inserted
51
+ # at the front of the selection list so a custom adapter for a
52
+ # specific runner takes precedence over the built-in ParallelTests
53
+ # and Generic adapters.
54
+ #
55
+ # class MyRunnerAdapter < SimpleCov::ParallelAdapters::Base
56
+ # def self.active? = ENV["MY_RUNNER_PID"]
57
+ # def self.first_worker? = ENV["MY_RUNNER_PID"].to_i == 1
58
+ # def self.expected_worker_count = ENV["MY_RUNNER_WORKERS"].to_i
59
+ # end
60
+ #
61
+ # SimpleCov::ParallelAdapters.register MyRunnerAdapter
62
+ def register(adapter)
63
+ reset_current!
64
+ adapters.unshift(adapter) unless adapters.include?(adapter)
65
+ adapter
66
+ end
67
+
68
+ # The adapter SimpleCov should consult for this process — the first
69
+ # registered adapter whose `active?` returns true. Returns nil when
70
+ # no adapter is active (i.e., we're not running under any recognized
71
+ # parallel test runner), in which case the caller should treat the
72
+ # process as single-worker.
73
+ def current
74
+ return @current if defined?(@current)
75
+
76
+ @current = adapters.find(&:active?)
77
+ end
78
+
79
+ # Clear the memoized `current` selection. Primarily for tests that
80
+ # mutate env vars between examples; production runs are single-shot.
81
+ def reset_current!
82
+ remove_instance_variable(:@current) if defined?(@current)
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,134 @@
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 = SimpleCov::ParallelAdapters.current
14
+ # No recognized parallel-test adapter. A subprocess forked while
15
+ # coverage was running is never the final reporter — the process that
16
+ # spawned it merges every slice and produces the report. Without this,
17
+ # fork-based runners that don't set TEST_ENV_NUMBER (e.g. Minitest's
18
+ # `parallelize`) have every worker produce the final report and its
19
+ # warnings. See issue #1171.
20
+ return !forked_subprocess? unless adapter
21
+
22
+ adapter.first_worker?
23
+ end
24
+
25
+ # @api private
26
+ def wait_for_other_processes
27
+ adapter = SimpleCov::ParallelAdapters.current
28
+ return unless adapter && final_result_process?
29
+
30
+ # Native synchronization first (adapters that wrap a runner with a
31
+ # real "wait" primitive — parallel_tests'
32
+ # `wait_for_other_processes_to_finish` — implement this; adapters
33
+ # without a native API no-op and rely on the polling fallback below).
34
+ adapter.wait_for_siblings
35
+
36
+ # The native wait can return before sibling at_exit handlers finish
37
+ # writing resultsets, and adapters without a native wait have
38
+ # nothing else. Either way, poll the resultset cache until all
39
+ # expected workers have reported or a timeout is reached. Capture
40
+ # the outcome so `ready_to_process_results?` can suppress min/max
41
+ # threshold checks against a partial total.
42
+ @parallel_results_complete =
43
+ wait_for_parallel_results(adapter.expected_worker_count, native_wait: adapter.native_wait?)
44
+ end
45
+
46
+ # @api private — true when every sibling reported its resultset
47
+ # before the wait deadline. Defaults to true outside a parallel
48
+ # run (when `wait_for_other_processes` is a no-op).
49
+ def parallel_results_complete?
50
+ defined?(@parallel_results_complete) ? @parallel_results_complete : true
51
+ end
52
+
53
+ # @api private — seconds the resultset count must hold steady, after a
54
+ # native wait, before we accept fewer than `expected` workers as final.
55
+ PARALLEL_RESULTS_SETTLE = 0.5
56
+ private_constant :PARALLEL_RESULTS_SETTLE
57
+
58
+ # @api private — returns true when the reporting worker has every
59
+ # resultset it's going to get, false on timeout. Single-process runs
60
+ # (expected <= 1) short-circuit to true with no waiting.
61
+ #
62
+ # Normally we poll until `expected` (= PARALLEL_TEST_GROUPS) workers have
63
+ # reported or `SimpleCov.parallel_wait_timeout` elapses; raise that setting
64
+ # when a slow worker routinely finishes well after the others.
65
+ #
66
+ # When a native wait already confirmed every sibling PROCESS exited
67
+ # (`native_wait`), no further resultset will appear, so a count below
68
+ # `expected` just means some workers produced none — e.g. parallel_test
69
+ # groups that got no spec file on a machine with more cores than files.
70
+ # Once the count then holds steady for `PARALLEL_RESULTS_SETTLE` we accept
71
+ # it as final rather than blocking for the whole timeout. Without a native
72
+ # wait (GenericAdapter) we can't tell an idle worker from a slow one, so we
73
+ # keep waiting the full timeout.
74
+ def wait_for_parallel_results(expected, native_wait: false)
75
+ return true unless expected > 1 # simplecov:disable branch — only false in real parallel runs
76
+
77
+ deadline = monotonic_time + parallel_wait_timeout
78
+ tracker = {count: 0, since: monotonic_time}
79
+ loop do
80
+ seen = current_parallel_worker_count
81
+ return true if seen >= expected
82
+ return true if native_wait && resultset_count_settled?(tracker, seen)
83
+ return false if parallel_wait_timed_out?(deadline, expected, seen)
84
+
85
+ sleep 0.1
86
+ end
87
+ end
88
+
89
+ def current_parallel_worker_count
90
+ resultset = SimpleCov::ResultMerger.read_resultset
91
+ SimpleCov::ResultMerger.worker_identities_for_run(resultset, SimpleCov.run_id, SimpleCov.process_start_time).size
92
+ end
93
+
94
+ # Track whether the resultset count has held steady (and positive) for
95
+ # `PARALLEL_RESULTS_SETTLE` seconds. `tracker` carries the last count and
96
+ # the time it last changed across poll iterations.
97
+ def resultset_count_settled?(tracker, count)
98
+ if count > tracker[:count]
99
+ tracker[:count] = count
100
+ tracker[:since] = monotonic_time
101
+ return false
102
+ end
103
+
104
+ count.positive? && (monotonic_time - tracker[:since]) >= PARALLEL_RESULTS_SETTLE
105
+ end
106
+
107
+ def monotonic_time
108
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
109
+ end
110
+
111
+ # @api private — true once the wait deadline has passed; warns on
112
+ # the first timeout so the user knows the merged total is partial.
113
+ def parallel_wait_timed_out?(deadline, expected, seen)
114
+ return false unless monotonic_time > deadline
115
+
116
+ warn_about_incomplete_parallel_results(expected, seen)
117
+ true
118
+ end
119
+
120
+ # @api private
121
+ def warn_about_incomplete_parallel_results(expected, seen)
122
+ return unless print_errors
123
+
124
+ warn SimpleCov::Color.colorize(
125
+ "Only #{seen} of #{expected} parallel-test workers reported within " \
126
+ "#{parallel_wait_timeout}s, so coverage totals are partial and minimum / " \
127
+ "maximum coverage checks are skipped for this run. Increase " \
128
+ "SimpleCov.parallel_wait_timeout if a worker routinely needs longer.",
129
+ :yellow
130
+ )
131
+ end
132
+ end
133
+ end
134
+ # simplecov:enable