turbo_tests2 3.2.16 → 4.0.0
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 +35 -1
- data/README.md +66 -11
- data/lib/turbo_tests/cli.rb +17 -8
- data/lib/turbo_tests/reporter.rb +1 -1
- data/lib/turbo_tests/runner.rb +112 -77
- data/lib/turbo_tests/runtime_logger.rb +77 -0
- data/lib/turbo_tests/version.rb +1 -1
- data/lib/turbo_tests.rb +2 -2
- data.tar.gz.sig +0 -0
- metadata +24 -25
- 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: 7e14a9905e357ba16f5489987c5fc3638078587c35f232443698a02961cacd21
|
|
4
|
+
data.tar.gz: 9ea2481ca612f26d150e8514f6ccf12a758d49080a6ddd545a05f558a76dfe9a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 86f70203dd5f5ea3824dbc3ef6acae6b4f469ec4b2c7798e4e6e0c847316a51fb7883a61885df45acc09b01d26373416c381b5632b82e9c27c1dd50d5be03bf5
|
|
7
|
+
data.tar.gz: 6ae7d9b0015581bda3e25ee998593becab94daffc2806039e52dc22b73bceb57d3117b95be595380ee9c9bd4db427321aa47cc8971600890256d63e0891fa6f0
|
checksums.yaml.gz.sig
CHANGED
|
Binary file
|
data/CHANGELOG.md
CHANGED
|
@@ -30,6 +30,36 @@ Please file a bug if you notice a violation of semantic versioning.
|
|
|
30
30
|
|
|
31
31
|
### Security
|
|
32
32
|
|
|
33
|
+
## [4.0.0] - 2026-10-15
|
|
34
|
+
|
|
35
|
+
- TAG: [v4.0.0][4.0.0t]
|
|
36
|
+
- COVERAGE: 96.01% -- 1034/1077 lines in 12 files
|
|
37
|
+
- BRANCH COVERAGE: 82.42% -- 272/330 branches in 12 files
|
|
38
|
+
- 37.39% documented
|
|
39
|
+
|
|
40
|
+
### Changed
|
|
41
|
+
|
|
42
|
+
- Breaking: replace the parallel_tests runtime dependency with runtime_groups (~> 0.1, >= 0.1.2) for process counts, pid bookkeeping, options-file discovery, and runtime-weighted grouping; turbo_tests2 now owns a TurboTests::RuntimeLogger RSpec formatter and reimplements the Rails db:create bootstrap directly on Process.spawn
|
|
43
|
+
|
|
44
|
+
- Breaking: the stall watchdog is now ON by default (TURBO_TESTS2_WATCHDOG_SECONDS defaults to 900, killing silent workers and failing the run with a stall report); set TURBO_TESTS2_WATCHDOG_SECONDS=0 to restore the 3.x wait-forever behavior. The 3.x deprecation notice is replaced by a provider-neutral one-time warning when the watchdog is disabled and workers go silent 900s, naming the most common cause of total silence in a parallel lane (silently OOM-killed workers) and the fix (cap workers x per-worker memory)
|
|
45
|
+
|
|
46
|
+
### Fixed
|
|
47
|
+
|
|
48
|
+
- --allowed-missing now actually bounds the share of files allowed to miss the runtime log when grouping (parallel_tests silently ignored the option); runtime log is now written by the first-party TurboTests::RuntimeLogger through RuntimeGroups::RuntimeLog, and the watchdog's tracked-pid fallback reads the read-before-write pid ledger correctly
|
|
49
|
+
|
|
50
|
+
## [3.2.17] - 2026-10-14
|
|
51
|
+
|
|
52
|
+
- TAG: [v3.2.17][3.2.17t]
|
|
53
|
+
- COVERAGE: 95.94% -- 993/1035 lines in 11 files
|
|
54
|
+
- BRANCH COVERAGE: 83.86% -- 265/316 branches in 11 files
|
|
55
|
+
- 39.42% documented
|
|
56
|
+
|
|
57
|
+
### Changed
|
|
58
|
+
|
|
59
|
+
- README: document the spawn-based (fork-free) worker architecture that makes parallel workers engine-portable to JRuby and TruffleRuby, and add a Resource-Constrained CI (JVM Engines) section with the canonical 2-worker + capped-heap + raised-watchdog setup, matching the kettle-jem CI templates
|
|
60
|
+
|
|
61
|
+
- Stall diagnostics and in-code docs now explain silent worker death: the 900s silence warning and the watchdog stall path point at the OOM killer as the most common cause of total silence in a parallel lane (spawned workers, JVM engines defaulting to 1/4 RAM heap each), with the fix inline (TURBO_TESTS2_MAX_PROCESSES=2 + capped heap); start_subprocess and max_processes_from_environment document the spawn-based engine-portable architecture
|
|
62
|
+
|
|
33
63
|
## [3.2.16] - 2026-10-14
|
|
34
64
|
|
|
35
65
|
- TAG: [v3.2.16][3.2.16t]
|
|
@@ -761,7 +791,11 @@ Please file a bug if you notice a violation of semantic versioning.
|
|
|
761
791
|
|
|
762
792
|
- Initial release
|
|
763
793
|
|
|
764
|
-
[Unreleased]: https://github.com/galtzo-floss/turbo_tests2/compare/
|
|
794
|
+
[Unreleased]: https://github.com/galtzo-floss/turbo_tests2/compare/v4.0.0...HEAD
|
|
795
|
+
[4.0.0]: https://github.com/galtzo-floss/turbo_tests2/compare/v3.2.17...v4.0.0
|
|
796
|
+
[4.0.0t]: https://github.com/galtzo-floss/turbo_tests2/releases/tag/v4.0.0
|
|
797
|
+
[3.2.17]: https://github.com/galtzo-floss/turbo_tests2/compare/v3.2.16...v3.2.17
|
|
798
|
+
[3.2.17t]: https://github.com/galtzo-floss/turbo_tests2/releases/tag/v3.2.17
|
|
765
799
|
[3.2.16]: https://github.com/galtzo-floss/turbo_tests2/compare/v3.2.15...v3.2.16
|
|
766
800
|
[3.2.16t]: https://github.com/galtzo-floss/turbo_tests2/releases/tag/v3.2.16
|
|
767
801
|
[3.2.15]: https://github.com/galtzo-floss/turbo_tests2/compare/v3.2.14...v3.2.15
|
data/README.md
CHANGED
|
@@ -25,7 +25,7 @@ I've summarized my thoughts in [this blog post](https://dev.to/galtzo/hostile-ta
|
|
|
25
25
|
|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
|
26
26
|
| I ([@pboling](https://github.com/pboling)) discussed merging this project back into the original `turbo_tests` with [@ilyazub](https://github.com/ilyazub), and we might move in that direction, but it will take time. Since I need this now I'm releasing it as a separate gem/project. We'll be discussing potentially unifying the projects again soon. |
|
|
27
27
|
|
|
28
|
-
`turbo_tests2` is an RSpec parallel test runner
|
|
28
|
+
`turbo_tests2` is an RSpec parallel test runner. It uses [runtime_groups](https://github.com/galtzo-floss/runtime_groups) for process counts, pid bookkeeping, file grouping, and runtime balancing, then replaces interleaved worker output with incremental summarized RSpec reporting. Source code of this gem is based on [Discourse](https://github.com/discourse/discourse/blob/6b9784cf8a18636bce281a7e4d18e65a0cbc6290/lib/turbo_tests.rb) and [RubyGems](https://github.com/rubygems/rubygems/tree/390335ceb351668cd433bd5bb9823dd021f82533/bundler/tool) work in this area. As of v4.0.0 the `parallel_tests` dependency is gone; the CLI keeps compatibility with the `parallel_tests` grouping surface documented below.
|
|
29
29
|
The project namespace remains `TurboTests`; the published gem and executable are `turbo_tests2`.
|
|
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.
|
|
@@ -384,6 +384,60 @@ count at the number of discovered test groups.
|
|
|
384
384
|
Known coverage formatter chatter remains collapsed into the concise coverage
|
|
385
385
|
summary.
|
|
386
386
|
|
|
387
|
+
### Resource-Constrained CI (JVM Engines)
|
|
388
|
+
|
|
389
|
+
Worker parallelism is spawn-based, not fork-based: each worker is started as a
|
|
390
|
+
real subprocess (`Process.spawn` / `Open3.popen3`), never `Kernel#fork`. That
|
|
391
|
+
makes parallel workers engine-portable — MRI, JRuby, and TruffleRuby all run
|
|
392
|
+
the same spawn-based pool, and the default worker count is the machine's core
|
|
393
|
+
count (`Etc.nprocessors`) on every engine. There is no serial-only mode for
|
|
394
|
+
JVM engines unless you ask for one with `TURBO_TESTS2_MAX_PROCESSES=1`.
|
|
395
|
+
|
|
396
|
+
The cost of that portability on JVM engines is memory: each JRuby or
|
|
397
|
+
TruffleRuby worker is a full JVM subprocess, and JRuby defaults its heap to
|
|
398
|
+
one quarter of machine RAM. On a CI runner, `Etc.nprocessors` workers each
|
|
399
|
+
grabbing a quarter of RAM is a recipe for the kernel's OOM killer, which kills
|
|
400
|
+
workers **silently** — every worker stops messaging at once, and the stall
|
|
401
|
+
watchdog then reports total silence. This is a resource problem, not a
|
|
402
|
+
parallelism problem.
|
|
403
|
+
|
|
404
|
+
The fix is to run fewer, smaller workers. This is the canonical
|
|
405
|
+
resource-constrained setup, and the `kettle-jem` CI templates bake it into
|
|
406
|
+
every JRuby workflow they generate:
|
|
407
|
+
|
|
408
|
+
```bash
|
|
409
|
+
TURBO_TESTS2_MAX_PROCESSES=2 \
|
|
410
|
+
JRUBY_OPTS="-J-Xmx1280m" \
|
|
411
|
+
TURBO_TESTS2_WATCHDOG_SECONDS=1500 \
|
|
412
|
+
bundle exec turbo_tests2
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
Or in a GitHub Actions workflow:
|
|
416
|
+
|
|
417
|
+
```yaml
|
|
418
|
+
env:
|
|
419
|
+
# Fewer, smaller JVM workers on constrained runners.
|
|
420
|
+
TURBO_TESTS2_MAX_PROCESSES: 2
|
|
421
|
+
# Cap each worker JVM's heap (JRuby default: 1/4 of runner RAM).
|
|
422
|
+
JRUBY_OPTS: "-J-Xmx1280m"
|
|
423
|
+
# JVM lanes are slow; give the watchdog room without hitting the job timeout.
|
|
424
|
+
TURBO_TESTS2_WATCHDOG_SECONDS: 1500
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
Guidance:
|
|
428
|
+
|
|
429
|
+
- **Cap workers** (`TURBO_TESTS2_MAX_PROCESSES=2`) when each worker is a heavy
|
|
430
|
+
process: JVM engines, large test suites, or shared/undersized runners. Two
|
|
431
|
+
JVM workers still roughly halve wall-clock time versus one.
|
|
432
|
+
- **Cap the heap** (`JRUBY_OPTS="-J-Xmx1280m"`, or your engine's equivalent)
|
|
433
|
+
so total memory is bounded regardless of runner size.
|
|
434
|
+
- **Raise the watchdog** (1500s) on slow engines: JIT warmup and heavy GC make
|
|
435
|
+
silence windows longer, and you want the watchdog report, not the provider
|
|
436
|
+
timeout, when something genuinely hangs.
|
|
437
|
+
- The same knobs apply to any engine where a worker process is expensive —
|
|
438
|
+
the principle is: bound `workers × per-worker memory` under the runner's
|
|
439
|
+
capacity.
|
|
440
|
+
|
|
387
441
|
### Stall watchdog
|
|
388
442
|
|
|
389
443
|
If the worker message queue goes silent — every worker alive but none
|
|
@@ -391,8 +445,9 @@ producing example/group messages — a run can otherwise hang until the CI
|
|
|
391
445
|
provider kills the job (GitHub Actions: 6 hours).
|
|
392
446
|
|
|
393
447
|
`TURBO_TESTS2_WATCHDOG_SECONDS=N` (integer, `0` = disabled) bounds that
|
|
394
|
-
silence. **Default is `
|
|
395
|
-
`
|
|
448
|
+
silence. **Default is `900` as of 4.0.** (3.x defaulted to `0` — disabled —
|
|
449
|
+
with a one-time deprecation notice; set `0` to keep the old wait-forever
|
|
450
|
+
behavior.)
|
|
396
451
|
|
|
397
452
|
When the watchdog fires it kills the remaining worker processes, prints the
|
|
398
453
|
unfinished groups and worker pids, and fails the run.
|
|
@@ -414,12 +469,12 @@ Notes:
|
|
|
414
469
|
limit; also set `timeout-minutes` on the CI job. Locally, an outer
|
|
415
470
|
supervisor (process watcher, container timeout) plays the same role.
|
|
416
471
|
- When the watchdog is disabled (`0`), a 900s silence prints a one-time
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
472
|
+
provider-neutral notice: with the watchdog off, whatever outer process
|
|
473
|
+
watcher exists — a CI job timeout, a container orchestrator, or a local
|
|
474
|
+
supervisor — is what will eventually end a hung run, usually without a
|
|
475
|
+
useful stall report. The notice also names the most common cause of total
|
|
476
|
+
silence in a parallel lane (silently OOM-killed workers) and the fix (cap
|
|
477
|
+
`workers x per-worker memory`).
|
|
423
478
|
|
|
424
479
|
`turbo_tests2` supports custom formatter such as Fuubar, but you might need to require it:
|
|
425
480
|
|
|
@@ -859,7 +914,7 @@ Thanks for RTFM. ☺️
|
|
|
859
914
|
[📌gitmoji]: https://gitmoji.dev
|
|
860
915
|
[📌gitmoji-img]: https://img.shields.io/badge/gitmoji_commits-%20%F0%9F%98%9C%20%F0%9F%98%8D-34495e.svg?style=flat-square
|
|
861
916
|
[🧮kloc]: https://www.youtube.com/watch?v=dQw4w9WgXcQ
|
|
862
|
-
[🧮kloc-img]: https://img.shields.io/badge/KLOC-1.
|
|
917
|
+
[🧮kloc-img]: https://img.shields.io/badge/KLOC-1.077-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue
|
|
863
918
|
[🔐security]: https://github.com/galtzo-floss/turbo_tests2/blob/main/SECURITY.md
|
|
864
919
|
[🔐security-img]: https://img.shields.io/badge/security-policy-259D6C.svg?style=flat
|
|
865
920
|
[📄copyright-notice-explainer]: https://opensource.stackexchange.com/questions/5778/why-do-licenses-such-as-the-mit-license-specify-a-single-year
|
|
@@ -885,7 +940,7 @@ Thanks for RTFM. ☺️
|
|
|
885
940
|
| Field | Value |
|
|
886
941
|
|---|---|
|
|
887
942
|
| Package | turbo_tests2 |
|
|
888
|
-
| Description | 🚀 turbo_tests2` is an RSpec parallel test runner
|
|
943
|
+
| Description | 🚀 turbo_tests2` is an RSpec parallel test runner. It uses runtime_groups for process counts, pid bookkeeping, file grouping, and runtime balancing, and replaces interleaved worker output with incremental summarized RSpec reporting. Source code of `turbo_test2` gem is based on Discourse and Rubygems work in this area (see README.md). |
|
|
889
944
|
| Homepage | https://github.com/galtzo-floss/turbo_tests2 |
|
|
890
945
|
| Source | https://github.com/galtzo-floss/turbo_tests2 |
|
|
891
946
|
| License | `MIT` |
|
data/lib/turbo_tests/cli.rb
CHANGED
|
@@ -243,6 +243,16 @@ module TurboTests
|
|
|
243
243
|
]
|
|
244
244
|
end
|
|
245
245
|
|
|
246
|
+
# Inclusive ceiling on the worker pool when neither --count nor --workers
|
|
247
|
+
# is passed. Explicit CLI counts always win. Two main uses:
|
|
248
|
+
#
|
|
249
|
+
# 1. Outer orchestrators (kettle-family) budgeting a machine-wide process
|
|
250
|
+
# pool across concurrent family jobs.
|
|
251
|
+
# 2. Resource-constrained CI, where each worker is an expensive process —
|
|
252
|
+
# JVM engines (JRuby, TruffleRuby) spawn a full JVM per worker, so cap
|
|
253
|
+
# the pool (typically 2) AND the per-worker heap (e.g. JRUBY_OPTS=
|
|
254
|
+
# "-J-Xmx1280m") to stay under runner RAM. See README
|
|
255
|
+
# "Resource-Constrained CI (JVM Engines)".
|
|
246
256
|
def max_processes_from_environment
|
|
247
257
|
value = ENV.fetch("TURBO_TESTS2_MAX_PROCESSES", "").strip
|
|
248
258
|
return if value.empty?
|
|
@@ -353,20 +363,19 @@ module TurboTests
|
|
|
353
363
|
exit(1)
|
|
354
364
|
end
|
|
355
365
|
|
|
356
|
-
processes =
|
|
357
|
-
|
|
358
|
-
(1..processes).map do |process_id|
|
|
366
|
+
processes = RuntimeGroups.process_count(count)
|
|
367
|
+
RuntimeGroups::PidFile.with do |pid_path|
|
|
368
|
+
pids = (1..processes).map do |process_id|
|
|
359
369
|
env = {
|
|
360
370
|
"TEST_ENV_NUMBER" => process_id.to_s,
|
|
361
371
|
"PARALLEL_TEST_GROUPS" => processes.to_s,
|
|
362
|
-
"PARALLEL_PID_FILE" =>
|
|
372
|
+
"PARALLEL_PID_FILE" => pid_path
|
|
363
373
|
}
|
|
364
374
|
Process.spawn(env, *args)
|
|
365
375
|
end
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
exit(statuses.all?(&:success?) ? 0 : 1)
|
|
376
|
+
statuses = pids.map { |pid| Process.wait2(pid).last }
|
|
377
|
+
statuses.all?(&:success?) ? 0 : 1
|
|
378
|
+
end.tap { |code| exit(code) }
|
|
370
379
|
rescue OptionParser::ParseError => e
|
|
371
380
|
warn(e.message)
|
|
372
381
|
warn(parser)
|
data/lib/turbo_tests/reporter.rb
CHANGED
data/lib/turbo_tests/runner.rb
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
require "json"
|
|
4
4
|
require "timeout"
|
|
5
5
|
require "fileutils"
|
|
6
|
-
require "
|
|
6
|
+
require "runtime_groups"
|
|
7
7
|
require "rspec/core"
|
|
8
8
|
require "shellwords"
|
|
9
9
|
require "tempfile"
|
|
@@ -30,14 +30,18 @@ module TurboTests
|
|
|
30
30
|
|
|
31
31
|
class << self
|
|
32
32
|
def create(count)
|
|
33
|
-
#
|
|
34
|
-
#
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
33
|
+
# Rails bootstrap: prepare the database once per worker, in parallel.
|
|
34
|
+
# Implemented directly with Process.spawn so the runner does not
|
|
35
|
+
# depend on parallel_tests for anything.
|
|
36
|
+
count = RuntimeGroups.process_count(count)
|
|
37
|
+
pids = (1..count).map do |process_id|
|
|
38
|
+
Process.spawn(
|
|
39
|
+
{"TEST_ENV_NUMBER" => process_id.to_s, "PARALLEL_TEST_GROUPS" => count.to_s},
|
|
40
|
+
"bundle", "exec", "rake", "db:create", "RAILS_ENV=#{ENV.fetch("RAILS_ENV", "test")}"
|
|
41
|
+
)
|
|
42
|
+
end
|
|
43
|
+
statuses = pids.map { |pid| Process.wait2(pid).last }
|
|
44
|
+
statuses.all?(&:success?) ? 0 : 1
|
|
41
45
|
end
|
|
42
46
|
|
|
43
47
|
def run(opts = {})
|
|
@@ -249,10 +253,11 @@ module TurboTests
|
|
|
249
253
|
@load_time = 0
|
|
250
254
|
@load_count = 0
|
|
251
255
|
@failure_count = 0
|
|
252
|
-
#
|
|
253
|
-
#
|
|
254
|
-
#
|
|
255
|
-
|
|
256
|
+
# 4.0 default: kill behavior on (900s of total worker silence). Set
|
|
257
|
+
# TURBO_TESTS2_WATCHDOG_SECONDS=0 to disable explicitly (a long-hang
|
|
258
|
+
# run then ends via whatever outer watcher exists). See README
|
|
259
|
+
# "Stall watchdog".
|
|
260
|
+
@watchdog_seconds = Float(ENV.fetch("TURBO_TESTS2_WATCHDOG_SECONDS", "900"))
|
|
256
261
|
@watchdog_warned = false
|
|
257
262
|
@last_message_monotonic = nil
|
|
258
263
|
|
|
@@ -279,10 +284,9 @@ module TurboTests
|
|
|
279
284
|
|
|
280
285
|
def run
|
|
281
286
|
parallel_tests_options = @parallel_options.reject { |key, _value| key == :only_group }
|
|
282
|
-
tests_with_size = ParallelTests::RSpec::Runner.tests_with_size(@files, parallel_tests_options.merge(quiet: true))
|
|
283
287
|
@num_processes = [
|
|
284
|
-
|
|
285
|
-
|
|
288
|
+
RuntimeGroups.process_count(@count),
|
|
289
|
+
@files.size
|
|
286
290
|
].min
|
|
287
291
|
|
|
288
292
|
if @num_processes.zero?
|
|
@@ -292,10 +296,13 @@ module TurboTests
|
|
|
292
296
|
end
|
|
293
297
|
|
|
294
298
|
tests_in_groups =
|
|
295
|
-
|
|
299
|
+
RuntimeGroups::Grouping.in_groups(
|
|
296
300
|
@files,
|
|
297
301
|
@num_processes,
|
|
298
|
-
|
|
302
|
+
group_by: parallel_tests_options[:group_by] || :runtime,
|
|
303
|
+
runtime_log: parallel_tests_options[:runtime_log],
|
|
304
|
+
allowed_missing: (parallel_tests_options[:allowed_missing_percent] || 100).to_f / 100,
|
|
305
|
+
unknown_runtime: parallel_tests_options[:unknown_runtime]
|
|
299
306
|
)
|
|
300
307
|
tests_in_groups = selected_groups(tests_in_groups) if @parallel_options[:only_group]
|
|
301
308
|
@tests_in_groups = tests_in_groups
|
|
@@ -304,60 +311,72 @@ module TurboTests
|
|
|
304
311
|
record_runtime: @record_runtime
|
|
305
312
|
}
|
|
306
313
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
314
|
+
pid_file_was_set = ENV.key?("PARALLEL_PID_FILE")
|
|
315
|
+
pid_file_before = ENV["PARALLEL_PID_FILE"]
|
|
316
|
+
begin
|
|
317
|
+
RuntimeGroups::PidFile.with do |pid_path|
|
|
318
|
+
ENV["PARALLEL_PID_FILE"] = pid_path
|
|
319
|
+
|
|
320
|
+
exit_status = nil
|
|
321
|
+
report_coverage = false
|
|
322
|
+
|
|
323
|
+
@reporter.report(tests_in_groups) do |_reporter|
|
|
324
|
+
old_signal = Signal.trap(:INT) { handle_interrupt }
|
|
325
|
+
|
|
326
|
+
@wait_threads = tests_in_groups.map.with_index do |tests, process_id|
|
|
327
|
+
start_regular_subprocess(tests, process_id + 1, **subprocess_opts)
|
|
328
|
+
end.compact
|
|
329
|
+
@interrupt_handled = false
|
|
330
|
+
|
|
331
|
+
handle_messages
|
|
332
|
+
|
|
333
|
+
if @watchdog_fired
|
|
334
|
+
# Workers were SIGKILLed by the watchdog; their reader threads end
|
|
335
|
+
# at EOF and waiters return as the OS reaps them — but if a kill
|
|
336
|
+
# failed (permissions, zombie, pid reuse) an unbounded join here
|
|
337
|
+
# would hang the run anyway, defeating the watchdog. Bound every
|
|
338
|
+
# join and treat any worker that still has not exited as failed.
|
|
339
|
+
@threads.each do |thread|
|
|
340
|
+
thread.join(FORCE_STOP_JOIN)
|
|
341
|
+
thread.kill if thread.alive?
|
|
342
|
+
end
|
|
343
|
+
@wait_threads.each do |wait_thr|
|
|
344
|
+
wait_thr.join(FORCE_STOP_JOIN)
|
|
345
|
+
end
|
|
346
|
+
flush_worker_output unless stream_worker_output?
|
|
347
|
+
exit_status = 1
|
|
348
|
+
else
|
|
349
|
+
@threads.each(&:join)
|
|
338
350
|
|
|
339
|
-
|
|
351
|
+
report_workers_without_results
|
|
340
352
|
|
|
341
|
-
|
|
353
|
+
report_failed_group(tests_in_groups) if @print_failed_group
|
|
342
354
|
|
|
343
|
-
|
|
355
|
+
Signal.trap(:INT, old_signal)
|
|
344
356
|
|
|
345
|
-
|
|
357
|
+
statuses = @wait_threads.map(&:value)
|
|
346
358
|
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
359
|
+
if @reporter.failed_examples.empty? && statuses.all?(&:success?)
|
|
360
|
+
flush_successful_worker_output
|
|
361
|
+
report_coverage = true
|
|
362
|
+
exit_status = 0
|
|
363
|
+
else
|
|
364
|
+
flush_worker_output unless stream_worker_output?
|
|
365
|
+
# From https://github.com/galtzo-floss/turbo_tests2/pull/20/
|
|
366
|
+
exit_status = statuses.map(&:exitstatus).max
|
|
367
|
+
end
|
|
355
368
|
end
|
|
356
369
|
end
|
|
357
|
-
end
|
|
358
370
|
|
|
359
|
-
|
|
360
|
-
|
|
371
|
+
flush_coverage_summary if report_coverage
|
|
372
|
+
exit_status
|
|
373
|
+
end
|
|
374
|
+
ensure
|
|
375
|
+
if pid_file_was_set
|
|
376
|
+
ENV["PARALLEL_PID_FILE"] = pid_file_before
|
|
377
|
+
else
|
|
378
|
+
ENV.delete("PARALLEL_PID_FILE")
|
|
379
|
+
end
|
|
361
380
|
end
|
|
362
381
|
end
|
|
363
382
|
|
|
@@ -408,6 +427,14 @@ module TurboTests
|
|
|
408
427
|
)
|
|
409
428
|
end
|
|
410
429
|
|
|
430
|
+
# Spawns one worker as a real OS subprocess via Open3.popen3 — never
|
|
431
|
+
# Kernel#fork. This is what makes the worker pool engine-portable: MRI,
|
|
432
|
+
# JRuby, and TruffleRuby all spawn subprocesses the same way, and the
|
|
433
|
+
# default pool size is Etc.nprocessors on every engine. Remember each
|
|
434
|
+
# worker is a full interpreter/VM process: on JVM engines that is a whole
|
|
435
|
+
# JVM with its own heap, so bound the pool (TURBO_TESTS2_MAX_PROCESSES)
|
|
436
|
+
# and the per-worker heap (e.g. JRUBY_OPTS) on memory-constrained
|
|
437
|
+
# runners — see README "Resource-Constrained CI (JVM Engines)".
|
|
411
438
|
def start_subprocess(env, extra_args, tests, process_id, record_runtime:)
|
|
412
439
|
if tests.empty?
|
|
413
440
|
@messages << {
|
|
@@ -439,7 +466,7 @@ module TurboTests
|
|
|
439
466
|
FileUtils.mkdir_p(File.dirname(@runtime_log))
|
|
440
467
|
[
|
|
441
468
|
"--format",
|
|
442
|
-
"
|
|
469
|
+
"TurboTests::RuntimeLogger",
|
|
443
470
|
"--out",
|
|
444
471
|
@runtime_log
|
|
445
472
|
]
|
|
@@ -459,7 +486,7 @@ module TurboTests
|
|
|
459
486
|
[]
|
|
460
487
|
end
|
|
461
488
|
|
|
462
|
-
spec_opts = self.class.worker_spec_opts(
|
|
489
|
+
spec_opts = self.class.worker_spec_opts(RuntimeGroups::Options.spec_opts)
|
|
463
490
|
|
|
464
491
|
command = [
|
|
465
492
|
*command_name,
|
|
@@ -548,11 +575,11 @@ module TurboTests
|
|
|
548
575
|
end
|
|
549
576
|
|
|
550
577
|
def track_parallel_pid(pid, pid_file_path = parallel_pid_file_path)
|
|
551
|
-
|
|
578
|
+
RuntimeGroups::Pids.new(pid_file_path).add(pid) if pid && pid_file_path
|
|
552
579
|
end
|
|
553
580
|
|
|
554
581
|
def untrack_parallel_pid(pid, pid_file_path = parallel_pid_file_path)
|
|
555
|
-
|
|
582
|
+
RuntimeGroups::Pids.new(pid_file_path).delete(pid) if pid && pid_file_path
|
|
556
583
|
end
|
|
557
584
|
|
|
558
585
|
def start_copy_thread(src, process_id, stream)
|
|
@@ -756,6 +783,14 @@ module TurboTests
|
|
|
756
783
|
# on the queue forever — CI jobs hung for hours with no diagnostics.
|
|
757
784
|
# On a stall we kill the surviving worker pids, report which groups
|
|
758
785
|
# never finished, and fail the run instead.
|
|
786
|
+
#
|
|
787
|
+
# Note the OOM killer is the most common silent worker death on CI: the
|
|
788
|
+
# kernel kills the process with no stderr, no exit message, and no
|
|
789
|
+
# partial output. Workers are spawn-based real processes (never forked),
|
|
790
|
+
# so on JVM engines (JRuby, TruffleRuby) each one is a full JVM whose
|
|
791
|
+
# default heap can be 1/4 of machine RAM — a core-count pool of those
|
|
792
|
+
# exhausts a CI runner. Bound `workers x per-worker memory` on
|
|
793
|
+
# constrained runners (see README "Resource-Constrained CI").
|
|
759
794
|
def pop_message_with_watchdog
|
|
760
795
|
# 0 (the default) disables the watchdog: block indefinitely, exactly the
|
|
761
796
|
# pre-watchdog behavior.
|
|
@@ -805,10 +840,10 @@ module TurboTests
|
|
|
805
840
|
end
|
|
806
841
|
|
|
807
842
|
def tracked_pids
|
|
808
|
-
path =
|
|
843
|
+
path = parallel_pid_file_path
|
|
809
844
|
return [] unless path && File.file?(path)
|
|
810
845
|
|
|
811
|
-
|
|
846
|
+
RuntimeGroups::Pids.new(path).to_a
|
|
812
847
|
rescue
|
|
813
848
|
[]
|
|
814
849
|
end
|
|
@@ -850,13 +885,13 @@ module TurboTests
|
|
|
850
885
|
if silence >= 900
|
|
851
886
|
warn(
|
|
852
887
|
"turbo_tests2: workers have been silent for #{silence.to_i}s. " \
|
|
853
|
-
"
|
|
854
|
-
"
|
|
855
|
-
"
|
|
856
|
-
"
|
|
857
|
-
"
|
|
858
|
-
"
|
|
859
|
-
"
|
|
888
|
+
"The stall watchdog is disabled (TURBO_TESTS2_WATCHDOG_SECONDS=0), so this run will keep waiting " \
|
|
889
|
+
"until some outer watcher ends it — a CI job timeout, a container orchestrator, or a local " \
|
|
890
|
+
"supervisor — usually without a useful stall report. Total silence in a PARALLEL lane usually means " \
|
|
891
|
+
"workers died rather than slowed down — the most common cause is the kernel OOM killer (each worker " \
|
|
892
|
+
"is a real spawned process; on JVM engines each is a full JVM whose default heap is 1/4 of RAM). " \
|
|
893
|
+
"Cap workers x per-worker memory: TURBO_TESTS2_MAX_PROCESSES=2 with a capped heap (e.g. " \
|
|
894
|
+
"JRUBY_OPTS=\"-J-Xmx1280m\"), or re-enable the watchdog to convert hangs into fast failures."
|
|
860
895
|
)
|
|
861
896
|
warned_silence = true
|
|
862
897
|
end
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rspec/core"
|
|
4
|
+
require "runtime_groups"
|
|
5
|
+
|
|
6
|
+
# RSpec formatter that records per-file runtimes into a runtime log
|
|
7
|
+
# (TurboGroups path:seconds format, read back by RuntimeGroups::Grouping).
|
|
8
|
+
# Semantics retained from parallel_tests' RuntimeLogger:
|
|
9
|
+
# - only records when running in parallel (TEST_ENV_NUMBER is set)
|
|
10
|
+
# - accumulates time across nested example groups per top-level file
|
|
11
|
+
# - appends at dump time, sorted slowest-first
|
|
12
|
+
# Min-merging across re-runs happens in RuntimeGroups::RuntimeLog.read via
|
|
13
|
+
# last-write-wins per line, and worker groups are disjoint, so concurrent
|
|
14
|
+
# workers never write the same file.
|
|
15
|
+
class TurboTests::RuntimeLogger
|
|
16
|
+
RSpec::Core::Formatters.register self, :example_group_started, :example_group_finished, :start_dump
|
|
17
|
+
|
|
18
|
+
def initialize(output)
|
|
19
|
+
@output = output
|
|
20
|
+
@example_times = Hash.new(0)
|
|
21
|
+
@group_nesting = 0
|
|
22
|
+
@time = 0.0
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def example_group_started(_notification)
|
|
26
|
+
@group_nesting += 1
|
|
27
|
+
@time = monotonic if @group_nesting == 1
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def example_group_finished(notification)
|
|
31
|
+
@group_nesting -= 1
|
|
32
|
+
if @group_nesting.zero?
|
|
33
|
+
@example_times[notification.group.file_path] += monotonic - @time
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def seed(*)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def dump_summary(*)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def dump_failures(*)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def dump_pending(*)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def message(*)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def close(*)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def start_dump(*)
|
|
56
|
+
return unless ENV["TEST_ENV_NUMBER"] # only record when running in parallel
|
|
57
|
+
|
|
58
|
+
log_path = output_path
|
|
59
|
+
return unless log_path
|
|
60
|
+
|
|
61
|
+
log = RuntimeGroups::RuntimeLog.new(log_path)
|
|
62
|
+
@example_times.sort_by(&:last).reverse_each do |file, time|
|
|
63
|
+
relative_path = file.sub(%r{^#{Regexp.escape(Dir.pwd)}/}, "").sub(%r{^\./}, "")
|
|
64
|
+
log.log(relative_path, [time, 0].max)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
private
|
|
69
|
+
|
|
70
|
+
def monotonic
|
|
71
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def output_path
|
|
75
|
+
@output.respond_to?(:path) ? @output.path : @output.to_s
|
|
76
|
+
end
|
|
77
|
+
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 = "
|
|
7
|
+
VERSION = "4.0.0"
|
|
8
8
|
end
|
|
9
9
|
# Current gem version exposed at the traditional constant location.
|
|
10
10
|
VERSION = Version::VERSION # Traditional Constant Location
|
data/lib/turbo_tests.rb
CHANGED
|
@@ -7,10 +7,10 @@ require "json"
|
|
|
7
7
|
|
|
8
8
|
require "rspec"
|
|
9
9
|
|
|
10
|
-
require "
|
|
11
|
-
require "parallel_tests/rspec/runner"
|
|
10
|
+
require "runtime_groups"
|
|
12
11
|
|
|
13
12
|
require "turbo_tests/reporter"
|
|
13
|
+
require "turbo_tests/runtime_logger"
|
|
14
14
|
require "turbo_tests/runner"
|
|
15
15
|
require "turbo_tests/json_rows_formatter"
|
|
16
16
|
|
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:
|
|
4
|
+
version: 4.0.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Illia
|
|
@@ -49,39 +49,39 @@ cert_chain:
|
|
|
49
49
|
date: 1980-01-02 00:00:00.000000000 Z
|
|
50
50
|
dependencies:
|
|
51
51
|
- !ruby/object:Gem::Dependency
|
|
52
|
-
name:
|
|
52
|
+
name: rspec
|
|
53
53
|
requirement: !ruby/object:Gem::Requirement
|
|
54
54
|
requirements:
|
|
55
55
|
- - ">="
|
|
56
56
|
- !ruby/object:Gem::Version
|
|
57
|
-
version: 3.
|
|
58
|
-
- - "<"
|
|
59
|
-
- !ruby/object:Gem::Version
|
|
60
|
-
version: '6'
|
|
57
|
+
version: '3.10'
|
|
61
58
|
type: :runtime
|
|
62
59
|
prerelease: false
|
|
63
60
|
version_requirements: !ruby/object:Gem::Requirement
|
|
64
61
|
requirements:
|
|
65
62
|
- - ">="
|
|
66
63
|
- !ruby/object:Gem::Version
|
|
67
|
-
version: 3.
|
|
68
|
-
- - "<"
|
|
69
|
-
- !ruby/object:Gem::Version
|
|
70
|
-
version: '6'
|
|
64
|
+
version: '3.10'
|
|
71
65
|
- !ruby/object:Gem::Dependency
|
|
72
|
-
name:
|
|
66
|
+
name: runtime_groups
|
|
73
67
|
requirement: !ruby/object:Gem::Requirement
|
|
74
68
|
requirements:
|
|
69
|
+
- - "~>"
|
|
70
|
+
- !ruby/object:Gem::Version
|
|
71
|
+
version: '0.1'
|
|
75
72
|
- - ">="
|
|
76
73
|
- !ruby/object:Gem::Version
|
|
77
|
-
version:
|
|
74
|
+
version: 0.1.3
|
|
78
75
|
type: :runtime
|
|
79
76
|
prerelease: false
|
|
80
77
|
version_requirements: !ruby/object:Gem::Requirement
|
|
81
78
|
requirements:
|
|
79
|
+
- - "~>"
|
|
80
|
+
- !ruby/object:Gem::Version
|
|
81
|
+
version: '0.1'
|
|
82
82
|
- - ">="
|
|
83
83
|
- !ruby/object:Gem::Version
|
|
84
|
-
version:
|
|
84
|
+
version: 0.1.3
|
|
85
85
|
- !ruby/object:Gem::Dependency
|
|
86
86
|
name: version_gem
|
|
87
87
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -284,11 +284,10 @@ dependencies:
|
|
|
284
284
|
- - ">="
|
|
285
285
|
- !ruby/object:Gem::Version
|
|
286
286
|
version: 2.0.15
|
|
287
|
-
description: "\U0001F680 turbo_tests2` is an RSpec parallel test runner
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
area (see README.md)."
|
|
287
|
+
description: "\U0001F680 turbo_tests2` is an RSpec parallel test runner. It uses runtime_groups
|
|
288
|
+
for process counts, pid bookkeeping, file grouping, and runtime balancing, and replaces
|
|
289
|
+
interleaved worker output with incremental summarized RSpec reporting. Source code
|
|
290
|
+
of `turbo_test2` gem is based on Discourse and Rubygems work in this area (see README.md)."
|
|
292
291
|
email:
|
|
293
292
|
- floss@galtzo.com
|
|
294
293
|
executables:
|
|
@@ -305,6 +304,7 @@ files:
|
|
|
305
304
|
- lib/turbo_tests/json_rows_formatter.rb
|
|
306
305
|
- lib/turbo_tests/reporter.rb
|
|
307
306
|
- lib/turbo_tests/runner.rb
|
|
307
|
+
- lib/turbo_tests/runtime_logger.rb
|
|
308
308
|
- lib/turbo_tests/shim.rb
|
|
309
309
|
- lib/turbo_tests/utils/paths.rb
|
|
310
310
|
- lib/turbo_tests/version.rb
|
|
@@ -316,10 +316,10 @@ licenses:
|
|
|
316
316
|
- MIT
|
|
317
317
|
metadata:
|
|
318
318
|
homepage_uri: https://turbo-tests2.galtzo.com
|
|
319
|
-
source_code_uri: https://github.com/galtzo-floss/turbo_tests2/tree/
|
|
320
|
-
changelog_uri: https://github.com/galtzo-floss/turbo_tests2/blob/
|
|
319
|
+
source_code_uri: https://github.com/galtzo-floss/turbo_tests2/tree/v4.0.0
|
|
320
|
+
changelog_uri: https://github.com/galtzo-floss/turbo_tests2/blob/v4.0.0/CHANGELOG.md
|
|
321
321
|
bug_tracker_uri: https://github.com/galtzo-floss/turbo_tests2/issues
|
|
322
|
-
documentation_uri: https://www.rubydoc.info/gems/turbo_tests2/
|
|
322
|
+
documentation_uri: https://www.rubydoc.info/gems/turbo_tests2/4.0.0
|
|
323
323
|
funding_uri: https://github.com/sponsors/pboling
|
|
324
324
|
wiki_uri: https://github.com/galtzo-floss/turbo_tests2/wiki
|
|
325
325
|
news_uri: https://www.railsbling.com/tags/turbo_tests2
|
|
@@ -328,8 +328,8 @@ metadata:
|
|
|
328
328
|
rubygems_mfa_required: 'true'
|
|
329
329
|
rdoc_options:
|
|
330
330
|
- "--title"
|
|
331
|
-
- "turbo_tests2 - \U0001F680 RSpec parallel test runner
|
|
332
|
-
|
|
331
|
+
- "turbo_tests2 - \U0001F680 RSpec parallel test runner with incremental summarized
|
|
332
|
+
output"
|
|
333
333
|
- "--main"
|
|
334
334
|
- README.md
|
|
335
335
|
- "--exclude"
|
|
@@ -352,6 +352,5 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
352
352
|
requirements: []
|
|
353
353
|
rubygems_version: 4.0.20
|
|
354
354
|
specification_version: 4
|
|
355
|
-
summary: "\U0001F680 RSpec parallel test runner
|
|
356
|
-
summarized output"
|
|
355
|
+
summary: "\U0001F680 RSpec parallel test runner with incremental summarized output"
|
|
357
356
|
test_files: []
|
metadata.gz.sig
CHANGED
|
Binary file
|