rspec-tracer 1.0.2 → 1.0.4

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: 57dd6b7ba2dafd73bbe30d91e42c36498c7e4fb505e02c405825cf45d8ac0787
4
- data.tar.gz: e8886b116e2412f566ae908d3f0e2d9213df9ff70e0c88d54a8a4fb28dab9788
3
+ metadata.gz: 24ed84e70e415d8eccd09ea63cee04cc318e4a167227dc9f937a3badb7229fb6
4
+ data.tar.gz: fa6d30e91e776b35b935b5e0e420422700d552859d894d001e16fee2b93e0874
5
5
  SHA512:
6
- metadata.gz: 88ea3fbe3fef4c4ab2b2d2664ac5915b4e7eaa97b5ecc0d2b77dfeb03604e7f54faf4a567f65ff31235464f351a60e8817527d51f64f5a2717bb267f1cf72261
7
- data.tar.gz: fbba1feed081e49a4cef7338c05b6d1022ecac5f013b499150b42b817e1cdb13bf2dc29ad993a502d8233bfe52dc89f991ad036eaeb513cde0b5d029b6413a46
6
+ metadata.gz: 4c3abb2234b5c02257f70c494775a4d01d5fe84dfeb9d61eba92f70064a649045607ac801b526ca8198b3defc75db8751db68597557ae379666f0c979236cbe0
7
+ data.tar.gz: 23b9a629e8a8d93af51d74b2d426c6770db64ed9971c50fb269f96dd74abcb1badc520f02289725d96a97a7e5077210e6d54e3405da2510a799a4ec956cd81c9
data/CHANGELOG.md CHANGED
@@ -1,3 +1,59 @@
1
+ ## [1.0.4] - 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
23
+ for crashed peers — their dirs are purged regardless of completion
24
+ state, and the merge accepts whatever's on disk.
25
+
26
+ ## [1.0.3] - 2026-05-04
27
+
28
+ ### Fixed
29
+
30
+ - **Carry-forward filter contract** — newly-added filters now apply
31
+ uniformly to both fresh attribution AND prior-snapshot carry-forward.
32
+ Previously, `Cache#load_all_files_cache` and `load_dependency_cache`
33
+ read previous-run state without re-applying the current filter list.
34
+ A user adding a new filter mid-development saw the filter take
35
+ effect only for fresh attributions on cold runs; previously-cached
36
+ paths matching the new filter persisted in `all_files` and
37
+ `dependency` until the next cold run. Filter additions now take
38
+ effect on the very next warm run. (from upstream
39
+ [PR #161](https://github.com/avmnu-sng/rspec-tracer/pull/161))
40
+
41
+ ### Note on exclusions
42
+
43
+ The companion default-filter expansion shipped in upstream PR #161
44
+ (adds `rspec_tracer_cache/`, `rspec_tracer_coverage/`,
45
+ `rspec_tracer_report/`, `rspec_tracer.lock` to the default
46
+ `add_filter` / `add_coverage_filter` lists) is intentionally NOT
47
+ backported to this 1.0.x line, for the same reason the broader
48
+ 1.1.0 default-filter expansion was excluded from 1.0.1: changing
49
+ the default filter set shifts the files present in `all_files.json`
50
+ for users who track tracer-self paths, which would invalidate their
51
+ existing caches on upgrade. Users on 1.0.x who want this default
52
+ hygiene can either upgrade to 1.2.x / 2.0.x OR add the four paths
53
+ to their own `.rspec-tracer` config explicitly. The carry-forward
54
+ filter check shipped here MAKES that user-side `add_filter` take
55
+ effect on the next warm run.
56
+
1
57
  ## [1.0.2] - 2026-05-01
2
58
 
3
59
  ### Fixed
@@ -152,9 +152,10 @@ module RSpecTracer
152
152
 
153
153
  return unless File.file?(file_name)
154
154
 
155
- @all_files = JSON.parse(File.read(file_name, encoding: 'UTF-8')).transform_values do |files|
155
+ raw = JSON.parse(File.read(file_name, encoding: 'UTF-8')).transform_values do |files|
156
156
  files.transform_keys(&:to_sym)
157
157
  end
158
+ @all_files = raw.reject { |fname, _| filtered_by_current_filters?(fname) }
158
159
  end
159
160
 
160
161
  def load_dependency_cache(cache_dir)
@@ -162,7 +163,18 @@ module RSpecTracer
162
163
 
163
164
  return unless File.file?(file_name)
164
165
 
165
- @dependency = JSON.parse(File.read(file_name, encoding: 'UTF-8')).transform_values(&:to_set)
166
+ raw = JSON.parse(File.read(file_name, encoding: 'UTF-8')).transform_values(&:to_set)
167
+ @dependency = raw.transform_values do |files|
168
+ files.reject { |fname| filtered_by_current_filters?(fname) }.to_set
169
+ end
170
+ end
171
+
172
+ # True iff `file_name` matches any currently-configured filter.
173
+ # Applied at carry-forward seed time so newly-added filters take
174
+ # effect on the very next warm run instead of waiting for a cold
175
+ # run.
176
+ def filtered_by_current_filters?(file_name)
177
+ RSpecTracer.filters.any? { |f| f.match?(file_name: file_name) }
166
178
  end
167
179
 
168
180
  def load_examples_coverage_cache(cache_dir)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RSpecTracer
4
- VERSION = '1.0.2'
4
+ VERSION = '1.0.4'
5
5
  end
data/lib/rspec_tracer.rb CHANGED
@@ -32,6 +32,17 @@ require_relative 'rspec_tracer/time_formatter'
32
32
  require_relative 'rspec_tracer/version'
33
33
 
34
34
  module RSpecTracer
35
+ # Filesystem barrier markers, layered on top of parallel_tests's
36
+ # pid-file wait to defend against the GHA-observed race where the
37
+ # gem's `wait_for_other_processes_to_finish` returns while a sibling
38
+ # worker hasn't fully flushed its `parallel_tests_N/` dir yet. Each
39
+ # worker writes BOOT at setup-time and DONE as the first step of its
40
+ # at_exit tasks; the elected worker waits for every booted peer's
41
+ # DONE marker (deadline-bounded) before proceeding to merge + purge.
42
+ PARALLEL_TESTS_BOOT_MARKER_FILENAME = '.rspec_tracer_boot'
43
+ PARALLEL_TESTS_DONE_MARKER_FILENAME = '.rspec_tracer_done'
44
+ PARALLEL_TESTS_PEER_DONE_DEADLINE_SECONDS = 5
45
+
35
46
  class << self
36
47
  attr_accessor :running, :pid, :no_examples
37
48
 
@@ -191,6 +202,27 @@ module RSpecTracer
191
202
  puts "Failed to load parallel tests (Error: #{e.message})"
192
203
  ensure
193
204
  track_parallel_tests_test_env_number
205
+ parallel_tests_touch_boot!
206
+ end
207
+
208
+ # Per-worker boot marker. Source-of-truth for "this worker booted
209
+ # past `RSpecTracer.start`", consumed by the elected worker's
210
+ # finalize-time peer enumeration. Idempotent; failures are warned
211
+ # and absorbed (boot-marker write must never block test execution).
212
+ def parallel_tests_touch_boot!
213
+ return unless parallel_tests?
214
+
215
+ FileUtils.mkdir_p(RSpecTracer.cache_path)
216
+ File.write(
217
+ File.join(RSpecTracer.cache_path, PARALLEL_TESTS_BOOT_MARKER_FILENAME),
218
+ JSON.generate(
219
+ pid: Process.pid,
220
+ test_env_number: ENV.fetch('TEST_ENV_NUMBER', ''),
221
+ started_at: Time.now.utc.iso8601
222
+ )
223
+ )
224
+ rescue StandardError => e
225
+ puts "RSpec tracer: failed to write boot marker (#{e.class}: #{e.message})"
194
226
  end
195
227
 
196
228
  def track_parallel_tests_test_env_number
@@ -324,6 +356,15 @@ module RSpecTracer
324
356
  end
325
357
 
326
358
  def run_parallel_tests_exit_tasks
359
+ # Every worker — elected or not — drops its `.done` marker as the
360
+ # first thing in finalize so the elected worker's
361
+ # `parallel_tests_wait_for_peer_done_markers!` can observe it.
362
+ # Non-elected workers stop here; the elected worker proceeds to
363
+ # the merge + purge sequence (gated by `parallel_tests_executed?`,
364
+ # which now layers the peer-done barrier on top of the existing
365
+ # pid-file wait).
366
+ parallel_tests_touch_done!
367
+
327
368
  return unless parallel_tests_executed?
328
369
 
329
370
  merge_parallel_tests_reports
@@ -333,6 +374,24 @@ module RSpecTracer
333
374
  purge_parallel_tests_reports
334
375
  end
335
376
 
377
+ # Per-worker done marker. Written by every worker (elected or not)
378
+ # as the first step of `run_parallel_tests_exit_tasks`. Pairs with
379
+ # the boot marker for the elected worker's peer-done barrier:
380
+ # presence of `.done` means "this worker has signalled completion
381
+ # of its own writes"; absence (with `.boot` present) means "still
382
+ # mid-flush or crashed". Idempotent; failures are warned + absorbed.
383
+ def parallel_tests_touch_done!
384
+ return unless parallel_tests?
385
+
386
+ FileUtils.mkdir_p(RSpecTracer.cache_path)
387
+ File.write(
388
+ File.join(RSpecTracer.cache_path, PARALLEL_TESTS_DONE_MARKER_FILENAME),
389
+ Time.now.utc.iso8601
390
+ )
391
+ rescue StandardError => e
392
+ puts "RSpec tracer: failed to write done marker (#{e.class}: #{e.message})"
393
+ end
394
+
336
395
  def merge_parallel_tests_reports
337
396
  return unless parallel_tests_executed?
338
397
 
@@ -432,9 +491,62 @@ module RSpecTracer
432
491
 
433
492
  ParallelTests.wait_for_other_processes_to_finish
434
493
 
494
+ # Belt-and-suspenders barrier: pid-file said everyone's done, but
495
+ # the gem's `wait_for_other_processes_to_finish` has been observed
496
+ # on GHA Linux x86_64 to return while a sibling's `parallel_tests_N/`
497
+ # is still mid-flush. Cross-check via the `.boot`/`.done` filesystem
498
+ # markers before declaring the peer set stable. Idempotent: once
499
+ # all peers have flushed, subsequent calls just glob, find nothing
500
+ # missing, and return.
501
+ parallel_tests_wait_for_peer_done_markers!
502
+
435
503
  true
436
504
  end
437
505
 
506
+ # Block until every peer that wrote `.boot` has also written `.done`,
507
+ # or the deadline elapses. Polled at 50ms — fine enough to close the
508
+ # typical "barrier returned a tick early" case within a poll or two,
509
+ # coarse enough not to dominate CPU.
510
+ #
511
+ # On timeout we log and proceed: a peer that never wrote `.done`
512
+ # either crashed (then its dir is orphan content; the subsequent
513
+ # purge cleans it) or is genuinely hung (the elected can't fix that
514
+ # — we choose merge correctness over indefinite wait).
515
+ def parallel_tests_wait_for_peer_done_markers!
516
+ base_dir = File.dirname(RSpecTracer.cache_path)
517
+ deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + PARALLEL_TESTS_PEER_DONE_DEADLINE_SECONDS
518
+
519
+ loop do
520
+ missing = parallel_tests_peer_dirs_missing_done(base_dir)
521
+ return if missing.empty?
522
+
523
+ if Process.clock_gettime(Process::CLOCK_MONOTONIC) >= deadline
524
+ puts 'RSpec tracer: peers booted without finishing within ' \
525
+ "#{PARALLEL_TESTS_PEER_DONE_DEADLINE_SECONDS}s: #{missing.inspect}; " \
526
+ 'proceeding (peer dirs will be purged regardless of completion state)'
527
+ return
528
+ end
529
+
530
+ sleep 0.05
531
+ end
532
+ end
533
+
534
+ # Set difference of `.boot`-bearing peer dirs and `.done`-bearing
535
+ # peer dirs under `base_dir`. A returned entry means "this peer
536
+ # registered but has not signalled completion yet" — either still
537
+ # mid-flush or crashed.
538
+ def parallel_tests_peer_dirs_missing_done(base_dir)
539
+ boot_dirs = parallel_tests_peer_dirs_with_marker(base_dir, PARALLEL_TESTS_BOOT_MARKER_FILENAME)
540
+ done_dirs = parallel_tests_peer_dirs_with_marker(base_dir, PARALLEL_TESTS_DONE_MARKER_FILENAME)
541
+ boot_dirs - done_dirs
542
+ end
543
+
544
+ def parallel_tests_peer_dirs_with_marker(base_dir, marker_filename)
545
+ Dir.glob(File.join(base_dir, 'parallel_tests_*', marker_filename)).map do |path|
546
+ File.dirname(path)
547
+ end
548
+ end
549
+
438
550
  # Elects the worker that performs the per-run merge. Delegates to
439
551
  # `::ParallelTests.first_process?`, which returns true iff
440
552
  # `TEST_ENV_NUMBER.to_i <= 1` — i.e. for exactly one worker per run,
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.0.2
4
+ version: 1.0.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Abhimanyu Singh
@@ -118,7 +118,7 @@ licenses:
118
118
  - MIT
119
119
  metadata:
120
120
  homepage_uri: https://github.com/avmnu-sng/rspec-tracer
121
- source_code_uri: https://github.com/avmnu-sng/rspec-tracer/tree/v1.0.2
121
+ source_code_uri: https://github.com/avmnu-sng/rspec-tracer/tree/v1.0.4
122
122
  changelog_uri: https://github.com/avmnu-sng/rspec-tracer/blob/main/CHANGELOG.md
123
123
  bug_tracker_uri: https://github.com/avmnu-sng/rspec-tracer/issues
124
124
  rdoc_options: []