1use crate::consts::*;
2use crate::resolver::MapEntry;
3use crate::resolver::ResolvedValue;
4use std::collections::{HashMap, HashSet};
5
6pub fn resolved_value_at_path(val: &ResolvedValue, path: &str) -> Option<ResolvedValue> {
7 let mut segments = path.splitn(2, '.');
8 let key = segments.next()?;
9 let remaining = segments.next();
10
11 let child = match val {
12 ResolvedValue::Concrete { value: json_val } => {
13 if path.contains("{}") {
14 let results = json_values_matching_wildcard_path(json_val, path);
15 return if results.is_empty() {
16 None
17 } else {
18 Some(ResolvedValue::Enum {
19 variants: results
20 .into_iter()
21 .map(|v| ResolvedValue::Concrete { value: v.clone().into() })
22 .collect(),
23 })
24 };
25 }
26 return json_value_at_path(json_val, path).map(|v| ResolvedValue::Concrete { value: v.clone().into() });
27 }
28 ResolvedValue::List { items } => {
29 if key == "{}" {
30 let walked: Vec<ResolvedValue> = items
31 .iter()
32 .filter_map(|item| match remaining {
33 Some(rest) if !rest.is_empty() => resolved_value_at_path(item, rest),
34 _ => Some(item.clone()),
35 })
36 .collect();
37 return if walked.is_empty() { None } else { Some(ResolvedValue::Enum { variants: walked }) };
38 }
39 let idx: usize = key.parse().ok()?;
40 items.get(idx)?.clone()
41 }
42 ResolvedValue::Map { entries } => entries.iter().find(|e| e.key == key).map(|e| e.value.clone())?,
43 ResolvedValue::Conditional { condition: cond, if_true: t, if_false: f } => {
44 let t_walked = resolved_value_at_path(t, path);
45 let f_walked = resolved_value_at_path(f, path);
46 return match (t_walked, f_walked) {
47 (Some(tw), Some(fw)) => Some(ResolvedValue::Conditional {
48 condition: cond.clone(),
49 if_true: Box::new(tw),
50 if_false: Box::new(fw),
51 }),
52 (Some(tw), None) => Some(ResolvedValue::Conditional {
53 condition: cond.clone(),
54 if_true: Box::new(tw),
55 if_false: Box::new(ResolvedValue::Dynamic { reason: "path not found in false branch".into() }),
56 }),
57 (None, Some(fw)) => Some(ResolvedValue::Conditional {
58 condition: cond.clone(),
59 if_true: Box::new(ResolvedValue::Dynamic { reason: "path not found in true branch".into() }),
60 if_false: Box::new(fw),
61 }),
62 (None, None) => None,
63 };
64 }
65 ResolvedValue::Enum { variants: vals } => {
66 let walked: Vec<ResolvedValue> = vals.iter().filter_map(|v| resolved_value_at_path(v, path)).collect();
67 return if walked.is_empty() { None } else { Some(ResolvedValue::Enum { variants: walked }) };
68 }
69 _ => return None,
70 };
71
72 match remaining {
73 Some(rest) if !rest.is_empty() => resolved_value_at_path(&child, rest),
74 _ => Some(child),
75 }
76}
77
78pub fn effective_path_from_scenario_source_path(source_path: &str) -> Option<String> {
83 if source_path.is_empty() {
84 return Some(String::new());
85 }
86 let segments: Vec<&str> = source_path.split('.').collect();
87 if segments.iter().any(|segment| segment.is_empty()) {
88 return None;
89 }
90 let mut effective_segments = Vec::with_capacity(segments.len());
91 let mut index = 0;
92 while index < segments.len() {
93 if segments[index] == FN_IF {
94 match segments.get(index + 1).copied() {
95 Some("1" | "2") => {
96 index += 2;
97 continue;
98 }
99 _ => return None,
100 }
101 }
102 effective_segments.push(segments[index]);
103 index += 1;
104 }
105 Some(effective_segments.join("."))
106}
107
108pub fn scenario_source_path_at(
109 value: &ResolvedValue,
110 effective_path: &str,
111 conditions: &HashMap<String, bool>,
112 source_path: &str,
113) -> Option<String> {
114 let segments: Vec<&str> = effective_path.split('.').filter(|segment| !segment.is_empty()).collect();
115 scenario_source_path_segments(value, &segments, conditions, source_path)
116}
117
118fn scenario_source_path_segments(
119 value: &ResolvedValue,
120 segments: &[&str],
121 conditions: &HashMap<String, bool>,
122 source_path: &str,
123) -> Option<String> {
124 match value {
125 ResolvedValue::Conditional { condition, if_true, if_false } => {
126 let is_true = *conditions.get(condition)?;
127 let branch_index = if is_true { 1 } else { 2 };
128 let branch = if is_true { if_true } else { if_false };
129 scenario_source_path_segments(
130 branch,
131 segments,
132 conditions,
133 &append_source_path(source_path, &format!("Fn::If.{branch_index}")),
134 )
135 }
136 ResolvedValue::Map { entries } => {
137 let Some((segment, remaining)) = segments.split_first() else {
138 return Some(source_path.to_string());
139 };
140 let entry = entries.iter().find(|entry| entry.key == *segment)?;
141 scenario_source_path_segments(
142 &entry.value,
143 remaining,
144 conditions,
145 &append_source_path(source_path, segment),
146 )
147 }
148 ResolvedValue::List { items } => {
149 let Some((segment, remaining)) = segments.split_first() else {
150 return Some(source_path.to_string());
151 };
152 let index: usize = segment.parse().ok()?;
153 scenario_source_path_segments(
154 items.get(index)?,
155 remaining,
156 conditions,
157 &append_source_path(source_path, segment),
158 )
159 }
160 ResolvedValue::Concrete { value } => json_scenario_source_path(value, segments, source_path),
161 ResolvedValue::Enum { variants } => variants
162 .iter()
163 .find_map(|variant| scenario_source_path_segments(variant, segments, conditions, source_path)),
164 ResolvedValue::Reference { .. } | ResolvedValue::Dynamic { .. } | ResolvedValue::TypedDynamic { .. } => {
165 segments.is_empty().then(|| source_path.to_string())
166 }
167 }
168}
169
170fn json_scenario_source_path(value: &serde_json::Value, segments: &[&str], source_path: &str) -> Option<String> {
171 let Some((segment, remaining)) = segments.split_first() else {
172 return Some(source_path.to_string());
173 };
174 let child = match value {
175 serde_json::Value::Object(map) => map.get(*segment)?,
176 serde_json::Value::Array(items) => items.get(segment.parse::<usize>().ok()?)?,
177 _ => return None,
178 };
179 json_scenario_source_path(child, remaining, &append_source_path(source_path, segment))
180}
181
182fn append_source_path(source_path: &str, segment: &str) -> String {
183 if source_path.is_empty() { segment.to_string() } else { format!("{source_path}.{segment}") }
184}
185
186pub fn collect_condition_refs_from_resolved(val: &ResolvedValue, out: &mut Vec<String>) {
187 match val {
188 ResolvedValue::Conditional { condition: cond, if_true: t, if_false: f } => {
189 out.push(cond.clone());
190 collect_condition_refs_from_resolved(t, out);
191 collect_condition_refs_from_resolved(f, out);
192 }
193 ResolvedValue::List { items } => {
194 for v in items {
195 collect_condition_refs_from_resolved(v, out);
196 }
197 }
198 ResolvedValue::Map { entries } => {
199 for e in entries {
200 collect_condition_refs_from_resolved(&e.value, out);
201 }
202 }
203 ResolvedValue::Enum { variants: vals } => {
204 for v in vals {
205 collect_condition_refs_from_resolved(v, out);
206 }
207 }
208 _ => {}
209 }
210}
211
212pub fn collect_conditional_nulls(val: &ResolvedValue, path: &str, out: &mut Vec<(String, String, bool)>) {
213 match val {
214 ResolvedValue::Conditional { condition: cond, if_true: t, if_false: f } => {
215 let t_null = is_resolved_null(t);
216 let f_null = is_resolved_null(f);
217 if t_null {
218 out.push((path.to_string(), cond.clone(), true));
219 }
220 if f_null {
221 out.push((path.to_string(), cond.clone(), false));
222 }
223 if !t_null {
224 collect_conditional_nulls(t, path, out);
225 }
226 if !f_null {
227 collect_conditional_nulls(f, path, out);
228 }
229 }
230 ResolvedValue::Map { entries } => {
231 for e in entries {
232 collect_conditional_nulls(&e.value, &format!("{}.{}", path, e.key), out);
233 }
234 }
235 ResolvedValue::List { items } => {
236 for (i, v) in items.iter().enumerate() {
237 collect_conditional_nulls(v, &format!("{}.{}", path, i), out);
238 }
239 }
240 ResolvedValue::Enum { variants: vals } => {
241 for v in vals {
242 collect_conditional_nulls(v, path, out);
243 }
244 }
245 _ => {}
246 }
247}
248
249pub fn is_resolved_null(val: &ResolvedValue) -> bool {
250 match val {
251 ResolvedValue::Concrete { value: v } if v.is_null() => true,
252 ResolvedValue::Conditional { if_true: t, if_false: f, .. } => is_resolved_null(t) && is_resolved_null(f),
253 _ => false,
254 }
255}
256
257fn contains_scenario_branching(value: &ResolvedValue) -> bool {
258 match value {
259 ResolvedValue::Conditional { .. } | ResolvedValue::Enum { .. } => true,
260 ResolvedValue::List { items } => items.iter().any(contains_scenario_branching),
261 ResolvedValue::Map { entries } => entries.iter().any(|entry| contains_scenario_branching(&entry.value)),
262 ResolvedValue::Concrete { .. }
263 | ResolvedValue::Reference { .. }
264 | ResolvedValue::Dynamic { .. }
265 | ResolvedValue::TypedDynamic { .. } => false,
266 }
267}
268
269pub fn collect_scenarios(
275 val: &ResolvedValue,
276 assumptions: &HashMap<String, bool>,
277 limit: usize,
278 results: &mut Vec<(ResolvedValue, HashMap<String, bool>)>,
279) -> bool {
280 let mut used_covering = false;
281 collect_scenarios_with_covering(val, assumptions, limit, results, &mut used_covering)
282}
283
284pub fn collect_scenarios_with_covering(
285 val: &ResolvedValue,
286 assumptions: &HashMap<String, bool>,
287 limit: usize,
288 results: &mut Vec<(ResolvedValue, HashMap<String, bool>)>,
289 used_covering: &mut bool,
290) -> bool {
291 collect_scenarios_reachable_with_covering(val, assumptions, limit, results, &|_| true, used_covering)
292}
293
294#[cfg(test)]
295pub(crate) fn collect_scenarios_reachable(
296 val: &ResolvedValue,
297 assumptions: &HashMap<String, bool>,
298 limit: usize,
299 results: &mut Vec<(ResolvedValue, HashMap<String, bool>)>,
300 is_reachable: &dyn Fn(&HashMap<String, bool>) -> bool,
301) -> bool {
302 let mut used_covering = false;
303 collect_scenarios_reachable_with_covering(val, assumptions, limit, results, is_reachable, &mut used_covering)
304}
305
306pub(crate) fn collect_scenarios_reachable_with_covering(
307 val: &ResolvedValue,
308 assumptions: &HashMap<String, bool>,
309 limit: usize,
310 results: &mut Vec<(ResolvedValue, HashMap<String, bool>)>,
311 is_reachable: &dyn Fn(&HashMap<String, bool>) -> bool,
312 used_covering: &mut bool,
313) -> bool {
314 match val {
315 ResolvedValue::Conditional { condition: cond, if_true: t, if_false: f } => {
316 if assumptions.contains_key(cond.as_str()) {
317 if assumptions[cond.as_str()] {
318 collect_scenarios_reachable_with_covering(
319 t,
320 assumptions,
321 limit,
322 results,
323 is_reachable,
324 used_covering,
325 )
326 } else {
327 collect_scenarios_reachable_with_covering(
328 f,
329 assumptions,
330 limit,
331 results,
332 is_reachable,
333 used_covering,
334 )
335 }
336 } else {
337 let mut true_assumptions = assumptions.clone();
338 true_assumptions.insert(cond.clone(), true);
339 let true_curtailed = collect_scenarios_reachable_with_covering(
340 t,
341 &true_assumptions,
342 limit,
343 results,
344 is_reachable,
345 used_covering,
346 );
347 if true_curtailed && results.len() >= limit {
348 return true;
349 }
350 let mut false_assumptions = assumptions.clone();
351 false_assumptions.insert(cond.clone(), false);
352 let false_curtailed = collect_scenarios_reachable_with_covering(
353 f,
354 &false_assumptions,
355 limit,
356 results,
357 is_reachable,
358 used_covering,
359 );
360 true_curtailed || false_curtailed
361 }
362 }
363 ResolvedValue::Enum { variants } => {
364 let mut curtailed = false;
365 for variant in variants {
366 curtailed |= collect_scenarios_reachable_with_covering(
367 variant,
368 assumptions,
369 limit,
370 results,
371 is_reachable,
372 used_covering,
373 );
374 if curtailed && results.len() >= limit {
375 break;
376 }
377 }
378 curtailed
379 }
380 ResolvedValue::List { items } => {
381 let has_branching = items.iter().any(contains_scenario_branching);
382 if has_branching {
383 expand_list_scenarios(items, assumptions, limit, results, is_reachable, used_covering)
384 } else {
385 push_scenario(val, assumptions, limit, results)
386 }
387 }
388 ResolvedValue::Map { entries } => {
389 let has_branching = entries.iter().any(|entry| contains_scenario_branching(&entry.value));
390 if has_branching {
391 expand_map_scenarios(entries, assumptions, limit, results, is_reachable, used_covering)
392 } else {
393 push_scenario(val, assumptions, limit, results)
394 }
395 }
396 _ => push_scenario(val, assumptions, limit, results),
397 }
398}
399
400fn push_scenario(
401 value: &ResolvedValue,
402 assumptions: &HashMap<String, bool>,
403 limit: usize,
404 results: &mut Vec<(ResolvedValue, HashMap<String, bool>)>,
405) -> bool {
406 if results.len() >= limit {
407 true
408 } else {
409 results.push((value.clone(), assumptions.clone()));
410 false
411 }
412}
413
414pub fn collect_scenarios_signaled(
417 value: &ResolvedValue,
418 assumptions: &HashMap<String, bool>,
419 results: &mut Vec<(ResolvedValue, HashMap<String, bool>)>,
420 curtailed: &mut bool,
421) {
422 *curtailed |= collect_scenarios(value, assumptions, MAX_SCENARIO_COMBINATIONS, results);
423}
424
425pub fn contains_dynamic_resolved(rv: &ResolvedValue) -> bool {
426 match rv {
427 ResolvedValue::Dynamic { reason: _ } | ResolvedValue::TypedDynamic { .. } | ResolvedValue::Reference { .. } => {
428 true
429 }
430 ResolvedValue::List { items } => items.iter().any(contains_dynamic_resolved),
431 ResolvedValue::Map { entries } => entries.iter().any(|e| contains_dynamic_resolved(&e.value)),
432 ResolvedValue::Enum { variants: vals } => vals.iter().any(contains_dynamic_resolved),
433 ResolvedValue::Conditional { if_true: t, if_false: f, .. } => {
434 contains_dynamic_resolved(t) || contains_dynamic_resolved(f)
435 }
436 ResolvedValue::Concrete { value: v } => json_contains_markers(v),
437 }
438}
439
440pub fn json_contains_markers(v: &serde_json::Value) -> bool {
441 match v {
442 serde_json::Value::Object(m) => {
443 m.contains_key(MARKER_DYNAMIC)
444 || m.contains_key(MARKER_REF)
445 || m.contains_key(MARKER_INTRINSIC)
446 || m.contains_key(MARKER_CONDITIONAL)
447 || m.contains_key(MARKER_ENUM)
448 || m.values().any(json_contains_markers)
449 }
450 serde_json::Value::Array(a) => a.iter().any(json_contains_markers),
451 _ => false,
452 }
453}
454
455pub fn estimate_resolved_string_length_bounds(val: &ResolvedValue) -> Option<(usize, usize)> {
463 match val {
464 ResolvedValue::Concrete { value: v } if v.is_string() => {
465 let length = v.as_str()?.chars().count();
466 Some((length, length))
467 }
468 ResolvedValue::Dynamic { reason: desc } if desc.starts_with(SUB_PARTIAL_PREFIX) => {
476 partial_length_bounds(&desc[SUB_PARTIAL_PREFIX.len()..])
477 }
478 ResolvedValue::Dynamic { reason: desc } if desc.starts_with(JOIN_PARTIAL_PREFIX) => {
479 partial_length_bounds(&desc[JOIN_PARTIAL_PREFIX.len()..])
480 }
481 ResolvedValue::Dynamic { reason: _ } => None,
482 ResolvedValue::Conditional { if_true: t, if_false: f, .. } => {
483 let (true_shortest, true_longest) = estimate_resolved_string_length_bounds(t)?;
484 let (false_shortest, false_longest) = estimate_resolved_string_length_bounds(f)?;
485 Some((true_shortest.min(false_shortest), true_longest.max(false_longest)))
486 }
487 ResolvedValue::Enum { variants } => {
488 let mut bounds: Option<(usize, usize)> = None;
489 for variant in variants {
490 let (shortest, longest) = estimate_resolved_string_length_bounds(variant)?;
491 bounds = Some(match bounds {
492 Some((known_shortest, known_longest)) => (known_shortest.min(shortest), known_longest.max(longest)),
493 None => (shortest, longest),
494 });
495 }
496 bounds
497 }
498 _ => None,
499 }
500}
501
502fn partial_length_bounds(partial: &str) -> Option<(usize, usize)> {
503 if has_interpolation_variable(partial)
504 || partial.contains(UNRESOLVED_REF_PLACEHOLDER_PREFIX)
505 || partial.contains(UNRESOLVED_DYNAMIC_PLACEHOLDER)
506 {
507 None
508 } else {
509 let length = partial.chars().count();
510 Some((length, length))
511 }
512}
513
514fn has_interpolation_variable(template: &str) -> bool {
515 let bytes = template.as_bytes();
516 let mut i = 0;
517 while i + 1 < bytes.len() {
518 if bytes[i] == b'$'
519 && bytes[i + 1] == b'{'
520 && let Some(_end) = template[i + 2..].find('}')
521 {
522 return true;
523 }
524 i += 1;
525 }
526 false
527}
528
529fn json_value_at_path<'a>(val: &'a serde_json::Value, path: &str) -> Option<&'a serde_json::Value> {
530 let mut current = val;
531 for segment in path.split('.') {
532 match current {
533 serde_json::Value::Object(map) => {
534 current = map.get(segment)?;
535 }
536 serde_json::Value::Array(arr) => {
537 if segment == "{}" {
538 return None;
539 }
540 let idx: usize = segment.parse().ok()?;
541 current = arr.get(idx)?;
542 }
543 _ => return None,
544 }
545 }
546 Some(current)
547}
548
549fn json_values_matching_wildcard_path(val: &serde_json::Value, path: &str) -> Vec<serde_json::Value> {
550 let mut segments: Vec<&str> = path.split('.').collect();
551 if segments.is_empty() {
552 return vec![val.clone()];
553 }
554 let key = segments.remove(0);
555 let remaining = if segments.is_empty() { String::new() } else { segments.join(".") };
556 match val {
557 serde_json::Value::Object(map) => {
558 if let Some(child) = map.get(key) {
559 if remaining.is_empty() {
560 vec![child.clone()]
561 } else {
562 json_values_matching_wildcard_path(child, &remaining)
563 }
564 } else {
565 vec![]
566 }
567 }
568 serde_json::Value::Array(arr) => {
569 if key == "{}" {
570 arr.iter()
571 .flat_map(|item| {
572 if remaining.is_empty() {
573 vec![item.clone()]
574 } else {
575 json_values_matching_wildcard_path(item, &remaining)
576 }
577 })
578 .collect()
579 } else if let Ok(idx) = key.parse::<usize>() {
580 arr.get(idx)
581 .map(|child| {
582 if remaining.is_empty() {
583 vec![child.clone()]
584 } else {
585 json_values_matching_wildcard_path(child, &remaining)
586 }
587 })
588 .unwrap_or_default()
589 } else {
590 vec![]
591 }
592 }
593 _ => vec![],
594 }
595}
596
597fn expand_cartesian_scenarios<T: Clone>(
611 items: &[(T, Vec<(ResolvedValue, HashMap<String, bool>)>)],
612 base_assumptions: &HashMap<String, bool>,
613 limit: usize,
614 build_result: impl Fn(Vec<(T, ResolvedValue)>) -> ResolvedValue,
615 results: &mut Vec<(ResolvedValue, HashMap<String, bool>)>,
616 is_reachable: &dyn Fn(&HashMap<String, bool>) -> bool,
617 used_covering: &mut bool,
618) -> bool {
619 if limit == 0 {
620 return !items.is_empty();
621 }
622 if !product_is_enumerable(items) {
623 *used_covering = true;
624 return cover_cartesian_scenarios(items, base_assumptions, limit, build_result, results, is_reachable);
625 }
626
627 let mut combos: Vec<(Vec<(T, ResolvedValue)>, HashMap<String, bool>)> =
628 vec![(Vec::new(), base_assumptions.clone())];
629 let mut curtailed = false;
630 for (key, item_scenarios) in items {
631 let mut new_combos = Vec::new();
632 'partials: for (partial, partial_assumptions) in &combos {
633 for (value, value_assumptions) in item_scenarios {
634 let mut merged = partial_assumptions.clone();
635 let mut conflict = false;
636 for (condition, expected) in value_assumptions {
637 if let Some(existing) = merged.get(condition) {
638 if existing != expected {
639 conflict = true;
640 break;
641 }
642 } else {
643 merged.insert(condition.clone(), *expected);
644 }
645 }
646 if conflict {
647 continue;
648 }
649 if new_combos.len() == limit {
650 curtailed = true;
651 break 'partials;
652 }
653 let mut collected = partial.clone();
654 collected.push((key.clone(), value.clone()));
655 new_combos.push((collected, merged));
656 }
657 }
658 combos = new_combos;
659 }
660
661 results.extend(combos.into_iter().map(|(collected, assumptions)| (build_result(collected), assumptions)));
662 curtailed
663}
664
665fn product_is_enumerable<T>(items: &[(T, Vec<(ResolvedValue, HashMap<String, bool>)>)]) -> bool {
668 let mut combinations = 1usize;
669 let mut nodes_per_combination = 0usize;
670 for (_, scenarios) in items {
671 combinations = combinations.saturating_mul(scenarios.len().max(1));
672 nodes_per_combination += scenarios.iter().map(|(value, _)| node_count(value)).max().unwrap_or(0);
673 }
674 combinations <= MAX_COMPLETE_SCENARIO_PRODUCT
675 && combinations.saturating_mul(nodes_per_combination.max(1)) <= MAX_SCENARIO_PRODUCT_NODES
676}
677
678pub fn node_count(value: &ResolvedValue) -> usize {
681 match value {
682 ResolvedValue::List { items } => 1 + items.iter().map(node_count).sum::<usize>(),
683 ResolvedValue::Map { entries } => 1 + entries.iter().map(|entry| node_count(&entry.value)).sum::<usize>(),
684 ResolvedValue::Enum { variants } => 1 + variants.iter().map(node_count).sum::<usize>(),
685 ResolvedValue::Conditional { if_true, if_false, .. } => 1 + node_count(if_true) + node_count(if_false),
686 ResolvedValue::Concrete { .. }
687 | ResolvedValue::Reference { .. }
688 | ResolvedValue::Dynamic { .. }
689 | ResolvedValue::TypedDynamic { .. } => 1,
690 }
691}
692
693fn merge_scenario_assumptions(
694 assumptions: &HashMap<String, bool>,
695 additions: &HashMap<String, bool>,
696) -> Option<HashMap<String, bool>> {
697 let mut merged = assumptions.clone();
698 for (condition, value) in additions {
699 if merged.get(condition).is_some_and(|existing| existing != value) {
700 return None;
701 }
702 merged.insert(condition.clone(), *value);
703 }
704 Some(merged)
705}
706
707fn cover_cartesian_scenarios<T: Clone>(
718 items: &[(T, Vec<(ResolvedValue, HashMap<String, bool>)>)],
719 base_assumptions: &HashMap<String, bool>,
720 limit: usize,
721 build_result: impl Fn(Vec<(T, ResolvedValue)>) -> ResolvedValue,
722 results: &mut Vec<(ResolvedValue, HashMap<String, bool>)>,
723 is_reachable: &dyn Fn(&HashMap<String, bool>) -> bool,
724) -> bool {
725 let mut conditions: Vec<&str> = items
726 .iter()
727 .flat_map(|(_, scenarios)| scenarios.iter().flat_map(|(_, assumptions)| assumptions.keys()))
728 .filter(|condition| !base_assumptions.contains_key(condition.as_str()))
729 .map(String::as_str)
730 .collect();
731 conditions.sort_unstable();
732 conditions.dedup();
733
734 let assignment = |decide: &dyn Fn(&str) -> bool| -> HashMap<String, bool> {
735 let mut assumptions = base_assumptions.clone();
736 assumptions.extend(conditions.iter().map(|condition| (condition.to_string(), decide(condition))));
737 assumptions
738 };
739
740 let mut seen: HashSet<(Vec<(String, bool)>, Vec<usize>)> = HashSet::new();
741 let mut curtailed = false;
742 let mut add_assignment = |seed: HashMap<String, bool>, forced: Option<(usize, usize)>| -> bool {
743 if !is_reachable(&seed) {
744 return false;
745 }
746 let mut merged = seed;
747 let mut collected = Vec::with_capacity(items.len());
748 let mut selected_indices = Vec::with_capacity(items.len());
749 for (item_index, (key, scenarios)) in items.iter().enumerate() {
750 let selected = if let Some((_, forced_scenario)) = forced.filter(|(item, _)| *item == item_index) {
751 scenarios.get(forced_scenario).and_then(|(value, assumptions)| {
752 merge_scenario_assumptions(&merged, assumptions)
753 .filter(|candidate| is_reachable(candidate))
754 .map(|candidate| (forced_scenario, value, candidate))
755 })
756 } else {
757 scenarios.iter().enumerate().find_map(|(scenario_index, (value, assumptions))| {
758 merge_scenario_assumptions(&merged, assumptions)
759 .filter(|candidate| is_reachable(candidate))
760 .map(|candidate| (scenario_index, value, candidate))
761 })
762 };
763 let Some((scenario_index, value, candidate)) = selected else {
764 curtailed = true;
765 return false;
766 };
767 merged = candidate;
768 collected.push((key.clone(), value.clone()));
769 selected_indices.push(scenario_index);
770 }
771
772 let mut canonical: Vec<(String, bool)> =
773 merged.iter().map(|(condition, value)| (condition.clone(), *value)).collect();
774 canonical.sort_unstable();
775 if !seen.insert((canonical, selected_indices)) {
776 return false;
777 }
778 if results.len() >= limit {
779 curtailed = true;
780 return true;
781 }
782 results.push((build_result(collected), merged));
783 false
784 };
785
786 if add_assignment(assignment(&|_| true), None) || add_assignment(assignment(&|_| false), None) {
787 return true;
788 }
789 for flipped in &conditions {
790 if add_assignment(assignment(&|condition| condition != *flipped), None) {
791 return true;
792 }
793 }
794
795 for (item_index, (_, scenarios)) in items.iter().enumerate() {
796 for (scenario_index, (_, scenario_assumptions)) in scenarios.iter().enumerate() {
797 let Some(seed) = merge_scenario_assumptions(base_assumptions, scenario_assumptions) else {
798 continue;
799 };
800 if add_assignment(seed, Some((item_index, scenario_index))) {
801 return true;
802 }
803 }
804 }
805 curtailed
806}
807
808fn expand_list_scenarios(
809 items: &[ResolvedValue],
810 base_assumptions: &HashMap<String, bool>,
811 limit: usize,
812 results: &mut Vec<(ResolvedValue, HashMap<String, bool>)>,
813 is_reachable: &dyn Fn(&HashMap<String, bool>) -> bool,
814 used_covering: &mut bool,
815) -> bool {
816 let remaining = limit.saturating_sub(results.len());
817 if remaining == 0 {
818 return true;
819 }
820
821 let mut nested_curtailed = false;
822 let mut prepared = Vec::with_capacity(items.len());
823 for (index, item) in items.iter().enumerate() {
824 let mut scenarios = Vec::new();
825 nested_curtailed |= collect_scenarios_reachable_with_covering(
826 item,
827 base_assumptions,
828 remaining,
829 &mut scenarios,
830 is_reachable,
831 used_covering,
832 );
833 if scenarios.is_empty() {
834 scenarios.push((item.clone(), base_assumptions.clone()));
835 }
836 prepared.push((index, scenarios));
837 }
838 let product_curtailed = expand_cartesian_scenarios(
839 &prepared,
840 base_assumptions,
841 remaining,
842 |collected| ResolvedValue::List { items: collected.into_iter().map(|(_, value)| value).collect() },
843 results,
844 is_reachable,
845 used_covering,
846 );
847 nested_curtailed || product_curtailed
848}
849
850fn expand_map_scenarios(
851 entries: &[MapEntry],
852 base_assumptions: &HashMap<String, bool>,
853 limit: usize,
854 results: &mut Vec<(ResolvedValue, HashMap<String, bool>)>,
855 is_reachable: &dyn Fn(&HashMap<String, bool>) -> bool,
856 used_covering: &mut bool,
857) -> bool {
858 let remaining = limit.saturating_sub(results.len());
859 if remaining == 0 {
860 return true;
861 }
862
863 let mut nested_curtailed = false;
864 let mut prepared = Vec::with_capacity(entries.len());
865 for entry in entries {
866 let mut scenarios = Vec::new();
867 nested_curtailed |= collect_scenarios_reachable_with_covering(
868 &entry.value,
869 base_assumptions,
870 remaining,
871 &mut scenarios,
872 is_reachable,
873 used_covering,
874 );
875 if scenarios.is_empty() {
876 scenarios.push((entry.value.clone(), base_assumptions.clone()));
877 }
878 prepared.push((entry.key.clone(), scenarios));
879 }
880 let product_curtailed = expand_cartesian_scenarios(
881 &prepared,
882 base_assumptions,
883 remaining,
884 |collected| ResolvedValue::Map {
885 entries: collected.into_iter().map(|(key, value)| MapEntry { key, value }).collect(),
886 },
887 results,
888 is_reachable,
889 used_covering,
890 );
891 nested_curtailed || product_curtailed
892}
893
894#[cfg(test)]
895mod tests {
896 use super::*;
897 use crate::resolver::{MapEntry, RefKind, ResolvedValue};
898 use serde_json::json;
899
900 #[test]
901 fn path_into_concrete_object() {
902 let val = ResolvedValue::Concrete { value: json!({"a": {"b": "found"}}).into() };
903 match resolved_value_at_path(&val, "a.b") {
904 Some(ResolvedValue::Concrete { value: v }) => assert_eq!(v.0, json!("found")),
905 other => panic!("expected Concrete(\"found\"), got {:?}", other),
906 }
907 }
908
909 #[test]
910 fn path_into_concrete_array() {
911 let val = ResolvedValue::Concrete { value: json!({"items": ["x", "y", "z"]}).into() };
912 match resolved_value_at_path(&val, "items.1") {
913 Some(ResolvedValue::Concrete { value: v }) => assert_eq!(v.0, json!("y")),
914 other => panic!("expected Concrete(\"y\"), got {:?}", other),
915 }
916 }
917
918 #[test]
919 fn path_missing_key_returns_none() {
920 let val = ResolvedValue::Concrete { value: json!({"a": 1}).into() };
921 assert!(resolved_value_at_path(&val, "b").is_none(), "missing key should return None");
922 }
923
924 #[test]
925 fn path_into_resolved_map() {
926 let val = ResolvedValue::Map {
927 entries: vec![MapEntry {
928 key: "key".into(),
929 value: ResolvedValue::Concrete { value: json!("val").into() },
930 }],
931 };
932 match resolved_value_at_path(&val, "key") {
933 Some(ResolvedValue::Concrete { value: v }) => assert_eq!(v.0, json!("val")),
934 other => panic!("expected Concrete, got {:?}", other),
935 }
936 }
937
938 #[test]
939 fn path_into_resolved_list_by_index() {
940 let val = ResolvedValue::List {
941 items: vec![
942 ResolvedValue::Concrete { value: json!("a").into() },
943 ResolvedValue::Concrete { value: json!("b").into() },
944 ],
945 };
946 match resolved_value_at_path(&val, "1") {
947 Some(ResolvedValue::Concrete { value: v }) => assert_eq!(v.0, json!("b")),
948 other => panic!("expected Concrete(\"b\"), got {:?}", other),
949 }
950 }
951
952 #[test]
953 fn path_through_conditional_both_branches() {
954 let val = ResolvedValue::Conditional {
955 condition: "C".into(),
956 if_true: Box::new(ResolvedValue::Concrete { value: json!({"x": "true_val"}).into() }),
957 if_false: Box::new(ResolvedValue::Concrete { value: json!({"x": "false_val"}).into() }),
958 };
959 match resolved_value_at_path(&val, "x") {
960 Some(ResolvedValue::Conditional { condition: c, if_true: t, if_false: f }) => {
961 assert_eq!(c, "C");
962 assert!(matches!(t.as_ref(), ResolvedValue::Concrete { value: v } if v.0 == json!("true_val")));
963 assert!(matches!(f.as_ref(), ResolvedValue::Concrete { value: v } if v.0 == json!("false_val")));
964 }
965 other => panic!("expected Conditional, got {:?}", other),
966 }
967 }
968
969 #[test]
970 fn path_through_conditional_one_branch_missing() {
971 let val = ResolvedValue::Conditional {
972 condition: "C".into(),
973 if_true: Box::new(ResolvedValue::Concrete { value: json!({"x": 1}).into() }),
974 if_false: Box::new(ResolvedValue::Concrete { value: json!({"y": 2}).into() }),
975 };
976 match resolved_value_at_path(&val, "x") {
977 Some(ResolvedValue::Conditional { if_true: t, if_false: f, .. }) => {
978 assert!(matches!(t.as_ref(), ResolvedValue::Concrete { value: v } if v.0 == json!(1)));
979 assert!(matches!(f.as_ref(), ResolvedValue::Dynamic { reason: _ }));
980 }
981 other => panic!("expected Conditional with Dynamic false branch, got {:?}", other),
982 }
983 }
984
985 #[test]
986 fn path_through_enum_collects_variants() {
987 let val = ResolvedValue::Enum {
988 variants: vec![
989 ResolvedValue::Concrete { value: json!({"k": "v1"}).into() },
990 ResolvedValue::Concrete { value: json!({"k": "v2"}).into() },
991 ],
992 };
993 match resolved_value_at_path(&val, "k") {
994 Some(ResolvedValue::Enum { variants: vals }) => assert_eq!(vals.len(), 2),
995 other => panic!("expected Enum, got {:?}", other),
996 }
997 }
998
999 #[test]
1000 fn wildcard_path_on_concrete_array() {
1001 let val = ResolvedValue::Concrete {
1002 value: json!({
1003 "items": [{"name": "a"}, {"name": "b"}]
1004 })
1005 .into(),
1006 };
1007 match resolved_value_at_path(&val, "items.{}.name") {
1008 Some(ResolvedValue::Enum { variants: vals }) => {
1009 assert_eq!(vals.len(), 2);
1010 }
1011 other => panic!("expected Enum from wildcard, got {:?}", other),
1012 }
1013 }
1014
1015 #[test]
1016 fn wildcard_path_on_resolved_list() {
1017 let val = ResolvedValue::List {
1018 items: vec![
1019 ResolvedValue::Map {
1020 entries: vec![MapEntry {
1021 key: "n".into(),
1022 value: ResolvedValue::Concrete { value: json!("x").into() },
1023 }],
1024 },
1025 ResolvedValue::Map {
1026 entries: vec![MapEntry {
1027 key: "n".into(),
1028 value: ResolvedValue::Concrete { value: json!("y").into() },
1029 }],
1030 },
1031 ],
1032 };
1033 match resolved_value_at_path(&val, "{}.n") {
1034 Some(ResolvedValue::Enum { variants: vals }) => assert_eq!(vals.len(), 2),
1035 other => panic!("expected Enum from wildcard on List, got {:?}", other),
1036 }
1037 }
1038
1039 #[test]
1040 fn path_into_dynamic_returns_none() {
1041 let val = ResolvedValue::Dynamic { reason: "unknown".into() };
1042 assert!(resolved_value_at_path(&val, "anything").is_none(), "path into Dynamic should return None");
1043 }
1044
1045 #[test]
1046 fn path_into_reference_returns_none() {
1047 let val = ResolvedValue::Reference { target: "R".into(), kind: RefKind::Ref };
1048 assert!(resolved_value_at_path(&val, "sub").is_none(), "path into Reference should return None");
1049 }
1050
1051 #[test]
1052 fn collect_condition_refs_from_nested() {
1053 let val = ResolvedValue::Conditional {
1054 condition: "C1".into(),
1055 if_true: Box::new(ResolvedValue::List {
1056 items: vec![ResolvedValue::Conditional {
1057 condition: "C2".into(),
1058 if_true: Box::new(ResolvedValue::Concrete { value: json!(1).into() }),
1059 if_false: Box::new(ResolvedValue::Concrete { value: json!(2).into() }),
1060 }],
1061 }),
1062 if_false: Box::new(ResolvedValue::Concrete { value: json!(3).into() }),
1063 };
1064 let mut refs = Vec::new();
1065 collect_condition_refs_from_resolved(&val, &mut refs);
1066 refs.sort();
1067 assert_eq!(refs, vec!["C1", "C2"]);
1068 }
1069
1070 #[test]
1071 fn effective_path_removes_conditional_branch_segments() {
1072 assert_eq!(
1073 effective_path_from_scenario_source_path("Properties.ResourceRecords.Fn::If.1.0"),
1074 Some("Properties.ResourceRecords.0".to_string())
1075 );
1076 }
1077
1078 #[test]
1079 fn effective_path_removes_nested_conditional_branch_segments() {
1080 assert_eq!(
1081 effective_path_from_scenario_source_path("Properties.Fn::If.2.Records.0.Fn::If.1.Value"),
1082 Some("Properties.Records.0.Value".to_string())
1083 );
1084 }
1085
1086 #[test]
1087 fn effective_path_preserves_other_intrinsic_segments() {
1088 assert_eq!(
1089 effective_path_from_scenario_source_path("Properties.Value.Fn::GetAtt.1"),
1090 Some("Properties.Value.Fn::GetAtt.1".to_string())
1091 );
1092 assert_eq!(effective_path_from_scenario_source_path("Properties.Value.Fn::If.0"), None);
1093 assert_eq!(effective_path_from_scenario_source_path("Properties..Value"), None);
1094 }
1095
1096 #[test]
1097 fn scenario_source_path_selects_conditional_list_branch() {
1098 let value = ResolvedValue::Conditional {
1099 condition: "ChooseFirst".into(),
1100 if_true: Box::new(ResolvedValue::Concrete { value: json!(["bad"]).into() }),
1101 if_false: Box::new(ResolvedValue::Concrete { value: json!(["good"]).into() }),
1102 };
1103 let mut conditions = HashMap::new();
1104 conditions.insert("ChooseFirst".to_string(), true);
1105 assert_eq!(
1106 scenario_source_path_at(&value, "0", &conditions, "Properties.ResourceRecords"),
1107 Some("Properties.ResourceRecords.Fn::If.1.0".to_string())
1108 );
1109 conditions.insert("ChooseFirst".to_string(), false);
1110 assert_eq!(
1111 scenario_source_path_at(&value, "0", &conditions, "Properties.ResourceRecords"),
1112 Some("Properties.ResourceRecords.Fn::If.2.0".to_string())
1113 );
1114 }
1115
1116 #[test]
1117 fn scenario_source_path_selects_conditional_list_item_branch() {
1118 let value = ResolvedValue::List {
1119 items: vec![ResolvedValue::Conditional {
1120 condition: "UseValue".into(),
1121 if_true: Box::new(ResolvedValue::Concrete { value: json!("value").into() }),
1122 if_false: Box::new(ResolvedValue::Concrete { value: json!(null).into() }),
1123 }],
1124 };
1125 let conditions = HashMap::from([("UseValue".to_string(), true)]);
1126 assert_eq!(
1127 scenario_source_path_at(&value, "0", &conditions, "Properties.ResourceRecords"),
1128 Some("Properties.ResourceRecords.0.Fn::If.1".to_string())
1129 );
1130 }
1131
1132 #[test]
1133 fn collect_conditional_nulls_detects_null_branch() {
1134 let val = ResolvedValue::Conditional {
1135 condition: "C".into(),
1136 if_true: Box::new(ResolvedValue::Concrete { value: json!(null).into() }),
1137 if_false: Box::new(ResolvedValue::Concrete { value: json!("ok").into() }),
1138 };
1139 let mut nulls = Vec::new();
1140 collect_conditional_nulls(&val, "prop", &mut nulls);
1141 assert_eq!(nulls.len(), 1);
1142 assert_eq!(nulls[0], ("prop".to_string(), "C".to_string(), true));
1143 }
1144
1145 #[test]
1146 fn collect_scenarios_concrete_single() {
1147 let val = ResolvedValue::Concrete { value: json!("hello").into() };
1148 let mut results = Vec::new();
1149 collect_scenarios(&val, &HashMap::new(), MAX_SCENARIO_COMBINATIONS, &mut results);
1150 assert_eq!(results.len(), 1);
1151 assert!(matches!(&results[0].0, ResolvedValue::Concrete { value: v } if v.0 == json!("hello")));
1152 }
1153
1154 #[test]
1155 fn collect_scenarios_conditional_splits() {
1156 let val = ResolvedValue::Conditional {
1157 condition: "C".into(),
1158 if_true: Box::new(ResolvedValue::Concrete { value: json!(1).into() }),
1159 if_false: Box::new(ResolvedValue::Concrete { value: json!(2).into() }),
1160 };
1161 let mut results = Vec::new();
1162 collect_scenarios(&val, &HashMap::new(), MAX_SCENARIO_COMBINATIONS, &mut results);
1163 assert_eq!(results.len(), 2);
1164 let (_, conds_true) = &results[0];
1165 assert_eq!(conds_true.get("C"), Some(&true));
1166 let (_, conds_false) = &results[1];
1167 assert_eq!(conds_false.get("C"), Some(&false));
1168 }
1169
1170 #[test]
1171 fn collect_scenarios_expands_nested_conditional() {
1172 let val = ResolvedValue::Map {
1173 entries: vec![MapEntry {
1174 key: "Statement".into(),
1175 value: ResolvedValue::List {
1176 items: vec![ResolvedValue::Map {
1177 entries: vec![MapEntry {
1178 key: "Resource".into(),
1179 value: ResolvedValue::Conditional {
1180 condition: "C".into(),
1181 if_true: Box::new(ResolvedValue::Concrete { value: json!("first").into() }),
1182 if_false: Box::new(ResolvedValue::Concrete { value: json!("second").into() }),
1183 },
1184 }],
1185 }],
1186 },
1187 }],
1188 };
1189 let mut results = Vec::new();
1190 collect_scenarios(&val, &HashMap::new(), MAX_SCENARIO_COMBINATIONS, &mut results);
1191
1192 assert_eq!(results.len(), 2);
1193 assert_eq!(results[0].1.get("C"), Some(&true));
1194 assert_eq!(results[1].1.get("C"), Some(&false));
1195 }
1196
1197 #[test]
1198 fn collect_scenarios_enum_expands() {
1199 let val = ResolvedValue::Enum {
1200 variants: vec![
1201 ResolvedValue::Concrete { value: json!("a").into() },
1202 ResolvedValue::Concrete { value: json!("b").into() },
1203 ],
1204 };
1205 let mut results = Vec::new();
1206 collect_scenarios(&val, &HashMap::new(), MAX_SCENARIO_COMBINATIONS, &mut results);
1207 assert_eq!(results.len(), 2);
1208 }
1209
1210 #[test]
1211 fn collect_scenarios_respects_existing_assumptions() {
1212 let val = ResolvedValue::Conditional {
1213 condition: "C".into(),
1214 if_true: Box::new(ResolvedValue::Concrete { value: json!(1).into() }),
1215 if_false: Box::new(ResolvedValue::Concrete { value: json!(2).into() }),
1216 };
1217 let mut assumptions = HashMap::new();
1218 assumptions.insert("C".to_string(), true);
1219 let mut results = Vec::new();
1220 collect_scenarios(&val, &assumptions, MAX_SCENARIO_COMBINATIONS, &mut results);
1221 assert_eq!(results.len(), 1);
1222 assert!(matches!(&results[0].0, ResolvedValue::Concrete { value: v } if v.0 == json!(1)));
1223 }
1224
1225 #[test]
1226 fn direct_enum_at_scenario_limit_is_not_curtailed() {
1227 let value = ResolvedValue::Enum {
1228 variants: (0..MAX_SCENARIO_COMBINATIONS)
1229 .map(|index| ResolvedValue::Concrete { value: json!(index).into() })
1230 .collect(),
1231 };
1232 let mut scenarios = Vec::new();
1233 let mut curtailed = false;
1234
1235 collect_scenarios_signaled(&value, &HashMap::new(), &mut scenarios, &mut curtailed);
1236
1237 assert_eq!(scenarios.len(), MAX_SCENARIO_COMBINATIONS);
1238 assert!(!curtailed, "an exact-fit direct enum must not report omitted scenarios");
1239 }
1240
1241 #[test]
1242 fn conditional_branch_one_over_scenario_limit_is_curtailed() {
1243 let value = ResolvedValue::Conditional {
1244 condition: "UseEnum".into(),
1245 if_true: Box::new(ResolvedValue::Enum {
1246 variants: (0..MAX_SCENARIO_COMBINATIONS)
1247 .map(|index| ResolvedValue::Concrete { value: json!(index).into() })
1248 .collect(),
1249 }),
1250 if_false: Box::new(ResolvedValue::Concrete { value: json!("fallback").into() }),
1251 };
1252 let mut scenarios = Vec::new();
1253 let mut curtailed = false;
1254
1255 collect_scenarios_signaled(&value, &HashMap::new(), &mut scenarios, &mut curtailed);
1256
1257 assert_eq!(scenarios.len(), MAX_SCENARIO_COMBINATIONS);
1258 assert!(curtailed, "the first omitted conditional-branch scenario must be reported");
1259 assert!(scenarios.iter().all(|(_, conditions)| conditions.get("UseEnum") == Some(&true)));
1260 }
1261
1262 #[test]
1263 fn contains_dynamic_detects_nested_dynamic() {
1264 let val = ResolvedValue::List {
1265 items: vec![
1266 ResolvedValue::Concrete { value: json!("ok").into() },
1267 ResolvedValue::Dynamic { reason: "unknown".into() },
1268 ],
1269 };
1270 assert!(contains_dynamic_resolved(&val));
1271 }
1272
1273 #[test]
1274 fn contains_dynamic_false_for_all_concrete() {
1275 let val = ResolvedValue::Map {
1276 entries: vec![MapEntry { key: "a".into(), value: ResolvedValue::Concrete { value: json!(1).into() } }],
1277 };
1278 assert!(!contains_dynamic_resolved(&val));
1279 }
1280
1281 #[test]
1282 fn contains_dynamic_detects_typed_dynamic() {
1283 let val = ResolvedValue::TypedDynamic { reason: "param".into(), param_type: "String".into() };
1284 assert!(contains_dynamic_resolved(&val));
1285 }
1286
1287 #[test]
1288 fn contains_dynamic_detects_reference() {
1289 let val = ResolvedValue::Reference { target: "R".into(), kind: RefKind::Ref };
1290 assert!(contains_dynamic_resolved(&val));
1291 }
1292
1293 #[test]
1294 fn json_contains_markers_detects_dynamic_marker() {
1295 let val = json!({MARKER_DYNAMIC: "reason"});
1296 assert!(json_contains_markers(&val));
1297 }
1298
1299 #[test]
1300 fn json_contains_markers_false_for_plain() {
1301 assert!(!json_contains_markers(&json!({"key": "value"})));
1302 assert!(!json_contains_markers(&json!("string")));
1303 assert!(!json_contains_markers(&json!(42)));
1304 }
1305
1306 #[test]
1307 fn bounds_of_a_literal_are_its_own_length() {
1308 let val = ResolvedValue::Concrete { value: json!("hello").into() };
1309 assert_eq!(estimate_resolved_string_length_bounds(&val), Some((5, 5)));
1310 }
1311
1312 #[test]
1313 fn bounds_of_a_conditional_span_both_branches() {
1314 let val = ResolvedValue::Conditional {
1315 condition: "C".into(),
1316 if_true: Box::new(ResolvedValue::Concrete { value: json!("short").into() }),
1317 if_false: Box::new(ResolvedValue::Concrete { value: json!("much longer string").into() }),
1318 };
1319 assert_eq!(estimate_resolved_string_length_bounds(&val), Some((5, "much longer string".len())));
1320 }
1321
1322 #[test]
1323 fn bounds_of_a_conditional_are_absent_when_one_branch_is_unknown() {
1324 let val = ResolvedValue::Conditional {
1327 condition: "C".into(),
1328 if_true: Box::new(ResolvedValue::Concrete { value: json!("short").into() }),
1329 if_false: Box::new(ResolvedValue::Dynamic { reason: "unknown".into() }),
1330 };
1331 assert_eq!(estimate_resolved_string_length_bounds(&val), None);
1332 }
1333
1334 #[test]
1335 fn bounds_of_allowed_values_span_every_choice() {
1336 let val = ResolvedValue::Enum {
1337 variants: vec![
1338 ResolvedValue::Concrete { value: json!("ab").into() },
1339 ResolvedValue::Concrete { value: json!("abcdef").into() },
1340 ],
1341 };
1342 assert_eq!(estimate_resolved_string_length_bounds(&val), Some((2, 6)));
1343 }
1344
1345 #[test]
1346 fn bounds_of_allowed_values_are_absent_when_one_choice_is_unknown() {
1347 let val = ResolvedValue::Enum {
1348 variants: vec![
1349 ResolvedValue::Concrete { value: json!("ab").into() },
1350 ResolvedValue::Dynamic { reason: "unknown".into() },
1351 ],
1352 };
1353 assert_eq!(estimate_resolved_string_length_bounds(&val), None);
1354 }
1355
1356 #[test]
1357 fn bounds_of_a_sub_with_interpolation_are_absent() {
1358 let val = ResolvedValue::Dynamic { reason: "Sub:arn:aws:s3:::${BucketName}".into() };
1361 assert_eq!(estimate_resolved_string_length_bounds(&val), None);
1362 }
1363
1364 #[test]
1365 fn bounds_of_a_fully_substituted_sub_are_its_length() {
1366 let val = ResolvedValue::Dynamic { reason: "Sub:no-variables-here".into() };
1367 assert_eq!(
1368 estimate_resolved_string_length_bounds(&val),
1369 Some(("no-variables-here".len(), "no-variables-here".len()))
1370 );
1371 }
1372
1373 #[test]
1374 fn bounds_are_absent_for_a_non_string_and_an_opaque_value() {
1375 assert_eq!(
1376 estimate_resolved_string_length_bounds(&ResolvedValue::Concrete { value: json!(42).into() }),
1377 None,
1378 "a number has no string length"
1379 );
1380 assert_eq!(
1381 estimate_resolved_string_length_bounds(&ResolvedValue::Dynamic { reason: "unknown".into() }),
1382 None,
1383 "an opaque value has no known length"
1384 );
1385 }
1386
1387 #[test]
1388 fn bounds_of_a_join_with_an_unresolved_reference_are_absent() {
1389 let val = ResolvedValue::Dynamic {
1394 reason: format!("{JOIN_PARTIAL_PREFIX}prefix-{UNRESOLVED_REF_PLACEHOLDER_PREFIX}Other}}-suffix"),
1395 };
1396 assert_eq!(estimate_resolved_string_length_bounds(&val), None, "a placeholder has no measurable length");
1397 }
1398
1399 #[test]
1400 fn bounds_of_a_join_with_an_opaque_deploy_time_value_are_absent() {
1401 let val = ResolvedValue::Dynamic {
1402 reason: format!("{JOIN_PARTIAL_PREFIX}prefix-{UNRESOLVED_DYNAMIC_PLACEHOLDER}-suffix"),
1403 };
1404 assert_eq!(
1405 estimate_resolved_string_length_bounds(&val),
1406 None,
1407 "a deploy-time placeholder has no measurable length"
1408 );
1409 }
1410
1411 #[test]
1412 fn bounds_of_a_join_of_known_parts_are_its_length() {
1413 let val = ResolvedValue::Dynamic { reason: "Join:prefix-middle-suffix".into() };
1414 assert_eq!(
1415 estimate_resolved_string_length_bounds(&val),
1416 Some(("prefix-middle-suffix".len(), "prefix-middle-suffix".len()))
1417 );
1418 }
1419
1420 #[test]
1421 fn bounds_of_a_sub_with_an_unresolved_reference_are_absent() {
1422 let val = ResolvedValue::Dynamic { reason: "Sub:name-{ref:Other}".into() };
1423 assert_eq!(estimate_resolved_string_length_bounds(&val), None, "a placeholder has no measurable length");
1424 }
1425
1426 fn make_branching_map(n: usize) -> ResolvedValue {
1432 let entries: Vec<MapEntry> = (0..n)
1433 .map(|i| MapEntry {
1434 key: format!("key{i}"),
1435 value: ResolvedValue::Conditional {
1436 condition: format!("C{i}"),
1437 if_true: Box::new(ResolvedValue::Concrete { value: json!(format!("true{i}")).into() }),
1438 if_false: Box::new(ResolvedValue::Concrete { value: json!(format!("false{i}")).into() }),
1439 },
1440 })
1441 .collect();
1442 ResolvedValue::Map { entries }
1443 }
1444
1445 fn make_branching_list(n: usize) -> ResolvedValue {
1448 let items: Vec<ResolvedValue> = (0..n)
1449 .map(|i| ResolvedValue::Conditional {
1450 condition: format!("C{i}"),
1451 if_true: Box::new(ResolvedValue::Concrete { value: json!(format!("true{i}")).into() }),
1452 if_false: Box::new(ResolvedValue::Concrete { value: json!(format!("false{i}")).into() }),
1453 })
1454 .collect();
1455 ResolvedValue::List { items }
1456 }
1457
1458 #[test]
1459 fn exact_limit_expansion_does_not_mark_curtailment() {
1460 let val = make_branching_map(4);
1464 let mut results = Vec::new();
1465 let curtailed = collect_scenarios(&val, &HashMap::new(), 16, &mut results);
1466 assert!(!curtailed, "exact-limit expansion must not be curtailed");
1467 assert_eq!(results.len(), 16, "all 2^4 scenarios must be produced");
1468 }
1469
1470 #[test]
1471 fn over_limit_expansion_marks_curtailment() {
1472 let val = make_branching_map(5);
1475 let mut results = Vec::new();
1476 let curtailed = collect_scenarios(&val, &HashMap::new(), 16, &mut results);
1477 assert!(curtailed, "over-limit expansion must be curtailed");
1478 assert!(results.len() <= 16, "at most `limit` scenarios may be returned; got {}", results.len());
1479 }
1480
1481 #[test]
1482 fn curtailed_map_scenarios_are_structurally_complete() {
1483 let val = make_branching_map(5);
1486 let mut results = Vec::new();
1487 let curtailed = collect_scenarios(&val, &HashMap::new(), 4, &mut results);
1488 assert!(curtailed, "should be curtailed with limit=4 and 5 conditions");
1489 for (scenario, _) in &results {
1490 match scenario {
1491 ResolvedValue::Map { entries } => {
1492 assert_eq!(
1493 entries.len(),
1494 5,
1495 "every returned map scenario must have all 5 entries; got {}: {:?}",
1496 entries.len(),
1497 entries.iter().map(|e| &e.key).collect::<Vec<_>>()
1498 );
1499 }
1500 other => panic!("expected Map, got {:?}", other),
1501 }
1502 }
1503 }
1504
1505 #[test]
1506 fn curtailed_list_scenarios_are_structurally_complete() {
1507 let val = make_branching_list(5);
1510 let mut results = Vec::new();
1511 let curtailed = collect_scenarios(&val, &HashMap::new(), 4, &mut results);
1512 assert!(curtailed, "should be curtailed with limit=4 and 5 conditions");
1513 for (scenario, _) in &results {
1514 match scenario {
1515 ResolvedValue::List { items } => {
1516 assert_eq!(
1517 items.len(),
1518 5,
1519 "every returned list scenario must have all 5 items; got {}",
1520 items.len()
1521 );
1522 }
1523 other => panic!("expected List, got {:?}", other),
1524 }
1525 }
1526 }
1527
1528 #[test]
1529 fn nested_curtailment_is_propagated_to_the_root() {
1530 let val = ResolvedValue::List { items: vec![make_branching_map(3)] };
1531 let mut results = Vec::new();
1532 let curtailed = collect_scenarios(&val, &HashMap::new(), 4, &mut results);
1533 assert!(curtailed, "curtailment inside a nested map must be visible to the root collector");
1534 assert_eq!(results.len(), 4);
1535 for (scenario, _) in results {
1536 let ResolvedValue::List { items } = scenario else {
1537 panic!("expected outer list scenario");
1538 };
1539 assert_eq!(items.len(), 1);
1540 let ResolvedValue::Map { entries } = &items[0] else {
1541 panic!("expected nested map scenario");
1542 };
1543 assert_eq!(entries.len(), 3, "nested scenarios must remain structurally complete");
1544 }
1545 }
1546
1547 #[test]
1548 fn enum_expansion_obeys_limit_and_exact_boundary() {
1549 let four = ResolvedValue::Enum {
1550 variants: (0..4).map(|value| ResolvedValue::Concrete { value: json!(value).into() }).collect(),
1551 };
1552 let mut exact_results = Vec::new();
1553 assert!(!collect_scenarios(&four, &HashMap::new(), 4, &mut exact_results));
1554 assert_eq!(exact_results.len(), 4);
1555
1556 let five = ResolvedValue::Enum {
1557 variants: (0..5).map(|value| ResolvedValue::Concrete { value: json!(value).into() }).collect(),
1558 };
1559 let mut limited_results = Vec::new();
1560 assert!(collect_scenarios(&five, &HashMap::new(), 4, &mut limited_results));
1561 assert_eq!(limited_results.len(), 4);
1562 }
1563
1564 #[test]
1565 fn expand_cartesian_returns_false_at_exact_limit() {
1566 let a_val = ResolvedValue::Concrete { value: json!("a").into() };
1569 let b_val = ResolvedValue::Concrete { value: json!("b").into() };
1570 let items: Vec<(usize, Vec<(ResolvedValue, HashMap<String, bool>)>)> = vec![
1571 (0, vec![(a_val.clone(), HashMap::new()), (b_val.clone(), HashMap::new())]),
1572 (1, vec![(a_val.clone(), HashMap::new()), (b_val.clone(), HashMap::new())]),
1573 ];
1574 let mut results = Vec::new();
1575 let mut used_covering = false;
1576 let curtailed = expand_cartesian_scenarios(
1577 &items,
1578 &HashMap::new(),
1579 4,
1580 |collected| ResolvedValue::List { items: collected.into_iter().map(|(_, v)| v).collect() },
1581 &mut results,
1582 &|_| true,
1583 &mut used_covering,
1584 );
1585 assert!(!curtailed, "4 combos with limit=4 must not be curtailed");
1586 assert!(!used_covering);
1587 assert_eq!(results.len(), 4);
1588 }
1589
1590 #[test]
1591 fn expand_cartesian_returns_true_over_limit() {
1592 let a_val = ResolvedValue::Concrete { value: json!("a").into() };
1594 let b_val = ResolvedValue::Concrete { value: json!("b").into() };
1595 let items: Vec<(usize, Vec<(ResolvedValue, HashMap<String, bool>)>)> = vec![
1596 (0, vec![(a_val.clone(), HashMap::new()), (b_val.clone(), HashMap::new())]),
1597 (1, vec![(a_val.clone(), HashMap::new()), (b_val.clone(), HashMap::new())]),
1598 (2, vec![(a_val.clone(), HashMap::new()), (b_val.clone(), HashMap::new())]),
1599 ];
1600 let mut results = Vec::new();
1601 let mut used_covering = false;
1602 let curtailed = expand_cartesian_scenarios(
1603 &items,
1604 &HashMap::new(),
1605 4,
1606 |collected| ResolvedValue::List { items: collected.into_iter().map(|(_, v)| v).collect() },
1607 &mut results,
1608 &|_| true,
1609 &mut used_covering,
1610 );
1611 assert!(curtailed, "8 combos with limit=4 must be curtailed");
1612 assert!(!used_covering);
1613 assert!(results.len() <= 4, "at most limit results; got {}", results.len());
1614 for (scenario, _) in &results {
1616 match scenario {
1617 ResolvedValue::List { items } => {
1618 assert_eq!(items.len(), 3, "every scenario must have all 3 items");
1619 }
1620 other => panic!("expected List, got {:?}", other),
1621 }
1622 }
1623 }
1624
1625 fn optional_item(condition: &str, value: &str) -> ResolvedValue {
1626 ResolvedValue::Conditional {
1627 condition: condition.to_string(),
1628 if_true: Box::new(ResolvedValue::Concrete { value: json!(value).into() }),
1629 if_false: Box::new(ResolvedValue::Concrete { value: json!(null).into() }),
1630 }
1631 }
1632
1633 fn present_items(scenario: &ResolvedValue) -> Vec<String> {
1634 let ResolvedValue::List { items } = scenario else { panic!("expected a list, got {scenario:?}") };
1635 items
1636 .iter()
1637 .filter_map(|item| match item {
1638 ResolvedValue::Concrete { value } => value.as_str().map(str::to_string),
1639 other => panic!("expected a concrete item, got {other:?}"),
1640 })
1641 .collect()
1642 }
1643
1644 #[test]
1645 fn independent_conditions_below_the_product_bound_are_enumerated_in_full() {
1646 let list = ResolvedValue::List { items: (0..4).map(|i| optional_item(&format!("C{i}"), "v")).collect() };
1647 let mut scenarios = Vec::new();
1648 let curtailed = collect_scenarios(&list, &HashMap::new(), MAX_SCENARIO_COMBINATIONS, &mut scenarios);
1649 assert!(!curtailed);
1650 assert_eq!(scenarios.len(), 16, "2^4 joint assignments");
1651 }
1652
1653 #[test]
1654 fn independent_conditions_above_the_product_bound_preserve_pairwise_condition_states() {
1655 let count = 13;
1656 let list = ResolvedValue::List {
1657 items: (0..count).map(|i| optional_item(&format!("C{i:02}"), &format!("v{i:02}"))).collect(),
1658 };
1659 let mut scenarios = Vec::new();
1660 let curtailed = collect_scenarios(&list, &HashMap::new(), MAX_SCENARIO_COMBINATIONS, &mut scenarios);
1661 assert!(!curtailed, "a covering set is a complete analysis strategy, not a curtailment");
1662 assert_eq!(scenarios.len(), count + 2, "all-true, all-false and all-true with one condition flipped");
1663
1664 let all_present = scenarios.iter().find(|(value, _)| present_items(value).len() == count).expect("all-present");
1665 assert!(all_present.1.values().all(|value| *value));
1666 let all_absent = scenarios.iter().find(|(value, _)| present_items(value).is_empty()).expect("all-absent");
1667 assert!(all_absent.1.values().all(|value| !*value));
1668 for i in 0..count {
1669 let flipped = format!("C{i:02}");
1670 let single_flip = scenarios
1671 .iter()
1672 .find(|(_, assumptions)| assumptions.iter().all(|(condition, value)| *value == (condition != &flipped)))
1673 .unwrap_or_else(|| panic!("a scenario with only {flipped} false"));
1674 let present = present_items(&single_flip.0);
1675 assert_eq!(present.len(), count - 1);
1676 assert!(!present.contains(&format!("v{i:02}")));
1677 }
1678 for (_, assumptions) in &scenarios {
1679 assert_eq!(assumptions.len(), count, "every scenario decides every condition");
1680 }
1681 }
1682
1683 #[test]
1684 fn covering_set_deduplicates_assignments_that_coincide() {
1685 let list = ResolvedValue::List { items: (0..3).map(|_| optional_item("Shared", "v")).collect() };
1687 let mut scenarios = Vec::new();
1688 collect_scenarios(&list, &HashMap::new(), MAX_SCENARIO_COMBINATIONS, &mut scenarios);
1689 assert_eq!(scenarios.len(), 2, "shared conditions never multiply");
1690 }
1691
1692 #[test]
1693 fn covering_set_includes_an_alternative_requiring_three_literals() {
1694 let selected_value = ResolvedValue::Conditional {
1695 condition: "CA".to_string(),
1696 if_true: Box::new(ResolvedValue::Concrete { value: json!("a").into() }),
1697 if_false: Box::new(ResolvedValue::Conditional {
1698 condition: "CB".to_string(),
1699 if_true: Box::new(ResolvedValue::Concrete { value: json!("b").into() }),
1700 if_false: Box::new(ResolvedValue::Conditional {
1701 condition: "CC".to_string(),
1702 if_true: Box::new(ResolvedValue::Concrete { value: json!("selected").into() }),
1703 if_false: Box::new(ResolvedValue::Concrete { value: json!("c").into() }),
1704 }),
1705 }),
1706 };
1707 let mut items = vec![selected_value];
1708 items.extend((0..11).map(|index| optional_item(&format!("C{index:02}"), "sibling")));
1709 let list = ResolvedValue::List { items };
1710
1711 let mut scenarios = Vec::new();
1712 let curtailed = collect_scenarios(&list, &HashMap::new(), MAX_SCENARIO_COMBINATIONS, &mut scenarios);
1713
1714 assert!(!curtailed);
1715 let selected = scenarios
1716 .iter()
1717 .find(|(scenario, _)| present_items(scenario).first().is_some_and(|value| value == "selected"))
1718 .expect("the three-literal alternative must be represented");
1719 assert_eq!(selected.1.get("CA"), Some(&false));
1720 assert_eq!(selected.1.get("CB"), Some(&false));
1721 assert_eq!(selected.1.get("CC"), Some(&true));
1722 }
1723
1724 #[test]
1725 fn covering_set_finds_a_reachable_completion_for_each_alternative() {
1726 let selected_value = ResolvedValue::Conditional {
1727 condition: "A".to_string(),
1728 if_true: Box::new(ResolvedValue::Concrete { value: json!("other").into() }),
1729 if_false: Box::new(ResolvedValue::Concrete { value: json!("selected").into() }),
1730 };
1731 let correlated_value = ResolvedValue::Conditional {
1732 condition: "B".to_string(),
1733 if_true: Box::new(ResolvedValue::Concrete { value: json!("b-true").into() }),
1734 if_false: Box::new(ResolvedValue::Concrete { value: json!("b-false").into() }),
1735 };
1736 let mut items = vec![selected_value, correlated_value];
1737 items.extend((0..11).map(|index| optional_item(&format!("C{index:02}"), "sibling")));
1738 let list = ResolvedValue::List { items };
1739 let is_reachable = |assumptions: &HashMap<String, bool>| match (assumptions.get("A"), assumptions.get("B")) {
1740 (Some(a), Some(b)) => a == b,
1741 _ => true,
1742 };
1743
1744 let mut scenarios = Vec::new();
1745 let curtailed = collect_scenarios_reachable(
1746 &list,
1747 &HashMap::new(),
1748 MAX_SCENARIO_COMBINATIONS,
1749 &mut scenarios,
1750 &is_reachable,
1751 );
1752
1753 assert!(!curtailed);
1754 assert!(scenarios.iter().all(|(_, assumptions)| is_reachable(assumptions)));
1755 let selected = scenarios
1756 .iter()
1757 .find(|(scenario, _)| present_items(scenario).first().is_some_and(|value| value == "selected"))
1758 .expect("the reachable false alternative must be represented");
1759 assert_eq!(selected.1.get("A"), Some(&false));
1760 assert_eq!(selected.1.get("B"), Some(&false));
1761 }
1762
1763 #[test]
1764 fn covering_set_is_chosen_by_materialized_size_as_well_as_count() {
1765 let wide_leaf = ResolvedValue::List {
1766 items: (0..600).map(|i| ResolvedValue::Concrete { value: json!(i).into() }).collect(),
1767 };
1768 let optional_wide = |condition: &str| ResolvedValue::Conditional {
1769 condition: condition.to_string(),
1770 if_true: Box::new(wide_leaf.clone()),
1771 if_false: Box::new(ResolvedValue::Concrete { value: json!(null).into() }),
1772 };
1773 let items: Vec<(usize, Vec<(ResolvedValue, HashMap<String, bool>)>)> = (0..11)
1774 .map(|i| {
1775 let mut scenarios = Vec::new();
1776 collect_scenarios(&optional_wide(&format!("C{i:02}")), &HashMap::new(), 16, &mut scenarios);
1777 (i, scenarios)
1778 })
1779 .collect();
1780 assert!(!product_is_enumerable(&items), "2^11 combos of ~600-node members exceed the node bound");
1781 let small: Vec<(usize, Vec<(ResolvedValue, HashMap<String, bool>)>)> = items
1782 .iter()
1783 .map(|(i, scenarios)| {
1784 (
1785 *i,
1786 scenarios
1787 .iter()
1788 .map(|(_, a)| (ResolvedValue::Concrete { value: json!(1).into() }, a.clone()))
1789 .collect(),
1790 )
1791 })
1792 .collect();
1793 assert!(product_is_enumerable(&small), "the same 2^11 combos of scalar members are enumerable");
1794 }
1795
1796 #[test]
1797 fn node_count_counts_every_resolved_node_once() {
1798 let value = ResolvedValue::Map {
1799 entries: vec![MapEntry {
1800 key: "k".into(),
1801 value: ResolvedValue::List {
1802 items: vec![optional_item("C", "v"), ResolvedValue::Concrete { value: json!(1).into() }],
1803 },
1804 }],
1805 };
1806 assert_eq!(node_count(&value), 6, "map + list + conditional + its two leaves + the concrete item");
1807 }
1808
1809 #[test]
1810 fn scenario_source_path_selects_resolved_container_branch() {
1811 let list = ResolvedValue::Conditional {
1812 condition: "ChooseFirst".into(),
1813 if_true: Box::new(ResolvedValue::List {
1814 items: vec![ResolvedValue::Concrete { value: json!("first").into() }],
1815 }),
1816 if_false: Box::new(ResolvedValue::List {
1817 items: vec![ResolvedValue::Concrete { value: json!("second").into() }],
1818 }),
1819 };
1820 let object = ResolvedValue::Conditional {
1821 condition: "ChooseFirst".into(),
1822 if_true: Box::new(ResolvedValue::Map {
1823 entries: vec![MapEntry {
1824 key: "Name".into(),
1825 value: ResolvedValue::Concrete { value: json!("first").into() },
1826 }],
1827 }),
1828 if_false: Box::new(ResolvedValue::Map {
1829 entries: vec![MapEntry {
1830 key: "Name".into(),
1831 value: ResolvedValue::Concrete { value: json!("second").into() },
1832 }],
1833 }),
1834 };
1835 let conditions = HashMap::from([("ChooseFirst".to_string(), false)]);
1836
1837 assert_eq!(
1838 scenario_source_path_at(&list, "", &conditions, "Properties.Values"),
1839 Some("Properties.Values.Fn::If.2".to_string())
1840 );
1841 assert_eq!(
1842 scenario_source_path_at(&object, "", &conditions, "Properties.Configuration"),
1843 Some("Properties.Configuration.Fn::If.2".to_string())
1844 );
1845 }
1846}