541 lines
19 KiB
Rust
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", ®_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);
|
|
}
|
|
}
|