rspec-tracer 1.2.1 → 1.2.3

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: 9bb2eb98496bfbfaf3582dd4fd91830e35dd9cfd54f242f53c2f445e9957c884
4
- data.tar.gz: a91f7c646270ec4e7402885d37702f65f3117497dc77ae66afd253075cf8a687
3
+ metadata.gz: 1c0ed3fbdb4a082d56a4d83a528dff26bd174abeb28cf1669dc5dd79de521cee
4
+ data.tar.gz: ebc2fbdfde4c6ac82072b5464f5dc0441d73efd24b4410ed96d3892ea765f1a9
5
5
  SHA512:
6
- metadata.gz: 1f205945ef89b10918091f2c2b01ca3634df82ccae6b30b4250053438890d8f8f513551b2b7f50814edfd4bd48f8bac47df97e1ad3758b78260784ec6034abe2
7
- data.tar.gz: 7ebf412bbce22cba94546e4935691e65cbc47e23304adf178a7cd8b44f3464b1194a6132badf48866d0562b7df4dda1ee4c56466c25d60f77e94067082c82369
6
+ metadata.gz: 1c28a6a997cf74dbb8d910972d9dd8e0b6e9f36aaacc9670e8a50a6bf7c0d4100f6cf421befcba8349750c3ba179a05b86d2a0cbdf740280d490d4bd42c37512
7
+ data.tar.gz: ce834032d235ea1e2775fb127ac040d6f12f560e7cf9babe5fd208a68cb5dd7260a2ac1f2af78b2a782a622902d9b8291adb2ca339837de70b9f12259e05a456
data/CHANGELOG.md CHANGED
@@ -1,3 +1,53 @@
1
+ ## [1.2.3] - 2026-05-06
2
+
3
+ ### Fixed
4
+
5
+ - **Parallel-tests purge race when a sibling worker is still mid-flush**
6
+ — the elected worker trusted only `parallel_tests`'s pid-file barrier
7
+ (`ParallelTests.wait_for_other_processes_to_finish`), which under
8
+ specific scheduling/I/O timing on GHA Linux x86_64 can return while a
9
+ sibling's `parallel_tests_N/` dir hasn't fully flushed. The elected
10
+ then merged + purged, racing the in-progress sibling. Symptoms:
11
+ intermittent leftover `parallel_tests_N/` dir post-purge AND/OR
12
+ silently dropped peer caches in the merge.
13
+
14
+ Backport of upstream PR
15
+ [#168](https://github.com/avmnu-sng/rspec-tracer/pull/168). Adds a
16
+ filesystem barrier layered on top of the pid-file wait. Each worker
17
+ writes a `.rspec_tracer_boot` marker at `RSpecTracer.start` time and
18
+ a `.rspec_tracer_done` marker as the first step of its at_exit tasks;
19
+ the elected worker waits for every booted peer's `.done` to
20
+ materialize before proceeding to merge + purge. Two independent
21
+ signals (pid file + filesystem) must agree before the elected worker
22
+ declares the peer set stable. Bounded at 5 s with a graceful warn for
23
+ crashed peers — their dirs are purged regardless of completion state,
24
+ and the merge accepts whatever's on disk.
25
+
26
+ ## [1.2.2] - 2026-05-04
27
+
28
+ ### Fixed
29
+
30
+ - **Default filter list now excludes rspec-tracer's own output
31
+ directories** (`rspec_tracer_cache/`, `rspec_tracer_coverage/`,
32
+ `rspec_tracer_report/`, and the `rspec_tracer.lock` file). Prior
33
+ versions did not exclude these paths, so any spec that read a tracer
34
+ cache file (typical for outer integration specs that assert on cache
35
+ state after a fixture subprocess run) had those paths attributed as
36
+ dependencies. The user-visible symptom was reverse-dependency reports
37
+ showing tracer-self paths as deps of unrelated specs, plus
38
+ spurious files-changed re-runs whenever the tracer rewrote its own
39
+ cache. Both `add_filter` and `add_coverage_filter` defaults updated.
40
+
41
+ - **Carry-forward filter contract** — newly added filters now apply
42
+ uniformly to both fresh attribution AND prior-snapshot carry-forward.
43
+ Previously, `Cache#load_all_files_cache` and `load_dependency_cache`
44
+ read previous-run state without re-applying the current filter list.
45
+ A user adding a new filter mid-development saw the filter take
46
+ effect only for fresh attributions on cold runs; previously-cached
47
+ paths matching the new filter persisted in `all_files` and
48
+ `dependency` until the next cold run. Filter additions now take
49
+ effect on the very next warm run.
50
+
1
51
  ## [1.2.1] - 2026-05-01
2
52
 
3
53
  ### Fixed
@@ -171,9 +171,10 @@ module RSpecTracer
171
171
 
172
172
  return unless File.file?(file_name)
173
173
 
174
- @all_files = JSON.parse(File.read(file_name, encoding: 'UTF-8')).transform_values do |files|
174
+ raw = JSON.parse(File.read(file_name, encoding: 'UTF-8')).transform_values do |files|
175
175
  files.transform_keys(&:to_sym)
176
176
  end
177
+ @all_files = raw.reject { |fname, _| filtered_by_current_filters?(fname) }
177
178
  end
178
179
 
179
180
  def load_dependency_cache(cache_dir)
@@ -181,7 +182,18 @@ module RSpecTracer
181
182
 
182
183
  return unless File.file?(file_name)
183
184
 
184
- @dependency = JSON.parse(File.read(file_name, encoding: 'UTF-8')).transform_values(&:to_set)
185
+ raw = JSON.parse(File.read(file_name, encoding: 'UTF-8')).transform_values(&:to_set)
186
+ @dependency = raw.transform_values do |files|
187
+ files.reject { |fname| filtered_by_current_filters?(fname) }.to_set
188
+ end
189
+ end
190
+
191
+ # True iff `file_name` matches any currently-configured filter.
192
+ # Applied at carry-forward seed time so newly-added filters take
193
+ # effect on the very next warm run instead of waiting for a cold
194
+ # run.
195
+ def filtered_by_current_filters?(file_name)
196
+ RSpecTracer.filters.any? { |f| f.match?(file_name: file_name) }
185
197
  end
186
198
 
187
199
  def load_examples_coverage_cache(cache_dir)
@@ -16,6 +16,10 @@ RSpecTracer.configure do
16
16
  /.rbenv/versions/
17
17
  /.asdf/installs/ruby/
18
18
  /.rvm/
19
+ /rspec_tracer_cache/
20
+ /rspec_tracer_coverage/
21
+ /rspec_tracer_report/
22
+ rspec_tracer.lock
19
23
  ].freeze
