simplecov 1.0.0.rc2 → 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: 95cc757bf172ce50cd60e636048c88e30fade408c962d8fe8c9478465b6b2d67
4
- data.tar.gz: 9ee4b98d8f3f790289415b03c3e37c436b32d1e2a9d193fd435dfdb966d4b658
3
+ metadata.gz: eda0f749acc9b248635b6b1128c601b28bd77304994f78ebbbec0fc9097a2969
4
+ data.tar.gz: 8d55b00892b8f2cd47f7d3960c52e5d177c4951a77cbcd4af42282ae7ec745fa
5
5
  SHA512:
6
- metadata.gz: 719bcdfee0e3e79de52e7da4fdd955d85416b2c33c3b8bcd437d13187b9d883c6430ea55e0d52ca17c392cfe4e506f6a433278d5d285091858702bd669eb43bd
7
- data.tar.gz: 4e70c6fabe5ed02ea4ece1ca811434c96b760e871507cc607a05af64710a9f0c14ee87c16dbcf82d19cdaaf23f3896d78ea0500e051130dbc15b7c6046e4b6b8
6
+ metadata.gz: 4cafbbdb4eb3ee18f0c6fad85b865e5522d7e5b86d8d154a42b716a61bfbf57a734d2a0381f95d0d59cd6b914d910f559054725ba92ed6c3f1388421299d824e
7
+ data.tar.gz: 79c267679cbf76017bbbc2541ab239a226145c2b798fc75731492f7600edafc9efe7c56e3e5a769392b4a0dff7b1a3a8d808af8a9d341b6ab26b6c19b1676a6a
data/CHANGELOG.md CHANGED
@@ -1,3 +1,16 @@
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
+
1
14
  1.0.0.rc2 (2026-06-10)
2
15
  ======================
3
16
 
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
 
@@ -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
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "set"
4
-
5
3
  module SimpleCov
6
4
  # Emits legacy-API deprecation warnings, deduplicated by the source
7
5
  # location that triggered them. A deprecated method called in a loop —
@@ -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
@@ -22,7 +22,13 @@ module SimpleCov
22
22
  # result, keeping that name identical across runs. See issue #1171.
23
23
  SimpleCov.next_subprocess_serial! if active
24
24
  pid = super
25
- SimpleCov.at_fork.call(::Process.pid) if pid.zero? && active
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
26
32
  pid
27
33
  end
28
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
@@ -86,7 +86,11 @@ module SimpleCov
86
86
  return nil unless coverage
87
87
 
88
88
  command_name = command_names.reject(&:empty?).sort.join(", ")
89
- 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)
90
94
  end
91
95
 
92
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
@@ -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.rc2"
4
+ VERSION = "1.0.0.rc3"
5
5
  end
data/lib/simplecov.rb CHANGED
@@ -37,6 +37,21 @@ module SimpleCov
37
37
  def next_subprocess_serial!
38
38
  @subprocess_serial = subprocess_serial + 1
39
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
40
55
  # Should we take care of at_exit behavior or something else? Used by the
41
56
  # minitest plugin. See lib/minitest/simplecov_plugin.rb.
42
57
  attr_accessor :external_at_exit
@@ -148,7 +163,7 @@ module SimpleCov
148
163
 
149
164
  #
150
165
  # Trigger Coverage.start with the configured criteria. Every supported
151
- # runtime (CRuby >= 3.1, JRuby >= 9.4, TruffleRuby >= 22) accepts the
166
+ # runtime (CRuby >= 3.2, JRuby >= 10, TruffleRuby >= 22) accepts the
152
167
  # criteria-hash form, so no compatibility fallback is needed.
153
168
  #
154
169
  def start_coverage_measurement
@@ -199,7 +214,6 @@ module SimpleCov
199
214
  end
200
215
 
201
216
  # requires are down here for a load order reason I'm not sure what it is about
202
- require "set"
203
217
  require "forwardable"
204
218
  require_relative "simplecov/color"
205
219
  require_relative "simplecov/deprecation"
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.rc2
4
+ version: 1.0.0.rc3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Erik Berlin
@@ -150,9 +150,9 @@ licenses:
150
150
  metadata:
151
151
  bug_tracker_uri: https://github.com/simplecov-ruby/simplecov/issues
152
152
  changelog_uri: https://github.com/simplecov-ruby/simplecov/blob/main/CHANGELOG.md
153
- documentation_uri: https://www.rubydoc.info/gems/simplecov/1.0.0.rc2
153
+ documentation_uri: https://www.rubydoc.info/gems/simplecov/1.0.0.rc3
154
154
  mailing_list_uri: https://groups.google.com/forum/#!forum/simplecov
155
- source_code_uri: https://github.com/simplecov-ruby/simplecov/tree/v1.0.0.rc2
155
+ source_code_uri: https://github.com/simplecov-ruby/simplecov/tree/v1.0.0.rc3
156
156
  rubygems_mfa_required: 'true'
157
157
  rdoc_options: []
158
158
  require_paths:
@@ -161,7 +161,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
161
161
  requirements:
162
162
  - - ">="
163
163
  - !ruby/object:Gem::Version
164
- version: '3.1'
164
+ version: '3.2'
165
165
  required_rubygems_version: !ruby/object:Gem::Requirement
166
166
  requirements:
167
167
  - - ">="