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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0ba760f9553f5ac9180f943b7b6ba4292f8e3ae332c2ee8f93d704fb6df39fd8
4
- data.tar.gz: 308a01984a1da21ef5a1005cd5f333404b8132b6b8b12717f9fcf1e6b213421d
3
+ metadata.gz: 7e14a9905e357ba16f5489987c5fc3638078587c35f232443698a02961cacd21
4
+ data.tar.gz: 9ea2481ca612f26d150e8514f6ccf12a758d49080a6ddd545a05f558a76dfe9a
5
5
  SHA512:
6
- metadata.gz: d15f6b3dc4601510f2fc2785b722b966fe543352a2408f084160e9b0d064fcf020cb234dbcc22b7c03baca495d1a7c87a13d41e24e74084a4bc43b6fe4aec5ac
7
- data.tar.gz: be13e868955aacabb64993bab93050538def66c7c341406b082878d9f06d8c3098c701052f40c1e1a7c36eed026de5bc1c6930c7db42ab2e109a8240944c5d48
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/v3.2.16...HEAD
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 built on [grosser/parallel_tests](https://github.com/grosser/parallel_tests). It uses `parallel_tests` for process counts, 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.
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 `0` (disabled) in 3.x.** In 4.0 the default becomes
395
- `900` and the kill behavior is always on.
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
- 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.
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.035-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue
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 built on `parallel_tests`. It uses `parallel_tests` for process counts, file grouping, and runtime balancing, then 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). |
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` |
@@ -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 = ParallelTests.determine_number_of_processes(count)
357
- pids = ParallelTests.with_pid_file do
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" => ParallelTests.pid_file_path
372
+ "PARALLEL_PID_FILE" => pid_path
363
373
  }
364
374
  Process.spawn(env, *args)
365
375
  end
366
- end
367
- statuses = pids.map { |pid| Process.wait2(pid).last }
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)
@@ -80,7 +80,7 @@ module TurboTests
80
80
  end
81
81
 
82
82
  def report_number_of_tests(groups)
83
- name = ParallelTests::RSpec::Runner.test_file_name
83
+ name = "spec"
84
84
 
85
85
  num_processes = groups.size
86
86
  num_tests = groups.map(&:size).sum
@@ -3,7 +3,7 @@
3
3
  require "json"
4
4
  require "timeout"
5
5
  require "fileutils"
6
- require "parallel_tests/rspec/runner"
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
- # We are unable to load parallel tests' tasks in the normal way (top of file)
34
- # because it requires that the Rails.application instance already be configured
35
- require "parallel_tests/tasks"
36
-
37
- ENV["PARALLEL_TEST_FIRST_IS_1"] = "true"
38
- command = ["bundle", "exec", "rake", "db:create", "RAILS_ENV=#{ParallelTests::Tasks.rails_env}"]
39
- args = {count: count.to_s}
40
- ParallelTests::Tasks.run_in_parallel(command, args)
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
- # 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
+ # 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
- ParallelTests.determine_number_of_processes(@count),
285
- tests_with_size.size
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
- ParallelTests::RSpec::Runner.tests_in_groups(
299
+ RuntimeGroups::Grouping.in_groups(
296
300
  @files,
297
301
  @num_processes,
298
- **parallel_tests_options
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
- ParallelTests.with_pid_file do
308
- exit_status = nil
309
- report_coverage = false
310
-
311
- @reporter.report(tests_in_groups) do |_reporter|
312
- old_signal = Signal.trap(:INT) { handle_interrupt }
313
-
314
- @wait_threads = tests_in_groups.map.with_index do |tests, process_id|
315
- start_regular_subprocess(tests, process_id + 1, **subprocess_opts)
316
- end.compact
317
- @interrupt_handled = false
318
-
319
- handle_messages
320
-
321
- if @watchdog_fired
322
- # Workers were SIGKILLed by the watchdog; their reader threads end
323
- # at EOF and waiters return as the OS reaps them — but if a kill
324
- # failed (permissions, zombie, pid reuse) an unbounded join here
325
- # would hang the run anyway, defeating the watchdog. Bound every
326
- # join and treat any worker that still has not exited as failed.
327
- @threads.each do |thread|
328
- thread.join(FORCE_STOP_JOIN)
329
- thread.kill if thread.alive?
330
- end
331
- @wait_threads.each do |wait_thr|
332
- wait_thr.join(FORCE_STOP_JOIN)
333
- end
334
- flush_worker_output unless stream_worker_output?
335
- exit_status = 1
336
- else
337
- @threads.each(&:join)
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
- report_workers_without_results
351
+ report_workers_without_results
340
352
 
341
- report_failed_group(tests_in_groups) if @print_failed_group
353
+ report_failed_group(tests_in_groups) if @print_failed_group
342
354
 
343
- Signal.trap(:INT, old_signal)
355
+ Signal.trap(:INT, old_signal)
344
356
 
345
- statuses = @wait_threads.map(&:value)
357
+ statuses = @wait_threads.map(&:value)
346
358
 
347
- if @reporter.failed_examples.empty? && statuses.all?(&:success?)
348
- flush_successful_worker_output
349
- report_coverage = true
350
- exit_status = 0
351
- else
352
- flush_worker_output unless stream_worker_output?
353
- # From https://github.com/galtzo-floss/turbo_tests2/pull/20/
354
- exit_status = statuses.map(&:exitstatus).max
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
- flush_coverage_summary if report_coverage
360
- exit_status
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
- "ParallelTests::RSpec::RuntimeLogger",
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(ParallelTests::RSpec::Runner.send(: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
- ParallelTests::Pids.new(pid_file_path).add(pid) if pid && pid_file_path
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
- ParallelTests::Pids.new(pid_file_path).delete(pid) if pid && pid_file_path
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 = ParallelTests.pid_file_path
843
+ path = parallel_pid_file_path
809
844
  return [] unless path && File.file?(path)
810
845
 
811
- File.read(path).lines.map { |l| l.strip.to_i }.reject(&:zero?)
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
- "In turbo_tests2 4.0 (and now when TURBO_TESTS2_WATCHDOG_SECONDS is set) a stall this long kills the " \
854
- "remaining workers and fails the run. To adopt the kill now set TURBO_TESTS2_WATCHDOG_SECONDS=900 (or " \
855
- "another value). To keep waiting indefinitely set TURBO_TESTS2_WATCHDOG_SECONDS=0; be aware that an " \
856
- "outer process watcher (a CI job timeout, a container orchestrator, or a local supervisor) will then " \
857
- "be what eventually ends the run, usually without a useful stall report. A single long test only " \
858
- "trips this in serial lanes (TURBO_TESTS2_MAX_PROCESSES=1); in parallel lanes other workers keep " \
859
- "messages flowing."
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
@@ -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.16"
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 "parallel_tests"
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: 3.2.16
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: parallel_tests
52
+ name: rspec
53
53
  requirement: !ruby/object:Gem::Requirement
54
54
  requirements:
55
55
  - - ">="
56
56
  - !ruby/object:Gem::Version
57
- version: 3.3.0
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.3.0
68
- - - "<"
69
- - !ruby/object:Gem::Version
70
- version: '6'
64
+ version: '3.10'
71
65
  - !ruby/object:Gem::Dependency
72
- name: rspec
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: '3.10'
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: '3.10'
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 built on `parallel_tests`.
288
- It uses `parallel_tests` for process counts, file grouping, and runtime balancing,
289
- then replaces interleaved worker output with incremental summarized RSpec reporting.
290
- Source code of `turbo_test2` gem is based on Discourse and Rubygems work in this
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/v3.2.16
320
- changelog_uri: https://github.com/galtzo-floss/turbo_tests2/blob/v3.2.16/CHANGELOG.md
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/3.2.16
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 built on `parallel_tests`,
332
- with incremental summarized output"
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 built on `parallel_tests`, with incremental
356
- summarized output"
355
+ summary: "\U0001F680 RSpec parallel test runner with incremental summarized output"
357
356
  test_files: []
metadata.gz.sig CHANGED
Binary file