use std::process::Stdio;
use std::time::Instant;
use anyhow::Result;
use clap::Parser;
use tokio::process::Command;
use ryra_core::registry::test_def::TestDef;
pub struct TestRunParams<'a> {
pub service: Option<&'a str>,
pub test_filter: Option<&'a str>,
pub project: Option<&'a std::path::PathBuf>,
pub vm: bool,
pub no_vm: bool,
pub retest: bool,
pub keep_alive: bool,
pub yes: bool,
pub verbose: bool,
pub list: bool,
pub parallel: Option<usize>,
pub names: &'a [String],
}
pub async fn run(params: TestRunParams<'_>) -> Result<()> {
if params.no_vm {
return run_no_vm(
params.names,
params.verbose,
params.list,
params.retest,
params.project,
)
.await;
}
if params.vm || params.keep_alive || params.list || params.project.is_some() {
return run_vm(
params.names,
params.keep_alive,
params.verbose,
params.list,
params.parallel,
params.project,
)
.await;
}
match params.service {
Some(service) => {
run_live_service(service, params.test_filter, params.yes, params.verbose).await
}
None => anyhow::bail!("specify a service name with --service <name>"),
}
}
fn warn_untrusted_repo(registry_name: &str, yes: bool) -> Result<()> {
if registry_name == ryra_core::REGISTRY_BUNDLED || registry_name.is_empty() {
return Ok(());
}
if yes {
eprintln!("warning: running test commands from custom registry: {registry_name}");
return Ok(());
}
if !super::is_interactive() {
anyhow::bail!(
"refusing to run test commands from custom registry in non-interactive mode.\n\
Registry: {registry_name}\n\
Pass -y to skip this check."
);
}
eprintln!("warning: about to run test commands from custom registry:\n {registry_name}\n");
let confirm = dialoguer::Confirm::new()
.with_prompt("Continue?")
.default(false)
.interact()?;
if !confirm {
anyhow::bail!("aborted");
}
Ok(())
}
async fn run_live_service(
service: &str,
test_filter: Option<&str>,
yes: bool,
verbose: bool,
) -> Result<()> {
let info = ryra_core::service_tests(service).await?;
warn_untrusted_repo(&info.registry_name, yes)?;
if info.tests.is_empty() {
println!("No tests defined for {service}.");
println!("Add [[tests]] sections to the service.toml in the registry.");
return Ok(());
}
if !info.env_file.exists() {
anyhow::bail!(
"env file not found at {} — is {service} installed and running?",
info.env_file.display()
);
}
let tests: Vec<&TestDef> = match test_filter {
Some(filter) => {
let filtered: Vec<_> = info.tests.iter().filter(|t| t.name == filter).collect();
if filtered.is_empty() {
let available: Vec<&str> = info.tests.iter().map(|t| t.name.as_str()).collect();
anyhow::bail!(
"no test named '{filter}' for {service}. Available: {}",
available.join(", ")
);
}
filtered
}
None => info.tests.iter().collect(),
};
println!("Testing {service} ({} tests)\n", tests.len());
let env_sources = vec![format!(". {}", info.env_file.display())];
run_tests(&tests, &env_sources, verbose).await
}
async fn run_tests(tests: &[&TestDef], env_sources: &[String], verbose: bool) -> Result<()> {
let mut passed = 0;
let mut failed = 0;
let total_start = Instant::now();
for test in tests {
let start = Instant::now();
let mut parts = Vec::new();
parts.extend(env_sources.iter().cloned());
for (key, val) in &test.env {
parts.push(format!("export {key}={val}"));
}
parts.push(test.run.clone());
let full_cmd = parts.join(" && ");
let timeout = std::time::Duration::from_secs(test.timeout);
let result = tokio::time::timeout(timeout, async {
Command::new("sh")
.args(["-c", &full_cmd])
.stdout(if verbose {
Stdio::inherit()
} else {
Stdio::piped()
})
.stderr(if verbose {
Stdio::inherit()
} else {
Stdio::piped()
})
.status()
.await
})
.await;
let elapsed = start.elapsed();
let elapsed_str = format!("{:.1}s", elapsed.as_secs_f64());
match result {
Ok(Ok(status)) if status.success() => {
passed += 1;
println!(" PASS {} ({elapsed_str})", test.name);
}
Ok(Ok(status)) => {
failed += 1;
let code = status.code().map(|c| c.to_string()).unwrap_or("?".into());
println!(" FAIL {} ({elapsed_str}) — exit code {code}", test.name);
if !verbose {
println!(" re-run with -v to see output");
}
}
Ok(Err(e)) => {
failed += 1;
println!(" FAIL {} ({elapsed_str}) — {e}", test.name);
}
Err(_) => {
failed += 1;
println!(
" FAIL {} ({elapsed_str}) — timed out after {}s",
test.name, test.timeout
);
}
}
}
let total = total_start.elapsed();
println!(
"\n{passed} passed, {failed} failed ({:.1}s)",
total.as_secs_f64()
);
if failed > 0 {
std::process::exit(1);
}
Ok(())
}
async fn run_vm(
names: &[String],
keep_alive: bool,
verbose: bool,
list: bool,
parallel: Option<usize>,
project: Option<&std::path::PathBuf>,
) -> Result<()> {
let mut args = ryra_test::Args::parse_from(std::iter::once("ryra-test"));
args.verbose = verbose;
args.keep_alive = keep_alive;
args.list = list;
if let Some(n) = parallel {
args.parallel = n;
}
args.project = project.cloned();
args.tests = names.to_vec();
ryra_test::run(args).await
}
async fn run_no_vm(
names: &[String],
verbose: bool,
list: bool,
retest: bool,
project: Option<&std::path::PathBuf>,
) -> Result<()> {
let mut args = ryra_test::Args::parse_from(std::iter::once("ryra-test"));
args.no_vm = true;
args.retest = retest;
args.verbose = verbose;
args.list = list;
args.project = project.cloned();
args.tests = names.to_vec();
ryra_test::run(args).await
}