use alembic_adapter_test::{
load_cases, run_builtin_with, run_cases, Builtins, Case, Failure, Outcome,
};
use anyhow::Context;
use clap::Parser;
use std::path::{Path, PathBuf};
use std::process::ExitCode;
use std::time::Duration;
#[derive(Parser)]
#[command(about = "check an external adapter executable for protocol conformance")]
struct Cli {
#[arg(long)]
cases: Option<PathBuf>,
#[arg(long, default_value_t = 10)]
timeout: u64,
#[arg(long)]
write_checks: bool,
#[arg(long, conflicts_with = "write_checks")]
no_provisioning_check: bool,
#[arg(last = true, required = true)]
adapter: Vec<String>,
}
fn main() -> ExitCode {
let cli = Cli::parse();
let timeout = Duration::from_secs(cli.timeout);
if cli.no_provisioning_check {
eprintln!("warning: --no-provisioning-check is the default now; drop it, or pass --write-checks to run the writing checks");
}
let builtins = Builtins {
writes: cli.write_checks,
};
let mut outcomes = run_builtin_with(&cli.adapter, timeout, builtins);
if let Some(path) = cli.cases {
let cases = match load_cases(&path) {
Ok(cases) if cases.is_empty() => {
eprintln!(
"error: no cases at {}: looked for `.json` files in that directory itself, not in subdirectories",
path.display()
);
return ExitCode::from(2);
}
Ok(cases) => cases,
Err(e) => {
eprintln!("error: {e:#}");
return ExitCode::from(2);
}
};
match unloaded_case_dirs(&path) {
Ok(dirs) if !dirs.is_empty() => {
let list: Vec<String> = dirs.iter().map(|d| d.display().to_string()).collect();
eprintln!(
"error: cases in {} are not loaded: `--cases` reads {} itself, not its subdirectories",
list.join(", "),
path.display()
);
return ExitCode::from(2);
}
Ok(_) => {}
Err(e) => {
eprintln!("error: {e:#}");
return ExitCode::from(2);
}
}
outcomes.extend(run_cases(&cli.adapter, timeout, &cases));
}
report(&outcomes)
}
fn unloaded_case_dirs(path: &Path) -> anyhow::Result<Vec<PathBuf>> {
if !path.is_dir() {
return Ok(Vec::new());
}
let mut dirs = Vec::new();
for entry in std::fs::read_dir(path).with_context(|| format!("reading {}", path.display()))? {
let entry = entry?;
let child = entry.path();
if entry.file_type()?.is_dir() && holds_case_files(&child)? {
dirs.push(child);
}
}
dirs.sort();
Ok(dirs)
}
fn holds_case_files(dir: &Path) -> anyhow::Result<bool> {
for entry in std::fs::read_dir(dir).with_context(|| format!("reading {}", dir.display()))? {
let entry = entry?;
let path = entry.path();
if entry.file_type()?.is_dir() {
if holds_case_files(&path)? {
return Ok(true);
}
} else if path.extension().and_then(|e| e.to_str()) == Some("json")
&& loads_as_a_case(&path)
{
return Ok(true);
}
}
Ok(false)
}
fn loads_as_a_case(path: &Path) -> bool {
std::fs::read_to_string(path).is_ok_and(|text| serde_json::from_str::<Case>(&text).is_ok())
}
fn report(outcomes: &[Outcome]) -> ExitCode {
let width = outcomes.iter().map(|o| o.name.len()).max().unwrap_or(0);
let mut failed = 0;
let mut skipped = 0;
for outcome in outcomes {
let name = &outcome.name;
match (&outcome.skipped, &outcome.failure) {
(Some(reason), _) => {
skipped += 1;
println!("{name:<width$} skipped ({reason})");
}
(None, None) => println!("{name:<width$} ok"),
(None, Some(failure)) => {
failed += 1;
println!("{name:<width$} FAILED");
print_failure(failure);
}
}
}
let passed = outcomes.len() - failed - skipped;
let mut summary = format!("{passed} passed");
if skipped > 0 {
summary.push_str(&format!(", {skipped} skipped"));
}
println!();
if failed == 0 {
println!("{summary}");
ExitCode::SUCCESS
} else {
println!("{summary}, {failed} failed");
ExitCode::from(1)
}
}
fn print_failure(failure: &Failure) {
println!(" error: {}", failure.message);
println!(" status: {}", failure.status);
print_stream("stdout", &failure.stdout);
print_stream("stderr", &failure.stderr);
}
fn print_stream(label: &str, content: &str) {
let content = content.trim_end_matches('\n');
if content.is_empty() {
println!(" {label}: <empty>");
} else {
println!(" {label}:");
for line in content.lines() {
println!(" {line}");
}
}
}