use std::path::{Path, PathBuf};
use std::process::Command;
use std::sync::OnceLock;
use std::time::Duration;
#[must_use]
pub fn docker_available() -> bool {
static OK: OnceLock<bool> = OnceLock::new();
*OK.get_or_init(|| {
Command::new("docker")
.args(["info", "--format", "{{.ServerVersion}}"])
.output()
.is_ok_and(|o| o.status.success())
})
}
#[must_use]
pub fn helm_available() -> bool {
static OK: OnceLock<bool> = OnceLock::new();
*OK.get_or_init(|| {
Command::new("helm")
.arg("version")
.output()
.is_ok_and(|o| o.status.success())
})
}
#[must_use]
pub fn kubeconform_available() -> bool {
static OK: OnceLock<bool> = OnceLock::new();
*OK.get_or_init(|| {
Command::new("kubeconform")
.arg("-v")
.output()
.is_ok_and(|o| o.status.success())
})
}
#[must_use]
pub fn kind_available() -> bool {
static OK: OnceLock<bool> = OnceLock::new();
*OK.get_or_init(|| {
Command::new("kind")
.arg("version")
.output()
.is_ok_and(|o| o.status.success())
})
}
#[must_use]
pub fn kubectl_available() -> bool {
static OK: OnceLock<bool> = OnceLock::new();
*OK.get_or_init(|| {
Command::new("kubectl")
.args(["version", "--client=true"])
.output()
.is_ok_and(|o| o.status.success())
})
}
#[must_use]
pub fn tier_b_enabled() -> bool {
std::env::var("HYPERI_E2E_CLUSTER").is_ok_and(|v| v == "1" || v.eq_ignore_ascii_case("true"))
}
pub fn skip(tier: &str, test_name: &str, reason: &str) {
let line = format!("HYPERCI-SKIP[contract-e2e][{tier}]: {test_name}: {reason}");
eprintln!("{line}");
if let Some(path) = skip_log_path() {
let _ = append_line(&path, &line);
}
}
fn skip_log_path() -> Option<PathBuf> {
if let Ok(dir) = std::env::var("CARGO_TARGET_TMPDIR") {
return Some(Path::new(&dir).join("contract-e2e-skips.log"));
}
let home = std::env::var("HOME")
.or_else(|_| std::env::var("USERPROFILE"))
.ok()?;
Some(Path::new(&home).join(".cache/hyperi-ai/contract-e2e-skips.log"))
}
fn append_line(path: &Path, line: &str) -> std::io::Result<()> {
use std::io::Write;
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
let mut f = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(path)?;
writeln!(f, "{line}")
}
#[must_use]
pub fn docker_empty_creds_json() -> &'static str {
r#"{"auths": {}}"#
}
#[derive(Debug)]
pub struct KindClusterGuard {
pub name: String,
pub kubeconfig: PathBuf,
cleanup_dir: bool,
}
impl Drop for KindClusterGuard {
fn drop(&mut self) {
let _ = Command::new("kind")
.args(["delete", "cluster", "--name", &self.name])
.output();
if self.cleanup_dir
&& let Some(parent) = self.kubeconfig.parent()
{
let _ = std::fs::remove_dir_all(parent);
}
}
}
#[must_use]
pub fn ensure_kind_cluster_in(test_name: &str, kubeconfig_dir: &Path) -> Option<KindClusterGuard> {
let cluster = prepare_cluster_or_skip(test_name)?;
let kubeconfig = kubeconfig_dir.join("kubeconfig");
let kc = Command::new("kind")
.args(["get", "kubeconfig", "--name", &cluster])
.output()
.ok()?;
std::fs::write(&kubeconfig, &kc.stdout).ok()?;
Some(KindClusterGuard {
name: cluster,
kubeconfig,
cleanup_dir: false,
})
}
#[must_use]
pub fn ensure_kind_cluster(test_name: &str) -> Option<KindClusterGuard> {
let cluster = prepare_cluster_or_skip(test_name)?;
let home = std::env::var("HOME")
.or_else(|_| std::env::var("USERPROFILE"))
.ok()?;
let dir = Path::new(&home)
.join(".cache/hyperi-ai/contract-test")
.join(&cluster);
if let Err(e) = std::fs::create_dir_all(&dir) {
skip(
"tier-b",
test_name,
&format!(
"could not create cluster scratch dir {}: {e}",
dir.display()
),
);
let _ = Command::new("kind")
.args(["delete", "cluster", "--name", &cluster])
.output();
return None;
}
let kubeconfig = dir.join("kubeconfig");
let kc = Command::new("kind")
.args(["get", "kubeconfig", "--name", &cluster])
.output()
.ok()?;
std::fs::write(&kubeconfig, &kc.stdout).ok()?;
Some(KindClusterGuard {
name: cluster,
kubeconfig,
cleanup_dir: true,
})
}
fn prepare_cluster_or_skip(test_name: &str) -> Option<String> {
if !kind_available() {
skip(
"tier-b",
test_name,
"kind CLI not on PATH (install: https://kind.sigs.k8s.io/)",
);
return None;
}
if !kubectl_available() {
skip("tier-b", test_name, "kubectl not on PATH");
return None;
}
if !docker_available() {
skip(
"tier-b",
test_name,
"docker daemon not reachable (kind requires docker)",
);
return None;
}
let suffix = test_name.bytes().fold(0u32, |acc, b| {
acc.wrapping_mul(31).wrapping_add(u32::from(b))
});
let cluster = format!("hctest-{suffix:08x}");
let create = Command::new("kind")
.args(["create", "cluster", "--name", &cluster, "--wait", "120s"])
.output()
.ok()?;
if !create.status.success() {
skip(
"tier-b",
test_name,
&format!(
"kind create cluster failed: {}",
String::from_utf8_lossy(&create.stderr).trim()
),
);
return None;
}
Some(cluster)
}
pub fn wait_until(deadline: Duration, interval: Duration, mut f: impl FnMut() -> bool) -> bool {
let start = std::time::Instant::now();
while start.elapsed() < deadline {
if f() {
return true;
}
std::thread::sleep(interval);
}
f()
}