use crate::{CapabilityGuard, HelperBranch, HelperBranchBody};
use helm_schema_ast::DefineIndex;
use indoc::indoc;
use test_util::prelude::sim_assert_eq;
fn literals_of(b: &HelperBranch) -> &[String] {
match &b.body {
HelperBranchBody::Literals { values } => values.as_slice(),
HelperBranchBody::Nested { .. } => panic!("expected Literals-bodied branch; got {b:?}"),
}
}
fn index_with(src: &str) -> DefineIndex {
let mut idx = DefineIndex::new();
idx.add_file_source("<inline:0>", src);
idx
}
fn evaluate_helper(name: &str, helpers: &DefineIndex) -> HelperBranchBody {
let analysis_db = crate::analysis_db::IrAnalysisDb::new(helpers);
let Some(body) = analysis_db.parsed_helper_body(name) else {
return HelperBranchBody::literals(Vec::new());
};
crate::resource_identity::HelperOutputEvaluator::default().evaluate_body(
body.source,
body.tree.root_node(),
&analysis_db,
0,
)
}
#[test]
fn single_literal_helper_resolves() {
let helpers = index_with(indoc! {r#"
{{- define "x.apiVersion" -}}
{{- print "apps/v1" -}}
{{- end -}}
"#});
sim_assert_eq!(
have: evaluate_helper("x.apiVersion", &helpers).all_literals(),
want: vec!["apps/v1"]
);
}
#[test]
fn if_else_helper_returns_both_branches() {
let helpers = index_with(indoc! {r#"
{{- define "rbac.apiVersion" -}}
{{- if .Capabilities.APIVersions.Has "rbac.authorization.k8s.io/v1" }}
{{- print "rbac.authorization.k8s.io/v1" -}}
{{- else -}}
{{- print "rbac.authorization.k8s.io/v1beta1" -}}
{{- end -}}
{{- end -}}
"#});
let outs = evaluate_helper("rbac.apiVersion", &helpers).all_literals();
assert!(
outs.contains(&"rbac.authorization.k8s.io/v1".to_string()),
"must include modern; got {outs:?}"
);
assert!(
outs.contains(&"rbac.authorization.k8s.io/v1beta1".to_string()),
"must include legacy; got {outs:?}"
);
}
#[test]
fn helper_with_values_reference_is_silent_about_dynamic_branch() {
let helpers = index_with(indoc! {r#"
{{- define "grafana.podDisruptionBudget.apiVersion" -}}
{{- if $.Values.podDisruptionBudget.apiVersion }}
{{- print $.Values.podDisruptionBudget.apiVersion }}
{{- else if $.Capabilities.APIVersions.Has "policy/v1/PodDisruptionBudget" }}
{{- print "policy/v1" }}
{{- else }}
{{- print "policy/v1beta1" }}
{{- end }}
{{- end }}
"#});
let outs = evaluate_helper("grafana.podDisruptionBudget.apiVersion", &helpers).all_literals();
assert!(
outs.contains(&"policy/v1".to_string()),
"must include policy/v1 literal branch; got {outs:?}"
);
assert!(
outs.contains(&"policy/v1beta1".to_string()),
"must include policy/v1beta1 literal branch; got {outs:?}"
);
}
#[test]
fn unknown_helper_returns_empty() {
let helpers = DefineIndex::new();
sim_assert_eq!(
have: evaluate_helper("nope", &helpers).all_literals(),
want: Vec::<String>::new()
);
}
#[test]
fn nested_helper_recurses_one_level() {
let helpers = index_with(indoc! {r#"
{{- define "outer" -}}
{{- template "inner" . -}}
{{- end -}}
{{- define "inner" -}}
{{- print "apps/v1" -}}
{{- end -}}
"#});
sim_assert_eq!(
have: evaluate_helper("outer", &helpers).all_literals(),
want: vec!["apps/v1"]
);
}
#[test]
fn cyclic_helper_does_not_stack_overflow() {
let helpers = index_with(indoc! {r#"
{{- define "a" -}}
{{- template "b" . -}}
{{- end -}}
{{- define "b" -}}
{{- template "a" . -}}
{{- end -}}
"#});
let outs = evaluate_helper("a", &helpers).all_literals();
assert!(
outs.is_empty(),
"cyclic helper must return empty, not infinite recursion; got {outs:?}"
);
}
#[test]
fn typed_output_preserves_guard_and_branch_literals() {
let helpers = index_with(indoc! {r#"
{{- define "rbac.apiVersion" -}}
{{- if .Capabilities.APIVersions.Has "rbac.authorization.k8s.io/v1" }}
{{- print "rbac.authorization.k8s.io/v1" -}}
{{- else -}}
{{- print "rbac.authorization.k8s.io/v1beta1" -}}
{{- end -}}
{{- end -}}
"#});
let out = evaluate_helper("rbac.apiVersion", &helpers);
let HelperBranchBody::Nested { branches } = out else {
panic!("expected Nested; got {out:?}");
};
sim_assert_eq!(have: branches.len(), want: 2, "expected 2 branches; got {branches:?}");
sim_assert_eq!(
have: branches[0].guard,
want: Some(CapabilityGuard::Has {
api: "rbac.authorization.k8s.io/v1".to_string(),
}),
"branch[0] guard mismatch"
);
sim_assert_eq!(
have: literals_of(&branches[0]),
want: vec!["rbac.authorization.k8s.io/v1".to_string()]
);
sim_assert_eq!(have: branches[1].guard, want: None, "branch[1] should be unguarded");
sim_assert_eq!(
have: literals_of(&branches[1]),
want: vec!["rbac.authorization.k8s.io/v1beta1".to_string()]
);
}
#[test]
fn typed_output_preserves_branches_through_wrapper_include() {
let helpers = index_with(indoc! {r#"
{{- define "outer.apiVersion" -}}
{{- include "rbac.apiVersion" . -}}
{{- end -}}
{{- define "rbac.apiVersion" -}}
{{- if .Capabilities.APIVersions.Has "rbac.authorization.k8s.io/v1" }}
{{- print "rbac.authorization.k8s.io/v1" -}}
{{- else -}}
{{- print "rbac.authorization.k8s.io/v1beta1" -}}
{{- end -}}
{{- end -}}
"#});
let out = evaluate_helper("outer.apiVersion", &helpers);
let HelperBranchBody::Nested { branches } = out else {
panic!(
"wrapper helper must preserve branched typed output from delegated callee; got {out:?}"
);
};
sim_assert_eq!(have: branches.len(), want: 2, "expected 2 branches; got {branches:?}");
sim_assert_eq!(
have: branches[0].guard,
want: Some(CapabilityGuard::Has {
api: "rbac.authorization.k8s.io/v1".to_string(),
}),
"branch[0] guard must carry the CapabilityHas decoded from the inner helper"
);
sim_assert_eq!(
have: literals_of(&branches[0]),
want: vec!["rbac.authorization.k8s.io/v1".to_string()]
);
sim_assert_eq!(have: branches[1].guard, want: None);
sim_assert_eq!(
have: literals_of(&branches[1]),
want: vec!["rbac.authorization.k8s.io/v1beta1".to_string()]
);
}
#[test]
fn typed_output_preserves_branches_through_wrapper_template() {
let helpers = index_with(indoc! {r#"
{{- define "outer.apiVersion" -}}
{{- template "rbac.apiVersion" . -}}
{{- end -}}
{{- define "rbac.apiVersion" -}}
{{- if .Capabilities.APIVersions.Has "rbac.authorization.k8s.io/v1" }}
{{- print "rbac.authorization.k8s.io/v1" -}}
{{- else -}}
{{- print "rbac.authorization.k8s.io/v1beta1" -}}
{{- end -}}
{{- end -}}
"#});
let out = evaluate_helper("outer.apiVersion", &helpers);
assert!(
matches!(out, HelperBranchBody::Nested { .. }),
"wrapper via template must preserve Nested output; got {out:?}"
);
}
#[test]
fn typed_output_preserves_branches_through_multi_level_wrapper() {
let helpers = index_with(indoc! {r#"
{{- define "outer" -}}
{{- include "middle" . -}}
{{- end -}}
{{- define "middle" -}}
{{- include "inner" . -}}
{{- end -}}
{{- define "inner" -}}
{{- if .Capabilities.APIVersions.Has "policy/v1" }}
{{- print "policy/v1" -}}
{{- else -}}
{{- print "policy/v1beta1" -}}
{{- end -}}
{{- end -}}
"#});
let out = evaluate_helper("outer", &helpers);
let HelperBranchBody::Nested { branches } = out else {
panic!("multi-level wrapper must preserve branched output; got {out:?}");
};
sim_assert_eq!(have: branches.len(), want: 2);
sim_assert_eq!(
have: branches[0].guard,
want: Some(CapabilityGuard::Has {
api: "policy/v1".to_string()
})
);
}
#[test]
fn typed_output_preserves_nested_branches_through_branch_body_delegation() {
let helpers = index_with(indoc! {r#"
{{- define "outer" -}}
{{- if .Capabilities.APIVersions.Has "A" -}}
{{- include "inner" . -}}
{{- else -}}
{{- print "fallback" -}}
{{- end -}}
{{- end -}}
{{- define "inner" -}}
{{- if .Capabilities.APIVersions.Has "B" -}}
{{- print "b" -}}
{{- else -}}
{{- print "b_legacy" -}}
{{- end -}}
{{- end -}}
"#});
let out = evaluate_helper("outer", &helpers);
let HelperBranchBody::Nested { branches } = out else {
panic!("outer must be Nested; got {out:?}");
};
sim_assert_eq!(have: branches.len(), want: 2, "expected 2 outer branches");
sim_assert_eq!(
have: branches[0].guard,
want: Some(CapabilityGuard::Has {
api: "A".to_string()
}),
);
let HelperBranchBody::Nested { branches: nested } = &branches[0].body else {
panic!(
"branch[0].body must be Nested to preserve inner Has-B guard; got {:?}",
branches[0].body
);
};
sim_assert_eq!(have: nested.len(), want: 2, "inner helper should contribute 2 branches");
sim_assert_eq!(
have: nested[0].guard,
want: Some(CapabilityGuard::Has {
api: "B".to_string()
}),
"nested branch[0] must preserve the inner Has-B guard"
);
sim_assert_eq!(have: literals_of(&nested[0]), want: vec!["b".to_string()]);
sim_assert_eq!(have: nested[1].guard, want: None);
sim_assert_eq!(have: literals_of(&nested[1]), want: vec!["b_legacy".to_string()]);
sim_assert_eq!(have: branches[1].guard, want: None);
sim_assert_eq!(have: literals_of(&branches[1]), want: vec!["fallback".to_string()]);
}
#[test]
fn typed_output_preserves_nested_branches_through_inline_nested_if() {
let helpers = index_with(indoc! {r#"
{{- define "outer" -}}
{{- if .Capabilities.APIVersions.Has "A" -}}
{{- if .Capabilities.APIVersions.Has "B" -}}
{{- print "b" -}}
{{- else -}}
{{- print "b_legacy" -}}
{{- end -}}
{{- else -}}
{{- print "fallback" -}}
{{- end -}}
{{- end -}}
"#});
let out = evaluate_helper("outer", &helpers);
let HelperBranchBody::Nested { branches } = out else {
panic!("outer must be Nested; got {out:?}");
};
sim_assert_eq!(have: branches.len(), want: 2);
let HelperBranchBody::Nested { branches: nested } = &branches[0].body else {
panic!(
"inline nested if must produce Nested body; got {:?}",
branches[0].body
);
};
sim_assert_eq!(have: nested.len(), want: 2);
sim_assert_eq!(
have: nested[0].guard,
want: Some(CapabilityGuard::Has {
api: "B".to_string()
})
);
}
#[test]
fn wrapper_with_mixed_content_does_not_promote_branches() {
let helpers = index_with(indoc! {r#"
{{- define "outer" -}}
prefix-{{ include "inner" . }}
{{- end -}}
{{- define "inner" -}}
{{- if .Capabilities.APIVersions.Has "X" }}
{{- print "X" -}}
{{- else -}}
{{- print "Y" -}}
{{- end -}}
{{- end -}}
"#});
let out = evaluate_helper("outer", &helpers);
assert!(
matches!(out, HelperBranchBody::Literals { .. }),
"mixed-content wrapper must fall through to flat literals; got {out:?}"
);
}
#[test]
fn wrapper_cycle_falls_through_safely() {
let helpers = index_with(indoc! {r#"
{{- define "a" -}}
{{- include "b" . -}}
{{- end -}}
{{- define "b" -}}
{{- include "a" . -}}
{{- end -}}
"#});
let out = evaluate_helper("a", &helpers);
assert!(
matches!(out, HelperBranchBody::Literals { .. }),
"cyclic wrapper chain must fall through cleanly; got {out:?}"
);
}
#[test]
fn typed_output_preserves_elif_chain() {
let helpers = index_with(indoc! {r#"
{{- define "grafana.pdb.apiVersion" -}}
{{- if $.Values.podDisruptionBudget.apiVersion }}
{{- print $.Values.podDisruptionBudget.apiVersion }}
{{- else if $.Capabilities.APIVersions.Has "policy/v1/PodDisruptionBudget" }}
{{- print "policy/v1" }}
{{- else }}
{{- print "policy/v1beta1" }}
{{- end }}
{{- end }}
"#});
let out = evaluate_helper("grafana.pdb.apiVersion", &helpers);
let HelperBranchBody::Nested { branches } = out else {
panic!("expected Nested; got {out:?}");
};
let has_branch = branches.iter().find(|b| {
matches!(
&b.guard,
Some(CapabilityGuard::Has { api }) if api == "policy/v1/PodDisruptionBudget"
)
});
assert!(
has_branch.is_some(),
"expected CapabilityHas branch; got {branches:?}"
);
sim_assert_eq!(
have: literals_of(has_branch.unwrap()),
want: vec!["policy/v1".to_string()]
);
let else_branch = branches.iter().find(|b| b.guard.is_none());
assert!(
else_branch.is_some(),
"expected unguarded else branch; got {branches:?}"
);
sim_assert_eq!(
have: literals_of(else_branch.unwrap()),
want: vec!["policy/v1beta1".to_string()]
);
}
#[test]
fn printf_compositional_format_resolves_exactly() {
let helpers = index_with(indoc! {r#"
{{- define "x.apiVersion" -}}
{{- printf "%s/%s" "apps" "v1" -}}
{{- end -}}
"#});
sim_assert_eq!(
have: evaluate_helper("x.apiVersion", &helpers).all_literals(),
want: vec!["apps/v1"]
);
}
#[test]
fn printf_single_substitution_resolves_to_arg() {
let helpers = index_with(indoc! {r#"
{{- define "x.apiVersion" -}}
{{- printf "%s" "apps/v1" -}}
{{- end -}}
"#});
sim_assert_eq!(
have: evaluate_helper("x.apiVersion", &helpers).all_literals(),
want: vec!["apps/v1"]
);
}
#[test]
fn printf_no_substitution_resolves_to_format() {
let helpers = index_with(indoc! {r#"
{{- define "x.apiVersion" -}}
{{- printf "apps/v1" -}}
{{- end -}}
"#});
sim_assert_eq!(
have: evaluate_helper("x.apiVersion", &helpers).all_literals(),
want: vec!["apps/v1"]
);
}
#[test]
fn printf_non_string_directive_emits_no_candidates() {
let helpers = index_with(indoc! {r#"
{{- define "x.apiVersion" -}}
{{- printf "%d" 1 -}}
{{- end -}}
"#});
let outs = evaluate_helper("x.apiVersion", &helpers).all_literals();
assert!(
outs.is_empty(),
"non-%s printf directive must emit no candidates; got {outs:?}"
);
}
#[test]
fn default_choice_does_not_emit_fallback_as_exact_identity() {
let helpers = index_with(indoc! {r#"
{{- define "x.apiVersion" -}}
{{- default "policy/v1beta1" "policy/v1" -}}
{{- end -}}
"#});
let outs = evaluate_helper("x.apiVersion", &helpers).all_literals();
assert!(
outs.is_empty(),
"resource identity literal evaluation must abstain on expression choices; got {outs:?}"
);
}
#[test]
fn quote_with_single_string_arg_resolves() {
let helpers = index_with(indoc! {r#"
{{- define "x.apiVersion" -}}
{{- quote "apps/v1" -}}
{{- end -}}
"#});
sim_assert_eq!(
have: evaluate_helper("x.apiVersion", &helpers).all_literals(),
want: vec!["apps/v1"]
);
}
#[test]
fn print_with_multiple_literal_args_resolves_exactly() {
let helpers = index_with(indoc! {r#"
{{- define "x.apiVersion" -}}
{{- print "apps/" "v1" -}}
{{- end -}}
"#});
sim_assert_eq!(
have: evaluate_helper("x.apiVersion", &helpers).all_literals(),
want: vec!["apps/v1"]
);
}
#[test]
fn pipeline_seed_literal_then_quote_resolves_to_seed() {
let helpers = index_with(indoc! {r#"
{{- define "x.apiVersion" -}}
{{- "apps/v1" | quote -}}
{{- end -}}
"#});
sim_assert_eq!(
have: evaluate_helper("x.apiVersion", &helpers).all_literals(),
want: vec!["apps/v1"]
);
}
#[test]
fn single_literal_helper_is_not_branched() {
let helpers = index_with(indoc! {r#"
{{- define "x.apiVersion" -}}
{{- print "apps/v1" -}}
{{- end -}}
"#});
let out = evaluate_helper("x.apiVersion", &helpers);
assert!(
matches!(out, HelperBranchBody::Literals { .. }),
"single-literal helper must not be Nested; got {out:?}"
);
}
#[test]
fn quoted_text_literals_decode_as_yaml_scalars() {
let helpers = index_with(indoc! {r#"
{{- define "policy.pdb.apiVersion" -}}
{{- if .Capabilities.APIVersions.Has "policy/v1/PodDisruptionBudget" -}}
"policy/v1"
{{- else -}}
"policy/v1beta1"
{{- end -}}
{{- end -}}
"#});
let out = evaluate_helper("policy.pdb.apiVersion", &helpers);
let HelperBranchBody::Nested { branches } = out else {
panic!("expected Nested; got {out:?}");
};
sim_assert_eq!(have: branches.len(), want: 2, "expected 2 branches; got {branches:?}");
sim_assert_eq!(
have: literals_of(&branches[0]),
want: vec!["policy/v1".to_string()]
);
sim_assert_eq!(
have: literals_of(&branches[1]),
want: vec!["policy/v1beta1".to_string()]
);
}