//! # Cargo test support.
//!
//! See for a guide on writing tests.
#![allow(clippy::all)]
use std::env;
use std::ffi::OsStr;
use std::fmt::Write;
use std::fs;
use std::os;
use std::path::{Path, PathBuf};
use std::process::{Command, Output};
use std::str;
use std::time::{self, Duration};
use anyhow::{bail, Result};
use cargo_util::{is_ci, ProcessBuilder, ProcessError};
use serde_json;
use url::Url;
use self::paths::CargoPathExt;
#[macro_export]
macro_rules! t {
($e:expr) => {
match $e {
Ok(e) => e,
Err(e) => $crate::panic_error(&format!("failed running {}", stringify!($e)), e),
}
};
}
#[macro_export]
macro_rules! curr_dir {
() => {
$crate::_curr_dir(std::path::Path::new(file!()));
};
}
#[doc(hidden)]
pub fn _curr_dir(mut file_path: &'static Path) -> &'static Path {
if !file_path.exists() {
// HACK: Must be running in the rust-lang/rust workspace, adjust the paths accordingly.
let prefix = PathBuf::from("src").join("tools").join("cargo");
if let Ok(crate_relative) = file_path.strip_prefix(prefix) {
file_path = crate_relative
}
}
assert!(file_path.exists(), "{} does not exist", file_path.display());
file_path.parent().unwrap()
}
#[track_caller]
pub fn panic_error(what: &str, err: impl Into) -> ! {
let err = err.into();
pe(what, err);
#[track_caller]
fn pe(what: &str, err: anyhow::Error) -> ! {
let mut result = format!("{}\nerror: {}", what, err);
for cause in err.chain().skip(1) {
drop(writeln!(result, "\nCaused by:"));
drop(write!(result, "{}", cause));
}
panic!("\n{}", result);
}
}
pub use cargo_test_macro::cargo_test;
pub mod compare;
pub mod containers;
pub mod cross_compile;
mod diff;
pub mod git;
pub mod install;
pub mod paths;
pub mod publish;
pub mod registry;
pub mod tools;
pub mod prelude {
pub use crate::ArgLine;
pub use crate::CargoCommand;
pub use crate::ChannelChanger;
pub use crate::TestEnv;
}
/*
*
* ===== Builders =====
*
*/
#[derive(PartialEq, Clone)]
struct FileBuilder {
path: PathBuf,
body: String,
executable: bool,
}
impl FileBuilder {
pub fn new(path: PathBuf, body: &str, executable: bool) -> FileBuilder {
FileBuilder {
path,
body: body.to_string(),
executable: executable,
}
}
fn mk(&mut self) {
if self.executable {
self.path.set_extension(env::consts::EXE_EXTENSION);
}
self.dirname().mkdir_p();
fs::write(&self.path, &self.body)
.unwrap_or_else(|e| panic!("could not create file {}: {}", self.path.display(), e));
#[cfg(unix)]
if self.executable {
use std::os::unix::fs::PermissionsExt;
let mut perms = fs::metadata(&self.path).unwrap().permissions();
let mode = perms.mode();
perms.set_mode(mode | 0o111);
fs::set_permissions(&self.path, perms).unwrap();
}
}
fn dirname(&self) -> &Path {
self.path.parent().unwrap()
}
}
#[derive(PartialEq, Clone)]
struct SymlinkBuilder {
dst: PathBuf,
src: PathBuf,
src_is_dir: bool,
}
impl SymlinkBuilder {
pub fn new(dst: PathBuf, src: PathBuf) -> SymlinkBuilder {
SymlinkBuilder {
dst,
src,
src_is_dir: false,
}
}
pub fn new_dir(dst: PathBuf, src: PathBuf) -> SymlinkBuilder {
SymlinkBuilder {
dst,
src,
src_is_dir: true,
}
}
#[cfg(unix)]
fn mk(&self) {
self.dirname().mkdir_p();
t!(os::unix::fs::symlink(&self.dst, &self.src));
}
#[cfg(windows)]
fn mk(&mut self) {
self.dirname().mkdir_p();
if self.src_is_dir {
t!(os::windows::fs::symlink_dir(&self.dst, &self.src));
} else {
if let Some(ext) = self.dst.extension() {
if ext == env::consts::EXE_EXTENSION {
self.src.set_extension(ext);
}
}
t!(os::windows::fs::symlink_file(&self.dst, &self.src));
}
}
fn dirname(&self) -> &Path {
self.src.parent().unwrap()
}
}
/// A cargo project to run tests against.
///
/// See [`ProjectBuilder`] or [`Project::from_template`] to get started.
pub struct Project {
root: PathBuf,
}
/// Create a project to run tests against
///
/// The project can be constructed programmatically or from the filesystem with [`Project::from_template`]
#[must_use]
pub struct ProjectBuilder {
root: Project,
files: Vec,
symlinks: Vec,
no_manifest: bool,
}
impl ProjectBuilder {
/// Root of the project, ex: `/path/to/cargo/target/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`
pub fn target_debug_dir(&self) -> PathBuf {
self.root.target_debug_dir()
}
pub fn new(root: PathBuf) -> ProjectBuilder {
ProjectBuilder {
root: Project { root },
files: vec![],
symlinks: vec![],
no_manifest: false,
}
}
pub fn at>(mut self, path: P) -> Self {
self.root = Project {
root: paths::root().join(path),
};
self
}
/// Adds a file to the project.
pub fn file>(mut self, path: B, body: &str) -> Self {
self._file(path.as_ref(), body, false);
self
}
/// Adds an executable file to the project.
pub fn executable>(mut self, path: B, body: &str) -> Self {
self._file(path.as_ref(), body, true);
self
}
fn _file(&mut self, path: &Path, body: &str, executable: bool) {
self.files.push(FileBuilder::new(
self.root.root().join(path),
body,
executable,
));
}
/// Adds a symlink to a file to the project.
pub fn symlink>(mut self, dst: T, src: T) -> Self {
self.symlinks.push(SymlinkBuilder::new(
self.root.root().join(dst),
self.root.root().join(src),
));
self
}
/// Create a symlink to a directory
pub fn symlink_dir>(mut self, dst: T, src: T) -> Self {
self.symlinks.push(SymlinkBuilder::new_dir(
self.root.root().join(dst),
self.root.root().join(src),
));
self
}
pub fn no_manifest(mut self) -> Self {
self.no_manifest = true;
self
}
/// Creates the project.
pub fn build(mut self) -> Project {
// First, clean the directory if it already exists
self.rm_root();
// Create the empty directory
self.root.root().mkdir_p();
let manifest_path = self.root.root().join("Cargo.toml");
if !self.no_manifest && self.files.iter().all(|fb| fb.path != manifest_path) {
self._file(
Path::new("Cargo.toml"),
&basic_manifest("foo", "0.0.1"),
false,
)
}
let past = time::SystemTime::now() - Duration::new(1, 0);
let ftime = filetime::FileTime::from_system_time(past);
for file in self.files.iter_mut() {
file.mk();
if is_coarse_mtime() {
// Place the entire project 1 second in the past to ensure
// that if cargo is called multiple times, the 2nd call will
// see targets as "fresh". Without this, if cargo finishes in
// under 1 second, the second call will see the mtime of
// source == mtime of output and consider it dirty.
filetime::set_file_times(&file.path, ftime, ftime).unwrap();
}
}
for symlink in self.symlinks.iter_mut() {
symlink.mk();
}
let ProjectBuilder { root, .. } = self;
root
}
fn rm_root(&self) {
self.root.root().rm_rf()
}
}
impl Project {
/// Copy the test project from a fixed state
pub fn from_template(template_path: impl AsRef) -> Self {
let root = paths::root();
let project_root = root.join("case");
snapbox::path::copy_template(template_path.as_ref(), &project_root).unwrap();
Self { root: project_root }
}
/// Root of the project, ex: `/path/to/cargo/target/cit/t0/foo`
pub fn root(&self) -> PathBuf {
self.root.clone()
}
/// Project's target dir, ex: `/path/to/cargo/target/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`
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`
pub fn url(&self) -> Url {
path2url(self.root())
}
/// 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`
pub fn example_lib(&self, name: &str, kind: &str) -> PathBuf {
self.target_debug_dir()
.join("examples")
.join(paths::get_lib_filename(name, kind))
}
/// Path to a debug binary.
/// ex: `/path/to/cargo/target/cit/t0/foo/target/debug/foo`
pub fn bin(&self, b: &str) -> PathBuf {
self.build_dir()
.join("debug")
.join(&format!("{}{}", b, env::consts::EXE_SUFFIX))
}
/// Path to a release binary.
/// ex: `/path/to/cargo/target/cit/t0/foo/target/release/foo`
pub fn release_bin(&self, b: &str) -> PathBuf {
self.build_dir()
.join("release")
.join(&format!("{}{}", b, env::consts::EXE_SUFFIX))
}
/// Path to a debug binary for a specific target triple.
/// ex: `/path/to/cargo/target/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!(
"{}{}",
b,
env::consts::EXE_SUFFIX
))
}
/// Returns an iterator of paths matching the glob pattern, which is
/// relative to the project root.
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.
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();
pub fn process>(&self, program: T) -> Execs {
let mut p = process(program);
p.cwd(self.root());
execs().with_process_builder(p)
}
/// Creates a `ProcessBuilder` to run cargo.
/// Arguments can be separated by spaces.
/// Example:
/// p.cargo("build --bin foo").run();
pub fn cargo(&self, cmd: &str) -> Execs {
let cargo = cargo_exe();
let mut execs = self.process(&cargo);
if let Some(ref mut p) = execs.process_builder {
p.env("CARGO", cargo);
p.arg_line(cmd);
}
execs
}
/// Safely run a process after `cargo build`.
///
/// Windows has a problem where a process cannot be reliably
/// be replaced, removed, or renamed immediately after executing it.
/// The action may fail (with errors like Access is denied), or
/// it may succeed, but future attempts to use the same filename
/// will fail with "Already Exists".
///
/// If you have a test that needs to do `cargo run` multiple
/// times, you should instead use `cargo build` and use this
/// method to run the executable. Each time you call this,
/// use a new name for `dst`.
/// See rust-lang/cargo#5481.
pub fn rename_run(&self, src: &str, dst: &str) -> Execs {
let src = self.bin(src);
let dst = self.bin(dst);
fs::rename(&src, &dst)
.unwrap_or_else(|e| panic!("Failed to rename `{:?}` to `{:?}`: {}", src, dst, e));
self.process(dst)
}
/// Returns the contents of `Cargo.lock`.
pub fn read_lockfile(&self) -> String {
self.read_file("Cargo.lock")
}
/// Returns the contents of a path in the project root
pub fn read_file(&self, path: &str) -> String {
let full = self.root().join(path);
fs::read_to_string(&full)
.unwrap_or_else(|e| panic!("could not read file {}: {}", full.display(), e))
}
/// Modifies `Cargo.toml` to remove all commented lines.
pub fn uncomment_root_manifest(&self) {
let contents = self.read_file("Cargo.toml").replace("#", "");
fs::write(self.root().join("Cargo.toml"), contents).unwrap();
}
pub fn symlink(&self, src: impl AsRef, dst: impl AsRef) {
let src = self.root().join(src.as_ref());
let dst = self.root().join(dst.as_ref());
#[cfg(unix)]
{
if let Err(e) = os::unix::fs::symlink(&src, &dst) {
panic!("failed to symlink {:?} to {:?}: {:?}", src, dst, e);
}
}
#[cfg(windows)]
{
if src.is_dir() {
if let Err(e) = os::windows::fs::symlink_dir(&src, &dst) {
panic!("failed to symlink {:?} to {:?}: {:?}", src, dst, e);
}
} else {
if let Err(e) = os::windows::fs::symlink_file(&src, &dst) {
panic!("failed to symlink {:?} to {:?}: {:?}", src, dst, e);
}
}
}
}
}
// Generates a project layout
pub fn project() -> ProjectBuilder {
ProjectBuilder::new(paths::root().join("foo"))
}
// Generates a project layout in given directory
pub fn project_in(dir: &str) -> ProjectBuilder {
ProjectBuilder::new(paths::root().join(dir).join("foo"))
}
// Generates a project layout inside our fake home dir
pub fn project_in_home(name: &str) -> ProjectBuilder {
ProjectBuilder::new(paths::home().join(name))
}
// === Helpers ===
pub fn main_file(println: &str, deps: &[&str]) -> String {
let mut buf = String::new();
for dep in deps.iter() {
buf.push_str(&format!("extern crate {};\n", dep));
}
buf.push_str("fn main() { println!(");
buf.push_str(println);
buf.push_str("); }\n");
buf
}
pub fn cargo_exe() -> PathBuf {
snapbox::cmd::cargo_bin("cargo")
}
/// This is the raw output from the process.
///
/// This is similar to `std::process::Output`, however the `status` is
/// translated to the raw `code`. This is necessary because `ProcessError`
/// 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,
}
#[must_use]
#[derive(Clone)]
pub struct Execs {
ran: bool,
process_builder: Option,
expect_stdout: Option,
expect_stdin: Option,
expect_stderr: Option,
expect_exit_code: Option,
expect_stdout_contains: Vec,
expect_stderr_contains: Vec,
expect_stdout_contains_n: Vec<(String, usize)>,
expect_stdout_not_contains: Vec,
expect_stderr_not_contains: Vec,
expect_stderr_unordered: Vec,
expect_stderr_with_without: Vec<(Vec, Vec)>,
expect_json: Option,
expect_json_contains_unordered: Option,
stream_output: bool,
}
impl Execs {
pub fn with_process_builder(mut self, p: ProcessBuilder) -> Execs {
self.process_builder = Some(p);
self
}
/// Verifies that stdout is equal to the given lines.
/// See [`compare`] for supported patterns.
pub fn with_stdout(&mut self, expected: S) -> &mut Self {
self.expect_stdout = Some(expected.to_string());
self
}
/// Verifies that stderr is equal to the given lines.
/// See [`compare`] for supported patterns.
pub fn with_stderr(&mut self, expected: S) -> &mut Self {
self.expect_stderr = Some(expected.to_string());
self
}
/// Writes the given lines to stdin.
pub fn with_stdin(&mut self, expected: S) -> &mut Self {
self.expect_stdin = Some(expected.to_string());
self
}
/// Verifies the exit code from the process.
///
/// This is not necessary if the expected exit code is `0`.
pub fn with_status(&mut self, expected: i32) -> &mut Self {
self.expect_exit_code = Some(expected);
self
}
/// Removes exit code check for the process.
///
/// By default, the expected exit code is `0`.
pub fn without_status(&mut self) -> &mut Self {
self.expect_exit_code = None;
self
}
/// Verifies that stdout contains the given contiguous lines somewhere in
/// its output.
///
/// See [`compare`] for supported patterns.
pub fn with_stdout_contains(&mut self, expected: S) -> &mut Self {
self.expect_stdout_contains.push(expected.to_string());
self
}
/// Verifies that stderr contains the given contiguous lines somewhere in
/// its output.
///
/// See [`compare`] for supported patterns.
pub fn with_stderr_contains(&mut self, expected: S) -> &mut Self {
self.expect_stderr_contains.push(expected.to_string());
self
}
/// Verifies that stdout contains the given contiguous lines somewhere in
/// its output, and should be repeated `number` times.
///
/// See [`compare`] for supported patterns.
pub fn with_stdout_contains_n(&mut self, expected: S, number: usize) -> &mut Self {
self.expect_stdout_contains_n
.push((expected.to_string(), number));
self
}
/// Verifies that stdout does not contain the given contiguous lines.
///
/// See [`compare`] for supported patterns.
///
/// See note on [`Self::with_stderr_does_not_contain`].
pub fn with_stdout_does_not_contain(&mut self, expected: S) -> &mut Self {
self.expect_stdout_not_contains.push(expected.to_string());
self
}
/// Verifies that stderr does not contain the given contiguous lines.
///
/// See [`compare`] for supported patterns.
///
/// Care should be taken when using this method because there is a
/// limitless number of possible things that *won't* appear. A typo means
/// your test will pass without verifying the correct behavior. If
/// possible, write the test first so that it fails, and then implement
/// your fix/feature to make it pass.
pub fn with_stderr_does_not_contain(&mut self, expected: S) -> &mut Self {
self.expect_stderr_not_contains.push(expected.to_string());
self
}
/// Verifies that all of the stderr output is equal to the given lines,
/// ignoring the order of the lines.
///
/// See [`compare`] for supported patterns.
///
/// This is useful when checking the output of `cargo build -v` since
/// the order of the output is not always deterministic.
/// Recommend use `with_stderr_contains` instead unless you really want to
/// check *every* line of output.
///
/// Be careful when using patterns such as `[..]`, because you may end up
/// with multiple lines that might match, and this is not smart enough to
/// do anything like longest-match. For example, avoid something like:
///
/// ```text
/// [RUNNING] `rustc [..]
/// [RUNNING] `rustc --crate-name foo [..]
/// ```
///
/// This will randomly fail if the other crate name is `bar`, and the
/// order changes.
pub fn with_stderr_unordered(&mut self, expected: S) -> &mut Self {
self.expect_stderr_unordered.push(expected.to_string());
self
}
/// Verify that a particular line appears in stderr with and without the
/// given substrings. Exactly one line must match.
///
/// The substrings are matched as `contains`. Example:
///
/// ```no_run
/// execs.with_stderr_line_without(
/// &[
/// "[RUNNING] `rustc --crate-name build_script_build",
/// "-C opt-level=3",
/// ],
/// &["-C debuginfo", "-C incremental"],
/// )
/// ```
///
/// This will check that a build line includes `-C opt-level=3` but does
/// not contain `-C debuginfo` or `-C incremental`.
///
/// Be careful writing the `without` fragments, see note in
/// `with_stderr_does_not_contain`.
pub fn with_stderr_line_without(
&mut self,
with: &[S],
without: &[S],
) -> &mut Self {
let with = with.iter().map(|s| s.to_string()).collect();
let without = without.iter().map(|s| s.to_string()).collect();
self.expect_stderr_with_without.push((with, without));
self
}
/// Verifies the JSON output matches the given JSON.
///
/// This is typically used when testing cargo commands that emit JSON.
/// Each separate JSON object should be separated by a blank line.
/// Example:
///
/// ```rust,ignore
/// assert_that(
/// p.cargo("metadata"),
/// execs().with_json(r#"
/// {"example": "abc"}
///
/// {"example": "def"}
/// "#)
/// );
/// ```
///
/// - Objects should match in the order given.
/// - The order of arrays is ignored.
/// - Strings support patterns described in [`compare`].
/// - Use `"{...}"` to match any object.
pub fn with_json(&mut self, expected: &str) -> &mut Self {
self.expect_json = Some(expected.to_string());
self
}
/// Verifies JSON output contains the given objects (in any order) somewhere
/// in its output.
///
/// CAUTION: Be very careful when using this. Make sure every object is
/// unique (not a subset of one another). Also avoid using objects that
/// could possibly match multiple output lines unless you're very sure of
/// what you are doing.
///
/// See `with_json` for more detail.
pub fn with_json_contains_unordered(&mut self, expected: &str) -> &mut Self {
match &mut self.expect_json_contains_unordered {
None => self.expect_json_contains_unordered = Some(expected.to_string()),
Some(e) => {
e.push_str("\n\n");
e.push_str(expected);
}
}
self
}
/// 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!
#[allow(unused)]
pub fn stream(&mut self) -> &mut Self {
self.stream_output = true;
self
}
pub fn arg>(&mut self, arg: T) -> &mut Self {
if let Some(ref mut p) = self.process_builder {
p.arg(arg);
}
self
}
pub fn cwd>(&mut self, path: T) -> &mut Self {
if let Some(ref mut p) = self.process_builder {
if let Some(cwd) = p.get_cwd() {
let new_path = cwd.join(path.as_ref());
p.cwd(new_path);
} else {
p.cwd(path);
}
}
self
}
fn get_cwd(&self) -> Option<&Path> {
self.process_builder.as_ref().and_then(|p| p.get_cwd())
}
pub fn env>(&mut self, key: &str, val: T) -> &mut Self {
if let Some(ref mut p) = self.process_builder {
p.env(key, val);
}
self
}
pub fn env_remove(&mut self, key: &str) -> &mut Self {
if let Some(ref mut p) = self.process_builder {
p.env_remove(key);
}
self
}
pub fn exec_with_output(&mut self) -> Result