//! Tests for custom cargo commands and other global command features. use std::env; use std::fs; use std::io::Read; use std::path::{Path, PathBuf}; use std::process::Stdio; use std::str; use cargo_test_support::basic_manifest; use cargo_test_support::paths::CargoPathExt; use cargo_test_support::prelude::*; use cargo_test_support::registry::Package; use cargo_test_support::str; use cargo_test_support::tools::echo_subcommand; use cargo_test_support::{ basic_bin_manifest, cargo_exe, cargo_process, paths, project, project_in_home, }; use cargo_util::paths::join_paths; fn path() -> Vec { env::split_paths(&env::var_os("PATH").unwrap_or_default()).collect() } #[cargo_test] fn list_commands_with_descriptions() { let p = project().build(); p.cargo("--list") .with_stdout_data( "\ ... b alias: build ... build Compile a local package and all of its dependencies ... c alias: check ... r alias: run ... read-manifest Print a JSON representation of a Cargo.toml manifest. ... t alias: test ... ", ) .run(); } #[cargo_test] fn list_custom_aliases_with_descriptions() { let p = project_in_home("proj") .file( &paths::home().join(".cargo").join("config"), r#" [alias] myaliasstr = "foo --bar" myaliasvec = ["foo", "--bar"] "#, ) .build(); p.cargo("--list") .with_stdout_data(str![[r#" ... myaliasstr alias: foo --bar myaliasvec alias: foo --bar ... "#]]) .run(); } #[cargo_test] fn list_dedupe() { let p = project() .executable(Path::new("path-test-1").join("cargo-dupe"), "") .executable(Path::new("path-test-2").join("cargo-dupe"), "") .build(); let mut path = path(); path.push(p.root().join("path-test-1")); path.push(p.root().join("path-test-2")); let path = env::join_paths(path.iter()).unwrap(); p.cargo("--list") .env("PATH", &path) .with_stdout_data(str![[r#" ... dupe ... "#]]) .run(); } #[cargo_test] fn list_command_looks_at_path() { let proj = project() .executable(Path::new("path-test").join("cargo-1"), "") .build(); let mut path = path(); path.push(proj.root().join("path-test")); let path = env::join_paths(path.iter()).unwrap(); let output = cargo_process("-v --list") .env("PATH", &path) .exec_with_output() .unwrap(); let output = str::from_utf8(&output.stdout).unwrap(); assert!( output.contains("\n 1 "), "missing 1: {}", output ); } #[cfg(windows)] #[cargo_test] fn list_command_looks_at_path_case_mismatch() { let proj = project() .executable(Path::new("path-test").join("cargo-1"), "") .build(); let mut path = path(); path.push(proj.root().join("path-test")); let path = env::join_paths(path.iter()).unwrap(); // See issue #11814: Environment variable names are case-insensitive on Windows. // We need to check that having "Path" instead of "PATH" is okay. let output = cargo_process("-v --list") .env("Path", &path) .env_remove("PATH") .exec_with_output() .unwrap(); let output = str::from_utf8(&output.stdout).unwrap(); assert!( output.contains("\n 1 "), "missing 1: {}", output ); } #[allow(deprecated)] #[cargo_test] fn list_command_handles_known_external_commands() { let p = project() .executable(Path::new("path-test").join("cargo-fmt"), "") .build(); let fmt_desc = " fmt Formats all bin and lib files of the current crate using rustfmt."; // Without path - fmt isn't there p.cargo("--list") .env("PATH", "") .with_stdout_does_not_contain(fmt_desc) .run(); // With path - fmt is there with known description let mut path = path(); path.push(p.root().join("path-test")); let path = env::join_paths(path.iter()).unwrap(); p.cargo("--list") .env("PATH", &path) .with_stdout_data(str![[r#" ... fmt Formats all bin and lib files of the current crate using rustfmt. ..."#]]) .run(); } #[cargo_test] fn list_command_resolves_symlinks() { let proj = project() .symlink(cargo_exe(), Path::new("path-test").join("cargo-2")) .build(); let mut path = path(); path.push(proj.root().join("path-test")); let path = env::join_paths(path.iter()).unwrap(); let output = cargo_process("-v --list") .env("PATH", &path) .exec_with_output() .unwrap(); let output = str::from_utf8(&output.stdout).unwrap(); assert!( output.contains("\n 2 "), "missing 2: {}", output ); } #[cargo_test] fn find_closest_capital_c_to_c() { cargo_process("C") .with_status(101) .with_stderr_data(str![[r#" [ERROR] no such command: `C` Did you mean `c`? View all installed commands with `cargo --list` Find a package to install `C` with `cargo search cargo-C` "#]]) .run(); } #[cargo_test] fn find_closest_capital_b_to_b() { cargo_process("B") .with_status(101) .with_stderr_data(str![[r#" [ERROR] no such command: `B` Did you mean `b`? View all installed commands with `cargo --list` Find a package to install `B` with `cargo search cargo-B` "#]]) .run(); } #[cargo_test] fn find_closest_biuld_to_build() { cargo_process("biuld") .with_status(101) .with_stderr_data(str![[r#" [ERROR] no such command: `biuld` Did you mean `build`? View all installed commands with `cargo --list` Find a package to install `biuld` with `cargo search cargo-biuld` "#]]) .run(); // But, if we actually have `biuld`, it must work! // https://github.com/rust-lang/cargo/issues/5201 Package::new("cargo-biuld", "1.0.0") .file( "src/main.rs", r#" fn main() { println!("Similar, but not identical to, build"); } "#, ) .publish(); cargo_process("install cargo-biuld").run(); cargo_process("biuld") .with_stdout_data(str![[r#" Similar, but not identical to, build "#]]) .run(); cargo_process("--list") .with_stdout_data(str![[r#" ... biuld ... build Compile a local package and all of its dependencies ..."#]]) .run(); } #[cargo_test] fn find_closest_alias() { let root = paths::root(); let my_home = root.join("my_home"); fs::create_dir(&my_home).unwrap(); fs::write( &my_home.join("config.toml"), r#" [alias] myalias = "build" "#, ) .unwrap(); cargo_process("myalais") .env("CARGO_HOME", &my_home) .with_status(101) .with_stderr_data(str![[r#" [ERROR] no such command: `myalais` Did you mean `myalias`? View all installed commands with `cargo --list` Find a package to install `myalais` with `cargo search cargo-myalais` "#]]) .run(); // But, if no alias is defined, it must not suggest one! cargo_process("myalais") .with_status(101) .with_stderr_data(str![[r#" [ERROR] no such command: `myalais` View all installed commands with `cargo --list` Find a package to install `myalais` with `cargo search cargo-myalais` "#]]) .run(); } // If a subcommand is more than an edit distance of 3 away, we don't make a suggestion. #[cargo_test] fn find_closest_dont_correct_nonsense() { cargo_process("there-is-no-way-that-there-is-a-command-close-to-this") .cwd(&paths::root()) .with_status(101) .with_stderr_data(str![[r#" [ERROR] no such command: `there-is-no-way-that-there-is-a-command-close-to-this` View all installed commands with `cargo --list` Find a package to install `there-is-no-way-that-there-is-a-command-close-to-this` with `cargo search cargo-there-is-no-way-that-there-is-a-command-close-to-this` "#]]) .run(); } #[cargo_test] fn displays_subcommand_on_error() { cargo_process("invalid-command") .with_status(101) .with_stderr_data(str![[r#" [ERROR] no such command: `invalid-command` View all installed commands with `cargo --list` Find a package to install `invalid-command` with `cargo search cargo-invalid-command` "#]]) .run(); } #[cargo_test] fn override_cargo_home() { let root = paths::root(); let my_home = root.join("my_home"); fs::create_dir(&my_home).unwrap(); fs::write( &my_home.join("config"), r#" [cargo-new] vcs = "none" "#, ) .unwrap(); cargo_process("new foo").env("CARGO_HOME", &my_home).run(); assert!(!paths::root().join("foo/.git").is_dir()); cargo_process("new foo2").run(); assert!(paths::root().join("foo2/.git").is_dir()); } #[allow(deprecated)] #[cargo_test] fn cargo_subcommand_env() { let src = format!( r#" use std::env; fn main() {{ println!("{{}}", env::var("{}").unwrap()); }} "#, cargo::CARGO_ENV ); let p = project() .at("cargo-envtest") .file("Cargo.toml", &basic_bin_manifest("cargo-envtest")) .file("src/main.rs", &src) .build(); let target_dir = p.target_debug_dir(); p.cargo("build").run(); assert!(p.bin("cargo-envtest").is_file()); let cargo = cargo_exe().canonicalize().unwrap(); let mut path = path(); path.push(target_dir.clone()); let path = env::join_paths(path.iter()).unwrap(); cargo_process("envtest") .env("PATH", &path) .with_stdout(cargo.to_str().unwrap()) .run(); // Check that subcommands inherit an overridden $CARGO let envtest_bin = target_dir .join("cargo-envtest") .with_extension(std::env::consts::EXE_EXTENSION) .canonicalize() .unwrap(); let envtest_bin = envtest_bin.to_str().unwrap(); cargo_process("envtest") .env("PATH", &path) .env(cargo::CARGO_ENV, &envtest_bin) .with_stdout(envtest_bin) .run(); } #[cargo_test] fn cargo_cmd_bins_vs_explicit_path() { // Set up `cargo-foo` binary in two places: inside `$HOME/.cargo/bin` and outside of it // // Return paths to both places fn set_up_cargo_foo() -> (PathBuf, PathBuf) { let p = project() .at("cargo-foo") .file("Cargo.toml", &basic_manifest("cargo-foo", "1.0.0")) .file( "src/bin/cargo-foo.rs", r#"fn main() { println!("INSIDE"); }"#, ) .file( "src/bin/cargo-foo2.rs", r#"fn main() { println!("OUTSIDE"); }"#, ) .build(); p.cargo("build").run(); let cargo_bin_dir = paths::home().join(".cargo/bin"); cargo_bin_dir.mkdir_p(); let root_bin_dir = paths::root().join("bin"); root_bin_dir.mkdir_p(); let exe_name = format!("cargo-foo{}", env::consts::EXE_SUFFIX); fs::rename(p.bin("cargo-foo"), cargo_bin_dir.join(&exe_name)).unwrap(); fs::rename(p.bin("cargo-foo2"), root_bin_dir.join(&exe_name)).unwrap(); (root_bin_dir, cargo_bin_dir) } let (outside_dir, inside_dir) = set_up_cargo_foo(); // If `$CARGO_HOME/bin` is not in a path, prefer it over anything in `$PATH`. // // This is the historical behavior we don't want to break. cargo_process("foo") .with_stdout_data(str![[r#" INSIDE "#]]) .run(); // When `$CARGO_HOME/bin` is in the `$PATH` // use only `$PATH` so the user-defined ordering is respected. { cargo_process("foo") .env( "PATH", join_paths(&[&inside_dir, &outside_dir], "PATH").unwrap(), ) .with_stdout_data(str![[r#" INSIDE "#]]) .run(); cargo_process("foo") // Note: trailing slash .env( "PATH", join_paths(&[inside_dir.join(""), outside_dir.join("")], "PATH").unwrap(), ) .with_stdout_data(str![[r#" INSIDE "#]]) .run(); cargo_process("foo") .env( "PATH", join_paths(&[&outside_dir, &inside_dir], "PATH").unwrap(), ) .with_stdout_data(str![[r#" OUTSIDE "#]]) .run(); cargo_process("foo") // Note: trailing slash .env( "PATH", join_paths(&[outside_dir.join(""), inside_dir.join("")], "PATH").unwrap(), ) .with_stdout_data(str![[r#" OUTSIDE "#]]) .run(); } } #[cargo_test] fn cargo_subcommand_args() { let p = echo_subcommand(); let cargo_foo_bin = p.bin("cargo-echo"); assert!(cargo_foo_bin.is_file()); let mut path = path(); path.push(p.target_debug_dir()); let path = env::join_paths(path.iter()).unwrap(); cargo_process("echo bar -v --help") .env("PATH", &path) .with_stdout_data(str![[r#" echo bar -v --help "#]]) .run(); } #[cargo_test] fn explain() { cargo_process("--explain E0001") .with_stdout_data(str![[r#" ... This error suggests that the expression arm corresponding to the noted pattern[..] ... "#]]) .run(); } #[cargo_test] fn closed_output_ok() { // Checks that closed output doesn't cause an error. let mut p = cargo_process("--list").build_command(); p.stdout(Stdio::piped()).stderr(Stdio::piped()); let mut child = p.spawn().unwrap(); // Close stdout drop(child.stdout.take()); // Read stderr let mut s = String::new(); child .stderr .as_mut() .unwrap() .read_to_string(&mut s) .unwrap(); let status = child.wait().unwrap(); assert!(status.success()); assert!(s.is_empty(), "{}", s); } #[cargo_test] fn subcommand_leading_plus_output_contains() { cargo_process("+nightly") .with_status(101) .with_stderr_data(str![[r#" [ERROR] no such command: `+nightly` Cargo does not handle `+toolchain` directives. Did you mean to invoke `cargo` through `rustup` instead? "#]]) .run(); } #[cargo_test] fn full_did_you_mean() { cargo_process("bluid") .with_status(101) .with_stderr_data(str![[r#" [ERROR] no such command: `bluid` Did you mean `build`? View all installed commands with `cargo --list` Find a package to install `bluid` with `cargo search cargo-bluid` "#]]) .run(); }