use super::apply_selection::{resolve_selection, Selection, Selectors};
use crate::core::types;
fn machine() -> types::Machine {
serde_yaml_ng::from_str("hostname: h\naddr: localhost\n").expect("machine fixture parses")
}
fn res(deps: &[&str], group: Option<&str>, machines: &[&str]) -> types::Resource {
types::Resource {
resource_type: types::ResourceType::File,
machine: types::MachineTarget::Multiple(
machines.iter().map(|m| (*m).to_string()).collect(),
),
depends_on: deps.iter().map(|d| (*d).to_string()).collect(),
resource_group: group.map(str::to_string),
..Default::default()
}
}
fn cfg() -> types::ForjarConfig {
let mut config = types::ForjarConfig {
version: "1.0".to_string(),
name: "selection".to_string(),
..Default::default()
};
config.machines.insert("local".to_string(), machine());
config.machines.insert("other".to_string(), machine());
config
.resources
.insert("a".to_string(), res(&["b"], Some("web"), &["local"]));
config
.resources
.insert("b".to_string(), res(&[], None, &["local"]));
config
.resources
.insert("c".to_string(), res(&[], None, &["other"]));
config
.resources
.insert("d".to_string(), res(&["b"], Some("web"), &["local"]));
config
}
fn chain() -> types::ForjarConfig {
let mut config = types::ForjarConfig {
version: "1.0".to_string(),
name: "chain".to_string(),
..Default::default()
};
config.machines.insert("local".to_string(), machine());
config
.resources
.insert("a".to_string(), res(&["b"], None, &["local"]));
config
.resources
.insert("b".to_string(), res(&["c"], None, &["local"]));
config
.resources
.insert("c".to_string(), res(&[], None, &["local"]));
config
}
fn ids(config: &types::ForjarConfig) -> Vec<&str> {
config.resources.keys().map(String::as_str).collect()
}
fn deps<'a>(config: &'a types::ForjarConfig, id: &str) -> &'a [String] {
&config.resources[id].depends_on
}
fn resolve(config: &mut types::ForjarConfig, sel: Selectors<'_>) -> Result<Selection, String> {
resolve_selection(config, &sel, false)
}
#[test]
fn subset_pulls_the_dependency_closure_in() {
let mut c = cfg();
let out = resolve(
&mut c,
Selectors {
subset: Some("a"),
..Default::default()
},
)
.expect("a declared dependency is never 'unknown'");
assert_eq!(ids(&c), vec!["a", "b"], "config order is preserved");
assert_eq!(out.selected, 1);
assert_eq!(out.dependencies_added, 1);
assert_eq!(out.total, 4);
assert!(out.removed.is_empty());
assert!(out.cut_edges.is_empty());
}
#[test]
fn resource_selector_pulls_the_dependency_closure_in() {
let mut c = cfg();
let out = resolve(
&mut c,
Selectors {
resource: Some("a"),
..Default::default()
},
)
.expect("-r resolves");
assert_eq!(ids(&c), vec!["a", "b"]);
assert_eq!(out.selected, 1);
assert_eq!(out.dependencies_added, 1);
}
#[test]
fn group_selector_pulls_the_dependency_closure_in() {
let mut c = cfg();
let out = resolve(
&mut c,
Selectors {
group: Some("web"),
..Default::default()
},
)
.expect("-g resolves");
assert_eq!(ids(&c), vec!["a", "b", "d"]);
assert_eq!(out.selected, 2, "a and d are in the group");
assert_eq!(out.dependencies_added, 1, "b is their shared prerequisite");
}
#[test]
fn resource_filter_is_a_positive_selector_like_subset() {
let mut c = cfg();
let out = resolve(
&mut c,
Selectors {
resource_filter: Some("a"),
..Default::default()
},
)
.expect("--resource-filter resolves");
assert_eq!(ids(&c), vec!["a", "b"]);
assert_eq!(out.dependencies_added, 1);
}
#[test]
fn no_selector_is_the_identity() {
let mut c = cfg();
let before = c.clone();
let out = resolve(&mut c, Selectors::default()).expect("an unscoped apply resolves");
assert_eq!(ids(&c), ids(&before), "same keys, same order");
for id in ids(&before) {
assert_eq!(deps(&c, id), deps(&before, id), "{id} depends_on is intact");
}
assert_eq!(
out,
Selection {
total: 4,
selected: 4,
dependencies_added: 0,
removed: Vec::new(),
cut_edges: Vec::new(),
}
);
}
#[test]
fn exclude_contracts_the_edge_it_cuts() {
let mut c = cfg();
let out = resolve(
&mut c,
Selectors {
subset: Some("a"),
exclude: Some("b"),
..Default::default()
},
)
.expect("an explicit exclusion is a decision, not an error");
assert_eq!(ids(&c), vec!["a"]);
assert!(deps(&c, "a").is_empty(), "the cut edge was contracted away");
assert_eq!(out.removed, vec!["b".to_string()]);
assert_eq!(out.cut_edges, vec![("a".to_string(), "b".to_string())]);
}
#[test]
fn skip_contracts_transitively_so_ordering_survives() {
let mut c = chain();
let out = resolve(
&mut c,
Selectors {
skip: Some("b"),
..Default::default()
},
)
.expect("--skip resolves");
assert_eq!(ids(&c), vec!["a", "c"]);
assert_eq!(deps(&c, "a"), ["c".to_string()]);
assert_eq!(out.cut_edges, vec![("a".to_string(), "b".to_string())]);
assert_eq!(out.removed, vec!["b".to_string()]);
}
#[test]
fn only_machine_narrows_and_contracts() {
let mut c = cfg();
let out = resolve(
&mut c,
Selectors {
only_machine: Some("other"),
..Default::default()
},
)
.expect("--only-machine resolves");
assert_eq!(ids(&c), vec!["c"]);
assert_eq!(
c.resources["c"].machine,
types::MachineTarget::Single("other".to_string()),
"the survivor targets only the requested machine"
);
assert_eq!(
out.removed,
vec!["a".to_string(), "b".to_string(), "d".to_string()]
);
}
#[test]
fn an_undeclared_dependency_is_reported_before_any_pruning() {
let mut c = cfg();
c.resources["a"].depends_on = vec!["ghost".to_string()];
let err = resolve(
&mut c,
Selectors {
subset: Some("a"),
..Default::default()
},
)
.expect_err("a dependency the file does not declare is still an error");
assert!(err.contains("depends on unknown 'ghost'"), "{err}");
assert_eq!(ids(&c), vec!["a", "b", "c", "d"], "validation runs first");
}
#[test]
fn a_subset_matching_nothing_is_an_error() {
let mut c = cfg();
let err = resolve(
&mut c,
Selectors {
subset: Some("zz-*"),
..Default::default()
},
)
.expect_err("a selector matching nothing is a typo, not a request for nothing");
assert_eq!(err, "no resources match subset pattern 'zz-*'");
}
#[test]
fn a_resource_filter_matching_nothing_keeps_its_flag_prefix() {
let mut c = cfg();
let err = resolve(
&mut c,
Selectors {
resource_filter: Some("zz-*"),
..Default::default()
},
)
.expect_err("--resource-filter matching nothing is an error");
assert_eq!(
err,
"--resource-filter: no resources match subset pattern 'zz-*'"
);
}
#[test]
fn an_unknown_resource_selector_is_an_error() {
let mut c = cfg();
let err = resolve(
&mut c,
Selectors {
resource: Some("zz"),
..Default::default()
},
)
.expect_err("typo'd -r must fail");
assert!(
err.starts_with("--resource 'zz' matches no resource"),
"{err}"
);
assert!(
err.contains("a, b, c, d"),
"the error names what exists: {err}"
);
}
#[test]
fn an_unknown_skip_is_an_error() {
let mut c = cfg();
let err = resolve(
&mut c,
Selectors {
skip: Some("zz"),
..Default::default()
},
)
.expect_err("typo'd --skip must fail");
assert!(err.starts_with("--skip 'zz' matches no resource"), "{err}");
}
#[test]
fn an_unknown_machine_is_an_error() {
let mut c = cfg();
let err = resolve(
&mut c,
Selectors {
only_machine: Some("ghost"),
..Default::default()
},
)
.expect_err("typo'd --only-machine must fail");
assert!(err.contains("--only-machine 'ghost'"), "{err}");
}
#[test]
fn an_unknown_tag_is_an_error_and_a_known_tag_prunes_nothing() {
let mut c = cfg();
let err = resolve(
&mut c,
Selectors {
tag: Some("nope"),
..Default::default()
},
)
.expect_err("typo'd -t must fail");
assert!(err.contains("--tag 'nope'"), "{err}");
let mut c = cfg();
c.resources["a"].tags = vec!["live".to_string()];
let out = resolve(
&mut c,
Selectors {
tag: Some("live"),
..Default::default()
},
)
.expect("-t exists");
assert_eq!(ids(&c), vec!["a", "b", "c", "d"]);
assert_eq!(out.selected, 4);
}
#[test]
fn intersecting_positive_selectors_that_match_nothing_name_the_selectors() {
let mut c = cfg();
let err = resolve(
&mut c,
Selectors {
resource: Some("a"),
subset: Some("c"),
..Default::default()
},
)
.expect_err("an empty intersection is an error, not an empty success");
assert!(err.contains("--resource 'a'"), "{err}");
assert!(err.contains("--subset 'c'"), "{err}");
}
#[test]
fn goals_select_their_own_prerequisite_closure() {
let mut c = cfg();
let goals = vec!["a".to_string()];
let out = resolve(
&mut c,
Selectors {
goals: &goals,
..Default::default()
},
)
.expect("a known goal resolves");
assert_eq!(ids(&c), vec!["a", "b"]);
assert_eq!(out.dependencies_added, 1);
}
#[test]
fn an_unknown_goal_keeps_makes_message() {
let mut c = cfg();
let goals = vec!["nope".to_string()];
let err = resolve(
&mut c,
Selectors {
goals: &goals,
..Default::default()
},
)
.expect_err("an unknown goal is an error");
assert!(err.starts_with("no rule to make target 'nope'"), "{err}");
}
#[test]
fn a_negative_that_removes_every_selected_resource_is_an_error() {
let mut c = cfg();
let err = resolve(
&mut c,
Selectors {
exclude: Some("*"),
..Default::default()
},
)
.unwrap_err();
assert!(err.contains("no resources remain"), "{err}");
assert!(err.contains("--exclude '*'"), "{err}");
}
#[test]
fn skipping_the_selected_resource_keeps_its_closure() {
let mut c = cfg();
let s = resolve(
&mut c,
Selectors {
resource: Some("a"),
skip: Some("a"),
..Default::default()
},
)
.expect("the dependency is still a valid selection");
assert_eq!(ids(&c), ["b"]);
assert_eq!(s.removed, ["a"]);
}
#[test]
fn stripping_an_unrequested_phony_contracts_through_it() {
let mut c = chain();
let machines: Vec<String> = c.machines.keys().cloned().collect();
let names: Vec<&str> = machines.iter().map(String::as_str).collect();
let mut phony = res(&["b"], None, &names);
phony.phony = true;
c.resources.insert("p".to_string(), phony);
c.resources.get_mut("a").expect("a").depends_on = vec!["p".to_string()];
super::apply_selection::strip_unrequested_phony(&mut c, &["a".to_string()]);
assert!(!c.resources.contains_key("p"));
assert_eq!(
deps(&c, "a"),
["b"],
"a stays ordered after b through the phony"
);
}