Skip to content

feat(zest): support parallel test execution#84

Merged
16bit-ykiko merged 16 commits intomainfrom
feat/parallel-test-runner
Mar 30, 2026
Merged

feat(zest): support parallel test execution#84
16bit-ykiko merged 16 commits intomainfrom
feat/parallel-test-runner

Conversation

@16bit-ykiko
Copy link
Copy Markdown
Member

@16bit-ykiko 16bit-ykiko commented Mar 29, 2026

Summary

  • Add bool parallel to RunnerOptions and --parallel CLI flag
  • When enabled, all test cases run concurrently via std::jthread, results printed after completion
  • Make deferred_cancel_await in when_tests thread-safe via thread_local

Test plan

  • Sequential: 677 tests pass
  • Parallel: most tests pass; some TCP/CLI tests have occasional failures due to resource contention (port binding, etc.) — expected for opt-in parallel mode

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Parallel test execution via CLI options and per-test serial flag to opt out.
    • API to query the local port of a listening TCP acceptor.
  • Improvements

    • Safer concurrent test execution with per-thread isolation and reordered result aggregation.
    • Per-thread event loop for file ops and serialized process spawning to avoid races.
  • Chores

    • CI updated to run tests in parallel mode.

Add `bool parallel` to RunnerOptions and `--parallel` CLI flag.
When enabled, all test cases run concurrently via std::jthread and
results are printed after completion. Also make deferred_cancel_await
in when_tests thread-safe via thread_local.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 29, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds parallel test execution to Zest with per-test serial attributes, merges suite/case attributes, introduces thread-local state in tests and runtime helpers, adds a tcp acceptor local-port API, and serializes libuv process spawning.

Changes

Cohort / File(s) Summary
Zest Test Runner Parallelization
include/eventide/zest/run.h, src/zest/runner.cpp
Added RunnerOptions.parallel and parallel_workers CLI flags; refactored test collection and execution to partition tests by TestAttrs.serial, run parallel-safe tests via worker pool with atomic task indexing, then run serial tests; results are computed before printing/aggregation.
Test Attribute System
include/eventide/zest/detail/registry.h, include/eventide/zest/detail/suite.h
Added bool serial to TestAttrs and constexpr merge_attrs() to merge suite- and case-level attributes (OR of flags), so registered test cases carry effective attrs.
Thread-Local State Isolation
tests/unit/async/when_tests.cpp, tests/unit/common/small_vector_tests.cpp, tests/unit/deco/runtime.cc, src/async/io/fs.cpp
Replaced several process-global static/inline static variables with thread_local equivalents (pending awaiter, lifetime counters, callback states, exception-injection counters, and per-thread sync libuv loop) to make shared state safe under parallel test execution.
TCP Local Port API
include/eventide/async/io/stream.h, src/async/io/acceptor.cpp
Added tcp::local_port(acceptor&) that validates the acceptor, calls uv_tcp_getsockname, maps errors, and returns the bound IPv4/IPv6 port or invalid_argument for unsupported families.
libuv Spawn Serialization
src/async/libuv.h
Changed uv::spawn from ALWAYS_INLINE to inline and added a process-wide static std::mutex with std::lock_guard to serialize ::uv_spawn calls and associated SIGCHLD registration.
Serial Test Case Annotations / Test Updates
tests/unit/async/cancellation_tests.cpp, tests/unit/async/process_tests.cpp, tests/unit/async/stream_tests.cpp
Marked several tests/suites .serial = true; refactored process_tests child readiness/termination to use stdout/stdin pipes instead of fixed delays.
CI: Run Tests in Parallel Mode
.github/workflows/cmake.yml
Added step to invoke built unit test binary with --parallel after ctest, with a console grouping line.

Sequence Diagram(s)

