Files
rustup/tests/suite/cli_paths.rs

541 lines
19 KiB
Rust

//! This file contains tests relevant to Rustup's handling of updating PATHs.
//! It depends on self-update working, so if absolutely everything here breaks,
//! check those tests as well.
// Prefer omitting actually unpacking content while just testing paths.
const INIT_NONE: [&str; 4] = ["rustup-init", "-y", "--default-toolchain", "none"];
#[cfg(unix)]
mod unix {
use std::fmt::Display;
use std::fs;
use std::path::PathBuf;
use super::INIT_NONE;
use rustup::test::{CliTestContext, Scenario};
use rustup::utils::raw;
// Let's write a fake .rc which looks vaguely like a real script.
const FAKE_RC: &str = r#"
# Sources fruity punch.
. ~/fruit/punch
# Adds apples to PATH.
export PATH="$HOME/apple/bin"
"#;
const DEFAULT_EXPORT: &str = "export PATH=\"$HOME/.cargo/bin:$PATH\"\n";
const POSIX_SH: &str = "env";
fn source(dir: impl Display, sh: impl Display) -> String {
format!(". \"{dir}/{sh}\"\n")
}
// In 1.23 we used `source` instead of `.` by accident. This is not POSIX
// so we want to ensure that if we put this into someone's dot files, then
// with newer rustups we will revert that.
fn non_posix_source(dir: impl Display, sh: impl Display) -> String {
format!("source \"{dir}/{sh}\"\n")
}
#[tokio::test]
async fn install_creates_necessary_scripts() {
let cx = CliTestContext::new(Scenario::Empty).await;
// Override the test harness so that cargo home looks like
// $HOME/.cargo by removing CARGO_HOME from the environment,
// otherwise the literal path will be written to the file.
let mut cmd = cx.config.cmd("rustup-init", &INIT_NONE[1..]);
let files: Vec<PathBuf> = [".cargo/env", ".profile", ".zshenv"]
.iter()
.map(|file| cx.config.homedir.join(file))
.collect();
for file in &files {
assert!(!file.exists());
}
cmd.env_remove("CARGO_HOME");
cmd.env("SHELL", "zsh");
assert!(cmd.output().unwrap().status.success());
let mut rcs = files.iter();
let env = rcs.next().unwrap();
let envfile = fs::read_to_string(env).unwrap();
let (_, envfile_export) = envfile.split_at(envfile.find("export PATH").unwrap_or(0));
assert_eq!(&envfile_export[..DEFAULT_EXPORT.len()], DEFAULT_EXPORT);
for rc in rcs {
let expected = source("$HOME/.cargo", POSIX_SH);
let new_profile = fs::read_to_string(rc).unwrap();
assert_eq!(new_profile, expected);
}
}
#[tokio::test]
async fn install_updates_bash_rcs() {
let cx = CliTestContext::new(Scenario::Empty).await;
let rcs: Vec<PathBuf> = [".bashrc", ".bash_profile", ".bash_login", ".profile"]
.iter()
.map(|rc| cx.config.homedir.join(rc))
.collect();
for rc in &rcs {
raw::write_file(rc, FAKE_RC).unwrap();
}
cx.config.expect(&INIT_NONE).await.is_ok();
let expected = FAKE_RC.to_owned() + &source(cx.config.cargodir.display(), POSIX_SH);
for rc in &rcs {
let new_rc = fs::read_to_string(rc).unwrap();
assert_eq!(new_rc, expected);
}
}
#[tokio::test]
async fn install_does_not_create_bash_rcs() {
let cx = CliTestContext::new(Scenario::Empty).await;
let rcs: Vec<PathBuf> = [".bashrc", ".bash_profile", ".bash_login"]
.iter()
.map(|rc| cx.config.homedir.join(rc))
.collect();
let rcs_before = rcs.iter().map(|rc| rc.exists());
cx.config.expect(&INIT_NONE).await.is_ok();
for (before, after) in rcs_before.zip(rcs.iter().map(|rc| rc.exists())) {
assert!(!before);
assert_eq!(before, after);
}
}
// This test should NOT be run as root!
#[tokio::test]
async fn install_errors_when_rc_cannot_be_updated() {
let cx = CliTestContext::new(Scenario::Empty).await;
let rc = cx.config.homedir.join(".profile");
fs::File::create(&rc).unwrap();
let mut perms = fs::metadata(&rc).unwrap().permissions();
perms.set_readonly(true);
fs::set_permissions(&rc, perms).unwrap();
cx.config
.expect(&INIT_NONE)
.await
.with_stderr(snapbox::str![[r#"
...
error: could not amend shell profile[..]
...
"#]])
.is_err();
}
#[tokio::test]
async fn install_with_zdotdir() {
let cx = CliTestContext::new(Scenario::Empty).await;
let zdotdir = tempfile::Builder::new()
.prefix("zdotdir")
.tempdir()
.unwrap();
let rc = zdotdir.path().join(".zshenv");
raw::write_file(&rc, FAKE_RC).unwrap();
let mut cmd = cx.config.cmd("rustup-init", &INIT_NONE[1..]);
cmd.env("SHELL", "zsh");
cmd.env("ZDOTDIR", zdotdir.path());
assert!(cmd.output().unwrap().status.success());
let new_rc = fs::read_to_string(&rc).unwrap();
let expected = FAKE_RC.to_owned() + &source(cx.config.cargodir.display(), POSIX_SH);
assert_eq!(new_rc, expected);
}
#[tokio::test]
async fn install_with_zdotdir_from_calling_zsh() {
// This test requires that zsh is callable.
if std::process::Command::new("zsh")
.arg("-c")
.arg("true")
.status()
.is_err()
{
return;
}
let cx = CliTestContext::new(Scenario::Empty).await;
let zdotdir = tempfile::Builder::new()
.prefix("zdotdir")
.tempdir()
.unwrap();
let rc = zdotdir.path().join(".zshenv");
raw::write_file(&rc, FAKE_RC).unwrap();
// If $SHELL doesn't include "zsh", Zsh::zdotdir() will call zsh to obtain $ZDOTDIR.
// ZDOTDIR could be set directly in the environment, but having ~/.zshenv set
// ZDOTDIR is a normal setup, and ensures that the value came from calling zsh.
let home_zshenv = cx.config.homedir.join(".zshenv");
let export_zdotdir = format!(
"export ZDOTDIR=\"{}\"\n",
zdotdir.path().as_os_str().to_str().unwrap()
);
raw::write_file(&home_zshenv, &export_zdotdir).unwrap();
let mut cmd = cx.config.cmd("rustup-init", &INIT_NONE[1..]);
cmd.env("SHELL", "/bin/sh");
assert!(cmd.output().unwrap().status.success());
let new_rc = fs::read_to_string(&rc).unwrap();
let expected = FAKE_RC.to_owned() + &source(cx.config.cargodir.display(), POSIX_SH);
assert_eq!(new_rc, expected);
}
#[tokio::test]
async fn install_adds_path_to_rc_just_once() {
let cx = CliTestContext::new(Scenario::Empty).await;
let profile = cx.config.homedir.join(".profile");
raw::write_file(&profile, FAKE_RC).unwrap();
cx.config.expect(&INIT_NONE).await.is_ok();
cx.config.expect(&INIT_NONE).await.is_ok();
let new_profile = fs::read_to_string(&profile).unwrap();
let expected = FAKE_RC.to_owned() + &source(cx.config.cargodir.display(), POSIX_SH);
assert_eq!(new_profile, expected);
}
#[tokio::test]
async fn install_adds_path_to_rc_handling_no_newline() {
let cx = CliTestContext::new(Scenario::Empty).await;
let profile = cx.config.homedir.join(".profile");
let fake_rc_modified = FAKE_RC.strip_suffix('\n').expect("Should end in a newline");
raw::write_file(&profile, fake_rc_modified).unwrap();
// Run once to add the configuration
cx.config.expect(&INIT_NONE).await.is_ok();
// Run twice to test that the process is idempotent
cx.config.expect(&INIT_NONE).await.is_ok();
let new_profile = fs::read_to_string(&profile).unwrap();
let expected = FAKE_RC.to_owned() + &source(cx.config.cargodir.display(), POSIX_SH);
assert_eq!(new_profile, expected);
}
#[tokio::test]
async fn install_adds_path_to_multiple_rc_files() {
let cx = CliTestContext::new(Scenario::Empty).await;
// Two RC files that are both from the same shell
let bash_profile = cx.config.homedir.join(".bash_profile");
let bashrc = cx.config.homedir.join(".bashrc");
let expected = FAKE_RC.to_owned() + &source(cx.config.cargodir.display(), POSIX_SH);
// The order that the two files are processed isn't known, so test both orders
for [path1, path2] in &[[&bash_profile, &bashrc], [&bashrc, &bash_profile]] {
raw::write_file(path1, &expected).unwrap();
raw::write_file(path2, FAKE_RC).unwrap();
cx.config.expect(&INIT_NONE).await.is_ok();
let new1 = fs::read_to_string(path1).unwrap();
assert_eq!(new1, expected);
let new2 = fs::read_to_string(path2).unwrap();
assert_eq!(new2, expected);
}
}
#[tokio::test]
async fn uninstall_removes_source_from_rcs() {
let cx = CliTestContext::new(Scenario::Empty).await;
let rcs: Vec<PathBuf> = [
".bashrc",
".bash_profile",
".bash_login",
".profile",
// This requires zsh to be installed, so we test it on macOS only.
#[cfg(target_os = "macos")]
".zshenv",
]
.iter()
.map(|rc| cx.config.homedir.join(rc))
.collect();
for rc in &rcs {
raw::write_file(rc, FAKE_RC).unwrap();
}
cx.config.expect(&INIT_NONE).await.is_ok();
cx.config
.expect(&["rustup", "self", "uninstall", "-y"])
.await
.is_ok();
for rc in &rcs {
let new_rc = fs::read_to_string(rc).unwrap();
assert_eq!(new_rc, FAKE_RC);
}
}
#[tokio::test]
async fn uninstall_doesnt_modify_rcs_with_no_modify_path() {
let cx = CliTestContext::new(Scenario::Empty).await;
let rcs = [
".bashrc",
".bash_profile",
".bash_login",
".profile",
// This requires zsh to be installed, so we test it on macOS only.
#[cfg(target_os = "macos")]
".zshenv",
]
.map(|rc| cx.config.homedir.join(rc));
for rc in &rcs {
raw::write_file(rc, FAKE_RC).unwrap();
}
let expected = FAKE_RC.to_owned() + &source(cx.config.cargodir.display(), POSIX_SH);
cx.config.expect(&INIT_NONE).await.is_ok();
// sanity check
for rc in &rcs {
let new_rc = fs::read_to_string(rc).unwrap();
assert_eq!(
new_rc, expected,
"Rc file {rc:?} does not contain a source after rustup-init",
);
}
cx.config
.expect(&["rustup", "self", "uninstall", "-y", "--no-modify-path"])
.await
.is_ok();
for rc in &rcs {
let new_rc = fs::read_to_string(rc).unwrap();
assert_eq!(
new_rc, expected,
"Rc file {rc:?} does not contain a source after uninstall",
);
}
}
#[tokio::test]
async fn install_adds_sources_while_removing_legacy_paths() {
let cx = CliTestContext::new(Scenario::Empty).await;
let zdotdir = tempfile::Builder::new()
.prefix("zdotdir")
.tempdir()
.unwrap();
let rcs: Vec<PathBuf> = [".bash_profile", ".profile"]
.iter()
.map(|rc| cx.config.homedir.join(rc))
.collect();
let zprofiles = vec![
cx.config.homedir.join(".zprofile"),
zdotdir.path().join(".zprofile"),
];
let old_rc =
FAKE_RC.to_owned() + DEFAULT_EXPORT + &non_posix_source("$HOME/.cargo", POSIX_SH);
for rc in rcs.iter().chain(zprofiles.iter()) {
raw::write_file(rc, &old_rc).unwrap();
}
let mut cmd = cx.config.cmd("rustup-init", &INIT_NONE[1..]);
cmd.env("SHELL", "zsh");
cmd.env("ZDOTDIR", zdotdir.path());
cmd.env_remove("CARGO_HOME");
assert!(cmd.output().unwrap().status.success());
let fixed_rc = FAKE_RC.to_owned() + &source("$HOME/.cargo", POSIX_SH);
for rc in &rcs {
let new_rc = fs::read_to_string(rc).unwrap();
assert_eq!(new_rc, fixed_rc);
}
for rc in &zprofiles {
let new_rc = fs::read_to_string(rc).unwrap();
assert_eq!(new_rc, FAKE_RC);
}
}
#[tokio::test]
async fn uninstall_cleans_up_legacy_paths() {
let cx = CliTestContext::new(Scenario::Empty).await;
// Install first, then overwrite.
cx.config.expect(&INIT_NONE).await.is_ok();
let zdotdir = tempfile::Builder::new()
.prefix("zdotdir")
.tempdir()
.unwrap();
let mut cmd = cx.config.cmd("rustup-init", &INIT_NONE[1..]);
cmd.env("SHELL", "zsh");
cmd.env("ZDOTDIR", zdotdir.path());
cmd.env_remove("CARGO_HOME");
assert!(cmd.output().unwrap().status.success());
let mut rcs: Vec<PathBuf> = [".bash_profile", ".profile", ".zprofile"]
.iter()
.map(|rc| cx.config.homedir.join(rc))
.collect();
rcs.push(zdotdir.path().join(".zprofile"));
let old_rc =
FAKE_RC.to_owned() + DEFAULT_EXPORT + &non_posix_source("$HOME/.cargo", POSIX_SH);
for rc in &rcs {
raw::write_file(rc, &old_rc).unwrap();
}
let mut cmd = cx.config.cmd("rustup", ["self", "uninstall", "-y"]);
cmd.env("SHELL", "zsh");
cmd.env("ZDOTDIR", zdotdir.path());
cmd.env_remove("CARGO_HOME");
assert!(cmd.output().unwrap().status.success());
for rc in &rcs {
let new_rc = fs::read_to_string(rc).unwrap();
// It's not ideal, but it's OK, if we leave whitespace.
assert_eq!(new_rc, FAKE_RC);
}
}
// In the default case we want to write $HOME/.cargo/bin as the path,
// not the full path.
#[tokio::test]
async fn when_cargo_home_is_the_default_write_path_specially() {
let cx = CliTestContext::new(Scenario::Empty).await;
// Override the test harness so that cargo home looks like
// $HOME/.cargo by removing CARGO_HOME from the environment,
// otherwise the literal path will be written to the file.
let profile = cx.config.homedir.join(".profile");
raw::write_file(&profile, FAKE_RC).unwrap();
let mut cmd = cx.config.cmd("rustup-init", &INIT_NONE[1..]);
cmd.env_remove("CARGO_HOME");
assert!(cmd.output().unwrap().status.success());
let new_profile = fs::read_to_string(&profile).unwrap();
let expected = format!("{FAKE_RC}. \"$HOME/.cargo/env\"\n");
assert_eq!(new_profile, expected);
let mut cmd = cx.config.cmd("rustup", ["self", "uninstall", "-y"]);
cmd.env_remove("CARGO_HOME");
assert!(cmd.output().unwrap().status.success());
let new_profile = fs::read_to_string(&profile).unwrap();
assert_eq!(new_profile, FAKE_RC);
}
#[tokio::test]
async fn install_doesnt_modify_path_if_passed_no_modify_path() {
let cx = CliTestContext::new(Scenario::Empty).await;
let profile = cx.config.homedir.join(".profile");
cx.config
.expect(&[
"rustup-init",
"-y",
"--no-modify-path",
"--default-toolchain",
"none",
])
.await
.is_ok();
assert!(!profile.exists());
}
}
#[cfg(windows)]
mod windows {
use super::INIT_NONE;
use rustup::test::{CliTestContext, Scenario};
use rustup::test::{RegistryGuard, USER_PATH, get_path};
use windows_registry::{HSTRING, Value};
#[tokio::test]
/// Smoke test for end-to-end code connectivity of the installer path mgmt on windows.
async fn install_uninstall_affect_path() {
let cx = CliTestContext::new(Scenario::Empty).await;
let _guard = RegistryGuard::new(&USER_PATH).unwrap();
let cfg_path = cx.config.cargodir.join("bin").display().to_string();
let get_path_ = || {
HSTRING::try_from(get_path().unwrap().unwrap())
.unwrap()
.to_string()
};
cx.config.expect(&INIT_NONE).await.is_ok();
assert!(
get_path_().contains(cfg_path.trim_matches('"')),
"`{}` not in `{}`",
cfg_path,
get_path_()
);
cx.config
.expect(&["rustup", "self", "uninstall", "-y"])
.await
.is_ok();
assert!(!get_path_().contains(&cfg_path));
}
#[tokio::test]
async fn uninstall_doesnt_affect_path_with_no_modify_path() {
let cx = CliTestContext::new(Scenario::Empty).await;
let _guard = RegistryGuard::new(&USER_PATH).unwrap();
let cfg_path = cx.config.cargodir.join("bin").display().to_string();
let get_path_ = || {
HSTRING::try_from(get_path().unwrap().unwrap())
.unwrap()
.to_string()
};
cx.config.expect(&INIT_NONE).await.is_ok();
cx.config
.expect(&["rustup", "self", "uninstall", "-y", "--no-modify-path"])
.await
.is_ok();
assert!(
get_path_().contains(cfg_path.trim_matches('"')),
"`{}` not in `{}`",
cfg_path,
get_path_()
);
}
#[tokio::test]
/// Smoke test for end-to-end code connectivity of the installer path mgmt on windows.
async fn install_uninstall_affect_path_with_non_unicode() {
use std::os::windows::ffi::OsStrExt;
use windows_registry::{CURRENT_USER, Type};
let cx = CliTestContext::new(Scenario::Empty).await;
let _guard = RegistryGuard::new(&USER_PATH).unwrap();
// Set up a non unicode PATH
let mut reg_value = Value::from([
0x00, 0xD8, // leading surrogate
0x01, 0x01, // bogus trailing surrogate
0x00, 0x00, // null
]);
reg_value.set_ty(Type::ExpandString);
CURRENT_USER
.create("Environment")
.unwrap()
.set_value("PATH", &reg_value)
.unwrap();
// compute expected path after installation
let mut expected = Value::from(
cx.config
.cargodir
.join("bin")
.as_os_str()
.encode_wide()
.flat_map(|v| vec![v as u8, (v >> 8) as u8])
.chain(vec![b';', 0])
.chain(reg_value.iter().copied())
.collect::<Vec<u8>>()
.as_slice(),
);
expected.set_ty(Type::ExpandString);
cx.config.expect(&INIT_NONE).await.is_ok();
assert_eq!(get_path().unwrap().unwrap(), expected);
cx.config
.expect(&["rustup", "self", "uninstall", "-y"])
.await
.is_ok();
assert_eq!(get_path().unwrap().unwrap(), reg_value);
}
}