use std::path::Path;
use std::time::Duration;
use lean_rs_worker_parent::{
LeanWorkerChild, LeanWorkerDeclarationInspectionRequest, LeanWorkerError, LeanWorkerHostHandleBuilder,
};
use serde::{Deserialize, Serialize};
use crate::toolchain::ToolchainId;
#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
#[serde(tag = "result", rename_all = "snake_case")]
pub(crate) enum SmokeOutcome {
Passed,
Failed { detail: String },
}
impl SmokeOutcome {
pub(crate) fn label(&self) -> &'static str {
match self {
Self::Passed => "runs",
Self::Failed { .. } => "crashed",
}
}
pub(crate) fn failure_detail(&self) -> Option<&str> {
match self {
Self::Passed => None,
Self::Failed { detail } => Some(detail),
}
}
}
const SMOKE_STARTUP_TIMEOUT: Duration = Duration::from_mins(1);
pub(crate) fn probe(worker_path: &Path, lean_sysroot: &Path, toolchain: &ToolchainId) -> SmokeOutcome {
let project_root = match tempfile::tempdir() {
Ok(dir) => dir,
Err(e) => {
return SmokeOutcome::Failed {
detail: format!("could not create smoke working directory for {toolchain}: {e}"),
};
}
};
let open = LeanWorkerHostHandleBuilder::shims_only(project_root.path(), std::iter::empty::<String>())
.worker_child(LeanWorkerChild::for_toolchain(worker_path, lean_sysroot))
.startup_timeout(SMOKE_STARTUP_TIMEOUT)
.long_running_requests()
.open();
let mut handle = match open {
Ok(handle) => handle,
Err(err) => return SmokeOutcome::Failed { detail: describe(&err) },
};
let request = LeanWorkerDeclarationInspectionRequest::new("Nat.add_zero");
let outcome = match handle.inspect_declaration_with_imports(vec!["Init".to_owned()], &request, None, None) {
Ok(_) => SmokeOutcome::Passed,
Err(err) => SmokeOutcome::Failed { detail: describe(&err) },
};
drop(handle.terminate());
outcome
}
#[allow(
clippy::wildcard_enum_match_arm,
reason = "only the two process-death variants carry an exit status to prefer; LeanWorkerError is upstream-evolving and every other variant falls back to its Display string"
)]
fn describe(err: &LeanWorkerError) -> String {
match err {
LeanWorkerError::ChildPanicOrAbort { exit } | LeanWorkerError::ChildExited { exit } => exit.status.clone(),
other => other.to_string(),
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
#[test]
fn outcome_round_trips_through_sidecar_json() {
let passed = SmokeOutcome::Passed;
let failed = SmokeOutcome::Failed {
detail: "signal: 11 (SIGSEGV)".to_owned(),
};
for outcome in [&passed, &failed] {
let json = serde_json::to_string(outcome).unwrap();
let back: SmokeOutcome = serde_json::from_str(&json).unwrap();
assert_eq!(&back, outcome);
}
assert!(
serde_json::to_string(&passed)
.unwrap()
.contains("\"result\":\"passed\"")
);
assert_eq!(passed.label(), "runs");
assert_eq!(failed.label(), "crashed");
assert_eq!(failed.failure_detail(), Some("signal: 11 (SIGSEGV)"));
assert_eq!(passed.failure_detail(), None);
}
#[test]
fn probe_of_a_non_worker_binary_fails_rather_than_panics() {
let toolchain = ToolchainId::parse("v4.30.0").unwrap();
let bogus = Path::new("/bin/echo");
let sysroot = std::env::temp_dir();
assert!(matches!(
probe(bogus, &sysroot, &toolchain),
SmokeOutcome::Failed { .. }
));
}
}