sequenceDiagram
    participant Runner as Zest Runner
    participant Collector as Test Collector
    participant Partitioner as Partitioner
    participant WorkerPool as Worker Pool
    participant SerialExec as Serial Executor
    participant Aggregator as Result Aggregator

    Runner->>Collector: collect runnable tests & active_suites
    Collector-->>Runner: runnable[]

    alt parallel mode
        Runner->>Partitioner: split runnable[] by attrs.serial
        Partitioner-->>Runner: parallel_tasks[], serial_tasks[]

        Runner->>WorkerPool: start workers (parallel_workers)
        loop workers fetch tasks
            WorkerPool->>WorkerPool: fetch next task (atomic index)
            WorkerPool->>WorkerPool: run test, measure duration
            WorkerPool-->>Runner: store result at task index
        end
        WorkerPool-->>Runner: all parallel results computed

        Runner->>SerialExec: run serial_tasks sequentially
        SerialExec-->>Runner: serial results
    else sequential mode
        loop for each test in runnable[]
            Runner->>SerialExec: run test sequentially
            SerialExec-->>Runner: result
        end
    end

    Runner->>Aggregator: print per-test output & update totals
    Aggregator-->>Runner: final summary
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰
Hopping through threads with a twitch in my nose,
Tests scurry in parallel where the spring clover grows.
Serial ones wait with a patient small hop,
Thread-local carrots keep races a stop.
Worker-burrows hum — a bunny-approved crop.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 9.76% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat(zest): support parallel test execution' directly and clearly summarizes the main change—adding parallel test execution support to the zest test runner.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/parallel-test-runner

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/zest/runner.cpp`:
- Around line 368-378: The current loop accumulates per-test durations into
summary.duration which yields CPU-time-like totals in parallel runs; detect
parallel execution (e.g., via options.parallel or the runner's parallel flag)
and, when parallel, replace the summed duration with a measured wall-clock
elapsed time taken for the whole run (measure steady_clock before starting tests
and after all threads join and assign that to summary.duration), while retaining
the per-test sum in a separate variable (e.g., cpu_time_sum) if you want to keep
both metrics; otherwise leave the sequential behavior unchanged (keep the
existing per-test summation in the loop) and update print_run_result/summary
only after choosing wall-clock vs summed duration.
- Around line 344-366: The current parallel branch spawns one std::jthread per
runnable item which can exhaust system resources; change it to use a bounded
worker pool (size = std::max(1u, std::thread::hardware_concurrency()) or a
configurable max) that pulls tasks from a queue/indices for runnable and writes
into results, and ensure worker threads are explicitly joined (or let the vector
go out of scope) instead of calling threads.clear(); update the code around
options.parallel, the creation of threads, and the place currently using
threads.emplace_back(...) and threads.clear(), keeping TestResult population
(results[i] = ...) and timing logic inside the worker task.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5c1aea95-3f5d-4822-811d-6fee6b5a596f

📥 Commits

Reviewing files that changed from the base of the PR and between 2aa17d2 and 9b9f9e2.

📒 Files selected for processing (3)
  • include/eventide/zest/run.h
  • src/zest/runner.cpp
  • tests/unit/async/when_tests.cpp

16bit-ykiko and others added 2 commits March 30, 2026 01:37
- Add tcp::local_port() to query bound port from acceptor, eliminating
  pick_free_port() TOCTOU race by binding to port 0 directly
- Convert global static state to thread_local in when_tests and deco
  runtime tests

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace N-thread-per-test with fixed-size thread pool (default:
hardware_concurrency) using atomic task counter. Add --parallel-workers
flag. Also fix small_vector test global state to thread_local.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/zest/runner.cpp`:
- Around line 354-388: The code currently spawns num_workers jthreads regardless
of how many tests exist; clamp the worker count to the amount of work by
computing an actual_worker_count = std::min(num_workers,
static_cast<unsigned>(runnable.size() ? runnable.size() : 1)); then use
actual_worker_count for pool.reserve(...) and the spawn loop (replace
for(unsigned w = 0; w < num_workers; ++w) with for(unsigned w = 0; w <
actual_worker_count; ++w)). Ensure you use the existing symbols num_workers,
runnable.size(), pool.reserve and the jthread spawn loop when making the change.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b8d07666-2294-4bf8-9165-0571224b6c3b

📥 Commits

