From 07d1f7ce4f77d898f93ed7261b5cb4bf18c73dd9 Mon Sep 17 00:00:00 2001 From: rami3l Date: Sun, 11 Oct 2026 12:00:41 +0200 Subject: [PATCH 1/6] refactor(test): simplify match arms in `ConstState::new()` --- src/test/clitools.rs | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/src/test/clitools.rs b/src/test/clitools.rs index 190b4b7a1d..e69419dd05 100644 --- a/src/test/clitools.rs +++ b/src/test/clitools.rs @@ -723,26 +723,26 @@ impl ConstState { Self { const_dist_dir, scenarios: enum_map! { - Scenario::ArchivesV1 => RwLock::new(None), - Scenario::ArchivesV2 => RwLock::new(None), - Scenario::ArchivesV2_2015_01_01 => RwLock::new(None), - Scenario::ArchivesV2TwoVersions => RwLock::new(None), - Scenario::BetaTag => RwLock::new(None), - Scenario::Empty => RwLock::new(None), - Scenario::Full => RwLock::new(None), - Scenario::HostGoesMissingBefore => RwLock::new(None), - Scenario::HostGoesMissingAfter => RwLock::new(None), - Scenario::MissingComponent => RwLock::new(None), - Scenario::MissingComponentMulti => RwLock::new(None), - Scenario::MissingNightly => RwLock::new(None), - Scenario::MultiHost => RwLock::new(None), - Scenario::None => RwLock::new(None), - Scenario::RemovedRls => RwLock::new(None), - Scenario::SimpleV1 => RwLock::new(None), - Scenario::SimpleV2 => RwLock::new(None), - Scenario::Unavailable => RwLock::new(None), - Scenario::UnavailableRls => RwLock::new(None), - Scenario::RecentStable => RwLock::new(None), + Scenario::ArchivesV1 + | Scenario::ArchivesV2 + | Scenario::ArchivesV2TwoVersions + | Scenario::ArchivesV2_2015_01_01 + | Scenario::BetaTag + | Scenario::Empty + | Scenario::Full + | Scenario::HostGoesMissingAfter + | Scenario::HostGoesMissingBefore + | Scenario::MissingComponent + | Scenario::MissingComponentMulti + | Scenario::MissingNightly + | Scenario::MultiHost + | Scenario::None + | Scenario::RecentStable + | Scenario::RemovedRls + | Scenario::SimpleV1 + | Scenario::SimpleV2 + | Scenario::Unavailable + | Scenario::UnavailableRls => RwLock::new(None), }, } } From e63b09a36070ccdef4c81ddde7af9a94fbbf1acb Mon Sep 17 00:00:00 2001 From: rami3l Date: Sun, 11 Oct 2026 12:03:20 +0200 Subject: [PATCH 2/6] refactor(test): take `Release` in `MockChannel::new()` --- src/test/dist.rs | 49 +++++++++++++++++------------------------------- 1 file changed, 17 insertions(+), 32 deletions(-) diff --git a/src/test/dist.rs b/src/test/dist.rs index 930f38c744..b8ce027826 100644 --- a/src/test/dist.rs +++ b/src/test/dist.rs @@ -141,27 +141,11 @@ impl Release { pub(super) fn mock(&self) -> MockChannel { if self.available { - MockChannel::new( - &self.channel, - &self.date, - &self.version, - &self.hash, - self.rls, - self.multi_arch, - false, - ) + MockChannel::new(self, self.multi_arch, false) } else if self.multi_arch { // unavailable but multiarch means to build only with host==MULTI_ARCH1 // instead of true multiarch - MockChannel::new( - &self.channel, - &self.date, - &self.version, - &self.hash, - self.rls, - false, - true, - ) + MockChannel::new(self, false, true) } else { MockChannel::unavailable(&self.channel, &self.date, &self.version, &self.hash) } @@ -287,15 +271,16 @@ pub(crate) struct MockChannel { } impl MockChannel { - pub(super) fn new( - channel: &str, - date: &str, - version: &str, - version_hash: &str, - rls: RlsStatus, - multi_arch: bool, - swap_tuples: bool, - ) -> Self { + pub(super) fn new(release: &Release, multi_arch: bool, swap_tuples: bool) -> Self { + let Release { + channel, + date, + version, + hash: version_hash, + rls, + available: _, + multi_arch: _, + } = release; // Build the mock installers let host_tuple = if swap_tuples { MULTI_ARCH1.to_owned() @@ -323,7 +308,7 @@ impl MockChannel { all.rustc.push((rustc, host_tuple.clone())); all.cargo.push((cargo, host_tuple.clone())); - if rls != RlsStatus::Unavailable { + if *rls != RlsStatus::Unavailable { let rls = MockInstallerBuilder::rls(version, version_hash, rls.pkg_name()); all.rls.push((rls, host_tuple.clone())); } else { @@ -350,7 +335,7 @@ impl MockChannel { all.rustc.push((rustc, target_tuple.clone())); all.cargo.push((cargo, target_tuple.clone())); - if rls != RlsStatus::Unavailable { + if *rls != RlsStatus::Unavailable { let rls = MockInstallerBuilder::rls(version, version_hash, rls.pkg_name()); all.rls.push((rls, target_tuple.clone())); } else { @@ -412,13 +397,13 @@ impl MockChannel { target: target.to_string(), is_extension: false, }); - if rls == RlsStatus::Renamed { + if *rls == RlsStatus::Renamed { target_pkg.components.push(MockComponent { name: "rls-preview".to_string(), target: target.to_string(), is_extension: true, }); - } else if rls == RlsStatus::Available { + } else if *rls == RlsStatus::Available { target_pkg.components.push(MockComponent { name: "rls".to_string(), target: target.to_string(), @@ -455,7 +440,7 @@ impl MockChannel { } let mut renames = HashMap::new(); - if rls == RlsStatus::Renamed { + if *rls == RlsStatus::Renamed { renames.insert("rls".to_owned(), "rls-preview".to_owned()); } From 4af0d95d16a7253b791f89c73dfb95b9ccd1ea70 Mon Sep 17 00:00:00 2001 From: rami3l Date: Sun, 11 Oct 2026 12:03:20 +0200 Subject: [PATCH 3/6] feat(test): add `Scenario::RustAnalyzer` with a mock `rust-analyzer` component --- src/test/clitools.rs | 13 +++++++++++++ src/test/dist.rs | 45 +++++++++++++++++++++++++++++++++++++++++--- src/test/mock.rs | 9 +++++++++ 3 files changed, 64 insertions(+), 3 deletions(-) diff --git a/src/test/clitools.rs b/src/test/clitools.rs index e69419dd05..b5268216b2 100644 --- a/src/test/clitools.rs +++ b/src/test/clitools.rs @@ -543,6 +543,8 @@ pub enum Scenario { SimpleV2, /// One date, v1 manifests SimpleV1, + /// One date, v2 manifests, `rust-analyzer` available as an extension component + RustAnalyzer, /// One date, v2 manifests, MULTI_ARCH1 host MultiHost, /// One date, v2 manifests, beta with tag @@ -627,6 +629,13 @@ impl Scenario { Release::beta("1.2.0", "2015-01-02"), Release::stable("1.1.0", "2015-01-02"), ], + Self::RustAnalyzer => vec![ + Release::new("nightly", "1.3.0", "2015-01-02", "2") + .with_rls(RlsStatus::Renamed) + .with_rust_analyzer(), + Release::beta("1.2.0", "2015-01-02").with_rust_analyzer(), + Release::stable("1.1.0", "2015-01-02").with_rust_analyzer(), + ], Self::MultiHost => vec![ Release::new("nightly", "1.3.0", "2015-01-02", "2").multi_arch(), Release::beta("1.2.0", "2015-01-02").multi_arch(), @@ -677,6 +686,7 @@ impl Scenario { | Self::ArchivesV2TwoVersions | Self::BetaTag | Self::MultiHost + | Self::RustAnalyzer | Self::Unavailable | Self::UnavailableRls | Self::RemovedRls @@ -739,6 +749,7 @@ impl ConstState { | Scenario::None | Scenario::RecentStable | Scenario::RemovedRls + | Scenario::RustAnalyzer | Scenario::SimpleV1 | Scenario::SimpleV2 | Scenario::Unavailable @@ -849,6 +860,7 @@ async fn setup_test_state(test_dist_dir: TempDir) -> (TempDir, Config) { let rustc_path = config.exedir.join(format!("rustc{EXE_SUFFIX}")); let cargo_path = config.exedir.join(format!("cargo{EXE_SUFFIX}")); let rls_path = config.exedir.join(format!("rls{EXE_SUFFIX}")); + let rust_analyzer_path = config.exedir.join(format!("rust-analyzer{EXE_SUFFIX}")); let rust_lldb_path = config.exedir.join(format!("rust-lldb{EXE_SUFFIX}")); const ESTIMATED_LINKS_PER_TEST: usize = 6 * 2; @@ -885,6 +897,7 @@ async fn setup_test_state(test_dist_dir: TempDir) -> (TempDir, Config) { hard_link(&rustup_path, rustc_path).unwrap(); hard_link(&rustup_path, cargo_path).unwrap(); hard_link(&rustup_path, rls_path).unwrap(); + hard_link(&rustup_path, rust_analyzer_path).unwrap(); hard_link(&rustup_path, rust_lldb_path).unwrap(); // Make sure the host tuple matches the build tuple. Otherwise testing a 32-bit build of diff --git a/src/test/dist.rs b/src/test/dist.rs index b8ce027826..4c8e59466f 100644 --- a/src/test/dist.rs +++ b/src/test/dist.rs @@ -85,6 +85,7 @@ pub(super) struct Release { version: String, hash: String, rls: RlsStatus, + rust_analyzer: bool, available: bool, multi_arch: bool, } @@ -111,6 +112,11 @@ impl Release { self } + pub(super) fn with_rust_analyzer(mut self) -> Self { + self.rust_analyzer = true; + self + } + pub(super) fn unavailable(mut self) -> Self { self.available = false; self @@ -136,6 +142,7 @@ impl Release { available: true, multi_arch: false, rls: RlsStatus::Available, + rust_analyzer: false, } } @@ -278,6 +285,7 @@ impl MockChannel { version, hash: version_hash, rls, + rust_analyzer, available: _, multi_arch: _, } = release; @@ -318,6 +326,11 @@ impl MockChannel { )); } + if *rust_analyzer { + let ra = MockInstallerBuilder::rust_analyzer(version, version_hash); + all.rust_analyzer.push((ra, host_tuple.clone())); + } + all.docs.push((rust_docs, host_tuple.clone())); all.src.push((rust_src, "*".to_string())); all.analysis.push((rust_analysis, "*".to_string())); @@ -345,6 +358,11 @@ impl MockChannel { )); } + if *rust_analyzer { + let ra = MockInstallerBuilder::rust_analyzer(version, version_hash); + all.rust_analyzer.push((ra, target_tuple.clone())); + } + all.docs.push((rust_docs, target_tuple.to_string())); all.combined.push((rust, target_tuple)); } @@ -374,7 +392,7 @@ impl MockChannel { // Add subcomponents of the rust package { - let rust_pkg = packages.last_mut().unwrap(); + let rust_pkg = packages.iter_mut().find(|p| p.name == "rust").unwrap(); for target_pkg in rust_pkg.targets.iter_mut() { let target = &target_pkg.target; target_pkg.components.push(MockComponent { @@ -416,6 +434,13 @@ impl MockChannel { is_extension: true, }) } + if *rust_analyzer { + target_pkg.components.push(MockComponent { + name: "rust-analyzer-preview".to_string(), + target: target.to_string(), + is_extension: true, + }); + } for other_target in &all_std_archs { if other_target != target { target_pkg.components.push(MockComponent { @@ -443,6 +468,12 @@ impl MockChannel { if *rls == RlsStatus::Renamed { renames.insert("rls".to_owned(), "rls-preview".to_owned()); } + if *rust_analyzer { + renames.insert( + "rust-analyzer".to_owned(), + "rust-analyzer-preview".to_owned(), + ); + } Self { name: channel.to_string(), @@ -498,6 +529,7 @@ struct MockChannelContent { rustc: Vec<(MockInstallerBuilder, String)>, cargo: Vec<(MockInstallerBuilder, String)>, rls: Vec<(MockInstallerBuilder, String)>, + rust_analyzer: Vec<(MockInstallerBuilder, String)>, docs: Vec<(MockInstallerBuilder, String)>, src: Vec<(MockInstallerBuilder, String)>, analysis: Vec<(MockInstallerBuilder, String)>, @@ -509,7 +541,7 @@ impl MockChannelContent { self, rls_name: &'static str, ) -> Vec<(&'static str, Vec<(MockInstallerBuilder, String)>)> { - vec![ + let mut packages = vec![ ("rust-std", self.std), ("rustc", self.rustc), ("cargo", self.cargo), @@ -518,7 +550,14 @@ impl MockChannelContent { ("rust-src", self.src), ("rust-analysis", self.analysis), ("rust", self.combined), - ] + ]; + + // Not every release ships `rust-analyzer`. + if !self.rust_analyzer.is_empty() { + packages.push(("rust-analyzer-preview", self.rust_analyzer)); + } + + packages } } diff --git a/src/test/mock.rs b/src/test/mock.rs index d5600b92d2..556addf21a 100644 --- a/src/test/mock.rs +++ b/src/test/mock.rs @@ -76,6 +76,15 @@ impl MockInstallerBuilder { } } + pub(super) fn rust_analyzer(version: &str, version_hash: &str) -> Self { + Self { + components: vec![MockComponentBuilder { + name: "rust-analyzer-preview".to_string(), + files: mock_bin("rust-analyzer", version, version_hash), + }], + } + } + pub(super) fn rust_doc() -> Self { let mut files: Vec = topical_doc_data::unique_paths() .map(|x| MockFile::new(x, b"")) From ea09c64d58dad58c84f756384a520c67d72e5212 Mon Sep 17 00:00:00 2001 From: rami3l Date: Sun, 11 Oct 2026 12:02:57 +0200 Subject: [PATCH 4/6] test(cli-misc): use `Scenario::RustAnalyzer` in existing tests --- tests/suite/cli_misc.rs | 24 ++++++------------------ 1 file changed, 6 insertions(+), 18 deletions(-) diff --git a/tests/suite/cli_misc.rs b/tests/suite/cli_misc.rs index e7ae790352..87e78f86d5 100644 --- a/tests/suite/cli_misc.rs +++ b/tests/suite/cli_misc.rs @@ -1707,7 +1707,7 @@ CARGO environment variable not set[..] #[tokio::test] async fn rust_analyzer_proxy_falls_back_external() { - let cx = CliTestContext::new(Scenario::SimpleV2).await; + let cx = CliTestContext::new(Scenario::RustAnalyzer).await; cx.config .expect([ "rustup", @@ -1715,7 +1715,7 @@ async fn rust_analyzer_proxy_falls_back_external() { "install", "stable", "--profile=minimal", - "--component=rls", + "--component=rust-analyzer", ]) .await .is_ok(); @@ -1724,21 +1724,6 @@ async fn rust_analyzer_proxy_falls_back_external() { .await .is_ok(); - // We pretend to have a `rust-analyzer` installation by reusing the `rls` - // proxy and mock binary. - let rls = format!("rls{EXE_SUFFIX}"); - let ra = format!("rust-analyzer{EXE_SUFFIX}"); - let exedir = &cx.config.exedir; - let bindir = &cx - .config - .rustupdir - .join("toolchains") - .join(format!("stable-{}", this_host_tuple())) - .join("bin"); - for dir in [exedir, bindir] { - fs::rename(dir.join(&rls), dir.join(&ra)).unwrap(); - } - // Base case: rustup-hosted RA installed, external RA unavailable, // use the former. let real_path = cx @@ -1771,7 +1756,10 @@ async fn rust_analyzer_proxy_falls_back_external() { .is_ok(); // Second case: rustup-hosted RA unavailable, fallback on the external RA. - fs::remove_file(bindir.join(&ra)).unwrap(); + cx.config + .expect(["rustup", "component", "remove", "rust-analyzer"]) + .await + .is_ok(); cx.config .expect_with_env( ["rust-analyzer", "--echo-current-exe"], From ec0fc5872d6d515f40b3d0b380b32b9f2cfa88f4 Mon Sep 17 00:00:00 2001 From: rami3l Date: Mon, 12 Oct 2026 10:01:00 +0200 Subject: [PATCH 5/6] test(cli-misc): simplify setup of `rust_analyzer_*()` tests --- tests/suite/cli_misc.rs | 26 +++++++++++--------------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/tests/suite/cli_misc.rs b/tests/suite/cli_misc.rs index 87e78f86d5..98f15e5572 100644 --- a/tests/suite/cli_misc.rs +++ b/tests/suite/cli_misc.rs @@ -1708,21 +1708,7 @@ CARGO environment variable not set[..] #[tokio::test] async fn rust_analyzer_proxy_falls_back_external() { let cx = CliTestContext::new(Scenario::RustAnalyzer).await; - cx.config - .expect([ - "rustup", - "toolchain", - "install", - "stable", - "--profile=minimal", - "--component=rust-analyzer", - ]) - .await - .is_ok(); - cx.config - .expect(["rustup", "default", "stable"]) - .await - .is_ok(); + cx.config.expect(RUST_ANALYZER_SETUP_CMD).await.is_ok(); // Base case: rustup-hosted RA installed, external RA unavailable, // use the former. @@ -1776,6 +1762,16 @@ info: falling back to "[EXTERN_PATH]" .is_ok(); } +const RUST_ANALYZER_SETUP_CMD: &[&str] = &[ + "rustup", + "toolchain", + "install", + "stable", + "--default", + "--profile=minimal", + "--component=rust-analyzer", +]; + #[tokio::test] async fn warn_auto_install_on_proxy() { let cx = CliTestContext::new(Scenario::SimpleV2).await; From 691e51b24d80199eca35492fff37d9dde873012f Mon Sep 17 00:00:00 2001 From: rami3l Date: Sun, 11 Oct 2026 20:41:10 +0200 Subject: [PATCH 6/6] test(cli-misc): split `rust_analyzer_*()` tests into separate cases --- tests/suite/cli_misc.rs | 35 ++++++++++++++++++++++++++++------- 1 file changed, 28 insertions(+), 7 deletions(-) diff --git a/tests/suite/cli_misc.rs b/tests/suite/cli_misc.rs index 98f15e5572..46d748ada4 100644 --- a/tests/suite/cli_misc.rs +++ b/tests/suite/cli_misc.rs @@ -1706,28 +1706,34 @@ CARGO environment variable not set[..] } #[tokio::test] -async fn rust_analyzer_proxy_falls_back_external() { +async fn rust_analyzer_proxy_uses_hosted_without_external() { let cx = CliTestContext::new(Scenario::RustAnalyzer).await; cx.config.expect(RUST_ANALYZER_SETUP_CMD).await.is_ok(); - // Base case: rustup-hosted RA installed, external RA unavailable, - // use the former. let real_path = cx .config .expect(["rust-analyzer", "--echo-current-exe"]) .await; real_path.is_ok(); let real_path = Path::new(real_path.output.stderr.trim()); - assert!(real_path.is_file()); +} + +#[tokio::test] +async fn rust_analyzer_proxy_uses_hosted_over_external() { + let cx = CliTestContext::new(Scenario::RustAnalyzer).await; + cx.config.expect(RUST_ANALYZER_SETUP_CMD).await.is_ok(); let tempdir = tempfile::Builder::new().prefix("rustup").tempdir().unwrap(); let extern_dir = tempdir.path(); let extern_path = &extern_dir.join("rust-analyzer"); + let real_path = cx + .config + .expect(["rust-analyzer", "--echo-current-exe"]) + .await; + let real_path = Path::new(real_path.output.stderr.trim()); fs::copy(real_path, extern_path).unwrap(); - // First case: rustup-hosted and external RA both installed, - // prioritize the former. cx.config .expect_with_env( ["rust-analyzer", "--echo-current-exe"], @@ -1740,12 +1746,27 @@ async fn rust_analyzer_proxy_falls_back_external() { "#]]) .is_ok(); +} + +#[tokio::test] +async fn rust_analyzer_proxy_uses_external_without_hosted() { + let cx = CliTestContext::new(Scenario::RustAnalyzer).await; + cx.config.expect(RUST_ANALYZER_SETUP_CMD).await.is_ok(); - // Second case: rustup-hosted RA unavailable, fallback on the external RA. + let tempdir = tempfile::Builder::new().prefix("rustup").tempdir().unwrap(); + let extern_dir = tempdir.path(); + let extern_path = &extern_dir.join("rust-analyzer"); + let real_path = cx + .config + .expect(["rust-analyzer", "--echo-current-exe"]) + .await; + let real_path = Path::new(real_path.output.stderr.trim()); + fs::copy(real_path, extern_path).unwrap(); cx.config .expect(["rustup", "component", "remove", "rust-analyzer"]) .await .is_ok(); + cx.config .expect_with_env( ["rust-analyzer", "--echo-current-exe"],