simplecov 1.0.0.rc1 → 1.0.0.rc3

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: aa29d54833537aa0434a697ada8befbfbb81c97c608afc1e7cddd6971009ec79
4
- data.tar.gz: df39d6ee8257c5cc7575621d83a428a96f80fc1db762bf3e247927ace5d1050d
3
+ metadata.gz: eda0f749acc9b248635b6b1128c601b28bd77304994f78ebbbec0fc9097a2969
4
+ data.tar.gz: 8d55b00892b8f2cd47f7d3960c52e5d177c4951a77cbcd4af42282ae7ec745fa
5
5
  SHA512:
6
- metadata.gz: be38ab39b2d235b3a2fbdd7fcd298684f1ff43d40e966a43174489da2b2680ac99dfba059b70ebcd774353265f5feab852a126ca242c97703f6e0bffea29f3c1
7
- data.tar.gz: 91e057b46a0f7be3d914af750bedd7fcc3abe174db97970a7babedd85b047bfe69a682b34723f0d60115d6d4b1f674e854c431f854ff4f054b6194c6e9ab07d3
6
+ metadata.gz: 4cafbbdb4eb3ee18f0c6fad85b865e5522d7e5b86d8d154a42b716a61bfbf57a734d2a0381f95d0d59cd6b914d910f559054725ba92ed6c3f1388421299d824e
7
+ data.tar.gz: 79c267679cbf76017bbbc2541ab239a226145c2b798fc75731492f7600edafc9efe7c56e3e5a769392b4a0dff7b1a3a8d808af8a9d341b6ab26b6c19b1676a6a
data/CHANGELOG.md CHANGED
@@ -1,3 +1,24 @@
1
+ 1.0.0.rc3 (2026-06-18)
2
+ ======================
3
+
4
+ ## Breaking Changes
5
+ * Dropped support for Ruby 3.1 and JRuby 9.4. The minimum is now Ruby 3.2 (and JRuby 10, which reports `RUBY_VERSION` 3.4). Ruby 3.1 reached end of life in March 2025, and a recent `i18n` release calls `Fiber[]`, a Ruby 3.2 API, at load time, so suites that load Rails no longer run on 3.1. Raising `required_ruby_version` to `>= 3.2` also excludes JRuby 9.4, which reports `RUBY_VERSION` 3.1.x. See #1171.
6
+
7
+ ## Enhancements
8
+ * Added `SimpleCov.parallel_wait_timeout` (default 60 seconds), which controls how long the process that writes the final report waits for the other parallel-test workers to finish writing their resultsets before it merges. Raise it when one worker runs much heavier test files and routinely finishes well after the others, so its coverage is included in the merge and the minimum and maximum coverage checks run against the full total instead of being skipped against a partial one. See #1171.
9
+
10
+ ## Bugfixes
11
+ * The "SimpleCov dropped N source file(s)" warning is now emitted at most once, from the process that writes the final report, instead of once per parallel worker. It was previously raised every time a result was built, so an eight-worker run produced eight copies. Fork-based runners that do not set `TEST_ENV_NUMBER`, such as Minitest's `parallelize`, match no parallel-test adapter, so SimpleCov now marks forked children (it already hooks `Process._fork`) and treats them as non-reporters when no adapter is active, leaving the process that did the forking to merge every slice and report once. See #1171.
12
+ * Using `[SimpleCov::Formatter::HTMLFormatter, SimpleCov::Formatter::JSONFormatter]` together no longer prints a false "coverage.json was written after this process started" warning. As of 1.0 the HTML formatter writes `coverage.json` itself, so the JSON formatter found a file its own run had just written and mistook it for a concurrent worker about to lose data. The warning is now skipped when the existing file belongs to the same merged result. Because the HTML formatter already writes `coverage.json`, listing `JSONFormatter` alongside it is redundant and can be removed. See #1171.
13
+
14
+ 1.0.0.rc2 (2026-06-10)
15
+ ======================
16
+
17
+ ## Bugfixes
18
+ * Forked subprocesses (for example Rails' `parallelize`) are now named by a stable per-run serial rather than the child's OS process id. Because the pid changed every run, a re-run's worker results were never named the same as the previous run's and so never overwrote them. They accumulated in `.resultset.json` until `merge_timeout` dropped them, and while several runs sat inside that window together SimpleCov merged them all. When the set of files had drifted between those runs (a deleted file, a changed filter) the stale results leaked into the report, inflating the denominator and changing the coverage percentage from one run to the next. The serial sequence is identical from one run to the next, so a re-run's workers now overwrite the previous run's entries and the resultset stays bounded. See #1171.
19
+ * The "Excluded N result(s) older than `merge_timeout`" warning is now emitted at most once, from the reporting process, instead of once per forked worker. Every worker merges the resultset too, and the default `at_fork` sets `print_errors false` for them, but this one warning did not honor the flag, so an eight-worker run printed eight copies. See #1171.
20
+ * Legacy-API deprecation warnings (`track_files`, `add_filter`, `add_group`, and the rest) are now deduplicated by call site and emitted at most once per source location. A deprecated method called in a loop, or a configuration block re-evaluated once per parallel worker or spec file, previously repeated the same notice until the surrounding output was unreadable. See #1204.
21
+
1
22
  1.0.0.rc1 (2026-06-02)
2
23
  ======================
3
24
 
data/README.md CHANGED
@@ -742,6 +742,12 @@ Cached coverage data eventually goes stale, so result sets older than `SimpleCov
742
742
  merge. The default is 600 seconds (10 minutes); raise or lower it with `SimpleCov.merge_timeout 3600` (1 hour), or
743
743
  `merge_timeout 3600` inside a configure/start block. Deactivate automatic merging entirely with `SimpleCov.merging false`.
744
744
 
745
+ In a parallel run, the process that writes the final report waits for the other workers to finish and write their
746
+ result sets before merging. It gives up after `SimpleCov.parallel_wait_timeout` seconds (default 60) and reports
747
+ whatever has arrived, skipping the minimum / maximum coverage checks against that partial total. If one worker runs
748
+ much heavier test files and routinely finishes a minute or more after the others, raise it with
749
+ `SimpleCov.parallel_wait_timeout 180` so its coverage is included.
750
+
745
751
  ### Merging across execution environments
746
752
 
747
753
  If your tests run in parallel across multiple build machines, download each run's `.resultset.json` and merge them into
@@ -1262,7 +1268,7 @@ anything; `-q` / `--quiet` suppresses status lines.
1262
1268
 
1263
1269
  ### Ruby version compatibility
1264
1270
 
1265
- SimpleCov is built in [Continuous Integration] on Ruby 3.1+ and JRuby 9.4+. On CRuby, every coverage criterion
1271
+ SimpleCov is built in [Continuous Integration] on Ruby 3.2+ and JRuby 10+. On CRuby, every coverage criterion
1266
1272
  described above is available on the supported versions, with one exception: [eval coverage](#eval-coverage) requires
1267
1273
  CRuby 3.2+.
1268
1274
 
@@ -110,8 +110,8 @@ module SimpleCov
110
110
 
111
111
  # DEPRECATED: prefer `enable_coverage :eval`.
112
112
  def enable_coverage_for_eval
113
- warn "#{Kernel.caller.first}: [DEPRECATION] `SimpleCov.enable_coverage_for_eval` is deprecated. " \
114
- "Replace with `SimpleCov.enable_coverage :eval`."
113
+ SimpleCov::Deprecation.warn("`SimpleCov.enable_coverage_for_eval` is deprecated. " \
114
+ "Replace with `SimpleCov.enable_coverage :eval`.")
115
115
  enable_eval_coverage
116
116
  end
117
117
 
@@ -58,8 +58,8 @@ module SimpleCov
58
58
  # historical `track_files` behavior) and restricts the report to the
59
59
  # matching set.
60
60
  def track_files(glob)
61
- warn "#{Kernel.caller.first}: [DEPRECATION] `SimpleCov.track_files` is deprecated. " \
62
- "#{track_files_replacement_hint(glob)}"
61
+ SimpleCov::Deprecation.warn("`SimpleCov.track_files` is deprecated. " \
62
+ "#{track_files_replacement_hint(glob)}")
63
63
  @tracked_files = glob
64
64
  end
65
65
 
@@ -102,8 +102,8 @@ module SimpleCov
102
102
  # DEPRECATED: alias for `skip`. Same matcher grammar, identical behavior.
103
103
  def add_filter(filter_argument = nil, &block)
104
104
  example = block ? "`SimpleCov.skip { ... }`" : "`SimpleCov.skip #{filter_argument.inspect}`"
105
- warn "#{Kernel.caller.first}: [DEPRECATION] `SimpleCov.add_filter` is deprecated. " \
106
- "Replace with `SimpleCov.skip` (same arguments, same behavior). Example: #{example}."
105
+ SimpleCov::Deprecation.warn("`SimpleCov.add_filter` is deprecated. " \
106
+ "Replace with `SimpleCov.skip` (same arguments, same behavior). Example: #{example}.")
107
107
  skip(filter_argument, &block)
108
108
  end
109
109
 
@@ -141,8 +141,10 @@ module SimpleCov
141
141
  else
142
142
  "`SimpleCov.group #{group_name.inspect}, #{filter_argument.inspect}`"
143
143
  end
144
- warn "#{Kernel.caller.first}: [DEPRECATION] `SimpleCov.add_group` is deprecated. " \
145
- "Replace with `SimpleCov.group` (same arguments, same behavior). Example: #{example}."
144
+ SimpleCov::Deprecation.warn(
145
+ "`SimpleCov.add_group` is deprecated. " \
146
+ "Replace with `SimpleCov.group` (same arguments, same behavior). Example: #{example}."
147
+ )
146
148
  group(group_name, filter_argument, &block)
147
149
  end
148
150
 
@@ -89,8 +89,8 @@ module SimpleCov
89
89
 
90
90
  # DEPRECATED: alias for `print_errors`. Same value, same behavior.
91
91
  def print_error_status
92
- warn "#{Kernel.caller.first}: [DEPRECATION] `SimpleCov.print_error_status` is deprecated. " \
93
- "Replace with `SimpleCov.print_errors` (same value)."
92
+ SimpleCov::Deprecation.warn("`SimpleCov.print_error_status` is deprecated. " \
93
+ "Replace with `SimpleCov.print_errors` (same value).")
94
94
  defined?(@print_error_status) ? @print_error_status : true
95
95
  end
96
96
 
@@ -101,8 +101,8 @@ module SimpleCov
101
101
  # be removed in a future release.
102
102
  #
103
103
  def nocov_token(nocov_token = nil)
104
- warn "#{Kernel.caller.first}: [DEPRECATION] `SimpleCov.nocov_token` and `SimpleCov.skip_token` are deprecated. " \
105
- "Replace with `# simplecov:disable` / `# simplecov:enable` block comments."
104
+ SimpleCov::Deprecation.warn("`SimpleCov.nocov_token` and `SimpleCov.skip_token` are deprecated. " \
105
+ "Replace with `# simplecov:disable` / `# simplecov:enable` block comments.")
106
106
  current_nocov_token(nocov_token)
107
107
  end
108
108
  alias skip_token nocov_token
@@ -36,8 +36,8 @@ module SimpleCov
36
36
 
37
37
  # DEPRECATED: alias for `merge_subprocesses`. Same value/behavior.
38
38
  def enable_for_subprocesses(value = nil)
39
- warn "#{Kernel.caller.first}: [DEPRECATION] `SimpleCov.enable_for_subprocesses` is deprecated. " \
40
- "Replace with `SimpleCov.merge_subprocesses` (same value, same behavior)."
39
+ SimpleCov::Deprecation.warn("`SimpleCov.enable_for_subprocesses` is deprecated. " \
40
+ "Replace with `SimpleCov.merge_subprocesses` (same value, same behavior).")
41
41
  return @enable_for_subprocesses if defined?(@enable_for_subprocesses) && value.nil?
42
42
 
43
43
  @enable_for_subprocesses = value || false
@@ -56,8 +56,8 @@ module SimpleCov
56
56
 
57
57
  # DEPRECATED: alias for `merging`. Same value, same behavior.
58
58
  def use_merging(use = nil)
59
- warn "#{Kernel.caller.first}: [DEPRECATION] `SimpleCov.use_merging` is deprecated. " \
60
- "Replace with `SimpleCov.merging` (same value, same behavior)."
59
+ SimpleCov::Deprecation.warn("`SimpleCov.use_merging` is deprecated. " \
60
+ "Replace with `SimpleCov.merging` (same value, same behavior).")
61
61
  @use_merging = use unless use.nil?
62
62
  @use_merging = true unless defined?(@use_merging) && @use_merging == false
63
63
  end
@@ -70,5 +70,18 @@ module SimpleCov
70
70
  @merge_timeout = seconds if seconds.is_a?(Integer)
71
71
  @merge_timeout ||= 600
72
72
  end
73
+
74
+ #
75
+ # Defines how long (in seconds) the reporting process waits for the
76
+ # remaining parallel-test workers to write their resultsets before it
77
+ # proceeds with a partial merge. Default is 60 seconds. Raise it when a
78
+ # slow worker routinely finishes well after the others, so its coverage
79
+ # is still included and the minimum / maximum coverage checks aren't
80
+ # skipped against a partial total.
81
+ #
82
+ def parallel_wait_timeout(seconds = nil)
83
+ @parallel_wait_timeout = seconds if seconds.is_a?(Integer)
84
+ @parallel_wait_timeout ||= 60
85
+ end
73
86
  end
74
87
  end
@@ -76,8 +76,8 @@ module SimpleCov
76
76
  coverage = {primary_coverage => coverage} if coverage.is_a?(Numeric)
77
77
  defaults, overrides = partition_per_file_thresholds(coverage)
78
78
 
79
- warn "#{Kernel.caller.first}: [DEPRECATION] `SimpleCov.minimum_coverage_by_file` is deprecated. " \
80
- "Replace it with:\n#{per_file_coverage_replacement(defaults, overrides)}"
79
+ SimpleCov::Deprecation.warn("`SimpleCov.minimum_coverage_by_file` is deprecated. " \
80
+ "Replace it with:\n#{per_file_coverage_replacement(defaults, overrides)}")
81
81
 
82
82
  raise_on_invalid_coverage(defaults, "minimum_coverage_by_file")
83
83
  overrides.each_value { |criteria| raise_on_invalid_coverage(criteria, "minimum_coverage_by_file") }
@@ -98,8 +98,8 @@ module SimpleCov
98
98
  def minimum_coverage_by_group(coverage = nil)
99
99
  return @minimum_coverage_by_group ||= {} unless coverage
100
100
 
101
- warn "#{Kernel.caller.first}: [DEPRECATION] `SimpleCov.minimum_coverage_by_group` is deprecated. " \
102
- "Replace it with:\n#{per_group_coverage_replacement(coverage)}"
101
+ SimpleCov::Deprecation.warn("`SimpleCov.minimum_coverage_by_group` is deprecated. " \
102
+ "Replace it with:\n#{per_group_coverage_replacement(coverage)}")
103
103
  @minimum_coverage_by_group = coverage.dup.transform_values do |group_coverage|
104
104
  group_coverage = {primary_coverage => group_coverage} if group_coverage.is_a?(Numeric)
105
105
  raise_on_invalid_coverage(group_coverage, "minimum_coverage_by_group")
@@ -113,14 +113,19 @@ module SimpleCov
113
113
 
114
114
  #
115
115
  # Gets or sets the behavior to start a new forked Process.
116
- # Defaults to adding " (subprocess: #{pid})" to command_name and
116
+ # Defaults to adding " (subprocess: #{serial})" to command_name and
117
117
  # starting SimpleCov in quiet mode.
118
118
  #
119
119
  def at_fork(&block)
120
120
  @at_fork = block if block
121
- @at_fork ||= lambda { |pid|
122
- # This needs a unique name so it won't be overwritten
123
- SimpleCov.command_name "#{SimpleCov.command_name} (subprocess: #{pid})"
121
+ @at_fork ||= lambda { |_pid|
122
+ # Needs a name that's unique per worker within a run yet identical
123
+ # across runs. Build it from SimpleCov's stable fork serial rather
124
+ # than the OS pid: with the pid, every run produced uniquely-named
125
+ # results that never overwrote the previous run's, so they piled up
126
+ # in .resultset.json until merge_timeout and the merged report's
127
+ # file set drifted from run to run. See issue #1171.
128
+ SimpleCov.command_name "#{SimpleCov.command_name} (subprocess: #{SimpleCov.subprocess_serial})"
124
129
  # be quiet, the parent process will use the regular formatter
125
130
  SimpleCov.print_errors false
126
131
  SimpleCov.formatter SimpleCov::Formatter::SimpleFormatter
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleCov
4
+ # Emits legacy-API deprecation warnings, deduplicated by the source
5
+ # location that triggered them. A deprecated method called in a loop —
6
+ # or a config block re-evaluated once per parallel worker / spec file —
7
+ # otherwise repeats the same notice until stderr is unreadable. Keying
8
+ # on the caller location collapses those repeats to a single line while
9
+ # still warning separately about each distinct call site the user needs
10
+ # to fix. See issue #1204.
11
+ module Deprecation
12
+ module_function
13
+
14
+ # Warn about a deprecated API. `message` is the notice without the
15
+ # `[DEPRECATION]` tag or location prefix (both are added here).
16
+ #
17
+ # `location` defaults to the caller of the deprecated method that
18
+ # called us — every shipped call site is a one-level alias such as
19
+ # `track_files`, so the frame two up is the user code. Pass `location:`
20
+ # explicitly when the relevant site isn't that frame (e.g. a source
21
+ # file and line discovered while parsing).
22
+ # `Array(...)` coerces a missing backtrace (nil) to `[]` so `.first`
23
+ # yields nil rather than raising — and, unlike `&.`, adds no branch for
24
+ # the unreachable no-caller case to the project's 100% coverage target.
25
+ def warn(message, location: Array(Kernel.caller(2..2)).first)
26
+ # Key on location when we have one (collapses a deprecated call in a
27
+ # loop to a single warning); fall back to the message so a missing
28
+ # backtrace never silently swallows every notice.
29
+ return unless emitted.add?(location || message)
30
+
31
+ Kernel.warn "#{"#{location}: " if location}[DEPRECATION] #{message}"
32
+ end
33
+
34
+ # Already-emitted dedup keys for this process. Parallel workers are
35
+ # separate processes with their own set, so each warns at most once.
36
+ def emitted
37
+ @emitted ||= Set.new
38
+ end
39
+
40
+ # @api private — reset emitted state between tests.
41
+ def reset!
42
+ @emitted = Set.new
43
+ end
44
+ end
45
+ end
@@ -8,14 +8,13 @@ require "English"
8
8
  module SimpleCov
9
9
  class << self
10
10
  # @api private
11
- CoverageLimits = Struct.new(
11
+ CoverageLimits = Data.define(
12
12
  :minimum_coverage,
13
13
  :minimum_coverage_by_file,
14
14
  :minimum_coverage_by_file_overrides,
15
15
  :minimum_coverage_by_group,
16
16
  :maximum_coverage,
17
- :maximum_coverage_drop,
18
- keyword_init: true
17
+ :maximum_coverage_drop
19
18
  )
20
19
 
21
20
  def at_exit_behavior
@@ -25,7 +25,7 @@ module SimpleCov
25
25
  def format(result)
26
26
  FileUtils.mkdir_p(output_path)
27
27
  path = File.join(output_path, FILENAME)
28
- warn_if_concurrent_overwrite(path)
28
+ warn_if_concurrent_overwrite(path, result)
29
29
  File.write(path, JSON.pretty_generate(self.class.build_hash(result)))
30
30
  # stderr, not stdout: this is a status message, not the program's
31
31
  # output. Keeps the line out of pipelines like `rspec -f json`.
@@ -46,23 +46,33 @@ module SimpleCov
46
46
  # process's start time — a strong signal that a sibling test process
47
47
  # (e.g., parallel_tests) wrote it while we were running, and that our
48
48
  # write is about to clobber their data.
49
- def warn_if_concurrent_overwrite(path)
49
+ def warn_if_concurrent_overwrite(path, result)
50
50
  start_time = SimpleCov.process_start_time or return
51
- existing_ts = existing_timestamp(path) or return
52
- return unless existing_ts > start_time
51
+ existing = existing_meta(path) or return
52
+ return unless existing[:timestamp] > start_time
53
53
 
54
- warn "simplecov: #{path} was written at #{existing_ts.iso8601} after " \
54
+ # The HTML formatter also writes coverage.json (it shares the file as
55
+ # a side artifact), so when both formatters are configured the file we
56
+ # find was just written by our own run, not a concurrent one. A
57
+ # matching command_name means the same merged result, so there's
58
+ # nothing to lose by overwriting. See issue #1171.
59
+ return if existing[:command_name] == result.command_name
60
+
61
+ warn "simplecov: #{path} was written at #{existing[:timestamp].iso8601} — after " \
55
62
  "this process started at #{start_time.iso8601}. Overwriting " \
56
63
  "likely loses coverage data from a concurrent test run. For " \
57
64
  "parallel test setups, use SimpleCov::ResultMerger or run a single " \
58
65
  "collation step after all workers finish."
59
66
  end
60
67
 
61
- def existing_timestamp(path)
68
+ def existing_meta(path)
62
69
  return nil unless File.exist?(path)
63
70
 
64
- timestamp = JSON.parse(File.read(path), symbolize_names: true).dig(:meta, :timestamp)
65
- timestamp && Time.iso8601(timestamp)
71
+ meta = JSON.parse(File.read(path), symbolize_names: true)
72
+ timestamp = meta.dig(:meta, :timestamp)
73
+ return nil unless timestamp
74
+
75
+ {timestamp: Time.iso8601(timestamp), command_name: meta.dig(:meta, :command_name)}
66
76
  rescue JSON::ParserError, ArgumentError
67
77
  nil
68
78
  end
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "set"
4
3
  require_relative "directive"
5
4
 
6
5
  module SimpleCov
@@ -8,21 +8,16 @@
8
8
  # parent this dogfood report measures.
9
9
  module SimpleCov
10
10
  class << self
11
- # @api private
12
- # How long the first worker is willing to wait for all sibling
13
- # workers' resultsets to appear in the cache before proceeding
14
- # with whatever it has. Tuned generously enough that slow CI
15
- # runners with one straggler don't trip the "incomplete results"
16
- # path on a routine basis. See #1065 for the parallel_rspec /
17
- # GenericAdapter case where there is no native wait primitive
18
- # and this poll is the only synchronization available.
19
- PARALLEL_RESULTS_WAIT_TIMEOUT = 60
20
- private_constant :PARALLEL_RESULTS_WAIT_TIMEOUT
21
-
22
11
  # @api private
23
12
  def final_result_process?
24
13
  adapter = SimpleCov::ParallelAdapters.current
25
- return true unless adapter
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
26
21
 
27
22
  adapter.first_worker?
28
23
  end
@@ -56,11 +51,13 @@ module SimpleCov
56
51
 
57
52
  # @api private — returns true when every expected worker reported
58
53
  # before the deadline, false on timeout. Single-process runs
59
- # (expected <= 1) short-circuit to true with no waiting.
54
+ # (expected <= 1) short-circuit to true with no waiting. The deadline
55
+ # is `SimpleCov.parallel_wait_timeout` seconds out; raise that setting
56
+ # when a slow worker routinely finishes well after the others.
60
57
  def wait_for_parallel_results(expected)
61
58
  return true unless expected > 1 # simplecov:disable branch — only false in real parallel runs
62
59
 
63
- deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + PARALLEL_RESULTS_WAIT_TIMEOUT
60
+ deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + parallel_wait_timeout
64
61
  loop do
65
62
  seen = SimpleCov::ResultMerger.read_resultset.size
66
63
  return true if seen >= expected
@@ -85,8 +82,9 @@ module SimpleCov
85
82
 
86
83
  warn SimpleCov::Color.colorize(
87
84
  "Only #{seen} of #{expected} parallel-test workers reported within " \
88
- "#{PARALLEL_RESULTS_WAIT_TIMEOUT}s. Coverage totals are partial; " \
89
- "minimum / maximum coverage checks are skipped for this run.",
85
+ "#{parallel_wait_timeout}s, so coverage totals are partial and minimum / " \
86
+ "maximum coverage checks are skipped for this run. Increase " \
87
+ "SimpleCov.parallel_wait_timeout if a worker routinely needs longer.",
90
88
  :yellow
91
89
  )
92
90
  end
@@ -15,8 +15,20 @@ module SimpleCov
15
15
  # the child.
16
16
  module ProcessForkHook
17
17
  def _fork
18
+ active = defined?(SimpleCov) && Coverage.running?
19
+ # Assign the next serial in the PARENT, before the fork, so the child
20
+ # inherits its own stable ordinal via copy-on-write. The default
21
+ # at_fork uses it (instead of the child pid) to name the subprocess's
22
+ # result, keeping that name identical across runs. See issue #1171.
23
+ SimpleCov.next_subprocess_serial! if active
18
24
  pid = super
19
- SimpleCov.at_fork.call(::Process.pid) if pid.zero? && defined?(SimpleCov) && Coverage.running?
25
+ if pid.zero? && active
26
+ # Mark the child here, independent of whatever custom at_fork block
27
+ # the user installed, so `final_result_process?` can keep forked
28
+ # workers from each producing the final report. See issue #1171.
29
+ SimpleCov.mark_forked_subprocess!
30
+ SimpleCov.at_fork.call(::Process.pid)
31
+ end
20
32
  pid
21
33
  end
22
34
  end
@@ -55,14 +55,14 @@ module SimpleCov
55
55
  # FilterConfig to opt out — useful for tests that build synthetic Results
56
56
  # and don't want the project's filters or groups applied.
57
57
  def initialize(original_result, command_name: nil, created_at: nil, not_loaded_files: Set.new,
58
- filter_config: FilterConfig.new)
58
+ report: false, filter_config: FilterConfig.new)
59
59
  @original_result = original_result.freeze
60
60
  @command_name = command_name
61
61
  @created_at = created_at
62
62
  @groups_config = filter_config.groups
63
63
  builder = SourceFileBuilder.new(original_result, not_loaded_files: not_loaded_files)
64
64
  @files = builder.call
65
- warn_about_missing_source_files(builder.missing_source_files, original_result.size)
65
+ warn_about_missing_source_files(builder.missing_source_files, original_result.size) if report
66
66
  apply_cover_filters!(filter_config.cover_filters)
67
67
  apply_filters!(filter_config.filters)
68
68
  end
@@ -138,6 +138,17 @@ module SimpleCov
138
138
  def warn_about_missing_source_files(missing, input_size)
139
139
  return if missing.empty?
140
140
 
141
+ # Emit only from the process that writes the final report. The merged
142
+ # result is rebuilt in every parallel worker (each one stores its own
143
+ # slice), so without this gate the warning prints once per worker — this
144
+ # is the same signal SimpleCov uses to pick the process that runs the
145
+ # report and threshold checks. It's intentionally not gated on
146
+ # print_errors: the default at_fork sets print_errors false on workers,
147
+ # and in many parallel runners the final-report process is itself a
148
+ # worker, so a print_errors gate would suppress the one warning we want.
149
+ # See issues #980 and #1171.
150
+ return unless SimpleCov.final_result_process?
151
+
141
152
  MissingSourceFilesReporter.new(
142
153
  missing,
143
154
  input_size: input_size,
@@ -11,8 +11,8 @@ module SimpleCov
11
11
  @result = result
12
12
  end
13
13
 
14
- def self.call(*args)
15
- new(*args).adapt
14
+ def self.call(*)
15
+ new(*).adapt
16
16
  end
17
17
 
18
18
  def adapt
@@ -68,6 +68,14 @@ module SimpleCov
68
68
  end
69
69
 
70
70
  def warn_about_expired_results(expired_command_names)
71
+ # Subprocesses merge the resultset too (each forked worker calls
72
+ # `SimpleCov.result` to store its slice), and the default `at_fork`
73
+ # sets `print_errors false` for them. Without this guard the warning
74
+ # is emitted once per worker — N copies of the same message for an
75
+ # N-worker run. Gate on `print_errors` like every other SimpleCov
76
+ # warning so only the reporting process speaks up.
77
+ return unless SimpleCov.print_errors
78
+
71
79
  warn "[SimpleCov]: Excluded #{expired_command_names.size} result(s) older than " \
72
80
  "merge_timeout (#{SimpleCov.merge_timeout}s) from the merged report: " \
73
81
  "#{expired_command_names.sort.join(', ')}. " \
@@ -78,7 +86,11 @@ module SimpleCov
78
86
  return nil unless coverage
79
87
 
80
88
  command_name = command_names.reject(&:empty?).sort.join(", ")
81
- SimpleCov::Result.new(coverage, command_name: command_name)
89
+ # The merged result is the authoritative one users actually see, so
90
+ # it's the one that warns about source files dropped because they no
91
+ # longer exist on disk (issue #980). The per-process slices built in
92
+ # `process_coverage_result` stay quiet to avoid one warning per worker.
93
+ SimpleCov::Result.new(coverage, command_name: command_name, report: true)
82
94
  end
83
95
 
84
96
  def merge_coverage(*results)
@@ -13,10 +13,10 @@ module SimpleCov
13
13
  # so all results in all files specified will be merged. Pass
14
14
  # `ignore_timeout: false` to honor it.
15
15
  #
16
- def collate(result_filenames, profile = nil, ignore_timeout: true, &block)
16
+ def collate(result_filenames, profile = nil, ignore_timeout: true, &)
17
17
  raise ArgumentError, "There are no reports to be merged" if result_filenames.empty?
18
18
 
19
- initial_setup(profile, &block)
19
+ initial_setup(profile, &)
20
20
 
21
21
  # Use the ResultMerger to produce a single, merged result, ready to use.
22
22
  @result = ResultMerger.merge_and_store(*result_filenames, ignore_timeout: ignore_timeout)
@@ -31,12 +31,16 @@ module SimpleCov
31
31
  def result
32
32
  return @result if result?
33
33
 
34
- # Collect our coverage result
35
- process_coverage_result if defined?(Coverage) && Coverage.running?
34
+ use_merging = merging
35
+
36
+ # Collect our coverage result. When merging is off there is no merge
37
+ # step, so this per-process result is the final one and reports any
38
+ # dropped source files; otherwise the merged result does the reporting.
39
+ process_coverage_result(report: !use_merging) if defined?(Coverage) && Coverage.running?
36
40
 
37
41
  # If we're using merging of results, store the current result
38
42
  # first (if there is one), then merge the results and return those
39
- if merging
43
+ if use_merging
40
44
  wait_for_other_processes
41
45
  SimpleCov::ResultMerger.store_result(@result) if result?
42
46
  @result = SimpleCov::ResultMerger.merged_result
@@ -152,11 +156,14 @@ module SimpleCov
152
156
  end
153
157
 
154
158
  # Run all the steps that handle processing the raw coverage result.
155
- def process_coverage_result
159
+ # `report:` is true only when this slice is the final result (merging
160
+ # off); with merging on the merged result reports dropped source files,
161
+ # so the per-process slice stays quiet to avoid one warning per worker.
162
+ def process_coverage_result(report:)
156
163
  @result = SimpleCov::UselessResultsRemover.call(Coverage.result)
157
164
  @result = SimpleCov::ResultAdapter.call(@result)
158
165
  result, not_loaded_files = add_not_loaded_files(@result)
159
- @result = SimpleCov::Result.new(result, not_loaded_files: not_loaded_files)
166
+ @result = SimpleCov::Result.new(result, not_loaded_files: not_loaded_files, report: report)
160
167
  end
161
168
  end
162
169
  end
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "set"
4
-
5
3
  module SimpleCov
6
4
  class SourceFile
7
5
  # Computes the set of line ranges that should be excluded from a
@@ -123,8 +123,8 @@ module SimpleCov
123
123
 
124
124
  # DEPRECATED: use `covered_percent(:branch)`.
125
125
  def branches_coverage_percent
126
- warn "#{Kernel.caller.first}: [DEPRECATION] `SimpleCov::SourceFile#branches_coverage_percent` is deprecated. " \
127
- "Use `covered_percent(:branch)`."
126
+ SimpleCov::Deprecation.warn("`SimpleCov::SourceFile#branches_coverage_percent` is deprecated. " \
127
+ "Use `covered_percent(:branch)`.")
128
128
  covered_percent(:branch)
129
129
  end
130
130
 
@@ -176,8 +176,8 @@ module SimpleCov
176
176
 
177
177
  # DEPRECATED: use `covered_percent(:method)`.
178
178
  def methods_coverage_percent
179
- warn "#{Kernel.caller.first}: [DEPRECATION] `SimpleCov::SourceFile#methods_coverage_percent` is deprecated. " \
180
- "Use `covered_percent(:method)`."
179
+ SimpleCov::Deprecation.warn("`SimpleCov::SourceFile#methods_coverage_percent` is deprecated. " \
180
+ "Use `covered_percent(:method)`.")
181
181
  covered_percent(:method)
182
182
  end
183
183
 
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "set"
4
-
5
3
  begin
6
4
  require "prism"
7
5
  rescue LoadError
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SimpleCov
4
- VERSION = "1.0.0.rc1"
4
+ VERSION = "1.0.0.rc3"
5
5
  end
data/lib/simplecov.rb CHANGED
@@ -21,6 +21,37 @@ module SimpleCov
21
21
  # so JSONFormatter can detect when an existing coverage.json was written
22
22
  # by a sibling process running concurrently.
23
23
  attr_accessor :process_start_time
24
+
25
+ # A monotonically increasing serial the parent assigns to each forked
26
+ # subprocess (see SimpleCov::ProcessForkHook). The default `at_fork`
27
+ # builds the worker's command_name from this rather than the OS pid:
28
+ # the serial sequence is the same from one run to the next, so a re-run
29
+ # overwrites the previous run's resultset entries instead of writing
30
+ # uniquely-named ones that pile up until merge_timeout. See issue #1171.
31
+ def subprocess_serial
32
+ @subprocess_serial ||= 0
33
+ end
34
+
35
+ # @api private — bump the serial in the parent before a fork so the
36
+ # child inherits its own ordinal via copy-on-write.
37
+ def next_subprocess_serial!
38
+ @subprocess_serial = subprocess_serial + 1
39
+ end
40
+
41
+ # @api private — true in a process that was forked while coverage was
42
+ # running (set by SimpleCov::ProcessForkHook in the child). Such a child
43
+ # stores its own slice but must not act as the final-result process: the
44
+ # process that forked it merges every slice and produces the report. Only
45
+ # consulted when no parallel-test adapter is active, since adapters answer
46
+ # `first_worker?` themselves. See issue #1171.
47
+ def forked_subprocess?
48
+ !!(defined?(@forked_subprocess) && @forked_subprocess)
49
+ end
50
+
51
+ # @api private — marked in the child immediately after a fork.
52
+ def mark_forked_subprocess!
53
+ @forked_subprocess = true
54
+ end
24
55
  # Should we take care of at_exit behavior or something else? Used by the
25
56
  # minitest plugin. See lib/minitest/simplecov_plugin.rb.
26
57
  attr_accessor :external_at_exit
@@ -132,7 +163,7 @@ module SimpleCov
132
163
 
133
164
  #
134
165
  # Trigger Coverage.start with the configured criteria. Every supported
135
- # runtime (CRuby >= 3.1, JRuby >= 9.4, TruffleRuby >= 22) accepts the
166
+ # runtime (CRuby >= 3.2, JRuby >= 10, TruffleRuby >= 22) accepts the
136
167
  # criteria-hash form, so no compatibility fallback is needed.
137
168
  #
138
169
  def start_coverage_measurement
@@ -183,9 +214,9 @@ module SimpleCov
183
214
  end
184
215
 
185
216
  # requires are down here for a load order reason I'm not sure what it is about
186
- require "set"
187
217
  require "forwardable"
188
218
  require_relative "simplecov/color"
219
+ require_relative "simplecov/deprecation"
189
220
  require_relative "simplecov/configuration"
190
221
  SimpleCov.extend SimpleCov::Configuration
191
222
  require_relative "simplecov/coverage_statistics"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: simplecov
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0.rc1
4
+ version: 1.0.0.rc3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Erik Berlin
@@ -71,6 +71,7 @@ files:
71
71
  - lib/simplecov/coverage_statistics.rb
72
72
  - lib/simplecov/coverage_violations.rb
73
73
  - lib/simplecov/defaults.rb
74
+ - lib/simplecov/deprecation.rb
74
75
  - lib/simplecov/directive.rb
75
76
  - lib/simplecov/exit_codes.rb
76
77
  - lib/simplecov/exit_codes/exit_code_handling.rb
@@ -149,9 +150,9 @@ licenses:
149
150
  metadata:
150
151
  bug_tracker_uri: https://github.com/simplecov-ruby/simplecov/issues
151
152
  changelog_uri: https://github.com/simplecov-ruby/simplecov/blob/main/CHANGELOG.md
152
- documentation_uri: https://www.rubydoc.info/gems/simplecov/1.0.0.rc1
153
+ documentation_uri: https://www.rubydoc.info/gems/simplecov/1.0.0.rc3
153
154
  mailing_list_uri: https://groups.google.com/forum/#!forum/simplecov
154
- source_code_uri: https://github.com/simplecov-ruby/simplecov/tree/v1.0.0.rc1
155
+ source_code_uri: https://github.com/simplecov-ruby/simplecov/tree/v1.0.0.rc3
155
156
  rubygems_mfa_required: 'true'
156
157
  rdoc_options: []
157
158
  require_paths:
@@ -160,14 +161,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
160
161
  requirements:
161
162
  - - ">="
162
163
  - !ruby/object:Gem::Version
163
- version: '3.1'
164
+ version: '3.2'
164
165
  required_rubygems_version: !ruby/object:Gem::Requirement
165
166
  requirements:
166
167
  - - ">="
167
168
  - !ruby/object:Gem::Version
168
169
  version: '0'
169
170
  requirements: []
170
- rubygems_version: 4.0.12
171
+ rubygems_version: 4.0.14
171
172
  specification_version: 4
172
173
  summary: Code coverage for Ruby
173
174
  test_files: []