use anyhow::bail; use anyhow::Context; use glob::glob; use pretty_assertions::StrComparison; use std::ffi::OsStr; use std::path::PathBuf; use std::{path::Path, process::Output}; use tokio::process::Command; lazy_static::lazy_static! { static ref COREUTILS_PATH: PathBuf = install_local_coreutils(); } #[tokio::test(flavor = "multi_thread")] async fn run_examples_checks() -> anyhow::Result<()> { let mut set = tokio::task::JoinSet::new(); for (example_name, file) in list_files(["examples/*/run.toml", "examples/*/run.toml.md"]) { set.spawn(async move { example_check(&file) .await .with_context(|| format!("example failed: {}", &example_name))?; Ok::(example_name) }); } while let Some(Ok(joined)) = set.join_next().await { let example_name = joined?; println!("[ok] {}", &example_name); } Ok(()) } #[tokio::test(flavor = "multi_thread")] async fn run_e2e_tests() -> anyhow::Result<()> { let mut set = tokio::task::JoinSet::new(); for (test_name, file) in list_files(["tests/**/*.toml", "tests/**/*.toml.md"]) { set.spawn(async move { e2e_test(&file) .await .with_context(|| format!("test failed: {}", &test_name))?; Ok::(test_name) }); } while let Some(Ok(joined)) = set.join_next().await { let test_name = joined?; println!("[ok] {}", &test_name); } Ok(()) } async fn example_check>(file: P) -> anyhow::Result<()> { let output = exec(&file, ["--check"]).await?; if !output.status.success() { let stderr = std::str::from_utf8(&output.stderr)?; bail!("unexpectedly failed with: {}", stderr); } Ok(()) } async fn e2e_test>(file: P) -> anyhow::Result<()> { let args = read_file(&file, ".args").await.unwrap_or_default(); let expected_stdout = read_file(&file, ".stdout").await.map(patch); let expected_stderr = read_file(&file, ".stderr").await.map(patch); if expected_stdout.is_none() && expected_stderr.is_none() { bail!("none of .stdout or .stderr found"); } // exec and get output let output = exec(&file, args.lines()).await?; let stdout = patch(std::str::from_utf8(&output.stdout)?); let stderr = patch(std::str::from_utf8(&output.stderr)?); if !output.status.success() && expected_stderr.is_none() { bail!("unexpectedly failed with: {}", stderr); } // assert stdout if let Some(expected) = expected_stdout { if expected != stdout { bail!( "stdout does not match: {}", StrComparison::new(&expected, &stdout) ); } } // assert stderr if let Some(expected) = expected_stderr { if expected != stderr { bail!( "stderr does not match: {}", StrComparison::new(&expected, &stderr) ); } } Ok(()) } async fn exec(file: P, args: I) -> anyhow::Result where P: AsRef, I: IntoIterator, S: AsRef, { let mut cmd = Command::new(env!("CARGO_BIN_EXE_run")); cmd.env("PATH", COREUTILS_PATH.to_str().unwrap()); cmd.arg("-f"); cmd.arg(file.as_ref()); cmd.args(args); Ok(cmd.output().await?) } async fn read_file>(filepath: P, suffix: &str) -> Option { let filepath = filepath.as_ref().to_str()?.to_string() + suffix; tokio::fs::read_to_string(&filepath).await.ok() } const CARGO_MANIFEST_DIR: &str = env!("CARGO_MANIFEST_DIR"); fn list_files(patterns: I) -> impl Iterator where I: IntoIterator, D: std::fmt::Display, { patterns .into_iter() .map(|pattern| format!("{CARGO_MANIFEST_DIR}/{pattern}")) .flat_map(|pattern| glob(&pattern).unwrap().map(Result::unwrap)) .map(|file| { let test_name = &file.strip_prefix(CARGO_MANIFEST_DIR).unwrap(); let test_name = test_name.to_str().unwrap().to_string(); (test_name, file) }) } /// patch performs a few operations to make sure the tests run nicely on all platforms: /// 0. it trims the string /// 1. it replaces `$CARGO_MANIFEST_DIR` with the actual path. This is useful as some path are /// actually absolute path, and we don't want to hardcode the absolute path in the test files. /// 2. it deletes windows extended length marker (\\?\) /// 3. it replaces the windows EOL with unix EOL /// 4. it replaces the backslash with forward slash fn patch>(s: S) -> String { s.as_ref() .trim() .replace("$CARGO_MANIFEST_DIR", CARGO_MANIFEST_DIR) .replace("\\\\?\\", "") .replace("\r\n", "\n") .replace('\\', "/") } fn install_local_coreutils() -> PathBuf { const CRATE_NAME: &str = "coreutils"; const CRATE_VERSION: &str = "0.0.18"; const CRATE_FEATURES: &[&str] = &["echo", "ls", "printenv", "sleep"]; let root = PathBuf::new() .join(env!("CARGO_MANIFEST_DIR")) .join(format!( ".bin/{}@{}#{}", CRATE_NAME, CRATE_VERSION, CRATE_FEATURES.join(",") )); if !root.is_dir() { let _: Result<_, _> = std::fs::remove_dir_all(&root); let mut cmd = std::process::Command::new(env!("CARGO")); cmd.arg("install"); cmd.arg("--debug"); cmd.arg("--root"); cmd.arg(&root); cmd.arg("--version"); cmd.arg(CRATE_VERSION); cmd.arg("--no-default-features"); cmd.arg("--features"); cmd.arg(CRATE_FEATURES.join(" ")); cmd.arg(CRATE_NAME); cmd.output().unwrap(); } root.join("bin") }