use super::apply_variants::*;
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::{executor, planner, resolver, types};
use std::path::{Path, PathBuf};
fn write_cfg(dir: &Path, target: &Path, content: &str) -> PathBuf {
let yaml = format!(
"version: \"1.0\"\nname: variants\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: {}\n",
target.display(),
content
);
let p = dir.join("forjar.yaml");
std::fs::write(&p, yaml).unwrap();
p
}
fn apply_local(config: &types::ForjarConfig, sd: &Path) -> Vec<types::ApplyResult> {
let cfg = executor::ApplyConfig {
config,
state_dir: sd,
force: false,
dry_run: false,
machine_filter: None,
resource_filter: None,
tag_filter: None,
group_filter: None,
timeout_secs: None,
force_unlock: false,
progress: false,
retry: 0,
parallel: None,
resource_timeout: None,
rollback_on_failure: false,
max_parallel: None,
trace: false,
run_id: None,
refresh: false,
force_tag: None,
};
executor::apply(&cfg).expect("apply")
}
fn converged_setup() -> (tempfile::TempDir, PathBuf, PathBuf, PathBuf) {
let dir = tempfile::tempdir().unwrap();
let target = dir.path().join("out.txt");
let cfg = write_cfg(dir.path(), &target, "hello");
let sd = dir.path().join("state");
std::fs::create_dir_all(&sd).unwrap();
let config = parse_and_validate(&cfg).unwrap();
let results = apply_local(&config, &sd);
assert_eq!(results[0].resources_converged, 1);
(dir, cfg, sd, target)
}
fn save_plan_for(cfg: &Path, sd: &Path, ws: Option<&str>, out: &Path) {
let mut config = parse_and_validate(cfg).unwrap();
inject_workspace_param(&mut config, ws);
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);
save_plan_file(&plan, &config, cfg, out).unwrap();
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn refresh_invalid_config_err() {
let dir = tempfile::tempdir().unwrap();
let p = dir.path().join("bad.yaml");
std::fs::write(&p, "not: valid: yaml: [[[").unwrap();
let r = cmd_refresh_only(&p, dir.path(), None, false, None, None, None);
assert!(r.is_err());
}
#[test]
fn refresh_empty_state_ok() {
let dir = tempfile::tempdir().unwrap();
let target = dir.path().join("o.txt");
let cfg = write_cfg(dir.path(), &target, "x");
let sd = dir.path().join("state");
std::fs::create_dir_all(&sd).unwrap();
let r = cmd_refresh_only(&cfg, &sd, None, false, None, None, None);
assert!(r.is_ok());
}
#[test]
fn refresh_after_converge_ok() {
let (_d, cfg, sd, _t) = converged_setup();
let r = cmd_refresh_only(&cfg, &sd, None, false, None, None, None);
assert!(r.is_ok(), "refresh on converged state: {r:?}");
let r2 = cmd_refresh_only(&cfg, &sd, None, true, None, None, None);
assert!(r2.is_ok());
}
#[test]
fn refresh_detects_drift_verbose() {
let (_d, cfg, sd, target) = converged_setup();
cmd_refresh_only(&cfg, &sd, None, false, None, None, None).unwrap();
std::fs::write(&target, "tampered").unwrap();
let r = cmd_refresh_only(&cfg, &sd, None, true, None, None, None);
assert!(r.is_ok(), "drift is reported, not an error: {r:?}");
}
#[test]
fn refresh_machine_filter_no_match_ok() {
let (_d, cfg, sd, _t) = converged_setup();
let r = cmd_refresh_only(&cfg, &sd, Some("ghost"), false, None, None, None);
assert!(r.is_ok());
}
#[test]
fn refresh_with_workspace_param_ok() {
let (_d, cfg, sd, _t) = converged_setup();
let r = cmd_refresh_only(&cfg, &sd, Some("m"), true, Some(30), None, Some("ws1"));
assert!(r.is_ok());
}
#[test]
fn refresh_missing_env_file_err() {
let (_d, cfg, sd, _t) = converged_setup();
let r = cmd_refresh_only(
&cfg,
&sd,
None,
false,
None,
Some(Path::new("/nonexistent/params.env")),
None,
);
assert!(r.is_err());
}
#[test]
fn refresh_skips_non_converged_resources() {
let dir = tempfile::tempdir().unwrap();
let cfg = write_cfg(dir.path(), Path::new("/dev/null/forjar-cov/x"), "y");
let sd = dir.path().join("state");
std::fs::create_dir_all(&sd).unwrap();
let config = parse_and_validate(&cfg).unwrap();
let _ = apply_local(&config, &sd); let r = cmd_refresh_only(&cfg, &sd, None, true, None, None, None);
assert!(r.is_ok(), "failed resources are skipped: {r:?}");
}
#[test]
fn from_plan_happy_path_ok() {
let dir = tempfile::tempdir().unwrap();
let target = dir.path().join("planned.txt");
let cfg = write_cfg(dir.path(), &target, "v1");
let sd = dir.path().join("state");
std::fs::create_dir_all(&sd).unwrap();
let plan_path = dir.path().join("plan.json");
save_plan_for(&cfg, &sd, Some("pfws"), &plan_path);
let r = cmd_apply_from_plan(&cfg, &sd, &plan_path, true, None, Some("pfws"));
assert!(r.is_ok(), "saved plan should apply cleanly: {r:?}");
assert!(target.exists(), "planned file resource should be created");
}
#[test]
fn from_plan_zero_changes_ok() {
let (d, cfg, sd, _t) = converged_setup();
let plan_path = d.path().join("plan.json");
save_plan_for(&cfg, &sd, Some("pfws"), &plan_path);
let r = cmd_apply_from_plan(&cfg, &sd, &plan_path, false, None, Some("pfws"));
assert!(r.is_ok(), "no-change plan exits early: {r:?}");
}
#[test]
fn from_plan_missing_file_err() {
let (d, cfg, sd, _t) = converged_setup();
let r = cmd_apply_from_plan(
&cfg,
&sd,
&d.path().join("nope.json"),
false,
None,
Some("pfws"),
);
assert!(r.is_err());
assert!(r.unwrap_err().contains("read plan file"));
}
#[test]
fn from_plan_bad_format_err() {
let (d, cfg, sd, _t) = converged_setup();
let plan_path = d.path().join("plan.json");
std::fs::write(&plan_path, r#"{"format": "bogus-v9"}"#).unwrap();
let r = cmd_apply_from_plan(&cfg, &sd, &plan_path, false, None, Some("pfws"));
assert!(r.is_err());
assert!(r.unwrap_err().contains("unsupported plan format"));
}
#[test]
fn from_plan_config_changed_err() {
let dir = tempfile::tempdir().unwrap();
let target = dir.path().join("c.txt");
let cfg = write_cfg(dir.path(), &target, "v1");
let sd = dir.path().join("state");
std::fs::create_dir_all(&sd).unwrap();
let plan_path = dir.path().join("plan.json");
save_plan_for(&cfg, &sd, Some("pfws"), &plan_path);
write_cfg(dir.path(), &target, "v2-changed");
let r = cmd_apply_from_plan(&cfg, &sd, &plan_path, false, None, Some("pfws"));
assert!(r.is_err());
assert!(r.unwrap_err().contains("config has changed"));
}
#[test]
fn from_plan_failing_resource_err() {
let dir = tempfile::tempdir().unwrap();
let cfg = write_cfg(dir.path(), Path::new("/dev/null/forjar-cov/p.txt"), "y");
let sd = dir.path().join("state");
std::fs::create_dir_all(&sd).unwrap();
let plan_path = dir.path().join("plan.json");
save_plan_for(&cfg, &sd, Some("pfws"), &plan_path);
let r = cmd_apply_from_plan(&cfg, &sd, &plan_path, false, None, Some("pfws"));
assert!(r.is_err());
assert!(r.unwrap_err().contains("failed"));
}
}