Skip to content

Commit

Permalink
Add some tests for simulating behavior under rustup.
Browse files Browse the repository at this point in the history
  • Loading branch information
ehuss committed Mar 31, 2023
1 parent 8205389 commit 18ed848
Show file tree
Hide file tree
Showing 2 changed files with 265 additions and 0 deletions.
1 change: 1 addition & 0 deletions tests/testsuite/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ mod rustdoc;
mod rustdoc_extern_html;
mod rustdocflags;
mod rustflags;
mod rustup;
mod search;
mod shell_quoting;
mod source_replacement;
Expand Down
264 changes: 264 additions & 0 deletions tests/testsuite/rustup.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,264 @@
//! Tests for Cargo's behavior under Rustup.
use cargo_test_support::paths::{home, root, CargoPathExt};
use cargo_test_support::{cargo_process, process, project};
use std::env;
use std::env::consts::EXE_EXTENSION;
use std::ffi::OsString;
use std::fs;
use std::path::{Path, PathBuf};

/// Helper to generate an executable.
fn make_exe(dest: &Path, name: &str, contents: &str, env: &[(&str, PathBuf)]) -> PathBuf {
let rs_name = format!("{name}.rs");
fs::write(
root().join(&rs_name),
&format!("fn main() {{ {contents} }}"),
)
.unwrap();
let mut pb = process("rustc");
env.iter().for_each(|(key, value)| {
pb.env(key, value);
});
pb.arg("--edition=2021")
.arg(root().join(&rs_name))
.exec()
.unwrap();
let exe = Path::new(name).with_extension(EXE_EXTENSION);
let output = dest.join(&exe);
fs::rename(root().join(&exe), &output).unwrap();
output
}

fn prepend_path(path: &Path) -> OsString {
let mut paths = vec![path.to_path_buf()];
paths.extend(env::split_paths(&env::var_os("PATH").unwrap_or_default()));
env::join_paths(paths).unwrap()
}

struct RustupEnvironment {
/// Path for ~/.cargo/bin
cargo_bin: PathBuf,
/// Path for ~/.rustup
rustup_home: PathBuf,
/// Path to the cargo executable in the toolchain directory
/// (~/.rustup/toolchain/test-toolchain/bin/cargo.exe).
cargo_toolchain_exe: PathBuf,
}

/// Creates an executable which prints a message and then runs the *real* rustc.
fn real_rustc_wrapper(bin_dir: &Path, message: &str) -> PathBuf {
let real_rustc = cargo_util::paths::resolve_executable("rustc".as_ref()).unwrap();
// The toolchain rustc needs to call the real rustc. In order to do that,
// it needs to restore or clear the RUSTUP environment variables so that
// if rustup is installed, it will call the correct rustc.
let rustup_toolchain_setup = match std::env::var_os("RUSTUP_TOOLCHAIN") {
Some(t) => format!(
".env(\"RUSTUP_TOOLCHAIN\", \"{}\")",
t.into_string().unwrap()
),
None => format!(".env_remove(\"RUSTUP_TOOLCHAIN\")"),
};
let mut env = vec![("CARGO_RUSTUP_TEST_real_rustc", real_rustc)];
let rustup_home_setup = match std::env::var_os("RUSTUP_HOME") {
Some(h) => {
env.push(("CARGO_RUSTUP_TEST_RUSTUP_HOME", h.into()));
format!(".env(\"RUSTUP_HOME\", env!(\"CARGO_RUSTUP_TEST_RUSTUP_HOME\"))")
}
None => format!(".env_remove(\"RUSTUP_HOME\")"),
};
make_exe(
bin_dir,
"rustc",
&format!(
r#"
eprintln!("{message}");
let r = std::process::Command::new(env!("CARGO_RUSTUP_TEST_real_rustc"))
.args(std::env::args_os().skip(1))
{rustup_toolchain_setup}
{rustup_home_setup}
.status();
std::process::exit(r.unwrap().code().unwrap_or(2));
"#
),
&env,
)
}