20
24
 
21
25
  coverage_filters.clear
@@ -33,6 +37,10 @@ RSpecTracer.configure do
33
37
  /.rbenv/versions/
34
38
  /.asdf/installs/ruby/
35
39
  /.rvm/
40
+ /rspec_tracer_cache/
41
+ /rspec_tracer_coverage/
42
+ /rspec_tracer_report/
43
+ rspec_tracer.lock
36
44
  ].freeze
37
45
  end
38
46
  # rubocop:enable Metrics/BlockLength
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RSpecTracer
4
- VERSION = '1.2.1'
4
+ VERSION = '1.2.3'
5
5
  end
data/lib/rspec_tracer.rb CHANGED
@@ -30,6 +30,17 @@ require_relative 'rspec_tracer/time_formatter'
30
30
  require_relative 'rspec_tracer/version'
31
31
 
32
32
  module RSpecTracer
33
+ # Filesystem barrier markers, layered on top of parallel_tests's
34
+ # pid-file wait to defend against the GHA-observed race where the
35
+ # gem's `wait_for_other_processes_to_finish` returns while a sibling
36
+ # worker hasn't fully flushed its `parallel_tests_N/` dir yet. Each
37
+ # worker writes BOOT at setup-time and DONE as the first step of its
38
+ # at_exit tasks; the elected worker waits for every booted peer's
39
+ # DONE marker (deadline-bounded) before proceeding to merge + purge.
40
+ PARALLEL_TESTS_BOOT_MARKER_FILENAME = '.rspec_tracer_boot'
41
+ PARALLEL_TESTS_DONE_MARKER_FILENAME = '.rspec_tracer_done'
42
+ PARALLEL_TESTS_PEER_DONE_DEADLINE_SECONDS = 5
43
+
33
44
  class << self
