use super::*;
use crate::core::types::{Resource, ResourceType};
fn run(script: &str) -> i32 {
let out = std::process::Command::new("sh")
.arg("-c")
.arg(script)
.output()
.expect("sh is available");
out.status.code().unwrap_or(-1)
}
fn tmp(tag: &str) -> std::path::PathBuf {
let d = std::env::temp_dir().join(format!("forjar-ckv-{}-{}", tag, std::process::id()));
let _ = std::fs::remove_dir_all(&d);
std::fs::create_dir_all(&d).unwrap();
d
}
#[test]
fn absent_file_check_exits_nonzero() {
let d = tmp("file");
let mut r = Resource {
resource_type: ResourceType::File,
..Default::default()
};
r.path = Some(d.join("definitely-absent.txt").display().to_string());
r.state = Some("file".to_string());
let script = check_script(&r).expect("file has a check script");
assert_ne!(
run(&script),
0,
"a file that does not exist must FAIL its check, not pass:\n{script}"
);
std::fs::write(d.join("definitely-absent.txt"), "x").unwrap();
assert_eq!(
run(&script),
0,
"a file that exists must PASS its check:\n{script}"
);
let _ = std::fs::remove_dir_all(&d);
}
#[test]
fn absent_directory_check_exits_nonzero() {
let d = tmp("dir");
let mut r = Resource {
resource_type: ResourceType::File,
..Default::default()
};
r.path = Some(d.join("no-such-dir").display().to_string());
r.state = Some("directory".to_string());
let script = check_script(&r).unwrap();
assert_ne!(run(&script), 0, "missing directory must fail:\n{script}");
std::fs::create_dir_all(d.join("no-such-dir")).unwrap();
assert_eq!(run(&script), 0, "present directory must pass:\n{script}");
let _ = std::fs::remove_dir_all(&d);
}
#[test]
fn file_state_absent_is_inverted_correctly() {
let d = tmp("absent");
let target = d.join("should-not-exist");
let mut r = Resource {
resource_type: ResourceType::File,
..Default::default()
};
r.path = Some(target.display().to_string());
r.state = Some("absent".to_string());
let script = check_script(&r).unwrap();
assert_eq!(
run(&script),
0,
"state:absent with the path already gone is CONVERGED:\n{script}"
);
std::fs::write(&target, "oops").unwrap();
assert_ne!(
run(&script),
0,
"state:absent with the path present must FAIL:\n{script}"
);
let _ = std::fs::remove_dir_all(&d);
}
#[test]
fn task_with_missing_output_artifact_exits_nonzero() {
let d = tmp("task");
let r = Resource {
resource_type: ResourceType::Task,
output_artifacts: vec![d.join("demo").display().to_string()],
..Default::default()
};
let script = check_script(&r).unwrap();
assert_ne!(
run(&script),
0,
"a task whose output artifact was deleted must FAIL:\n{script}"
);
std::fs::write(d.join("demo"), "bin").unwrap();
assert_eq!(run(&script), 0, "artifact present must pass:\n{script}");
let _ = std::fs::remove_dir_all(&d);
}
#[test]
fn task_completion_check_drives_the_exit_code() {
let mut r = Resource {
resource_type: ResourceType::Task,
..Default::default()
};
r.completion_check = Some("false".to_string());
assert_ne!(
run(&check_script(&r).unwrap()),
0,
"a failing completion_check must fail the resource check"
);
r.completion_check = Some("true".to_string());
assert_eq!(
run(&check_script(&r).unwrap()),
0,
"a passing completion_check must pass"
);
}
#[test]
fn absent_user_check_exits_nonzero() {
let mut r = Resource {
resource_type: ResourceType::User,
..Default::default()
};
r.name = Some("forjar-no-such-user-ckv".to_string());
assert_ne!(
run(&check_script(&r).unwrap()),
0,
"a user that does not exist must fail its check"
);
r.name = Some("root".to_string());
assert_eq!(
run(&check_script(&r).unwrap()),
0,
"root exists, so its check must pass"
);
}
#[test]
fn absent_docker_container_check_exits_nonzero() {
let mut r = Resource {
resource_type: ResourceType::Docker,
..Default::default()
};
r.name = Some("forjar-no-such-container-ckv".to_string());
r.image = Some("scratch".to_string());
assert_ne!(
run(&check_script(&r).unwrap()),
0,
"a container that does not exist must fail its check"
);
}
#[test]
fn a_task_declaring_nothing_is_not_reported_as_converged() {
let r = Resource {
resource_type: ResourceType::Task,
command: Some("echo hi".to_string()),
..Default::default()
};
assert_ne!(
run(&check_script(&r).unwrap()),
0,
"a task with no completion evidence must not report converged"
);
}
#[test]
fn no_resource_type_generates_an_unfailable_check_script() {
let d = tmp("completeness");
let absent = d.join("absent-everything");
let cases: Vec<(String, Resource)> = ALL_CHECKABLE_TYPES
.iter()
.map(|ty| {
let mut r = Resource {
resource_type: ty.clone(),
..Default::default()
};
r.name = Some("forjar-absent-ckv".to_string());
r.path = Some(absent.display().to_string());
r.state = Some("file".to_string());
r.image = Some("forjar/absent:ckv".to_string());
r.schedule = Some("0 * * * *".to_string());
r.command = Some("/bin/true".to_string());
r.port = Some("65534".to_string());
r.packages = vec!["forjar-absent-pkg-ckv".to_string()];
r.output_artifacts = vec![absent.display().to_string()];
(ty.to_string(), r)
})
.collect();
let mut unfailable = Vec::new();
for (label, r) in &cases {
let Ok(script) = check_script(r) else {
continue; };
let code = run(&script);
if code == 0 {
unfailable.push(format!(
"{label}: exited 0 (claimed converged) for a resource that does \
not exist:\n{script}\n"
));
}
}
assert!(
unfailable.is_empty(),
"these check scripts report success for a resource that does not exist \
— the exact defect that made `forjar check` a no-op:\n\n{}",
unfailable.join("\n")
);
let _ = std::fs::remove_dir_all(&d);
}
#[test]
fn gpu_check_is_not_unconditional() {
let mut present = Resource {
resource_type: ResourceType::Gpu,
..Default::default()
};
present.name = Some("gpu0".to_string());
present.state = Some("present".to_string());
let mut absent = present.clone();
absent.state = Some("absent".to_string());
let p = run(&check_script(&present).unwrap());
let a = run(&check_script(&absent).unwrap());
assert_ne!(
(p == 0),
(a == 0),
"state:present and state:absent must not agree — exactly one describes \
this host. present exited {p}, absent exited {a}"
);
}
const ALL_CHECKABLE_TYPES: &[ResourceType] = &[
ResourceType::Package,
ResourceType::File,
ResourceType::Service,
ResourceType::Mount,
ResourceType::User,
ResourceType::Docker,
ResourceType::Pepita,
ResourceType::Network,
ResourceType::Cron,
ResourceType::Model,
ResourceType::Task,
ResourceType::WasmBundle,
ResourceType::Image,
ResourceType::Build,
ResourceType::GithubRelease,
ResourceType::OverlayInterface,
];