diff --git a/crates/cargo-test-macro/src/lib.rs b/crates/cargo-test-macro/src/lib.rs index 9d4a5fbaa99..2e62f6a32ff 100644 --- a/crates/cargo-test-macro/src/lib.rs +++ b/crates/cargo-test-macro/src/lib.rs @@ -14,6 +14,49 @@ use std::path::Path; use std::process::Command; use std::sync::Once; +/// Replacement for `#[test]` +/// +/// The `#[cargo_test]` attribute extends `#[test]` with some setup before starting the test. +/// It will create a filesystem "sandbox" under the "cargo integration test" directory for each test, such as `/path/to/cargo/target/tmp/cit/t123/`. +/// The sandbox will contain a `home` directory that will be used instead of your normal home directory. +/// +/// The `#[cargo_test]` attribute takes several options that will affect how the test is generated. +/// They are listed in parentheses separated with commas, such as: +/// +/// ```rust,ignore +/// #[cargo_test(nightly, reason = "-Zfoo is unstable")] +/// ``` +/// +/// The options it supports are: +/// +/// * `>=1.64` --- This indicates that the test will only run with the given version of `rustc` or newer. +/// This can be used when a new `rustc` feature has been stabilized that the test depends on. +/// If this is specified, a `reason` is required to explain why it is being checked. +/// * `nightly` --- This will cause the test to be ignored if not running on the nightly toolchain. +/// This is useful for tests that use unstable options in `rustc` or `rustdoc`. +/// These tests are run in Cargo's CI, but are disabled in rust-lang/rust's CI due to the difficulty of updating both repos simultaneously. +/// A `reason` field is required to explain why it is nightly-only. +/// * `requires_` --- This indicates a command that is required to be installed to be run. +/// For example, `requires_rustfmt` means the test will only run if the executable `rustfmt` is installed. +/// These tests are *always* run on CI. +/// This is mainly used to avoid requiring contributors from having every dependency installed. +/// * `build_std_real` --- This is a "real" `-Zbuild-std` test (in the `build_std` integration test). +/// This only runs on nightly, and only if the environment variable `CARGO_RUN_BUILD_STD_TESTS` is set (these tests on run on Linux). +/// * `build_std_mock` --- This is a "mock" `-Zbuild-std` test (which uses a mock standard library). +/// This only runs on nightly, and is disabled for windows-gnu. +/// * `public_network_test` --- This tests contacts the public internet. +/// These tests are disabled unless the `CARGO_PUBLIC_NETWORK_TESTS` environment variable is set. +/// Use of this should be *extremely rare*, please avoid using it if possible. +/// The hosts it contacts should have a relatively high confidence that they are reliable and stable (such as github.com), especially in CI. +/// The tests should be carefully considered for developer security and privacy as well. +/// * `container_test` --- This indicates that it is a test that uses Docker. +/// These tests are disabled unless the `CARGO_CONTAINER_TESTS` environment variable is set. +/// This requires that you have Docker installed. +/// The SSH tests also assume that you have OpenSSH installed. +/// These should work on Linux, macOS, and Windows where possible. +/// Unfortunately these tests are not run in CI for macOS or Windows (no Docker on macOS, and Windows does not support Linux images). +/// See [`cargo-test-support::containers`](https://doc.rust-lang.org/nightly/nightly-rustc/cargo_test_support/containers) for more on writing these tests. +/// * `ignore_windows="reason"` --- Indicates that the test should be ignored on windows for the given reason. #[proc_macro_attribute] pub fn cargo_test(attr: TokenStream, item: TokenStream) -> TokenStream { // Ideally these options would be embedded in the test itself. However, I diff --git a/crates/cargo-test-support/Cargo.toml b/crates/cargo-test-support/Cargo.toml index f6e1ffa2d39..42e0d64658e 100644 --- a/crates/cargo-test-support/Cargo.toml +++ b/crates/cargo-test-support/Cargo.toml @@ -8,9 +8,6 @@ homepage.workspace = true repository.workspace = true description = "Testing framework for Cargo's testsuite." -[lib] -doctest = false - [dependencies] anstream.workspace = true anstyle.workspace = true diff --git a/crates/cargo-test-support/src/compare.rs b/crates/cargo-test-support/src/compare.rs index dae405a1c9e..e822bb3822c 100644 --- a/crates/cargo-test-support/src/compare.rs +++ b/crates/cargo-test-support/src/compare.rs @@ -1,6 +1,11 @@ //! Routines for comparing and diffing output. //! -//! # Patterns +//! # Deprecated comparisons +//! +//! Cargo's tests are in transition from internal-only pattern and normalization routines used in +//! asserts like [`crate::Execs::with_stdout`] to [`assert_e2e`] and [`assert_ui`]. +//! +//! ## Patterns //! //! Many of these functions support special markup to assist with comparing //! text that may vary or is otherwise uninteresting for the test at hand. The @@ -22,7 +27,7 @@ //! can use this to avoid duplicating the `with_stderr` call like: //! `if cfg!(target_env = "msvc") {e.with_stderr("...[DIRTY]...");} else {e.with_stderr("...");}`. //! -//! # Normalization +//! ## Normalization //! //! In addition to the patterns described above, the strings are normalized //! in such a way to avoid unwanted differences. The normalizations are: @@ -86,6 +91,19 @@ macro_rules! regex { /// Other heuristics are applied to try to ensure Windows-style paths aren't /// a problem. /// - Carriage returns are removed, which can help when running on Windows. +/// +/// # Example +/// +/// ```no_run +/// # use cargo_test_support::compare::assert_e2e; +/// # use cargo_test_support::file; +/// # let p = cargo_test_support::project().build(); +/// # let stdout = ""; +/// assert_e2e().eq(stdout, file!["stderr.term.svg"]); +/// ``` +/// ```console +/// $ SNAPSHOTS=overwrite cargo test +/// ``` pub fn assert_ui() -> snapbox::Assert { let mut subs = snapbox::Redactions::new(); subs.extend(MIN_LITERAL_REDACTIONS.into_iter().cloned()) @@ -129,6 +147,18 @@ pub fn assert_ui() -> snapbox::Assert { /// Other heuristics are applied to try to ensure Windows-style paths aren't /// a problem. /// - Carriage returns are removed, which can help when running on Windows. +/// +/// # Example +/// +/// ```no_run +/// # use cargo_test_support::compare::assert_e2e; +/// # use cargo_test_support::str; +/// # let p = cargo_test_support::project().build(); +/// assert_e2e().eq(p.read_lockfile(), str![]); +/// ``` +/// ```console +/// $ SNAPSHOTS=overwrite cargo test +/// ``` pub fn assert_e2e() -> snapbox::Assert { let mut subs = snapbox::Redactions::new(); subs.extend(MIN_LITERAL_REDACTIONS.into_iter().cloned()) diff --git a/crates/cargo-test-support/src/git.rs b/crates/cargo-test-support/src/git.rs index 9863c5045ba..803e4dafd49 100644 --- a/crates/cargo-test-support/src/git.rs +++ b/crates/cargo-test-support/src/git.rs @@ -1,42 +1,44 @@ -/* -# Git Testing Support - -## Creating a git dependency -`git::new()` is an easy way to create a new git repository containing a -project that you can then use as a dependency. It will automatically add all -the files you specify in the project and commit them to the repository. -Example: - -``` -let git_project = git::new("dep1", |project| { - project - .file("Cargo.toml", &basic_manifest("dep1", "1.0.0")) - .file("src/lib.rs", r#"pub fn f() { println!("hi!"); } "#) -}); - -// Use the `url()` method to get the file url to the new repository. -let p = project() - .file("Cargo.toml", &format!(r#" - [package] - name = "a" - version = "1.0.0" - - [dependencies] - dep1 = {{ git = '{}' }} - "#, git_project.url())) - .file("src/lib.rs", "extern crate dep1;") - .build(); -``` - -## Manually creating repositories -`git::repo()` can be used to create a `RepoBuilder` which provides a way of -adding files to a blank repository and committing them. - -If you want to then manipulate the repository (such as adding new files or -tags), you can use `git2::Repository::open()` to open the repository and then -use some of the helper functions in this file to interact with the repository. - -*/ +//! # Git Testing Support +//! +//! ## Creating a git dependency +//! [`new()`] is an easy way to create a new git repository containing a +//! project that you can then use as a dependency. It will automatically add all +//! the files you specify in the project and commit them to the repository. +//! +//! ### Example: +//! +//! ```no_run +//! # use cargo_test_support::project; +//! # use cargo_test_support::basic_manifest; +//! # use cargo_test_support::git; +//! let git_project = git::new("dep1", |project| { +//! project +//! .file("Cargo.toml", &basic_manifest("dep1", "1.0.0")) +//! .file("src/lib.rs", r#"pub fn f() { println!("hi!"); } "#) +//! }); +//! +//! // Use the `url()` method to get the file url to the new repository. +//! let p = project() +//! .file("Cargo.toml", &format!(r#" +//! [package] +//! name = "a" +//! version = "1.0.0" +//! +//! [dependencies] +//! dep1 = {{ git = '{}' }} +//! "#, git_project.url())) +//! .file("src/lib.rs", "extern crate dep1;") +//! .build(); +//! ``` +//! +//! ## Manually creating repositories +//! +//! [`repo()`] can be used to create a [`RepoBuilder`] which provides a way of +//! adding files to a blank repository and committing them. +//! +//! If you want to then manipulate the repository (such as adding new files or +//! tags), you can use `git2::Repository::open()` to open the repository and then +//! use some of the helper functions in this file to interact with the repository. use crate::{paths::CargoPathExt, project, Project, ProjectBuilder, SymlinkBuilder}; use std::fs; @@ -44,17 +46,21 @@ use std::path::{Path, PathBuf}; use std::sync::Once; use url::Url; +/// Manually construct a [`Repository`] +/// +/// See also [`new`], [`repo`] #[must_use] pub struct RepoBuilder { repo: git2::Repository, files: Vec, } +/// See [`new`] pub struct Repository(git2::Repository); -/// Create a `RepoBuilder` to build a new git repository. +/// Create a [`RepoBuilder`] to build a new git repository. /// -/// Call `build()` to finalize and create the repository. +/// Call [`RepoBuilder::build()`] to finalize and create the repository. pub fn repo(p: &Path) -> RepoBuilder { RepoBuilder::init(p) } @@ -130,7 +136,7 @@ impl Repository { } } -/// Initialize a new repository at the given path. +/// *(`git2`)* Initialize a new repository at the given path. pub fn init(path: &Path) -> git2::Repository { default_search_path(); let repo = t!(git2::Repository::init(path)); @@ -158,7 +164,7 @@ fn default_repo_cfg(repo: &git2::Repository) { t!(cfg.set_str("user.name", "Foo Bar")); } -/// Create a new git repository with a project. +/// Create a new [`Project`] in a git [`Repository`] pub fn new(name: &str, callback: F) -> Project where F: FnOnce(ProjectBuilder) -> ProjectBuilder, @@ -166,8 +172,7 @@ where new_repo(name, callback).0 } -/// Create a new git repository with a project. -/// Returns both the Project and the git Repository. +/// Create a new [`Project`] with access to the [`Repository`] pub fn new_repo(name: &str, callback: F) -> (Project, git2::Repository) where F: FnOnce(ProjectBuilder) -> ProjectBuilder, @@ -182,14 +187,14 @@ where (git_project, repo) } -/// Add all files in the working directory to the git index. +/// *(`git2`)* Add all files in the working directory to the git index pub fn add(repo: &git2::Repository) { let mut index = t!(repo.index()); t!(index.add_all(["*"].iter(), git2::IndexAddOption::DEFAULT, None)); t!(index.write()); } -/// Add a git submodule to the repository. +/// *(`git2`)* Add a git submodule to the repository pub fn add_submodule<'a>( repo: &'a git2::Repository, url: &str, @@ -207,7 +212,7 @@ pub fn add_submodule<'a>( s } -/// Commit changes to the git repository. +/// *(`git2`)* Commit changes to the git repository pub fn commit(repo: &git2::Repository) -> git2::Oid { let tree_id = t!(t!(repo.index()).write_tree()); let sig = t!(repo.signature()); @@ -226,7 +231,7 @@ pub fn commit(repo: &git2::Repository) -> git2::Oid { )) } -/// Create a new tag in the git repository. +/// *(`git2`)* Create a new tag in the git repository pub fn tag(repo: &git2::Repository, name: &str) { let head = repo.head().unwrap().target().unwrap(); t!(repo.tag( diff --git a/crates/cargo-test-support/src/install.rs b/crates/cargo-test-support/src/install.rs index d71bdcfe9db..3a8503d75d8 100644 --- a/crates/cargo-test-support/src/install.rs +++ b/crates/cargo-test-support/src/install.rs @@ -1,3 +1,5 @@ +//! Helpers for testing `cargo install` + use std::env::consts::EXE_SUFFIX; use std::path::Path; @@ -23,6 +25,7 @@ fn check_has_installed_exe>(path: P, name: &'static str) -> bool path.as_ref().join("bin").join(exe(name)).is_file() } +/// `$name$EXE` pub fn exe(name: &str) -> String { format!("{}{}", name, EXE_SUFFIX) } diff --git a/crates/cargo-test-support/src/lib.rs b/crates/cargo-test-support/src/lib.rs index 6509c71d509..a9cafdd0b7a 100644 --- a/crates/cargo-test-support/src/lib.rs +++ b/crates/cargo-test-support/src/lib.rs @@ -2,11 +2,44 @@ //! //! See for a guide on writing tests. //! -//! WARNING: You might not want to use this outside of Cargo. +//! There are two places you can find API documentation +//! +//! - : +//! targeted at external tool developers testing cargo-related code +//! - Released with every rustc release +//! - : +//! targeted at cargo contributors +//! - Updated on each update of the `cargo` submodule in `rust-lang/rust` +//! +//! **WARNING:** You might not want to use this outside of Cargo. //! //! * This is designed for testing Cargo itself. Use at your own risk. //! * No guarantee on any stability across versions. //! * No feature request would be accepted unless proved useful for testing Cargo. +//! +//! # Example +//! +//! ```rust,no_run +//! use cargo_test_support::prelude::*; +//! use cargo_test_support::str; +//! use cargo_test_support::project; +//! +//! #[cargo_test] +//! fn some_test() { +//! let p = project() +//! .file("src/main.rs", r#"fn main() { println!("hi!"); }"#) +//! .build(); +//! +//! p.cargo("run --bin foo") +//! .with_stderr_data(str![[r#" +//! [COMPILING] foo [..] +//! [FINISHED] [..] +//! [RUNNING] `target/debug/foo` +//! "#]]) +//! .with_stdout_data(str![["hi!"]]) +//! .run(); +//! } +//! ``` #![allow(clippy::disallowed_methods)] #![allow(clippy::print_stderr)] @@ -30,6 +63,14 @@ use url::Url; use self::paths::CargoPathExt; +/// Unwrap a `Result` with a useful panic message +/// +/// # Example +/// +/// ```rust +/// use cargo_test_support::t; +/// t!(std::fs::read_to_string("Cargo.toml")); +/// ``` #[macro_export] macro_rules! t { ($e:expr) => { @@ -45,6 +86,7 @@ pub use snapbox::file; pub use snapbox::str; pub use snapbox::utils::current_dir; +/// `panic!`, reporting the specified error , see also [`t!`] #[track_caller] pub fn panic_error(what: &str, err: impl Into) -> ! { let err = err.into(); @@ -191,7 +233,13 @@ pub struct Project { /// Create a project to run tests against /// -/// The project can be constructed programmatically or from the filesystem with [`Project::from_template`] +/// - Creates a [`basic_manifest`] if one isn't supplied +/// +/// To get started, see: +/// - [`project`] +/// - [`project_in`] +/// - [`project_in_home`] +/// - [`Project::from_template`] #[must_use] pub struct ProjectBuilder { root: Project, @@ -201,16 +249,21 @@ pub struct ProjectBuilder { } impl ProjectBuilder { - /// Root of the project, ex: `/path/to/cargo/target/cit/t0/foo` + /// Root of the project + /// + /// ex: `$CARGO_TARGET_TMPDIR/cit/t0/foo` pub fn root(&self) -> PathBuf { self.root.root() } - /// Project's debug dir, ex: `/path/to/cargo/target/cit/t0/foo/target/debug` + /// Project's debug dir + /// + /// ex: `$CARGO_TARGET_TMPDIR/cit/t0/foo/target/debug` pub fn target_debug_dir(&self) -> PathBuf { self.root.target_debug_dir() } + /// Create project in `root` pub fn new(root: PathBuf) -> ProjectBuilder { ProjectBuilder { root: Project { root }, @@ -220,6 +273,7 @@ impl ProjectBuilder { } } + /// Create project, relative to [`paths::root`] pub fn at>(mut self, path: P) -> Self { self.root = Project { root: paths::root().join(path), @@ -324,30 +378,40 @@ impl Project { Self { root: project_root } } - /// Root of the project, ex: `/path/to/cargo/target/cit/t0/foo` + /// Root of the project + /// + /// ex: `$CARGO_TARGET_TMPDIR/cit/t0/foo` pub fn root(&self) -> PathBuf { self.root.clone() } - /// Project's target dir, ex: `/path/to/cargo/target/cit/t0/foo/target` + /// Project's target dir + /// + /// ex: `$CARGO_TARGET_TMPDIR/cit/t0/foo/target` pub fn build_dir(&self) -> PathBuf { self.root().join("target") } - /// Project's debug dir, ex: `/path/to/cargo/target/cit/t0/foo/target/debug` + /// Project's debug dir + /// + /// ex: `$CARGO_TARGET_TMPDIR/cit/t0/foo/target/debug` pub fn target_debug_dir(&self) -> PathBuf { self.build_dir().join("debug") } - /// File url for root, ex: `file:///path/to/cargo/target/cit/t0/foo` + /// File url for root + /// + /// ex: `file://$CARGO_TARGET_TMPDIR/cit/t0/foo` pub fn url(&self) -> Url { use paths::CargoPathExt; self.root().to_url() } /// Path to an example built as a library. + /// /// `kind` should be one of: "lib", "rlib", "staticlib", "dylib", "proc-macro" - /// ex: `/path/to/cargo/target/cit/t0/foo/target/debug/examples/libex.rlib` + /// + /// ex: `$CARGO_TARGET_TMPDIR/cit/t0/foo/target/debug/examples/libex.rlib` pub fn example_lib(&self, name: &str, kind: &str) -> PathBuf { self.target_debug_dir() .join("examples") @@ -355,7 +419,8 @@ impl Project { } /// Path to a debug binary. - /// ex: `/path/to/cargo/target/cit/t0/foo/target/debug/foo` + /// + /// ex: `$CARGO_TARGET_TMPDIR/cit/t0/foo/target/debug/foo` pub fn bin(&self, b: &str) -> PathBuf { self.build_dir() .join("debug") @@ -363,7 +428,8 @@ impl Project { } /// Path to a release binary. - /// ex: `/path/to/cargo/target/cit/t0/foo/target/release/foo` + /// + /// ex: `$CARGO_TARGET_TMPDIR/cit/t0/foo/target/release/foo` pub fn release_bin(&self, b: &str) -> PathBuf { self.build_dir() .join("release") @@ -371,7 +437,8 @@ impl Project { } /// Path to a debug binary for a specific target triple. - /// ex: `/path/to/cargo/target/cit/t0/foo/target/i686-apple-darwin/debug/foo` + /// + /// ex: `$CARGO_TARGET_TMPDIR/cit/t0/foo/target/i686-apple-darwin/debug/foo` pub fn target_bin(&self, target: &str, b: &str) -> PathBuf { self.build_dir().join(target).join("debug").join(&format!( "{}{}", @@ -380,25 +447,36 @@ impl Project { )) } - /// Returns an iterator of paths matching the glob pattern, which is - /// relative to the project root. + /// Returns an iterator of paths within [`Project::root`] matching the glob pattern pub fn glob>(&self, pattern: P) -> glob::Paths { let pattern = self.root().join(pattern); glob::glob(pattern.to_str().expect("failed to convert pattern to str")) .expect("failed to glob") } - /// Changes the contents of an existing file. + /// Overwrite a file with new content + /// + // # Example: + /// + /// ```no_run + /// # let p = cargo_test_support::project().build(); + /// p.change_file("src/lib.rs", "fn new_fn() {}"); + /// ``` pub fn change_file(&self, path: &str, body: &str) { FileBuilder::new(self.root().join(path), body, false).mk() } /// Creates a `ProcessBuilder` to run a program in the project /// and wrap it in an Execs to assert on the execution. - /// Example: - /// p.process(&p.bin("foo")) - /// .with_stdout("bar\n") - /// .run(); + /// + /// # Example: + /// + /// ```no_run + /// # let p = cargo_test_support::project().build(); + /// p.process(&p.bin("foo")) + /// .with_stdout("bar\n") + /// .run(); + /// ``` pub fn process>(&self, program: T) -> Execs { let mut p = process(program); p.cwd(self.root()); @@ -406,9 +484,17 @@ impl Project { } /// Creates a `ProcessBuilder` to run cargo. + /// /// Arguments can be separated by spaces. - /// Example: - /// p.cargo("build --bin foo").run(); + /// + /// For `cargo run`, see [`Project::rename_run`]. + /// + /// # Example: + /// + /// ```no_run + /// # let p = cargo_test_support::project().build(); + /// p.cargo("build --bin foo").run(); + /// ``` pub fn cargo(&self, cmd: &str) -> Execs { let cargo = cargo_exe(); let mut execs = self.process(&cargo); @@ -482,27 +568,41 @@ impl Project { } } -// Generates a project layout +/// Generates a project layout, see [`ProjectBuilder`] pub fn project() -> ProjectBuilder { ProjectBuilder::new(paths::root().join("foo")) } -// Generates a project layout in given directory +/// Generates a project layout in given directory, see [`ProjectBuilder`] pub fn project_in(dir: &str) -> ProjectBuilder { ProjectBuilder::new(paths::root().join(dir).join("foo")) } -// Generates a project layout inside our fake home dir +/// Generates a project layout inside our fake home dir, see [`ProjectBuilder`] pub fn project_in_home(name: &str) -> ProjectBuilder { ProjectBuilder::new(paths::home().join(name)) } // === Helpers === -pub fn main_file(println: &str, deps: &[&str]) -> String { +/// Generate a `main.rs` printing the specified text +/// +/// ```rust +/// # use cargo_test_support::main_file; +/// # mod dep { +/// # fn bar() -> &'static str { +/// # "world" +/// # } +/// # } +/// main_file( +/// r#""hello {}", dep::bar()"#, +/// &[] +/// ); +/// ``` +pub fn main_file(println: &str, externed_deps: &[&str]) -> String { let mut buf = String::new(); - for dep in deps.iter() { + for dep in externed_deps.iter() { buf.push_str(&format!("extern crate {};\n", dep)); } @@ -513,6 +613,7 @@ pub fn main_file(println: &str, deps: &[&str]) -> String { buf } +/// Path to the cargo binary pub fn cargo_exe() -> PathBuf { snapbox::cmd::cargo_bin("cargo") } @@ -524,12 +625,20 @@ pub fn cargo_exe() -> PathBuf { /// does not have access to the raw `ExitStatus` because `ProcessError` needs /// to be serializable (for the Rustc cache), and `ExitStatus` does not /// provide a constructor. -pub struct RawOutput { - pub code: Option, - pub stdout: Vec, - pub stderr: Vec, +struct RawOutput { + #[allow(dead_code)] + code: Option, + stdout: Vec, + #[allow(dead_code)] + stderr: Vec, } +/// Run and verify a [`ProcessBuilder`] +/// +/// Construct with +/// - [`execs`] +/// - [`cargo_process`] +/// - [`Project`] methods #[must_use] #[derive(Clone)] pub struct Execs { @@ -560,7 +669,10 @@ impl Execs { self.process_builder = Some(p); self } +} +/// # Configure assertions +impl Execs { /// Verifies that stdout is equal to the given lines. /// See [`compare`] for supported patterns. #[deprecated(note = "replaced with `Execs::with_stdout_data(expected)`")] @@ -790,7 +902,10 @@ impl Execs { } self } +} +/// # Configure the process +impl Execs { /// Forward subordinate process stdout/stderr to the terminal. /// Useful for printf debugging of the tests. /// CAUTION: CI will fail if you leave this in your test! @@ -844,20 +959,6 @@ impl Execs { self } - pub fn exec_with_output(&mut self) -> Result { - self.ran = true; - // TODO avoid unwrap - let p = (&self.process_builder).clone().unwrap(); - p.exec_with_output() - } - - pub fn build_command(&mut self) -> Command { - self.ran = true; - // TODO avoid unwrap - let p = (&self.process_builder).clone().unwrap(); - p.build_command() - } - /// Enables nightly features for testing /// /// The list of reasons should be why nightly cargo is needed. If it is @@ -906,6 +1007,23 @@ impl Execs { } self } +} + +/// # Run and verify the process +impl Execs { + pub fn exec_with_output(&mut self) -> Result { + self.ran = true; + // TODO avoid unwrap + let p = (&self.process_builder).clone().unwrap(); + p.exec_with_output() + } + + pub fn build_command(&mut self) -> Command { + self.ran = true; + // TODO avoid unwrap + let p = (&self.process_builder).clone().unwrap(); + p.build_command() + } #[track_caller] pub fn run(&mut self) { @@ -1113,6 +1231,7 @@ impl Drop for Execs { } } +/// Run and verify a process, see [`Execs`] pub fn execs() -> Execs { Execs { ran: false, @@ -1138,6 +1257,7 @@ pub fn execs() -> Execs { } } +/// Generate a basic `Cargo.toml` pub fn basic_manifest(name: &str, version: &str) -> String { format!( r#" @@ -1151,6 +1271,7 @@ pub fn basic_manifest(name: &str, version: &str) -> String { ) } +/// Generate a `Cargo.toml` with the specified `bin.name` pub fn basic_bin_manifest(name: &str) -> String { format!( r#" @@ -1169,6 +1290,7 @@ pub fn basic_bin_manifest(name: &str) -> String { ) } +/// Generate a `Cargo.toml` with the specified `lib.name` pub fn basic_lib_manifest(name: &str) -> String { format!( r#" @@ -1237,8 +1359,13 @@ pub fn is_nightly() -> bool { && (vv.contains("-nightly") || vv.contains("-dev")) } -pub fn process>(t: T) -> ProcessBuilder { - _process(t.as_ref()) +/// Run `$bin` in the test's environment, see [`ProcessBuilder`] +/// +/// For more on the test environment, see +/// - [`paths::root`] +/// - [`TestEnvCommandExt`] +pub fn process>(bin: T) -> ProcessBuilder { + _process(bin.as_ref()) } fn _process(t: &OsStr) -> ProcessBuilder { @@ -1428,17 +1555,19 @@ impl ArgLineCommandExt for snapbox::cmd::Command { } } -pub fn cargo_process(s: &str) -> Execs { +/// Run `cargo $arg_line`, see [`Execs`] +pub fn cargo_process(arg_line: &str) -> Execs { let cargo = cargo_exe(); let mut p = process(&cargo); p.env("CARGO", cargo); - p.arg_line(s); + p.arg_line(arg_line); execs().with_process_builder(p) } -pub fn git_process(s: &str) -> ProcessBuilder { +/// Run `git $arg_line`, see [`ProcessBuilder`] +pub fn git_process(arg_line: &str) -> ProcessBuilder { let mut p = process("git"); - p.arg_line(s); + p.arg_line(arg_line); p } diff --git a/crates/cargo-test-support/src/paths.rs b/crates/cargo-test-support/src/paths.rs index 8328992fb62..e9d51502aad 100644 --- a/crates/cargo-test-support/src/paths.rs +++ b/crates/cargo-test-support/src/paths.rs @@ -1,3 +1,5 @@ +//! Access common paths and manipulate the filesystem + use filetime::FileTime; use std::cell::RefCell; @@ -41,6 +43,9 @@ fn set_global_root(tmp_dir: Option<&'static str>) { } } +/// Path to the parent directory of all test [`root`]s +/// +/// ex: `$CARGO_TARGET_TMPDIR/cit` pub fn global_root() -> PathBuf { let lock = GLOBAL_ROOT .get_or_init(|| Default::default()) @@ -64,10 +69,12 @@ thread_local! { static TEST_ID: RefCell> = RefCell::new(None); } +/// See [`init_root`] pub struct TestIdGuard { _private: (), } +/// For test harnesses like [`crate::cargo_test`] pub fn init_root(tmp_dir: Option<&'static str>) -> TestIdGuard { static NEXT_ID: AtomicUsize = AtomicUsize::new(0); @@ -90,6 +97,9 @@ impl Drop for TestIdGuard { } } +/// Path to the test's filesystem scratchpad +/// +/// ex: `$CARGO_TARGET_TMPDIR/cit/t0` pub fn root() -> PathBuf { let id = TEST_ID.with(|n| { n.borrow().expect( @@ -103,6 +113,9 @@ pub fn root() -> PathBuf { root } +/// Path to the current test's `$HOME` +/// +/// ex: `$CARGO_TARGET_TMPDIR/cit/t0/home` pub fn home() -> PathBuf { let mut path = root(); path.push("home"); @@ -110,10 +123,14 @@ pub fn home() -> PathBuf { path } +/// Path to the current test's `$CARGO_HOME` +/// +/// ex: `$CARGO_TARGET_TMPDIR/cit/t0/home/.cargo` pub fn cargo_home() -> PathBuf { home().join(".cargo") } +/// Common path and file operations pub trait CargoPathExt { fn to_url(&self) -> url::Url; @@ -275,18 +292,29 @@ where /// Get the filename for a library. /// -/// `kind` should be one of: "lib", "rlib", "staticlib", "dylib", "proc-macro" +/// `kind` should be one of: +/// - `lib` +/// - `rlib` +/// - `staticlib` +/// - `dylib` +/// - `proc-macro` /// -/// For example, dynamic library named "foo" would return: -/// - macOS: "libfoo.dylib" -/// - Windows: "foo.dll" -/// - Unix: "libfoo.so" +/// # Examples +/// ``` +/// # use cargo_test_support::paths::get_lib_filename; +/// get_lib_filename("foo", "dylib"); +/// ``` +/// would return: +/// - macOS: `"libfoo.dylib"` +/// - Windows: `"foo.dll"` +/// - Unix: `"libfoo.so"` pub fn get_lib_filename(name: &str, kind: &str) -> String { let prefix = get_lib_prefix(kind); let extension = get_lib_extension(kind); format!("{}{}.{}", prefix, name, extension) } +/// See [`get_lib_filename`] for more details pub fn get_lib_prefix(kind: &str) -> &str { match kind { "lib" | "rlib" => "lib", @@ -301,6 +329,7 @@ pub fn get_lib_prefix(kind: &str) -> &str { } } +/// See [`get_lib_filename`] for more details pub fn get_lib_extension(kind: &str) -> &str { match kind { "lib" | "rlib" => "rlib", @@ -324,7 +353,7 @@ pub fn get_lib_extension(kind: &str) -> &str { } } -/// Returns the sysroot as queried from rustc. +/// Path to `rustc`s sysroot pub fn sysroot() -> String { let output = Command::new("rustc") .arg("--print=sysroot") diff --git a/crates/cargo-test-support/src/publish.rs b/crates/cargo-test-support/src/publish.rs index f850330c191..a673c466b94 100644 --- a/crates/cargo-test-support/src/publish.rs +++ b/crates/cargo-test-support/src/publish.rs @@ -1,3 +1,62 @@ +//! Helpers for testing `cargo package` / `cargo publish` +//! +//! # Example +//! +//! ```no_run +//! # use cargo_test_support::registry::RegistryBuilder; +//! # use cargo_test_support::publish::validate_upload; +//! # use cargo_test_support::project; +//! // This replaces `registry::init()` and must be called before `Package::new().publish()` +//! let registry = RegistryBuilder::new().http_api().http_index().build(); +//! +//! let p = project() +//! .file( +//! "Cargo.toml", +//! r#" +//! [package] +//! name = "foo" +//! version = "0.0.1" +//! edition = "2015" +//! authors = [] +//! license = "MIT" +//! description = "foo" +//! "#, +//! ) +//! .file("src/main.rs", "fn main() {}") +//! .build(); +//! +//! p.cargo("publish --no-verify") +//! .replace_crates_io(registry.index_url()) +//! .run(); +//! +//! validate_upload( +//! r#" +//! { +//! "authors": [], +//! "badges": {}, +//! "categories": [], +//! "deps": [], +//! "description": "foo", +//! "documentation": null, +//! "features": {}, +//! "homepage": null, +//! "keywords": [], +//! "license": "MIT", +//! "license_file": null, +//! "links": null, +//! "name": "foo", +//! "readme": null, +//! "readme_file": null, +//! "repository": null, +//! "rust_version": null, +//! "vers": "0.0.1" +//! } +//! "#, +//! "foo-0.0.1.crate", +//! &["Cargo.lock", "Cargo.toml", "Cargo.toml.orig", "src/main.rs"], +//! ); +//! ``` + use crate::compare::{assert_match_exact, find_json_mismatch}; use crate::registry::{self, alt_api_path, FeatureMap}; use flate2::read::GzDecoder; @@ -17,7 +76,7 @@ where Ok(u32::from_le_bytes(buf)) } -/// Checks the result of a crate publish. +/// Check the `cargo publish` API call pub fn validate_upload(expected_json: &str, expected_crate_name: &str, expected_files: &[&str]) { let new_path = registry::api_path().join("api/v1/crates/new"); _validate_upload( @@ -29,7 +88,7 @@ pub fn validate_upload(expected_json: &str, expected_crate_name: &str, expected_ ); } -/// Checks the result of a crate publish, along with the contents of the files. +/// Check the `cargo publish` API call, with file contents pub fn validate_upload_with_contents( expected_json: &str, expected_crate_name: &str, @@ -46,7 +105,7 @@ pub fn validate_upload_with_contents( ); } -/// Checks the result of a crate publish to an alternative registry. +/// Check the `cargo publish` API call to the alternative test registry pub fn validate_alt_upload( expected_json: &str, expected_crate_name: &str, diff --git a/crates/cargo-test-support/src/registry.rs b/crates/cargo-test-support/src/registry.rs index b63ee44350e..b69e981c844 100644 --- a/crates/cargo-test-support/src/registry.rs +++ b/crates/cargo-test-support/src/registry.rs @@ -1,3 +1,46 @@ +//! Interact with the [`TestRegistry`] +//! +//! # Example +//! +//! ```no_run +//! use cargo_test_support::registry::Package; +//! use cargo_test_support::project; +//! +//! // Publish package "a" depending on "b". +//! Package::new("a", "1.0.0") +//! .dep("b", "1.0.0") +//! .file("src/lib.rs", r#" +//! extern crate b; +//! pub fn f() -> i32 { b::f() * 2 } +//! "#) +//! .publish(); +//! +//! // Publish package "b". +//! Package::new("b", "1.0.0") +//! .file("src/lib.rs", r#" +//! pub fn f() -> i32 { 12 } +//! "#) +//! .publish(); +//! +//! // Create a project that uses package "a". +//! let p = project() +//! .file("Cargo.toml", r#" +//! [package] +//! name = "foo" +//! version = "0.0.1" +//! +//! [dependencies] +//! a = "1.0" +//! "#) +//! .file("src/main.rs", r#" +//! extern crate a; +//! fn main() { println!("{}", a::f()); } +//! "#) +//! .build(); +//! +//! p.cargo("run").with_stdout("24").run(); +//! ``` + use crate::git::repo; use crate::paths; use crate::publish::{create_index_line, write_to_index}; @@ -20,39 +63,64 @@ use time::format_description::well_known::Rfc3339; use time::{Duration, OffsetDateTime}; use url::Url; -/// Gets the path to the local index pretending to be crates.io. This is a Git repo +/// Path to the local index for psuedo-crates.io. +/// +/// This is a Git repo /// initialized with a `config.json` file pointing to `dl_path` for downloads /// and `api_path` for uploads. +/// +/// ex: `$CARGO_TARGET_TMPDIR/cit/t0/registry` pub fn registry_path() -> PathBuf { generate_path("registry") } -/// Gets the path for local web API uploads. Cargo will place the contents of a web API + +/// Path to the local web API uploads +/// +/// Cargo will place the contents of a web API /// request here. For example, `api/v1/crates/new` is the result of publishing a crate. +/// +/// ex: `$CARGO_TARGET_TMPDIR/cit/t0/api` pub fn api_path() -> PathBuf { generate_path("api") } -/// Gets the path where crates can be downloaded using the web API endpoint. Crates + +/// Path to download `.crate` files using the web API endpoint. +/// +/// Crates /// should be organized as `{name}/{version}/download` to match the web API /// endpoint. This is rarely used and must be manually set up. -fn dl_path() -> PathBuf { +/// +/// ex: `$CARGO_TARGET_TMPDIR/cit/t0/dl` +pub fn dl_path() -> PathBuf { generate_path("dl") } -/// Gets the alternative-registry version of `registry_path`. -fn alt_registry_path() -> PathBuf { + +/// Path to the alternative-registry version of [`registry_path`] +/// +/// ex: `$CARGO_TARGET_TMPDIR/cit/t0/alternative-registry` +pub fn alt_registry_path() -> PathBuf { generate_path("alternative-registry") } -/// Gets the alternative-registry version of `registry_url`. + +/// URL to the alternative-registry version of `registry_url` fn alt_registry_url() -> Url { generate_url("alternative-registry") } -/// Gets the alternative-registry version of `dl_path`. + +/// Path to the alternative-registry version of [`dl_path`] +/// +/// ex: `$CARGO_TARGET_TMPDIR/cit/t0/alternative-dl` pub fn alt_dl_path() -> PathBuf { generate_path("alternative-dl") } -/// Gets the alternative-registry version of `api_path`. + +/// Path to the alternative-registry version of [`api_path`] +/// +/// ex: `$CARGO_TARGET_TMPDIR/cit/t0/alternative-api` pub fn alt_api_path() -> PathBuf { generate_path("alternative-api") } + fn generate_path(name: &str) -> PathBuf { paths::root().join(name) } @@ -60,6 +128,7 @@ fn generate_url(name: &str) -> Url { Url::from_file_path(generate_path(name)).ok().unwrap() } +/// Auth-token for publishing, see [`RegistryBuilder::token`] #[derive(Clone)] pub enum Token { Plaintext(String), @@ -68,6 +137,7 @@ pub enum Token { impl Token { /// This is a valid PASETO secret key. + /// /// This one is already publicly available as part of the text of the RFC so is safe to use for tests. pub fn rfc_key() -> Token { Token::Keys( @@ -80,7 +150,9 @@ impl Token { type RequestCallback = Box Response>; -/// A builder for initializing registries. +/// Prepare a local [`TestRegistry`] fixture +/// +/// See also [`init`] and [`alt_init`] pub struct RegistryBuilder { /// If set, configures an alternate registry with the given name. alternative: Option, @@ -108,6 +180,9 @@ pub struct RegistryBuilder { credential_provider: Option, } +/// A local registry fixture +/// +/// Most tests won't need to call this directly but instead interact with [`Package`] pub struct TestRegistry { server: Option, index_url: Url, @@ -459,71 +534,31 @@ impl RegistryBuilder { } } -/// A builder for creating a new package in a registry. +/// Published package builder for [`TestRegistry`] /// /// This uses "source replacement" using an automatically generated /// `.cargo/config` file to ensure that dependencies will use these packages /// instead of contacting crates.io. See `source-replacement.md` for more /// details on how source replacement works. /// -/// Call `publish` to finalize and create the package. +/// Call [`Package::publish`] to finalize and create the package. /// /// If no files are specified, an empty `lib.rs` file is automatically created. /// /// The `Cargo.toml` file is automatically generated based on the methods -/// called on `Package` (for example, calling `dep()` will add to the +/// called on `Package` (for example, calling [`Package::dep()`] will add to the /// `[dependencies]` automatically). You may also specify a `Cargo.toml` file /// to override the generated one. /// /// This supports different registry types: /// - Regular source replacement that replaces `crates.io` (the default). /// - A "local registry" which is a subset for vendoring (see -/// `Package::local`). +/// [`Package::local`]). /// - An "alternative registry" which requires specifying the registry name -/// (see `Package::alternative`). +/// (see [`Package::alternative`]). /// /// This does not support "directory sources". See `directory.rs` for /// `VendorPackage` which implements directory sources. -/// -/// # Example -/// ```no_run -/// use cargo_test_support::registry::Package; -/// use cargo_test_support::project; -/// -/// // Publish package "a" depending on "b". -/// Package::new("a", "1.0.0") -/// .dep("b", "1.0.0") -/// .file("src/lib.rs", r#" -/// extern crate b; -/// pub fn f() -> i32 { b::f() * 2 } -/// "#) -/// .publish(); -/// -/// // Publish package "b". -/// Package::new("b", "1.0.0") -/// .file("src/lib.rs", r#" -/// pub fn f() -> i32 { 12 } -/// "#) -/// .publish(); -/// -/// // Create a project that uses package "a". -/// let p = project() -/// .file("Cargo.toml", r#" -/// [package] -/// name = "foo" -/// version = "0.0.1" -/// -/// [dependencies] -/// a = "1.0" -/// "#) -/// .file("src/main.rs", r#" -/// extern crate a; -/// fn main() { println!("{}", a::f()); } -/// "#) -/// .build(); -/// -/// p.cargo("run").with_stdout("24").run(); -/// ``` #[must_use] pub struct Package { name: String, @@ -544,6 +579,7 @@ pub struct Package { pub(crate) type FeatureMap = BTreeMap>; +/// Published package dependency builder, see [`Package::add_dep`] #[derive(Clone)] pub struct Dependency { name: String, @@ -582,14 +618,18 @@ struct PackageFile { const DEFAULT_MODE: u32 = 0o644; -/// Initializes the on-disk registry and sets up the config so that crates.io -/// is replaced with the one on disk. +/// Setup a local psuedo-crates.io [`TestRegistry`] +/// +/// This is implicitly called by [`Package::new`]. +/// +/// When calling `cargo publish`, see instead [`crate::publish`]. pub fn init() -> TestRegistry { RegistryBuilder::new().build() } -/// Variant of `init` that initializes the "alternative" registry and crates.io -/// replacement. +/// Setup a local "alternative" [`TestRegistry`] +/// +/// When calling `cargo publish`, see instead [`crate::publish`]. pub fn alt_init() -> TestRegistry { init(); RegistryBuilder::new().alternative().build() @@ -1234,6 +1274,8 @@ impl Package { /// See `src/doc/src/reference/registries.md` for more details on /// alternative registries. See `alt_registry.rs` for the tests that use /// this. + /// + /// **Requires:** [`alt_init`] pub fn alternative(&mut self, alternative: bool) -> &mut Package { self.alternative = alternative; self @@ -1666,6 +1708,7 @@ impl Package { } } +/// Generate a checksum pub fn cksum(s: &[u8]) -> String { Sha256::new().update(s).finish_hex() } diff --git a/src/doc/contrib/src/tests/writing.md b/src/doc/contrib/src/tests/writing.md index 8994dd3f945..be92184af84 100644 --- a/src/doc/contrib/src/tests/writing.md +++ b/src/doc/contrib/src/tests/writing.md @@ -12,9 +12,8 @@ and verify its behavior, located in the [`testsuite`] directory. The There are two styles of tests that can roughly be categorized as - functional tests - The fixture is programmatically defined - - The assertions are regular string comparisons + - The assertions may be in-source snapshots, hard-coded strings, or programmatically generated - Easier to share in an issue as a code block is completely self-contained - - More resilient to insignificant changes though ui tests are easy to update when a change does occur - ui tests - The fixture is file-based - The assertions use file-backed snapshots that can be updated with an env variable @@ -31,6 +30,8 @@ stdout and stderr output against the expected output. Generally, a functional test will be placed in `tests/testsuite/.rs` and will look roughly like: ```rust,ignore use cargo_test_support::prelude::*; +use cargo_test_support::str; +use cargo_test_support::project; #[cargo_test] fn () { @@ -39,20 +40,19 @@ fn () { .build(); p.cargo("run --bin foo") - .with_stderr( - "\ - [COMPILING] foo [..] - [FINISHED] [..] - [RUNNING] `target/debug/foo` - ", - ) - .with_stdout("hi!") + .with_stderr_data(str![[r#" +[COMPILING] foo [..] +[FINISHED] [..] +[RUNNING] `target/debug/foo` +"#]]) + .with_stdout_data(str![["hi!"]]) .run(); - } } ``` -The [`#[cargo_test]` attribute](#cargo_test-attribute) is used in place of `#[test]` to inject some setup code. +The [`#[cargo_test]` attribute][cargo_test attribute] is used in place of +`#[test]` to inject some setup code and declare requirements for running the +test. [`ProjectBuilder`] via `project()`: - Each project is in a separate directory in the sandbox @@ -64,50 +64,6 @@ The [`#[cargo_test]` attribute](#cargo_test-attribute) is used in place of `#[te - See [`support::compare`] for an explanation of the string pattern matching. Patterns are used to make it easier to match against the expected output. -#### `#[cargo_test]` attribute - -The `#[cargo_test]` attribute injects code which does some setup before starting the test. -It will create a filesystem "sandbox" under the "cargo integration test" directory for each test, such as `/path/to/cargo/target/tmp/cit/t123/`. -The sandbox will contain a `home` directory that will be used instead of your normal home directory. - -The `#[cargo_test]` attribute takes several options that will affect how the test is generated. -They are listed in parentheses separated with commas, such as: - -```rust,ignore -#[cargo_test(nightly, reason = "-Zfoo is unstable")] -``` - -The options it supports are: - -* `nightly` --- This will cause the test to be ignored if not running on the nightly toolchain. - This is useful for tests that use unstable options in `rustc` or `rustdoc`. - These tests are run in Cargo's CI, but are disabled in rust-lang/rust's CI due to the difficulty of updating both repos simultaneously. - A `reason` field is required to explain why it is nightly-only. -* `build_std_real` --- This is a "real" `-Zbuild-std` test (in the `build_std` integration test). - This only runs on nightly, and only if the environment variable `CARGO_RUN_BUILD_STD_TESTS` is set (these tests on run on Linux). -* `build_std_mock` --- This is a "mock" `-Zbuild-std` test (which uses a mock standard library). - This only runs on nightly, and is disabled for windows-gnu. -* `requires_` --- This indicates a command that is required to be installed to be run. - For example, `requires_rustfmt` means the test will only run if the executable `rustfmt` is installed. - These tests are *always* run on CI. - This is mainly used to avoid requiring contributors from having every dependency installed. -* `>=1.64` --- This indicates that the test will only run with the given version of `rustc` or newer. - This can be used when a new `rustc` feature has been stabilized that the test depends on. - If this is specified, a `reason` is required to explain why it is being checked. -* `public_network_test` --- This tests contacts the public internet. - These tests are disabled unless the `CARGO_PUBLIC_NETWORK_TESTS` environment variable is set. - Use of this should be *extremely rare*, please avoid using it if possible. - The hosts it contacts should have a relatively high confidence that they are reliable and stable (such as github.com), especially in CI. - The tests should be carefully considered for developer security and privacy as well. -* `container_test` --- This indicates that it is a test that uses Docker. - These tests are disabled unless the `CARGO_CONTAINER_TESTS` environment variable is set. - This requires that you have Docker installed. - The SSH tests also assume that you have OpenSSH installed. - These should work on Linux, macOS, and Windows where possible. - Unfortunately these tests are not run in CI for macOS or Windows (no Docker on macOS, and Windows does not support Linux images). - See [`crates/cargo-test-support/src/containers.rs`](https://github.com/rust-lang/cargo/blob/master/crates/cargo-test-support/src/containers.rs) for more on writing these tests. -* `ignore_windows="reason"` --- Indicates that the test should be ignored on windows for the given reason. - #### Testing Nightly Features If you are testing a Cargo feature that only works on "nightly" Cargo, then @@ -330,6 +286,7 @@ environment. The general process is: 2. Set a breakpoint, for example: `b generate_root_units` 3. Run with arguments: `r check` +[cargo_test attribute]: https://doc.rust-lang.org/nightly/nightly-rustc/cargo_test_macro/attr.cargo_test.html [`testsuite`]: https://github.com/rust-lang/cargo/tree/master/tests/testsuite/ [`ProjectBuilder`]: https://github.com/rust-lang/cargo/blob/d847468768446168b596f721844193afaaf9d3f2/crates/cargo-test-support/src/lib.rs#L196-L202 [`Execs`]: https://github.com/rust-lang/cargo/blob/d847468768446168b596f721844193afaaf9d3f2/crates/cargo-test-support/src/lib.rs#L531-L550