34
45
  attr_accessor :running, :pid, :no_examples, :duplicate_examples
35
46
 
@@ -186,6 +197,29 @@ module RSpecTracer
186
197
  RSpecTracer.logger.error "Failed to load parallel tests (Error: #{e.message})"
187
198
  ensure
188
199
  track_parallel_tests_test_env_number
200
+ parallel_tests_touch_boot!
201
+ end
202
+
203
+ # Per-worker boot marker. Source-of-truth for "this worker booted
204
+ # past `RSpecTracer.start`", consumed by the elected worker's
205
+ # finalize-time peer enumeration. Idempotent; failures are warned
206
+ # and absorbed (boot-marker write must never block test execution).
207
+ def parallel_tests_touch_boot!
208
+ return unless parallel_tests?
209
+
210
+ FileUtils.mkdir_p(RSpecTracer.cache_path)
211
+ File.write(
212
+ File.join(RSpecTracer.cache_path, PARALLEL_TESTS_BOOT_MARKER_FILENAME),
213
+ JSON.generate(
214
+ pid: Process.pid,
215
+ test_env_number: ENV.fetch('TEST_ENV_NUMBER', ''),
216
+ started_at: Time.now.utc.iso8601
217
+ )
218
+ )
219
+ rescue StandardError => e
220
+ RSpecTracer.logger.warn(
221
+ "RSpec tracer: failed to write boot marker (#{e.class}: #{e.message})"
222
+ )
189
223
  end
190
224
 
191
225
  def track_parallel_tests_test_env_number
@@ -315,6 +349,15 @@ module RSpecTracer
315
349
  end
316
350
 
317
351
  def run_parallel_tests_exit_tasks
352
+ # Every worker — elected or not — drops its `.done` marker as the
353
+ # first thing in finalize so the elected worker's
354
+ # `parallel_tests_wait_for_peer_done_markers!` can observe it.
355
+ # Non-elected workers stop here; the elected worker proceeds to
356
+ # the merge + purge sequence (gated by `parallel_tests_executed?`,
357
+ # which now layers the peer-done barrier on top of the existing
358
+ # pid-file wait).
359
+ parallel_tests_touch_done!
360
+
318
361
  return unless parallel_tests_executed?
319
362
 
320
363
  merge_parallel_tests_reports
@@ -324,6 +367,26 @@ module RSpecTracer
324
367
  purge_parallel_tests_reports
325
368
  end
326
369
 
370
+ # Per-worker done marker. Written by every worker (elected or not)
371
+ # as the first step of `run_parallel_tests_exit_tasks`. Pairs with
372
+ # the boot marker for the elected worker's peer-done barrier:
373
+ # presence of `.done` means "this worker has signalled completion
374
+ # of its own writes"; absence (with `.boot` present) means "still
375
+ # mid-flush or crashed". Idempotent; failures are warned + absorbed.
376
+ def parallel_tests_touch_done!
377
+ return unless parallel_tests?
378
+
379
+ FileUtils.mkdir_p(RSpecTracer.cache_path)
380
+ File.write(
381
+ File.join(RSpecTracer.cache_path, PARALLEL_TESTS_DONE_MARKER_FILENAME),
382
+ Time.now.utc.iso8601
383
+ )
384
+ rescue StandardError => e
385
+ RSpecTracer.logger.warn(
386
+ "RSpec tracer: failed to write done marker (#{e.class}: #{e.message})"
387
+ )
388
+ end
389
+
327
390
  def merge_parallel_tests_reports
328
391
  return unless parallel_tests_executed?
329
392
 
@@ -423,9 +486,64 @@ module RSpecTracer
423
486
 
424
487
  ParallelTests.wait_for_other_processes_to_finish
425
488
 