/// Creates a simulation of a rustup environment with `~/.cargo/bin` and
/// `~/.rustup` directories populated with some executables that simulate
/// rustup.
fn simulated_rustup_environment() -> RustupEnvironment {
// Set up ~/.rustup/toolchains/test-toolchain/bin with a custom rustc and cargo.
let rustup_home = home().join(".rustup");
let toolchain_bin = rustup_home
.join("toolchains")
.join("test-toolchain")
.join("bin");
toolchain_bin.mkdir_p();
let rustc_toolchain_exe = real_rustc_wrapper(&toolchain_bin, "real rustc running");
let cargo_toolchain_exe = make_exe(
&toolchain_bin,
"cargo",
r#"panic!("cargo toolchain should not be called");"#,
&[],
);

// Set up ~/.cargo/bin with a typical set of rustup proxies.
let cargo_bin = home().join(".cargo").join("bin");
cargo_bin.mkdir_p();

let rustc_proxy = make_exe(
&cargo_bin,
"rustc",
&format!(
r#"
match std::env::args().next().unwrap().as_ref() {{
"rustc" => {{}}
arg => panic!("proxy only supports rustc, got {{arg:?}}"),
}}
eprintln!("rustc proxy running");
let r = std::process::Command::new(env!("CARGO_RUSTUP_TEST_rustc_toolchain_exe"))
.args(std::env::args_os().skip(1))
.status();
std::process::exit(r.unwrap().code().unwrap_or(2));
"#
),
&[("CARGO_RUSTUP_TEST_rustc_toolchain_exe", rustc_toolchain_exe)],
);
fs::hard_link(
&rustc_proxy,
cargo_bin.join("cargo").with_extension(EXE_EXTENSION),
)
.unwrap();
fs::hard_link(
&rustc_proxy,
cargo_bin.join("rustup").with_extension(EXE_EXTENSION),
)
.unwrap();

RustupEnvironment {
cargo_bin,
rustup_home,
cargo_toolchain_exe,
}
}

#[cargo_test]
fn typical_rustup() {
// Test behavior under a typical rustup setup with a normal toolchain.
let RustupEnvironment {
cargo_bin,
rustup_home,
cargo_toolchain_exe,
} = simulated_rustup_environment();

// Set up a project and run a normal cargo build.
let p = project().file("src/lib.rs", "").build();
// The path is modified so that cargo will call `rustc` from
// `~/.cargo/bin/rustc to use our custom rustup proxies.
let path = prepend_path(&cargo_bin);
p.cargo("check")
.env("RUSTUP_TOOLCHAIN", "test-toolchain")
.env("RUSTUP_HOME", &rustup_home)
.env("PATH", &path)
.with_stderr(
"\
[CHECKING] foo v0.0.1 [..]
rustc proxy running
real rustc running
[FINISHED] [..]
",
)
.run();

// Do a similar test, but with a toolchain link that does not have cargo
// (which normally would do a fallback to nightly/beta/stable).
cargo_toolchain_exe.rm_rf();
p.build_dir().rm_rf();

p.cargo("check")
.env("RUSTUP_TOOLCHAIN", "test-toolchain")
.env("RUSTUP_HOME", &rustup_home)
.env("PATH", &path)
.with_stderr(
"\
[CHECKING] foo v0.0.1 [..]
rustc proxy running
real rustc running
[FINISHED] [..]
",
)
.run();
}

// This doesn't work on Windows because Cargo forces the PATH to contain the
// sysroot_libdir, which is actually `bin`, preventing the test from
// overriding the bin directory.
#[cargo_test(ignore_windows = "PATH can't be overridden on Windows")]
fn custom_calls_other_cargo() {
// Test behavior when a custom subcommand tries to manipulate PATH to use
// a different toolchain.
let RustupEnvironment {
cargo_bin,
rustup_home,
cargo_toolchain_exe: _,
} = simulated_rustup_environment();

// Create a directory with a custom toolchain (outside of the rustup universe).
let custom_bin = root().join("custom-bin");
custom_bin.mkdir_p();
// `cargo` points to the real cargo.
let cargo_exe = cargo_test_support::cargo_exe();
fs::hard_link(&cargo_exe, custom_bin.join(cargo_exe.file_name().unwrap())).unwrap();
// `rustc` executes the real rustc.
real_rustc_wrapper(&custom_bin, "custom toolchain rustc running");

// A project that cargo-custom will try to build.
let p = project().file("src/lib.rs", "").build();

// Create a custom cargo subcommand.
// This will modify PATH to a custom toolchain and call cargo from that.
make_exe(
&cargo_bin,
"cargo-custom",
r#"
use std::env;
use std::process::Command;
eprintln!("custom command running");
let mut paths = vec![std::path::PathBuf::from(env!("CARGO_RUSTUP_TEST_custom_bin"))];
paths.extend(env::split_paths(&env::var_os("PATH").unwrap_or_default()));
let path = env::join_paths(paths).unwrap();
let status = Command::new("cargo")
.arg("check")
.current_dir(env!("CARGO_RUSTUP_TEST_project_dir"))
.env("PATH", path)
.status()
.unwrap();
assert!(status.success());
"#,
&[
("CARGO_RUSTUP_TEST_custom_bin", custom_bin),
("CARGO_RUSTUP_TEST_project_dir", p.root()),
],
);

cargo_process("custom")
// Set these to simulate what would happen when running under rustup.
// We want to make sure that cargo-custom does not try to use the
// rustup proxies.
.env("RUSTUP_TOOLCHAIN", "test-toolchain")
.env("RUSTUP_HOME", &rustup_home)
.with_stderr(
"\
custom command running
[CHECKING] foo [..]
custom toolchain rustc running
[FINISHED] [..]
",
)
.run();
}

0 comments on commit 18ed848

Please sign in to comment.