#![allow(clippy::disallowed_methods)]
#![allow(clippy::print_stderr)]
#![allow(clippy::print_stdout)]
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::sync::OnceLock;
use std::thread::JoinHandle;
use std::time::{self, Duration};
use anyhow::{bail, Result};
use cargo_util::{is_ci, ProcessBuilder, ProcessError};
use snapbox::IntoData as _;
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),
}
};
}
pub use snapbox::file;
pub use snapbox::str;
pub use snapbox::utils::current_dir;
#[track_caller]
pub fn panic_error(what: &str, err: impl Into<anyhow::Error>) -> ! {
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) {
let _ = writeln!(result, "\nCaused by:");
let _ = 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::cargo_test;
pub use crate::ArgLine;
pub use crate::CargoCommand;
pub use crate::ChannelChanger;
pub use crate::TestEnv;
pub use snapbox::IntoData;
}
#[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 {
let mut path = self.path.clone().into_os_string();
write!(path, "{}", env::consts::EXE_SUFFIX).unwrap();
self.path = path.into();
}
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()
}
}
pub struct Project {
root: PathBuf,
}
#[must_use]
pub struct ProjectBuilder {
root: Project,
files: Vec<FileBuilder>,
symlinks: Vec<SymlinkBuilder>,
no_manifest: bool,
}
impl ProjectBuilder {
pub fn root(&self) -> PathBuf {
self.root.root()
}
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<P: AsRef<Path>>(mut self, path: P) -> Self {
self.root = Project {
root: paths::root().join(path),
};
self
}
pub fn file<B: AsRef<Path>>(mut self, path: B, body: &str) -> Self {
self._file(path.as_ref(), body, false);
self
}
pub fn executable<B: AsRef<Path>>(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,
));
}
pub fn symlink<T: AsRef<Path>>(mut self, dst: T, src: T) -> Self {
self.symlinks.push(SymlinkBuilder::new(
self.root.root().join(dst),
self.root.root().join(src),
));
self
}
pub fn symlink_dir<T: AsRef<Path>>(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
}
pub fn build(mut self) -> Project {
self.rm_root();
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() {
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 {
pub fn from_template(template_path: impl AsRef<std::path::Path>) -> Self {
let root = paths::root();
let project_root = root.join("case");
snapbox::dir::copy_template(template_path.as_ref(), &project_root).unwrap();
Self { root: project_root }
}
pub fn root(&self) -> PathBuf {
self.root.clone()
}
pub fn build_dir(&self) -> PathBuf {
self.root().join("target")
}
pub fn target_debug_dir(&self) -> PathBuf {
self.build_dir().join("debug")
}
pub fn url(&self) -> Url {
path2url(self.root())
}
pub fn example_lib(&self, name: &str, kind: &str) -> PathBuf {
self.target_debug_dir()
.join("examples")
.join(paths::get_lib_filename(name, kind))
}
pub fn bin(&self, b: &str) -> PathBuf {
self.build_dir()
.join("debug")
.join(&format!("{}{}", b, env::consts::EXE_SUFFIX))
}
pub fn release_bin(&self, b: &str) -> PathBuf {
self.build_dir()
.join("release")
.join(&format!("{}{}", b, env::consts::EXE_SUFFIX))
}
pub fn target_bin(&self, target: &str, b: &str) -> PathBuf {
self.build_dir().join(target).join("debug").join(&format!(
"{}{}",
b,
env::consts::EXE_SUFFIX
))
}
pub fn glob<P: AsRef<Path>>(&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")
}
pub fn change_file(&self, path: &str, body: &str) {
FileBuilder::new(self.root().join(path), body, false).mk()
}
pub fn process<T: AsRef<OsStr>>(&self, program: T) -> Execs {
let mut p = process(program);
p.cwd(self.root());
execs().with_process_builder(p)
}
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
}
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)
}
pub fn read_lockfile(&self) -> String {
self.read_file("Cargo.lock")
}
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))
}
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<Path>, dst: impl AsRef<Path>) {
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);
}
}
}
}
}
pub fn project() -> ProjectBuilder {
ProjectBuilder::new(paths::root().join("foo"))
}
pub fn project_in(dir: &str) -> ProjectBuilder {
ProjectBuilder::new(paths::root().join(dir).join("foo"))
}
pub fn project_in_home(name: &str) -> ProjectBuilder {
ProjectBuilder::new(paths::home().join(name))
}
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")
}
pub struct RawOutput {
pub code: Option<i32>,
pub stdout: Vec<u8>,
pub stderr: Vec<u8>,
}
#[must_use]
#[derive(Clone)]
pub struct Execs {
ran: bool,
process_builder: Option<ProcessBuilder>,
expect_stdout: Option<String>,
expect_stdin: Option<String>,
expect_stderr: Option<String>,
expect_exit_code: Option<i32>,
expect_stdout_data: Option<snapbox::Data>,
expect_stderr_data: Option<snapbox::Data>,
expect_stdout_contains: Vec<String>,
expect_stderr_contains: Vec<String>,
expect_stdout_contains_n: Vec<(String, usize)>,
expect_stdout_not_contains: Vec<String>,
expect_stderr_not_contains: Vec<String>,
expect_stdout_unordered: Vec<String>,
expect_stderr_unordered: Vec<String>,
expect_stderr_with_without: Vec<(Vec<String>, Vec<String>)>,
expect_json: Option<String>,
expect_json_contains_unordered: Option<String>,
stream_output: bool,
assert: snapbox::Assert,
}
impl Execs {
pub fn with_process_builder(mut self, p: ProcessBuilder) -> Execs {
self.process_builder = Some(p);
self
}
#[deprecated(note = "replaced with `Execs::with_stdout_data(expected)`")]
pub fn with_stdout<S: ToString>(&mut self, expected: S) -> &mut Self {
self.expect_stdout = Some(expected.to_string());
self
}
#[deprecated(note = "replaced with `Execs::with_stderr_data(expected)`")]
pub fn with_stderr<S: ToString>(&mut self, expected: S) -> &mut Self {
self.expect_stderr = Some(expected.to_string());
self
}
pub fn with_stdout_data(&mut self, expected: impl snapbox::IntoData) -> &mut Self {
self.expect_stdout_data = Some(expected.into_data());
self
}
pub fn with_stderr_data(&mut self, expected: impl snapbox::IntoData) -> &mut Self {
self.expect_stderr_data = Some(expected.into_data());
self
}
pub fn with_stdin<S: ToString>(&mut self, expected: S) -> &mut Self {
self.expect_stdin = Some(expected.to_string());
self
}
pub fn with_status(&mut self, expected: i32) -> &mut Self {
self.expect_exit_code = Some(expected);
self
}
pub fn without_status(&mut self) -> &mut Self {
self.expect_exit_code = None;
self
}
#[deprecated(note = "replaced with `Execs::with_stdout_data(expected)`")]
pub fn with_stdout_contains<S: ToString>(&mut self, expected: S) -> &mut Self {
self.expect_stdout_contains.push(expected.to_string());
self
}
#[deprecated(note = "replaced with `Execs::with_stderr_data(expected)`")]
pub fn with_stderr_contains<S: ToString>(&mut self, expected: S) -> &mut Self {
self.expect_stderr_contains.push(expected.to_string());
self
}
#[deprecated(note = "replaced with `Execs::with_stdout_data(expected)`")]
pub fn with_stdout_contains_n<S: ToString>(&mut self, expected: S, number: usize) -> &mut Self {
self.expect_stdout_contains_n
.push((expected.to_string(), number));
self
}
#[deprecated]
pub fn with_stdout_does_not_contain<S: ToString>(&mut self, expected: S) -> &mut Self {
self.expect_stdout_not_contains.push(expected.to_string());
self
}
#[deprecated]
pub fn with_stderr_does_not_contain<S: ToString>(&mut self, expected: S) -> &mut Self {
self.expect_stderr_not_contains.push(expected.to_string());
self
}
#[deprecated(note = "replaced with `Execs::with_stdout_data(expected.unordered())`")]
pub fn with_stdout_unordered<S: ToString>(&mut self, expected: S) -> &mut Self {
self.expect_stdout_unordered.push(expected.to_string());
self
}
#[deprecated(note = "replaced with `Execs::with_stderr_data(expected.unordered())`")]
pub fn with_stderr_unordered<S: ToString>(&mut self, expected: S) -> &mut Self {
self.expect_stderr_unordered.push(expected.to_string());
self
}
#[deprecated]
pub fn with_stderr_line_without<S: ToString>(
&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
}
#[deprecated(note = "replaced with `Execs::with_stdout_data(expected.json_lines())`")]
pub fn with_json(&mut self, expected: &str) -> &mut Self {
self.expect_json = Some(expected.to_string());
self
}
#[deprecated]
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
}
#[allow(unused)]
pub fn stream(&mut self) -> &mut Self {
self.stream_output = true;
self
}
pub fn arg<T: AsRef<OsStr>>(&mut self, arg: T) -> &mut Self {
if let Some(ref mut p) = self.process_builder {
p.arg(arg);
}
self
}
pub fn args<T: AsRef<OsStr>>(&mut self, args: &[T]) -> &mut Self {
if let Some(ref mut p) = self.process_builder {
p.args(args);
}
self
}
pub fn cwd<T: AsRef<OsStr>>(&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<T: AsRef<OsStr>>(&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<Output> {
self.ran = true;
let p = (&self.process_builder).clone().unwrap();
p.exec_with_output()
}
pub fn build_command(&mut self) -> Command {
self.ran = true;
let p = (&self.process_builder).clone().unwrap();
p.build_command()
}
pub fn masquerade_as_nightly_cargo(&mut self, reasons: &[&str]) -> &mut Self {
if let Some(ref mut p) = self.process_builder {
p.masquerade_as_nightly_cargo(reasons);
}
self
}
pub fn replace_crates_io(&mut self, url: &Url) -> &mut Self {
if let Some(ref mut p) = self.process_builder {
p.env("__CARGO_TEST_CRATES_IO_URL_DO_NOT_USE_THIS", url.as_str());
}
self
}
pub fn overlay_registry(&mut self, url: &Url, path: &str) -> &mut Self {
if let Some(ref mut p) = self.process_builder {
let env_value = format!("{}={}", url, path);
p.env(
"__CARGO_TEST_DEPENDENCY_CONFUSION_VULNERABILITY_DO_NOT_USE_THIS",
env_value,
);
}
self
}
pub fn enable_split_debuginfo_packed(&mut self) -> &mut Self {
self.env("CARGO_PROFILE_DEV_SPLIT_DEBUGINFO", "packed")
.env("CARGO_PROFILE_TEST_SPLIT_DEBUGINFO", "packed")
.env("CARGO_PROFILE_RELEASE_SPLIT_DEBUGINFO", "packed")
.env("CARGO_PROFILE_BENCH_SPLIT_DEBUGINFO", "packed");
self
}
pub fn enable_mac_dsym(&mut self) -> &mut Self {
if cfg!(target_os = "macos") {
return self.enable_split_debuginfo_packed();
}
self
}
#[track_caller]
pub fn run(&mut self) {
self.ran = true;
let mut p = (&self.process_builder).clone().unwrap();
if let Some(stdin) = self.expect_stdin.take() {
p.stdin(stdin);
}
if let Err(e) = self.match_process(&p) {
panic_error(&format!("test failed running {}", p), e);
}
}
#[track_caller]
pub fn run_json(&mut self) -> serde_json::Value {
self.ran = true;
let p = (&self.process_builder).clone().unwrap();
match self.match_process(&p) {
Err(e) => panic_error(&format!("test failed running {}", p), e),
Ok(output) => serde_json::from_slice(&output.stdout).unwrap_or_else(|e| {
panic!(
"\nfailed to parse JSON: {}\n\
output was:\n{}\n",
e,
String::from_utf8_lossy(&output.stdout)
);
}),
}
}
#[track_caller]
pub fn run_output(&mut self, output: &Output) {
self.ran = true;
if let Err(e) = self.match_output(output.status.code(), &output.stdout, &output.stderr) {
panic_error("process did not return the expected result", e)
}
}
#[track_caller]
fn verify_checks_output(&self, stdout: &[u8], stderr: &[u8]) {
if self.expect_exit_code.unwrap_or(0) != 0
&& self.expect_stdout.is_none()
&& self.expect_stdin.is_none()
&& self.expect_stderr.is_none()
&& self.expect_stdout_data.is_none()
&& self.expect_stderr_data.is_none()
&& self.expect_stdout_contains.is_empty()
&& self.expect_stderr_contains.is_empty()
&& self.expect_stdout_contains_n.is_empty()
&& self.expect_stdout_not_contains.is_empty()
&& self.expect_stderr_not_contains.is_empty()
&& self.expect_stdout_unordered.is_empty()
&& self.expect_stderr_unordered.is_empty()
&& self.expect_stderr_with_without.is_empty()
&& self.expect_json.is_none()
&& self.expect_json_contains_unordered.is_none()
{
panic!(
"`with_status()` is used, but no output is checked.\n\
The test must check the output to ensure the correct error is triggered.\n\
--- stdout\n{}\n--- stderr\n{}",
String::from_utf8_lossy(stdout),
String::from_utf8_lossy(stderr),
);
}
}
#[track_caller]
fn match_process(&self, process: &ProcessBuilder) -> Result<RawOutput> {
println!("running {}", process);
let res = if self.stream_output {
if is_ci() {
panic!("`.stream()` is for local debugging")
}
process.exec_with_streaming(
&mut |out| {
println!("{}", out);
Ok(())
},
&mut |err| {
eprintln!("{}", err);
Ok(())
},
true,
)
} else {
process.exec_with_output()
};
match res {
Ok(out) => {
self.match_output(out.status.code(), &out.stdout, &out.stderr)?;
return Ok(RawOutput {
stdout: out.stdout,
stderr: out.stderr,
code: out.status.code(),
});
}
Err(e) => {
if let Some(ProcessError {
stdout: Some(stdout),
stderr: Some(stderr),
code,
..
}) = e.downcast_ref::<ProcessError>()
{
self.match_output(*code, stdout, stderr)?;
return Ok(RawOutput {
stdout: stdout.to_vec(),
stderr: stderr.to_vec(),
code: *code,
});
}
bail!("could not exec process {}: {:?}", process, e)
}
}
}
#[track_caller]
fn match_output(&self, code: Option<i32>, stdout: &[u8], stderr: &[u8]) -> Result<()> {
self.verify_checks_output(stdout, stderr);
let stdout = std::str::from_utf8(stdout).expect("stdout is not utf8");
let stderr = std::str::from_utf8(stderr).expect("stderr is not utf8");
let cwd = self.get_cwd();
match self.expect_exit_code {
None => {}
Some(expected) if code == Some(expected) => {}
Some(expected) => bail!(
"process exited with code {} (expected {})\n--- stdout\n{}\n--- stderr\n{}",
code.unwrap_or(-1),
expected,
stdout,
stderr
),
}
if let Some(expect_stdout) = &self.expect_stdout {
compare::match_exact(expect_stdout, stdout, "stdout", stderr, cwd)?;
}
if let Some(expect_stderr) = &self.expect_stderr {
compare::match_exact(expect_stderr, stderr, "stderr", stdout, cwd)?;
}
if let Some(expect_stdout_data) = &self.expect_stdout_data {
if let Err(err) = self.assert.try_eq(
Some(&"stdout"),
stdout.into_data(),
expect_stdout_data.clone(),
) {
panic!("{err}")
}
}
if let Some(expect_stderr_data) = &self.expect_stderr_data {
if let Err(err) = self.assert.try_eq(
Some(&"stderr"),
stderr.into_data(),
expect_stderr_data.clone(),
) {
panic!("{err}")
}
}
for expect in self.expect_stdout_contains.iter() {
compare::match_contains(expect, stdout, cwd)?;
}
for expect in self.expect_stderr_contains.iter() {
compare::match_contains(expect, stderr, cwd)?;
}
for &(ref expect, number) in self.expect_stdout_contains_n.iter() {
compare::match_contains_n(expect, number, stdout, cwd)?;
}
for expect in self.expect_stdout_not_contains.iter() {
compare::match_does_not_contain(expect, stdout, cwd)?;
}
for expect in self.expect_stderr_not_contains.iter() {
compare::match_does_not_contain(expect, stderr, cwd)?;
}
for expect in self.expect_stdout_unordered.iter() {
compare::match_unordered(expect, stdout, cwd)?;
}
for expect in self.expect_stderr_unordered.iter() {
compare::match_unordered(expect, stderr, cwd)?;
}
for (with, without) in self.expect_stderr_with_without.iter() {
compare::match_with_without(stderr, with, without, cwd)?;
}
if let Some(ref expect_json) = self.expect_json {
compare::match_json(expect_json, stdout, cwd)?;
}
if let Some(ref expected) = self.expect_json_contains_unordered {
compare::match_json_contains_unordered(expected, stdout, cwd)?;
}
Ok(())
}
}
impl Drop for Execs {
fn drop(&mut self) {
if !self.ran && !std::thread::panicking() {
panic!("forgot to run this command");
}
}
}
pub fn execs() -> Execs {
Execs {
ran: false,
process_builder: None,
expect_stdout: None,
expect_stderr: None,
expect_stdin: None,
expect_exit_code: Some(0),
expect_stdout_data: None,
expect_stderr_data: None,
expect_stdout_contains: Vec::new(),
expect_stderr_contains: Vec::new(),
expect_stdout_contains_n: Vec::new(),
expect_stdout_not_contains: Vec::new(),
expect_stderr_not_contains: Vec::new(),
expect_stdout_unordered: Vec::new(),
expect_stderr_unordered: Vec::new(),
expect_stderr_with_without: Vec::new(),
expect_json: None,
expect_json_contains_unordered: None,
stream_output: false,
assert: compare::assert_e2e(),
}
}
pub fn basic_manifest(name: &str, version: &str) -> String {
format!(
r#"
[package]
name = "{}"
version = "{}"
authors = []
edition = "2015"
"#,
name, version
)
}
pub fn basic_bin_manifest(name: &str) -> String {
format!(
r#"
[package]
name = "{}"
version = "0.5.0"
authors = ["wycats@example.com"]
edition = "2015"
[[bin]]
name = "{}"
"#,
name, name
)
}
pub fn basic_lib_manifest(name: &str) -> String {
format!(
r#"
[package]
name = "{}"
version = "0.5.0"
authors = ["wycats@example.com"]
edition = "2015"
[lib]
name = "{}"
"#,
name, name
)
}
pub fn path2url<P: AsRef<Path>>(p: P) -> Url {
Url::from_file_path(p).ok().unwrap()
}
struct RustcInfo {
verbose_version: String,
host: String,
}
impl RustcInfo {
fn new() -> RustcInfo {
let output = ProcessBuilder::new("rustc")
.arg("-vV")
.exec_with_output()
.expect("rustc should exec");
let verbose_version = String::from_utf8(output.stdout).expect("utf8 output");
let host = verbose_version
.lines()
.filter_map(|line| line.strip_prefix("host: "))
.next()
.expect("verbose version has host: field")
.to_string();
RustcInfo {
verbose_version,
host,
}
}
}
fn rustc_info() -> &'static RustcInfo {
static RUSTC_INFO: OnceLock<RustcInfo> = OnceLock::new();
RUSTC_INFO.get_or_init(RustcInfo::new)
}
pub fn rustc_host() -> &'static str {
&rustc_info().host
}
pub fn rustc_host_env() -> String {
rustc_host().to_uppercase().replace('-', "_")
}
pub fn is_nightly() -> bool {
let vv = &rustc_info().verbose_version;
env::var("CARGO_TEST_DISABLE_NIGHTLY").is_err()
&& (vv.contains("-nightly") || vv.contains("-dev"))
}
pub fn process<T: AsRef<OsStr>>(t: T) -> ProcessBuilder {
_process(t.as_ref())
}
fn _process(t: &OsStr) -> ProcessBuilder {
let mut p = ProcessBuilder::new(t);
p.cwd(&paths::root()).test_env();
p
}
pub trait ChannelChanger {
fn masquerade_as_nightly_cargo(self, _reasons: &[&str]) -> Self;
}
impl ChannelChanger for &mut ProcessBuilder {
fn masquerade_as_nightly_cargo(self, _reasons: &[&str]) -> Self {
self.env("__CARGO_TEST_CHANNEL_OVERRIDE_DO_NOT_USE_THIS", "nightly")
}
}
impl ChannelChanger for snapbox::cmd::Command {
fn masquerade_as_nightly_cargo(self, _reasons: &[&str]) -> Self {
self.env("__CARGO_TEST_CHANNEL_OVERRIDE_DO_NOT_USE_THIS", "nightly")
}
}
pub trait TestEnv: Sized {
fn test_env(mut self) -> Self {
for (k, _v) in env::vars() {
if k.starts_with("CARGO_") {
self = self.env_remove(&k);
}
}
if env::var_os("RUSTUP_TOOLCHAIN").is_some() {
static RUSTC_DIR: OnceLock<PathBuf> = OnceLock::new();
let rustc_dir = RUSTC_DIR.get_or_init(|| {
match ProcessBuilder::new("rustup")
.args(&["which", "rustc"])
.exec_with_output()
{
Ok(output) => {
let s = std::str::from_utf8(&output.stdout).expect("utf8").trim();
let mut p = PathBuf::from(s);
p.pop();
p
}
Err(e) => {
panic!("RUSTUP_TOOLCHAIN was set, but could not run rustup: {}", e);
}
}
});
let path = env::var_os("PATH").unwrap_or_default();
let paths = env::split_paths(&path);
let new_path =
env::join_paths(std::iter::once(rustc_dir.clone()).chain(paths)).unwrap();
self = self.env("PATH", new_path);
}
self = self
.current_dir(&paths::root())
.env("HOME", paths::home())
.env("CARGO_HOME", paths::home().join(".cargo"))
.env("__CARGO_TEST_ROOT", paths::global_root())
.env("__CARGO_TEST_CHANNEL_OVERRIDE_DO_NOT_USE_THIS", "stable")
.env("__CARGO_TEST_DISABLE_GLOBAL_KNOWN_HOST", "1")
.env("__CARGO_TEST_FIXED_RETRY_SLEEP_MS", "1")
.env("CARGO_INCREMENTAL", "0")
.env("GIT_CONFIG_NOSYSTEM", "1")
.env_remove("__CARGO_DEFAULT_LIB_METADATA")
.env_remove("ALL_PROXY")
.env_remove("EMAIL")
.env_remove("GIT_AUTHOR_EMAIL")
.env_remove("GIT_AUTHOR_NAME")
.env_remove("GIT_COMMITTER_EMAIL")
.env_remove("GIT_COMMITTER_NAME")
.env_remove("http_proxy")
.env_remove("HTTPS_PROXY")
.env_remove("https_proxy")
.env_remove("MAKEFLAGS")
.env_remove("MFLAGS")
.env_remove("MSYSTEM") .env_remove("RUSTC")
.env_remove("RUSTC_WORKSPACE_WRAPPER")
.env_remove("RUSTC_WRAPPER")
.env_remove("RUSTDOC")
.env_remove("RUSTDOCFLAGS")
.env_remove("RUSTFLAGS")
.env_remove("SSH_AUTH_SOCK") .env_remove("USER") .env_remove("XDG_CONFIG_HOME") .env_remove("OUT_DIR"); if cfg!(windows) {
self = self.env("USERPROFILE", paths::home());
}
self
}
fn current_dir<S: AsRef<std::path::Path>>(self, path: S) -> Self;
fn env<S: AsRef<std::ffi::OsStr>>(self, key: &str, value: S) -> Self;
fn env_remove(self, key: &str) -> Self;
}
impl TestEnv for &mut ProcessBuilder {
fn current_dir<S: AsRef<std::path::Path>>(self, path: S) -> Self {
let path = path.as_ref();
self.cwd(path)
}
fn env<S: AsRef<std::ffi::OsStr>>(self, key: &str, value: S) -> Self {
self.env(key, value)
}
fn env_remove(self, key: &str) -> Self {
self.env_remove(key)
}
}
impl TestEnv for snapbox::cmd::Command {
fn current_dir<S: AsRef<std::path::Path>>(self, path: S) -> Self {
self.current_dir(path)
}
fn env<S: AsRef<std::ffi::OsStr>>(self, key: &str, value: S) -> Self {
self.env(key, value)
}
fn env_remove(self, key: &str) -> Self {
self.env_remove(key)
}
}
pub trait CargoCommand {
fn cargo_ui() -> Self;
}
impl CargoCommand for snapbox::cmd::Command {
fn cargo_ui() -> Self {
Self::new(cargo_exe())
.with_assert(compare::assert_ui())
.env("CARGO_TERM_COLOR", "always")
.test_env()
}
}
pub trait ArgLine: Sized {
fn arg_line(mut self, s: &str) -> Self {
for mut arg in s.split_whitespace() {
if (arg.starts_with('"') && arg.ends_with('"'))
|| (arg.starts_with('\'') && arg.ends_with('\''))
{
arg = &arg[1..(arg.len() - 1).max(1)];
} else if arg.contains(&['"', '\''][..]) {
panic!("shell-style argument parsing is not supported")
}
self = self.arg(arg);
}
self
}
fn arg<S: AsRef<std::ffi::OsStr>>(self, s: S) -> Self;
}
impl ArgLine for &mut ProcessBuilder {
fn arg<S: AsRef<std::ffi::OsStr>>(self, s: S) -> Self {
self.arg(s)
}
}
impl ArgLine for snapbox::cmd::Command {
fn arg<S: AsRef<std::ffi::OsStr>>(self, s: S) -> Self {
self.arg(s)
}
}
pub fn cargo_process(s: &str) -> Execs {
let cargo = cargo_exe();
let mut p = process(&cargo);
p.env("CARGO", cargo);
p.arg_line(s);
execs().with_process_builder(p)
}
pub fn git_process(s: &str) -> ProcessBuilder {
let mut p = process("git");
p.arg_line(s);
p
}
pub fn sleep_ms(ms: u64) {
::std::thread::sleep(Duration::from_millis(ms));
}
pub fn is_coarse_mtime() -> bool {
cfg!(emulate_second_only_system) ||
cfg!(target_os = "macos") && is_ci()
}
pub fn slow_cpu_multiplier(main: u64) -> Duration {
static SLOW_CPU_MULTIPLIER: OnceLock<u64> = OnceLock::new();
let slow_cpu_multiplier = SLOW_CPU_MULTIPLIER.get_or_init(|| {
env::var("CARGO_TEST_SLOW_CPU_MULTIPLIER")
.ok()
.and_then(|m| m.parse().ok())
.unwrap_or(1)
});
Duration::from_secs(slow_cpu_multiplier * main)
}
#[cfg(windows)]
pub fn symlink_supported() -> bool {
if is_ci() {
return true;
}
let src = paths::root().join("symlink_src");
fs::write(&src, "").unwrap();
let dst = paths::root().join("symlink_dst");
let result = match os::windows::fs::symlink_file(&src, &dst) {
Ok(_) => {
fs::remove_file(&dst).unwrap();
true
}
Err(e) => {
eprintln!(
"symlinks not supported: {:?}\n\
Windows 10 users should enable developer mode.",
e
);
false
}
};
fs::remove_file(&src).unwrap();
return result;
}
#[cfg(not(windows))]
pub fn symlink_supported() -> bool {
true
}
pub fn no_such_file_err_msg() -> String {
std::io::Error::from_raw_os_error(2).to_string()
}
pub fn retry<F, R>(n: u32, mut f: F) -> R
where
F: FnMut() -> Option<R>,
{
let mut count = 0;
let start = std::time::Instant::now();
loop {
if let Some(r) = f() {
return r;
}
count += 1;
if count > n {
panic!(
"test did not finish within {n} attempts ({:?} total)",
start.elapsed()
);
}
sleep_ms(100);
}
}
#[test]
#[should_panic(expected = "test did not finish")]
fn retry_fails() {
retry(2, || None::<()>);
}
pub fn thread_wait_timeout<T>(n: u32, thread: JoinHandle<T>) -> T {
retry(n, || thread.is_finished().then_some(()));
thread.join().unwrap()
}
pub fn threaded_timeout<F, R>(n: u32, f: F) -> R
where
F: FnOnce() -> R + Send + 'static,
R: Send + 'static,
{
let thread = std::thread::spawn(|| f());
thread_wait_timeout(n, thread)
}