489
+ # Belt-and-suspenders barrier: pid-file said everyone's done, but
490
+ # the gem's `wait_for_other_processes_to_finish` has been observed
491
+ # on GHA Linux x86_64 to return while a sibling's `parallel_tests_N/`
492
+ # is still mid-flush. Cross-check via the `.boot`/`.done` filesystem
493
+ # markers before declaring the peer set stable. Idempotent: once
494
+ # all peers have flushed, subsequent calls just glob, find nothing
495
+ # missing, and return.
496
+ parallel_tests_wait_for_peer_done_markers!
497
+
426
498
  true
427
499
  end
428
500
 
501
+ # Block until every peer that wrote `.boot` has also written `.done`,
502
+ # or the deadline elapses. Polled at 50ms — fine enough to close the
503
+ # typical "barrier returned a tick early" case within a poll or two,
504
+ # coarse enough not to dominate CPU.
505
+ #
506
+ # On timeout we log a warn and proceed: a peer that never wrote
507
+ # `.done` either crashed (then its dir is orphan content; the
508
+ # subsequent purge cleans it) or is genuinely hung (the elected
509
+ # can't fix that — we choose merge correctness over indefinite wait).
510
+ def parallel_tests_wait_for_peer_done_markers!
511
+ base_dir = File.dirname(RSpecTracer.cache_path)
512
+ deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + PARALLEL_TESTS_PEER_DONE_DEADLINE_SECONDS
513
+
514
+ loop do
515
+ missing = parallel_tests_peer_dirs_missing_done(base_dir)
516
+ return if missing.empty?
517
+
518
+ if Process.clock_gettime(Process::CLOCK_MONOTONIC) >= deadline
519
+ RSpecTracer.logger.warn(
520
+ 'RSpec tracer: peers booted without finishing within ' \
521
+ "#{PARALLEL_TESTS_PEER_DONE_DEADLINE_SECONDS}s: #{missing.inspect}; " \
522
+ 'proceeding (peer dirs will be purged regardless of completion state)'
523
+ )
524
+ return
525
+ end
526
+
527
+ sleep 0.05
528
+ end
529
+ end
530
+
531
+ # Set difference of `.boot`-bearing peer dirs and `.done`-bearing
532
+ # peer dirs under `base_dir`. A returned entry means "this peer
533
+ # registered but has not signalled completion yet" — either still
534
+ # mid-flush or crashed.
535
+ def parallel_tests_peer_dirs_missing_done(base_dir)
536
+ boot_dirs = parallel_tests_peer_dirs_with_marker(base_dir, PARALLEL_TESTS_BOOT_MARKER_FILENAME)
537
+ done_dirs = parallel_tests_peer_dirs_with_marker(base_dir, PARALLEL_TESTS_DONE_MARKER_FILENAME)
538
+ boot_dirs - done_dirs
539
+ end
540
+
541
+ def parallel_tests_peer_dirs_with_marker(base_dir, marker_filename)
542
+ Dir.glob(File.join(base_dir, 'parallel_tests_*', marker_filename)).map do |path|
543
+ File.dirname(path)
544
+ end
545
+ end
546
+
429
547
  # Elects the worker that performs the per-run merge. Delegates to
430
548
  # `::ParallelTests.first_process?`, which returns true iff
431
549
  # `TEST_ENV_NUMBER.to_i <= 1` — i.e. for exactly one worker
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rspec-tracer
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.1
4
+ version: 1.2.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Abhimanyu Singh
@@ -111,7 +111,7 @@ licenses:
111
111
  - MIT
112
112
  metadata:
113
113
  homepage_uri: https://github.com/avmnu-sng/rspec-tracer
114
- source_code_uri: https://github.com/avmnu-sng/rspec-tracer/tree/v1.2.1
114
+ source_code_uri: https://github.com/avmnu-sng/rspec-tracer/tree/v1.2.3
115
115
  changelog_uri: https://github.com/avmnu-sng/rspec-tracer/blob/main/CHANGELOG.md
116
116
  bug_tracker_uri: https://github.com/avmnu-sng/rspec-tracer/issues
117
117
  rdoc_options: []