diff --git a/Cargo.lock b/Cargo.lock index 0a094621b4..975f5285a2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2281,6 +2281,7 @@ dependencies = [ "sha2", "sharded-slab", "similar-asserts", + "snapbox", "strsim", "tar", "tempfile", diff --git a/Cargo.toml b/Cargo.toml index 09ef76b4dd..041966b7d9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,7 +37,7 @@ otel = [ ] # Exports code dependent on private interfaces for the integration test suite -test = ["dep:similar-asserts", "dep:walkdir"] +test = ["dep:similar-asserts", "dep:snapbox", "dep:walkdir"] # Sorted by alphabetic order [dependencies] @@ -77,8 +77,6 @@ semver = "1.0" serde = { version = "1.0", features = ["derive"] } sha2 = "0.10" sharded-slab = "0.1.1" -# test only (depends on `test` feature) -similar-asserts = { version = "1.7", optional = true } strsim = "0.11" tar = "0.4.26" tempfile = "3.8" @@ -95,10 +93,14 @@ tracing-opentelemetry = { version = "0.30", optional = true } tracing-subscriber = { version = "0.3.16", features = ["env-filter"] } url = "2.4" wait-timeout = "0.2" -walkdir = { version = "2", optional = true } xz2 = "0.1.3" zstd = "0.13" +# test only (depends on `test` feature) +similar-asserts = { version = "1.7", optional = true } +snapbox = { version = "0.6.21", optional = true } +walkdir = { version = "2", optional = true } + [target."cfg(windows)".dependencies] cc = "1" scopeguard = "1" diff --git a/src/test/clitools.rs b/src/test/clitools.rs index 30d0210d02..18f623bef0 100644 --- a/src/test/clitools.rs +++ b/src/test/clitools.rs @@ -19,6 +19,7 @@ use std::{ use enum_map::{Enum, EnumMap, enum_map}; use similar_asserts::SimpleDiff; +use snapbox::{IntoData, RedactedValue, Redactions, assert_data_eq}; use tempfile::TempDir; use url::Url; @@ -62,6 +63,78 @@ pub struct Config { pub test_root_dir: PathBuf, } +#[derive(Clone)] +pub struct Assert { + output: SanitizedOutput, + redactions: Redactions, +} + +impl Assert { + /// Creates a new [`Assert`] object with the given command [`SanitizedOutput`]. + pub fn new(output: SanitizedOutput) -> Self { + let mut redactions = Redactions::new(); + redactions + .extend([("[HOST_TRIPLE]", this_host_triple())]) + .expect("invalid redactions detected"); + Self { output, redactions } + } + + /// Extend the redaction rules used in the currrent assertion with new values. + pub fn extend_redactions( + &mut self, + vars: impl IntoIterator)>, + ) -> &mut Self { + self.redactions + .extend(vars) + .expect("invalid redactions detected"); + self + } + + /// Asserts that the command exited with an ok status. + pub fn is_ok(&self) -> &Self { + assert!(self.output.ok); + self + } + + /// Asserts that the command exited with an error. + pub fn is_err(&self) -> &Self { + assert!(!self.output.ok); + self + } + + /// Asserts that the command exited with the given `expected` stdout pattern. + pub fn with_stdout(&self, expected: impl IntoData) -> &Self { + let stdout = self.redactions.redact(&self.output.stdout); + assert_data_eq!(&stdout, expected); + self + } + + /// Asserts that the command exited without the given `unexpected` stdout pattern. + pub fn without_stdout(&self, unexpected: &str) -> &Self { + if self.output.stdout.contains(unexpected) { + print_indented("expected.stdout.does_not_contain", unexpected); + panic!(); + } + self + } + + /// Asserts that the command exited with the given `expected` stderr pattern. + pub fn with_stderr(&self, expected: impl IntoData) -> &Self { + let stderr = self.redactions.redact(&self.output.stderr); + assert_data_eq!(&stderr, expected); + self + } + + /// Asserts that the command exited without the given `unexpected` stderr pattern. + pub fn without_stderr(&self, unexpected: &str) -> &Self { + if self.output.stderr.contains(unexpected) { + print_indented("expected.stderr.does_not_contain", unexpected); + panic!(); + } + self + } +} + impl Config { pub fn current_dir(&self) -> PathBuf { self.workdir.borrow().clone() @@ -137,12 +210,35 @@ impl Config { } } + /// Returns an [`Assert`] object to check the output of running the command + /// specified by `args` under the default environment. + #[must_use] + pub async fn expect(&self, args: impl AsRef<[&str]>) -> Assert { + self.expect_with_env(args, &[]).await + } + + /// Returns an [`Assert`] object to check the output of running the command + /// specified by `args` and under the environment specified by `env`. + #[must_use] + pub async fn expect_with_env( + &self, + args: impl AsRef<[&str]>, + env: impl AsRef<[(&str, &str)]>, + ) -> Assert { + let args = args.as_ref(); + let output = self.run(args[0], &args[1..], env.as_ref()).await; + Assert::new(output) + } + /// Expect an ok status + #[deprecated(note = "use `.expect().await.is_ok()` instead")] + #[allow(deprecated)] pub async fn expect_ok(&mut self, args: &[&str]) { self.expect_ok_env(args, &[]).await } /// Expect an ok status with extra environment variables + #[deprecated(note = "use `.expect_with_env().await.is_ok()` instead")] pub async fn expect_ok_env(&self, args: &[&str], env: &[(&str, &str)]) { let out = self.run(args[0], &args[1..], env).await; if !out.ok { @@ -153,11 +249,14 @@ impl Config { } /// Expect an err status and a string in stderr + #[deprecated(note = "use `.expect().await.is_err()` instead")] + #[allow(deprecated)] pub async fn expect_err(&self, args: &[&str], expected: &str) { self.expect_err_env(args, &[], expected).await } /// Expect an err status and a string in stderr, with extra environment variables + #[deprecated(note = "use `.expect_with_env().await.is_err()` instead")] pub async fn expect_err_env(&self, args: &[&str], env: &[(&str, &str)], expected: &str) { let out = self.run(args[0], &args[1..], env).await; if out.ok || !out.stderr.contains(expected) { @@ -169,6 +268,7 @@ impl Config { } /// Expect an ok status and a string in stdout + #[deprecated(note = "use `.expect().await.is_ok().with_stdout()` instead")] pub async fn expect_stdout_ok(&self, args: &[&str], expected: &str) { let out = self.run(args[0], &args[1..], &[]).await; if !out.ok || !out.stdout.contains(expected) { @@ -179,6 +279,7 @@ impl Config { } } + #[deprecated(note = "use `.expect().await.is_ok().without_stdout()` instead")] pub async fn expect_not_stdout_ok(&self, args: &[&str], expected: &str) { let out = self.run(args[0], &args[1..], &[]).await; if !out.ok || out.stdout.contains(expected) { @@ -189,6 +290,7 @@ impl Config { } } + #[deprecated(note = "use `.expect().await.is_ok().without_stderr()` instead")] pub async fn expect_not_stderr_ok(&self, args: &[&str], expected: &str) { let out = self.run(args[0], &args[1..], &[]).await; if !out.ok || out.stderr.contains(expected) { @@ -199,6 +301,7 @@ impl Config { } } + #[deprecated(note = "use `.expect().await.is_err().without_stderr()` instead")] pub async fn expect_not_stderr_err(&self, args: &[&str], expected: &str) { let out = self.run(args[0], &args[1..], &[]).await; if out.ok || out.stderr.contains(expected) { @@ -210,6 +313,7 @@ impl Config { } /// Expect an ok status and a string in stderr + #[deprecated(note = "use `.expect().await.is_ok().with_stderr()` instead")] pub async fn expect_stderr_ok(&self, args: &[&str], expected: &str) { let out = self.run(args[0], &args[1..], &[]).await; if !out.ok || !out.stderr.contains(expected) { @@ -221,12 +325,17 @@ impl Config { } /// Expect an exact strings on stdout/stderr with an ok status code + #[deprecated(note = "use `.expect().await.is_ok().with_stdout().with_stderr()` instead")] + #[allow(deprecated)] pub async fn expect_ok_ex(&mut self, args: &[&str], stdout: &str, stderr: &str) { self.expect_ok_ex_env(args, &[], stdout, stderr).await; } /// Expect an exact strings on stdout/stderr with an ok status code, /// with extra environment variables + #[deprecated( + note = "use `.expect_with_env().await.is_ok().with_stdout().with_stderr()` instead" + )] pub async fn expect_ok_ex_env( &mut self, args: &[&str], @@ -249,6 +358,7 @@ impl Config { } /// Expect an exact strings on stdout/stderr with an error status code + #[deprecated(note = "use `.expect().await.is_err().with_stdout().with_stderr()` instead")] pub async fn expect_err_ex(&self, args: &[&str], stdout: &str, stderr: &str) { let out = self.run(args[0], &args[1..], &[]).await; if out.ok || out.stdout != stdout || out.stderr != stderr { @@ -1045,7 +1155,7 @@ pub struct Output { pub stderr: Vec, } -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct SanitizedOutput { pub ok: bool, pub stdout: String, diff --git a/tests/suite/cli_exact.rs b/tests/suite/cli_exact.rs index 46c43f56d0..e48e190057 100644 --- a/tests/suite/cli_exact.rs +++ b/tests/suite/cli_exact.rs @@ -1,11 +1,14 @@ //! Yet more cli test cases. These are testing that the output //! is exactly as expected. +#![allow(deprecated)] + use rustup::for_host; use rustup::test::{ CROSS_ARCH1, CROSS_ARCH2, CliTestContext, MULTI_ARCH1, Scenario, this_host_triple, }; use rustup::utils::raw; +use snapbox::str; #[tokio::test] async fn update_once() { @@ -309,23 +312,21 @@ info: default toolchain set to 'nightly-{0}' #[tokio::test] async fn override_again() { - let mut cx = CliTestContext::new(Scenario::SimpleV2).await; - let cwd = cx.config.current_dir(); + let cx = &CliTestContext::new(Scenario::SimpleV2).await; cx.config - .expect_ok(&["rustup", "override", "add", "nightly"]) - .await; + .expect(["rustup", "override", "add", "nightly"]) + .await + .is_ok(); cx.config - .expect_ok_ex( - &["rustup", "override", "add", "nightly"], - "", - &format!( - r"info: override toolchain for '{}' set to 'nightly-{1}' -", - cwd.display(), - &this_host_triple() - ), - ) - .await; + .expect(["rustup", "override", "add", "nightly"]) + .await + .extend_redactions([("[CWD]", cx.config.current_dir().display().to_string())]) + .is_ok() + .with_stdout("") + .with_stderr(str![[r#" +info: override toolchain for '[CWD]' set to 'nightly-[HOST_TRIPLE]' + +"#]]); } #[tokio::test] diff --git a/tests/suite/cli_inst_interactive.rs b/tests/suite/cli_inst_interactive.rs index 332ddb7b52..d4df5b2441 100644 --- a/tests/suite/cli_inst_interactive.rs +++ b/tests/suite/cli_inst_interactive.rs @@ -1,5 +1,7 @@ //! Tests of the interactive console installer +#![allow(deprecated)] + use std::env::consts::EXE_SUFFIX; use std::io::Write; use std::process::Stdio; diff --git a/tests/suite/cli_misc.rs b/tests/suite/cli_misc.rs index 8fb21e8b6e..12d6391b87 100644 --- a/tests/suite/cli_misc.rs +++ b/tests/suite/cli_misc.rs @@ -1,6 +1,8 @@ //! Test cases of the rustup command that do not depend on the //! dist server, mostly derived from multirust/test-v2.sh +#![allow(deprecated)] + use std::fs; use std::str; use std::{env::consts::EXE_SUFFIX, path::Path}; @@ -11,6 +13,7 @@ use rustup::test::{ }; use rustup::utils; use rustup::utils::raw::symlink_dir; +use snapbox::str; #[tokio::test] async fn smoke_test() { @@ -113,16 +116,15 @@ async fn custom_invalid_names_with_archive_dates() { async fn update_all_no_update_whitespace() { let cx = CliTestContext::new(Scenario::SimpleV2).await; cx.config - .expect_stdout_ok( - &["rustup", "update", "nightly"], - for_host!( - r" - nightly-{} installed - 1.3.0 (hash-nightly-2) + .expect(["rustup", "update", "nightly"]) + .await + .is_ok() + .with_stdout(str![[r#" -" - ), - ) - .await; + nightly-[HOST_TRIPLE] installed - 1.3.0 (hash-nightly-2) + + +"#]]); } // Issue #145 diff --git a/tests/suite/cli_paths.rs b/tests/suite/cli_paths.rs index 1c1aedf5c5..8fb4fb5487 100644 --- a/tests/suite/cli_paths.rs +++ b/tests/suite/cli_paths.rs @@ -2,6 +2,8 @@ //! It depends on self-update working, so if absolutely everything here breaks, //! check those tests as well. +#![allow(deprecated)] + // Prefer omitting actually unpacking content while just testing paths. const INIT_NONE: [&str; 4] = ["rustup-init", "-y", "--default-toolchain", "none"]; diff --git a/tests/suite/cli_rustup.rs b/tests/suite/cli_rustup.rs index 23668604ed..08a5869837 100644 --- a/tests/suite/cli_rustup.rs +++ b/tests/suite/cli_rustup.rs @@ -1,5 +1,7 @@ //! Test cases for new rustup UI +#![allow(deprecated)] + use std::fs; use std::path::{MAIN_SEPARATOR, PathBuf}; use std::{env::consts::EXE_SUFFIX, path::Path}; diff --git a/tests/suite/cli_self_upd.rs b/tests/suite/cli_self_upd.rs index 08aa19bbfb..be9747e8a7 100644 --- a/tests/suite/cli_self_upd.rs +++ b/tests/suite/cli_self_upd.rs @@ -1,5 +1,7 @@ //! Testing self install, uninstall and update +#![allow(deprecated)] + use std::env; use std::env::consts::EXE_SUFFIX; use std::fs; @@ -20,6 +22,7 @@ use rustup::test::{ use rustup::test::{RegistryGuard, RegistryValueId, USER_PATH}; use rustup::utils::{self, raw}; use rustup::{DUP_TOOLS, TOOLS, for_host}; +use snapbox::str; #[cfg(windows)] use windows_registry::Value; @@ -388,26 +391,32 @@ info: downloading self-update (new version: {TEST_VERSION}) #[tokio::test] async fn update_precise() { - let version = env!("CARGO_PKG_VERSION"); - let expected_output = format!( - "info: checking for self-update (current version: {version}) -info: `RUSTUP_VERSION` has been set to `{TEST_VERSION}` -info: downloading self-update (new version: {TEST_VERSION}) -" - ); - - let mut cx = SelfUpdateTestContext::new(TEST_VERSION).await; + let cx = SelfUpdateTestContext::new(TEST_VERSION).await; cx.config - .expect_ok(&["rustup-init", "-y", "--no-modify-path"]) - .await; + .expect(["rustup-init", "-y", "--no-modify-path"]) + .await + .is_ok(); cx.config - .expect_ok_ex_env( - &["rustup", "self", "update"], - &[("RUSTUP_VERSION", TEST_VERSION)], - &format!(" rustup updated - {version} (from {version})\n\n",), - &expected_output, + .expect_with_env( + ["rustup", "self", "update"], + [("RUSTUP_VERSION", TEST_VERSION)], ) - .await; + .await + .extend_redactions([ + ("[TEST_VERSION]", TEST_VERSION), + ("[VERSION]", env!("CARGO_PKG_VERSION")), + ]) + .with_stdout(str![[r#" + rustup updated - [VERSION] (from [VERSION]) + + +"#]]) + .with_stderr(str![[r#" +info: checking for self-update (current version: [VERSION]) +info: `RUSTUP_VERSION` has been set to `[TEST_VERSION]` +info: downloading self-update (new version: [TEST_VERSION]) + +"#]]); } #[cfg(windows)] diff --git a/tests/suite/cli_v1.rs b/tests/suite/cli_v1.rs index 5ec9e8b069..8f7ba6d342 100644 --- a/tests/suite/cli_v1.rs +++ b/tests/suite/cli_v1.rs @@ -1,20 +1,26 @@ //! Test cases of the rustup command, using v1 manifests, mostly //! derived from multirust/test-v2.sh +#![allow(deprecated)] + use std::fs; use rustup::for_host; use rustup::test::{CliTestContext, Scenario}; +use snapbox::str; #[tokio::test] async fn rustc_no_default_toolchain() { let cx = CliTestContext::new(Scenario::SimpleV1).await; cx.config - .expect_err( - &["rustc"], - "rustup could not choose a version of rustc to run", - ) - .await; + .expect(["rustc"]) + .await + .is_err() + .with_stderr(str![[r#" +error: rustup could not choose a version of rustc to run, because one wasn't specified explicitly, and no default is configured. +help: run 'rustup default stable' to download the latest stable release of Rust and set it as your default toolchain. + +"#]]); } #[tokio::test] diff --git a/tests/suite/cli_v2.rs b/tests/suite/cli_v2.rs index ad0ab9eb89..c09bfdf826 100644 --- a/tests/suite/cli_v2.rs +++ b/tests/suite/cli_v2.rs @@ -1,6 +1,8 @@ //! Test cases of the rustup command, using v2 manifests, mostly //! derived from multirust/test-v2.sh +#![allow(deprecated)] + use std::fs; use std::io::Write; use std::path::PathBuf;