cargo/tests/testsuite/cargo_command.rs
Ed Page ec3ed20d2a fix(cli): Suggest cargo-search on bad commands
This is a low-tech solution alternative to the options proposed in #4682
- Search `[[bin]]`s within all packages in the registry (which aren't
  tracked atm), suggesting to `cargo install` what is found
- Check if `cargo-<cmd>` is in the registry, suggesting `cargo install
  if it is

By suggesting `cargo search`, we are giving them a tool so they can
verify if the package is what they want (a `cargo info` would help as a
next step).

Is is needed?
- New users might not know of `cargo search` but they can search on
  crates.io
- New users might not be aware of the `cargo-<cmd>` naming pattern

Seems like this can still offer some benefit

Fixes #4682
2023-10-17 14:46:30 -05:00

541 lines
14 KiB
Rust

//! 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::registry::Package;
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<PathBuf> {
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_contains(
" build Compile a local package and all of its dependencies",
)
// Assert that `read-manifest` prints the right one-line description followed by another
// command, indented.
.with_stdout_contains(
" read-manifest Print a JSON representation of a Cargo.toml manifest.",
)
.run();
}
#[cargo_test]
fn list_builtin_aliases_with_descriptions() {
let p = project().build();
p.cargo("--list")
.with_stdout_contains(" b alias: build")
.with_stdout_contains(" c alias: check")
.with_stdout_contains(" r alias: run")
.with_stdout_contains(" 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_contains(" myaliasstr alias: foo --bar")
.with_stdout_contains(" 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_contains_n(" dupe", 1)
.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
);
}
#[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_contains(fmt_desc)
.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_contains(
"\
error: no such command: `C`
<tab>Did you mean `c`?
",
)
.run();
}
#[cargo_test]
fn find_closest_capital_b_to_b() {
cargo_process("B")
.with_status(101)
.with_stderr_contains(
"\
error: no such command: `B`
<tab>Did you mean `b`?
",
)
.run();
}
#[cargo_test]
fn find_closest_biuld_to_build() {
cargo_process("biuld")
.with_status(101)
.with_stderr_contains(
"\
error: no such command: `biuld`
<tab>Did you mean `build`?
",
)
.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("Similar, but not identical to, build\n")
.run();
cargo_process("--list")
.with_stdout_contains(
" build Compile a local package and all of its dependencies\n",
)
.with_stdout_contains(" biuld\n")
.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"),
r#"
[alias]
myalias = "build"
"#,
)
.unwrap();
cargo_process("myalais")
.env("CARGO_HOME", &my_home)
.with_status(101)
.with_stderr_contains(
"\
error: no such command: `myalais`
<tab>Did you mean `myalias`?
",
)
.run();
// But, if no alias is defined, it must not suggest one!
cargo_process("myalais")
.with_status(101)
.with_stderr_contains(
"\
error: no such command: `myalais`
",
)
.with_stderr_does_not_contain(
"\
<tab>Did you mean `myalias`?
",
)
.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(
"\
[ERROR] no such command: `there-is-no-way-that-there-is-a-command-close-to-this`
<tab>View all installed commands with `cargo --list`
<tab>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(
"\
[ERROR] no such command: `invalid-command`
<tab>View all installed commands with `cargo --list`
<tab>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());
}
#[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_contains("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_contains("INSIDE")
.run();
cargo_process("foo")
// Note: trailing slash
.env(
"PATH",
join_paths(&[inside_dir.join(""), outside_dir.join("")], "PATH").unwrap(),
)
.with_stdout_contains("INSIDE")
.run();
cargo_process("foo")
.env(
"PATH",
join_paths(&[&outside_dir, &inside_dir], "PATH").unwrap(),
)
.with_stdout_contains("OUTSIDE")
.run();
cargo_process("foo")
// Note: trailing slash
.env(
"PATH",
join_paths(&[outside_dir.join(""), inside_dir.join("")], "PATH").unwrap(),
)
.with_stdout_contains("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("echo bar -v --help")
.run();
}
#[cargo_test]
fn explain() {
cargo_process("--explain E0001")
.with_stdout_contains(
"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(
"\
error: no such command: `+nightly`
<tab>Cargo does not handle `+toolchain` directives.
<tab>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(
"\
error: no such command: `bluid`
<tab>Did you mean `build`?
<tab>View all installed commands with `cargo --list`
<tab>Find a package to install `bluid` with `cargo search cargo-bluid`
",
)
.run();
}