feat(runner): optional parallel execution with isolated clients (#12) #30
No reviewers
Labels
No labels
area:assertions
area:cli
area:client
area:harness
area:meta
area:reporting
area:runner
type:user-story
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
charles/ws-rpc-test!30
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "feat/12-parallel-mode"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes #12. Stacked on #29.
Summary
Adds opt-in parallel execution where each eligible test runs on its own
RpcClientconnection.Config
RunConfig.parallel: bool— whentrue, tests marked.parallel()run concurrently in a bounded batch after the non-parallel tests.RunConfig.parallel_workers: Option<usize>— defaults tostd::thread::available_parallelism(), fallback 1.TestRunner::parallel(bool)andTestRunner::parallel_workers(usize)setters.TestCase::parallel()opt-in marker (newparallel_eligible: boolfield).Architecture
SharedTestFn = Arc<dyn Fn(Arc<RpcClient>) -> BoxedTestFut + Send + Sync>at the top ofrun()so tokio tasks can clone them cheaply. (Reuses the existingArc::from(box)impl — no allocation changes in the sequential path.)run()partitions tests after applying the filter: filtered →report.filtered, parallel-eligible → parallel phase, else → sequential phase.tokio::task::JoinSetgated bytokio::sync::Semaphore(parallel_workers). Each task acquires its ownRpcClientviaconnect()— per the spec, "each parallel test gets its own connection". Hooks (before_each/after_each) run on the task's own connection.before_all/after_allstill run once on the shared connection.skip_ifis evaluated on the scheduling task so we don't bother spawning a worker for an obviously-skipped test.AtomicBoolis set when any parallel test fails (or connect errors). The scheduler checks it before spawning each new task; skipped tests are reported withreason = "fail-fast".Extracted helpers
skipped_result(case, reason)→TestCaseResultwith status Skipped, duration 0.print_result(result)→ the PASS/FAIL/SKIP output line plus any error message. Used by both sequential and parallel phases.Checklist (from issue #12)
.parallel()per-test marker (newparallel_eligiblefield)RunConfig.parallel: bool— false ignores marks entirelyRunConfig.parallel_workers: Option<usize>— default viaavailable_parallelism()JoinSetRpcClientbefore_each/after_eachrun on the test's own connectionbefore_all/after_allrun once on a shared connection.parallel()warns about shared server-side stateTest plan (4 new tests)
parallel_mode_disabled_runs_everything_sequentially—.parallel()marks are ignored when the runner isn't in parallel mode; registration order preservedparallel_mode_runs_eligible_tests_concurrently— 3 tests × 200 ms each complete under 550 ms total (serial would need ≥ 600 ms) — proves actual concurrencyparallel_mode_isolates_client_connections— two tests call different echo methods; each task's own connection sees its own responseparallel_mode_with_failfast_skips_later_parallel_tests—parallel_workers=1, first test fails, subsequent tests recorded as Skippedjust qagreen locally — 89/89 unit testsNotes for the reviewer
print_resultextraction also simplifies the sequential phase — therun()body got noticeably shorter after introducing it. (Net +324 lines, but a lot of that is tests and the multi-phase run logic.)parallel_workers=1in the fail-fast test is deliberate — it ensures the first failing test completes and sets the AtomicBool before the scheduler even looks at the next task, which keeps the test deterministic. Higher worker counts would race and occasionally let all three tests start concurrently.RpcClient::connectfails is reported as aFailedresult with the connect error preserved. This is treated the same as any other test failure for fail-fast purposes.Review — parallel execution avec clients isolés (#12)
Architecture : séquentiel d'abord, parallèle ensuite
Tests non marqués
.parallel()→ phase séquentielle avec client partagé. Tests marqués → phase parallèle, chacun avec sa propre connexion. C'est la sémantique la plus sûre par défaut.SharedTestFn = Arc<dyn Fn(...)>Nécessaire pour cloner les hooks vers les tâches parallèles. Naming clair. La transformation
before_all.take().map(Arc::from)est propre.fail_fast_tripped— AtomicBool partagéLe flag est vérifié avant de créer chaque tâche, pas pendant son exécution. Il y a donc une fenêtre TOCTOU : une tâche peut démarrer juste avant qu'une autre tâche la tripe, et toutes deux s'exécutent. C'est inévitable en parallèle sans synchronisation bloquante — comportement documenté acceptable.
Ordre des résultats dans le rapport
Les résultats parallèles sont collectés via
JoinSet::join_next()en ordre de complétion (pas d'enregistrement). Lereport.testssera donc partiellement désordonné (séquentiel en ordre, puis parallèle en ordre d'achèvement). Pourrait surprendre des utilisateurs qui s'attendent à un rapport ordonné. Vaut la peine de le documenter.Test de timing fragile
3 tâches de 200ms en parallèle avec workers=4 → ~200ms attendu. La marge de 550ms est généreuse mais peut échouer sur une machine CI très chargée. Acceptable pour un test d'intégration.
Tests
parallel_mode_disabled_runs_everything_sequentially— vérifie que.parallel()sur les tests est ignoré si le mode n'est pas activé.parallel_mode_with_failfast_skips_later_parallel_tests— asserts souples (skipped >= 1) pour tenir compte du non-déterminisme.Aucun bloquant. (Le bug timeout de #11 est ici déjà atténué —
TestRunner::new()utilisewith_defaults().)✅ Pas de bloquant — architecture séquentiel-d'abord correcte, isolation des clients propre. À documenter : résultats parallèles en ordre de complétion, pas d'enregistrement.
22c6403e86to180c7e6f1d