use super::commands::*;
use super::dispatch_apply_b::dispatch_apply_cmd;
use super::helpers::parse_and_validate;
use super::helpers_state::load_machine_locks;
use super::plan::save_plan_file;
use super::workspace::inject_workspace_param;
use crate::core::{planner, resolver};
use std::path::{Path, PathBuf};
#[derive(clap::Parser)]
#[command(name = "forjar")]
struct TestCli {
#[command(subcommand)]
cmd: Commands,
}
fn apply_cmd(extra: &[String]) -> Commands {
let mut argv: Vec<String> = vec!["forjar".into(), "apply".into()];
argv.extend(extra.iter().cloned());
let cli = <TestCli as clap::Parser>::try_parse_from(argv).expect("clap parse");
cli.cmd
}
fn run_apply(extra: &[&str]) -> Result<(), String> {
let argv: Vec<String> = extra.iter().map(|s| s.to_string()).collect();
std::thread::Builder::new()
.stack_size(16 * 1024 * 1024)
.spawn(move || dispatch_apply_cmd(apply_cmd(&argv), false))
.expect("spawn apply thread")
.join()
.expect("join apply thread")
}
fn write_cfg(dir: &Path, target: &Path) -> PathBuf {
let yaml = format!(
"version: \"1.0\"\nname: modes\nmachines:\n m:\n hostname: m\n addr: 127.0.0.1\nresources:\n a:\n type: file\n machine: m\n path: {}\n content: hello\n",
target.display()
);
let p = dir.join("forjar.yaml");
std::fs::write(&p, yaml).unwrap();
p
}
fn setup() -> (tempfile::TempDir, PathBuf, PathBuf) {
let dir = tempfile::tempdir().unwrap();
let target = dir.path().join("out.txt");
let cfg = write_cfg(dir.path(), &target);
let sd = dir.path().join("state");
std::fs::create_dir_all(&sd).unwrap();
(dir, cfg, sd)
}
#[cfg(test)]
mod tests {
use super::*;
fn s(p: &Path) -> String {
p.display().to_string()
}
#[test]
fn mode_preview_ok() {
let (_d, cfg, sd) = setup();
let r = run_apply(&["-f", &s(&cfg), "--state-dir", &s(&sd), "--preview"]);
assert!(r.is_ok());
}
#[test]
fn mode_output_scripts_ok() {
let (d, cfg, sd) = setup();
let scripts = d.path().join("scripts");
let r = run_apply(&[
"-f",
&s(&cfg),
"--state-dir",
&s(&sd),
"--output-scripts",
&s(&scripts),
]);
assert!(r.is_ok());
assert!(scripts.exists(), "script dir should be created");
}
#[test]
fn mode_diff_only_ok() {
let (_d, cfg, sd) = setup();
let r = run_apply(&["-f", &s(&cfg), "--state-dir", &s(&sd), "--diff-only"]);
assert!(r.is_ok());
}
#[test]
fn mode_diff_only_json_ok() {
let (_d, cfg, sd) = setup();
let r = run_apply(&[
"-f",
&s(&cfg),
"--state-dir",
&s(&sd),
"--diff-only",
"--json",
]);
assert!(r.is_ok());
}
#[test]
fn mode_check_after_apply_ok() {
let (_d, cfg, sd) = setup();
let applied = run_apply(&["-f", &s(&cfg), "--state-dir", &s(&sd), "--yes"]);
assert!(applied.is_ok(), "full apply should converge: {applied:?}");
let r = run_apply(&["-f", &s(&cfg), "--state-dir", &s(&sd), "--check"]);
assert!(r.is_ok(), "check after converge should pass: {r:?}");
}
#[test]
fn mode_check_json_ok() {
let (_d, cfg, sd) = setup();
let r = run_apply(&["-f", &s(&cfg), "--state-dir", &s(&sd), "--check", "--json"]);
assert!(r.is_ok());
}
#[test]
fn mode_check_invalid_config_err() {
let (d, _cfg, sd) = setup();
let bad = d.path().join("bad.yaml");
std::fs::write(&bad, "not: valid: yaml: [[[").unwrap();
let r = run_apply(&["-f", &s(&bad), "--state-dir", &s(&sd), "--check"]);
assert!(r.is_err());
}
#[test]
fn mode_refresh_only_empty_state_ok() {
let (_d, cfg, sd) = setup();
let r = run_apply(&["-f", &s(&cfg), "--state-dir", &s(&sd), "--refresh-only"]);
assert!(r.is_ok());
}
#[test]
fn mode_plan_file_roundtrip_ok() {
let (d, cfg, sd) = setup();
let mut config = parse_and_validate(&cfg).unwrap();
inject_workspace_param(&mut config, Some("pfws"));
resolver::resolve_data_sources(&mut config).unwrap();
let order = resolver::build_execution_order(&config).unwrap();
let locks = load_machine_locks(&config, &sd, None).unwrap();
let plan = planner::plan(&config, &order, &locks, None);
let plan_path = d.path().join("plan.json");
save_plan_file(&plan, &config, &cfg, &plan_path).unwrap();
let r = run_apply(&[
"-f",
&s(&cfg),
"--state-dir",
&s(&sd),
"--workspace",
"pfws",
"--plan-file",
&s(&plan_path),
]);
assert!(r.is_ok(), "plan-file apply should succeed: {r:?}");
}
#[test]
fn early_dry_run_verbose_ok() {
let (_d, cfg, sd) = setup();
let r = run_apply(&["-f", &s(&cfg), "--state-dir", &s(&sd), "--dry-run-verbose"]);
assert!(r.is_ok());
}
#[test]
fn early_dry_run_graph_ok() {
let (_d, cfg, sd) = setup();
let r = run_apply(&["-f", &s(&cfg), "--state-dir", &s(&sd), "--dry-run-graph"]);
assert!(r.is_ok());
}
#[test]
fn early_dry_run_cost_ok() {
let (_d, cfg, sd) = setup();
let r = run_apply(&["-f", &s(&cfg), "--state-dir", &s(&sd), "--dry-run-cost"]);
assert!(r.is_ok());
}
#[test]
fn early_canary_machine_single_ok() {
let (_d, cfg, sd) = setup();
let r = run_apply(&[
"-f",
&s(&cfg),
"--state-dir",
&s(&sd),
"--canary-machine",
"m",
]);
assert!(r.is_ok(), "canary on single machine should pass: {r:?}");
}
#[test]
fn pre_confirmation_and_preflight_ok() {
let (_d, cfg, sd) = setup();
let r = run_apply(&[
"-f",
&s(&cfg),
"--state-dir",
&s(&sd),
"--confirmation-message",
"really?",
"--pre-flight",
"true",
"--preview",
]);
assert!(r.is_ok());
}
#[test]
fn pre_preflight_failure_err() {
let (_d, cfg, sd) = setup();
let r = run_apply(&[
"-f",
&s(&cfg),
"--state-dir",
&s(&sd),
"--pre-flight",
"false",
"--preview",
]);
assert!(r.is_err());
assert!(r.unwrap_err().contains("Pre-flight"));
}
#[test]
fn pre_script_failure_err() {
let (d, cfg, sd) = setup();
let script = d.path().join("pre.sh");
std::fs::write(&script, "#!/bin/bash\nexit 1\n").unwrap();
let r = run_apply(&[
"-f",
&s(&cfg),
"--state-dir",
&s(&sd),
"--pre-script",
&s(&script),
"--preview",
]);
assert!(r.is_err());
}
#[test]
fn pre_webhook_before_best_effort_ok() {
let (_d, cfg, sd) = setup();
let r = run_apply(&[
"-f",
&s(&cfg),
"--state-dir",
&s(&sd),
"--webhook-before",
"http://127.0.0.1:9/forjar",
"--preview",
]);
assert!(r.is_ok());
}
#[test]
fn pre_abort_on_drift_clean_state_ok() {
let (_d, cfg, sd) = setup();
let applied = run_apply(&["-f", &s(&cfg), "--state-dir", &s(&sd), "--yes"]);
assert!(applied.is_ok());
let r = run_apply(&[
"-f",
&s(&cfg),
"--state-dir",
&s(&sd),
"--abort-on-drift",
"--preview",
]);
assert!(r.is_ok(), "clean state should not abort: {r:?}");
}
#[test]
fn backup_and_snapshot_before_apply_ok() {
let (_d, cfg, sd) = setup();
let r = run_apply(&[
"-f",
&s(&cfg),
"--state-dir",
&s(&sd),
"--yes",
"--backup",
"--snapshot-before",
"pre1",
]);
assert!(r.is_ok(), "apply with backups should succeed: {r:?}");
}
#[test]
fn dry_run_apply_ok() {
let (_d, cfg, sd) = setup();
let r = run_apply(&["-f", &s(&cfg), "--state-dir", &s(&sd), "--dry-run", "--yes"]);
assert!(r.is_ok());
}
}