Skip to content

Commit 18db95f

Browse files
committed
test(cli): cover interrupted staged toolchain installs
Covers kills before and after publication, stage reuse across repeated interruptions, reclamation of a stale stage without leaking its contents into the published toolchain, and a stray update hash being ignored during a staged install and refreshed after publication.
1 parent db82681 commit 18db95f

1 file changed

Lines changed: 206 additions & 4 deletions

File tree

tests/suite/cli_crash.rs

Lines changed: 206 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,225 @@
1-
use rustup::{
2-
dist::manifestation::CHECKPOINT_UPDATE_BEFORE_METADATA,
3-
test::{CliTestContext, Scenario},
1+
use std::{fs, path::PathBuf, process::Command, time::Duration};
2+
3+
use rustup::dist::manifestation::CHECKPOINT_UPDATE_BEFORE_METADATA;
4+
use rustup::install::{
5+
CHECKPOINT_INSTALL_AFTER_PUBLISH, CHECKPOINT_INSTALL_BEFORE_PUBLISH, STAGING_DIR_PREFIX,
46
};
7+
use rustup::test::{CliTestContext, Scenario, this_host_tuple};
8+
use wait_timeout::ChildExt;
9+
10+
fn assert_completes_successfully(mut command: Command) {
11+
let mut child = command.spawn().expect("failed to start command");
12+
let Some(status) = child
13+
.wait_timeout(Duration::from_secs(10))
14+
.expect("failed to wait for command")
15+
else {
16+
let _ = child.kill();
17+
let _ = child.wait();
18+
panic!("command did not complete within 10 seconds");
19+
};
20+
assert!(status.success(), "command failed with status {status}");
21+
}
22+
23+
fn nightly_path(cx: &CliTestContext) -> PathBuf {
24+
cx.config
25+
.rustupdir
26+
.join("toolchains")
27+
.join(format!("nightly-{}", this_host_tuple()))
28+
}
29+
30+
fn nightly_update_hash_path(cx: &CliTestContext) -> PathBuf {
31+
cx.config
32+
.rustupdir
33+
.join("update-hashes")
34+
.join(format!("nightly-{}", this_host_tuple()))
35+
}
36+
37+
fn nightly_staging_root(cx: &CliTestContext) -> PathBuf {
38+
cx.config
39+
.rustupdir
40+
.join("toolchains")
41+
.join(format!("{STAGING_DIR_PREFIX}nightly-{}", this_host_tuple()))
42+
}
43+
44+
fn staging_paths(cx: &CliTestContext) -> Vec<PathBuf> {
45+
fs::read_dir(cx.config.rustupdir.join("toolchains"))
46+
.expect("failed to read toolchains directory")
47+
.map(|entry| entry.expect("failed to read toolchains entry").path())
48+
.filter(|path| {
49+
path.file_name()
50+
.and_then(|name| name.to_str())
51+
.is_some_and(|name| name.starts_with(STAGING_DIR_PREFIX))
52+
})
53+
.collect()
54+
}
55+
56+
async fn assert_nightly_is_complete(cx: &CliTestContext) {
57+
cx.config
58+
.expect(["rustup", "+nightly", "component", "list", "--installed"])
59+
.await
60+
.with_stdout(snapbox::str![[r#"
61+
cargo-[HOST_TUPLE]
62+
rust-docs-[HOST_TUPLE]
63+
rust-std-[HOST_TUPLE]
64+
rustc-[HOST_TUPLE]
65+
66+
"#]])
67+
.is_ok();
68+
cx.config
69+
.expect(["rustc", "+nightly", "--version"])
70+
.await
71+
.with_stdout(snapbox::str![[r#"
72+
1.3.0 (hash-nightly-2)
73+
74+
"#]])
75+
.is_ok();
76+
}
77+
78+
async fn assert_unpublished_install_can_be_retried(checkpoint: &str) {
79+
let cx = CliTestContext::new(Scenario::SimpleV2).await;
80+
let status = cx.kill_at(checkpoint, ["rustup", "toolchain", "install", "nightly"]);
81+
assert!(!status.success());
82+
assert!(
83+
!nightly_path(&cx).exists(),
84+
"an interrupted staging operation published the toolchain"
85+
);
86+
assert_eq!(staging_paths(&cx), vec![nightly_staging_root(&cx)]);
87+
cx.config
88+
.expect(["rustup", "toolchain", "list"])
89+
.await
90+
.without_stdout("rustup-staging")
91+
.without_stdout("nightly")
92+
.is_ok();
93+
94+
assert_completes_successfully(cx.config.cmd("rustup", ["toolchain", "install", "nightly"]));
95+
assert!(nightly_path(&cx).is_dir());
96+
assert_nightly_is_complete(&cx).await;
97+
}
598

699
#[tokio::test]
7100
async fn interrupted_install_can_be_retried() {
101+
assert_unpublished_install_can_be_retried(CHECKPOINT_UPDATE_BEFORE_METADATA).await;
102+
}
103+
104+
#[tokio::test]
105+
async fn interrupted_install_before_publication_can_be_retried() {
106+
assert_unpublished_install_can_be_retried(CHECKPOINT_INSTALL_BEFORE_PUBLISH).await;
107+
}
108+
109+
#[tokio::test]
110+
async fn interrupted_installs_do_not_accumulate_stages() {
8111
let cx = CliTestContext::new(Scenario::SimpleV2).await;
112+
113+
for _ in 0..2 {
114+
let status = cx.kill_at(
115+
CHECKPOINT_INSTALL_BEFORE_PUBLISH,
116+
["rustup", "toolchain", "install", "nightly"],
117+
);
118+
assert!(!status.success());
119+
assert_eq!(staging_paths(&cx), vec![nightly_staging_root(&cx)]);
120+
}
121+
122+
assert_completes_successfully(cx.config.cmd("rustup", ["toolchain", "install", "nightly"]));
123+
assert!(staging_paths(&cx).is_empty());
124+
assert_nightly_is_complete(&cx).await;
125+
}
126+
127+
#[tokio::test]
128+
async fn stale_stage_is_reclaimed_by_the_next_install() {
129+
let cx = CliTestContext::new(Scenario::SimpleV2).await;
130+
let junk = nightly_staging_root(&cx)
131+
.join("toolchain")
132+
.join("bin")
133+
.join("stale-junk");
134+
fs::create_dir_all(junk.parent().unwrap()).unwrap();
135+
fs::write(&junk, "junk").unwrap();
136+
fs::write(nightly_staging_root(&cx).join("update-hash"), "stale-hash").unwrap();
137+
138+
assert_completes_successfully(cx.config.cmd("rustup", ["toolchain", "install", "nightly"]));
139+
assert!(staging_paths(&cx).is_empty());
140+
assert!(
141+
!nightly_path(&cx).join("bin").join("stale-junk").exists(),
142+
"contents of a stale stage leaked into the published toolchain"
143+
);
144+
assert_nightly_is_complete(&cx).await;
145+
}
146+
147+
#[tokio::test]
148+
async fn unpublished_install_does_not_change_update_hash() {
149+
let cx = CliTestContext::new(Scenario::SimpleV2).await;
150+
let update_hash = nightly_update_hash_path(&cx);
151+
fs::create_dir_all(update_hash.parent().unwrap()).unwrap();
152+
fs::write(&update_hash, "stale-hash").unwrap();
153+
9154
let status = cx.kill_at(
10-
CHECKPOINT_UPDATE_BEFORE_METADATA,
155+
CHECKPOINT_INSTALL_BEFORE_PUBLISH,
156+
["rustup", "toolchain", "install", "nightly"],
157+
);
158+
159+
assert!(!status.success());
160+
assert_eq!(fs::read_to_string(update_hash).unwrap(), "stale-hash");
161+
assert!(!nightly_path(&cx).exists());
162+
}
163+
164+
#[tokio::test]
165+
async fn interrupted_install_after_publication_is_complete() {
166+
let cx = CliTestContext::new(Scenario::SimpleV2).await;
167+
let status = cx.kill_at(
168+
CHECKPOINT_INSTALL_AFTER_PUBLISH,
11169
["rustup", "toolchain", "install", "nightly"],
12170
);
13171
assert!(!status.success());
172+
assert!(nightly_path(&cx).is_dir());
173+
assert!(staging_paths(&cx).is_empty());
174+
assert_nightly_is_complete(&cx).await;
175+
176+
assert_completes_successfully(cx.config.cmd("rustup", ["toolchain", "install", "nightly"]));
177+
assert_nightly_is_complete(&cx).await;
178+
}
179+
180+
#[tokio::test]
181+
async fn failed_install_removes_staging_directory() {
182+
let cx = CliTestContext::new(Scenario::UnavailableRls).await;
183+
cx.config.set_current_dist_date("2015-01-01");
184+
cx.config
185+
.expect(["rustup", "set", "profile", "complete"])
186+
.await
187+
.is_ok();
14188

189+
cx.config
190+
.expect(["rustup", "toolchain", "install", "nightly"])
191+
.await
192+
.is_err();
193+
194+
assert!(!nightly_path(&cx).exists());
195+
assert!(staging_paths(&cx).is_empty());
196+
}
197+
198+
#[tokio::test]
199+
async fn interrupted_update_can_be_retried() {
200+
let cx = CliTestContext::new(Scenario::ArchivesV2).await;
201+
cx.config.set_current_dist_date("2015-01-01");
15202
cx.config
16203
.expect(["rustup", "toolchain", "install", "nightly"])
17204
.await
18205
.is_ok();
19206
cx.config
20207
.expect(["rustc", "+nightly", "--version"])
21208
.await
209+
.with_stdout(snapbox::str![[r#"
210+
1.2.0 (hash-nightly-1)
211+
212+
"#]])
22213
.is_ok();
214+
215+
cx.config.set_current_dist_date("2015-01-02");
216+
let status = cx.kill_at(
217+
CHECKPOINT_UPDATE_BEFORE_METADATA,
218+
["rustup", "update", "nightly"],
219+
);
220+
assert!(!status.success());
221+
222+
assert_completes_successfully(cx.config.cmd("rustup", ["update", "nightly"]));
223+
224+
assert_nightly_is_complete(&cx).await;
23225
}

0 commit comments

Comments
 (0)