Reviewing files that changed from the base of the PR and between 9b9f9e2 and a6c0345.

📒 Files selected for processing (7)
  • include/eventide/async/io/stream.h
  • include/eventide/zest/run.h
  • src/async/io/acceptor.cpp
  • src/zest/runner.cpp
  • tests/unit/async/stream_tests.cpp
  • tests/unit/common/small_vector_tests.cpp
  • tests/unit/deco/runtime.cc
✅ Files skipped from review due to trivial changes (1)
  • tests/unit/async/stream_tests.cpp
🚧 Files skipped from review as they are similar to previous changes (1)
  • include/eventide/zest/run.h

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (2)
src/zest/runner.cpp (2)

371-375: ⚠️ Potential issue | 🟡 Minor

Use elapsed wall time for the parallel summary.

record_result() always adds per-test durations, so summary.duration turns into a CPU-time-like sum once multiple workers run concurrently. The printed “total” can then exceed the actual elapsed runtime.

♻️ Proposed fix
-    auto record_result = [&](const TestResult& result) {
+    auto record_result = [&](const TestResult& result, bool accumulate_duration = true) {
         const bool failed = is_failure(result.state);
         print_run_result(result.display_name, failed, result.duration, options.only_failed_output);
-        summary.duration += result.duration;
+        if(accumulate_duration) {
+            summary.duration += result.duration;
+        }
         if(failed) {
             summary.failed += 1;
             summary.failed_tests.push_back(
                 FailedTest{result.display_name, result.path, result.line});
         }
@@
     if(options.parallel) {
+        using namespace std::chrono;
+        auto parallel_begin = steady_clock::now();
+
         // Partition: parallel-safe tests first, serial tests after.
         std::vector<std::size_t> parallel_indices;
         std::vector<std::size_t> serial_indices;
@@
         // Print all results in original order.
         for(const auto& result: results) {
-            record_result(result);
+            record_result(result, false);
         }
+        summary.duration = duration_cast<milliseconds>(steady_clock::now() - parallel_begin);
     } else {

Also applies to: 385-432

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/zest/runner.cpp` around lines 371 - 375, The summary currently
accumulates per-test durations inside the record_result lambda (reference:
record_result, summary.duration), which double-counts wall time when tests run
in parallel; instead record a single run start timestamp before workers start
and, when emitting the parallel summary, compute summary.duration as the elapsed
wall time (now - run_start) rather than summing result.duration; update all
places that currently add to summary.duration (including the block around
print_run_result and the later summary aggregation) to use this elapsed value so
the printed “total” reflects real elapsed time.

397-421: ⚠️ Potential issue | 🟠 Major

Clamp the pool size to the parallel batch.

--parallel-workers is still used as the reserve/spawn count even when parallel_indices is tiny or empty. A large value here can exhaust memory or OS threads before any test starts.

♻️ Proposed fix
-        const auto num_workers =
+        const auto configured_workers =
             std::max(1u,
                      options.parallel_workers ? options.parallel_workers
                                               : std::thread::hardware_concurrency());
+        const auto num_workers =
+            std::min<std::size_t>(configured_workers, parallel_indices.size());
 
         std::atomic<std::size_t> next_task{0};
@@
-        {
+        if(num_workers > 0) {
             std::vector<std::jthread> pool;
             pool.reserve(num_workers);
-            for(unsigned w = 0; w < num_workers; ++w) {
+            for(std::size_t w = 0; w < num_workers; ++w) {
                 pool.emplace_back(worker);
             }
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/zest/runner.cpp` around lines 397 - 421, The code currently uses
num_workers (computed from options.parallel_workers or hardware_concurrency)
directly when reserving and spawning the thread pool, which can create many
threads even when parallel_indices is small or empty; change the spawn count to
clamp to the work size by computing a worker_count = std::min(num_workers,
static_cast<unsigned>(parallel_indices.size())); then use worker_count for
pool.reserve(...) and the for loop that calls pool.emplace_back(worker) (replace
uses of num_workers in reserve/spawn with worker_count) so you never allocate
more threads than there are parallel tasks (and allow zero threads when
parallel_indices is empty).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/zest/runner.cpp`:
- Around line 1-10: This file relies on transitive headers for std::function and
std::max; add direct includes for <functional> (to cover the std::function usage
where callbacks/functional types are declared) and <algorithm> (to cover
std::max usage where maximum values are computed) so the symbols are always
available across toolchains and standard-library implementations.

---

Duplicate comments:
In `@src/zest/runner.cpp`:
- Around line 371-375: The summary currently accumulates per-test durations
inside the record_result lambda (reference: record_result, summary.duration),
which double-counts wall time when tests run in parallel; instead record a
single run start timestamp before workers start and, when emitting the parallel
summary, compute summary.duration as the elapsed wall time (now - run_start)
rather than summing result.duration; update all places that currently add to
summary.duration (including the block around print_run_result and the later
summary aggregation) to use this elapsed value so the printed “total” reflects
real elapsed time.
- Around line 397-421: The code currently uses num_workers (computed from
options.parallel_workers or hardware_concurrency) directly when reserving and
spawning the thread pool, which can create many threads even when
parallel_indices is small or empty; change the spawn count to clamp to the work
size by computing a worker_count = std::min(num_workers,
static_cast<unsigned>(parallel_indices.size())); then use worker_count for
pool.reserve(...) and the for loop that calls pool.emplace_back(worker) (replace
uses of num_workers in reserve/spawn with worker_count) so you never allocate
more threads than there are parallel tasks (and allow zero threads when
parallel_indices is empty).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 6fcd7e5c-b91b-4f2e-a2a6-54ae8cde3307

📥 Commits

Reviewing files that changed from the base of the PR and between a6c0345 and 27fca3d.

📒 Files selected for processing (10)
  • .github/workflows/cmake.yml
  • include/eventide/zest/detail/registry.h
  • include/eventide/zest/detail/suite.h
  • include/eventide/zest/macro.h
  • src/async/io/fs.cpp
  • src/async/libuv.h
  • src/zest/runner.cpp
  • tests/unit/async/cancellation_tests.cpp
  • tests/unit/async/process_tests.cpp
  • tests/unit/deco/runtime.cc
✅ Files skipped from review due to trivial changes (1)
  • include/eventide/zest/detail/registry.h

- Add `serial` attribute to zest test framework (suite-level and case-level)
- Fix thread-safety issues: static → thread_local for deco args helper,
  thread-local libuv sync loop, mutex-guarded uv_spawn
- Mark process_io suite and cancellation pool-exhaustion tests as serial
- Rewrite query_info_child test with pipe synchronization (no sleep)
- Add parallel test run to CI alongside serial run

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@16bit-ykiko 16bit-ykiko force-pushed the feat/parallel-test-runner branch from 717169c to 7d11140 Compare March 30, 2026 04:16
16bit-ykiko and others added 3 commits March 30, 2026 12:17
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Adapt serial attribute support to work with main's inheritance-based
fixture pattern. Suite-level attrs are now detected via Derived::suite_attrs
static member instead of a template parameter.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (1)
src/zest/runner.cpp (1)

371-375: ⚠️ Potential issue | 🟡 Minor

Use elapsed wall time for the parallel summary.

summary.duration is still built by summing per-test runtimes, so the final “ms total” in --parallel mode can be much larger than actual elapsed time. Measure once around the whole parallel branch instead.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/zest/runner.cpp` around lines 371 - 375, The summary.duration is being
accumulated from per-test durations inside the record_result lambda which
inflates the total in --parallel mode; instead, record the wall-clock start
timestamp before entering the parallel branch and after the parallel work
finishes compute summary.duration = elapsed_ms (overwriting the summed value) so
the final summary uses real elapsed time; adjust code around the parallel
execution block and stop updating summary.duration inside record_result for
parallel runs (use options.parallel or the parallel execution scope to detect
this) and keep print_run_result/is_failure/record_result behavior unchanged
except not summing durations for parallel mode.
🧹 Nitpick comments (2)
.github/workflows/cmake.yml (1)

168-169: If CI time becomes a problem, narrow the extra parallel pass.

tests/CMakeLists.txt:188 already runs unit_tests via ctest, so these lines execute the suite twice in every matrix job. A smaller representative slice usually keeps the new path covered with much less CI time and flake surface.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/cmake.yml around lines 168 - 169, The workflow is running
the entire test suite twice because tests/CMakeLists.txt already runs unit_tests
via ctest; remove or narrow the explicit "${build_dir}/unit_tests" --parallel
invocation: either delete these two lines to rely on the existing ctest run, or
replace them with a targeted, small representative invocation such as running
the test binary with a specific filter (e.g., "${build_dir}/unit_tests"
--gtest_filter=SomeSmokeTest* --parallel) or invoking ctest with a focused regex
(ctest -R smoke -j ...), so the new parallel path covers only a small slice
rather than the full suite.
src/zest/runner.cpp (1)

53-62: Don’t silently ignore --parallel-workers.

unit_tests --parallel-workers 8 still runs sequentially unless --parallel is also set. Either reject that combination here or make a nonzero worker count imply parallel mode.

💡 One simple way to tighten this up
 auto to_runner_options(ZestCliOptions options)
     -> std::expected<eventide::zest::RunnerOptions, std::string> {
     if(options.test_filter_input.has_value() && !options.test_filter->empty()) {
         return std::unexpected("cannot use both positional filter and --test-filter");
     }
+    if(*options.parallel_workers != 0 && !*options.parallel) {
+        return std::unexpected("--parallel-workers requires --parallel");
+    }
 
     eventide::zest::RunnerOptions runner_options;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/zest/runner.cpp` around lines 53 - 62, to_runner_options currently allows
a nonzero options.parallel_workers to be set while options.parallel is false,
causing sequential execution; update to_runner_options to either (a) validate
and reject the combination by returning std::unexpected when
options.parallel_workers > 0 and !*options.parallel, or (b) infer parallel mode
by setting runner_options.parallel = true whenever *options.parallel_workers >
0; implement the chosen behavior in to_runner_options (use symbols
options.parallel_workers, options.parallel, to_runner_options, and
eventide::zest::RunnerOptions::parallel/parallel_workers to locate the change).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/async/io/fs.cpp`:
- Around line 626-638: The sync_loop() implementation currently ignores return
values from uv_loop_init and uv_loop_close; change sync_loop_holder to store the
uv_loop_init return code (e.g., an int init_result) and, because sync_loop() is
noexcept, call std::terminate() from the constructor if uv_loop_init(&loop)
returns non-zero, and likewise call uv_loop_close(&loop) in the destructor and
call std::terminate() if uv_loop_close returns non-zero (or UV_EBUSY), so the
code never silently returns an invalid/Leaked loop; reference the
sync_loop_holder struct, its constructor/destructor, uv_loop_init,
uv_loop_close, and holder.loop when making the changes.

In `@src/zest/runner.cpp`:
- Around line 416-424: The current worker pool uses std::vector<std::thread>
pool and manually joins threads, which can call std::terminate if
emplace_back(worker) throws; change the container to std::vector<std::jthread>
pool so each jthread will automatically join on destruction, keep
pool.reserve(num_workers), continue to emplace_back(worker) (jthread is
movable), and remove the manual join loop (for(auto& t: pool) t.join();). Ensure
the code still references num_workers and worker unchanged and that <thread> is
available for std::jthread.

---

Duplicate comments:
In `@src/zest/runner.cpp`:
- Around line 371-375: The summary.duration is being accumulated from per-test
durations inside the record_result lambda which inflates the total in --parallel
mode; instead, record the wall-clock start timestamp before entering the
parallel branch and after the parallel work finishes compute summary.duration =
elapsed_ms (overwriting the summed value) so the final summary uses real elapsed
time; adjust code around the parallel execution block and stop updating
summary.duration inside record_result for parallel runs (use options.parallel or
the parallel execution scope to detect this) and keep
print_run_result/is_failure/record_result behavior unchanged except not summing
durations for parallel mode.

---

Nitpick comments:
In @.github/workflows/cmake.yml:
- Around line 168-169: The workflow is running the entire test suite twice
because tests/CMakeLists.txt already runs unit_tests via ctest; remove or narrow
the explicit "${build_dir}/unit_tests" --parallel invocation: either delete
these two lines to rely on the existing ctest run, or replace them with a
targeted, small representative invocation such as running the test binary with a
specific filter (e.g., "${build_dir}/unit_tests" --gtest_filter=SomeSmokeTest*
--parallel) or invoking ctest with a focused regex (ctest -R smoke -j ...), so
the new parallel path covers only a small slice rather than the full suite.

In `@src/zest/runner.cpp`:
- Around line 53-62: to_runner_options currently allows a nonzero
options.parallel_workers to be set while options.parallel is false, causing
sequential execution; update to_runner_options to either (a) validate and reject
the combination by returning std::unexpected when options.parallel_workers > 0
and !*options.parallel, or (b) infer parallel mode by setting
runner_options.parallel = true whenever *options.parallel_workers > 0; implement
the chosen behavior in to_runner_options (use symbols options.parallel_workers,
options.parallel, to_runner_options, and
eventide::zest::RunnerOptions::parallel/parallel_workers to locate the change).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 18dc8d9a-98b0-41b0-900d-d7fd00f5e1ec

📥 Commits

Reviewing files that changed from the base of the PR and between 27fca3d and cc20ea1.

📒 Files selected for processing (9)
  • .github/workflows/cmake.yml
  • include/eventide/zest/detail/registry.h
  • include/eventide/zest/detail/suite.h
  • src/async/io/fs.cpp
  • src/async/libuv.h
  • src/zest/runner.cpp
  • tests/unit/async/cancellation_tests.cpp
  • tests/unit/async/process_tests.cpp
  • tests/unit/deco/runtime.cc
✅ Files skipped from review due to trivial changes (2)
  • tests/unit/async/cancellation_tests.cpp
  • tests/unit/deco/runtime.cc
🚧 Files skipped from review as they are similar to previous changes (3)
  • include/eventide/zest/detail/registry.h
  • include/eventide/zest/detail/suite.h
  • tests/unit/async/process_tests.cpp

16bit-ykiko and others added 9 commits March 30, 2026 13:34
pipe.connect_failure and pipe.stop use a hardcoded pipe name on Windows,
causing conflicts when run in parallel.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…E usage

TEST_CASE(name, TestAttrs{.serial = true}) instead of
TEST_CASE(name, zest::TestAttrs{.serial = true})

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
TEST_CASE(name, serial = true) instead of
TEST_CASE(name, TestAttrs{.serial = true})

Uses the same DECO pattern: __VA_ARGS__ becomes statements inside
a constructor of a struct inheriting from TestAttrs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
TEST_SUITE(process_io, loop_fixture) {
    TEST_SUITE_ATTRS(serial = true);
    ...
};

Eliminates the need for a separate fixture struct just to carry attrs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add missing <algorithm> and <functional> includes
- Use wall-clock time for parallel mode summary duration
- Clamp thread pool size to number of parallel tests
- Add error handling for uv_loop_init/uv_loop_close

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Use designated initializers instead of local struct in constexpr lambda
for TEST_CASE/TEST_SUITE_ATTRS — GCC rejects constructing a local struct
within the same constexpr lambda where it's defined.

Syntax is now: TEST_CASE(name, .serial = true)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace constexpr local struct (GCC-incompatible) and designated
initializers with a ZEST_MAKE_ATTRS helper that declares local
references to TestAttrs fields, enabling plain assignment syntax:

  TEST_CASE(name, serial = true)
  TEST_SUITE_ATTRS(serial = true)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace individual field references with `auto& [skip, focus, serial] = _a;`
to eliminate duplication between TestAttrs fields and macro variables.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@16bit-ykiko 16bit-ykiko merged commit 310cfaf into main Mar 30, 2026
33 checks passed
@16bit-ykiko 16bit-ykiko deleted the feat/parallel-test-runner branch March 30, 2026 07:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant