feat(harness): builder, lifecycle, stdout/stderr capture (#2) #20
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!20
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "feat/2-harness-lifecycle"
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 #2.
Stacked on #19 (feat/1-bootstrap-crate-skeleton).
Summary
Implements the process harness builder, spawn/stop/restart lifecycle, env forwarding, and stdout/stderr ring-buffer capture. Health polling is not in this PR — it lives in #3, which extends
start()with the HTTP poll loop.Builder
HarnessBuilderwithcommand,arg,args,env,working_dir,health_url,ws_url,startup_timeout,shutdown_timeout,health_poll_interval,buffer_bytes,build().ProcessHarness::builder()shortcut.Spawn
start()usestokio::process::Command, pipes stdout/stderr, setskill_on_drop.DISPLAY,WAYLAND_DISPLAY,XDG_RUNTIME_DIR,XDG_SESSION_TYPE,XDG_CURRENT_DESKTOP,DBUS_SESSION_BUS_ADDRESSfrom the parent. User-set env wins.OutputBuffers — ring over bytes, oldest dropped on overflow.pid(),stdout(),stderr()accessors.Stop & restart
stop()sendsSIGTERMvialibc::kill, waits up toshutdown_timeout, escalates tostart_kill()(SIGKILL) on timeout.restart()=stop() + start().Dropsends synchronousSIGTERM+ SIGKILL so a panicking test never leaks the child process.Dependency added
libc = "0.2"gated[target.'cfg(unix)'.dependencies]. Already a transitive dep of tokio; declaring it explicitly is the cheapest way to callkill(2).Checklist (from issue #2)
ProcessHarness::builder()start()spawns childDISPLAY/XDG_RUNTIME_DIRauto-forwardedworking_dirset if providedstop()SIGTERM → wait → SIGKILLDroptriggers synchronous best-effort SIGTERM + SIGKILLrestart()= stop + start (health re-wait deferred to #3)stdout()/stderr()accessorsstderr_tail()available for #3 to use in error messages (allow(dead_code) until then)cfg(unix)for the kill pathTest plan
just qagreen locally — 16/16 unit tests pass (10 new for the harness)Notes for the reviewer
stderr_tail()and theSTDERR_TAIL_LINESconstant carry an#[allow(dead_code)]because they're not yet called by anything in this PR — they exist as the API surface that #3's health-polling error path needs. Removing the allow is part of #3.wait_for_health()stub. #3 will inline the polling at the bottom ofstart(). Avoiding a placeholder method keeps the diff in #3 clean./bin/sh -cso they don't depend on a project-specific binary; portable across any Linux runner.stop().await.Review — harness lifecycle + stdout/stderr capture (#2)
OutputBufferMutex<Vec<u8>>. L'éviction utilisebuf.drain(..drop_n)qui est O(n) (décalage de tous les éléments restants). UneVecDequeavecpop_front()serait plus adaptée sémantiquement et plus efficace. À l'échelle d'une sortie de test (quelques Ko à Mo), ça n'a pas d'impact pratique, mais c'est un écart de design visible.String::from_utf8_lossypour la troncature mid-byte — bon choix défensif.Lifecycle SIGTERM/SIGKILL
stop(): SIGTERM → attenteshutdown_timeout→start_kill()(SIGKILL) — correct.Drop: SIGTERM puis SIGKILL immédiat sanswait(). Le process devient techniquement un zombie jusqu'à la fin du parent. Acceptable pour des binaires de test courte durée, surtout aveckill_on_drop(true)sur la commande tokio.Tests
drop_kills_running_child:Dropétant synchrone (pas d'await), le test passe toujours en < 3s — c'est un vrai check mais l'assertion temporelle est un peu trompeuse. Un commentaire explicitant l'intention serait bienvenu.Aucun bloquant.
✅ Pas de bloquant — implémentation solide, bonne couverture. Notes cosmétiques sur
VecvsVecDequepourOutputBufferet l'intention du testdrop_kills_running_child.