turbo_tests2 3.2.13 → 3.2.15
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 +4 -4
- checksums.yaml.gz.sig +0 -0
- data/CHANGELOG.md +57 -1
- data/README.md +64 -16
- data/lib/turbo_tests/runner.rb +161 -25
- data/lib/turbo_tests/utils/paths.rb +235 -0
- data/lib/turbo_tests/version.rb +1 -1
- data.tar.gz.sig +0 -0
- metadata +11 -10
- metadata.gz.sig +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b455a333f0fe9a3521173ff0e73b309b74baa6b37da905f5fcfa1a79a5eb075b
|
|
4
|
+
data.tar.gz: 8a2f7d15910049b3b8366fd1ed6e604581a6c7444a6e95babdcb2f344e73b054
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3cee9299d9a75a189a763621873df30959b68999972212f05f4ea81ba04200c66d9cbb56e2cc402947b58689881b16cdbc25ac9579205c473c4f359a2cab72ca
|
|
7
|
+
data.tar.gz: 8ec3ac430a73305ae22bbc58c4ad9e82e0f04f41645853b8da4d50a24f868fe5a6611e5a222774a423df2db89e5176f4f69221ed32adcb85771fab482093b85b
|
checksums.yaml.gz.sig
CHANGED
|
Binary file
|
data/CHANGELOG.md
CHANGED
|
@@ -30,6 +30,58 @@ Please file a bug if you notice a violation of semantic versioning.
|
|
|
30
30
|
|
|
31
31
|
### Security
|
|
32
32
|
|
|
33
|
+
## [3.2.15] - 2026-10-13
|
|
34
|
+
|
|
35
|
+
- TAG: [v3.2.15][3.2.15t]
|
|
36
|
+
- COVERAGE: 96.67% -- 988/1022 lines in 11 files
|
|
37
|
+
- BRANCH COVERAGE: 85.06% -- 262/308 branches in 11 files
|
|
38
|
+
- 39.42% documented
|
|
39
|
+
|
|
40
|
+
### Changed
|
|
41
|
+
|
|
42
|
+
- [kc] kettle-jem/prepare: updated 13 project files:
|
|
43
|
+
- configuration (1)
|
|
44
|
+
- dependencies (12)
|
|
45
|
+
- [kc] kettle-jem/template: updated 12 project files:
|
|
46
|
+
- code and tests (2)
|
|
47
|
+
- documentation (1)
|
|
48
|
+
- other (2)
|
|
49
|
+
- workflows (7)
|
|
50
|
+
|
|
51
|
+
### Fixed
|
|
52
|
+
|
|
53
|
+
- The “Runtime and Platform Support” table in the README listed TruffleRuby 22.3, 23.0 and TruffleRuby-head as covered by CI, and described `rspec-pending_for` spec exclusions for 22.3/23.0. No workflow runs those engines (CI covers TruffleRuby 23.1, 24.2, 25.0, 33.0 and current), `heads.yml` runs `ruby-head` and `jruby-head` only, and `rspec-pending_for` is neither a dependency nor referenced by any spec. The badge rows for 22.3/23.0 had already been removed by templating, leaving the prose table describing CI that no longer runs.
|
|
54
|
+
|
|
55
|
+
- Watchdog on the worker message queue: if no worker message arrives within TURBO_TESTS2_WATCHDOG_SECONDS (default 900), remaining workers are killed, unfinished groups reported, and the run fails — instead of blocking forever when a worker dies without an exit message (the parallel-worker CI hang class on 4-core runners)
|
|
56
|
+
|
|
57
|
+
## [3.2.14] - 2026-10-08
|
|
58
|
+
|
|
59
|
+
- TAG: [v3.2.14][3.2.14t]
|
|
60
|
+
- COVERAGE: 96.93% -- 947/977 lines in 11 files
|
|
61
|
+
- BRANCH COVERAGE: 85.57% -- 255/298 branches in 11 files
|
|
62
|
+
- 39.42% documented
|
|
63
|
+
|
|
64
|
+
### Changed
|
|
65
|
+
|
|
66
|
+
- [kc] kettle-jem/prepare: updated 6 project files:
|
|
67
|
+
- dependencies (6)
|
|
68
|
+
|
|
69
|
+
- [kc] kettle-jem/template: updated 4 project files:
|
|
70
|
+
- code and tests (1)
|
|
71
|
+
- documentation (1)
|
|
72
|
+
- other (1)
|
|
73
|
+
- workflows (1)
|
|
74
|
+
|
|
75
|
+
### Fixed
|
|
76
|
+
|
|
77
|
+
- 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.
|
|
78
|
+
|
|
79
|
+
- 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.
|
|
80
|
+
|
|
81
|
+
- 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?.
|
|
82
|
+
|
|
83
|
+
- [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.
|
|
84
|
+
|
|
33
85
|
## [3.2.13] - 2026-10-01
|
|
34
86
|
|
|
35
87
|
- TAG: [v3.2.13][3.2.13t]
|
|
@@ -690,7 +742,11 @@ Please file a bug if you notice a violation of semantic versioning.
|
|
|
690
742
|
|
|
691
743
|
- Initial release
|
|
692
744
|
|
|
693
|
-
[Unreleased]: https://github.com/galtzo-floss/turbo_tests2/compare/v3.2.
|
|
745
|
+
[Unreleased]: https://github.com/galtzo-floss/turbo_tests2/compare/v3.2.15...HEAD
|
|
746
|
+
[3.2.15]: https://github.com/galtzo-floss/turbo_tests2/compare/v3.2.14...v3.2.15
|
|
747
|
+
[3.2.15t]: https://github.com/galtzo-floss/turbo_tests2/releases/tag/v3.2.15
|
|
748
|
+
[3.2.14]: https://github.com/galtzo-floss/turbo_tests2/compare/v3.2.13...v3.2.14
|
|
749
|
+
[3.2.14t]: https://github.com/galtzo-floss/turbo_tests2/releases/tag/v3.2.14
|
|
694
750
|
[3.2.13]: https://github.com/galtzo-floss/turbo_tests2/compare/v3.2.12...v3.2.13
|
|
695
751
|
[3.2.13t]: https://github.com/galtzo-floss/turbo_tests2/releases/tag/v3.2.13
|
|
696
752
|
[3.2.12]: https://github.com/galtzo-floss/turbo_tests2/compare/v3.2.11...v3.2.12
|
data/README.md
CHANGED
|
@@ -48,18 +48,17 @@ work in an untested environment:
|
|
|
48
48
|
| MRI head | Parallel support | Unsupported | Unsupported | CI is allowed to fail while Ruby is in development. |
|
|
49
49
|
| JRuby 9.2-9.4 | Parallel support | Unsupported | Unsupported | Versioned JRuby workflows run on Linux. |
|
|
50
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
51
|
| TruffleRuby 23.1 | Parallel support | Unsupported | Unsupported | Experimental. |
|
|
54
52
|
| 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
53
|
|
|
57
|
-
The
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
54
|
+
The Windows current-Ruby job runs in parallel, but runtime-log recording is
|
|
55
|
+
disabled there because the runtime logger needs file locking that Windows does
|
|
56
|
+
not provide reliably (`record_runtime = !Gem.win_platform?`); see below.
|
|
57
|
+
|
|
58
|
+
TruffleRuby 22.3, 23.0 and TruffleRuby-head are not listed because no workflow
|
|
59
|
+
covers them. `heads.yml` runs `ruby-head` and `jruby-head` only, and
|
|
60
|
+
`dep-heads.yml` runs the released `truffleruby` engine against head-of-branch
|
|
61
|
+
dependencies, which is not a head engine build.
|
|
63
62
|
|
|
64
63
|
### Why incremental output?
|
|
65
64
|
|
|
@@ -99,7 +98,7 @@ Finished in 2 minute 25.15 seconds (files took 0 seconds to load)
|
|
|
99
98
|
| Tokens to Remember | [![Gem name][⛳️name-img]][⛳️gem-name] [![Gem namespace][⛳️namespace-img]][⛳️gem-namespace] |
|
|
100
99
|
|-------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
|
101
100
|
| Works with JRuby | [![JRuby 9.2 Compat][💎jruby-9.2i]][🚎jruby-9.2-wf] [![JRuby 9.3 Compat][💎jruby-9.3i]][🚎jruby-9.3-wf] <br/> [![JRuby 9.4 Compat][💎jruby-9.4i]][🚎jruby-9.4-wf] [![JRuby 10.0 Compat][💎jruby-10.0i]][🚎jruby-10.0-wf] [![JRuby current Compat][💎jruby-c-i]][🚎10-j-wf] [![JRuby HEAD Compat][💎jruby-headi]][🚎3-hd-wf]|
|
|
102
|
-
| Works with Truffle Ruby | [![Truffle Ruby
|
|
101
|
+
| Works with Truffle Ruby | [![Truffle Ruby 23.1 Compat][💎truby-23.1i]][🚎truby-23.1-wf] <br/> [![Truffle Ruby 24.2 Compat][💎truby-24.2i]][🚎truby-24.2-wf] [![Truffle Ruby 25.0 Compat][💎truby-25.0i]][🚎truby-25.0-wf] [![Truffle Ruby 33.0 Compat][💎truby-33.0i]][🚎truby-33.0-wf] [![Truffle Ruby current Compat][💎truby-c-i]][🚎9-t-wf]|
|
|
103
102
|
| Works with MRI Ruby 4 | [![Ruby current Compat][💎ruby-c-i]][🚎11-c-wf] [![Ruby HEAD Compat][💎ruby-headi]][🚎3-hd-wf]|
|
|
104
103
|
| Works with MRI Ruby 3 | [![Ruby 3.0 Compat][💎ruby-3.0i]][🚎ruby-3.0-wf] [![Ruby 3.1 Compat][💎ruby-3.1i]][🚎ruby-3.1-wf] [![Ruby 3.2 Compat][💎ruby-3.2i]][🚎ruby-3.2-wf] [![Ruby 3.3 Compat][💎ruby-3.3i]][🚎ruby-3.3-wf] [![Ruby 3.4 Compat][💎ruby-3.4i]][🚎ruby-3.4-wf]|
|
|
105
104
|
| Works with MRI Ruby 2 | [![Ruby 2.4 Compat][💎ruby-2.4i]][🚎ruby-2.4-wf] [![Ruby 2.5 Compat][💎ruby-2.5i]][🚎ruby-2.5-wf] [![Ruby 2.6 Compat][💎ruby-2.6i]][🚎ruby-2.6-wf] [![Ruby 2.7 Compat][💎ruby-2.7i]][🚎ruby-2.7-wf]|
|
|
@@ -138,6 +137,23 @@ The _amazing_ test matrix is powered by the kettle-dev stack.
|
|
|
138
137
|
|
|
139
138
|
</details>
|
|
140
139
|
|
|
140
|
+
### Runtime and Platform Support
|
|
141
|
+
|
|
142
|
+
Support tiers describe what this project's CI actually exercises, not what may
|
|
143
|
+
work in an untested environment:
|
|
144
|
+
|
|
145
|
+
- **Parallel support**: CI runs the test suite with multiple workers available.
|
|
146
|
+
- **Unsupported**: the engine/platform combination is not covered by CI and is
|
|
147
|
+
not a project compatibility claim.
|
|
148
|
+
|
|
149
|
+
| Ruby engine/version | Linux | macOS | Windows | Notes |
|
|
150
|
+
|---------------------|-------|-------|---------|-------|
|
|
151
|
+
| MRI 3.1-3.4 | Parallel support | Unsupported | Unsupported | Versioned Ruby workflows run on Linux. |
|
|
152
|
+
| MRI current | Parallel support | Parallel support | Unsupported | Current Ruby workflow covers the declared OS platforms. |
|
|
153
|
+
| MRI head | Parallel support | Unsupported | Unsupported | CI is allowed to fail while Ruby is in development. |
|
|
154
|
+
| JRuby (all CI lanes) | Parallel support | Unsupported | Unsupported | Engine workflows run on Linux; macOS and Windows are not exercised by CI. |
|
|
155
|
+
| TruffleRuby (all CI lanes) | Parallel support | Unsupported | Unsupported | Never shipped a Windows build (zero Windows assets across 66 releases scanned 2026-10-10); setup-ruby hard-fails ([source](https://github.com/ruby/setup-ruby/blob/master/README.md)) |
|
|
156
|
+
|
|
141
157
|
### Federated DVCS
|
|
142
158
|
|
|
143
159
|
<details markdown="1">
|
|
@@ -368,6 +384,43 @@ count at the number of discovered test groups.
|
|
|
368
384
|
Known coverage formatter chatter remains collapsed into the concise coverage
|
|
369
385
|
summary.
|
|
370
386
|
|
|
387
|
+
### Stall watchdog
|
|
388
|
+
|
|
389
|
+
If the worker message queue goes silent — every worker alive but none
|
|
390
|
+
producing example/group messages — a run can otherwise hang until the CI
|
|
391
|
+
provider kills the job (GitHub Actions: 6 hours).
|
|
392
|
+
|
|
393
|
+
`TURBO_TESTS2_WATCHDOG_SECONDS=N` (integer, `0` = disabled) bounds that
|
|
394
|
+
silence. **Default is `0` (disabled) in 3.x.** In 4.0 the default becomes
|
|
395
|
+
`900` and the kill behavior is always on.
|
|
396
|
+
|
|
397
|
+
When the watchdog fires it kills the remaining worker processes, prints the
|
|
398
|
+
unfinished groups and worker pids, and fails the run.
|
|
399
|
+
|
|
400
|
+
Setting it:
|
|
401
|
+
|
|
402
|
+
```bash
|
|
403
|
+
TURBO_TESTS2_WATCHDOG_SECONDS=900 bundle exec turbo_tests2
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
Notes:
|
|
407
|
+
|
|
408
|
+
- The clock measures **silence across all workers**, not per-test duration.
|
|
409
|
+
In parallel lanes any other worker finishing an example keeps the run
|
|
410
|
+
alive; only serial lanes (`TURBO_TESTS2_MAX_PROCESSES=1`) can trip on a
|
|
411
|
+
single long test. Raise the value there if you have legitimately long,
|
|
412
|
+
silent tests.
|
|
413
|
+
- CI jobs that hang despite the watchdog will still hit the provider
|
|
414
|
+
limit; also set `timeout-minutes` on the CI job. Locally, an outer
|
|
415
|
+
supervisor (process watcher, container timeout) plays the same role.
|
|
416
|
+
- When the watchdog is disabled (`0`), a 900s silence prints a one-time
|
|
417
|
+
deprecation notice describing the 4.0 default change and both options
|
|
418
|
+
(adopt the kill via `TURBO_TESTS2_WATCHDOG_SECONDS`, or explicitly opt out
|
|
419
|
+
with `0`). The notice is provider-neutral: it warns that with the
|
|
420
|
+
watchdog off, whatever outer process watcher exists — a CI job timeout,
|
|
421
|
+
a container orchestrator, or a local supervisor — is what will eventually
|
|
422
|
+
end a hung run.
|
|
423
|
+
|
|
371
424
|
`turbo_tests2` supports custom formatter such as Fuubar, but you might need to require it:
|
|
372
425
|
|
|
373
426
|
```bash
|
|
@@ -734,8 +787,6 @@ Thanks for RTFM. ☺️
|
|
|
734
787
|
[🚎jruby-9.3-wf]: https://github.com/galtzo-floss/turbo_tests2/actions/workflows/jruby-9.3.yml
|
|
735
788
|
[🚎jruby-9.4-wf]: https://github.com/galtzo-floss/turbo_tests2/actions/workflows/jruby-9.4.yml
|
|
736
789
|
[🚎jruby-10.0-wf]: https://github.com/galtzo-floss/turbo_tests2/actions/workflows/jruby-10.0.yml
|
|
737
|
-
[🚎truby-22.3-wf]: https://github.com/galtzo-floss/turbo_tests2/actions/workflows/truffleruby-22.3.yml
|
|
738
|
-
[🚎truby-23.0-wf]: https://github.com/galtzo-floss/turbo_tests2/actions/workflows/truffleruby-23.0.yml
|
|
739
790
|
[🚎truby-23.1-wf]: https://github.com/galtzo-floss/turbo_tests2/actions/workflows/truffleruby-23.1.yml
|
|
740
791
|
[🚎truby-24.2-wf]: https://github.com/galtzo-floss/turbo_tests2/actions/workflows/truffleruby-24.2.yml
|
|
741
792
|
[🚎truby-25.0-wf]: https://github.com/galtzo-floss/turbo_tests2/actions/workflows/truffleruby-25.0.yml
|
|
@@ -771,14 +822,11 @@ Thanks for RTFM. ☺️
|
|
|
771
822
|
[💎ruby-3.4i]: https://img.shields.io/badge/Ruby-3.4-CC342D?style=for-the-badge&logo=ruby&logoColor=white
|
|
772
823
|
[💎ruby-c-i]: https://img.shields.io/badge/Ruby-current-CC342D?style=for-the-badge&logo=ruby&logoColor=green
|
|
773
824
|
[💎ruby-headi]: https://img.shields.io/badge/Ruby-HEAD-CC342D?style=for-the-badge&logo=ruby&logoColor=blue
|
|
774
|
-
[💎truby-22.3i]: https://img.shields.io/badge/Truffle_Ruby-22.3-34BCB1?style=for-the-badge&logo=ruby&logoColor=pink
|
|
775
|
-
[💎truby-23.0i]: https://img.shields.io/badge/Truffle_Ruby-23.0-34BCB1?style=for-the-badge&logo=ruby&logoColor=pink
|
|
776
825
|
[💎truby-23.1i]: https://img.shields.io/badge/Truffle_Ruby-23.1-34BCB1?style=for-the-badge&logo=ruby&logoColor=pink
|
|
777
826
|
[💎truby-24.2i]: https://img.shields.io/badge/Truffle_Ruby-24.2-34BCB1?style=for-the-badge&logo=ruby&logoColor=pink
|
|
778
827
|
[💎truby-25.0i]: https://img.shields.io/badge/Truffle_Ruby-25.0-34BCB1?style=for-the-badge&logo=ruby&logoColor=pink
|
|
779
828
|
[💎truby-33.0i]: https://img.shields.io/badge/Truffle_Ruby-33.0-34BCB1?style=for-the-badge&logo=ruby&logoColor=pink
|
|
780
829
|
[💎truby-c-i]: https://img.shields.io/badge/Truffle_Ruby-current-34BCB1?style=for-the-badge&logo=ruby&logoColor=green
|
|
781
|
-
[💎truby-headi]: https://img.shields.io/badge/Truffle_Ruby-HEAD-34BCB1?style=for-the-badge&logo=ruby&logoColor=blue
|
|
782
830
|
[💎jruby-9.2i]: https://img.shields.io/badge/JRuby-9.2-FBE742?style=for-the-badge&logo=ruby&logoColor=red
|
|
783
831
|
[💎jruby-9.3i]: https://img.shields.io/badge/JRuby-9.3-FBE742?style=for-the-badge&logo=ruby&logoColor=red
|
|
784
832
|
[💎jruby-9.4i]: https://img.shields.io/badge/JRuby-9.4-FBE742?style=for-the-badge&logo=ruby&logoColor=red
|
|
@@ -811,7 +859,7 @@ Thanks for RTFM. ☺️
|
|
|
811
859
|
[📌gitmoji]: https://gitmoji.dev
|
|
812
860
|
[📌gitmoji-img]: https://img.shields.io/badge/gitmoji_commits-%20%F0%9F%98%9C%20%F0%9F%98%8D-34495e.svg?style=flat-square
|
|
813
861
|
[🧮kloc]: https://www.youtube.com/watch?v=dQw4w9WgXcQ
|
|
814
|
-
[🧮kloc-img]: https://img.shields.io/badge/KLOC-
|
|
862
|
+
[🧮kloc-img]: https://img.shields.io/badge/KLOC-1.022-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue
|
|
815
863
|
[🔐security]: https://github.com/galtzo-floss/turbo_tests2/blob/main/SECURITY.md
|
|
816
864
|
[🔐security-img]: https://img.shields.io/badge/security-policy-259D6C.svg?style=flat
|
|
817
865
|
[📄copyright-notice-explainer]: https://opensource.stackexchange.com/questions/5778/why-do-licenses-such-as-the-mit-license-specify-a-single-year
|
data/lib/turbo_tests/runner.rb
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "json"
|
|
4
|
+
require "timeout"
|
|
4
5
|
require "fileutils"
|
|
5
6
|
require "parallel_tests/rspec/runner"
|
|
6
7
|
require "rspec/core"
|
|
@@ -8,6 +9,7 @@ require "shellwords"
|
|
|
8
9
|
require "tempfile"
|
|
9
10
|
|
|
10
11
|
require_relative "../utils/hash_extension"
|
|
12
|
+
require_relative "utils/paths"
|
|
11
13
|
|
|
12
14
|
module TurboTests
|
|
13
15
|
class Runner
|
|
@@ -15,6 +17,16 @@ module TurboTests
|
|
|
15
17
|
DEFAULT_RUNTIME_LOG = "tmp/turbo_rspec_runtime.log"
|
|
16
18
|
DEFAULT_WORKER_OUTPUT_MODE = :warnings
|
|
17
19
|
WORKER_OUTPUT_MODES = %i[warnings stream buffered quiet].freeze
|
|
20
|
+
# Shared wall-clock budget, in seconds, for draining a worker's stdout and
|
|
21
|
+
# stderr readers after the child has exited. Normally the pipe EOFs in
|
|
22
|
+
# milliseconds, so this is not on the hot path; the bound exists only for a
|
|
23
|
+
# wedged pipe whose write end is still held open (e.g. by a grandchild),
|
|
24
|
+
# which can never EOF. Shared across both streams so teardown cannot take
|
|
25
|
+
# twice the budget.
|
|
26
|
+
READER_DRAIN_TIMEOUT = 1
|
|
27
|
+
# Short join used only after the pipe is force-closed, to give the reader a
|
|
28
|
+
# moment to notice EOF before it is killed.
|
|
29
|
+
FORCE_STOP_JOIN = 0.1
|
|
18
30
|
|
|
19
31
|
class << self
|
|
20
32
|
def create(count)
|
|
@@ -160,23 +172,26 @@ module TurboTests
|
|
|
160
172
|
def rspec_configured_files_to_run
|
|
161
173
|
configuration = RSpec::Core::Configuration.new
|
|
162
174
|
RSpec::Core::ConfigurationOptions.new(["spec"]).configure(configuration)
|
|
175
|
+
# Normalize against the cwd that owns the RSpec configuration being
|
|
176
|
+
# read, NOT the nearest .rspec ancestor of each file. parallel_tests
|
|
177
|
+
# stats these paths and spawns workers from this cwd, so the returned
|
|
178
|
+
# paths must resolve from here. In a stacked monorepo the root .rspec
|
|
179
|
+
# aggregates per-gem specs (each gem may also have its own .rspec);
|
|
180
|
+
# walking to the nearest .rspec would strip those to bare
|
|
181
|
+
# member-relative "spec/..." paths that do not exist at the root,
|
|
182
|
+
# crashing File.stat during group sizing.
|
|
183
|
+
#
|
|
184
|
+
# Compare paths by filesystem identity (File.identical?) rather than by
|
|
185
|
+
# string. On the Windows CI runner Dir.pwd reports the 8.3 short name
|
|
186
|
+
# (C:/Users/RUNNER~1/...) while Dir.glob, which RSpec uses to expand
|
|
187
|
+
# --pattern, reports the long name (C:/Users/runneradmin/...). Both
|
|
188
|
+
# spellings name the same directory, but File.realpath does NOT expand
|
|
189
|
+
# 8.3 names, so neither a string prefix nor Pathname#relative_path_from
|
|
190
|
+
# can reconcile them. The same class of mismatch arises from a symlinked
|
|
191
|
+
# ancestor. See TurboTests::Utils::Paths.
|
|
192
|
+
root = File.expand_path(Dir.pwd)
|
|
163
193
|
configuration.files_to_run.map do |path|
|
|
164
|
-
|
|
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
|
|
194
|
+
TurboTests::Utils::Paths.relative_from(File.expand_path(path.to_s, root), root)
|
|
180
195
|
end
|
|
181
196
|
end
|
|
182
197
|
|
|
@@ -234,6 +249,12 @@ module TurboTests
|
|
|
234
249
|
@load_time = 0
|
|
235
250
|
@load_count = 0
|
|
236
251
|
@failure_count = 0
|
|
252
|
+
# Deprecation-period default: disabled. In 4.0 the kill behavior
|
|
253
|
+
# becomes the default (TURBO_TESTS2_WATCHDOG_SECONDS=900) and this
|
|
254
|
+
# warning is removed. See README "Stall watchdog".
|
|
255
|
+
@watchdog_seconds = Float(ENV.fetch("TURBO_TESTS2_WATCHDOG_SECONDS", "0"))
|
|
256
|
+
@watchdog_warned = false
|
|
257
|
+
@last_message_monotonic = nil
|
|
237
258
|
|
|
238
259
|
# Supports runtime_log as a top level option,
|
|
239
260
|
# but also nested inside parallel_options
|
|
@@ -477,10 +498,21 @@ module TurboTests
|
|
|
477
498
|
status = wait_thr.value
|
|
478
499
|
@failed_worker_statuses.store(process_id, status) unless status.success?
|
|
479
500
|
@messages << {type: "error", process_id: process_id} unless status.success?
|
|
480
|
-
@messages << {type: "exit", process_id: process_id}
|
|
481
501
|
ensure
|
|
482
|
-
|
|
483
|
-
|
|
502
|
+
# Drain BOTH readers before signaling this process's exit. The
|
|
503
|
+
# stdout reader parses the pipe into @messages rows and pushes its
|
|
504
|
+
# own {type: "exit"} at EOF; the stderr copy thread only buffers
|
|
505
|
+
# text into @worker_output and queues nothing, but it is drained
|
|
506
|
+
# too so no buffered warning output is lost. Signaling exit here
|
|
507
|
+
# first let handle_messages reach process_count exits and break
|
|
508
|
+
# while this worker's stdout rows were still queued behind that
|
|
509
|
+
# exit, silently dropping them (intermittent example undercount
|
|
510
|
+
# and missing streamed output under CI contention). The exit below
|
|
511
|
+
# is a deduped fallback for a stdout reader that was force-stopped
|
|
512
|
+
# before reaching EOF, and being in ensure also prevents a hang if
|
|
513
|
+
# wait_thr.value raises.
|
|
514
|
+
drain_worker_readers(stdout_thread, stdout, stderr_thread, stderr)
|
|
515
|
+
@messages << {type: "exit", process_id: process_id}
|
|
484
516
|
untrack_parallel_pid(wait_thr.pid, pid_file_path)
|
|
485
517
|
end
|
|
486
518
|
end
|
|
@@ -675,22 +707,126 @@ module TurboTests
|
|
|
675
707
|
"#{kind} Coverage: #{match[3]} (#{match[1]} / #{match[2]})"
|
|
676
708
|
end
|
|
677
709
|
|
|
678
|
-
def
|
|
679
|
-
|
|
710
|
+
def drain_worker_readers(stdout_thread, stdout, stderr_thread, stderr)
|
|
711
|
+
# One shared deadline across both streams so a wedged pipe cannot double
|
|
712
|
+
# teardown time or hang the watcher. stdout carries the worker's JSON
|
|
713
|
+
# rows; stderr only carries buffered text.
|
|
714
|
+
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + READER_DRAIN_TIMEOUT
|
|
715
|
+
stop_reader_thread(stdout_thread, stdout, deadline)
|
|
716
|
+
stop_reader_thread(stderr_thread, stderr, deadline)
|
|
717
|
+
end
|
|
718
|
+
|
|
719
|
+
def stop_reader_thread(thread, io, deadline)
|
|
720
|
+
# Let the reader reach EOF and queue any buffered worker rows BEFORE any
|
|
721
|
+
# force-stop. Closing the pipe while the child's output is still
|
|
722
|
+
# kernel-buffered would discard it and silently drop that worker's
|
|
723
|
+
# results (intermittent example undercount / missing streamed output).
|
|
724
|
+
# The shared deadline bounds teardown when a pipe can never EOF because
|
|
725
|
+
# its write end is still held open (e.g. by a grandchild).
|
|
726
|
+
remaining = deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
727
|
+
return if remaining.positive? && thread.join(remaining)
|
|
680
728
|
|
|
681
729
|
close_io(io)
|
|
682
|
-
return if thread.join(
|
|
730
|
+
return if thread.join(FORCE_STOP_JOIN)
|
|
731
|
+
|
|
732
|
+
thread.kill.join(FORCE_STOP_JOIN)
|
|
733
|
+
end
|
|
734
|
+
|
|
735
|
+
# Blocks for the next worker message, enforcing a stall watchdog. A
|
|
736
|
+
# worker whose stdout reader and exit-ensuring thread both fail to
|
|
737
|
+
# deliver an exit message (process killed by the OOM killer, forked
|
|
738
|
+
# child wedged in a native call) used to leave handle_messages blocked
|
|
739
|
+
# on the queue forever — CI jobs hung for hours with no diagnostics.
|
|
740
|
+
# On a stall we kill the surviving worker pids, report which groups
|
|
741
|
+
# never finished, and fail the run instead.
|
|
742
|
+
def pop_message_with_watchdog
|
|
743
|
+
# 0 (the default) disables the watchdog: block indefinitely, exactly the
|
|
744
|
+
# pre-watchdog behavior.
|
|
745
|
+
return @messages.pop if @watchdog_seconds <= 0
|
|
746
|
+
|
|
747
|
+
loop do
|
|
748
|
+
timeout = [@watchdog_seconds - (Process.clock_gettime(Process::CLOCK_MONOTONIC) - @last_message_monotonic), 1.0].max
|
|
749
|
+
begin
|
|
750
|
+
message = Timeout.timeout(timeout) { @messages.pop }
|
|
751
|
+
@last_message_monotonic = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
752
|
+
return message
|
|
753
|
+
rescue Timeout::Error
|
|
754
|
+
stall_report = remaining_workers_report
|
|
755
|
+
warn("turbo_tests2: no worker messages for #{@watchdog_seconds}s; killing stalled workers and failing the run")
|
|
756
|
+
warn(stall_report)
|
|
757
|
+
kill_remaining_workers
|
|
758
|
+
@reporter.error_outside_of_examples(
|
|
759
|
+
"turbo_tests2: watchdog fired after #{@watchdog_seconds}s without worker messages; workers killed\n#{stall_report}"
|
|
760
|
+
)
|
|
761
|
+
@error = true
|
|
762
|
+
return {type: "exit", process_id: :watchdog}
|
|
763
|
+
end
|
|
764
|
+
end
|
|
765
|
+
end
|
|
766
|
+
|
|
767
|
+
def remaining_workers_report
|
|
768
|
+
pids = tracked_pids
|
|
769
|
+
live = []
|
|
770
|
+
pids.each do |pid|
|
|
771
|
+
begin
|
|
772
|
+
Process.kill(0, pid)
|
|
773
|
+
live << pid
|
|
774
|
+
rescue
|
|
775
|
+
nil
|
|
776
|
+
end
|
|
777
|
+
end
|
|
778
|
+
lines = ["Workers total: #{@num_processes}; tracked pids: #{pids.inspect}; live: #{live.inspect}"]
|
|
779
|
+
(@tests_in_groups || []).each_with_index do |group, idx|
|
|
780
|
+
lines << "group #{idx + 1}: #{Array(group).join(", ")}" unless group.nil?
|
|
781
|
+
end
|
|
782
|
+
lines.join("\n")
|
|
783
|
+
end
|
|
683
784
|
|
|
684
|
-
|
|
685
|
-
|
|
785
|
+
def tracked_pids
|
|
786
|
+
path = ParallelTests.pid_file_path
|
|
787
|
+
return [] unless path && File.file?(path)
|
|
788
|
+
|
|
789
|
+
File.read(path).lines.map { |l| l.strip.to_i }.reject(&:zero?)
|
|
790
|
+
rescue
|
|
791
|
+
[]
|
|
792
|
+
end
|
|
793
|
+
|
|
794
|
+
def kill_remaining_workers
|
|
795
|
+
tracked_pids.each do |pid|
|
|
796
|
+
begin
|
|
797
|
+
Process.kill("KILL", pid)
|
|
798
|
+
rescue
|
|
799
|
+
nil
|
|
800
|
+
end
|
|
801
|
+
end
|
|
686
802
|
end
|
|
687
803
|
|
|
688
804
|
def handle_messages
|
|
689
805
|
exited_process_ids = {}
|
|
690
806
|
process_count = @tests_in_groups&.size || @num_processes
|
|
691
807
|
|
|
808
|
+
# Tests pre-seed @last_message_monotonic to simulate a stall; only
|
|
809
|
+
# initialize it here when unset.
|
|
810
|
+
@last_message_monotonic ||= Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
811
|
+
warned_silence = false
|
|
692
812
|
loop do
|
|
693
|
-
message =
|
|
813
|
+
message = pop_message_with_watchdog
|
|
814
|
+
unless warned_silence
|
|
815
|
+
silence = Process.clock_gettime(Process::CLOCK_MONOTONIC) - @last_message_monotonic
|
|
816
|
+
if silence >= 900
|
|
817
|
+
warn(
|
|
818
|
+
"turbo_tests2: workers have been silent for #{silence.to_i}s. " \
|
|
819
|
+
"In turbo_tests2 4.0 (and now when TURBO_TESTS2_WATCHDOG_SECONDS is set) a stall this long kills the " \
|
|
820
|
+
"remaining workers and fails the run. To adopt the kill now set TURBO_TESTS2_WATCHDOG_SECONDS=900 (or " \
|
|
821
|
+
"another value). To keep waiting indefinitely set TURBO_TESTS2_WATCHDOG_SECONDS=0; be aware that an " \
|
|
822
|
+
"outer process watcher (a CI job timeout, a container orchestrator, or a local supervisor) will then " \
|
|
823
|
+
"be what eventually ends the run, usually without a useful stall report. A single long test only " \
|
|
824
|
+
"trips this in serial lanes (TURBO_TESTS2_MAX_PROCESSES=1); in parallel lanes other workers keep " \
|
|
825
|
+
"messages flowing."
|
|
826
|
+
)
|
|
827
|
+
warned_silence = true
|
|
828
|
+
end
|
|
829
|
+
end
|
|
694
830
|
case message[:type]
|
|
695
831
|
when "example_passed"
|
|
696
832
|
example = FakeExample.from_obj(message[:example])
|
|
@@ -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
|
data/lib/turbo_tests/version.rb
CHANGED
|
@@ -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.
|
|
7
|
+
VERSION = "3.2.15"
|
|
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.
|
|
4
|
+
version: 3.2.15
|
|
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.
|
|
114
|
+
version: 3.1.11
|
|
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.
|
|
124
|
+
version: 3.1.11
|
|
125
125
|
- !ruby/object:Gem::Dependency
|
|
126
126
|
name: bundler-audit
|
|
127
127
|
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.
|
|
222
|
+
version: 2.0.24
|
|
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.
|
|
232
|
+
version: 2.0.24
|
|
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.
|
|
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.
|
|
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.
|
|
319
|
-
changelog_uri: https://github.com/galtzo-floss/turbo_tests2/blob/v3.2.
|
|
319
|
+
source_code_uri: https://github.com/galtzo-floss/turbo_tests2/tree/v3.2.15
|
|
320
|
+
changelog_uri: https://github.com/galtzo-floss/turbo_tests2/blob/v3.2.15/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.
|
|
322
|
+
documentation_uri: https://www.rubydoc.info/gems/turbo_tests2/3.2.15
|
|
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
|