Skip to content

refactor(test/clitools)!: refine semantics of CliTestContext::kill_at()#4966

Draft
rami3l wants to merge 4 commits into
rust-lang:mainfrom
rami3l:refactor/polish-kill-at-checkpoint
Draft

refactor(test/clitools)!: refine semantics of CliTestContext::kill_at()#4966
rami3l wants to merge 4 commits into
rust-lang:mainfrom
rami3l:refactor/polish-kill-at-checkpoint

Conversation

@rami3l

@rami3l rami3l commented Jul 20, 2026

Copy link
Copy Markdown
Member

Refines #4963 before going further with e.g. #4965.

cc @cachebag

@cachebag cachebag left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

View changes since this review

Comment thread src/test/clitools.rs Outdated
@rami3l

rami3l commented Jul 24, 2026

Copy link
Copy Markdown
Member Author

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.

@cachebag

cachebag commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

@rami3l

patch
From 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)

rami3l and others added 3 commits July 24, 2026 17:41
…t()`

Co-authored-by: akrm al-hakimi <alhakimiakrmj@gmail.com>
Co-authored-by: akrm al-hakimi <alhakimiakrmj@gmail.com>
@rami3l
rami3l force-pushed the refactor/polish-kill-at-checkpoint branch from 09b779d to bd6325c Compare July 24, 2026 15:43
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.

2 participants