alopex-core 0.7.2

Core storage engine for Alopex DB - LSM-tree, columnar storage, and vector index
Documentation
use chrono::{DateTime, Utc};
use serde::Serialize;
use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use std::process::Command;

use super::lane::Lane;
use super::metrics::{MetricsReport, SloConfig};

#[derive(Clone, Debug)]
pub struct ArtifactPaths {
    pub root: PathBuf,
    pub logs_dir: PathBuf,
    pub checks_dir: PathBuf,
}

#[derive(Clone, Debug, Serialize)]
pub struct RunMetadata {
    pub test_name: String,
    pub lane: String,
    pub seed: u64,
    pub topology: Option<String>,
    pub binary_version: String,
    pub execution_model: String,
    pub concurrency: usize,
    pub scenario_timeout_ms: u64,
    pub operation_timeout_ms: u64,
    pub metrics_interval_ms: u64,
    pub warmup_ops: usize,
    pub slo: Option<SloConfig>,
    pub storage_mode: Option<String>,
    pub replay: bool,
    pub started_at: String,
    pub git_sha: Option<String>,
    pub system: SystemInfo,
    pub env: BTreeMap<String, String>,
}

#[derive(Clone, Debug, Serialize)]
pub struct SystemInfo {
    pub os: String,
    pub arch: String,
    pub kernel: String,
    pub cpu: String,
    pub mem_total_mb: u64,
    pub disk_total_gb: u64,
    pub compiler: String,
}

#[derive(Clone, Debug, Serialize)]
pub struct CheckSummary {
    pub skipped: bool,
    pub success: bool,
    pub watchdog: String,
    pub slo_passed: bool,
    pub error: Option<String>,
    pub duration_ms: u64,
}

pub fn prepare_artifacts(
    lane: Lane,
    test_name: &str,
    started_at: &DateTime<Utc>,
) -> Option<ArtifactPaths> {
    if let Ok(value) = std::env::var("STRESS_ARTIFACTS_DIR") {
        if value.trim().is_empty() {
            return None;
        }
    }

    let root = artifacts_root();
    if root.as_os_str().is_empty() {
        return None;
    }
    let stamp = started_at.format("%Y%m%dT%H%M%SZ").to_string();
    let base = root
        .join(lane.as_str())
        .join(sanitize(test_name))
        .join(stamp);

    let logs_dir = base.join("logs");
    let checks_dir = base.join("checks");

    if std::fs::create_dir_all(&logs_dir).is_err() {
        return None;
    }
    if std::fs::create_dir_all(&checks_dir).is_err() {
        return None;
    }

    Some(ArtifactPaths {
        root: base,
        logs_dir,
        checks_dir,
    })
}

pub fn write_run_json(paths: &ArtifactPaths, metadata: &RunMetadata) {
    let path = paths.root.join("run.json");
    if let Ok(body) = serde_json::to_string_pretty(metadata) {
        let _ = std::fs::write(path, body);
    }
}

pub fn write_command_txt(paths: &ArtifactPaths, command: &str) {
    let path = paths.root.join("command.txt");
    let _ = std::fs::write(path, command);
}

pub fn write_metrics(paths: &ArtifactPaths, report: &MetricsReport) {
    let path = paths.root.join("metrics.json");
    let _ = std::fs::write(path, report.to_json());
}

pub fn write_checks(paths: &ArtifactPaths, summary: &CheckSummary) {
    let path = paths.checks_dir.join("summary.json");
    if let Ok(body) = serde_json::to_string_pretty(summary) {
        let _ = std::fs::write(path, body);
    }
}

pub fn write_log(paths: &ArtifactPaths, message: &str) {
    let path = paths.logs_dir.join("harness.log");
    let _ = std::fs::write(path, message);
}

fn artifacts_root() -> PathBuf {
    std::env::var("STRESS_ARTIFACTS_DIR")
        .map(PathBuf::from)
        .unwrap_or_else(|_| PathBuf::from("artifacts"))
}

fn sanitize(value: &str) -> String {
    value
        .chars()
        .map(|c| {
            if c.is_ascii_alphanumeric() || c == '-' || c == '_' {
                c
            } else {
                '_'
            }
        })
        .collect()
}

pub fn collect_env_metadata() -> BTreeMap<String, String> {
    let mut out = BTreeMap::new();
    for (key, value) in std::env::vars() {
        if should_capture_env(&key) {
            out.insert(key, value);
        }
    }
    out
}

pub fn detect_git_sha() -> Option<String> {
    std::env::var("GITHUB_SHA")
        .ok()
        .or_else(|| std::env::var("GIT_SHA").ok())
}

pub fn command_hint(lane: Lane, seed: u64, package: &str) -> String {
    let feature = format!("lane_{}", lane.as_str());
    format!(
        "STRESS_LANE={lane} STRESS_SEED={seed} cargo test -p {package} --tests --features {feature}"
    )
}

pub fn topology_env() -> Option<String> {
    std::env::var("STRESS_TOPOLOGY")
        .ok()
        .and_then(|v| normalize_non_empty(&v))
}

