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::<String, anyhow::Error>(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::<String, anyhow::Error>(test_name)
});
}
while let Some(Ok(joined)) = set.join_next().await {
let test_name = joined?;
println!("[ok] {}", &test_name);
}
Ok(())
}
async fn example_check<P: AsRef<Path>>(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<P: AsRef<Path>>(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");
}
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);
}
if let Some(expected) = expected_stdout {
if expected != stdout {
bail!(
"stdout does not match: {}",
StrComparison::new(&expected, &stdout)
);
}
}
if let Some(expected) = expected_stderr {
if expected != stderr {
bail!(
"stderr does not match: {}",
StrComparison::new(&expected, &stderr)
);
}
}
Ok(())
}
async fn exec<P, I, S>(file: P, args: I) -> anyhow::Result<Output>
where
P: AsRef<Path>,
I: IntoIterator<Item = S>,
S: AsRef<OsStr>,
{
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<P: AsRef<Path>>(filepath: P, suffix: &str) -> Option<String> {
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<I, D>(patterns: I) -> impl Iterator<Item = (String, PathBuf)>
where
I: IntoIterator<Item = D>,
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)
})
}
fn patch<S: AsRef<str>>(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")
}