use super::*;
use minijinja::Environment;
use proptest::prelude::*;
use serde_json::json;
fn foreach_doc(section: &str, items: &[String], when: Option<&str>) -> ManifestValue {
let mut entry = json!({
"name": "literal",
"description": "Build {{ item }}",
"command": "echo hi",
"foreach": items,
});
if let Some(expr) = when
&& let Some(map) = entry.as_object_mut()
{
map.insert("when".into(), json!(expr));
}
let mut doc = ManifestMap::new();
doc.insert(section.into(), json!([entry]));
ManifestValue::Object(doc)
}
fn item_names(max: usize) -> impl Strategy<Value = Vec<String>> {
proptest::collection::vec("[a-z]{1,8}", 0..=max)
}
fn keep_skip_items(max: usize) -> impl Strategy<Value = Vec<String>> {
proptest::collection::vec(
prop_oneof![Just("keep".to_owned()), Just("skip".to_owned())],
0..=max,
)
}
fn expanded_entries<'a>(
doc: &'a ManifestValue,
section: &str,
) -> Result<&'a [ManifestValue], TestCaseError> {
doc.get(section)
.and_then(|v| v.as_array())
.map(Vec::as_slice)
.ok_or_else(|| TestCaseError::fail(format!("{section} sequence missing")))
}
proptest! {
#[test]
fn expansion_yields_one_entry_per_item(items in item_names(10)) {
let env = Environment::new();
for section in ["targets", "actions"] {
let mut doc = foreach_doc(section, &items, None);
let stats = expand_foreach(&mut doc, &env)
.map_err(|e| TestCaseError::fail(format!("expansion failed: {e}")))?;
prop_assert_eq!(stats.filtered_targets, 0);
prop_assert_eq!(stats.filtered_actions, 0);
let entries = expanded_entries(&doc, section)?;
prop_assert_eq!(entries.len(), items.len());
}
}
#[test]
fn indexes_are_sequential_and_unique(items in item_names(10)) {
let env = Environment::new();
for section in ["targets", "actions"] {
let mut doc = foreach_doc(section, &items, None);
expand_foreach(&mut doc, &env)
.map_err(|e| TestCaseError::fail(format!("expansion failed: {e}")))?;
let entries = expanded_entries(&doc, section)?;
let mut indexes = Vec::new();
for entry in entries {
let index = entry
.get("vars")
.and_then(|v| v.get("index"))
.and_then(ManifestValue::as_u64)
.ok_or_else(|| TestCaseError::fail("missing numeric index var"))?;
indexes.push(index);
}
let expected: Vec<u64> = (0..items.len() as u64).collect();
prop_assert_eq!(indexes, expected);
}
}
#[test]
fn foreach_preserves_description_templates(items in item_names(10)) {
let env = Environment::new();
for section in ["targets", "actions"] {
let mut doc = foreach_doc(section, &items, None);
expand_foreach(&mut doc, &env)
.map_err(|e| TestCaseError::fail(format!("expansion failed: {e}")))?;
let descriptions: Result<Vec<_>, TestCaseError> = expanded_entries(&doc, section)?
.iter()
.map(|entry| {
entry
.get("description")
.and_then(ManifestValue::as_str)
.map(str::to_owned)
.ok_or_else(|| TestCaseError::fail("expanded description missing"))
})
.collect();
prop_assert_eq!(
descriptions?,
vec!["Build {{ item }}".to_owned(); items.len()]
);
}
}
#[test]
fn foreach_key_is_removed_from_all_entries(items in keep_skip_items(10)) {
let env = Environment::new();
for section in ["targets", "actions"] {
let mut doc = foreach_doc(section, &items, Some("item == 'keep'"));
expand_foreach(&mut doc, &env)
.map_err(|e| TestCaseError::fail(format!("expansion failed: {e}")))?;
for entry in expanded_entries(&doc, section)? {
let map = entry
.as_object()
.ok_or_else(|| TestCaseError::fail("expanded entry is not an object"))?;
prop_assert!(!map.contains_key("foreach"));
prop_assert!(!map.contains_key("when"));
}
}
}
#[test]
fn when_filtering_is_deterministic(items in keep_skip_items(10)) {
let env = Environment::new();
for section in ["targets", "actions"] {
let mut first = foreach_doc(section, &items, Some("item != 'skip'"));
let mut second = first.clone();
let first_stats = expand_foreach(&mut first, &env)
.map_err(|e| TestCaseError::fail(format!("first expansion failed: {e}")))?;
let second_stats = expand_foreach(&mut second, &env)
.map_err(|e| TestCaseError::fail(format!("second expansion failed: {e}")))?;
prop_assert_eq!(first_stats, second_stats);
prop_assert_eq!(first, second);
}
}
#[test]
fn whitespace_only_when_is_rejected(
ws in proptest::collection::vec(
prop::sample::select(vec![
'\u{0009}', '\u{000A}', '\u{000B}', '\u{000C}', '\u{000D}', '\u{0020}',
'\u{0085}', '\u{00A0}', '\u{1680}', '\u{2000}', '\u{2001}', '\u{2002}',
'\u{2003}', '\u{2004}', '\u{2005}', '\u{2006}', '\u{2007}', '\u{2008}',
'\u{2009}', '\u{200A}', '\u{2028}', '\u{2029}', '\u{202F}', '\u{205F}',
'\u{3000}',
]),
1..10,
),
items in item_names(3),
) {
let env = Environment::new();
let expr: String = ws.into_iter().collect();
for section in ["targets", "actions"] {
let mut doc = foreach_doc(section, &items, Some(&expr));
let mut plain_section = ManifestMap::new();
plain_section.insert(
section.into(),
json!([{ "name": "literal", "command": "echo hi", "when": expr.as_str() }]),
);
let mut plain = ManifestValue::Object(plain_section);
prop_assert!(expand_foreach(&mut plain, &env).is_err());
if !items.is_empty() {
prop_assert!(expand_foreach(&mut doc, &env).is_err());
}
}
}
}