use std::path::Path;
use std::process::Command;
use crate::project::CheckmateProject;
#[derive(Debug, Clone, Copy)]
pub enum HookEvent {
PreRun,
PostRun,
OnPass,
OnFail,
}
impl HookEvent {
fn script_name(&self) -> &'static str {
match self {
HookEvent::PreRun => "pre_run",
HookEvent::PostRun => "post_run",
HookEvent::OnPass => "on_pass",
HookEvent::OnFail => "on_fail",
}
}
}
#[derive(Debug, Default)]
pub struct HookContext {
pub run_id: Option<String>,
pub spec: Option<String>,
pub total: usize,
pub passed: usize,
pub failed: usize,
pub errors: usize,
pub duration_ms: u64,
}
pub fn fire_hook(project: &CheckmateProject, event: HookEvent, ctx: &HookContext) {
let script_name = event.script_name();
let hook_path = find_hook_script(&project.hooks_dir, script_name);
let Some(hook_path) = hook_path else {
return; };
let mut cmd = Command::new(&hook_path);
if let Some(ref run_id) = ctx.run_id {
cmd.env("CM_RUN_ID", run_id);
}
if let Some(ref spec) = ctx.spec {
cmd.env("CM_SPEC", spec);
}
cmd.env("CM_TOTAL", ctx.total.to_string());
cmd.env("CM_PASSED", ctx.passed.to_string());
cmd.env("CM_FAILED", ctx.failed.to_string());
cmd.env("CM_ERRORS", ctx.errors.to_string());
cmd.env("CM_DURATION_MS", ctx.duration_ms.to_string());
cmd.current_dir(&project.root);
match cmd.spawn() {
Ok(_) => {
}
Err(e) => {
eprintln!("Warning: Failed to run hook {}: {}", script_name, e);
}
}
}
fn find_hook_script(hooks_dir: &Path, name: &str) -> Option<std::path::PathBuf> {
let base = hooks_dir.join(name);
if base.exists() && is_executable(&base) {
return Some(base);
}
for ext in &["sh", "bash", "py", "rb", "js"] {
let with_ext = hooks_dir.join(format!("{}.{}", name, ext));
if with_ext.exists() {
return Some(with_ext);
}
}
None
}
#[cfg(unix)]
fn is_executable(path: &Path) -> bool {
use std::os::unix::fs::PermissionsExt;
path.metadata()
.map(|m| m.permissions().mode() & 0o111 != 0)
.unwrap_or(false)
}
#[cfg(not(unix))]
fn is_executable(path: &Path) -> bool {
path.exists()
}
pub fn fire_pre_run(project: &CheckmateProject, spec: Option<&str>) {
let ctx = HookContext {
spec: spec.map(String::from),
..Default::default()
};
fire_hook(project, HookEvent::PreRun, &ctx);
}
pub fn fire_post_run(
project: &CheckmateProject,
run_id: &str,
spec: Option<&str>,
total: usize,
passed: usize,
failed: usize,
errors: usize,
duration_ms: u64,
) {
let ctx = HookContext {
run_id: Some(run_id.to_string()),
spec: spec.map(String::from),
total,
passed,
failed,
errors,
duration_ms,
};
fire_hook(project, HookEvent::PostRun, &ctx);
if failed == 0 && errors == 0 {
fire_hook(project, HookEvent::OnPass, &ctx);
} else {
fire_hook(project, HookEvent::OnFail, &ctx);
}
}