use super::preprocess;
#[test]
fn static_regex_literals_compile() {
let _ = preprocess("{{ Version }}");
let _ = preprocess("{{ replace Version \"v\" \"\" }}");
let _ = preprocess("{{ Version | replace \"v\" \"\" }}");
let _ = preprocess("{{ in (list \"a\" \"b\") \"a\" }}");
let _ = preprocess("{{ Now.Format \"2006\" }}");
let _ = preprocess("{% if eq .Os \"linux\" %}x{% end %}");
let _ = preprocess("{{ map \"k1\" \"v1\" }}");
}
#[test]
fn test_preprocess_positional_replace() {
let input = "{{ replace Version \"v\" \"\" }}";
let result = preprocess(input);
assert_eq!(result, "{{ replace(s=Version, old=\"v\", new=\"\") }}");
}
#[test]
fn test_preprocess_positional_replace_piped() {
let input = "{{ Version | replace \"v\" \"\" }}";
let result = preprocess(input);
assert_eq!(result, "{{ Version | replace(from=\"v\", to=\"\") }}");
}
#[test]
fn test_preprocess_positional_split() {
let input = "{{ split Version \".\" }}";
let result = preprocess(input);
assert_eq!(result, "{{ split(s=Version, sep=\".\") }}");
}
#[test]
fn test_preprocess_positional_contains() {
let input = "{{ contains Version \"rc\" }}";
let result = preprocess(input);
assert_eq!(result, "{{ contains(s=Version, substr=\"rc\") }}");
}
#[test]
fn test_preprocess_positional_piped_split() {
let input = "{{ Version | split \".\" }}";
let result = preprocess(input);
assert_eq!(result, "{{ Version | split(sep=\".\") }}");
}
#[test]
fn test_preprocess_positional_piped_contains() {
let input = "{{ Version | contains \"rc\" }}";
let result = preprocess(input);
assert_eq!(result, "{{ Version | contains(substr=\"rc\") }}");
}
#[test]
fn test_preprocess_named_args_unchanged() {
let input = "{{ replace(s=Version, old=\"v\", new=\"\") }}";
let result = preprocess(input);
assert_eq!(result, input);
}
#[test]
fn test_preprocess_named_filter_unchanged() {
let input = "{{ Version | replace(from=\"v\", to=\"\") }}";
let result = preprocess(input);
assert_eq!(result, input);
}
#[test]
fn test_preprocess_control_block_rewritten() {
let input = "{% if contains Version \"rc\" %}yes{% endif %}";
let result = preprocess(input);
assert_eq!(
result,
"{% if contains(s=Version, substr=\"rc\") %}yes{% endif %}"
);
}
#[test]
fn test_preprocess_control_block_non_positional_unchanged() {
let input = "{% if Version %}yes{% endif %}";
let result = preprocess(input);
assert_eq!(result, input);
}
#[test]
fn test_positional_replace_with_dot_var() {
let input = "{{ replace .Tag \"v\" \"\" }}";
let result = preprocess(input);
assert_eq!(result, "{{ replace(s=Tag, old=\"v\", new=\"\") }}");
}
#[test]
fn test_positional_piped_with_dot_var() {
let input = "{{ .Tag | replace \"v\" \"\" }}";
let result = preprocess(input);
assert_eq!(result, "{{ Tag | replace(from=\"v\", to=\"\") }}");
}
#[test]
fn test_positional_no_spaces_compact() {
let input = "{{replace .Tag \"v\" \"\"}}";
let result = preprocess(input);
assert_eq!(result, "{{replace(s=Tag, old=\"v\", new=\"\")}}");
}
#[test]
fn test_optional_chaining_dot_survives_go_leading_dot_strip() {
let input = "{{ Some?.Missing or \"fallback\" }}";
let result = preprocess(input);
assert_eq!(result, "{{ Some?.Missing or \"fallback\" }}");
}
#[test]
fn test_unrelated_expression_unchanged() {
let input = "{{ Version }}";
let result = preprocess(input);
assert_eq!(result, "{{ Version }}");
}
#[test]
fn test_unrelated_filter_unchanged() {
let input = "{{ Version | upper }}";
let result = preprocess(input);
assert_eq!(result, "{{ Version | upper }}");
}
#[test]
fn test_positional_replace_whitespace_control() {
let input = "{{- replace Version \"v\" \"\" -}}";
let result = preprocess(input);
assert_eq!(result, "{{- replace(s=Version, old=\"v\", new=\"\") -}}");
}
#[test]
fn test_positional_replace_whitespace_control_left_only() {
let input = "{{- replace Version \"v\" \"\" }}";
let result = preprocess(input);
assert_eq!(result, "{{- replace(s=Version, old=\"v\", new=\"\") }}");
}
#[test]
fn test_chained_named_filter_then_positional_rewrite() {
let input = "{{ Version | trimprefix(prefix=\"v\") | replace \".\" \"-\" }}";
let result = preprocess(input);
assert_eq!(
result,
"{{ Version | trimprefix(prefix=\"v\") | replace(from=\".\", to=\"-\") }}"
);
}
#[test]
fn test_preprocess_in_with_list_subexpr() {
let input = "{{ in (list \"a\" \"b\" \"c\") \"b\" }}";
let result = preprocess(input);
assert_eq!(result, "{{ in(items=[\"a\", \"b\", \"c\"], value=\"b\") }}");
}
#[test]
fn test_preprocess_in_with_variable() {
let input = "{{ in myList \"b\" }}";
let result = preprocess(input);
assert_eq!(result, "{{ in(items=myList, value=\"b\") }}");
}
#[test]
fn test_preprocess_in_named_args_unchanged() {
let input = "{{ in(items=[\"a\", \"b\"], value=\"a\") }}";
let result = preprocess(input);
assert_eq!(result, input);
}
#[test]
fn test_preprocess_in_with_dot_var() {
let input = "{{ in .MyList \"val\" }}";
let result = preprocess(input);
assert_eq!(result, "{{ in(items=MyList, value=\"val\") }}");
}
#[test]
fn test_preprocess_in_control_block() {
let input = "{% if in myList \"b\" %}yes{% endif %}";
let result = preprocess(input);
assert_eq!(
result,
"{% if in(items=myList, value=\"b\") %}yes{% endif %}"
);
}
#[test]
fn test_preprocess_list_subexpr_rewrite() {
let input = "{{ in (list \"x\" \"y\") \"x\" }}";
let result = preprocess(input);
assert_eq!(result, "{{ in(items=[\"x\", \"y\"], value=\"x\") }}");
}
#[test]
fn test_preprocess_in_control_block_with_list_subexpr() {
let input = "{% if in (list \"a\" \"b\") \"a\" %}yes{% endif %}";
let result = preprocess(input);
assert_eq!(
result,
"{% if in(items=[\"a\", \"b\"], value=\"a\") %}yes{% endif %}"
);
}
#[test]
fn test_preprocess_re_replace_all_positional() {
let input = "{{ reReplaceAll \"(.*)\" \"hello\" \"$1-world\" }}";
let result = preprocess(input);
assert_eq!(
result,
"{{ reReplaceAll(pattern=\"(.*)\", input=\"hello\", replacement=\"$1-world\") }}"
);
}
#[test]
fn test_preprocess_re_replace_all_with_variable() {
let input = "{{ reReplaceAll \"(v)(.*)\" Tag \"prefix-$2\" }}";
let result = preprocess(input);
assert_eq!(
result,
"{{ reReplaceAll(pattern=\"(v)(.*)\", input=Tag, replacement=\"prefix-$2\") }}"
);
}
#[test]
fn test_preprocess_re_replace_all_named_args_unchanged() {
let input = "{{ reReplaceAll(pattern=\"x\", input=\"ax\", replacement=\"y\") }}";
let result = preprocess(input);
assert_eq!(result, input);
}
#[test]
fn test_preprocess_re_replace_all_piped() {
let input = "{{ Message | reReplaceAll \"(.*)\" \"$1-done\" }}";
let result = preprocess(input);
assert_eq!(
result,
"{{ Message | reReplaceAll(pattern=\"(.*)\", replacement=\"$1-done\") }}"
);
}
#[test]
fn test_preprocess_re_replace_all_control_block() {
let input = "{% if reReplaceAll \"v\" Tag \"\" %}yes{% endif %}";
let result = preprocess(input);
assert_eq!(
result,
"{% if reReplaceAll(pattern=\"v\", input=Tag, replacement=\"\") %}yes{% endif %}"
);
}
#[test]
fn test_preprocess_in_piped() {
let input = "{{ myList | in \"val\" }}";
let result = preprocess(input);
assert_eq!(result, "{{ myList | in(value=\"val\") }}");
}
#[test]
fn test_preprocess_list_subexpr_escaped_double_quotes_error_loudly() {
use crate::template::{TemplateVars, render};
let input = r#"{{ in (list "hello \"world\"" "plain") "plain" }}"#;
assert_eq!(
preprocess(input),
r#"{{ in(items=(list "hello \"world\"" "plain"), value="plain") }}"#
);
assert!(render(input, &TemplateVars::new()).is_err());
}
#[test]
fn test_preprocess_list_subexpr_escaped_single_quotes_error_loudly() {
use crate::template::{TemplateVars, render};
let input = "{{ in (list 'it\\'s' 'fine') \"fine\" }}";
assert_eq!(preprocess(input), input);
assert!(render(input, &TemplateVars::new()).is_err());
}
#[test]
fn test_preprocess_list_subexpr_mixed_quote_styles() {
let input = "{{ in (list \"double\" 'single' \"another\") \"double\" }}";
let result = preprocess(input);
assert_eq!(
result,
"{{ in(items=[\"double\", 'single', \"another\"], value=\"double\") }}"
);
}
#[test]
fn test_preprocess_list_subexpr_with_bare_identifier() {
let input = "{{ in (list .Os \"windows\") \"linux\" }}";
let result = preprocess(input);
assert_eq!(result, "{{ in(items=[Os, \"windows\"], value=\"linux\") }}");
}
#[test]
fn test_preprocess_list_subexpr_with_dotted_path() {
let input = "{{ in (list .Env.FOO \"fallback\") \"val\" }}";
let result = preprocess(input);
assert_eq!(
result,
"{{ in(items=[Env.FOO, \"fallback\"], value=\"val\") }}"
);
}
#[test]
fn test_preprocess_list_subexpr_all_bare_identifiers() {
let input = "{{ in (list .Os .Arch) \"linux\" }}";
let result = preprocess(input);
assert_eq!(result, "{{ in(items=[Os, Arch], value=\"linux\") }}");
}
#[test]
fn test_preprocess_list_subexpr_mixed_vars_and_strings() {
let input = "{{ in (list .Os \"windows\" .Arch) \"test\" }}";
let result = preprocess(input);
assert_eq!(
result,
"{{ in(items=[Os, \"windows\", Arch], value=\"test\") }}"
);
}
#[test]
fn test_preprocess_now_format_go_style() {
let input = "{{ .Now.Format \"2006-01-02\" }}";
let result = preprocess(input);
assert_eq!(result, "{{ Now | now_format(format=\"2006-01-02\") }}");
}
#[test]
fn test_preprocess_now_format_no_dot_prefix() {
let input = "{{ Now.Format \"2006-01-02\" }}";
let result = preprocess(input);
assert_eq!(result, "{{ Now | now_format(format=\"2006-01-02\") }}");
}
#[test]
fn test_preprocess_now_format_with_time_pattern() {
let input = "{{ .Now.Format \"2006-01-02 15:04:05\" }}";
let result = preprocess(input);
assert_eq!(
result,
"{{ Now | now_format(format=\"2006-01-02 15:04:05\") }}"
);
}
#[test]
fn test_preprocess_now_format_single_quotes() {
let input = "{{ .Now.Format '2006-01-02' }}";
let result = preprocess(input);
assert_eq!(result, "{{ Now | now_format(format='2006-01-02') }}");
}
#[test]
fn test_preprocess_now_format_whitespace_control() {
let input = "{{- .Now.Format \"2006-01-02\" -}}";
let result = preprocess(input);
assert_eq!(result, "{{- Now | now_format(format=\"2006-01-02\") -}}");
}
#[test]
fn test_preprocess_now_format_compact() {
let input = "{{.Now.Format \"2006-01-02\"}}";
let result = preprocess(input);
assert_eq!(result, "{{Now | now_format(format=\"2006-01-02\")}}");
}
#[test]
fn test_preprocess_now_format_does_not_affect_other_blocks() {
let input = "{{ Version }} - {{ .Now.Format \"2006-01-02\" }}";
let result = preprocess(input);
assert_eq!(
result,
"{{ Version }} - {{ Now | now_format(format=\"2006-01-02\") }}"
);
}
#[test]
fn test_go_if_end() {
let input = "{{ if .IsSnapshot }}pre{{ end }}";
let result = preprocess(input);
assert_eq!(result, "{% if IsSnapshot %}pre{% endif %}");
}
#[test]
fn test_go_if_else_end() {
let input = "{{ if .IsSnapshot }}pre{{ else }}stable{{ end }}";
let result = preprocess(input);
assert_eq!(result, "{% if IsSnapshot %}pre{% else %}stable{% endif %}");
}
#[test]
fn test_go_if_else_if_end() {
let input =
"{{ if eq .Os \"windows\" }}win{{ else if eq .Os \"darwin\" }}mac{{ else }}linux{{ end }}";
let result = preprocess(input);
assert_eq!(
result,
"{% if Os == \"windows\" %}win{% elif Os == \"darwin\" %}mac{% else %}linux{% endif %}"
);
}
#[test]
fn test_go_range_bare() {
let input = "{{ range .Maintainers }}# {{ . }}{{ end }}";
let result = preprocess(input);
assert_eq!(
result,
"{% for val in Maintainers %}# {{ val }}{% endfor %}"
);
}
#[test]
fn test_go_range_with_variable() {
let input = "{{ range $release := .Packages }}{{ $release.Name }}{{ end }}";
let result = preprocess(input);
assert_eq!(
result,
"{% for release in Packages %}{{ release.Name }}{% endfor %}"
);
}
#[test]
fn test_go_range_kv() {
let input = "{{ range $key, $value := .Checksums }}{{ $value }} {{ $key }}{{ end }}";
let result = preprocess(input);
assert_eq!(
result,
"{% for key, value in Checksums %}{{ value }} {{ key }}{% endfor %}"
);
}
#[test]
fn test_go_with() {
let input = "{{ with .Arm }}v{{ . }}{{ end }}";
let result = preprocess(input);
assert_eq!(result, "{% if Arm %}v{{ Arm }}{% endif %}");
}
#[test]
fn test_go_var_assignment() {
let input = "{{ $m := map \"a\" \"1\" }}{{ index $m \"a\" }}";
let result = preprocess(input);
assert_eq!(
result,
"{% set m = map(pairs=[\"a\", \"1\"]) %}{{ index(collection=m, key=\"a\") }}"
);
}
#[test]
fn test_go_whitespace_trim() {
let input = "{{- if .Cond -}}yes{{- end -}}";
let result = preprocess(input);
assert_eq!(result, "{%- if Cond -%}yes{%- endif -%}");
}
#[test]
fn test_go_nested_if_range() {
let input = "{{ range .Items }}{{ if .Active }}*{{ end }}{{ end }}";
let result = preprocess(input);
assert_eq!(
result,
"{% for val in Items %}{% if Active %}*{% endif %}{% endfor %}"
);
}
#[test]
fn test_go_blocks_plain_expressions_unchanged() {
let input = "{{ .ProjectName }}_{{ .Version }}";
let result = preprocess(input);
assert_eq!(result, "{{ ProjectName }}_{{ Version }}");
}
#[test]
fn test_go_complex_nfpm_template() {
let input = "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}{{ with .Arm }}v{{ . }}{{ end }}{{ if not (eq .Amd64 \"v1\") }}{{ .Amd64 }}{{ end }}";
let result = preprocess(input);
assert_eq!(
result,
"{{ ProjectName }}_{{ Version }}_{{ Os }}_{{ Arch }}{% if Arm %}v{{ Arm }}{% endif %}{% if not Amd64 == \"v1\" %}{{ Amd64 }}{% endif %}"
);
}
#[test]
fn test_eq_in_if_block() {
let input = "{% if eq Os \"windows\" %}win{% endif %}";
let result = preprocess(input);
assert_eq!(result, "{% if Os == \"windows\" %}win{% endif %}");
}
#[test]
fn test_eq_variadic_three_args() {
let input = r#"{% if eq Os "linux" "darwin" %}unix{% endif %}"#;
let result = preprocess(input);
assert_eq!(
result,
r#"{% if Os == "linux" or Os == "darwin" %}unix{% endif %}"#
);
}
#[test]
fn test_eq_variadic_four_args() {
let input = r#"{% if eq Arch "amd64" "arm64" "386" %}supported{% endif %}"#;
let result = preprocess(input);
assert_eq!(
result,
r#"{% if Arch == "amd64" or Arch == "arm64" or Arch == "386" %}supported{% endif %}"#
);
}
#[test]
fn test_ne_in_if_block() {
let input = "{% if ne Os \"windows\" %}not-win{% endif %}";
let result = preprocess(input);
assert_eq!(result, "{% if Os != \"windows\" %}not-win{% endif %}");
}
#[test]
fn test_gt_in_if_block() {
let input = "{% if gt Major 1 %}gt1{% endif %}";
let result = preprocess(input);
assert_eq!(result, "{% if Major > 1 %}gt1{% endif %}");
}
#[test]
fn test_lt_in_if_block() {
let input = "{% if lt Minor 5 %}lt5{% endif %}";
let result = preprocess(input);
assert_eq!(result, "{% if Minor < 5 %}lt5{% endif %}");
}
#[test]
fn test_ge_in_if_block() {
let input = "{% if ge Patch 3 %}ge3{% endif %}";
let result = preprocess(input);
assert_eq!(result, "{% if Patch >= 3 %}ge3{% endif %}");
}
#[test]
fn test_le_in_if_block() {
let input = "{% if le Patch 3 %}le3{% endif %}";
let result = preprocess(input);
assert_eq!(result, "{% if Patch <= 3 %}le3{% endif %}");
}
#[test]
fn test_eq_with_string_literal() {
let input = "{% if eq Arch \"amd64\" %}yes{% endif %}";
let result = preprocess(input);
assert_eq!(result, "{% if Arch == \"amd64\" %}yes{% endif %}");
}
#[test]
fn test_eq_with_numeric_literal() {
let input = "{% if eq Major 1 %}v1{% endif %}";
let result = preprocess(input);
assert_eq!(result, "{% if Major == 1 %}v1{% endif %}");
}
#[test]
fn test_eq_parenthesized_not() {
let input = "{% if not (eq Os \"windows\") %}yes{% endif %}";
let result = preprocess(input);
assert_eq!(result, "{% if not Os == \"windows\" %}yes{% endif %}");
}
#[test]
fn test_eq_in_elif_block() {
let input = "{% if eq Os \"linux\" %}lin{% elif eq Os \"darwin\" %}mac{% endif %}";
let result = preprocess(input);
assert_eq!(
result,
"{% if Os == \"linux\" %}lin{% elif Os == \"darwin\" %}mac{% endif %}"
);
}
#[test]
fn test_eq_in_expression_block() {
let input = "{{ eq Os \"linux\" }}";
let result = preprocess(input);
assert_eq!(result, "{{ Os == \"linux\" }}");
}
#[test]
fn test_eq_with_already_stripped_dot_var() {
let input = "{% if eq Os \"windows\" %}win{% endif %}";
let result = preprocess(input);
assert_eq!(result, "{% if Os == \"windows\" %}win{% endif %}");
}
#[test]
fn test_eq_with_dotted_path() {
let input = "{% if eq Env.FOO \"bar\" %}yes{% endif %}";
let result = preprocess(input);
assert_eq!(result, "{% if Env.FOO == \"bar\" %}yes{% endif %}");
}
#[test]
fn test_and_prefix_to_infix() {
let input = "{% if and A B %}yes{% endif %}";
let result = preprocess(input);
assert_eq!(result, "{% if A and B %}yes{% endif %}");
}
#[test]
fn test_or_prefix_to_infix() {
let input = "{% if or A B %}yes{% endif %}";
let result = preprocess(input);
assert_eq!(result, "{% if A or B %}yes{% endif %}");
}
#[test]
fn test_and_with_parenthesized_or() {
let input = "{% if and A (or B C) %}yes{% endif %}";
let result = preprocess(input);
assert_eq!(result, "{% if A and (B or C) %}yes{% endif %}");
}
#[test]
fn test_or_with_parenthesized_eq() {
let input = "{% if or (eq Os \"linux\") (eq Os \"darwin\") %}yes{% endif %}";
let result = preprocess(input);
assert_eq!(
result,
"{% if Os == \"linux\" or Os == \"darwin\" %}yes{% endif %}"
);
}
#[test]
fn test_len_in_expression() {
let input = "{{ len Items }}";
let result = preprocess(input);
assert_eq!(result, "{{ Items | length }}");
}
#[test]
fn test_len_in_if_block() {
let input = "{% if len Items %}has items{% endif %}";
let result = preprocess(input);
assert_eq!(result, "{% if Items | length %}has items{% endif %}");
}
#[test]
fn test_len_with_dotted_path() {
let input = "{{ len Env.PATH }}";
let result = preprocess(input);
assert_eq!(result, "{{ Env.PATH | length }}");
}
#[test]
fn test_len_does_not_match_partial_word() {
let input = "{{ Items | length }}";
let result = preprocess(input);
assert_eq!(result, "{{ Items | length }}");
}
#[test]
fn test_map_positional_two_args() {
let input = "{{ map \"a\" \"1\" }}";
let result = preprocess(input);
assert_eq!(result, "{{ map(pairs=[\"a\", \"1\"]) }}");
}
#[test]
fn test_map_positional_four_args() {
let input = "{{ map \"a\" \"1\" \"b\" \"2\" }}";
let result = preprocess(input);
assert_eq!(result, "{{ map(pairs=[\"a\", \"1\", \"b\", \"2\"]) }}");
}
#[test]
fn test_map_named_args_unchanged() {
let input = "{{ map(pairs=[\"a\", \"1\"]) }}";
let result = preprocess(input);
assert_eq!(result, input);
}
#[test]
fn test_map_in_set_block() {
let input = "{% set m = map \"x\" \"y\" %}";
let result = preprocess(input);
assert_eq!(result, "{% set m = map(pairs=[\"x\", \"y\"]) %}");
}
#[test]
fn test_index_positional_two_args() {
let input = "{{ index myMap \"key\" }}";
let result = preprocess(input);
assert_eq!(result, "{{ index(collection=myMap, key=\"key\") }}");
}
#[test]
fn test_index_named_args_unchanged() {
let input = "{{ index(collection=myMap, key=\"key\") }}";
let result = preprocess(input);
assert_eq!(result, input);
}
#[test]
fn test_index_in_control_block() {
let input = "{% if index myMap \"key\" %}yes{% endif %}";
let result = preprocess(input);
assert_eq!(
result,
"{% if index(collection=myMap, key=\"key\") %}yes{% endif %}"
);
}
#[test]
fn test_go_style_full_pipeline_eq_and_map() {
let input = "{{ $m := map \"a\" \"1\" }}{{ if eq (index $m \"a\") \"1\" }}yes{{ end }}";
let result = preprocess(input);
assert_eq!(
result,
"{% set m = map(pairs=[\"a\", \"1\"]) %}\
{% if (index(collection=m, key=\"a\")) == \"1\" %}yes{% endif %}"
);
}
#[test]
fn test_preprocess_positional_time() {
let input = "{{ time \"2006-01-02\" }}";
let result = preprocess(input);
assert_eq!(result, "{{ time(format=\"2006-01-02\") }}");
}
#[test]
fn test_preprocess_slice_three_args() {
let input = "{{ slice Commit 0 7 }}";
let result = preprocess(input);
assert_eq!(result, "{{ Commit | slice(start=0, end=7) }}");
}
#[test]
fn test_preprocess_slice_two_args() {
let input = "{{ slice Commit 0 }}";
let result = preprocess(input);
assert_eq!(result, "{{ Commit | slice(start=0) }}");
}
#[test]
fn test_preprocess_slice_string_literal() {
let input = "{{ slice \"abcdefghij\" 0 7 }}";
let result = preprocess(input);
assert_eq!(result, "{{ \"abcdefghij\" | slice(start=0, end=7) }}");
}
#[test]
fn test_preprocess_slice_named_unchanged() {
let input = "{{ Commit | slice(start=0, end=7) }}";
let result = preprocess(input);
assert_eq!(result, input);
}
#[test]
fn test_preprocess_printf_variadic() {
let input = "{{ printf \"%04d\" Patch }}";
let result = preprocess(input);
assert_eq!(result, "{{ printf(format=\"%04d\", args=[Patch]) }}");
}
#[test]
fn test_preprocess_printf_multiple_args() {
let input = "{{ printf \"%s-%d\" Os Patch }}";
let result = preprocess(input);
assert_eq!(result, "{{ printf(format=\"%s-%d\", args=[Os, Patch]) }}");
}
#[test]
fn test_preprocess_printf_no_args() {
let input = "{{ printf \"literal\" }}";
let result = preprocess(input);
assert_eq!(result, "{{ printf(format=\"literal\", args=[]) }}");
}
#[test]
fn test_preprocess_print() {
let input = "{{ print \"a\" \"b\" }}";
let result = preprocess(input);
assert_eq!(result, "{{ print(args=[\"a\", \"b\"]) }}");
}
#[test]
fn test_preprocess_println() {
let input = "{{ println \"x\" }}";
let result = preprocess(input);
assert_eq!(result, "{{ println(args=[\"x\"]) }}");
}
#[test]
fn test_preprocess_printf_named_unchanged() {
let input = "{{ printf(format=\"%d\", args=[Patch]) }}";
let result = preprocess(input);
assert_eq!(result, input);
}
#[test]
fn test_preprocess_preserves_emoji_in_literal_text() {
let input = "Released with anodizer 🦀";
let result = preprocess(input);
assert!(result.contains('🦀'), "emoji must survive, got: {result:?}");
assert_eq!(result, input);
}
#[test]
fn test_preprocess_preserves_multibyte_mix_in_literal_text() {
let input = "café — 日本語 🚀 end";
let result = preprocess(input);
assert_eq!(result, input);
}
#[test]
fn test_preprocess_preserves_emoji_inside_block_string() {
let input = "{{ printf \"%s\" \"🦀\" }}";
let result = preprocess(input);
assert!(
result.contains('🦀'),
"emoji inside block string must survive, got: {result:?}"
);
}
#[test]
fn test_optional_index_dot_survives_go_leading_dot_strip() {
let input = "{{ Some?[0].Field or \"fallback\" }}";
let result = preprocess(input);
assert_eq!(result, "{{ Some?[0].Field or \"fallback\" }}");
}
#[test]
fn test_plain_index_dot_survives_go_leading_dot_strip() {
let input = "{{ Some[0].Field }}";
let result = preprocess(input);
assert_eq!(result, "{{ Some[0].Field }}");
}
#[test]
fn test_numeric_index_simple() {
assert_eq!(preprocess("{{ list.0 }}"), "{{ list[0] }}");
}
#[test]
fn test_numeric_index_multi_digit() {
assert_eq!(preprocess("{{ list.12 }}"), "{{ list[12] }}");
}
#[test]
fn test_numeric_index_then_field() {
assert_eq!(preprocess("{{ a.0.b }}"), "{{ a[0].b }}");
}
#[test]
fn test_numeric_index_chained_indices() {
assert_eq!(preprocess("{{ a.0.1 }}"), "{{ a[0][1] }}");
}
#[test]
fn test_numeric_index_after_bracket_index() {
assert_eq!(preprocess("{{ x[1].0 }}"), "{{ x[1][0] }}");
}
#[test]
fn test_numeric_index_optional_chaining() {
assert_eq!(preprocess("{{ a?.0 }}"), "{{ a?[0] }}");
}
#[test]
fn test_numeric_index_go_style_leading_dot() {
assert_eq!(
preprocess("{{ .Artifacts.0.Name }}"),
"{{ Artifacts[0].Name }}"
);
}
#[test]
fn test_numeric_index_in_statement_block() {
assert_eq!(
preprocess("{% if list.0 %}x{% endif %}"),
"{% if list[0] %}x{% endif %}"
);
}
#[test]
fn test_numeric_index_float_literal_untouched() {
assert_eq!(preprocess("{{ 1.0 }}"), "{{ 1.0 }}");
assert_eq!(preprocess("{{ 1.5 | round }}"), "{{ 1.5 | round }}");
}
#[test]
fn test_numeric_index_version_literal_untouched() {
assert_eq!(preprocess("{{ 1.2 + 0.5 }}"), "{{ 1.2 + 0.5 }}");
}
#[test]
fn test_numeric_index_inside_string_literal_untouched() {
assert_eq!(preprocess("{{ \"a.0\" }}"), "{{ \"a.0\" }}");
assert_eq!(
preprocess("{{ foo | replace(from=\"v1.0\", to=\"\") }}"),
"{{ foo | replace(from=\"v1.0\", to=\"\") }}"
);
}
#[test]
fn test_numeric_index_identifierish_segment_untouched() {
assert_eq!(preprocess("{{ a.0x }}"), "{{ a.0x }}");
}
#[test]
fn test_numeric_index_outside_blocks_untouched() {
assert_eq!(preprocess("v1.0 of {{ name }}"), "v1.0 of {{ name }}");
}
#[test]
fn test_numeric_index_end_to_end_render() {
use crate::template::{TemplateVars, render};
let mut vars = TemplateVars::new();
vars.set_structured("list", serde_json::json!(["first", "second"]));
vars.set_structured("items", serde_json::json!([{"name": "n0"}]));
assert_eq!(render("{{ list.0 }}", &vars).unwrap(), "first");
assert_eq!(render("{{ items.0.name }}", &vars).unwrap(), "n0");
assert_eq!(render("{{ list.1 | upper }}", &vars).unwrap(), "SECOND");
}
#[test]
fn test_backtick_string_skipped_by_numeric_index_pass() {
assert_eq!(preprocess("{{ `v1.0` }}"), "{{ `v1.0` }}");
}
#[test]
fn test_backtick_string_skipped_by_dollar_strip() {
assert_eq!(preprocess("{{ `$foo` }}"), "{{ `$foo` }}");
}
#[test]
fn test_backtick_string_skipped_by_dot_strip() {
assert_eq!(preprocess("{{ `.Field` }}"), "{{ `.Field` }}");
}
#[test]
fn test_backtick_filter_arg_content_untouched() {
let input = "{{ 'v1.0-x' | replace(from=`v1.0`, to=\"Z\") }}";
assert_eq!(preprocess(input), input);
}
#[test]
fn test_string_closes_at_first_quote_even_after_backslash() {
let input = r"{{ 'Q\' ~ 'v1.0' }}";
assert_eq!(preprocess(input), input);
}
#[test]
fn test_multiline_block_dot_strip_applies() {
assert_eq!(preprocess("{{\n .Version }}"), "{{\n Version }}");
}
#[test]
fn test_multiline_block_dollar_strip_applies() {
assert_eq!(preprocess("{{\n $foo }}"), "{{\n foo }}");
}
#[test]
fn test_multiline_block_builtin_rewrite_applies() {
assert_eq!(
preprocess("{% if eq Os \"linux\"\n%}yes{% endif %}"),
"{% if Os == \"linux\"\n%}yes{% endif %}"
);
}
#[test]
fn test_multiline_block_method_call_rewrite_applies() {
assert_eq!(
preprocess("{{\n .Now.Format \"2006-01-02\" }}"),
"{{\n Now | now_format(format=\"2006-01-02\") }}"
);
}
#[test]
fn test_multiline_block_numeric_index_rewrite_applies() {
assert_eq!(preprocess("{{\n list.0 }}"), "{{\n list[0] }}");
}
#[test]
fn every_registered_builtin_has_positional_handling_or_an_exemption() {
use super::positional::{NO_POSITIONAL_FORM, PREPROCESSED_ELSEWHERE, positional_builtin_names};
use std::collections::BTreeSet;
let registered = crate::template::registered_builtin_names();
let accounted_for: BTreeSet<&str> = positional_builtin_names()
.chain(PREPROCESSED_ELSEWHERE.iter().copied())
.chain(NO_POSITIONAL_FORM.iter().copied())
.collect();
let unhandled: Vec<&str> = registered.difference(&accounted_for).copied().collect();
assert!(
unhandled.is_empty(),
"registered builtin(s) with no Go positional form and no exemption: {unhandled:?}\n\
Add each to POSITIONAL_FUNCTIONS/UNARY_FUNCTIONS in \
template_preprocess/positional.rs, or — if it genuinely has no Go \
call form — to NO_POSITIONAL_FORM with the reason."
);
let stale: Vec<&str> = accounted_for.difference(®istered).copied().collect();
assert!(
stale.is_empty(),
"positional roster names(s) that are no longer registered builtins: {stale:?}"
);
}
#[test]
fn test_preprocess_positional_tolower_toupper() {
assert_eq!(
preprocess("{{ tolower \"ABC\" }}"),
"{{ tolower(s=\"ABC\") }}"
);
assert_eq!(preprocess("{{ toupper .Os }}"), "{{ toupper(s=Os) }}");
}
#[test]
fn test_preprocess_positional_trim_and_title() {
assert_eq!(
preprocess("{{ trim \" x \" }}"),
"{{ trim(s=\" x \") }}"
);
assert_eq!(
preprocess("{{ title \"hello world\" }}"),
"{{ title(s=\"hello world\") }}"
);
}
#[test]
fn test_preprocess_positional_trimprefix_trimsuffix() {
assert_eq!(
preprocess("{{ trimprefix .Tag \"v\" }}"),
"{{ trimprefix(s=Tag, prefix=\"v\") }}"
);
assert_eq!(
preprocess("{{ trimsuffix .Name \".exe\" }}"),
"{{ trimsuffix(s=Name, suffix=\".exe\") }}"
);
}
#[test]
fn test_preprocess_piped_trimprefix_trimsuffix() {
assert_eq!(
preprocess("{{ .Tag | trimprefix \"v\" }}"),
"{{ Tag | trimprefix(prefix=\"v\") }}"
);
assert_eq!(
preprocess("{{ .Name | trimsuffix \".exe\" }}"),
"{{ Name | trimsuffix(suffix=\".exe\") }}"
);
}
#[test]
fn test_zero_arg_pipes_are_left_alone() {
for template in [
"{{ Description | trim }}",
"{{ Description | title }}",
"{{ Version | incpatch }}",
"{{ Path | readFile }}",
] {
assert_eq!(preprocess(template), template);
}
}
#[test]
fn test_preprocess_positional_path_and_digest_builtins() {
assert_eq!(
preprocess("{{ dir \"a/b/c.txt\" }}"),
"{{ dir(s=\"a/b/c.txt\") }}"
);
assert_eq!(
preprocess("{{ base .ArtifactPath }}"),
"{{ base(s=ArtifactPath) }}"
);
assert_eq!(
preprocess("{{ sha256 .ArtifactPath }}"),
"{{ sha256(s=ArtifactPath) }}"
);
assert_eq!(
preprocess("{{ sha3_512 .ArtifactPath }}"),
"{{ sha3_512(s=ArtifactPath) }}"
);
}
#[test]
fn test_preprocess_positional_version_and_env_builtins() {
assert_eq!(
preprocess("{{ incpatch .Version }}"),
"{{ incpatch(v=Version) }}"
);
assert_eq!(
preprocess("{{ isEnvSet \"CI\" }}"),
"{{ isEnvSet(name=\"CI\") }}"
);
assert_eq!(
preprocess("{{ envOrDefault \"CI\" \"no\" }}"),
"{{ envOrDefault(name=\"CI\", default=\"no\") }}"
);
assert_eq!(
preprocess("{{ indexOrDefault .Env \"CI\" \"no\" }}"),
"{{ indexOrDefault(map=Env, key=\"CI\", default=\"no\") }}"
);
}
#[test]
fn test_preprocess_bare_list_call() {
assert_eq!(
preprocess("{{ list \"a\" \"b\" }}"),
"{{ list(items=[\"a\", \"b\"]) }}"
);
assert_eq!(preprocess("{{ list }}"), "{{ list }}");
assert_eq!(preprocess("{{ list.0 }}"), "{{ list[0] }}");
}
#[test]
fn test_control_block_uses_extended_roster() {
assert_eq!(
preprocess("{{ if isEnvSet \"CI\" }}yes{{ end }}"),
"{% if isEnvSet(name=\"CI\") %}yes{% endif %}"
);
}
#[test]
fn test_subexpr_argument_in_standalone_call() {
assert_eq!(
preprocess("{{ trimprefix (base .Path) \"v\" }}"),
"{{ trimprefix(s=(base(s=Path)), prefix=\"v\") }}"
);
assert_eq!(
preprocess("{{ indexOrDefault (map \"k\" \"v\") \"k\" \"d\" }}"),
"{{ indexOrDefault(map=(map(pairs=[\"k\", \"v\"])), key=\"k\", default=\"d\") }}"
);
}
#[test]
fn test_subexpr_argument_nests() {
assert_eq!(
preprocess("{{ trimprefix (base (dir .Path)) \"v\" }}"),
"{{ trimprefix(s=(base(s=(dir(s=Path)))), prefix=\"v\") }}"
);
assert_eq!(
preprocess("{{ toupper (trimprefix (base (dir .Path)) \"v\") }}"),
"{{ toupper(s=(trimprefix(s=(base(s=(dir(s=Path)))), prefix=\"v\"))) }}"
);
}
#[test]
fn test_subexpr_in_variadic_tail() {
assert_eq!(
preprocess("{{ printf \"%s-%s\" (tolower .Os) (base .Path) }}"),
"{{ printf(format=\"%s-%s\", args=[(tolower(s=Os)), (base(s=Path))]) }}"
);
assert_eq!(
preprocess("{{ print (tolower .Os) }}"),
"{{ print(args=[(tolower(s=Os))]) }}"
);
assert_eq!(
preprocess("{{ println (tolower .Os) }}"),
"{{ println(args=[(tolower(s=Os))]) }}"
);
assert_eq!(
preprocess("{{ list (tolower \"A\") (toupper \"b\") }}"),
"{{ list(items=[(tolower(s=\"A\")), (toupper(s=\"b\"))]) }}"
);
}
#[test]
fn test_subexpr_as_slice_item() {
assert_eq!(
preprocess("{{ slice (base .Path) 0 7 }}"),
"{{ (base(s=Path)) | slice(start=0, end=7) }}"
);
}
#[test]
fn test_subexpr_in_piped_call() {
assert_eq!(
preprocess("{{ .Version | replace (base .Path) \"-\" }}"),
"{{ Version | replace(from=(base(s=Path)), to=\"-\") }}"
);
assert_eq!(
preprocess("{{ (base .Path) | trimprefix \"v\" }}"),
"{{ (base(s=Path)) | trimprefix(prefix=\"v\") }}"
);
assert_eq!(
preprocess("{{ (base .Path) | upper }}"),
"{{ (base(s=Path)) | upper }}"
);
}
#[test]
fn test_subexpr_in_control_block() {
assert_eq!(
preprocess("{% if contains (tolower .Os) \"win\" %}W{% endif %}"),
"{% if contains(s=(tolower(s=Os)), substr=\"win\") %}W{% endif %}"
);
assert_eq!(
preprocess("{{ if contains (tolower .Os) \"win\" }}W{{ end }}"),
"{% if contains(s=(tolower(s=Os)), substr=\"win\") %}W{% endif %}"
);
assert_eq!(
preprocess("{% elif (isEnvSet \"CI\") %}C{% endif %}"),
"{% elif (isEnvSet(name=\"CI\")) %}C{% endif %}"
);
}
#[test]
fn test_subexpr_standing_alone() {
assert_eq!(preprocess("{{ (tolower .Os) }}"), "{{ (tolower(s=Os)) }}");
assert_eq!(
preprocess("{{ (tolower .Os) ~ \"-\" ~ (base .Path) }}"),
"{{ (tolower(s=Os)) ~ \"-\" ~ (base(s=Path)) }}"
);
}
#[test]
fn test_parens_inside_string_literals_do_not_nest() {
assert_eq!(
preprocess("{{ trimprefix (base \"x/(v9)\") \"(v\" }}"),
"{{ trimprefix(s=(base(s=\"x/(v9)\")), prefix=\"(v\") }}"
);
assert_eq!(
preprocess("{{ toupper (trimprefix \"a\\\" \"a\") }}"),
"{{ toupper(s=(trimprefix(s=\"a\\\", prefix=\"a\"))) }}"
);
}
#[test]
fn test_named_arg_calls_are_not_treated_as_subexpressions() {
for template in [
"{{ trimprefix(s=Path, prefix=\"v\") }}",
"{{ Version | replace(from=\"v\", to=\"\") }}",
"{{ trimprefix(s=base(s=Path), prefix=\"v\") }}",
"{{ printf(format=\"%s\", args=[tolower(s=Os)]) }}",
] {
assert_eq!(preprocess(template), template);
}
}
#[test]
fn test_every_rostered_builtin_accepts_a_subexpr_argument() {
use super::positional::positional_specs;
for (name, params) in positional_specs() {
let filler = vec!["\"x\""; params.len() - 1];
let mut call = format!("{{{{ {name} (tolower \"A\")");
for arg in &filler {
call.push(' ');
call.push_str(arg);
}
call.push_str(" }}");
let mut expected_params = vec![format!("{}=(tolower(s=\"A\"))", params[0])];
for (param, arg) in params[1..].iter().zip(filler.iter()) {
expected_params.push(format!("{param}={arg}"));
}
let expected = format!("{{{{ {name}({}) }}}}", expected_params.join(", "));
assert_eq!(preprocess(&call), expected, "builtin `{name}`");
}
}
#[test]
fn test_unclosed_subexpr_is_reported_not_rewritten() {
let input = "{{ trimprefix (base \"dist/v1\" \"v\" }}";
assert_eq!(preprocess(input), input);
let err = super::check_block_expressions(input)
.expect_err("an unclosed group must be rejected")
.to_string();
assert!(err.contains("1 unclosed `(`"), "{err}");
assert!(err.contains(input), "{err}");
assert!(
err.contains("trimprefix (base Path)"),
"hint missing: {err}"
);
}
#[test]
fn test_unmatched_close_paren_is_reported() {
let err = super::check_block_expressions("{{ trimprefix base \"v\") }}")
.expect_err("a stray `)` must be rejected")
.to_string();
assert!(err.contains("no matching `(`"), "{err}");
}
#[test]
fn test_multiple_unclosed_groups_are_counted() {
let err = super::check_block_expressions("{{ toupper (trimprefix (base .Path \"v\" }}")
.expect_err("two unclosed groups must be rejected")
.to_string();
assert!(err.contains("2 unclosed `(`"), "{err}");
}
#[test]
fn test_unterminated_string_literal_is_named_as_the_cause() {
let err = super::check_block_expressions("{{ base (trimprefix \"a\\\"b/v1\" \"a\") }}")
.expect_err("an unterminated literal must be rejected")
.to_string();
assert!(err.contains("unterminated string literal"), "{err}");
}
#[test]
fn test_balanced_expressions_pass_the_check() {
for template in [
"{{ Version }}",
"{{ trimprefix (base .Path) \"v\" }}",
"{{ trimprefix (base (dir .Path)) \"v\" }}",
"{{ trimprefix (base \"x/(v9)\") \"(v\" }}",
"{{ replace(s=Version, old=\"(\", new=\"\") }}",
"{{ \"a b\" }}",
"{% if contains (tolower .Os) \"win\" %}W{% endif %}",
] {
assert!(
super::check_block_expressions(template).is_ok(),
"rejected a balanced template: {template}"
);
}
}
#[test]
fn test_raw_blocks_are_exempt_from_the_balance_check() {
let raw = "{% raw %}{{ trimprefix (base \"x\" }}{% endraw %}";
assert!(
super::check_block_expressions(raw).is_ok(),
"raw block rejected"
);
let after = "{% raw %}ok{% endraw %}{{ trimprefix (base \"x\" }}";
assert!(super::check_block_expressions(after).is_err());
}
#[test]
fn test_imbalance_diagnostic_is_bounded_and_utf8_safe() {
let long = format!("{{{{ trimprefix (base \"{}\" }}}}", "日本語".repeat(80));
let err = super::check_block_expressions(&long)
.expect_err("still unbalanced")
.to_string();
assert!(std::str::from_utf8(err.as_bytes()).is_ok());
assert!(err.contains('…'), "long block must be truncated: {err}");
}
#[test]
fn test_positional_call_in_for_block() {
assert_eq!(
preprocess("{% for x in filter .Lines \"^v\" %}{{ x }}{% endfor %}"),
"{% for x in filter(items=Lines, regexp=\"^v\") %}{{ x }}{% endfor %}"
);
assert_eq!(
preprocess("{{ range filter .Lines \"^v\" }}{{ . }}{{ end }}"),
"{% for val in filter(items=Lines, regexp=\"^v\") %}{{ val }}{% endfor %}"
);
assert_eq!(
preprocess("{% for x in (filter .Lines \"^v\") %}{{ x }}{% endfor %}"),
"{% for x in (filter(items=Lines, regexp=\"^v\")) %}{{ x }}{% endfor %}"
);
assert_eq!(
preprocess("{% for k, v in filter .Lines \"^v\" %}{{ k }}{% endfor %}"),
"{% for k, v in filter(items=Lines, regexp=\"^v\") %}{{ k }}{% endfor %}"
);
}
#[test]
fn test_positional_call_in_set_block() {
assert_eq!(
preprocess("{{ $v := trimprefix .Tag \"v\" }}"),
"{% set v = trimprefix(s=Tag, prefix=\"v\") %}"
);
assert_eq!(
preprocess("{{ $v := printf \"%s-%s\" .Os .Arch }}"),
"{% set v = printf(format=\"%s-%s\", args=[Os, Arch]) %}"
);
assert_eq!(
preprocess("{{ $v := trimprefix (base .Path) \"v\" }}"),
"{% set v = trimprefix(s=(base(s=Path)), prefix=\"v\") %}"
);
}
#[test]
fn test_plain_for_and_set_expressions_are_untouched() {
for template in [
"{% for x in Tags %}{{ x }}{% endfor %}",
"{% for k, v in Env %}{{ k }}{% endfor %}",
"{% for x in Tags | reverse %}{{ x }}{% endfor %}",
"{% set v = Version %}",
"{% set v = trimprefix(s=Tag, prefix=\"v\") %}",
"{% endfor %}",
"{% else %}",
] {
assert_eq!(preprocess(template), template);
}
}
#[test]
fn test_raw_blocks_survive_every_pass_verbatim() {
for shape in [
"{{ if .X }}y{{ end }}", "{{ $v := .Tag }}", "{{ .Version }}", "{{ in (list \"a\" \"b\") .Os }}", "{{ eq .A .B }}", "{{ len .Tags }}", "{{ map \"k\" \"v\" }}", "{{ trimprefix .Tag \"v\" }}", "{{ trimprefix (base .Path) \"v\" }}", "{{ .Version | replace \"v\" \"\" }}", "{{ slice .Commit 0 7 }}", "{{ .Now.Format \"2006\" }}", "{{ list.0 }}", ] {
let template = format!("{{% raw %}}{shape}{{% endraw %}}");
assert_eq!(preprocess(&template), template, "shape: {shape}");
}
}
#[test]
fn test_raw_blocks_exempt_every_rostered_builtin() {
use super::positional::positional_specs;
for (name, params) in positional_specs() {
let args = vec!["\"x\""; params.len()].join(" ");
let template = format!("{{% raw %}}{{{{ {name} {args} }}}}{{% endraw %}}");
assert_eq!(preprocess(&template), template, "builtin `{name}`");
}
}
#[test]
fn test_rewrites_resume_outside_the_raw_span() {
assert_eq!(
preprocess("{{ .A }}{% raw %}{{ .B }}{% endraw %}{{ .C }}"),
"{{ A }}{% raw %}{{ .B }}{% endraw %}{{ C }}"
);
assert_eq!(
preprocess("{% raw %}{{ .A }}{% endraw %}{{ .B }}{% raw %}{{ .C }}{% endraw %}"),
"{% raw %}{{ .A }}{% endraw %}{{ B }}{% raw %}{{ .C }}{% endraw %}"
);
assert_eq!(
preprocess("{%- raw -%}{{ .A }}{%- endraw -%}{{ .B }}"),
"{%- raw -%}{{ .A }}{%- endraw -%}{{ B }}"
);
}
#[test]
fn test_unterminated_raw_covers_the_tail() {
assert_eq!(
preprocess("{{ .A }}{% raw %}{{ .B }}"),
"{{ A }}{% raw %}{{ .B }}"
);
}
#[test]
fn test_expression_blocks_do_not_delimit_a_raw_span() {
assert_eq!(
preprocess("{% raw %}{{ endraw }}{{ .B }}{% endraw %}{{ .C }}"),
"{% raw %}{{ endraw }}{{ .B }}{% endraw %}{{ C }}"
);
assert_eq!(preprocess("{{ raw }}{{ .B }}"), "{{ raw }}{{ B }}");
}
#[test]
fn test_positional_call_before_a_pipe() {
assert_eq!(
preprocess("{{ trimprefix .Tag \"v\" | upper }}"),
"{{ trimprefix(s=Tag, prefix=\"v\") | upper }}"
);
assert_eq!(
preprocess("{{ .Version | replace \"v\" \"\" | upper }}"),
"{{ Version | replace(from=\"v\", to=\"\") | upper }}"
);
assert_eq!(
preprocess("{{ list \"a\" \"b\" | join(sep=\" \") }}"),
"{{ list(items=[\"a\", \"b\"]) | join(sep=\" \") }}"
);
assert_eq!(
preprocess("{{ printf \"%s\" (tolower .Os) | upper }}"),
"{{ printf(format=\"%s\", args=[(tolower(s=Os))]) | upper }}"
);
assert_eq!(
preprocess("{{ slice .Commit 0 7 | upper }}"),
"{{ Commit | slice(start=0, end=7) | upper }}"
);
assert_eq!(
preprocess("{% for x in filter .Lines \"^v\" | reverse %}{{ x }}{% endfor %}"),
"{% for x in filter(items=Lines, regexp=\"^v\") | reverse %}{{ x }}{% endfor %}"
);
}
#[test]
fn test_every_pipeline_segment_is_rewritten() {
assert_eq!(
preprocess("{{ .Tag | trimprefix \"v\" | replace \".\" \"-\" | split \"-\" }}"),
"{{ Tag | trimprefix(prefix=\"v\") | replace(from=\".\", to=\"-\") | split(sep=\"-\") }}"
);
}
#[test]
fn test_pipe_inside_a_literal_or_subexpr_is_not_a_boundary() {
assert_eq!(
preprocess("{{ replace .Tag \"|\" \"-\" }}"),
"{{ replace(s=Tag, old=\"|\", new=\"-\") }}"
);
assert_eq!(
preprocess("{{ trimprefix (replace .Tag \"|\" \"-\") \"v\" }}"),
"{{ trimprefix(s=(replace(s=Tag, old=\"|\", new=\"-\")), prefix=\"v\") }}"
);
assert_eq!(
preprocess("{{ (replace .Tag \"a|b\" \"-\") | upper }}"),
"{{ (replace(s=Tag, old=\"a|b\", new=\"-\")) | upper }}"
);
}
fn nested_calls(n: usize) -> String {
format!("{{{{ {}\"A\"{} }}}}", "tolower (".repeat(n), ")".repeat(n))
}
#[test]
fn test_expression_nesting_is_capped_with_a_named_diagnostic() {
let limit = super::MAX_EXPR_NESTING;
super::check_block_expressions(&nested_calls(limit))
.expect("the limit itself must be accepted");
let err = super::check_block_expressions(&nested_calls(limit + 1))
.expect_err("one level past the limit must be rejected")
.to_string();
assert!(err.contains("over-nested expression in template"), "{err}");
assert!(
err.contains(&format!(
"parentheses nest {} deep, past the limit of {limit}",
limit + 1
)),
"{err}"
);
assert!(err.contains("{{ tolower (tolower"), "{err}");
assert!(err.contains('…'), "long block must be truncated: {err}");
}
#[test]
fn test_the_deepest_accepted_nesting_is_fully_rewritten() {
let at_limit = nested_calls(super::MAX_EXPR_NESTING);
super::check_block_expressions(&at_limit).expect("the limit must be accepted");
let out = preprocess(&at_limit);
assert!(!out.contains("tolower ("), "a Go call survived the rewrite");
assert_eq!(out.matches("tolower(s=").count(), super::MAX_EXPR_NESTING);
}
#[test]
fn test_preprocess_stops_descending_past_the_cap() {
let past_limit = nested_calls(super::MAX_EXPR_NESTING + 20);
let out = preprocess(&past_limit);
assert_eq!(
out.matches("tolower(s=").count(),
super::MAX_EXPR_NESTING + 1
);
assert!(
out.contains("tolower ("),
"the tail past the cap must stay verbatim"
);
}