rspec-tracer 1.1.2 → 1.2.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 43ff2641751b61443860449bb5857cfeb2a4028881e05a7c58001f1bc6a7681d
4
- data.tar.gz: aabe43c08122caf4e0af52ba77ef5ec85443953803488aae5d5bba83af7cdf6e
3
+ metadata.gz: 9bb2eb98496bfbfaf3582dd4fd91830e35dd9cfd54f242f53c2f445e9957c884
4
+ data.tar.gz: a91f7c646270ec4e7402885d37702f65f3117497dc77ae66afd253075cf8a687
5
5
  SHA512:
6
- metadata.gz: 90b4b9795ff2b78e8c8a167e90736d639a74a4b4b082e69813cbaf66f4001852e603ad81ad761bb03565d7d4a0319d52ee3d0d79857b7880a7de82c1932b8640
7
- data.tar.gz: 8784ef81e6fc6e076b64d22372215feeba7cfb7b5349b79b2b2e1618a745cced55c29e24ae768b941bda4f3a3058b28420ef4533a8b343f4e112d3bc9b47723f
6
+ metadata.gz: 1f205945ef89b10918091f2c2b01ca3634df82ccae6b30b4250053438890d8f8f513551b2b7f50814edfd4bd48f8bac47df97e1ad3758b78260784ec6034abe2
7
+ data.tar.gz: 7ebf412bbce22cba94546e4935691e65cbc47e23304adf178a7cd8b44f3464b1194a6132badf48866d0562b7df4dda1ee4c56466c25d60f77e94067082c82369
data/CHANGELOG.md CHANGED
@@ -1,3 +1,86 @@
1
+ ## [1.2.1] - 2026-05-01
2
+
3
+ ### Fixed
4
+
5
+ - **Parallel-tests merge silently dropped peer caches and left worker
6
+ directories behind** when the spawned-worker count exceeded
7
+ `ENV['PARALLEL_TEST_GROUPS']`. The merge + purge call-sites in
8
+ `lib/rspec_tracer.rb` (`merge_parallel_tests_reports`,
9
+ `merge_parallel_tests_coverage_reports`,
10
+ `purge_parallel_tests_reports`) iterated `1..ENV['PARALLEL_TEST_GROUPS']`
11
+ to construct per-worker directory names. But parallel_tests sets
12
+ `PARALLEL_TEST_GROUPS = num_processes.to_s` for each child, where
13
+ `num_processes` is the user-requested process count
14
+ (`Parallel.processor_count` by default) — not the actual worker
15
+ count. When `num_processes < spawned_worker_count` (e.g. when the
16
+ spec-count partition produces more non-empty groups than
17
+ `num_processes`, or shared-runner CPU detection drifts mid-run),
18
+ peer caches with `TEST_ENV_NUMBER` above the env bound were silently
19
+ dropped from the merge (warm-run skip decisions get made against
20
+ an under-sampled merged manifest) and left behind by the purge
21
+ (visible as straggler `parallel_tests_<N>/` directories under
22
+ `rspec_tracer_cache/`). The same gem behaviour was documented on
23
+ v1.1.1's `last_process?` fix
24
+ ([PR #101](https://github.com/avmnu-sng/rspec-tracer/pull/101)) but
25
+ the iteration call-sites kept the buggy bound. Each method now globs
26
+ the actual `parallel_tests_*` subdirectories under its base path,
27
+ making the merge + purge robust to whatever count parallel_tests
28
+ spawned. No cache format change.
29
+
30
+ ## [1.2.0] - 2026-04-24
31
+
32
+ ### Added
33
+
34
+ - **`USE_TEST_SUITE_ID_CACHE` env flag** for per-suite remote-cache
35
+ validation. When set to the exact string `"true"`, the validator
36
+ accepts the current `TEST_SUITE_ID`'s cache independently of peer
37
+ suites' state — so an aborted suite on one CI run no longer forces
38
+ a cold run across every suite on the next. Default unset preserves
39
+ 1.1.x behaviour byte-for-byte. Ports
40
+ [upstream PR #70](https://github.com/avmnu-sng/rspec-tracer/pull/70)
41
+ (kirpalsangha), with credit to the interviewstreet fork
42
+ ([PR #1](https://github.com/interviewstreet/rspec-tracer/pull/1))
43
+ where the same fix was shipped to HackerRank's production CI in 2024.
44
+
45
+ ### Fixed
46
+
47
+ - **`SourceFile#file_path` silently dropped dependencies on external
48
+ absolute paths** (closes the issue behind upstream PRs
49
+ [#37](https://github.com/avmnu-sng/rspec-tracer/pull/37) and
50
+ [#67](https://github.com/avmnu-sng/rspec-tracer/pull/67)). When a
51
+ spec's `metadata[:file_path]` resolved to an absolute path **outside**
52
+ `RSpecTracer.root` — typical for shared examples from vendored gems
53
+ (`/opt/bundle/gems/rspec-rails-X/lib/shared_examples/...`) or for
54
+ monorepo spec files adjacent to the project tree — the method stripped
55
+ the leading `/` and expanded against `RSpecTracer.root`, producing a
56
+ non-existent path like `<root>/opt/bundle/...`. `File.file?` returned
57
+ false, `from_path` returned nil, and the tracer **silently skipped
58
+ dependency registration** for that file. Cache-staleness silent-
59
+ correctness bug: shared examples from external gems never appeared as
60
+ dependencies, so changes to them never invalidated the cache. The fix
61
+ returns the input unchanged only when it's an absolute path to an
62
+ existing file outside `RSpecTracer.root`; all other inputs (relative
63
+ paths, stripped-root forms, in-root absolute paths) continue through
64
+ the existing project-relative expansion, preserving byte-for-byte
65
+ behaviour for 1.1.x configurations.
66
+ - **`ValidationError` constant now defined.** `remote_cache/validator.rb`
67
+ previously referenced `ValidationError` in its XOR-guard `raise` but
68
+ the constant was never declared anywhere. Tripping the guard
69
+ (setting exactly one of `TEST_SUITE_ID` / `TEST_SUITES`) raised
70
+ `NameError: uninitialized constant` instead of the intended
71
+ error class. Added `class ValidationError < StandardError` inside
72
+ `Validator`, mirroring `Aws::AwsError`.
73
+ - **Typo in the XOR-guard error message** — `"enviornment"` →
74
+ `"environment"`.
75
+ - **Single-suite `@cached_files_regex` now anchored** — `$` appended
76
+ so the pattern no longer matches extraneous-extension files like
77
+ `/ref/hash/foo.json.backup`. Multi-suite regex was already anchored
78
+ in 1.1.x.
79
+ - **`remote_cache/aws.rb#upload_dir` error message** — reported
80
+ `"Failed to download files from …"` when the upload failed. Closes
81
+ upstream [PR #64](https://github.com/avmnu-sng/rspec-tracer/pull/64)
82
+ (C3).
83
+
1
84
  ## [1.1.2] - 2026-04-24
2
85
 
3
86
  ### Fixed
data/README.md CHANGED
@@ -149,6 +149,22 @@ uses cache for specific test suites and not merge them.
149
149
  TEST_SUITE_ID=2 bundle exec rspec spec/helpers
150
150
  ```
151
151
 
152
+ - **`USE_TEST_SUITE_ID_CACHE`** (optional, default unset) changes how the remote
153
+ cache validator treats per-suite caches. When set to the exact string `"true"`,
154
+ the validator accepts the current `TEST_SUITE_ID`'s cache independently of the
155
+ other suites' state — so if one suite aborts mid-CI, the remaining suites can
156
+ still reuse their own cached results on the next run. When unset (the default),
157
+ 1.1.x behaviour is preserved byte-for-byte: the validator requires every
158
+ `TEST_SUITES` slot to be present before accepting any cache.
159
+ ```sh
160
+ export TEST_SUITES=3
161
+ export TEST_SUITE_ID=1
162
+ export USE_TEST_SUITE_ID_CACHE=true
163
+ bundle exec rspec spec/models
164
+ ```
165
+ Only the literal string `"true"` activates the flag; `"1"`, `"yes"`, or any
166
+ other truthy value keeps the default behaviour.
167
+
152
168
  ## Advanced Configuration
153
169
 
154
170
  Configuration settings must be defined in **`.rspec-tracer`** file:
@@ -8,6 +8,8 @@ module RSpecTracer
8
8
  def initialize
9
9
  @s3_bucket, @s3_path = setup_s3
10
10
  @aws_cli = RSpecTracer.use_local_aws ? 'awslocal' : 'aws'
11
+ @use_test_suite_id_cache = ENV.fetch('USE_TEST_SUITE_ID_CACHE', nil) == 'true'
12
+ @test_suite_id = ENV.fetch('TEST_SUITE_ID', nil) if @use_test_suite_id_cache
11
13
  end
12
14
 
13
15
  def branch_refs?(branch_name)
@@ -60,7 +62,11 @@ module RSpecTracer
60
62
  end
61
63
 
62
64
  def cache_files_list(ref)
63
- prefix = "s3://#{@s3_bucket}/#{@s3_path}/#{ref}/"
65
+ prefix = if @use_test_suite_id_cache && !@test_suite_id.nil?
66
+ "s3://#{@s3_bucket}/#{@s3_path}/#{ref}/#{@test_suite_id}/"
67
+ else
68
+ "s3://#{@s3_bucket}/#{@s3_path}/#{ref}/"
69
+ end
64
70
 
65
71
  `#{@aws_cli} s3 ls #{prefix} --recursive`.chomp.split("\n")
66
72
  end
@@ -125,7 +131,7 @@ module RSpecTracer
125
131
  remote_dir = s3_dir(ref, run_id)
126
132
  local_dir = File.join(RSpecTracer.cache_path, run_id)
127
133
 
128
- raise AwsError, "Failed to download files from #{local_dir}" unless system(
134
+ raise AwsError, "Failed to upload files from #{local_dir}" unless system(
129
135
  @aws_cli,
130
136
  's3',
131
137
  'cp',
@@ -3,16 +3,19 @@
3
3
  module RSpecTracer
4
4
  module RemoteCache
5
5
  class Validator
6
+ class ValidationError < StandardError; end
7
+
6
8
  CACHE_FILES_PER_TEST_SUITE = 11
7
9
 
8
10
  def initialize
9
11
  @test_suite_id = ENV.fetch('TEST_SUITE_ID', nil)
10
12
  @test_suites = ENV.fetch('TEST_SUITES', nil)
13
+ @use_test_suite_id_cache = ENV.fetch('USE_TEST_SUITE_ID_CACHE', nil) == 'true'
11
14
 
12
15
  if @test_suite_id.nil? ^ @test_suites.nil?
13
16
  raise(
14
17
  ValidationError,
15
- 'Both the enviornment variables TEST_SUITE_ID and TEST_SUITES are not set'
18
+ 'Both the environment variables TEST_SUITE_ID and TEST_SUITES are not set'
16
19
  )
17
20
  end
18
21
 
@@ -20,13 +23,11 @@ module RSpecTracer
20
23
  end
21
24
 
22
25
  def valid?(ref, cache_files)
23
- last_run_regex = Regexp.new(format(@last_run_files_regex, ref: ref))
24
-
25
- return false if cache_files.count { |file| file.match?(last_run_regex) } != @last_run_files_count
26
-
27
- cache_regex = Regexp.new(format(@cached_files_regex, ref: ref))
28
-
29
- cache_files.count { |file| file.match?(cache_regex) } == @cached_files_count
26
+ if @use_test_suite_id_cache
27
+ test_suite_id_specific_validation?(ref, cache_files)
28
+ else
29
+ general_validation?(ref, cache_files)
30
+ end
30
31
  end
31
32
 
32
33
  private
@@ -36,7 +37,7 @@ module RSpecTracer
36
37
  @last_run_files_count = 1
37
38
  @last_run_files_regex = '/%<ref>s/last_run.json$'
38
39
  @cached_files_count = CACHE_FILES_PER_TEST_SUITE
39
- @cached_files_regex = '/%<ref>s/[0-9a-f]{32}/.+.json'
40
+ @cached_files_regex = '/%<ref>s/[0-9a-f]{32}/.+.json$'
40
41
  else
41
42
  @test_suites = @test_suites.to_i
42
43
  @test_suites_regex = (1..@test_suites).to_a.join('|')
@@ -47,6 +48,25 @@ module RSpecTracer
47
48
  @cached_files_regex = "/%<ref>s/(#{@test_suites_regex})/[0-9a-f]{32}/.+.json$"
48
49
  end
49
50
  end
51
+
52
+ def general_validation?(ref, cache_files)
53
+ last_run_regex = Regexp.new(format(@last_run_files_regex, ref: ref))
54
+
55
+ return false if cache_files.count { |file| file.match?(last_run_regex) } != @last_run_files_count
56
+
57
+ cache_regex = Regexp.new(format(@cached_files_regex, ref: ref))
58
+
59
+ cache_files.count { |file| file.match?(cache_regex) } == @cached_files_count
60
+ end
61
+
62
+ def test_suite_id_specific_validation?(ref, cache_files)
63
+ last_run_regex = Regexp.new("/#{ref}/#{@test_suite_id}/last_run.json$")
64
+ cache_regex = Regexp.new("/#{ref}/#{@test_suite_id}/[0-9a-f]{32}/.+.json$")
65
+
66
+ return false unless cache_files.any? { |file| file.match?(last_run_regex) }
67
+
68
+ cache_files.any? { |file| file.match?(cache_regex) }
69
+ end
50
70
  end
51
71
  end
52
72
  end
@@ -25,7 +25,15 @@ module RSpecTracer
25
25
  end
26
26
 
27
27
  def file_path(file_name)
28
+ return file_name if absolute_external_file?(file_name)
29
+
28
30
  File.expand_path(file_name.sub(%r{^/}, ''), RSpecTracer.root)
29
31
  end
32
+
33
+ def absolute_external_file?(file_name)
34
+ file_name.start_with?('/') &&
35
+ !file_name.start_with?(RSpecTracer.root) &&
36
+ File.file?(file_name)
37
+ end
30
38
  end
31
39
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RSpecTracer
4
- VERSION = '1.1.2'
4
+ VERSION = '1.2.1'
5
5
  end
data/lib/rspec_tracer.rb CHANGED
@@ -330,12 +330,7 @@ module RSpecTracer
330
330
  starting = Process.clock_gettime(Process::CLOCK_MONOTONIC)
331
331
  reports_dir = []
332
332
 
333
- 1.upto(ENV['PARALLEL_TEST_GROUPS'].to_i) do |test_num|
334
- cache_path = File.dirname(RSpecTracer.cache_path)
335
- cache_dir = File.join(cache_path, "parallel_tests_#{test_num}")
336
-
337
- next unless File.directory?(cache_dir)
338
-
333
+ parallel_tests_peer_dirs(File.dirname(RSpecTracer.cache_path)).each do |cache_dir|
339
334
  run_id = JSON.parse(File.read(File.join(cache_dir, 'last_run.json'), encoding: 'UTF-8'))['run_id']
340
335
 
341
336
  reports_dir << File.join(cache_dir, run_id)
@@ -365,14 +360,8 @@ module RSpecTracer
365
360
  return unless parallel_tests_executed? && !simplecov?
366
361
 
367
362
  starting = Process.clock_gettime(Process::CLOCK_MONOTONIC)
368
- reports_dir = []
369
363
 
370
- 1.upto(ENV['PARALLEL_TEST_GROUPS'].to_i) do |test_num|
371
- coverage_path = File.dirname(RSpecTracer.coverage_path)
372
- coverage_dir = File.join(coverage_path, "parallel_tests_#{test_num}")
373
-
374
- reports_dir << coverage_dir if File.directory?(coverage_dir)
375
- end
364
+ reports_dir = parallel_tests_peer_dirs(File.dirname(RSpecTracer.coverage_path))
376
365
 
377
366
  coverage_merger.merge(reports_dir)
378
367
 
@@ -401,13 +390,34 @@ module RSpecTracer
401
390
  def purge_parallel_tests_reports
402
391
  return unless parallel_tests_executed?
403
392
 
404
- 1.upto(ENV['PARALLEL_TEST_GROUPS'].to_i) do |test_num|
405
- [RSpecTracer.cache_path, RSpecTracer.coverage_path, RSpecTracer.report_path].each do |path|
406
- FileUtils.rm_rf(File.join(File.dirname(path), "parallel_tests_#{test_num}"))
393
+ [RSpecTracer.cache_path, RSpecTracer.coverage_path, RSpecTracer.report_path].each do |path|
394
+ parallel_tests_peer_dirs(File.dirname(path)).each do |worker_dir|
395
+ FileUtils.rm_rf(worker_dir)
407
396
  end
408
397
  end
409
398
  end
410
399
 
400
+ # Returns every `parallel_tests_*` subdirectory directly under
401
+ # `base_path`. Used by the parallel_tests merge + purge paths.
402
+ #
403
+ # Earlier patches iterated `1..ENV['PARALLEL_TEST_GROUPS'].to_i`
404
+ # to construct dir names, but parallel_tests's own runner sets
405
+ # PARALLEL_TEST_GROUPS to the user-requested process count
406
+ # (`Parallel.processor_count` by default), NOT the actual worker
407
+ # count. When num_processes < spawned_worker_count, the upper
408
+ # bound was too small: peer caches with TEST_ENV_NUMBER above the
409
+ # bound were silently dropped from the merge AND left behind by
410
+ # the purge. PR #101's commit message documented this gem
411
+ # behaviour for `last_process?` detection but did not extend the
412
+ # fix to the iteration call-sites; this method closes that gap.
413
+ # Globbing the actual filesystem state is robust to the env
414
+ # discrepancy regardless of how the gem partitions specs.
415
+ def parallel_tests_peer_dirs(base_path)
416
+ Dir.glob(File.join(base_path, 'parallel_tests_*')).select do |path|
417
+ File.directory?(path)
418
+ end
419
+ end
420
+
411
421
  def parallel_tests_executed?
412
422
  return false unless parallel_tests? && parallel_tests_last_process?
413
423
 
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.1.2
4
+ version: 1.2.1
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.1.2
114
+ source_code_uri: https://github.com/avmnu-sng/rspec-tracer/tree/v1.2.1
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: []