refactor(test/clitools)!: refine semantics of CliTestContext::kill_at()#4966
refactor(test/clitools)!: refine semantics of CliTestContext::kill_at()#4966rami3l wants to merge 4 commits into
CliTestContext::kill_at()#4966Conversation
There was a problem hiding this comment.
I reworked #4965 a bit to follow the patterns you have here. I will plan to rebase this onto it once it is merged. One thing maybe to note for myself it to drop my local copy of the metadata checkpoint name in favor ofCHECKPOINT_UPDATE_BEFORE_METADATA.
One question if you don't mind: for testing concurrent installs of the same toolchain, I'll need to keep a child parked at a checkpoint while a second command runs, then kill it explicitly. Would you be open to a spawn_at(checkpoint, args)-style primitive returning a guard (child killed on drop), with kill_at becoming a thin wrapper over it? I can propose that when I rebase.
@cachebag Thanks for the feedback! Please feel free to suggest a patch based on the changes here and I'll add you as a coauthor here. |
patchFrom 91705e92efcc5fdd5adcb4b29f15dd624f5ad45f Mon Sep 17 00:00:00 2001
From: akrm al-hakimi <alhakimiakrmj@gmail.com>
Date: Fri, 24 Jul 2026 11:09:26 -0400
Subject: [PATCH] refactor(test/clitools): extract `spawn_at()` and a
kill-on-drop guard
`kill_at()` becomes a thin wrapper over `spawn_at()`, which parks the
command at its checkpoint and returns a `ParkedChild` guard that kills
it on drop, covering the timeout and early-exit panic paths as well.
The `CMD_LOCK` read guard is scoped to the spawn itself: it cannot stay
held while the child is parked, since a writer queued behind it would
block every command spawned in the meantime, including the ones a test
wants to run against the parked process.
---
src/test.rs | 2 +-
src/test/clitools.rs | 79 +++++++++++++++++++++++++++++++++-----------
2 files changed, 61 insertions(+), 20 deletions(-)
diff --git a/src/test.rs b/src/test.rs
index 314b4624..7c96bff6 100644
--- a/src/test.rs
+++ b/src/test.rs
@@ -28,7 +28,7 @@ pub use crate::cli::self_update::{RegistryGuard, RegistryValueId, USER_PATH, get
mod clitools;
pub use clitools::{
- Assert, CliTestContext, Config, SanitizedOutput, Scenario, SelfUpdateTestContext,
+ Assert, CliTestContext, Config, ParkedChild, SanitizedOutput, Scenario, SelfUpdateTestContext,
output_release_file, print_command, print_indented,
};
pub(super) mod dist;
diff --git a/src/test/clitools.rs b/src/test/clitools.rs
index a8cfe9fb..6c563059 100644
--- a/src/test/clitools.rs
+++ b/src/test/clitools.rs
@@ -12,7 +12,7 @@ use std::{
mem,
ops::{Deref, DerefMut},
path::{Path, PathBuf},
- process::{Command, ExitStatus},
+ process::{Child, Command, ExitStatus},
string::FromUtf8Error,
sync::{Arc, LazyLock, RwLock, RwLockWriteGuard},
thread,
@@ -973,13 +973,14 @@ impl CliTestContext {
Self { config, _test_dir }
}
- /// Run a rustup command until it reaches `checkpoint`, then terminate it
- /// without giving destructors an opportunity to run.
- pub fn kill_at<S: AsRef<OsStr> + Clone + Debug>(
+ /// Run a rustup command until it reaches `checkpoint` and keep it parked
+ /// there, e.g. to observe or race the paused operation from another
+ /// process. The returned guard kills the command when dropped.
+ pub fn spawn_at<S: AsRef<OsStr> + Clone + Debug>(
&self,
checkpoint: &str,
args: impl AsRef<[S]>,
- ) -> ExitStatus {
+ ) -> ParkedChild {
let marker = checkpoint_path(&self.config.test_root_dir, checkpoint);
if let Err(error) = fs::remove_file(&marker)
&& error.kind() != io::ErrorKind::NotFound
@@ -1000,36 +1001,46 @@ impl CliTestContext {
);
cmd.env(CHECKPOINT_ENV, checkpoint);
- let _lock = CMD_LOCK.read().unwrap();
- let mut child = cmd
- .spawn()
- .expect("failed to start command for checkpoint test");
+ // The lock covers the spawn itself. It must not be held while the
+ // child stays parked: a writer queued behind it would then block
+ // every command spawned in the meantime, including the ones a test
+ // wants to run against the parked process.
+ let child = {
+ let _lock = CMD_LOCK.read().unwrap();
+ cmd.spawn()
+ .expect("failed to start command for checkpoint test")
+ };
+ let mut parked = ParkedChild { child: Some(child) };
let deadline = Instant::now() + StdDuration::from_secs(10);
loop {
if matches!(fs::read_to_string(&marker), Ok(contents) if contents == checkpoint) {
- break;
+ break parked;
}
- if let Some(status) = child
+ if let Some(status) = parked
+ .child
+ .as_mut()
+ .unwrap()
.try_wait()
.expect("failed to query checkpoint test command")
{
panic!("command exited before reaching checkpoint {checkpoint:?}: {status}");
}
if Instant::now() >= deadline {
- let _ = child.kill();
- let _ = child.wait();
panic!("timed out waiting for checkpoint {checkpoint:?}");
}
thread::sleep(StdDuration::from_millis(10));
}
+ }
- child
- .kill()
- .expect("failed to terminate command at checkpoint");
- child
- .wait()
- .expect("failed to reap command after checkpoint")
+ /// Run a rustup command until it reaches `checkpoint`, then terminate it
+ /// without giving destructors an opportunity to run.
+ pub fn kill_at<S: AsRef<OsStr> + Clone + Debug>(
+ &self,
+ checkpoint: &str,
+ args: impl AsRef<[S]>,
+ ) -> ExitStatus {
+ self.spawn_at(checkpoint, args).kill()
}
/// Move the dist server to the specified scenario and restore it
@@ -1108,6 +1119,36 @@ impl Drop for WorkDirGuard<'_> {
}
}
+/// A rustup command spawned by [`CliTestContext::spawn_at`], parked at a
+/// checkpoint. Killed on drop unless [`ParkedChild::kill`] was called first.
+#[must_use]
+pub struct ParkedChild {
+ child: Option<Child>,
+}
+
+impl ParkedChild {
+ /// Terminate the parked command without giving destructors an opportunity
+ /// to run, then reap it.
+ pub fn kill(mut self) -> ExitStatus {
+ let mut child = self.child.take().unwrap();
+ child
+ .kill()
+ .expect("failed to terminate command at checkpoint");
+ child
+ .wait()
+ .expect("failed to reap command after checkpoint")
+ }
+}
+
+impl Drop for ParkedChild {
+ fn drop(&mut self) {
+ if let Some(mut child) = self.child.take() {
+ let _ = child.kill();
+ let _ = child.wait();
+ }
+ }
+}
+
#[must_use]
pub struct DistDirGuard<'a> {
inner: &'a mut CliTestContext,
--
2.39.5 (Apple Git-154) |
…t()` Co-authored-by: akrm al-hakimi <alhakimiakrmj@gmail.com>
Co-authored-by: akrm al-hakimi <alhakimiakrmj@gmail.com>
09b779d to
bd6325c
Compare
Refines #4963 before going further with e.g. #4965.
cc @cachebag