Module: TurboTests::Utils + + + +
+-
+
- Defined in: +
- lib/turbo_tests/utils/paths.rb + +
Defined Under Namespace
++ + + Modules: Paths + + + + +
+ + + + + + + + + +diff --git a/CHANGELOG.md b/CHANGELOG.md index a000245..7347f26 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,18 +28,14 @@ Please file a bug if you notice a violation of semantic versioning. ### Fixed -- 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. - -- 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?. - ### Security -## [3.2.14] - 2026-10-05 +## [3.2.14] - 2026-10-07 - TAG: [v3.2.14][3.2.14t] -- COVERAGE: 96.81% -- 910/940 lines in 10 files -- BRANCH COVERAGE: 84.72% -- 244/288 branches in 10 files -- 36.73% documented +- COVERAGE: 96.93% -- 947/977 lines in 11 files +- BRANCH COVERAGE: 85.57% -- 255/298 branches in 11 files +- 39.42% documented ### Changed @@ -56,6 +52,10 @@ Please file a bug if you notice a violation of semantic versioning. - 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. +- 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. + +- 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?. + ## [3.2.13] - 2026-10-01 - TAG: [v3.2.13][3.2.13t] diff --git a/Gemfile.lock b/Gemfile.lock index e5cff06..d2238d9 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -138,7 +138,7 @@ GEM geom2d (0.4.1) gitmoji-regex (2.0.15) version_gem (~> 1.1, >= 1.1.15) - hexapdf (1.11.0) + hexapdf (1.11.1) cmdparse (~> 3.0, >= 3.0.3) geom2d (~> 0.4, >= 0.4.1) openssl (>= 2.2.1) @@ -149,7 +149,7 @@ GEM prism (>= 1.3.0) rdoc (>= 4.0.0) reline (>= 0.4.2) - json (2.21.2) + json (3.0.2) json-merge (7.1.9) ast-merge (= 7.1.9) tree_haver (= 7.1.9) @@ -158,7 +158,7 @@ GEM ast-crispr-markdown-markly (~> 7.1, >= 7.1.1) kettle-dev (~> 3.1, >= 3.1.2) version_gem (~> 1.1, >= 1.1.15) - kettle-dev (3.1.7) + kettle-dev (3.1.8) emoji_regex (>= 15.0) kettle-gha-pins (~> 0.3, >= 0.3.16) kettle-ndjson (~> 0.1, >= 0.1.18) @@ -167,11 +167,11 @@ GEM ruby-progressbar (~> 1.13) tty-progressbar (~> 0.18) version_gem (~> 1.1, >= 1.1.15) - kettle-family (1.3.5) + kettle-family (1.3.6) command_kit (~> 0.6) command_kit-completion (~> 0.1) kettle-changelog (~> 1.0, >= 1.0.8) - kettle-dev (~> 3.1, >= 3.1.7) + kettle-dev (~> 3.1, >= 3.1.8) kettle-gha-pins (~> 0.3, >= 0.3.16) kettle-jem (~> 7.1, >= 7.1.28) kettle-test (~> 2.0, >= 2.0.23) @@ -358,8 +358,8 @@ GEM rspec-support (3.13.7) rspec_junit_formatter (0.6.0) rspec-core (>= 2, < 4, != 2.12.0) - rubocop (1.88.2) - json (~> 2.3) + rubocop (1.91.0) + json (>= 2.3) language_server-protocol (~> 3.17.0.2) lint_roller (~> 1.1.0) parallel (>= 1.10) @@ -407,9 +407,9 @@ GEM rubocop-md (2.0.4) lint_roller (~> 1.1) rubocop (>= 1.72.1) - rubocop-minitest (0.40.0) + rubocop-minitest (0.41.0) lint_roller (~> 1.1) - rubocop (>= 1.75.0, < 2.0) + rubocop (>= 1.89.0, < 2.0) rubocop-ast (>= 1.38.0, < 2.0) rubocop-on-rbs (2.2.0) lint_roller (~> 1.1) @@ -429,9 +429,9 @@ GEM rubocop-rspec-extra (0.3.0) lint_roller (~> 1.1) rubocop (>= 1.72.1, < 2.0) - rubocop-rspec_rails (2.32.0) + rubocop-rspec_rails (2.33.0) lint_roller (~> 1.1) - rubocop (~> 1.72, >= 1.72.1) + rubocop (~> 1.89) rubocop-rspec (~> 3.5) rubocop-ruby2_4 (3.0.8) rubocop-gradual (~> 0.3, >= 0.3.6) @@ -440,9 +440,9 @@ GEM rubocop-thread_safety (~> 0.7, >= 0.7.3) standard-rubocop-lts (~> 2.0, >= 2.0.14) version_gem (~> 1.1, >= 1.1.15) - rubocop-thread_safety (0.7.3) + rubocop-thread_safety (0.8.0) lint_roller (~> 1.1) - rubocop (~> 1.72, >= 1.72.1) + rubocop (~> 1.89) rubocop-ast (>= 1.44.0, < 2.0) ruby-merge (7.1.9) ast-merge (= 7.1.9) @@ -468,11 +468,11 @@ GEM simplecov (>= 0.4.1) simplecov_lcov_formatter (0.9.0) simplecov (>= 0.18) - sorbet-runtime (0.6.13528) - standard (1.56.0) + sorbet-runtime (0.6.13532) + standard (1.57.0) language_server-protocol (~> 3.17.0.2) lint_roller (~> 1.0) - rubocop (~> 1.88.0) + rubocop (~> 1.91.0) standard-custom (~> 1.0.0) standard-performance (~> 1.8) standard-custom (1.0.2) @@ -645,7 +645,7 @@ CHECKSUMS bash-merge (7.1.9) sha256=fde66a0e5eaaa2df0eae456bd5a45be631e61806c356f345d2a43085e4138757 benchmark (0.5.0) sha256=465df122341aedcb81a2a24b4d3bd19b6c67c1530713fd533f3ff034e419236c bigdecimal (4.1.3) sha256=61ebe1e5e559bdc3cc6f2c0ee7f427321fc838f59611c294356eb04d6e21cf66 - bundler (4.0.22) sha256=d8d5ec84c8555e0af71db63ed7aee4d1a8fb839ec46d84212d61979242a5d75a + bundler (4.0.21) sha256=7cbf5499b426076aa16c8d28a59e4c2c3aad5a438402abcd961f0119f29f1dd0 bundler-audit (0.9.3) sha256=81c8766c71e47d0d28a0f98c7eed028539f21a6ea3cd8f685eb6f42333c9b4e9 cgi (0.5.2) sha256=61ca30298171190fd4fa0d8018e57ada456eae9b7a2b78526debf7f0a0e6f8bb citrus (3.0.2) sha256=4ec2412fc389ad186735f4baee1460f7900a8e130ffe3f216b30d4f9c684f650 @@ -682,14 +682,14 @@ CHECKSUMS gem_bench (2.0.5) sha256=0dc0fb44a5a5eb7b2f5c1c68a5b0164d72007132822c012bac3abe976b199ead geom2d (0.4.1) sha256=ea0998ea90c4f2752e24fe13d85a4f89bee689d151316140ebcc6369bf634ed9 gitmoji-regex (2.0.15) sha256=c6571bffd757f95c06d84a7d2d0b3f180eca7fe576882c371cee1f1a081cd968 - hexapdf (1.11.0) sha256=72e0459dd2482ec1791f9a44a97144566a00188889cc8742dda97413ca679c62 + hexapdf (1.11.1) sha256=914578c3b2024f76b533144344379c7123d3eaa3ea586f4d98add59534b783a9 io-console (0.9.4) sha256=a403304f32928ee85e56d457b4612ca43f2703b561a3258844992bd24085fbb1 irb (1.18.0) sha256=de9454a0703a54704b9811a5ef31a60c86949fbf4013fcf244fabc7c775248e3 - json (2.21.2) sha256=1f1d3b7cf2b3ba1a69beca0bb6db13d5438b80bff3cd54cdaaa620b9b07c1c6a + json (3.0.2) sha256=8e6d7e7b11384c21230430cef90b71f14849a34a1f4452796670f7c981bd19df json-merge (7.1.9) sha256=13c979c1398e6e3126ec6c1d4acccd08a138fb06a58152f51ad9079caf03b39e kettle-changelog (1.0.8) sha256=08ded13e84276e58cd6375c1750ee6dab45fd2f67c0f0fed623a7267ddb9e300 - kettle-dev (3.1.7) sha256=23e4b9d678d307a1ad314122b028087430811da0c3ee99131849ff6aa6795c87 - kettle-family (1.3.5) sha256=792328eae94386c84873cf04ba6e22a3b8285b2b2c30299fb45994a9bdfce472 + kettle-dev (3.1.8) sha256=0923d5e6908e7554d65ff6d064b587aef01cf1d328bb64c184ce29acbc190a9e + kettle-family (1.3.6) sha256=73aac0e22a44651773c4f6aba45d878c2e743292b63fb1473e985512c52d7623 kettle-gha-pins (0.3.16) sha256=4ee906232dae309e8dbd1fca8e993bbfdad20f24892fc73776c9960081bbbbdb kettle-jem (7.1.28) sha256=6bd38a850bc3a73786cd26f3c3489f37ab6040832143df8fbd5e41ad73085d14 kettle-ndjson (0.1.18) sha256=0608518194739eafee1d2e84871a3f9759350ced5ed7aaaa1a81c72e7d17468c @@ -749,7 +749,7 @@ CHECKSUMS rspec-stubbed_env (1.0.16) sha256=7ea81332e37f637c3b6289b1c7afc8e6ea0a1c3e3ab36ff2c9611b0416367d52 rspec-support (3.13.7) sha256=0640e5570872aafefd79867901deeeeb40b0c9875a36b983d85f54fb7381c47c rspec_junit_formatter (0.6.0) sha256=40dde674e6ae4e6cc0ff560da25497677e34fefd2338cc467a8972f602b62b15 - rubocop (1.88.2) sha256=8def251c90cd955feb4daa3edc0ab56893250c4ce90ef81e6c80c03f9a939bbf + rubocop (1.91.0) sha256=9c82b7bf391c5d7e3798c5b9996e22a1fe3bd7468e351dfdeb96140c058296d0 rubocop-ast (1.50.0) sha256=b9ca88300da0803ee222ad20cdb30494c0a784eed06fdc35d254b06d662788db rubocop-capybara (2.23.0) sha256=f9ea1ba3a7561ee8e88cf76fc378ce517ce5327155f305ee7b5c2500e5aee357 rubocop-env (0.1.0) sha256=5ab9a259e71822bf514c8e900a9b68cc2f74ccc87d5493efcd03213f3068e718 @@ -759,16 +759,16 @@ CHECKSUMS rubocop-lts-rspec (1.0.7) sha256=7bf96c2745b9d000f418ab739f27a11037ba2b1dc4980aff53bbc9fec2ac4272 rubocop-lts-ruby (0.1.5) sha256=09c47d2d666693ee759e1d5549e5534a5465f10e2580fbf9049c8a341a04999e rubocop-md (2.0.4) sha256=0d076b6b5e99dea2ddc928c4bd702497decb9a8400da7808091e02ebcafcfb93 - rubocop-minitest (0.40.0) sha256=353c698199115f12151144cf0b5a96f69bb9d77b660cf6536df2c4250c672a9d + rubocop-minitest (0.41.0) sha256=c15bfe00134a30c8a00ce670b07ecc80440455ba277b8682cf0d515c98ee7426 rubocop-on-rbs (2.2.0) sha256=a78c1fa10fd9d78e26a9de67e9428a1ef6b5dc3b61e3db69e201222ccd588cf5 rubocop-packaging (0.6.0) rubocop-performance (1.26.1) sha256=cd19b936ff196df85829d264b522fd4f98b6c89ad271fa52744a8c11b8f71834 rubocop-rake (0.7.1) sha256=3797f2b6810c3e9df7376c26d5f44f3475eda59eb1adc38e6f62ecf027cbae4d rubocop-rspec (3.10.2) sha256=0b3e2ecc592cd10ecbf0095bb58d1e357905276e069643523cc19eb7495f65e2 rubocop-rspec-extra (0.3.0) sha256=ee4b6484d0332a0d23c4a465eeb607e41888eb3d4ed479f850b9f5212514f787 - rubocop-rspec_rails (2.32.0) sha256=4a0d641c72f6ebb957534f539d9d0a62c47abd8ce0d0aeee1ef4701e892a9100 + rubocop-rspec_rails (2.33.0) sha256=669e0a7a9a5fca6d6ccaf853ac65b5b8bdfde3519f604616f2c5278cbf565689 rubocop-ruby2_4 (3.0.8) sha256=d2d047d30d89330745af7101218931defcdc8b99363a36dc159b0fbc5465d72e - rubocop-thread_safety (0.7.3) sha256=067cdd52fbf5deffc18995437e45b5194236eaff4f71de3375a1f6052e48f431 + rubocop-thread_safety (0.8.0) sha256=71fe5e7894fb9acfbd4c2dcd394feabc35bd690f75b0bc846e1504d9e31f9034 ruby-merge (7.1.9) sha256=935774793dcec28614019a7c0802f14f93f805664684e7a29184ad1b4db241fa ruby-progressbar (1.13.0) sha256=80fc9c47a9b640d6834e0dc7b3c94c9df37f08cb072b7761e4a71e22cff29b33 ruby_engine (2.0.3) sha256=ca2ae7def8cfcdc2bfb6a186d36d5d59c8b403245dc0256c3802b767fa278aaa @@ -780,8 +780,8 @@ CHECKSUMS simplecov-console (0.9.5) sha256=b1108bcfff5f210143e2b8301698c367b01586f20d25a73e95475a5df6fc6ff6 simplecov-rcov (0.3.7) sha256=372f50bf6df6b6350b7d0c840f2f8bdabe021861a43c26877b747c9ac96139fc simplecov_lcov_formatter (0.9.0) sha256=4c1839bb31dcf7a8f7a6474c94ed228aa03b994e9dd5e1965e58b4121b049bed - sorbet-runtime (0.6.13528) sha256=c89d1c6936db4d2107732be5cc6d7649ccf3b5a095ae211a312e21fba82fd53a - standard (1.56.0) sha256=ae2af4d9669589162ac69ed5ef59dcf9f346d4afc81f7e62b84339310dfcb787 + sorbet-runtime (0.6.13532) sha256=b75bf990d7d4b9e07a4bf1c470d6ff8df1f3c4a1b0137edebbc4f90d25781150 + standard (1.57.0) sha256=4a65ea92421fbaf77fb54da91e16093cf79b335b8f62c35aaa3dd714c8592858 standard-custom (1.0.2) sha256=424adc84179a074f1a2a309bb9cf7cd6bfdb2b6541f20c6bf9436c0ba22a652b standard-performance (1.9.0) sha256=49483d31be448292951d80e5e67cdcb576c2502103c7b40aec6f1b6e9c88e3f2 standard-rspec (0.5.1) sha256=94c6a0c94f6d303867df5cc403a0d4994cf0d72973e86c0a0c82e3b89843989a @@ -822,4 +822,4 @@ CHECKSUMS zeitwerk (2.8.3) sha256=2c85125a8467ce069e20123d1e709a08955c9d29c118c25b46b7b7fafdbb92e5 BUNDLED WITH - 4.0.22 + 4.0.21 diff --git a/README.md b/README.md index daf205e..6d30ffa 100644 --- a/README.md +++ b/README.md @@ -811,7 +811,7 @@ Thanks for RTFM. ☺️ [📌gitmoji]: https://gitmoji.dev [📌gitmoji-img]: https://img.shields.io/badge/gitmoji_commits-%20%F0%9F%98%9C%20%F0%9F%98%8D-34495e.svg?style=flat-square [🧮kloc]: https://www.youtube.com/watch?v=dQw4w9WgXcQ -[🧮kloc-img]: https://img.shields.io/badge/KLOC-0.940-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue +[🧮kloc-img]: https://img.shields.io/badge/KLOC-0.977-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue [🔐security]: https://github.com/galtzo-floss/turbo_tests2/blob/main/SECURITY.md [🔐security-img]: https://img.shields.io/badge/security-policy-259D6C.svg?style=flat [📄copyright-notice-explainer]: https://opensource.stackexchange.com/questions/5778/why-do-licenses-such-as-the-mit-license-specify-a-single-year diff --git a/docs/TurboTests.html b/docs/TurboTests.html index 79e0fb5..a2f0691 100644 --- a/docs/TurboTests.html +++ b/docs/TurboTests.html @@ -81,7 +81,7 @@
- Modules: Version + Modules: Utils, Version diff --git a/docs/TurboTests/Runner.html b/docs/TurboTests/Runner.html index bf08336..837751f 100644 --- a/docs/TurboTests/Runner.html +++ b/docs/TurboTests/Runner.html @@ -126,6 +126,42 @@
%i[warnings stream buffered quiet].freeze
Shared wall-clock budget, in seconds, for draining a worker’s stdout and
+stderr readers after the child has exited. Normally the pipe EOFs in
+milliseconds, so this is not on the hot path; the bound exists only for a
+wedged pipe whose write end is still held open (e.g. by a grandchild),
+which can never EOF. Shared across both streams so teardown cannot take
+twice the budget.
1Short join used only after the pipe is force-closed, to give the reader a
+moment to notice EOF before it is killed.
0.1-214 -215 -216 -217 -218 -219 -220 -221 -222 -223 -224 -225 -226 -227 -228 -229 -230 -231 232 233 234 @@ -525,10 +543,28 @@+253 +254 +255 +256 +257 +258 +259 +260 +261 +262 +263 +264 +265 +266 +267 +268 +269 +270 +271250 251 252 -253
# File 'lib/turbo_tests/runner.rb', line 214 +# File 'lib/turbo_tests/runner.rb', line 232 def initialize(**opts) @formatters = opts[:formatters] @@ -597,19 +633,19 @@
-20 -21 -22 -23 -24 -25 -26 -27 -28 -29+31 +32 +33 +34 +35 +36 +37 +38 +39 +40
# File 'lib/turbo_tests/runner.rb', line 20 +# File 'lib/turbo_tests/runner.rb', line 31 def create(count) # We are unable to load parallel tests' tasks in the normal way (top of file) @@ -641,12 +677,12 @@
-156 -157 -158+167 +168 +169
# File 'lib/turbo_tests/runner.rb', line 156 +# File 'lib/turbo_tests/runner.rb', line 167 def generate_seed (Random.new_seed % 65_535).to_s @@ -694,16 +730,16 @@
-148 -149 -150 -151 -152 -153 -154+159 +160 +161 +162 +163 +164 +165
# File 'lib/turbo_tests/runner.rb', line 148 +# File 'lib/turbo_tests/runner.rb', line 159 def normalize_order(order) order = order.to_s.strip.downcase @@ -732,23 +768,23 @@
-108 -109 -110 -111 -112 -113 -114 -115 -116 -117 -118 119 120 -121+121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132
# File 'lib/turbo_tests/runner.rb', line 108 +# File 'lib/turbo_tests/runner.rb', line 119 def normalize_rspec_file_selection(files) selectors = {} @@ -807,19 +843,19 @@
-97 -98 -99 -100 -101 -102 -103 -104 -105 -106+108 +109 +110 +111 +112 +113 +114 +115 +116 +117
# File 'lib/turbo_tests/runner.rb', line 97 +# File 'lib/turbo_tests/runner.rb', line 108 def normalize_worker_output_mode(mode) value = mode.to_s.strip.downcase.tr("-", "_") @@ -851,17 +887,17 @@
-204 -205 -206 -207 -208 -209 -210 -211+222 +223 +224 +225 +226 +227 +228 +229
# File 'lib/turbo_tests/runner.rb', line 204 +# File 'lib/turbo_tests/runner.rb', line 222 def (root = Dir.pwd) %w[.rspec .rspec-local].flat_map do |path| @@ -891,27 +927,34 @@
-160 -161 -162 -163 -164 -165 -166 -167 -168 -169 -170 171 172 173 174 175 176 -177+177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195
# File 'lib/turbo_tests/runner.rb', line 160 +# File 'lib/turbo_tests/runner.rb', line 171 def rspec_configured_files_to_run configuration = RSpec::Core::Configuration.new @@ -924,11 +967,18 @@# walking to the nearest .rspec would strip those to bare # member-relative "spec/..." paths that do not exist at the root, # crashing File.stat during group sizing. - root = File.(Dir.pwd).tr("\\", "/") - root_prefix = "#{root}/" + # + # Compare paths by filesystem identity (File.identical?) rather than by + # string. On the Windows CI runner Dir.pwd reports the 8.3 short name + # (C:/Users/RUNNER~1/...) while Dir.glob, which RSpec uses to expand + # --pattern, reports the long name (C:/Users/runneradmin/...). Both + # spellings name the same directory, but File.realpath does NOT expand + # 8.3 names, so neither a string prefix nor Pathname#relative_path_from + # can reconcile them. The same class of mismatch arises from a symlinked + # ancestor. See TurboTests::Utils::Paths. + root = File.(Dir.pwd) configuration.files_to_run.map do |path| - = File.(path.to_s, root).tr("\\", "/") - .start_with?(root_prefix) ? [root_prefix.length..-1] : + TurboTests::Utils::Paths.relative_from(File.(path.to_s, root), root) end end
-31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 42 43 44 @@ -1015,10 +1054,21 @@+95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +10692 93 94 -95
# File 'lib/turbo_tests/runner.rb', line 31 +# File 'lib/turbo_tests/runner.rb', line 42 def run(opts = {}) default_file_discovery = !opts.key?(:files) || opts[:files].nil? @@ -1105,23 +1155,23 @@
-133 -134 -135 -136 -137 -138 -139 -140 -141 -142 -143 144 145 -146+146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157
# File 'lib/turbo_tests/runner.rb', line 133 +# File 'lib/turbo_tests/runner.rb', line 144 def runtime_log_from_example_status(example_status_log) statuses = RSpec::Core::ExampleStatusPersister.load_from(example_status_log) @@ -1157,18 +1207,18 @@
-123 -124 -125 -126 -127 -128 -129 -130 -131+134 +135 +136 +137 +138 +139 +140 +141 +142
# File 'lib/turbo_tests/runner.rb', line 123 +# File 'lib/turbo_tests/runner.rb', line 134 def split_rspec_location(entry) value = entry.to_s @@ -1199,33 +1249,33 @@
-179 -180 -181 -182 -183 -184 -185 -186 -187 -188 -189 -190 -191 -192 -193 -194 -195 -196 197 198 199 200 201 -202+202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220
# File 'lib/turbo_tests/runner.rb', line 179 +# File 'lib/turbo_tests/runner.rb', line 197 def worker_spec_opts(spec_opts) args = + Array(spec_opts) @@ -1277,24 +1327,6 @@
-255 -256 -257 -258 -259 -260 -261 -262 -263 -264 -265 -266 -267 -268 -269 -270 -271 -272 273 274 275 @@ -1342,10 +1374,28 @@+320 +321 +322 +323 +324 +325 +326 +327 +328 +329 +330 +331 +332 +333 +334 +335 +336 +337 +338317 318 319 -320
# File 'lib/turbo_tests/runner.rb', line 255 +# File 'lib/turbo_tests/runner.rb', line 273 def run = @parallel_options.reject { |key, _value| key == :only_group } diff --git a/docs/TurboTests/Utils.html b/docs/TurboTests/Utils.html new file mode 100644 index 0000000..ef8843d --- /dev/null +++ b/docs/TurboTests/Utils.html @@ -0,0 +1,118 @@ + + + + + ++ Module: TurboTests::Utils + + — Documentation by YARD 0.9.45 + + + + + + + + + + + + + + + + + + + + + +++ + diff --git a/docs/TurboTests/Utils/Paths.html b/docs/TurboTests/Utils/Paths.html new file mode 100644 index 0000000..e78067c --- /dev/null +++ b/docs/TurboTests/Utils/Paths.html @@ -0,0 +1,508 @@ + + + + + ++ + ++ ++ + + + + + ++ ++ + + +Module: TurboTests::Utils + + + +
++ + + + + + + + + + + ++ ++
+ +- Defined in:
+- lib/turbo_tests/utils/paths.rb +
+Defined Under Namespace
++ + + Modules: Paths + + + + +
+ + + + + + + + + ++ Module: TurboTests::Utils::Paths + + — Documentation by YARD 0.9.45 + + + + + + + + + + + + + + + + + + + + + +++ + diff --git a/docs/_index.html b/docs/_index.html index 7c5faae..a57248d 100644 --- a/docs/_index.html +++ b/docs/_index.html @@ -172,6 +172,21 @@+ + ++ ++ + + + + + ++ ++ + + +Module: TurboTests::Utils::Paths + + + +
++ + + + + + + + + + + ++ ++
+ +- Defined in:
+- lib/turbo_tests/utils/paths.rb +
+Overview
++ + + + + + + + +++Makes discovered spec paths relative to the cwd that owns the RSpec
+ +
+configuration, in a way that survives the same directory being spelled
+two different ways.== Why string comparison of paths is not safe here
+ +A path string is a name, not an identity. One directory can have
+ +
+several valid names, and the two names can reach this module from
+different APIs within the same process.On the Windows CI runner (windows-latest, github/actions) this is not
+ +
+hypothetical. Measured from the runner itself:Dir.pwd => “C:/Users/RUNNER~1/AppData/Local/Temp/turbo-tests2-…-ujisy2”
+ +
+ ENV[“TEMP”] => “C:\Users\RUNNER~1\AppData\Local\Temp”
+ Dir.tmpdir => “C:/Users/RUNNER~1/AppData/Local/Temp”
+ Dir.glob result => “C:/Users/runneradmin/AppData/Local/Temp/turbo-tests2-…-ujisy2/gems/example/spec/example_spec.rb”+ +
RUNNER~1is an 8.3 short name;runneradminis the long name of the
+same directory.Dir.pwdand the environment report the short form while
+Dir.glob— which RSpec uses to expand--pattern— reports the long
+form. Both are correct, and they are never equal as strings.In this codebase the mismatch has a second, platform-independent face:
+ +
+a symlinked ancestor. On Fedora,/home/pbolingis a symlink to
+/var/home/pboling, soDir.pwdcan report one form while a path built
+from an ENV var or a stored fixture reports the other. That is what makes
+this bug reproducible in specs without a Windows runner.== Approaches that do NOT work, and why
+ +All four were tried on this branch and all failed. Recorded so nobody
+ +
+repeats the sequence.+
+ +- +
+String prefix, the original code:
+ +root_prefix = “#{root}/”
+ +
+ expanded.start_with?(root_prefix)Never matches when the two spellings differ, so the absolute temp path
+
+leaks through. parallel_tests then calls File.stat on it during group
+sizing. It also wrongly reports “project-other/spec” as inside
+“project”, which is why spec/utils/paths_spec.rb covers siblings.- +
+Canonicalize with File.realpath, then compare:
+ +File.realpath(path) == File.realpath(root) # conceptually
+ +WRONG PREMISE: File.realpath does NOT expand 8.3 short names. Measured
+ +
+on the runner, realpath of the short path returns the short path
+unchanged:root_realpath => “C:/Users/RUNNER~1/AppData/Local/Temp/turbo-tests2-…”
+ +Canonicalization therefore cannot reconcile the two spellings at all.
+ +
+(kettle-dev’s Kettle::Dev::Paths.canonical documents realpath as
+expanding 8.3 names; that comment is inaccurate. Worth correcting if
+this module is ever extracted to a shared gem.)realpath also resolves symlinks, which introduces a second failure
+
+mode: root and file can legitimately traverse different symlink paths
+to one directory, and canonicalizing only one side makes them diverge.- +
+Component-wise comparison of the canonicalized parts (split on
+
+File::SEPARATOR, compare element by element, case-insensitively on
+Windows). Built on the same false premise as #2, so it fails
+identically —RUNNER~1never equalsrunneradminin any case
+folding. It additionally depended on File::ALT_SEPARATOR, which
+measured as “” (empty string, not nil) on that runner, making the
+separator-normalizingtra silent no-op. Do not trust ALT_SEPARATOR
+to be present.- +
+Pathname#relative_path_from, Ruby’s own stdlib helper:
+ +Pathname.new(file).relative_path_from(Pathname.new(root)).to_s
+ +It is purely lexical. With differing spellings it walks all the way up
+ +
+and back down, producing:”../../../../../runneradmin/AppData/Local/Temp/…/example_spec.rb”
+ +Note it does not raise, so a
+rescue ArgumentErrorguard cannot catch
+it. The result is a plausible-looking but wrong relative path, which is
+worse than the leak in #1 because it silently selects the wrong files.
+The same nonsense appears on the symlink case:
+“../../../../../../var/home/pboling/src/my/…”.== The approach that works
+ +File.identical? asks the filesystem whether two names denote one object,
+ +
+by inode on Unix and by file index on Windows. It is spelling-agnostic,
+which is exactly the property needed. Measured on the runner:File.identical?(root, root) => true
+ +
+ File.identical?(file, file) => trueand a walk-up from the long-name file to the short-name root produced the
+ +
+correct “gems/example/spec/example_spec.rb”, while the string-prefix and
+Pathname baselines both failed in the same run.So: ascend from the file, asking at each level “is this the root?”, and
+ +
+re-join the basenames on the way back down. The relative path is built
+from the file’s spelling, which is what the caller needs, since
+parallel_tests stats these paths and spawns workers from this cwd.== How this was finally diagnosed
+ +The failure only reproduced on the Windows CI runner, and its output was
+ +
+swallowed: these discovery specs run a subprocess via Open3.capture3 whose
+stderr is only surfaced when the status check fails, and the assertion
+that failed was the JSON comparison. Four blind fixes failed in a row.What resolved it was a temporary, Windows-guarded spec
+ +
+(if: Gem.win_platform?) that deliberately failed with the diagnostic
+JSON embedded in the expectation message, so the runner’s real path forms
+printed into the CI log. Guarding it kept the other 22 checks green while
+one job reported the data. That spec is deleted; the findings it produced
+are this comment plus spec/utils/paths_spec.rb.Lesson: when a bug is environment-specific and the environment is not
+ + +
+available locally, spend the iteration on instrumenting the real
+environment rather than on another hypothesized fix.+ Class Method Summary + collapse +
+ ++ +
+ + + + + +- + + + .relative_from(file, root) ⇒ String + + + + + + + + + + + + + +
+ + ++ +Returns the path of +file+ relative to +root+ when +file+ is +root+ itself or lives inside it, compared by filesystem identity rather than by string.
+- + + + .within?(file, root) ⇒ Boolean + + + + + + + + + + + + + +
+ + ++ +Returns true when +file+ is +root+ itself or lives inside it, compared by filesystem identity.
+++ +Class Method Details
+ + +++ ++ + .relative_from(file, root) ⇒ String + + + + + +
++++Returns the path of +file+ relative to +root+ when +file+ is +root+
+ +
+itself or lives inside it, compared by filesystem identity rather than
+by string.Returns the expanded +file+ unchanged when it is not inside +root+
+ + +
+(there is nothing to strip) or when identity cannot be established,
+e.g. because one side does not exist. That matches the long-standing
+behavior of passing through paths outside the root untouched.+
++ ++ ++ + +145 +146 +147 +148 +149 +150++ +# File 'lib/turbo_tests/utils/paths.rb', line 145 + +def relative_from(file, root) + = File.(file.to_s) + relative = relative_to_root(, root) + + relative || +end+++ ++ + .within?(file, root) ⇒ Boolean + + + + + +
++++Returns true when +file+ is +root+ itself or lives inside it, compared
+ +
+by filesystem identity.Unlike a string prefix test this does not treat a sibling directory
+ + +
+that merely shares a prefix (“project-other”) as being inside “project”.+
++ ++ ++ + +161 +162 +163 +164 +165++ +# File 'lib/turbo_tests/utils/paths.rb', line 161 + +def within?(file, root) + return false if blank?(file) || blank?(root) + + relative_to_root(File.(file.to_s), root).is_a?(String) +end+Namespace Listing A-Z
+
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.
+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.
+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?.
+