use super::apply_selection::strip_unrequested_phony;
use crate::core::types::{ForjarConfig, Resource};
fn config() -> ForjarConfig {
let mut c = ForjarConfig::default();
let mut clean = Resource {
phony: true,
command: Some("rm -rf build".to_string()),
..Default::default()
};
clean.name = Some("clean".to_string());
c.resources.insert("clean".to_string(), clean);
let build = Resource {
command: Some("cc -o build/app a.c".to_string()),
output_artifacts: vec!["build/app".to_string()],
depends_on: vec!["clean".to_string(), "mkdir".to_string()],
..Default::default()
};
c.resources.insert("build".to_string(), build);
c.resources
.insert("mkdir".to_string(), Resource::default());
c
}
#[test]
fn a_bulk_apply_drops_every_phony_resource() {
let mut c = config();
strip_unrequested_phony(&mut c, &[]);
assert!(!c.resources.contains_key("clean"));
assert!(c.resources.contains_key("build"));
assert!(c.resources.contains_key("mkdir"));
}
#[test]
fn dropping_a_phony_resource_scrubs_edges_to_it() {
let mut c = config();
strip_unrequested_phony(&mut c, &[]);
assert_eq!(c.resources["build"].depends_on, vec!["mkdir".to_string()]);
}
#[test]
fn an_explicitly_requested_phony_resource_is_kept() {
let mut c = config();
strip_unrequested_phony(&mut c, &["clean".to_string()]);
assert!(c.resources.contains_key("clean"));
assert_eq!(
c.resources["build"].depends_on,
vec!["clean".to_string(), "mkdir".to_string()],
"its edges survive when it does"
);
}
#[test]
fn a_phony_prerequisite_is_not_pulled_in_by_another_goal() {
let mut c = config();
strip_unrequested_phony(&mut c, &["build".to_string()]);
assert!(!c.resources.contains_key("clean"));
}
#[test]
fn non_phony_resources_are_never_touched() {
let mut c = ForjarConfig::default();
c.resources.insert("a".to_string(), Resource::default());
let before = c.resources.len();
strip_unrequested_phony(&mut c, &[]);
assert_eq!(c.resources.len(), before);
}
#[test]
fn a_requested_phony_resource_always_plans_as_a_change() {
use crate::core::planner;
use crate::core::types::StateLock;
let mut c: ForjarConfig = serde_yaml_ng::from_str(
r#"
version: "1.0"
name: phony-plan
machines:
local:
hostname: localhost
addr: localhost
resources:
test:
type: task
machine: local
phony: true
command: "cargo test"
"#,
)
.expect("fixture parses");
let r = c.resources["test"].clone();
let hash = planner::hash_desired_state(&r);
let lock: StateLock = serde_yaml_ng::from_str(&format!(
"schema: \"1.0\"\nmachine: local\nhostname: localhost\ngenerated_at: \"now\"\n\
generator: test\nblake3_version: \"1\"\nresources:\n test:\n type: task\n\
\x20 status: converged\n hash: \"{hash}\"\n"
))
.expect("lock fixture parses");
let mut locks = std::collections::HashMap::new();
locks.insert("local".to_string(), lock);
let plan = planner::plan(&c, &["test".to_string()], &locks, None);
assert_eq!(
plan.changes.len(),
1,
"a converged, hash-matching phony must still be planned: {:?}",
plan.changes
);
assert!(
!format!("{:?}", plan.changes[0].action).contains("NoOp"),
"requesting an action by name means running it, got {:?}",
plan.changes[0].action
);
strip_unrequested_phony(&mut c, &[]);
assert!(c.resources.is_empty(), "bulk apply must not see it");
}
fn validation_errors(resources_yaml: &str) -> Result<(), String> {
let d = tempfile::tempdir().unwrap();
let f = d.path().join("forjar.yaml");
std::fs::write(
&f,
format!(
"version: \"1.0\"\nname: v\nmachines:\n local:\n hostname: localhost\n\
\x20 addr: localhost\nresources:\n{resources_yaml}"
),
)
.unwrap();
super::helpers::parse_and_validate(&f).map(|_| ())
}
#[test]
fn a_phony_resource_may_have_no_command() {
let grouping = " app:\n type: task\n machine: local\n command: \"cc\"\n\
\x20 all:\n type: task\n machine: local\n phony: true\n\
\x20 depends_on: [app]\n";
assert!(
validation_errors(grouping).is_ok(),
"a phony grouping node needs no command: {:?}",
validation_errors(grouping)
);
let bare = " deny:\n type: task\n machine: local\n phony: true\n";
assert!(
validation_errors(bare).is_ok(),
"a bare .PHONY name with no rule is legal in make: {:?}",
validation_errors(bare)
);
}
#[test]
fn a_non_phony_task_still_requires_a_command() {
let err = validation_errors(" t:\n type: task\n machine: local\n")
.expect_err("a non-phony task with no command is invalid");
assert!(err.contains("has no command"), "{err}");
}