turbo_tests2 3.2.12 → 3.2.14

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: 9f0466a1bb4c09b6337f7cd3c62df308c83fdd3f7ea950d9cc9489e118d634e1
4
- data.tar.gz: 177994c048ccb4acdf41c99e5c0bea8573dc388e1e34a4c99b0f818ec6f8f107
3
+ metadata.gz: ec1ea300b0be3e2bc5007a7ee4ae67583593425134cd9b33d593519cf9cab77f
4
+ data.tar.gz: b43556ccb6edeefc5d6a2d60e1be283f7fa0e343a5287f1b8c665742ed59ec5a
5
5
  SHA512:
6
- metadata.gz: 62c6914590a7bd5637d38fb872e8f7cbd3de840ddd0c400c2aab688528146c09b0e39c83dfeda00cfbc3038e2244785162f9ba572c3c7179ffceaf2b610c5152
7
- data.tar.gz: bec33a7549867ad9cdfa0998abe1e99ad198220159cb1fe2e5a07a219ba72202c8075c4c5a8e246ccbf8e7baba6f2cf65ca3732ed356e245d15df8beb7f68a79
6
+ metadata.gz: 194f8c64f3de3952badaf69504e9570600d67d942c1092e4b57fea3b218739ff0b23f7e02b552e446d4cdaf8bf439b1e0e98f4db6b37f1ff51d861d3e7fbe866
7
+ data.tar.gz: bd4ee15030dcbdeef8b0e5ffe681db2578a53a1f9975883e36bd287acd2ae6aea17db55cf2524a83eca45fb29c79e26d00177b4213f9f32690442cbeba3a9844
checksums.yaml.gz.sig CHANGED
Binary file
data/CHANGELOG.md CHANGED
@@ -30,6 +30,57 @@ Please file a bug if you notice a violation of semantic versioning.
30
30
 
31
31
  ### Security
32
32
 
