From f05fc233fb9b1e1d14b729a51c541d86f004769e Mon Sep 17 00:00:00 2001 From: Francisco Gouveia Date: Mon, 12 Oct 2026 10:43:28 +0100 Subject: [PATCH 1/2] refactor(completions): move completion logic to its own file --- src/cli.rs | 1 + src/cli/completions.rs | 292 +++++++++++++++++++++++++++++++++++++++++ src/cli/rustup_mode.rs | 291 +--------------------------------------- 3 files changed, 297 insertions(+), 287 deletions(-) create mode 100644 src/cli/completions.rs diff --git a/src/cli.rs b/src/cli.rs index 31807fa261..d17977a9e3 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -2,6 +2,7 @@ #[macro_use] pub mod log; pub mod common; +mod completions; mod docs; pub mod errors; mod help; diff --git a/src/cli/completions.rs b/src/cli/completions.rs new file mode 100644 index 0000000000..02974ba878 --- /dev/null +++ b/src/cli/completions.rs @@ -0,0 +1,292 @@ +use std::iter; + +use clap::CommandFactory; +use clap_complete::{ArgValueCandidates, engine::CompletionCandidate}; + +use crate::{ + cli::rustup_mode::Rustup, + config::Cfg, + toolchain::{ChannelToolchain, ToolchainName, ToolchainNameOrPath}, +}; + +pub(super) fn rustup_command(cfg: &Cfg<'_>) -> clap::Command { + let mut toolchains = cfg.list_toolchains(true).unwrap_or_default(); + toolchains.sort(); + + // Collect available targets from the first installed + // distributable toolchain. Uses only on-disk manifest data + let available_targets = toolchains + .iter() + .find_map(|name| { + let ToolchainName::Channel(desc) = name else { + return None; + }; + let dist = ChannelToolchain::new(cfg, desc.clone()).ok()?; + let components = dist.components().ok()?; + Some( + components + .into_iter() + .filter_map(|c| { + (c.component.short_name() == "rust-std" && c.available) + .then(|| c.component.target.map(|t| t.to_string()))? + }) + .collect::>(), + ) + }) + .unwrap_or_default(); + + let active_channel_toolchain = match cfg.active_toolchain() { + Ok(Some((ToolchainNameOrPath::Named(ToolchainName::Channel(desc)), _))) => { + ChannelToolchain::new(cfg, desc).ok() + } + _ => None, + }; + let installed_targets = active_channel_toolchain + .and_then(|dist| dist.toolchain.installed_targets().ok()) + .unwrap_or_default(); + + Rustup::command() + .mut_arg("+toolchain", move |arg| { + arg.add(ArgValueCandidates::new(move || { + toolchains + .iter() + .map(|t| CompletionCandidate::new(format!("+{t}"))) + .collect() + })) + }) + .mut_subcommand("target", move |target_cmd| { + target_cmd + .mut_subcommand("add", move |add_cmd| { + add_cmd.mut_arg("target", move |arg| { + arg.add(ArgValueCandidates::new(move || { + // "all" is a special keyword that installs every available target + iter::once("all") + .chain(available_targets.iter().map(AsRef::as_ref)) + .map(CompletionCandidate::new) + .collect() + })) + }) + }) + .mut_subcommand("remove", move |remove_cmd| { + remove_cmd.mut_arg("target", move |arg| { + arg.add(ArgValueCandidates::new(move || { + installed_targets + .iter() + .map(|t| CompletionCandidate::new(t.to_string())) + .collect() + })) + }) + }) + }) +} + +#[cfg(all(test, feature = "test"))] +mod tests { + use std::{ + collections::{HashMap, HashSet}, + ffi::OsString, + fs, + }; + + use super::rustup_command; + use crate::{config::Cfg, process::TestProcess}; + + fn complete(cfg: &Cfg<'_>, args: &[&str], index: usize) -> Vec { + let mut command = rustup_command(cfg); + clap_complete::engine::complete( + &mut command, + args.iter().map(OsString::from).collect(), + index, + Some(&cfg.current_dir), + ) + .unwrap() + .into_iter() + .map(|candidate| candidate.get_value().to_string_lossy().into_owned()) + .collect() + } + + #[test] + fn dynamic_completion_distinguishes_toolchains_and_subcommands() { + let (rustup_home, process) = completion_cfg(); + fs::create_dir_all(rustup_home.path().join("toolchains/custom")).unwrap(); + let cfg = Cfg::from_env(rustup_home.path().to_owned(), true, false, &process).unwrap(); + + assert_eq!(complete(&cfg, &["rustup", "+cus"], 1), ["+custom"]); + + for (args, index) in [ + (&["rustup", "component", ""][..], 2), + (&["rustup", "+custom", "component", ""][..], 3), + ] { + let candidates = complete(&cfg, args, index); + let candidates = candidates + .iter() + .map(String::as_str) + .collect::>(); + let expected = HashSet::from(["list", "add", "remove"]); + assert!( + candidates.is_superset(&expected), + "missing component subcommands for {args:?}: {:?}", + expected.difference(&candidates).collect::>() + ); + let root_only = HashSet::from(["install", "toolchain", "show"]); + assert!( + candidates.is_disjoint(&root_only), + "unexpected root-only subcommands for {args:?}: {:?}", + candidates.intersection(&root_only).collect::>() + ); + } + } + + #[test] + fn dynamic_completion_target_add_offers_all_keyword() { + let (rustup_home, process) = completion_cfg(); + let cfg = Cfg::from_env(rustup_home.path().to_owned(), true, false, &process).unwrap(); + + let candidates = complete(&cfg, &["rustup", "target", "add", ""], 3); + assert!(candidates.contains(&"all".to_owned())); + } + + #[test] + fn dynamic_completion_target_add_prefix_filters_all() { + let (rustup_home, process) = completion_cfg(); + let cfg = Cfg::from_env(rustup_home.path().to_owned(), true, false, &process).unwrap(); + + // "al" prefix matches only "all" + let candidates = complete(&cfg, &["rustup", "target", "add", "al"], 3); + assert_eq!(candidates, ["all"]); + + let candidates = complete(&cfg, &["rustup", "target", "add", "x86"], 3); + assert!(!candidates.contains(&"all".to_owned())); + } + + #[test] + fn dynamic_completion_target_add_does_not_bleed_into_other_subcmds() { + let (rustup_home, process) = completion_cfg(); + let cfg = Cfg::from_env(rustup_home.path().to_owned(), true, false, &process).unwrap(); + + let candidates = complete(&cfg, &["rustup", "target", "remove", ""], 3); + assert!(!candidates.contains(&"all".to_owned())); + } + + #[test] + fn dynamic_completion_target_remove() { + let (rustup_home, process) = completion_cfg(); + let manifest_dir = rustup_home + .path() + .join("toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib"); + fs::create_dir_all(&manifest_dir).unwrap(); + fs::write( + manifest_dir.join("components"), + "rustc-x86_64-unknown-linux-gnu\n\ + rust-std-x86_64-unknown-linux-gnu\n\ + rust-std-x86_64-unknown-linux-musl\n", + ) + .unwrap(); + + let cfg = Cfg::from_env(rustup_home.path().to_owned(), true, false, &process).unwrap(); + cfg.set_default(Some(&"stable-x86_64-unknown-linux-gnu".parse().unwrap())) + .unwrap(); + + assert_eq!( + complete(&cfg, &["rustup", "target", "remove", "x86_64"], 3), + ["x86_64-unknown-linux-gnu", "x86_64-unknown-linux-musl"] + ); + } + + #[test] + fn dynamic_completion_target_add_offers_real_targets() { + let (rustup_home, process) = completion_cfg(); + + // The toolchain name must be parseable as an official ToolchainDesc + let toolchain_name = "stable-x86_64-unknown-linux-gnu"; + let toolchain_dir = rustup_home.path().join("toolchains").join(toolchain_name); + + // Manifest lives at: /lib/rustlib/multirust-channel-manifest.toml + let manifest_dir = toolchain_dir.join("lib/rustlib"); + fs::create_dir_all(&manifest_dir).unwrap(); + + // Minimal manifest with available rust-std targets + let manifest = r#" +manifest-version = "2" +date = "2025-01-01" +[pkg.rust] + version = "rustc 1.80.0 (fake)" + [pkg.rust.target.x86_64-unknown-linux-gnu] + available = true + url = "https://example.com" + hash = "deadbeef" + [[pkg.rust.target.x86_64-unknown-linux-gnu.components]] + pkg = "rustc" + target = "x86_64-unknown-linux-gnu" + [[pkg.rust.target.x86_64-unknown-linux-gnu.components]] + pkg = "rust-std" + target = "x86_64-unknown-linux-gnu" + [[pkg.rust.target.x86_64-unknown-linux-gnu.extensions]] + pkg = "rust-std" + target = "aarch64-unknown-linux-gnu" + [[pkg.rust.target.x86_64-unknown-linux-gnu.extensions]] + pkg = "rust-std" + target = "x86_64-unknown-linux-musl" +[pkg.rustc] + version = "rustc 1.80.0 (fake)" + [pkg.rustc.target.x86_64-unknown-linux-gnu] + available = true + url = "https://example.com" + hash = "deadbeef" +[pkg.rust-std] + version = "rustc 1.80.0 (fake)" + [pkg.rust-std.target.x86_64-unknown-linux-gnu] + available = true + url = "https://example.com" + hash = "deadbeef" + [pkg.rust-std.target.aarch64-unknown-linux-gnu] + available = true + url = "https://example.com" + hash = "deadbeef" + [pkg.rust-std.target.x86_64-unknown-linux-musl] + available = true + url = "https://example.com" + hash = "deadbeef" +"#; + fs::write( + manifest_dir.join("multirust-channel-manifest.toml"), + manifest, + ) + .unwrap(); + + let cfg = Cfg::from_env(rustup_home.path().to_owned(), true, false, &process).unwrap(); + + let candidates = complete(&cfg, &["rustup", "target", "add", ""], 3); + + assert!(candidates.contains(&"aarch64-unknown-linux-gnu".to_owned())); + assert!(candidates.contains(&"x86_64-unknown-linux-musl".to_owned())); + assert!(candidates.contains(&"x86_64-unknown-linux-gnu".to_owned())); + assert!(candidates.contains(&"all".to_owned())); + + let candidates = complete(&cfg, &["rustup", "target", "add", "aarch64"], 3); + assert_eq!(candidates, ["aarch64-unknown-linux-gnu"]); + } + + /// Creates a bare rustup home with no toolchains installed. + /// Used by completion tests to verify the completer is robust + /// and to set up a base environment for further customisation. + fn completion_cfg() -> (tempfile::TempDir, crate::process::Process) { + let rustup_home = tempfile::tempdir().unwrap(); + let vars = HashMap::from([ + ( + "RUSTUP_HOME".to_owned(), + rustup_home.path().display().to_string(), + ), + ( + "RUSTUP_OVERRIDE_UNIX_FALLBACK_SETTINGS".to_owned(), + rustup_home + .path() + .join("missing-settings.toml") + .display() + .to_string(), + ), + ]); + let process = TestProcess::new(rustup_home.path(), &["rustup"], vars, "").process; + (rustup_home, process) + } +} diff --git a/src/cli/rustup_mode.rs b/src/cli/rustup_mode.rs index 3a703bc0b9..a4d8e49d50 100644 --- a/src/cli/rustup_mode.rs +++ b/src/cli/rustup_mode.rs @@ -3,7 +3,6 @@ use std::{ env::consts::EXE_SUFFIX, fmt, io::{self, Write}, - iter, path::{Path, PathBuf}, process::ExitStatus, str::FromStr, @@ -19,7 +18,7 @@ use clap::{ builder::{PossibleValue, ValueHint}, }; use clap_cargo::style::{CONTEXT, ERROR, GOOD, HEADER, TRANSIENT, WARN}; -use clap_complete::{ArgValueCandidates, Shell, engine::CompletionCandidate}; +use clap_complete::Shell; use futures_util::stream::StreamExt; use indicatif::{MultiProgress, ProgressBar, ProgressStyle}; use serde::Serialize; @@ -30,7 +29,7 @@ use tracing_subscriber::{EnvFilter, Registry, reload::Handle}; use crate::{ cli::{ common::{self, PackageUpdate, report_error, update_console_filter}, - docs, + completions, docs, errors::CliError, help::{ check_help, completions_help, default_help, doc_help, install_help, @@ -88,7 +87,7 @@ fn handle_epipe(res: anyhow::Result) -> anyhow::Result { after_help = rustup_help(), styles = clap_cargo::style::CLAP_STYLING, )] -struct Rustup { +pub(super) struct Rustup { /// Set log level to 'DEBUG' if 'RUSTUP_LOG' is unset #[arg(short, long, conflicts_with = "quiet")] verbose: bool, @@ -695,7 +694,7 @@ pub async fn main( console_filter: Handle, ) -> anyhow::Result { let cfg = &mut Cfg::from_env(current_dir, true, false, process)?; - clap_complete::CompleteEnv::with_factory(|| completion_command(cfg)) + clap_complete::CompleteEnv::with_factory(|| completions::rustup_command(cfg)) .var("RUSTUP_COMPLETE") .bin("rustup") .complete(); @@ -899,77 +898,6 @@ pub async fn main( Ok(exit_code) } -fn completion_command(cfg: &Cfg<'_>) -> clap::Command { - let mut toolchains = cfg.list_toolchains(true).unwrap_or_default(); - toolchains.sort(); - - // Collect available targets from the first installed - // distributable toolchain. Uses only on-disk manifest data - let available_targets = toolchains - .iter() - .find_map(|name| { - let ToolchainName::Channel(desc) = name else { - return None; - }; - let dist = ChannelToolchain::new(cfg, desc.clone()).ok()?; - let components = dist.components().ok()?; - Some( - components - .into_iter() - .filter_map(|c| { - (c.component.short_name() == "rust-std" && c.available) - .then(|| c.component.target.map(|t| t.to_string()))? - }) - .collect::>(), - ) - }) - .unwrap_or_default(); - - let active_channel_toolchain = match cfg.active_toolchain() { - Ok(Some((ToolchainNameOrPath::Named(ToolchainName::Channel(desc)), _))) => { - ChannelToolchain::new(cfg, desc).ok() - } - _ => None, - }; - let installed_targets = active_channel_toolchain - .and_then(|dist| dist.toolchain.installed_targets().ok()) - .unwrap_or_default(); - - Rustup::command() - .mut_arg("+toolchain", move |arg| { - arg.add(ArgValueCandidates::new(move || { - toolchains - .iter() - .map(|t| CompletionCandidate::new(format!("+{t}"))) - .collect() - })) - }) - .mut_subcommand("target", move |target_cmd| { - target_cmd - .mut_subcommand("add", move |add_cmd| { - add_cmd.mut_arg("target", move |arg| { - arg.add(ArgValueCandidates::new(move || { - // "all" is a special keyword that installs every available target - iter::once("all") - .chain(available_targets.iter().map(AsRef::as_ref)) - .map(CompletionCandidate::new) - .collect() - })) - }) - }) - .mut_subcommand("remove", move |remove_cmd| { - remove_cmd.mut_arg("target", move |arg| { - arg.add(ArgValueCandidates::new(move || { - installed_targets - .iter() - .map(|t| CompletionCandidate::new(t.to_string())) - .collect() - })) - }) - }) - }) -} - async fn default_( cfg: &Cfg<'_>, toolchain: Option>, @@ -1993,214 +1921,3 @@ async fn display_version(cfg: &mut Cfg<'_>) -> anyhow::Result<()> { Ok(()) } - -#[cfg(all(test, feature = "test"))] -mod tests { - use std::{ - collections::{HashMap, HashSet}, - ffi::OsString, - fs, - }; - - use super::completion_command; - use crate::{config::Cfg, process::TestProcess}; - - fn complete(cfg: &Cfg<'_>, args: &[&str], index: usize) -> Vec { - let mut command = completion_command(cfg); - clap_complete::engine::complete( - &mut command, - args.iter().map(OsString::from).collect(), - index, - Some(&cfg.current_dir), - ) - .unwrap() - .into_iter() - .map(|candidate| candidate.get_value().to_string_lossy().into_owned()) - .collect() - } - - #[test] - fn dynamic_completion_distinguishes_toolchains_and_subcommands() { - let (rustup_home, process) = completion_cfg(); - fs::create_dir_all(rustup_home.path().join("toolchains/custom")).unwrap(); - let cfg = Cfg::from_env(rustup_home.path().to_owned(), true, false, &process).unwrap(); - - assert_eq!(complete(&cfg, &["rustup", "+cus"], 1), ["+custom"]); - - for (args, index) in [ - (&["rustup", "component", ""][..], 2), - (&["rustup", "+custom", "component", ""][..], 3), - ] { - let candidates = complete(&cfg, args, index); - let candidates = candidates - .iter() - .map(String::as_str) - .collect::>(); - let expected = HashSet::from(["list", "add", "remove"]); - assert!( - candidates.is_superset(&expected), - "missing component subcommands for {args:?}: {:?}", - expected.difference(&candidates).collect::>() - ); - let root_only = HashSet::from(["install", "toolchain", "show"]); - assert!( - candidates.is_disjoint(&root_only), - "unexpected root-only subcommands for {args:?}: {:?}", - candidates.intersection(&root_only).collect::>() - ); - } - } - - #[test] - fn dynamic_completion_target_add_offers_all_keyword() { - let (rustup_home, process) = completion_cfg(); - let cfg = Cfg::from_env(rustup_home.path().to_owned(), true, false, &process).unwrap(); - - let candidates = complete(&cfg, &["rustup", "target", "add", ""], 3); - assert!(candidates.contains(&"all".to_owned())); - } - - #[test] - fn dynamic_completion_target_add_prefix_filters_all() { - let (rustup_home, process) = completion_cfg(); - let cfg = Cfg::from_env(rustup_home.path().to_owned(), true, false, &process).unwrap(); - - // "al" prefix matches only "all" - let candidates = complete(&cfg, &["rustup", "target", "add", "al"], 3); - assert_eq!(candidates, ["all"]); - - let candidates = complete(&cfg, &["rustup", "target", "add", "x86"], 3); - assert!(!candidates.contains(&"all".to_owned())); - } - - #[test] - fn dynamic_completion_target_add_does_not_bleed_into_other_subcmds() { - let (rustup_home, process) = completion_cfg(); - let cfg = Cfg::from_env(rustup_home.path().to_owned(), true, false, &process).unwrap(); - - let candidates = complete(&cfg, &["rustup", "target", "remove", ""], 3); - assert!(!candidates.contains(&"all".to_owned())); - } - - #[test] - fn dynamic_completion_target_remove() { - let (rustup_home, process) = completion_cfg(); - let manifest_dir = rustup_home - .path() - .join("toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib"); - fs::create_dir_all(&manifest_dir).unwrap(); - fs::write( - manifest_dir.join("components"), - "rustc-x86_64-unknown-linux-gnu\n\ - rust-std-x86_64-unknown-linux-gnu\n\ - rust-std-x86_64-unknown-linux-musl\n", - ) - .unwrap(); - - let cfg = Cfg::from_env(rustup_home.path().to_owned(), true, false, &process).unwrap(); - cfg.set_default(Some(&"stable-x86_64-unknown-linux-gnu".parse().unwrap())) - .unwrap(); - - assert_eq!( - complete(&cfg, &["rustup", "target", "remove", "x86_64"], 3), - ["x86_64-unknown-linux-gnu", "x86_64-unknown-linux-musl"] - ); - } - - #[test] - fn dynamic_completion_target_add_offers_real_targets() { - let (rustup_home, process) = completion_cfg(); - - // The toolchain name must be parseable as an official ToolchainDesc - let toolchain_name = "stable-x86_64-unknown-linux-gnu"; - let toolchain_dir = rustup_home.path().join("toolchains").join(toolchain_name); - - // Manifest lives at: /lib/rustlib/multirust-channel-manifest.toml - let manifest_dir = toolchain_dir.join("lib/rustlib"); - fs::create_dir_all(&manifest_dir).unwrap(); - - // Minimal manifest with available rust-std targets - let manifest = r#" -manifest-version = "2" -date = "2025-01-01" -[pkg.rust] - version = "rustc 1.80.0 (fake)" - [pkg.rust.target.x86_64-unknown-linux-gnu] - available = true - url = "https://example.com" - hash = "deadbeef" - [[pkg.rust.target.x86_64-unknown-linux-gnu.components]] - pkg = "rustc" - target = "x86_64-unknown-linux-gnu" - [[pkg.rust.target.x86_64-unknown-linux-gnu.components]] - pkg = "rust-std" - target = "x86_64-unknown-linux-gnu" - [[pkg.rust.target.x86_64-unknown-linux-gnu.extensions]] - pkg = "rust-std" - target = "aarch64-unknown-linux-gnu" - [[pkg.rust.target.x86_64-unknown-linux-gnu.extensions]] - pkg = "rust-std" - target = "x86_64-unknown-linux-musl" -[pkg.rustc] - version = "rustc 1.80.0 (fake)" - [pkg.rustc.target.x86_64-unknown-linux-gnu] - available = true - url = "https://example.com" - hash = "deadbeef" -[pkg.rust-std] - version = "rustc 1.80.0 (fake)" - [pkg.rust-std.target.x86_64-unknown-linux-gnu] - available = true - url = "https://example.com" - hash = "deadbeef" - [pkg.rust-std.target.aarch64-unknown-linux-gnu] - available = true - url = "https://example.com" - hash = "deadbeef" - [pkg.rust-std.target.x86_64-unknown-linux-musl] - available = true - url = "https://example.com" - hash = "deadbeef" -"#; - fs::write( - manifest_dir.join("multirust-channel-manifest.toml"), - manifest, - ) - .unwrap(); - - let cfg = Cfg::from_env(rustup_home.path().to_owned(), true, false, &process).unwrap(); - - let candidates = complete(&cfg, &["rustup", "target", "add", ""], 3); - - assert!(candidates.contains(&"aarch64-unknown-linux-gnu".to_owned())); - assert!(candidates.contains(&"x86_64-unknown-linux-musl".to_owned())); - assert!(candidates.contains(&"x86_64-unknown-linux-gnu".to_owned())); - assert!(candidates.contains(&"all".to_owned())); - - let candidates = complete(&cfg, &["rustup", "target", "add", "aarch64"], 3); - assert_eq!(candidates, ["aarch64-unknown-linux-gnu"]); - } - - /// Creates a bare rustup home with no toolchains installed. - /// Used by completion tests to verify the completer is robust - /// and to set up a base environment for further customisation. - fn completion_cfg() -> (tempfile::TempDir, crate::process::Process) { - let rustup_home = tempfile::tempdir().unwrap(); - let vars = HashMap::from([ - ( - "RUSTUP_HOME".to_owned(), - rustup_home.path().display().to_string(), - ), - ( - "RUSTUP_OVERRIDE_UNIX_FALLBACK_SETTINGS".to_owned(), - rustup_home - .path() - .join("missing-settings.toml") - .display() - .to_string(), - ), - ]); - let process = TestProcess::new(rustup_home.path(), &["rustup"], vars, "").process; - (rustup_home, process) - } -} From 1c7607e13d8e92254ff364d7d5fc75ead02849e7 Mon Sep 17 00:00:00 2001 From: Francisco Gouveia Date: Mon, 12 Oct 2026 11:02:42 +0100 Subject: [PATCH 2/2] test(completions): extract toolchain name and manifest into constants Although currently the `MANIFEST` constant is only used once, it is preferable to have it extracted as it will be used in the tests of later sub-commands' completions (e.g., `rustup component add/remove`). --- src/cli/completions.rs | 100 +++++++++++++++++++---------------------- 1 file changed, 46 insertions(+), 54 deletions(-) diff --git a/src/cli/completions.rs b/src/cli/completions.rs index 02974ba878..66d9d53243 100644 --- a/src/cli/completions.rs +++ b/src/cli/completions.rs @@ -171,9 +171,7 @@ mod tests { #[test] fn dynamic_completion_target_remove() { let (rustup_home, process) = completion_cfg(); - let manifest_dir = rustup_home - .path() - .join("toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib"); + let manifest_dir = rustup_home.path().join(MANIFEST_DIR); fs::create_dir_all(&manifest_dir).unwrap(); fs::write( manifest_dir.join("components"), @@ -184,8 +182,7 @@ mod tests { .unwrap(); let cfg = Cfg::from_env(rustup_home.path().to_owned(), true, false, &process).unwrap(); - cfg.set_default(Some(&"stable-x86_64-unknown-linux-gnu".parse().unwrap())) - .unwrap(); + cfg.set_default(Some(&TOOLCHAIN.parse().unwrap())).unwrap(); assert_eq!( complete(&cfg, &["rustup", "target", "remove", "x86_64"], 3), @@ -196,17 +193,53 @@ mod tests { #[test] fn dynamic_completion_target_add_offers_real_targets() { let (rustup_home, process) = completion_cfg(); + let manifest_dir = rustup_home.path().join(MANIFEST_DIR); + fs::create_dir_all(&manifest_dir).unwrap(); + fs::write( + manifest_dir.join("multirust-channel-manifest.toml"), + MANIFEST, + ) + .unwrap(); - // The toolchain name must be parseable as an official ToolchainDesc - let toolchain_name = "stable-x86_64-unknown-linux-gnu"; - let toolchain_dir = rustup_home.path().join("toolchains").join(toolchain_name); + let cfg = Cfg::from_env(rustup_home.path().to_owned(), true, false, &process).unwrap(); - // Manifest lives at: /lib/rustlib/multirust-channel-manifest.toml - let manifest_dir = toolchain_dir.join("lib/rustlib"); - fs::create_dir_all(&manifest_dir).unwrap(); + let candidates = complete(&cfg, &["rustup", "target", "add", ""], 3); + + assert!(candidates.contains(&"aarch64-unknown-linux-gnu".to_owned())); + assert!(candidates.contains(&"x86_64-unknown-linux-musl".to_owned())); + assert!(candidates.contains(&"x86_64-unknown-linux-gnu".to_owned())); + assert!(candidates.contains(&"all".to_owned())); + + let candidates = complete(&cfg, &["rustup", "target", "add", "aarch64"], 3); + assert_eq!(candidates, ["aarch64-unknown-linux-gnu"]); + } + + /// Creates a bare rustup home with no toolchains installed. + /// Used by completion tests to verify the completer is robust + /// and to set up a base environment for further customisation. + fn completion_cfg() -> (tempfile::TempDir, crate::process::Process) { + let rustup_home = tempfile::tempdir().unwrap(); + let vars = HashMap::from([ + ( + "RUSTUP_HOME".to_owned(), + rustup_home.path().display().to_string(), + ), + ( + "RUSTUP_OVERRIDE_UNIX_FALLBACK_SETTINGS".to_owned(), + rustup_home + .path() + .join("missing-settings.toml") + .display() + .to_string(), + ), + ]); + let process = TestProcess::new(rustup_home.path(), &["rustup"], vars, "").process; + (rustup_home, process) + } - // Minimal manifest with available rust-std targets - let manifest = r#" + const TOOLCHAIN: &str = "stable-x86_64-unknown-linux-gnu"; + const MANIFEST_DIR: &str = "toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib"; + const MANIFEST: &str = r#" manifest-version = "2" date = "2025-01-01" [pkg.rust] @@ -248,45 +281,4 @@ date = "2025-01-01" url = "https://example.com" hash = "deadbeef" "#; - fs::write( - manifest_dir.join("multirust-channel-manifest.toml"), - manifest, - ) - .unwrap(); - - let cfg = Cfg::from_env(rustup_home.path().to_owned(), true, false, &process).unwrap(); - - let candidates = complete(&cfg, &["rustup", "target", "add", ""], 3); - - assert!(candidates.contains(&"aarch64-unknown-linux-gnu".to_owned())); - assert!(candidates.contains(&"x86_64-unknown-linux-musl".to_owned())); - assert!(candidates.contains(&"x86_64-unknown-linux-gnu".to_owned())); - assert!(candidates.contains(&"all".to_owned())); - - let candidates = complete(&cfg, &["rustup", "target", "add", "aarch64"], 3); - assert_eq!(candidates, ["aarch64-unknown-linux-gnu"]); - } - - /// Creates a bare rustup home with no toolchains installed. - /// Used by completion tests to verify the completer is robust - /// and to set up a base environment for further customisation. - fn completion_cfg() -> (tempfile::TempDir, crate::process::Process) { - let rustup_home = tempfile::tempdir().unwrap(); - let vars = HashMap::from([ - ( - "RUSTUP_HOME".to_owned(), - rustup_home.path().display().to_string(), - ), - ( - "RUSTUP_OVERRIDE_UNIX_FALLBACK_SETTINGS".to_owned(), - rustup_home - .path() - .join("missing-settings.toml") - .display() - .to_string(), - ), - ]); - let process = TestProcess::new(rustup_home.path(), &["rustup"], vars, "").process; - (rustup_home, process) - } }