use super::*;
use minijinja::Environment;
use proptest::prelude::*;
proptest! {
#[test]
fn foreach_expands_generated_sequence_values(values in proptest::collection::vec(-100_i16..100, 1..8)) {
let env = Environment::new();
let yaml = format!(
"targets:\n - bare-string\n - name: generated\n foreach: {values:?}\n command: echo {{{{ item }}}}"
);
let mut doc: ManifestValue = serde_saphyr::from_str(&yaml)
.map_err(|error| TestCaseError::fail(error.to_string()))?;
expand_foreach(&mut doc, &env)
.map_err(|error| TestCaseError::fail(error.to_string()))?;
let expanded = targets(&doc).map_err(|error| TestCaseError::fail(error.to_string()))?;
prop_assert_eq!(expanded.len(), values.len() + 1);
prop_assert_eq!(expanded.first().and_then(ManifestValue::as_str), Some("bare-string"));
for (index, (entry, value)) in expanded.iter().skip(1).zip(&values).enumerate() {
let map = entry.as_object().ok_or_else(|| {
TestCaseError::fail(format!("expanded entry {index} should be an object: {entry:?}"))
})?;
prop_assert!(!map.contains_key("foreach"));
prop_assert_eq!(map.get("name").and_then(ManifestValue::as_str), Some("generated"));
let vars = map.get("vars").and_then(ManifestValue::as_object).ok_or_else(|| {
TestCaseError::fail(format!("expanded entry {index} should contain vars: {map:?}"))
})?;
prop_assert_eq!(vars.get("item").and_then(ManifestValue::as_i64), Some(i64::from(*value)));
prop_assert_eq!(vars.get("index").and_then(ManifestValue::as_u64), Some(index as u64));
}
}
#[test]
fn foreach_generated_iteration_values_override_entry_vars(
values in proptest::collection::vec("[a-z]{1,6}", 1..8),
entry_item in "[a-z]{1,6}",
other_value in "[a-z]{1,6}",
) {
let env = Environment::new();
let foreach = serde_json::to_string(&values)
.map_err(|error| TestCaseError::fail(error.to_string()))?;
let entry_item_json = serde_json::to_string(&entry_item)
.map_err(|error| TestCaseError::fail(error.to_string()))?;
let other = serde_json::to_string(&other_value)
.map_err(|error| TestCaseError::fail(error.to_string()))?;
let yaml = format!(
"targets:\n - name: variables\n foreach: {foreach}\n vars:\n item: {entry_item_json}\n other: {other}"
);
let mut doc: ManifestValue = serde_saphyr::from_str(&yaml)
.map_err(|error| TestCaseError::fail(error.to_string()))?;
expand_foreach(&mut doc, &env)
.map_err(|error| TestCaseError::fail(error.to_string()))?;
let expanded = targets(&doc).map_err(|error| TestCaseError::fail(error.to_string()))?;
prop_assert_eq!(expanded.len(), values.len());
for (index, (entry, value)) in expanded.iter().zip(&values).enumerate() {
let map = entry.as_object().ok_or_else(|| {
TestCaseError::fail(format!("expanded entry {index} should be an object: {entry:?}"))
})?;
let vars = map.get("vars").and_then(ManifestValue::as_object).ok_or_else(|| {
TestCaseError::fail(format!("expanded entry {index} should contain vars: {map:?}"))
})?;
prop_assert_eq!(vars.get("item").and_then(ManifestValue::as_str), Some(value.as_str()));
prop_assert_eq!(vars.get("other").and_then(ManifestValue::as_str), Some(other_value.as_str()));
prop_assert_eq!(vars.get("index").and_then(ManifestValue::as_u64), Some(index as u64));
}
}
#[test]
fn foreach_filters_generated_sequence_values(
values in proptest::collection::vec(-20_i16..21, 0..8),
threshold in -20_i16..21,
) {
let env = Environment::new();
let yaml = format!(
"targets:\n - name: filtered\n foreach: {values:?}\n when: 'item > {threshold}'"
);
let mut doc: ManifestValue = serde_saphyr::from_str(&yaml)
.map_err(|error| TestCaseError::fail(error.to_string()))?;
expand_foreach(&mut doc, &env)
.map_err(|error| TestCaseError::fail(error.to_string()))?;
let expanded = targets(&doc).map_err(|error| TestCaseError::fail(error.to_string()))?;
let expected: Vec<_> = values
.iter()
.enumerate()
.filter(|(_, value)| **value > threshold)
.collect();
prop_assert_eq!(expanded.len(), expected.len());
for (entry, (index, value)) in expanded.iter().zip(expected) {
let map = entry.as_object().ok_or_else(|| {
TestCaseError::fail(format!("filtered entry should be an object: {entry:?}"))
})?;
prop_assert!(!map.contains_key("foreach"));
let vars = map.get("vars").and_then(ManifestValue::as_object).ok_or_else(|| {
TestCaseError::fail(format!("filtered entry should contain vars: {map:?}"))
})?;
prop_assert_eq!(vars.get("item").and_then(ManifestValue::as_i64), Some(i64::from(*value)));
prop_assert_eq!(vars.get("index").and_then(ManifestValue::as_u64), Some(index as u64));
}
}
#[test]
fn foreach_preserves_generated_source_key_order(order in prop_oneof![
Just(vec!["name", "vars", "after"]),
Just(vec!["name", "after", "vars"]),
Just(vec!["vars", "name", "after"]),
Just(vec!["vars", "after", "name"]),
Just(vec!["after", "name", "vars"]),
Just(vec!["after", "vars", "name"]),
]) {
let env = Environment::new();
let mut yaml = String::from("targets:\n");
for (index, key) in order.iter().enumerate() {
yaml.push_str(if index == 0 { " - " } else { " " });
match *key {
"name" => yaml.push_str("name: ordered\n"),
"vars" => yaml.push_str("vars:\n static: keep\n"),
"after" => yaml.push_str("after: done\n"),
_ => return Err(TestCaseError::fail("property strategy produced an unknown key".to_owned())),
}
}
yaml.push_str(" foreach: [value]");
let mut doc: ManifestValue = serde_saphyr::from_str(&yaml)
.map_err(|error| TestCaseError::fail(error.to_string()))?;
expand_foreach(&mut doc, &env)
.map_err(|error| TestCaseError::fail(error.to_string()))?;
let expanded = targets(&doc).map_err(|error| TestCaseError::fail(error.to_string()))?;
prop_assert_eq!(expanded.len(), 1);
let entry = expanded.first().ok_or_else(|| {
TestCaseError::fail("expected one expanded target after length check".to_owned())
})?;
let map = entry.as_object().ok_or_else(|| {
TestCaseError::fail(format!("expanded target should be an object: {entry:?}"))
})?;
let keys: Vec<_> = map.keys().map(String::as_str).collect();
prop_assert_eq!(keys, order);
}
}