33
+ ## [3.2.14] - 2026-10-08
34
+
35
+ - TAG: [v3.2.14][3.2.14t]
36
+ - COVERAGE: 96.93% -- 947/977 lines in 11 files
37
+ - BRANCH COVERAGE: 85.57% -- 255/298 branches in 11 files
38
+ - 39.42% documented
39
+
40
+ ### Changed
41
+
42
+ - [kc] kettle-jem/prepare: updated 6 project files:
43
+ - dependencies (6)
44
+
45
+ - [kc] kettle-jem/template: updated 4 project files:
46
+ - code and tests (1)
47
+ - documentation (1)
48
+ - other (1)
49
+ - workflows (1)
50
+
51
+ ### Fixed
52
+
53
+ - rspec_configured_files_to_run: normalize discovered spec paths against the root cwd instead of the nearest ancestor .rspec. In stacked monorepos (root .rspec aggregating gems/*/spec via --pattern, where each gem also has its own .rspec), the nearest-ancestor walk stripped aggregated specs to bare member-relative spec/... paths that do not exist at the root, crashing parallel_tests File.stat during group sizing and aborting the whole run. Regression introduced in 3.2.13.
54
+
55
+ - Fix a teardown race that intermittently dropped a worker's results. The exit-watcher thread previously signaled process exit as soon as the child was reaped, before the stdout/stderr readers had drained the pipe. Under CI contention handle_messages could reach process_count exits and stop while that worker's rows were still queued behind the exit signal, so they were never reported. Symptom was a nested run reporting "1 example, 0 failures" instead of "4 examples, 0 failures, 3 pending", with which worker vanished varying run to run (spec/integration/multi_process_spec.rb failed at :46 on one run and :52-54 on the next). Readers are now drained to EOF before the watcher signals exit, and the exit/error messages are a deduped fallback for a reader force-stopped before EOF. Drain uses one shared monotonic deadline across both streams (READER_DRAIN_TIMEOUT) so a wedged pipe whose write end stays open cannot double teardown time or hang; force-stop still closes the pipe and kills the reader as before.
56
+
57
+ - Fix spec discovery returning absolute paths on Windows, which crashed parallel_tests' File.stat during group sizing. rspec_configured_files_to_run made discovered paths relative to the cwd with a string prefix check. On Windows the cwd and the discovered files can name the same directory with two different spellings: Dir.pwd and ENV["TEMP"] report the 8.3 short name (C:/Users/RUNNER~1/AppData/Local/Temp) while Dir.glob, which RSpec uses to expand --pattern, reports the long name (C:/Users/runneradmin/AppData/Local/Temp). The prefix therefore never matched and the absolute temp path leaked through. File.realpath does not expand 8.3 short names, so canonicalizing first cannot reconcile the two spellings, and Pathname#relative_path_from returns nonsense such as "../../../../../runneradmin/...". The same class of mismatch arises from a symlinked ancestor on any platform, e.g. a Dir.pwd of /var/home/u/proj against a path spelled /home/u/proj/... Discovered paths are now made relative by walking up from the file and comparing each ancestor with File.identical?, which compares by filesystem identity (inode on Unix, file index on Windows) and so treats every spelling of one directory as equal. Extracted as TurboTests::Utils::Paths.relative_from/.within?.
58
+
59
+ - [kc] license-eye-workflow-retry: The Apache SkyWalking Eyes workflow retries the license check once before failing. The pinned `apache/skywalking-eyes/dependency` action is a composite that builds license-eye from source on every run, and its own dependency cache is broken upstream: `dependency/action.yml` passes `cache-dependency-path: ${{ github.action_path }}/go.sum`, but `go.sum` lives at the action's repository root rather than in `dependency/` (404 versus 200). setup-go therefore logs "Restore cache failed: Some specified paths were not resolved", the module cache never restores, and each run re-downloads roughly 35 Go modules and re-runs `make`. Every run is consequently exposed to a transient `proxy.golang.org` error — one such run failed with `stream error: stream ID 133; INTERNAL_ERROR; received from peer` after eleven consecutive successes. The retry follows the existing `[Attempt 1]`/`[Attempt 2]` idiom used by the Ruby matrix workflows.
60
+
61
+ ## [3.2.13] - 2026-10-01
62
+
63
+ - TAG: [v3.2.13][3.2.13t]
64
+ - COVERAGE: 96.83% -- 917/947 lines in 10 files
65
+ - BRANCH COVERAGE: 84.59% -- 247/292 branches in 10 files
66
+ - 36.36% documented
67
+
68
+ ### Changed
69
+
70
+ - [kc] kettle-jem/prepare: updated 15 project files:
71
+ - dependencies (15)
72
+ - [kc] kettle-jem/template: updated 35 project files:
73
+ - code and tests (2)
74
+ - dependencies (1)
75
+ - other (3)
76
+ - workflows (29)
77
+
78
+ - Documented runtime and platform support tiers, including the TruffleRuby single-worker CI exception and skipped process-spec groups.
79
+
80
+ ### Fixed
81
+
82
+ - Cap the TruffleRuby 23.0 compatibility workflow at one test worker after an intermittent parallel-run hang; maintained runtimes retain parallel test coverage.
83
+
33
84
  ## [3.2.12] - 2026-09-26
34
85
 
35
86
  - TAG: [v3.2.12][3.2.12t]
@@ -667,7 +718,11 @@ Please file a bug if you notice a violation of semantic versioning.
667
718
 
668
719
  - Initial release
669
720
 
670
- [Unreleased]: https://github.com/galtzo-floss/turbo_tests2/compare/v3.2.12...HEAD
721
+ [Unreleased]: https://github.com/galtzo-floss/turbo_tests2/compare/v3.2.14...HEAD
722
+ [3.2.14]: https://github.com/galtzo-floss/turbo_tests2/compare/v3.2.13...v3.2.14
723
+ [3.2.14t]: https://github.com/galtzo-floss/turbo_tests2/releases/tag/v3.2.14
724
+ [3.2.13]: https://github.com/galtzo-floss/turbo_tests2/compare/v3.2.12...v3.2.13
725
+ [3.2.13t]: https://github.com/galtzo-floss/turbo_tests2/releases/tag/v3.2.13
671
726
  [3.2.12]: https://github.com/galtzo-floss/turbo_tests2/compare/v3.2.11...v3.2.12
672
727
  [3.2.12t]: https://github.com/galtzo-floss/turbo_tests2/releases/tag/v3.2.12
673
728
  [3.2.11]: https://github.com/galtzo-floss/turbo_tests2/compare/v3.2.10...v3.2.11
data/README.md CHANGED
@@ -30,6 +30,37 @@ The project namespace remains `TurboTests`; the published gem and executable are
30
30
 
31
31
  Incremental summarized output doesn't [fit the vision](https://github.com/grosser/parallel_tests/issues/708) of the `parallel_tests` author, and [RSpec doesn't support built-in parallel testing yet](https://github.com/rspec/rspec-rails/issues/2104#issuecomment-658474900). This gem targets RSpec, not every framework supported by `parallel_tests`, and may not be useful once one of the issues above is implemented.
32
32
 
33
+ ### Runtime and Platform Support
34
+
35
+ Support tiers describe what this project's CI actually exercises, not what may
36
+ work in an untested environment:
37
+
38
+ - **Parallel support**: CI runs the test suite with multiple workers available.
39
+ - **Single-worker support**: CI runs the suite with one worker; parallel runner
40
+ behavior is not validated for that combination.
41
+ - **Unsupported**: the engine/platform combination is not covered by CI and is
42
+ not a project compatibility claim.
43
+
44
+ | Ruby engine/version | Linux | macOS | Windows | Notes |
45
+ |---------------------|-------|-------|---------|-------|
46
+ | MRI 2.4-3.4 | Parallel support | Unsupported | Unsupported | Versioned Ruby workflows run on Linux. |
47
+ | MRI current | Parallel support | Parallel support | Parallel support | Current Ruby workflow covers all three platforms. |
48
+ | MRI head | Parallel support | Unsupported | Unsupported | CI is allowed to fail while Ruby is in development. |
49
+ | JRuby 9.2-9.4 | Parallel support | Unsupported | Unsupported | Versioned JRuby workflows run on Linux. |
50
+ | JRuby 10.0/current/head | Parallel support | Unsupported | Unsupported | Experimental/current and head jobs are non-blocking in CI. |
51
+ | TruffleRuby 22.3 | Parallel support | Unsupported | Unsupported | Experimental. The CLI and multi-process spec groups are skipped because this release reports Ruby compatibility version 3.0. |
52
+ | TruffleRuby 23.0 | Single-worker support | Unsupported | Unsupported | Experimental and end-of-life; CI caps workers at one. The CLI and multi-process spec groups are also skipped. |
53
+ | TruffleRuby 23.1 | Parallel support | Unsupported | Unsupported | Experimental. |
54
+ | TruffleRuby 24.2/25.0/33.0/current | Parallel support | Unsupported | Unsupported | Linux workflows run the suite with parallel workers. |
55
+ | TruffleRuby head | Parallel support | Unsupported | Unsupported | CI is allowed to fail while TruffleRuby is in development. |
56
+
57
+ The TruffleRuby 22.3/23.0 spec exclusions use `rspec-pending_for`'s Ruby
58
+ compatibility-version matching (`3.0`), not TruffleRuby's release number. They
59
+ exclude the complete CLI spec group and multi-process integration group; the
60
+ remaining suite still runs in parallel on 22.3 and serially on 23.0. The
61
+ Windows current-Ruby job also runs in parallel, but runtime-log recording is
62
+ disabled there because Windows file locking is unreliable (see below).
63
+
33
64
  ### Why incremental output?
34
65
 
35
66
  `parallel_tests` is great, but parallel RSpec output is hard to read:
@@ -780,7 +811,7 @@ Thanks for RTFM. ☺️
780
811
  [📌gitmoji]: https://gitmoji.dev
781
812
  [📌gitmoji-img]: https://img.shields.io/badge/gitmoji_commits-%20%F0%9F%98%9C%20%F0%9F%98%8D-34495e.svg?style=flat-square
782
813
  [🧮kloc]: https://www.youtube.com/watch?v=dQw4w9WgXcQ
783
- [🧮kloc-img]: https://img.shields.io/badge/KLOC-0.947-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue
814
+ [🧮kloc-img]: https://img.shields.io/badge/KLOC-0.977-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue
784
815
  [🔐security]: https://github.com/galtzo-floss/turbo_tests2/blob/main/SECURITY.md
785
816
  [🔐security-img]: https://img.shields.io/badge/security-policy-259D6C.svg?style=flat
786
817
  [📄copyright-notice-explainer]: https://opensource.stackexchange.com/questions/5778/why-do-licenses-such-as-the-mit-license-specify-a-single-year
@@ -8,6 +8,7 @@ require "shellwords"
8
8
  require "tempfile"
9
9
 
10
10
  require_relative "../utils/hash_extension"
11
+ require_relative "utils/paths"
11
12
 
12
13
  module TurboTests
13
14
  class Runner
@@ -15,6 +16,16 @@ module TurboTests
15
16
  DEFAULT_RUNTIME_LOG = "tmp/turbo_rspec_runtime.log"
16
17
  DEFAULT_WORKER_OUTPUT_MODE = :warnings
17
18
  WORKER_OUTPUT_MODES = %i[warnings stream buffered quiet].freeze
19
+ # Shared wall-clock budget, in seconds, for draining a worker's stdout and
20
+ # stderr readers after the child has exited. Normally the pipe EOFs in
21
+ # milliseconds, so this is not on the hot path; the bound exists only for a
22
+ # wedged pipe whose write end is still held open (e.g. by a grandchild),
23
+ # which can never EOF. Shared across both streams so teardown cannot take
24
+ # twice the budget.
25
+ READER_DRAIN_TIMEOUT = 1
26
+ # Short join used only after the pipe is force-closed, to give the reader a
27
+ # moment to notice EOF before it is killed.
28
+ FORCE_STOP_JOIN = 0.1
18
29
 
19
30
  class << self
20
31
  def create(count)
@@ -160,23 +171,26 @@ module TurboTests
160
171
  def rspec_configured_files_to_run
161
172
  configuration = RSpec::Core::Configuration.new
162
173
  RSpec::Core::ConfigurationOptions.new(["spec"]).configure(configuration)
174
+ # Normalize against the cwd that owns the RSpec configuration being
175
+ # read, NOT the nearest .rspec ancestor of each file. parallel_tests
176
+ # stats these paths and spawns workers from this cwd, so the returned
177
+ # paths must resolve from here. In a stacked monorepo the root .rspec
178
+ # aggregates per-gem specs (each gem may also have its own .rspec);
179
+ # walking to the nearest .rspec would strip those to bare
180
+ # member-relative "spec/..." paths that do not exist at the root,
181
+ # crashing File.stat during group sizing.
182
+ #
183
+ # Compare paths by filesystem identity (File.identical?) rather than by
184
+ # string. On the Windows CI runner Dir.pwd reports the 8.3 short name
185
+ # (C:/Users/RUNNER~1/...) while Dir.glob, which RSpec uses to expand
186
+ # --pattern, reports the long name (C:/Users/runneradmin/...). Both
187
+ # spellings name the same directory, but File.realpath does NOT expand
188
+ # 8.3 names, so neither a string prefix nor Pathname#relative_path_from
189
+ # can reconcile them. The same class of mismatch arises from a symlinked
190
+ # ancestor. See TurboTests::Utils::Paths.
191
+ root = File.expand_path(Dir.pwd)
163
192
  configuration.files_to_run.map do |path|
164
- expanded_path = File.expand_path(path.to_s).tr("\\", "/")
165
- root = rspec_root_for(expanded_path)
166
- root_prefix = "#{root}/"
167
- expanded_path.start_with?(root_prefix) ? expanded_path[root_prefix.length..-1] : expanded_path
168
- end
169
- end
170
-
171
- def rspec_root_for(path)
172
- root = File.dirname(path)
173
- loop do
174
- return root.tr("\\", "/") if File.file?(File.join(root, ".rspec"))
175
-
176
- parent = File.dirname(root)
177
- return File.expand_path(Dir.pwd).tr("\\", "/") if parent == root
178
-
179
- root = parent
193
+ TurboTests::Utils::Paths.relative_from(File.expand_path(path.to_s, root), root)
180
194
  end
181
195
  end
182
196
 
@@ -477,10 +491,21 @@ module TurboTests
477
491
  status = wait_thr.value
478
492
  @failed_worker_statuses.store(process_id, status) unless status.success?
479
493
  @messages << {type: "error", process_id: process_id} unless status.success?
480
- @messages << {type: "exit", process_id: process_id}
481
494
  ensure
482
- stop_reader_thread(stdout_thread, stdout)
483
- stop_reader_thread(stderr_thread, stderr)
495
+ # Drain BOTH readers before signaling this process's exit. The
496
+ # stdout reader parses the pipe into @messages rows and pushes its
497
+ # own {type: "exit"} at EOF; the stderr copy thread only buffers
498
+ # text into @worker_output and queues nothing, but it is drained
499
+ # too so no buffered warning output is lost. Signaling exit here
500
+ # first let handle_messages reach process_count exits and break
501
+ # while this worker's stdout rows were still queued behind that
502
+ # exit, silently dropping them (intermittent example undercount
503
+ # and missing streamed output under CI contention). The exit below
504
+ # is a deduped fallback for a stdout reader that was force-stopped
505
+ # before reaching EOF, and being in ensure also prevents a hang if
506
+ # wait_thr.value raises.
507
+ drain_worker_readers(stdout_thread, stdout, stderr_thread, stderr)
508
+ @messages << {type: "exit", process_id: process_id}
484
509
  untrack_parallel_pid(wait_thr.pid, pid_file_path)
485
510
  end
486
511
  end
@@ -675,14 +700,29 @@ module TurboTests
675
700
  "#{kind} Coverage: #{match[3]} (#{match[1]} / #{match[2]})"
676
701
  end
677
702
 
678
- def stop_reader_thread(thread, io)
679
- return if thread.join(0.1)
703
+ def drain_worker_readers(stdout_thread, stdout, stderr_thread, stderr)
704
+ # One shared deadline across both streams so a wedged pipe cannot double
705
+ # teardown time or hang the watcher. stdout carries the worker's JSON
706
+ # rows; stderr only carries buffered text.
707
+ deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + READER_DRAIN_TIMEOUT
708
+ stop_reader_thread(stdout_thread, stdout, deadline)
709
+ stop_reader_thread(stderr_thread, stderr, deadline)
710
+ end
711
+
712
+ def stop_reader_thread(thread, io, deadline)
713
+ # Let the reader reach EOF and queue any buffered worker rows BEFORE any
714
+ # force-stop. Closing the pipe while the child's output is still
715
+ # kernel-buffered would discard it and silently drop that worker's
716
+ # results (intermittent example undercount / missing streamed output).
717
+ # The shared deadline bounds teardown when a pipe can never EOF because
718
+ # its write end is still held open (e.g. by a grandchild).
719
+ remaining = deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC)
720
+ return if remaining.positive? && thread.join(remaining)
680
721
 
681
722
  close_io(io)
682
- return if thread.join(0.1)
723
+ return if thread.join(FORCE_STOP_JOIN)
683
724
 
684
- thread.kill
685
- thread.join(0.1)
725
+ thread.kill.join(FORCE_STOP_JOIN)
686
726
  end
687
727
 
688
728
  def handle_messages
@@ -0,0 +1,235 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TurboTests
4
+ module Utils
5
+ # Makes discovered spec paths relative to the cwd that owns the RSpec
6
+ # configuration, in a way that survives the same directory being spelled
7
+ # two different ways.
8
+ #
9
+ # == Why string comparison of paths is not safe here
10
+ #
11
+ # A path string is a *name*, not an identity. One directory can have
12
+ # several valid names, and the two names can reach this module from
13
+ # different APIs within the same process.
14
+ #
15
+ # On the Windows CI runner (windows-latest, github/actions) this is not
16
+ # hypothetical. Measured from the runner itself:
17
+ #
18
+ # Dir.pwd => "C:/Users/RUNNER~1/AppData/Local/Temp/turbo-tests2-...-ujisy2"
19
+ # ENV["TEMP"] => "C:\\Users\\RUNNER~1\\AppData\\Local\\Temp"
20
+ # Dir.tmpdir => "C:/Users/RUNNER~1/AppData/Local/Temp"
21
+ # Dir.glob result => "C:/Users/runneradmin/AppData/Local/Temp/turbo-tests2-...-ujisy2/gems/example/spec/example_spec.rb"
22
+ #
23
+ # `RUNNER~1` is an 8.3 short name; `runneradmin` is the long name of the
24
+ # same directory. `Dir.pwd` and the environment report the short form while
25
+ # `Dir.glob` — which RSpec uses to expand `--pattern` — reports the long
26
+ # form. Both are correct, and they are never equal as strings.
27
+ #
28
+ # In this codebase the mismatch has a second, platform-independent face:
29
+ # a symlinked ancestor. On Fedora, `/home/pboling` is a symlink to
30
+ # `/var/home/pboling`, so `Dir.pwd` can report one form while a path built
31
+ # from an ENV var or a stored fixture reports the other. That is what makes
32
+ # this bug reproducible in specs without a Windows runner.
33
+ #
34
+ # == Approaches that do NOT work, and why
35
+ #
36
+ # All four were tried on this branch and all failed. Recorded so nobody
37
+ # repeats the sequence.
38
+ #
39
+ # 1. String prefix, the original code:
40
+ #
41
+ # root_prefix = "#{root}/"
42
+ # expanded.start_with?(root_prefix)
43
+ #
44
+ # Never matches when the two spellings differ, so the absolute temp path
45
+ # leaks through. parallel_tests then calls File.stat on it during group
46
+ # sizing. It also wrongly reports "project-other/spec" as inside
47
+ # "project", which is why spec/utils/paths_spec.rb covers siblings.
48
+ #
49
+ # 2. Canonicalize with File.realpath, then compare:
50
+ #
51
+ # File.realpath(path) == File.realpath(root) # conceptually
52
+ #
53
+ # WRONG PREMISE: File.realpath does NOT expand 8.3 short names. Measured
54
+ # on the runner, realpath of the short path returns the short path
55
+ # unchanged:
56
+ #
57
+ # root_realpath => "C:/Users/RUNNER~1/AppData/Local/Temp/turbo-tests2-..."
58
+ #
59
+ # Canonicalization therefore cannot reconcile the two spellings at all.
60
+ # (kettle-dev's Kettle::Dev::Paths.canonical documents realpath as
61
+ # expanding 8.3 names; that comment is inaccurate. Worth correcting if
62
+ # this module is ever extracted to a shared gem.)
63
+ #
64
+ # realpath also resolves symlinks, which introduces a second failure
65
+ # mode: root and file can legitimately traverse different symlink paths
66
+ # to one directory, and canonicalizing only one side makes them diverge.
67
+ #
68
+ # 3. Component-wise comparison of the canonicalized parts (split on
69
+ # File::SEPARATOR, compare element by element, case-insensitively on
70
+ # Windows). Built on the same false premise as #2, so it fails
71
+ # identically — `RUNNER~1` never equals `runneradmin` in any case
72
+ # folding. It additionally depended on File::ALT_SEPARATOR, which
73
+ # measured as "" (empty string, not nil) on that runner, making the
74
+ # separator-normalizing `tr` a silent no-op. Do not trust ALT_SEPARATOR
75
+ # to be present.
76
+ #
77
+ # 4. Pathname#relative_path_from, Ruby's own stdlib helper:
78
+ #
79
+ # Pathname.new(file).relative_path_from(Pathname.new(root)).to_s
80
+ #
81
+ # It is purely lexical. With differing spellings it walks all the way up
82
+ # and back down, producing:
83
+ #
84
+ # "../../../../../runneradmin/AppData/Local/Temp/.../example_spec.rb"
85
+ #
86
+ # Note it does not raise, so a `rescue ArgumentError` guard cannot catch
87
+ # it. The result is a plausible-looking but wrong relative path, which is
88
+ # worse than the leak in #1 because it silently selects the wrong files.
89
+ # The same nonsense appears on the symlink case:
90
+ # "../../../../../../var/home/pboling/src/my/...".
91
+ #
92
+ # == The approach that works
93
+ #
94
+ # File.identical? asks the filesystem whether two names denote one object,
95
+ # by inode on Unix and by file index on Windows. It is spelling-agnostic,
96
+ # which is exactly the property needed. Measured on the runner:
97
+ #
98
+ # File.identical?(root, root) => true
99
+ # File.identical?(file, file) => true
100
+ #
101
+ # and a walk-up from the long-name file to the short-name root produced the
102
+ # correct "gems/example/spec/example_spec.rb", while the string-prefix and
103
+ # Pathname baselines both failed in the same run.
104
+ #
105
+ # So: ascend from the file, asking at each level "is this the root?", and
106
+ # re-join the basenames on the way back down. The relative path is built
107
+ # from the *file's* spelling, which is what the caller needs, since
108
+ # parallel_tests stats these paths and spawns workers from this cwd.
109
+ #
110
+ # == How this was finally diagnosed
111
+ #
112
+ # The failure only reproduced on the Windows CI runner, and its output was
113
+ # swallowed: these discovery specs run a subprocess via Open3.capture3 whose
114
+ # stderr is only surfaced when the status check fails, and the assertion
115
+ # that failed was the JSON comparison. Four blind fixes failed in a row.
116
+ #
117
+ # What resolved it was a temporary, Windows-guarded spec
118
+ # (`if: Gem.win_platform?`) that deliberately failed with the diagnostic
119
+ # JSON embedded in the expectation message, so the runner's real path forms
120
+ # printed into the CI log. Guarding it kept the other 22 checks green while
121
+ # one job reported the data. That spec is deleted; the findings it produced
122
+ # are this comment plus spec/utils/paths_spec.rb.
123
+ #
124
+ # Lesson: when a bug is environment-specific and the environment is not
125
+ # available locally, spend the iteration on instrumenting the real
126
+ # environment rather than on another hypothesized fix.
127
+ module Paths
128
+ module_function
129
+
130
+ # Returns the path of +file+ relative to +root+ when +file+ is +root+
131
+ # itself or lives inside it, compared by filesystem identity rather than
132
+ # by string.
133
+ #
134
+ # Returns the expanded +file+ unchanged when it is not inside +root+
135
+ # (there is nothing to strip) or when identity cannot be established,
136
+ # e.g. because one side does not exist. That matches the long-standing
137
+ # behavior of passing through paths outside the root untouched.
138
+ #
139
+ # @param file [String, Pathname] path to make relative; expanded against
140
+ # the current directory when relative
141
+ # @param root [String, Pathname] directory to make +file+ relative to
142
+ # @return [String] relative path using File::SEPARATOR, "." when the two
143
+ # name the same directory, or the expanded +file+ when it is not inside
144
+ # +root+
145
+ def relative_from(file, root)
146
+ expanded_file = File.expand_path(file.to_s)
147
+ relative = relative_to_root(expanded_file, root)
148
+
149
+ relative || expanded_file
150
+ end
151
+
152
+ # Returns true when +file+ is +root+ itself or lives inside it, compared
153
+ # by filesystem identity.
154
+ #
155
+ # Unlike a string prefix test this does not treat a sibling directory
156
+ # that merely shares a prefix ("project-other") as being inside "project".
157
+ #
158
+ # @param file [String, Pathname, nil]
159
+ # @param root [String, Pathname, nil]
160
+ # @return [Boolean] false when either argument is nil or empty
161
+ def within?(file, root)
162
+ return false if blank?(file) || blank?(root)
163
+
164
+ relative_to_root(File.expand_path(file.to_s), root).is_a?(String)
165
+ end
166
+
167
+ # The path of +expanded_file+ relative to +root+: "." when they name the
168
+ # same directory, or nil when +root+ is not an ancestor.
169
+ #
170
+ # Guarded by File.exist? on both sides because File.identical? raises
171
+ # ENOENT rather than returning false for a missing path, and because a
172
+ # path that does not exist has no filesystem identity to compare.
173
+ def relative_to_root(expanded_file, root)
174
+ expanded_root = File.expand_path(root.to_s)
175
+ return nil unless File.exist?(expanded_file) && File.exist?(expanded_root)
176
+
177
+ relative_parts(expanded_file, expanded_root)
178
+ end
179
+ private_class_method :relative_to_root
180
+
181
+ # Ascend until the two paths are identical by filesystem identity,
182
+ # re-joining basenames on the way back down.
183
+ #
184
+ # Recursion terminates on its own: File.dirname of the filesystem root
185
+ # returns itself ("C:/" or "/"), so the `parent == expanded_file` check
186
+ # ends every walk. No depth constant is needed. Depth is bounded by the
187
+ # path length, which is shallow for any real spec tree; the cost is one
188
+ # stat pair per level.
189
+ #
190
+ # @return [String, nil] relative path, "." for the same directory, or nil
191
+ # when the root was never reached
192
+ def relative_parts(expanded_file, expanded_root)
193
+ return "." if identical?(expanded_file, expanded_root)
194
+
195
+ parent = File.dirname(expanded_file)
196
+ return nil if parent == expanded_file
197
+
198
+ append_basename(relative_parts(parent, expanded_root), File.basename(expanded_file))
199
+ end
200
+ private_class_method :relative_parts
201
+
202
+ # Prepend +basename+ to the tail produced by the level above. Propagates
203
+ # nil, so "root not found" survives the unwind instead of being rebuilt
204
+ # into a partial path.
205
+ def append_basename(tail, basename)
206
+ return nil unless tail
207
+
208
+ (tail == ".") ? basename : File.join(tail, basename)
209
+ end
210
+ private_class_method :append_basename
211
+
212
+ # Filesystem-identity comparison that tolerates a missing path, where
213
+ # File.identical? raises ENOENT. Falls back to string equality, which is
214
+ # the correct answer when neither side is a symlink or an 8.3 short name.
215
+ #
216
+ # Rescues SystemCallError rather than Errno::ENOENT alone, because
217
+ # ENOTDIR, ELOOP and EACCES all mean "identity unknown" here, and the
218
+ # string fallback is the safest available answer in each case.
219
+ def identical?(left, right)
220
+ File.identical?(left, right)
221
+ rescue SystemCallError
222
+ left == right
223
+ end
224
+ private_class_method :identical?
225
+
226
+ # Treats nil and "" alike. Uses to_s so nil never reaches a nil-check
227
+ # that a linter would flag, and so a Pathname or other to_s-able object
228
+ # is accepted.
229
+ def blank?(value)
230
+ value.to_s.empty?
231
+ end
232
+ private_class_method :blank?
233
+ end
234
+ end
235
+ end
@@ -4,7 +4,7 @@ module TurboTests
4
4
  # Version namespace for this gem.
5
5
  module Version
6
6
  # Current gem version.
7
- VERSION = "3.2.12"
7
+ VERSION = "3.2.14"
8
8
  end
9
9
  # Current gem version exposed at the traditional constant location.
10
10
  VERSION = Version::VERSION # Traditional Constant Location
data.tar.gz.sig CHANGED
Binary file
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: turbo_tests2
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.2.12
4
+ version: 3.2.14
5
5
  platform: ruby
6
6
  authors:
7
7
  - Illia
@@ -111,7 +111,7 @@ dependencies:
111
111
  version: '3.1'
112
112
  - - ">="
113
113
  - !ruby/object:Gem::Version
114
- version: 3.1.1
114
+ version: 3.1.6
115
115
  type: :development
116
116
  prerelease: false
117
117
  version_requirements: !ruby/object:Gem::Requirement
@@ -121,7 +121,7 @@ dependencies:
121
121
  version: '3.1'
122
122
  - - ">="
123
123
  - !ruby/object:Gem::Version
124
- version: 3.1.1
124
+ version: 3.1.6
125
125
  - !ruby/object:Gem::Dependency
126
126
  name: bundler-audit
127
127
  requirement: !ruby/object:Gem::Requirement
@@ -199,7 +199,7 @@ dependencies:
199
199
  version: '3.2'
200
200
  - - ">="
201
201
  - !ruby/object:Gem::Version
202
- version: 3.2.4
202
+ version: 3.2.5
203
203
  type: :development
204
204
  prerelease: false
205
205
  version_requirements: !ruby/object:Gem::Requirement
@@ -209,7 +209,7 @@ dependencies:
209
209
  version: '3.2'
210
210
  - - ">="
211
211
  - !ruby/object:Gem::Version
212
- version: 3.2.4
212
+ version: 3.2.5
213
213
  - !ruby/object:Gem::Dependency
214
214
  name: kettle-test
215
215
  requirement: !ruby/object:Gem::Requirement
@@ -219,7 +219,7 @@ dependencies:
219
219
  version: '2.0'
220
220
  - - ">="
221
221
  - !ruby/object:Gem::Version
222
- version: 2.0.22
222
+ version: 2.0.23
223
223
  type: :development
224
224
  prerelease: false
225
225
  version_requirements: !ruby/object:Gem::Requirement
@@ -229,7 +229,7 @@ dependencies:
229
229
  version: '2.0'
230
230
  - - ">="
231
231
  - !ruby/object:Gem::Version
232
- version: 2.0.22
232
+ version: 2.0.23
233
233
  - !ruby/object:Gem::Dependency
234
234
  name: ruby-progressbar
235
235
  requirement: !ruby/object:Gem::Requirement
@@ -273,7 +273,7 @@ dependencies:
273
273
  version: '2.0'
274
274
  - - ">="
275
275
  - !ruby/object:Gem::Version
276
- version: 2.0.13
276
+ version: 2.0.15
277
277
  type: :development
278
278
  prerelease: false
279
279
  version_requirements: !ruby/object:Gem::Requirement
@@ -283,7 +283,7 @@ dependencies:
283
283
  version: '2.0'
284
284
  - - ">="
285
285
  - !ruby/object:Gem::Version
286
- version: 2.0.13
286
+ version: 2.0.15
287
287
  description: "\U0001F680 turbo_tests2` is an RSpec parallel test runner built on `parallel_tests`.
288
288
  It uses `parallel_tests` for process counts, file grouping, and runtime balancing,
289
289
  then replaces interleaved worker output with incremental summarized RSpec reporting.
@@ -306,6 +306,7 @@ files:
306
306
  - lib/turbo_tests/reporter.rb
307
307
  - lib/turbo_tests/runner.rb
308
308
  - lib/turbo_tests/shim.rb
309
+ - lib/turbo_tests/utils/paths.rb
309
310
  - lib/turbo_tests/version.rb
310
311
  - lib/turbo_tests2.rb
311
312
  - lib/turbo_tests2/rspec/shared_contexts/simplecov_spawn.rb
@@ -315,10 +316,10 @@ licenses:
315
316
  - MIT
316
317
  metadata:
317
318
  homepage_uri: https://turbo-tests2.galtzo.com
318
- source_code_uri: https://github.com/galtzo-floss/turbo_tests2/tree/v3.2.12
319
- changelog_uri: https://github.com/galtzo-floss/turbo_tests2/blob/v3.2.12/CHANGELOG.md
319
+ source_code_uri: https://github.com/galtzo-floss/turbo_tests2/tree/v3.2.14
320
+ changelog_uri: https://github.com/galtzo-floss/turbo_tests2/blob/v3.2.14/CHANGELOG.md
320
321
  bug_tracker_uri: https://github.com/galtzo-floss/turbo_tests2/issues
321
- documentation_uri: https://www.rubydoc.info/gems/turbo_tests2/3.2.12
322
+ documentation_uri: https://www.rubydoc.info/gems/turbo_tests2/3.2.14
322
323
  funding_uri: https://github.com/sponsors/pboling
323
324
  wiki_uri: https://github.com/galtzo-floss/turbo_tests2/wiki
324
325
  news_uri: https://www.railsbling.com/tags/turbo_tests2
metadata.gz.sig CHANGED
Binary file