pub fn binary_version() -> String {
    format!("{}@{}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION"))
}

pub fn system_info() -> SystemInfo {
    SystemInfo {
        os: std::env::consts::OS.to_string(),
        arch: std::env::consts::ARCH.to_string(),
        kernel: kernel_version(),
        cpu: cpu_info(),
        mem_total_mb: mem_total_mb(),
        disk_total_gb: disk_total_gb(),
        compiler: compiler_version(),
    }
}

pub fn storage_mode_env() -> Option<String> {
    std::env::var("STRESS_STORAGE_MODE").ok()
}

pub fn log_path(paths: &ArtifactPaths, file: impl AsRef<Path>) -> PathBuf {
    paths.logs_dir.join(file)
}

fn should_capture_env(key: &str) -> bool {
    if key == "CI"
        || key.starts_with("STRESS_")
        || key.starts_with("RUST")
        || key.starts_with("CARGO_")
        || key.starts_with("GITHUB_")
        || key.starts_with("RUNNER_")
    {
        return true;
    }
    matches!(
        key,
        "HOSTNAME"
            | "COMPUTERNAME"
            | "USER"
            | "USERNAME"
            | "SHELL"
            | "TERM"
            | "PWD"
            | "RUSTUP_HOME"
            | "RUSTUP_TOOLCHAIN"
            | "CARGO_HOME"
            | "CARGO_TARGET_DIR"
    )
}

fn normalize_non_empty(value: &str) -> Option<String> {
    let trimmed = value.trim();
    if trimmed.is_empty() {
        None
    } else {
        Some(trimmed.to_string())
    }
}

fn kernel_version() -> String {
    if let Some(val) = read_trimmed(Path::new("/proc/sys/kernel/osrelease")) {
        return val;
    }
    command_output("uname", &["-r"]).unwrap_or_else(|| "unknown".to_string())
}

fn cpu_info() -> String {
    if let Some(val) = linux_cpu_model() {
        return val;
    }
    if let Ok(val) = std::env::var("PROCESSOR_IDENTIFIER") {
        if let Some(val) = normalize_non_empty(&val) {
            return val;
        }
    }
    if let Some(val) = command_output("sysctl", &["-n", "machdep.cpu.brand_string"]) {
        return val;
    }
    "unknown".to_string()
}

fn mem_total_mb() -> u64 {
    if let Some(kb) = linux_mem_total_kb() {
        return kb / 1024;
    }
    if let Some(bytes) =
        command_output("sysctl", &["-n", "hw.memsize"]).and_then(|v| v.parse::<u64>().ok())
    {
        return bytes / 1024 / 1024;
    }
    0
}

fn disk_total_gb() -> u64 {
    disk_total_bytes(Path::new("/"))
        .map(|bytes| bytes / 1024 / 1024 / 1024)
        .unwrap_or(0)
}

fn compiler_version() -> String {
    let rustc = std::env::var("RUSTC").unwrap_or_else(|_| "rustc".to_string());
    command_output(&rustc, &["--version"]).unwrap_or_else(|| "unknown".to_string())
}

fn read_trimmed(path: &Path) -> Option<String> {
    std::fs::read_to_string(path)
        .ok()
        .and_then(|v| normalize_non_empty(&v))
}

fn linux_cpu_model() -> Option<String> {
    let content = std::fs::read_to_string("/proc/cpuinfo").ok()?;
    for line in content.lines() {
        if let Some(value) = line.strip_prefix("model name") {
            return normalize_non_empty(value.trim_start_matches(':'));
        }
        if let Some(value) = line.strip_prefix("Hardware") {
            return normalize_non_empty(value.trim_start_matches(':'));
        }
    }
    None
}

fn linux_mem_total_kb() -> Option<u64> {
    let content = std::fs::read_to_string("/proc/meminfo").ok()?;
    for line in content.lines() {
        if let Some(value) = line.strip_prefix("MemTotal:") {
            let digits = value.split_whitespace().next()?;
            return digits.parse::<u64>().ok();
        }
    }
    None
}

fn command_output(program: &str, args: &[&str]) -> Option<String> {
    let output = Command::new(program).args(args).output().ok()?;
    if !output.status.success() {
        return None;
    }
    let text = String::from_utf8_lossy(&output.stdout);
    normalize_non_empty(&text)
}

#[cfg(target_family = "unix")]
fn disk_total_bytes(path: &Path) -> Option<u64> {
    use std::ffi::CString;
    use std::os::unix::ffi::OsStrExt;

    let mut stat: libc::statvfs = unsafe { std::mem::zeroed() };
    let c_path = CString::new(path.as_os_str().as_bytes()).ok()?;
    let res = unsafe { libc::statvfs(c_path.as_ptr(), &mut stat) };
    if res != 0 {
        return None;
    }
    let block_size = stat.f_frsize as u64;
    Some(block_size.saturating_mul(stat.f_blocks as u64))
}

#[cfg(not(target_family = "unix"))]
fn disk_total_bytes(_path: &Path) -> Option<u64> {
    None
}