2026-01-16 11:22:06 +01:00
#include <string>
#include <iostream>
#include <random>
#include <cstdlib>
#include <nlohmann/json.hpp>
2026-01-18 08:14:27 +01:00
#include <sheredom/subprocess.h>
2026-01-16 11:22:06 +01:00
#include "jinja/runtime.h"
#include "jinja/parser.h"
#include "jinja/lexer.h"
2026-01-27 19:50:42 +01:00
#include "jinja/utils.h"
2026-01-16 11:22:06 +01:00
#include "testing.h"
using json = nlohmann :: ordered_json ;
static void test_template ( testing & t , const std :: string & name , const std :: string & tmpl , const json & vars , const std :: string & expect );
static void test_whitespace_control ( testing & t );
static void test_conditionals ( testing & t );
static void test_loops ( testing & t );
static void test_expressions ( testing & t );
static void test_set_statement ( testing & t );
static void test_filters ( testing & t );
static void test_literals ( testing & t );
static void test_comments ( testing & t );
static void test_macros ( testing & t );
static void test_namespace ( testing & t );
static void test_tests ( testing & t );
static void test_string_methods ( testing & t );
static void test_array_methods ( testing & t );
static void test_object_methods ( testing & t );
2026-01-27 19:50:42 +01:00
static void test_hasher ( testing & t );
2026-02-22 21:08:23 +01:00
static void test_stats ( testing & t );
2026-01-16 11:22:06 +01:00
static void test_fuzzing ( testing & t );
2026-01-18 08:14:27 +01:00
static bool g_python_mode = false ;
2026-01-16 11:22:06 +01:00
int main ( int argc , char * argv []) {
testing t ( std :: cout );
t . verbose = true ;
2026-01-18 08:14:27 +01:00
// usage: test-jinja [-py] [filter_regex]
// -py : enable python mode (use python jinja2 for rendering expected output)
// only use this for cross-checking, not for correctness
// note: the implementation of this flag is basic, only intented to be used by maintainers
for ( int i = 1 ; i < argc ; i ++ ) {
std :: string arg = argv [ i ];
if ( arg == "-py" ) {
g_python_mode = true ;
} else {
t . set_filter ( arg );
}
2026-01-16 11:22:06 +01:00
}
t . test ( "whitespace control" , test_whitespace_control );
t . test ( "conditionals" , test_conditionals );
t . test ( "loops" , test_loops );
t . test ( "expressions" , test_expressions );
t . test ( "set statement" , test_set_statement );
t . test ( "filters" , test_filters );
t . test ( "literals" , test_literals );
t . test ( "comments" , test_comments );
t . test ( "macros" , test_macros );
t . test ( "namespace" , test_namespace );
t . test ( "tests" , test_tests );
t . test ( "string methods" , test_string_methods );
t . test ( "array methods" , test_array_methods );
t . test ( "object methods" , test_object_methods );
2026-01-18 08:14:27 +01:00
if ( ! g_python_mode ) {
2026-01-27 19:50:42 +01:00
t . test ( "hasher" , test_hasher );
2026-02-22 21:08:23 +01:00
t . test ( "stats" , test_stats );
2026-01-18 08:14:27 +01:00
t . test ( "fuzzing" , test_fuzzing );
}
2026-01-16 11:22:06 +01:00
return t . summary ();
}
static void test_whitespace_control ( testing & t ) {
test_template ( t , "trim_blocks removes newline after tag" ,
"{% if true %} \n "
"hello \n "
"{% endif %} \n " ,
json :: object (),
"hello \n "
);
test_template ( t , "lstrip_blocks removes leading whitespace" ,
" {% if true %} \n "
" hello \n "
" {% endif %} \n " ,
json :: object (),
" hello \n "
);
test_template ( t , "for loop with trim_blocks" ,
"{% for i in items %} \n "
"{{ i }} \n "
"{% endfor %} \n " ,
{{ "items" , json :: array ({ 1 , 2 , 3 })}},
"1 \n 2 \n 3 \n "
);
test_template ( t , "explicit strip both" ,
" {%- if true -%} \n "
"hello \n "
" {%- endif -%} \n " ,
json :: object (),
"hello"
);
test_template ( t , "expression whitespace control" ,
" {{- 'hello' -}} \n " ,
json :: object (),
"hello"
);
test_template ( t , "inline block no newline" ,
"{% if true %}yes{% endif %}" ,
json :: object (),
"yes"
);
}
static void test_conditionals ( testing & t ) {
test_template ( t , "if true" ,
"{% if cond %}yes{% endif %}" ,
{{ "cond" , true }},
"yes"
);
test_template ( t , "if false" ,
"{% if cond %}yes{% endif %}" ,
{{ "cond" , false }},
""
);
test_template ( t , "if else" ,
"{% if cond %}yes{% else %}no{% endif %}" ,
{{ "cond" , false }},
"no"
);
test_template ( t , "if elif else" ,
"{% if a %}A{% elif b %}B{% else %}C{% endif %}" ,
{{ "a" , false }, { "b" , true }},
"B"
);
test_template ( t , "nested if" ,
"{% if outer %}{% if inner %}both{% endif %}{% endif %}" ,
{{ "outer" , true }, { "inner" , true }},
"both"
);
test_template ( t , "comparison operators" ,
"{% if x > 5 %}big{% endif %}" ,
{{ "x" , 10 }},
"big"
);
2026-01-27 19:50:42 +01:00
test_template ( t , "object comparison" ,
"{% if {0: 1, none: 2, 1.0: 3, '0': 4, true: 5} == {false: 1, none: 2, 1: 5, '0': 4} %}equal{% endif %}" ,
json :: object (),
"equal"
);
test_template ( t , "array comparison" ,
"{% if [0, 1.0, false] == [false, 1, 0.0] %}equal{% endif %}" ,
json :: object (),
"equal"
);
2026-01-16 11:22:06 +01:00
test_template ( t , "logical and" ,
"{% if a and b %}both{% endif %}" ,
{{ "a" , true }, { "b" , true }},
"both"
);
test_template ( t , "logical or" ,
"{% if a or b %}either{% endif %}" ,
{{ "a" , false }, { "b" , true }},
"either"
);
test_template ( t , "logical not" ,
"{% if not a %}negated{% endif %}" ,
{{ "a" , false }},
"negated"
);
2026-02-02 12:00:55 -08:00
test_template ( t , "in operator (element in array)" ,
2026-01-16 11:22:06 +01:00
"{% if 'x' in items %}found{% endif %}" ,
{{ "items" , json :: array ({ "x" , "y" })}},
"found"
);
2026-02-02 12:00:55 -08:00
test_template ( t , "in operator (substring)" ,
"{% if 'bc' in 'abcd' %}found{% endif %}" ,
json :: object (),
"found"
);
test_template ( t , "in operator (object key)" ,
"{% if 'key' in obj %}found{% endif %}" ,
{{ "obj" , {{ "key" , 1 }, { "other" , 2 }}}},
"found"
);
2026-01-16 11:22:06 +01:00
test_template ( t , "is defined" ,
"{% if x is defined %}yes{% else %}no{% endif %}" ,
{{ "x" , 1 }},
"yes"
);
test_template ( t , "is not defined" ,
"{% if y is not defined %}yes{% else %}no{% endif %}" ,
json :: object (),
"yes"
);
2026-01-19 20:29:43 +01:00
test_template ( t , "is undefined falsy" ,
"{{ 'yes' if not y else 'no' }}" ,
json :: object (),
"yes"
);
test_template ( t , "is undefined attribute falsy" ,
"{{ 'yes' if not y.x else 'no' }}" ,
{{ "y" , true }},
"yes"
);
test_template ( t , "is undefined key falsy" ,
"{{ 'yes' if not y['x'] else 'no' }}" ,
{{ "y" , {{}}}},
"yes"
);
test_template ( t , "is empty array falsy" ,
"{{ 'yes' if not y else 'no' }}" ,
{{ "y" , json :: array ()}},
"yes"
);
test_template ( t , "is empty object falsy" ,
"{{ 'yes' if not y else 'no' }}" ,
{{ "y" , json :: object ()}},
"yes"
);
test_template ( t , "is empty string falsy" ,
"{{ 'yes' if not y else 'no' }}" ,
{{ "y" , "" }},
"yes"
);
test_template ( t , "is 0 falsy" ,
"{{ 'yes' if not y else 'no' }}" ,
{{ "y" , 0 }},
"yes"
);
test_template ( t , "is 0.0 falsy" ,
"{{ 'yes' if not y else 'no' }}" ,
{{ "y" , 0.0 }},
"yes"
);
test_template ( t , "is non-empty array truthy" ,
"{{ 'yes' if y else 'no' }}" ,
{{ "y" , json :: array ({ "" })}},
"yes"
);
test_template ( t , "is non-empty object truthy" ,
"{{ 'yes' if y else 'no' }}" ,
{{ "y" , { "x" , false }}},
"yes"
);
test_template ( t , "is non-empty string truthy" ,
"{{ 'yes' if y else 'no' }}" ,
{{ "y" , "0" }},
"yes"
);
test_template ( t , "is 1 truthy" ,
"{{ 'yes' if y else 'no' }}" ,
{{ "y" , 1 }},
"yes"
);
test_template ( t , "is 1.0 truthy" ,
"{{ 'yes' if y else 'no' }}" ,
{{ "y" , 1.0 }},
"yes"
);
2026-01-16 11:22:06 +01:00
}
static void test_loops ( testing & t ) {
test_template ( t , "simple for" ,
"{% for i in items %}{{ i }}{% endfor %}" ,
{{ "items" , json :: array ({ 1 , 2 , 3 })}},
"123"
);
test_template ( t , "loop.index" ,
"{% for i in items %}{{ loop.index }}{% endfor %}" ,
{{ "items" , json :: array ({ "a" , "b" , "c" })}},
"123"
);
test_template ( t , "loop.index0" ,
"{% for i in items %}{{ loop.index0 }}{% endfor %}" ,
{{ "items" , json :: array ({ "a" , "b" , "c" })}},
"012"
);
test_template ( t , "loop.first and loop.last" ,
"{% for i in items %}{% if loop.first %}[{% endif %}{{ i }}{% if loop.last %}]{% endif %}{% endfor %}" ,
{{ "items" , json :: array ({ 1 , 2 , 3 })}},
"[123]"
);
test_template ( t , "loop.length" ,
"{% for i in items %}{{ loop.length }}{% endfor %}" ,
{{ "items" , json :: array ({ "a" , "b" })}},
"22"
);
test_template ( t , "for over dict items" ,
"{% for k, v in data.items() %}{{ k }}={{ v }} {% endfor %}" ,
{{ "data" , {{ "x" , 1 }, { "y" , 2 }}}},
"x=1 y=2 "
);
test_template ( t , "for else empty" ,
"{% for i in items %}{{ i }}{% else %}empty{% endfor %}" ,
{{ "items" , json :: array ()}},
"empty"
);
2026-01-28 14:40:29 +01:00
test_template ( t , "for undefined empty" ,
"{% for i in items %}{{ i }}{% else %}empty{% endfor %}" ,
json :: object (),
"empty"
);
2026-01-16 11:22:06 +01:00
test_template ( t , "nested for" ,
"{% for i in a %}{% for j in b %}{{ i }}{{ j }}{% endfor %}{% endfor %}" ,
{{ "a" , json :: array ({ 1 , 2 })}, { "b" , json :: array ({ "x" , "y" })}},
"1x1y2x2y"
);
test_template ( t , "for with range" ,
"{% for i in range(3) %}{{ i }}{% endfor %}" ,
json :: object (),
"012"
);
}
static void test_expressions ( testing & t ) {
test_template ( t , "simple variable" ,
"{{ x }}" ,
{{ "x" , 42 }},
"42"
);
test_template ( t , "dot notation" ,
"{{ user.name }}" ,
{{ "user" , {{ "name" , "Bob" }}}},
"Bob"
);
2026-01-18 00:57:51 +01:00
test_template ( t , "negative float (not dot notation)" ,
"{{ -1.0 }}" ,
json :: object (),
"-1.0"
);
2026-01-16 11:22:06 +01:00
test_template ( t , "bracket notation" ,
"{{ user['name'] }}" ,
{{ "user" , {{ "name" , "Bob" }}}},
"Bob"
);
2026-03-30 14:08:46 -04:00
test_template ( t , "empty computed member defaults to undefined" ,
"{{ a[]|default('fallback') }}" ,
{{ "a" , {{ "name" , "Bob" }}}},
"fallback"
);
test_template ( t , "empty computed member is undefined" ,
"{{ a[] is undefined }}" ,
{{ "a" , {{ "name" , "Bob" }}}},
"True"
);
test_template ( t , "undefined computed member is undefined" ,
"{{ a[undefined] is undefined }}" ,
{{ "a" , {{ "name" , "Bob" }}}},
"True"
);
2026-01-16 11:22:06 +01:00
test_template ( t , "array access" ,
"{{ items[1] }}" ,
{{ "items" , json :: array ({ "a" , "b" , "c" })}},
"b"
);
2026-01-27 19:50:42 +01:00
test_template ( t , "array negative access" ,
"{{ items[-1] }}" ,
{{ "items" , json :: array ({ "a" , "b" , "c" })}},
"c"
);
test_template ( t , "array slice" ,
"{{ items[1:-1]|string }}" ,
{{ "items" , json :: array ({ "a" , "b" , "c" })}},
"['b']"
);
test_template ( t , "array slice step" ,
"{{ items[::2]|string }}" ,
{{ "items" , json :: array ({ "a" , "b" , "c" })}},
"['a', 'c']"
);
test_template ( t , "tuple slice" ,
"{{ ('a', 'b', 'c')[::-1]|string }}" ,
json :: object (),
"('c', 'b', 'a')"
);
2026-01-16 11:22:06 +01:00
test_template ( t , "arithmetic" ,
"{{ (a + b) * c }}" ,
{{ "a" , 2 }, { "b" , 3 }, { "c" , 4 }},
"20"
);
test_template ( t , "string concat ~" ,
"{{ 'hello' ~ ' ' ~ 'world' }}" ,
json :: object (),
"hello world"
);
2026-04-09 17:28:33 +08:00
test_template ( t , "string repetition" ,
"{{ 'ab' * 3 }}" ,
json :: object (),
"ababab"
);
test_template ( t , "reversed string repetition" ,
"{{ 3 * 'ab' }}" ,
json :: object (),
"ababab"
);
2026-01-16 11:22:06 +01:00
test_template ( t , "ternary" ,
"{{ 'yes' if cond else 'no' }}" ,
{{ "cond" , true }},
"yes"
);
}
static void test_set_statement ( testing & t ) {
test_template ( t , "simple set" ,
"{% set x = 5 %}{{ x }}" ,
json :: object (),
"5"
);
test_template ( t , "set with expression" ,
"{% set x = a + b %}{{ x }}" ,
{{ "a" , 10 }, { "b" , 20 }},
"30"
);
test_template ( t , "set list" ,
"{% set items = [1, 2, 3] %}{{ items|length }}" ,
json :: object (),
"3"
);
test_template ( t , "set dict" ,
"{% set d = {'a': 1} %}{{ d.a }}" ,
json :: object (),
"1"
);
2026-01-27 19:50:42 +01:00
test_template ( t , "set dict with mixed type keys" ,
"{% set d = {0: 1, none: 2, 1.0: 3, '0': 4, (0, 0): 5, false: 6, 1: 7} %}{{ d[(0, 0)] + d[0] + d[none] + d['0'] + d[false] + d[1.0] + d[1] }}" ,
json :: object (),
"37"
);
test_template ( t , "print dict with mixed type keys" ,
"{% set d = {0: 1, none: 2, 1.0: 3, '0': 4, (0, 0): 5, true: 6} %}{{ d|string }}" ,
json :: object (),
"{0: 1, None: 2, 1.0: 6, '0': 4, (0, 0): 5}"
);
test_template ( t , "print array with mixed types" ,
"{% set d = [0, none, 1.0, '0', true, (0, 0)] %}{{ d|string }}" ,
json :: object (),
"[0, None, 1.0, '0', True, (0, 0)]"
);
test_template ( t , "object member assignment with mixed key types" ,
"{% set d = namespace() %}{% set d.a = 123 %}{{ d['a'] == 123 }}" ,
json :: object (),
"True"
);
test_template ( t , "tuple unpacking" ,
"{% set t = (1, 2, 3) %}{% set a, b, c = t %}{{ a + b + c }}" ,
json :: object (),
"6"
);
2026-01-16 11:22:06 +01:00
}
static void test_filters ( testing & t ) {
test_template ( t , "upper" ,
"{{ 'hello'|upper }}" ,
json :: object (),
"HELLO"
);
test_template ( t , "lower" ,
"{{ 'HELLO'|lower }}" ,
json :: object (),
"hello"
);
2026-04-03 15:03:33 +02:00
test_template ( t , "upper array" ,
"{{ items|upper }}" ,
{{ "items" , json :: array ({ "hello" , "world" })}},
"['HELLO', 'WORLD']"
);
test_template ( t , "upper dict" ,
"{{ items|upper }}" ,
{{ "items" , {{ "hello" , "world" }}}},
"{'HELLO': 'WORLD'}"
);
2026-01-16 11:22:06 +01:00
test_template ( t , "capitalize" ,
"{{ 'heLlo World'|capitalize }}" ,
json :: object (),
"Hello world"
);
test_template ( t , "title" ,
"{{ 'hello world'|title }}" ,
json :: object (),
"Hello World"
);
test_template ( t , "trim" ,
"{{ ' \r\n\t hello \t\n\r '|trim }}" ,
json :: object (),
"hello"
);
test_template ( t , "trim chars" ,
"{{ 'xyxhelloxyx'|trim('xy') }}" ,
json :: object (),
"hello"
);
test_template ( t , "length string" ,
"{{ 'hello'|length }}" ,
json :: object (),
"5"
);
test_template ( t , "replace" ,
"{{ 'hello world'|replace('world', 'jinja') }}" ,
json :: object (),
"hello jinja"
);
test_template ( t , "length list" ,
"{{ items|length }}" ,
{{ "items" , json :: array ({ 1 , 2 , 3 })}},
"3"
);
test_template ( t , "first" ,
"{{ items|first }}" ,
{{ "items" , json :: array ({ 10 , 20 , 30 })}},
"10"
);
test_template ( t , "last" ,
"{{ items|last }}" ,
{{ "items" , json :: array ({ 10 , 20 , 30 })}},
"30"
);
test_template ( t , "reverse" ,
"{% for i in items|reverse %}{{ i }}{% endfor %}" ,
{{ "items" , json :: array ({ 1 , 2 , 3 })}},
"321"
);
test_template ( t , "sort" ,
"{% for i in items|sort %}{{ i }}{% endfor %}" ,
{{ "items" , json :: array ({ 3 , 1 , 2 })}},
"123"
);
2026-01-18 02:53:01 +01:00
test_template ( t , "sort reverse" ,
"{% for i in items|sort(true) %}{{ i }}{% endfor %}" ,
{{ "items" , json :: array ({ 3 , 1 , 2 })}},
"321"
);
test_template ( t , "sort with attribute" ,
"{{ items|sort(attribute='name')|join(attribute='age') }}" ,
{{ "items" , json :: array ({
json ({{ "name" , "c" }, { "age" , 3 }}),
json ({{ "name" , "a" }, { "age" , 1 }}),
json ({{ "name" , "b" }, { "age" , 2 }}),
})}},
"123"
);
test_template ( t , "sort with numeric attribute" ,
"{{ items|sort(attribute=0)|join(attribute=1) }}" ,
{{ "items" , json :: array ({
json :: array ({ 3 , "z" }),
json :: array ({ 1 , "x" }),
json :: array ({ 2 , "y" }),
})}},
"xyz"
);
2026-01-16 11:22:06 +01:00
test_template ( t , "join" ,
"{{ items|join(', ') }}" ,
{{ "items" , json :: array ({ "a" , "b" , "c" })}},
"a, b, c"
);
test_template ( t , "join default separator" ,
"{{ items|join }}" ,
{{ "items" , json :: array ({ "x" , "y" , "z" })}},
"xyz"
);
test_template ( t , "abs" ,
"{{ -5|abs }}" ,
json :: object (),
"5"
);
test_template ( t , "int from string" ,
"{{ '42'|int }}" ,
json :: object (),
"42"
);
test_template ( t , "int from string with default" ,
"{{ ''|int(1) }}" ,
json :: object (),
"1"
);
test_template ( t , "int from string with base" ,
"{{ '11'|int(base=2) }}" ,
json :: object (),
"3"
);
test_template ( t , "float from string" ,
"{{ '3.14'|float }}" ,
json :: object (),
"3.14"
);
test_template ( t , "default with value" ,
"{{ x|default('fallback') }}" ,
{{ "x" , "actual" }},
"actual"
);
test_template ( t , "default without value" ,
"{{ y|default('fallback') }}" ,
json :: object (),
"fallback"
);
test_template ( t , "default with falsy value" ,
"{{ ''|default('fallback', true) }}" ,
json :: object (),
"fallback"
);
test_template ( t , "tojson ensure_ascii=true" ,
"{{ data|tojson(ensure_ascii=true) }}" ,
{{ "data" , " \u2713 " }},
" \"\\ u2713 \" "
);
2026-04-09 17:28:33 +08:00
test_template ( t , "tojson ensure_ascii=true nested object" ,
"{{ data|tojson(ensure_ascii=true) }}" ,
{{ "data" , {
{ "text" , " \u2713 " },
{ "items" , json :: array ({ "é" , {{ "snowman" , "☃" }}})}
}}},
"{ \" text \" : \"\\ u2713 \" , \" items \" : [ \"\\ u00e9 \" , { \" snowman \" : \"\\ u2603 \" }]}"
);
test_template ( t , "tojson ensure_ascii=true indent=2" ,
"{{ data|tojson(ensure_ascii=true, indent=2) }}" ,
{{ "data" , {
{ "text" , " \u2713 " },
{ "nested" , {{ "accent" , "é" }}}
}}},
"{ \n \" text \" : \"\\ u2713 \" , \n \" nested \" : { \n \" accent \" : \"\\ u00e9 \"\n } \n }"
);
test_template ( t , "tojson ensure_ascii=true preserves existing escapes" ,
"{{ data|tojson(ensure_ascii=true) }}" ,
{{ "data" , {
{ "emoji" , "😀" },
{ "line" , "a \n b" }
}}},
"{ \" emoji \" : \"\\ ud83d \\ ude00 \" , \" line \" : \" a \\ nb \" }"
);
2026-01-16 11:22:06 +01:00
test_template ( t , "tojson sort_keys=true" ,
"{{ data|tojson(sort_keys=true) }}" ,
{{ "data" , {{ "b" , 2 }, { "a" , 1 }}}},
"{ \" a \" : 1, \" b \" : 2}"
);
test_template ( t , "tojson" ,
"{{ data|tojson }}" ,
{{ "data" , {{ "a" , 1 }, { "b" , json :: array ({ 1 , 2 })}}}},
"{ \" a \" : 1, \" b \" : [1, 2]}"
);
test_template ( t , "tojson indent=4" ,
"{{ data|tojson(indent=4) }}" ,
{{ "data" , {{ "a" , 1 }, { "b" , json :: array ({ 1 , 2 })}}}},
"{ \n \" a \" : 1, \n \" b \" : [ \n 1, \n 2 \n ] \n }"
);
test_template ( t , "tojson separators=(',',':')" ,
"{{ data|tojson(separators=(',',':')) }}" ,
{{ "data" , {{ "a" , 1 }, { "b" , json :: array ({ 1 , 2 })}}}},
"{ \" a \" :1, \" b \" :[1,2]}"
);
test_template ( t , "tojson separators=(',',': ') indent=2" ,
"{{ data|tojson(separators=(',',': '), indent=2) }}" ,
{{ "data" , {{ "a" , 1 }, { "b" , json :: array ({ 1 , 2 })}}}},
"{ \n \" a \" : 1, \n \" b \" : [ \n 1, \n 2 \n ] \n }"
);
2026-02-19 00:25:52 +01:00
test_template ( t , "indent" ,
"{{ data|indent(2) }}" ,
{{ "data" , "foo \n bar" }},
"foo \n bar"
);
test_template ( t , "indent first only" ,
"{{ data|indent(width=3,first=true) }}" ,
{{ "data" , "foo \n bar" }},
" foo \n bar"
);
test_template ( t , "indent blank lines and first line" ,
"{{ data|indent(width=5,blank=true,first=true) }}" ,
{{ "data" , "foo \n\n bar" }},
" foo \n \n bar"
);
test_template ( t , "indent with default width" ,
"{{ data|indent() }}" ,
{{ "data" , "foo \n bar" }},
"foo \n bar"
);
test_template ( t , "indent with no newline" ,
"{{ data|indent }}" ,
{{ "data" , "foo" }},
"foo"
);
test_template ( t , "indent with trailing newline" ,
"{{ data|indent(blank=true) }}" ,
{{ "data" , "foo \n " }},
"foo \n "
);
test_template ( t , "indent with string" ,
"{{ data|indent(width='>>>>') }}" ,
{{ "data" , "foo \n bar" }},
"foo \n >>>>bar"
);
2026-01-16 11:22:06 +01:00
test_template ( t , "chained filters" ,
"{{ ' HELLO '|trim|lower }}" ,
json :: object (),
"hello"
);
2026-01-21 19:24:37 +01:00
2026-04-09 17:28:33 +08:00
test_template ( t , "int filter on integer is identity" ,
"{{ value|int }}" ,
{{ "value" , 7 }},
"7"
);
2026-01-21 19:24:37 +01:00
test_template ( t , "none to string" ,
"{{ x|string }}" ,
{{ "x" , nullptr }},
"None"
);
2026-01-16 11:22:06 +01:00
}
static void test_literals ( testing & t ) {
test_template ( t , "integer" ,
"{{ 42 }}" ,
json :: object (),
"42"
);
test_template ( t , "float" ,
"{{ 3.14 }}" ,
json :: object (),
"3.14"
);
test_template ( t , "string" ,
"{{ 'hello' }}" ,
json :: object (),
"hello"
);
test_template ( t , "boolean true" ,
"{{ true }}" ,
json :: object (),
"True"
);
test_template ( t , "boolean false" ,
"{{ false }}" ,
json :: object (),
"False"
);
test_template ( t , "none" ,
"{% if x is none %}null{% endif %}" ,
{{ "x" , nullptr }},
"null"
);
test_template ( t , "list literal" ,
"{% for i in [1, 2, 3] %}{{ i }}{% endfor %}" ,
json :: object (),
"123"
);
test_template ( t , "dict literal" ,
"{% set d = {'a': 1} %}{{ d.a }}" ,
json :: object (),
"1"
);
2026-01-18 01:05:09 +01:00
test_template ( t , "integer|abs" ,
"{{ -42 | abs }}" ,
json :: object (),
"42"
);
test_template ( t , "integer|float" ,
"{{ 42 | float }}" ,
json :: object (),
"42.0"
);
test_template ( t , "integer|tojson" ,
"{{ 42 | tojson }}" ,
json :: object (),
"42"
);
test_template ( t , "float|abs" ,
"{{ -3.14 | abs }}" ,
json :: object (),
"3.14"
);
test_template ( t , "float|int" ,
"{{ 3.14 | int }}" ,
json :: object (),
"3"
);
test_template ( t , "float|tojson" ,
"{{ 3.14 | tojson }}" ,
json :: object (),
"3.14"
);
test_template ( t , "string|tojson" ,
"{{ 'hello' | tojson }}" ,
json :: object (),
" \" hello \" "
);
test_template ( t , "boolean|int" ,
"{{ true | int }}" ,
json :: object (),
"1"
);
test_template ( t , "boolean|float" ,
"{{ true | float }}" ,
json :: object (),
"1.0"
);
test_template ( t , "boolean|tojson" ,
"{{ true | tojson }}" ,
json :: object (),
"true"
);
2026-01-16 11:22:06 +01:00
}
static void test_comments ( testing & t ) {
test_template ( t , "inline comment" ,
"before{# comment #}after" ,
json :: object (),
"beforeafter"
);
test_template ( t , "comment ignores code" ,
"{% set x = 1 %}{# {% set x = 999 %} #}{{ x }}" ,
json :: object (),
"1"
);
}
static void test_macros ( testing & t ) {
test_template ( t , "simple macro" ,
"{% macro greet(name) %}Hello {{ name }}{% endmacro %}{{ greet('World') }}" ,
json :: object (),
"Hello World"
);
test_template ( t , "macro default arg" ,
"{% macro greet(name='Guest') %}Hi {{ name }}{% endmacro %}{{ greet() }}" ,
json :: object (),
"Hi Guest"
);
2026-03-25 12:22:48 +01:00
test_template ( t , "macro kwargs input" ,
"{% macro my_func(a, b=False) %}{% if b %}{{ a }}{% else %}nope{% endif %}{% endmacro %}{{ my_func(1, b=True) }}" ,
json :: object (),
"1"
);
test_template ( t , "macro with multiple args" ,
"{% macro add(a, b, c=0) %}{{ a + b + c }}{% endmacro %}{{ add(1, 2) }},{{ add(1, 2, 3) }},{{ add(1, b=10) }},{{ add(1, 2, c=5) }}" ,
json :: object (),
"3,6,11,8"
);
test_template ( t , "macro with kwarg out-of-order input" ,
"{% macro greet(first, last, greeting='Hello') %}{{ greeting }}, {{ first }} {{ last }}{% endmacro %}{{ greet(last='Smith', first='John') }},{{ greet(last='Doe', greeting='Hi', first='Jane') }}" ,
json :: object (),
"Hello, John Smith,Hi, Jane Doe"
);
2026-01-16 11:22:06 +01:00
}
static void test_namespace ( testing & t ) {
test_template ( t , "namespace counter" ,
"{% set ns = namespace(count=0) %}{% for i in range(3) %}{% set ns.count = ns.count + 1 %}{% endfor %}{{ ns.count }}" ,
json :: object (),
"3"
);
}
static void test_tests ( testing & t ) {
test_template ( t , "is odd" ,
"{% if 3 is odd %}yes{% endif %}" ,
json :: object (),
"yes"
);
test_template ( t , "is even" ,
"{% if 4 is even %}yes{% endif %}" ,
json :: object (),
"yes"
);
test_template ( t , "is false" ,
"{{ 'yes' if x is false }}" ,
{{ "x" , false }},
"yes"
);
test_template ( t , "is true" ,
"{{ 'yes' if x is true }}" ,
{{ "x" , true }},
"yes"
);
test_template ( t , "string is false" ,
"{{ 'yes' if x is false else 'no' }}" ,
{{ "x" , "" }},
"no"
);
test_template ( t , "is divisibleby" ,
"{{ 'yes' if x is divisibleby(2) }}" ,
{{ "x" , 2 }},
"yes"
);
test_template ( t , "is eq" ,
"{{ 'yes' if 3 is eq(3) }}" ,
json :: object (),
"yes"
);
test_template ( t , "is not equalto" ,
"{{ 'yes' if 3 is not equalto(4) }}" ,
json :: object (),
"yes"
);
test_template ( t , "is ge" ,
"{{ 'yes' if 3 is ge(3) }}" ,
json :: object (),
"yes"
);
test_template ( t , "is gt" ,
"{{ 'yes' if 3 is gt(2) }}" ,
json :: object (),
"yes"
);
test_template ( t , "is greaterthan" ,
"{{ 'yes' if 3 is greaterthan(2) }}" ,
json :: object (),
"yes"
);
test_template ( t , "is lt" ,
"{{ 'yes' if 2 is lt(3) }}" ,
json :: object (),
"yes"
);
test_template ( t , "is lessthan" ,
"{{ 'yes' if 2 is lessthan(3) }}" ,
json :: object (),
"yes"
);
test_template ( t , "is ne" ,
"{{ 'yes' if 2 is ne(3) }}" ,
json :: object (),
"yes"
);
test_template ( t , "is lower" ,
"{{ 'yes' if 'lowercase' is lower }}" ,
json :: object (),
"yes"
);
test_template ( t , "is upper" ,
"{{ 'yes' if 'UPPERCASE' is upper }}" ,
json :: object (),
"yes"
);
test_template ( t , "is sameas" ,
"{{ 'yes' if x is sameas(false) }}" ,
{{ "x" , false }},
"yes"
);
test_template ( t , "is boolean" ,
"{{ 'yes' if x is boolean }}" ,
{{ "x" , true }},
"yes"
);
test_template ( t , "is callable" ,
"{{ 'yes' if ''.strip is callable }}" ,
json :: object (),
"yes"
);
test_template ( t , "is escaped" ,
"{{ 'yes' if 'foo'|safe is escaped }}" ,
json :: object (),
"yes"
);
test_template ( t , "is filter" ,
"{{ 'yes' if 'trim' is filter }}" ,
json :: object (),
"yes"
);
test_template ( t , "is float" ,
"{{ 'yes' if x is float }}" ,
{{ "x" , 1.1 }},
"yes"
);
test_template ( t , "is integer" ,
"{{ 'yes' if x is integer }}" ,
{{ "x" , 1 }},
"yes"
);
test_template ( t , "is sequence" ,
"{{ 'yes' if x is sequence }}" ,
{{ "x" , json :: array ({ 1 , 2 , 3 })}},
"yes"
);
test_template ( t , "is test" ,
"{{ 'yes' if 'sequence' is test }}" ,
json :: object (),
"yes"
);
test_template ( t , "is undefined" ,
"{{ 'yes' if x is undefined }}" ,
json :: object (),
"yes"
);
test_template ( t , "is none" ,
"{% if x is none %}yes{% endif %}" ,
{{ "x" , nullptr }},
"yes"
);
test_template ( t , "is string" ,
"{% if x is string %}yes{% endif %}" ,
{{ "x" , "hello" }},
"yes"
);
test_template ( t , "is number" ,
"{% if x is number %}yes{% endif %}" ,
{{ "x" , 42 }},
"yes"
);
test_template ( t , "is iterable" ,
"{% if x is iterable %}yes{% endif %}" ,
{{ "x" , json :: array ({ 1 , 2 , 3 })}},
"yes"
);
test_template ( t , "is mapping" ,
"{% if x is mapping %}yes{% endif %}" ,
{{ "x" , {{ "a" , 1 }}}},
"yes"
);
2026-01-28 14:40:29 +01:00
test_template ( t , "undefined is sequence" ,
"{{ 'yes' if x is sequence }}" ,
json :: object (),
"yes"
);
test_template ( t , "undefined is iterable" ,
"{{ 'yes' if x is iterable }}" ,
json :: object (),
"yes"
);
2026-02-02 12:00:55 -08:00
test_template ( t , "is in (array, true)" ,
"{{ 'yes' if 2 is in([1, 2, 3]) }}" ,
json :: object (),
"yes"
);
test_template ( t , "is in (array, false)" ,
"{{ 'yes' if 5 is in([1, 2, 3]) else 'no' }}" ,
json :: object (),
"no"
);
test_template ( t , "is in (string)" ,
"{{ 'yes' if 'bc' is in('abcde') }}" ,
json :: object (),
"yes"
);
test_template ( t , "is in (object keys)" ,
"{{ 'yes' if 'a' is in(obj) }}" ,
{{ "obj" , {{ "a" , 1 }, { "b" , 2 }}}},
"yes"
);
test_template ( t , "reject with in test" ,
"{{ items | reject('in', skip) | join(', ') }}" ,
{{ "items" , json :: array ({ "a" , "b" , "c" , "d" })}, { "skip" , json :: array ({ "b" , "d" })}},
"a, c"
);
test_template ( t , "select with in test" ,
"{{ items | select('in', keep) | join(', ') }}" ,
{{ "items" , json :: array ({ "a" , "b" , "c" , "d" })}, { "keep" , json :: array ({ "b" , "c" })}},
"b, c"
);
2026-01-16 11:22:06 +01:00
}
static void test_string_methods ( testing & t ) {
test_template ( t , "string.upper()" ,
"{{ s.upper() }}" ,
{{ "s" , "hello" }},
"HELLO"
);
test_template ( t , "string.lower()" ,
"{{ s.lower() }}" ,
{{ "s" , "HELLO" }},
"hello"
);
test_template ( t , "string.strip()" ,
"[{{ s.strip() }}]" ,
{{ "s" , " hello " }},
"[hello]"
);
test_template ( t , "string.lstrip()" ,
"[{{ s.lstrip() }}]" ,
{{ "s" , " hello" }},
"[hello]"
);
test_template ( t , "string.rstrip()" ,
"[{{ s.rstrip() }}]" ,
{{ "s" , "hello " }},
"[hello]"
);
test_template ( t , "string.title()" ,
"{{ s.title() }}" ,
{{ "s" , "hello world" }},
"Hello World"
);
test_template ( t , "string.capitalize()" ,
"{{ s.capitalize() }}" ,
{{ "s" , "heLlo World" }},
"Hello world"
);
test_template ( t , "string.startswith() true" ,
"{% if s.startswith('hel') %}yes{% endif %}" ,
{{ "s" , "hello" }},
"yes"
);
test_template ( t , "string.startswith() false" ,
"{% if s.startswith('xyz') %}yes{% else %}no{% endif %}" ,
{{ "s" , "hello" }},
"no"
);
test_template ( t , "string.endswith() true" ,
"{% if s.endswith('lo') %}yes{% endif %}" ,
{{ "s" , "hello" }},
"yes"
);
test_template ( t , "string.endswith() false" ,
"{% if s.endswith('xyz') %}yes{% else %}no{% endif %}" ,
{{ "s" , "hello" }},
"no"
);
test_template ( t , "string.split() with sep" ,
"{{ s.split(',')|join('-') }}" ,
{{ "s" , "a,b,c" }},
"a-b-c"
);
test_template ( t , "string.split() with maxsplit" ,
"{{ s.split(',', 1)|join('-') }}" ,
{{ "s" , "a,b,c" }},
"a-b,c"
);
test_template ( t , "string.rsplit() with sep" ,
"{{ s.rsplit(',')|join('-') }}" ,
{{ "s" , "a,b,c" }},
"a-b-c"
);
test_template ( t , "string.rsplit() with maxsplit" ,
"{{ s.rsplit(',', 1)|join('-') }}" ,
{{ "s" , "a,b,c" }},
"a,b-c"
);
test_template ( t , "string.replace() basic" ,
"{{ s.replace('world', 'jinja') }}" ,
{{ "s" , "hello world" }},
"hello jinja"
);
2026-06-13 16:56:59 +02:00
test_template ( t , "string.replace() empty" ,
"{{ s.replace('', '.') }}" ,
{{ "s" , "hello world" }},
".h.e.l.l.o. .w.o.r.l.d."
);
2026-01-16 11:22:06 +01:00
test_template ( t , "string.replace() with count" ,
"{{ s.replace('a', 'X', 2) }}" ,
{{ "s" , "banana" }},
"bXnXna"
);
2026-01-28 14:40:29 +01:00
test_template ( t , "undefined|capitalize" ,
"{{ arr|capitalize }}" ,
json :: object (),
""
);
test_template ( t , "undefined|title" ,
"{{ arr|title }}" ,
json :: object (),
""
);
test_template ( t , "undefined|truncate" ,
"{{ arr|truncate(9) }}" ,
json :: object (),
""
);
test_template ( t , "undefined|upper" ,
"{{ arr|upper }}" ,
json :: object (),
""
);
test_template ( t , "undefined|lower" ,
"{{ arr|lower }}" ,
json :: object (),
""
);
test_template ( t , "undefined|replace" ,
"{{ arr|replace('a', 'b') }}" ,
json :: object (),
""
);
test_template ( t , "undefined|trim" ,
"{{ arr|trim }}" ,
json :: object (),
""
);
test_template ( t , "undefined|wordcount" ,
"{{ arr|wordcount }}" ,
json :: object (),
"0"
);
2026-01-16 11:22:06 +01:00
}
static void test_array_methods ( testing & t ) {
test_template ( t , "array|selectattr by attribute" ,
"{% for item in items|selectattr('active') %}{{ item.name }} {% endfor %}" ,
{{ "items" , json :: array ({
{{ "name" , "a" }, { "active" , true }},
{{ "name" , "b" }, { "active" , false }},
{{ "name" , "c" }, { "active" , true }}
})}},
"a c "
);
test_template ( t , "array|selectattr with operator" ,
"{% for item in items|selectattr('value', 'equalto', 5) %}{{ item.name }} {% endfor %}" ,
{{ "items" , json :: array ({
{{ "name" , "a" }, { "value" , 3 }},
{{ "name" , "b" }, { "value" , 5 }},
{{ "name" , "c" }, { "value" , 5 }}
})}},
"b c "
);
test_template ( t , "array|tojson" ,
"{{ arr|tojson }}" ,
{{ "arr" , json :: array ({ 1 , 2 , 3 })}},
"[1, 2, 3]"
);
test_template ( t , "array|tojson with strings" ,
"{{ arr|tojson }}" ,
{{ "arr" , json :: array ({ "a" , "b" , "c" })}},
"[ \" a \" , \" b \" , \" c \" ]"
);
test_template ( t , "array|tojson nested" ,
"{{ arr|tojson }}" ,
{{ "arr" , json :: array ({ json :: array ({ 1 , 2 }), json :: array ({ 3 , 4 })})}},
"[[1, 2], [3, 4]]"
);
test_template ( t , "array|last" ,
"{{ arr|last }}" ,
{{ "arr" , json :: array ({ 10 , 20 , 30 })}},
"30"
);
test_template ( t , "array|last single element" ,
"{{ arr|last }}" ,
{{ "arr" , json :: array ({ 42 })}},
"42"
);
test_template ( t , "array|join with separator" ,
"{{ arr|join(', ') }}" ,
{{ "arr" , json :: array ({ "a" , "b" , "c" })}},
"a, b, c"
);
test_template ( t , "array|join with custom separator" ,
"{{ arr|join(' | ') }}" ,
{{ "arr" , json :: array ({ 1 , 2 , 3 })}},
"1 | 2 | 3"
);
test_template ( t , "array|join default separator" ,
"{{ arr|join }}" ,
{{ "arr" , json :: array ({ "x" , "y" , "z" })}},
"xyz"
);
test_template ( t , "array|join attribute" ,
2026-01-18 02:53:01 +01:00
"{{ arr|join(attribute='age') }}" ,
{{ "arr" , json :: array ({
json ({{ "name" , "a" }, { "age" , 1 }}),
json ({{ "name" , "b" }, { "age" , 2 }}),
json ({{ "name" , "c" }, { "age" , 3 }}),
})}},
"123"
);
test_template ( t , "array|join numeric attribute" ,
"{{ arr|join(attribute=-1) }}" ,
2026-01-16 11:22:06 +01:00
{{ "arr" , json :: array ({ json :: array ({ 1 }), json :: array ({ 2 }), json :: array ({ 3 })})}},
"123"
);
test_template ( t , "array.pop() last" ,
"{{ arr.pop() }}-{{ arr|join(',') }}" ,
{{ "arr" , json :: array ({ "a" , "b" , "c" })}},
"c-a,b"
);
test_template ( t , "array.pop() with index" ,
"{{ arr.pop(0) }}-{{ arr|join(',') }}" ,
{{ "arr" , json :: array ({ "a" , "b" , "c" })}},
"a-b,c"
);
test_template ( t , "array.append()" ,
"{% set _ = arr.append('d') %}{{ arr|join(',') }}" ,
{{ "arr" , json :: array ({ "a" , "b" , "c" })}},
"a,b,c,d"
);
2026-01-18 02:53:01 +01:00
test_template ( t , "array|map with attribute" ,
"{% for v in arr|map(attribute='age') %}{{ v }} {% endfor %}" ,
2026-01-16 11:22:06 +01:00
{{ "arr" , json :: array ({
json ({{ "name" , "a" }, { "age" , 1 }}),
json ({{ "name" , "b" }, { "age" , 2 }}),
json ({{ "name" , "c" }, { "age" , 3 }}),
})}},
"1 2 3 "
);
2026-01-18 02:53:01 +01:00
test_template ( t , "array|map with attribute default" ,
"{% for v in arr|map(attribute='age', default=3) %}{{ v }} {% endfor %}" ,
{{ "arr" , json :: array ({
json ({{ "name" , "a" }, { "age" , 1 }}),
json ({{ "name" , "b" }, { "age" , 2 }}),
json ({{ "name" , "c" }}),
})}},
"1 2 3 "
);
test_template ( t , "array|map without attribute default" ,
"{% for v in arr|map(attribute='age') %}{{ v }} {% endfor %}" ,
{{ "arr" , json :: array ({
json ({{ "name" , "a" }, { "age" , 1 }}),
json ({{ "name" , "b" }, { "age" , 2 }}),
json ({{ "name" , "c" }}),
})}},
"1 2 "
);
test_template ( t , "array|map with numeric attribute" ,
"{% for v in arr|map(attribute=0) %}{{ v }} {% endfor %}" ,
2026-01-16 11:22:06 +01:00
{{ "arr" , json :: array ({
json :: array ({ 10 , "x" }),
json :: array ({ 20 , "y" }),
json :: array ({ 30 , "z" }),
})}},
"10 20 30 "
);
2026-01-18 02:53:01 +01:00
test_template ( t , "array|map with negative attribute" ,
"{% for v in arr|map(attribute=-1) %}{{ v }} {% endfor %}" ,
{{ "arr" , json :: array ({
json :: array ({ 10 , "x" }),
json :: array ({ 20 , "y" }),
json :: array ({ 30 , "z" }),
})}},
"x y z "
);
test_template ( t , "array|map with filter" ,
"{{ arr|map('int')|sum }}" ,
{{ "arr" , json :: array ({ "1" , "2" , "3" })}},
"6"
);
2026-01-16 11:22:06 +01:00
// not used by any chat templates
// test_template(t, "array.insert()",
// "{% set _ = arr.insert(1, 'x') %}{{ arr|join(',') }}",
// {{"arr", json::array({"a", "b", "c"})}},
// "a,x,b,c"
// );
2026-01-28 14:40:29 +01:00
test_template ( t , "undefined|select" ,
"{% for item in items|select('odd') %}{{ item.name }} {% endfor %}" ,
json :: object (),
""
);
test_template ( t , "undefined|selectattr" ,
"{% for item in items|selectattr('active') %}{{ item.name }} {% endfor %}" ,
json :: object (),
""
);
test_template ( t , "undefined|reject" ,
"{% for item in items|reject('even') %}{{ item.name }} {% endfor %}" ,
json :: object (),
""
);
test_template ( t , "undefined|rejectattr" ,
"{% for item in items|rejectattr('active') %}{{ item.name }} {% endfor %}" ,
json :: object (),
""
);
test_template ( t , "undefined|list" ,
"{{ arr|list|string }}" ,
json :: object (),
"[]"
);
test_template ( t , "undefined|string" ,
"{{ arr|string }}" ,
json :: object (),
""
);
test_template ( t , "undefined|first" ,
"{{ arr|first }}" ,
json :: object (),
""
);
test_template ( t , "undefined|last" ,
"{{ arr|last }}" ,
json :: object (),
""
);
test_template ( t , "undefined|length" ,
"{{ arr|length }}" ,
json :: object (),
"0"
);
test_template ( t , "undefined|join" ,
"{{ arr|join }}" ,
json :: object (),
""
);
test_template ( t , "undefined|sort" ,
"{{ arr|sort|string }}" ,
json :: object (),
"[]"
);
test_template ( t , "undefined|reverse" ,
"{{ arr|reverse|join }}" ,
json :: object (),
""
);
test_template ( t , "undefined|map" ,
"{% for v in arr|map(attribute='age') %}{{ v }} {% endfor %}" ,
json :: object (),
""
);
test_template ( t , "undefined|min" ,
"{{ arr|min }}" ,
json :: object (),
""
);
test_template ( t , "undefined|max" ,
"{{ arr|max }}" ,
json :: object (),
""
);
test_template ( t , "undefined|unique" ,
"{{ arr|unique|join }}" ,
json :: object (),
""
);
test_template ( t , "undefined|sum" ,
"{{ arr|sum }}" ,
json :: object (),
"0"
);
2026-01-16 11:22:06 +01:00
}
static void test_object_methods ( testing & t ) {
test_template ( t , "object.get() existing key" ,
"{{ obj.get('a') }}" ,
{{ "obj" , {{ "a" , 1 }, { "b" , 2 }}}},
"1"
);
test_template ( t , "object.get() missing key" ,
"[{{ obj.get('c') is none }}]" ,
{{ "obj" , {{ "a" , 1 }}}},
"[True]"
);
test_template ( t , "object.get() missing key with default" ,
"{{ obj.get('c', 'default') }}" ,
{{ "obj" , {{ "a" , 1 }}}},
"default"
);
test_template ( t , "object.items()" ,
"{% for k, v in obj.items() %}{{ k }}={{ v }} {% endfor %}" ,
{{ "obj" , {{ "x" , 1 }, { "y" , 2 }}}},
"x=1 y=2 "
);
test_template ( t , "object.keys()" ,
"{% for k in obj.keys() %}{{ k }} {% endfor %}" ,
{{ "obj" , {{ "a" , 1 }, { "b" , 2 }}}},
"a b "
);
test_template ( t , "object.values()" ,
"{% for v in obj.values() %}{{ v }} {% endfor %}" ,
{{ "obj" , {{ "a" , 1 }, { "b" , 2 }}}},
"1 2 "
);
test_template ( t , "dictsort ascending by key" ,
"{% for k, v in obj|dictsort %}{{ k }}={{ v }} {% endfor %}" ,
{{ "obj" , {{ "z" , 2 }, { "a" , 3 }, { "m" , 1 }}}},
"a=3 m=1 z=2 "
);
test_template ( t , "dictsort descending by key" ,
"{% for k, v in obj|dictsort(reverse=true) %}{{ k }}={{ v }} {% endfor %}" ,
{{ "obj" , {{ "a" , 1 }, { "b" , 2 }, { "c" , 3 }}}},
"c=3 b=2 a=1 "
);
test_template ( t , "dictsort by value" ,
"{% for k, v in obj|dictsort(by='value') %}{{ k }}={{ v }} {% endfor %}" ,
{{ "obj" , {{ "a" , 3 }, { "b" , 1 }, { "c" , 2 }}}},
"b=1 c=2 a=3 "
);
test_template ( t , "dictsort case sensitive" ,
"{% for k, v in obj|dictsort(case_sensitive=true) %}{{ k }}={{ v }} {% endfor %}" ,
{{ "obj" , {{ "a" , 1 }, { "A" , 1 }, { "b" , 2 }, { "B" , 2 }, { "c" , 3 }}}},
"A=1 B=2 a=1 b=2 c=3 "
);
test_template ( t , "object|tojson" ,
"{{ obj|tojson }}" ,
{{ "obj" , {{ "name" , "test" }, { "value" , 42 }}}},
"{ \" name \" : \" test \" , \" value \" : 42}"
);
test_template ( t , "nested object|tojson" ,
"{{ obj|tojson }}" ,
{{ "obj" , {{ "outer" , {{ "inner" , "value" }}}}}},
"{ \" outer \" : { \" inner \" : \" value \" }}"
);
test_template ( t , "array in object|tojson" ,
"{{ obj|tojson }}" ,
{{ "obj" , {{ "items" , json :: array ({ 1 , 2 , 3 })}}}},
"{ \" items \" : [1, 2, 3]}"
);
2026-01-18 00:48:55 +01:00
test_template ( t , "object attribute and key access" ,
"{{ obj.keys()|join(',') }} vs {{ obj['keys'] }} vs {{ obj.test }}" ,
{{ "obj" , {{ "keys" , "value" }, { "test" , "attr_value" }}}},
"keys,test vs value vs attr_value"
);
test_template ( t , "env should not have object methods" ,
"{{ keys is undefined }} {{ obj.keys is defined }}" ,
{{ "obj" , {{ "a" , "b" }}}},
"True True"
);
2026-01-27 19:50:42 +01:00
test_template ( t , "expression as object key" ,
"{% set d = {'ab': 123} %}{{ d['a' + 'b'] == 123 }}" ,
json :: object (),
"True"
);
test_template ( t , "numeric as object key (template: Seed-OSS)" ,
"{% set d = {1: 'a', 2: 'b'} %}{{ d[1] == 'a' and d[2] == 'b' }}" ,
json :: object (),
"True"
);
2026-01-28 14:40:29 +01:00
test_template ( t , "undefined|items" ,
"{{ arr|items|join }}" ,
json :: object (),
""
);
2026-01-27 19:50:42 +01:00
}
static void test_hasher ( testing & t ) {
static const std :: vector < std :: pair < size_t , size_t >> chunk_sizes = {
{ 1 , 2 },
{ 1 , 16 },
{ 8 , 1 },
{ 1 , 1024 },
{ 5 , 512 },
{ 16 , 256 },
{ 45 , 122 },
{ 70 , 634 },
};
static auto random_bytes = []( size_t length ) -> std :: string {
std :: string data ;
data . resize ( length );
for ( size_t i = 0 ; i < length ; ++ i ) {
data [ i ] = static_cast < char > ( rand () % 256 );
}
return data ;
};
t . test ( "state unchanged with empty input" , []( testing & t ) {
jinja :: hasher hasher ;
hasher . update ( "some data" );
size_t initial_state = hasher . digest ();
hasher . update ( "" , 0 );
size_t final_state = hasher . digest ();
t . assert_true ( "Hasher state should remain unchanged" , initial_state == final_state );
});
t . test ( "different inputs produce different hashes" , []( testing & t ) {
jinja :: hasher hasher1 ;
hasher1 . update ( "data one" );
size_t hash1 = hasher1 . digest ();
jinja :: hasher hasher2 ;
hasher2 . update ( "data two" );
size_t hash2 = hasher2 . digest ();
t . assert_true ( "Different inputs should produce different hashes" , hash1 != hash2 );
});
t . test ( "same inputs produce same hashes" , []( testing & t ) {
jinja :: hasher hasher1 ;
hasher1 . update ( "consistent data" );
size_t hash1 = hasher1 . digest ();
jinja :: hasher hasher2 ;
hasher2 . update ( "consistent data" );
size_t hash2 = hasher2 . digest ();
t . assert_true ( "Same inputs should produce same hashes" , hash1 == hash2 );
});
t . test ( "property: update(a ~ b) == update(a).update(b)" , []( testing & t ) {
for ( const auto & [ size1 , size2 ] : chunk_sizes ) {
std :: string data1 = random_bytes ( size1 );
std :: string data2 = random_bytes ( size2 );
jinja :: hasher hasher1 ;
hasher1 . update ( data1 );
hasher1 . update ( data2 );
size_t hash1 = hasher1 . digest ();
jinja :: hasher hasher2 ;
hasher2 . update ( data1 + data2 );
size_t hash2 = hasher2 . digest ();
t . assert_true (
"Hashing in multiple updates should match single update (" + std :: to_string ( size1 ) + ", " + std :: to_string ( size2 ) + ")" ,
hash1 == hash2 );
}
});
t . test ( "property: update(a ~ b) == update(a).update(b) with more update passes" , []( testing & t ) {
static const std :: vector < size_t > sizes = { 3 , 732 , 131 , 13 , 17 , 256 , 436 , 99 , 4 };
jinja :: hasher hasher1 ;
jinja :: hasher hasher2 ;
std :: string combined_data ;
for ( size_t size : sizes ) {
std :: string data = random_bytes ( size );
hasher1 . update ( data );
combined_data += data ;
}
hasher2 . update ( combined_data );
size_t hash1 = hasher1 . digest ();
size_t hash2 = hasher2 . digest ();
t . assert_true (
"Hashing in multiple updates should match single update with many chunks" ,
hash1 == hash2 );
});
t . test ( "property: non associativity of update" , []( testing & t ) {
for ( const auto & [ size1 , size2 ] : chunk_sizes ) {
std :: string data1 = random_bytes ( size1 );
std :: string data2 = random_bytes ( size2 );
jinja :: hasher hasher1 ;
hasher1 . update ( data1 );
hasher1 . update ( data2 );
size_t hash1 = hasher1 . digest ();
jinja :: hasher hasher2 ;
hasher2 . update ( data2 );
hasher2 . update ( data1 );
size_t hash2 = hasher2 . digest ();
t . assert_true (
"Hashing order should matter (" + std :: to_string ( size1 ) + ", " + std :: to_string ( size2 ) + ")" ,
hash1 != hash2 );
}
});
t . test ( "property: different lengths produce different hashes (padding block size)" , []( testing & t ) {
std :: string random_data = random_bytes ( 64 );
jinja :: hasher hasher1 ;
hasher1 . update ( random_data );
size_t hash1 = hasher1 . digest ();
for ( int i = 0 ; i < 16 ; ++ i ) {
random_data . push_back ( 'A' ); // change length
jinja :: hasher hasher2 ;
hasher2 . update ( random_data );
size_t hash2 = hasher2 . digest ();
t . assert_true ( "Different lengths should produce different hashes (length " + std :: to_string ( random_data . size ()) + ")" , hash1 != hash2 );
hash1 = hash2 ;
}
});
2026-01-16 11:22:06 +01:00
}
2026-02-22 21:08:23 +01:00
static void test_stats ( testing & t ) {
static auto get_stats = []( const std :: string & tmpl , const json & vars ) -> jinja :: value {
jinja :: lexer lexer ;
auto lexer_res = lexer . tokenize ( tmpl );
jinja :: program prog = jinja :: parse_from_tokens ( lexer_res );
jinja :: context ctx ( tmpl );
jinja :: global_from_json ( ctx , json {{ "val" , vars }}, true );
ctx . is_get_stats = true ;
jinja :: runtime runtime ( ctx );
runtime . execute ( prog );
return ctx . get_val ( "val" );
};
t . test ( "stats" , []( testing & t ) {
jinja :: value val = get_stats (
"{{val.num}} "
"{{val.str}} "
"{{val.arr[0]}} "
"{{val.obj.key1}} "
"{{val.nested | tojson}}" ,
// Note: the json below will be wrapped inside "val" in the context
json {
{ "num" , 1 },
{ "str" , "abc" },
{ "arr" , json :: array ({ 1 , 2 , 3 })},
{ "obj" , json :: object ({{ "key1" , 1 }, { "key2" , 2 }, { "key3" , 3 }})},
{ "nested" , json :: object ({
{ "inner_key1" , json :: array ({ 1 , 2 })},
{ "inner_key2" , json :: object ({{ "a" , "x" }, { "b" , "y" }})}
})},
{ "mixed" , json :: object ({
{ "used" , 1 },
{ "unused" , 2 },
})},
}
);
t . assert_true ( "num is used" , val -> at ( "num" ) -> stats . used );
t . assert_true ( "str is used" , val -> at ( "str" ) -> stats . used );
t . assert_true ( "arr is used" , val -> at ( "arr" ) -> stats . used );
t . assert_true ( "arr[0] is used" , val -> at ( "arr" ) -> at ( 0 ) -> stats . used );
t . assert_true ( "arr[1] is not used" , ! val -> at ( "arr" ) -> at ( 1 ) -> stats . used );
t . assert_true ( "obj is used" , val -> at ( "obj" ) -> stats . used );
t . assert_true ( "obj.key1 is used" , val -> at ( "obj" ) -> at ( "key1" ) -> stats . used );
t . assert_true ( "obj.key2 is not used" , ! val -> at ( "obj" ) -> at ( "key2" ) -> stats . used );
t . assert_true ( "inner_key1[0] is used" , val -> at ( "nested" ) -> at ( "inner_key1" ) -> at ( 0 ) -> stats . used );
t . assert_true ( "inner_key2.a is used" , val -> at ( "nested" ) -> at ( "inner_key2" ) -> at ( "a" ) -> stats . used );
});
}
2026-01-18 08:14:27 +01:00
static void test_template_cpp ( testing & t , const std :: string & name , const std :: string & tmpl , const json & vars , const std :: string & expect ) {
2026-01-16 11:22:06 +01:00
t . test ( name , [ & tmpl , & vars , & expect ]( testing & t ) {
jinja :: lexer lexer ;
auto lexer_res = lexer . tokenize ( tmpl );
jinja :: program ast = jinja :: parse_from_tokens ( lexer_res );
jinja :: context ctx ( tmpl );
jinja :: global_from_json ( ctx , vars , true );
jinja :: runtime runtime ( ctx );
try {
const jinja :: value results = runtime . execute ( ast );
auto parts = runtime . gather_string_parts ( results );
std :: string rendered ;
for ( const auto & part : parts -> as_string (). parts ) {
rendered += part . val ;
}
if ( ! t . assert_true ( "Template render mismatch" , expect == rendered )) {
t . log ( "Template: " + json ( tmpl ). dump ());
t . log ( "Expected: " + json ( expect ). dump ());
t . log ( "Actual : " + json ( rendered ). dump ());
}
} catch ( const jinja :: not_implemented_exception & e ) {
// TODO @ngxson : remove this when the test framework supports skipping tests
t . log ( "Skipped: " + std :: string ( e . what ()));
}
});
}
2026-01-18 08:14:27 +01:00
// keep this in-sync with https://github.com/huggingface/transformers/blob/main/src/transformers/utils/chat_template_utils.py
// note: we use SandboxedEnvironment instead of ImmutableSandboxedEnvironment to allow usage of in-place array methods like append() and pop()
static std :: string py_script = R "(
import jinja2
import jinja2 . ext as jinja2_ext
import json
import sys
from datetime import datetime
from jinja2 . sandbox import SandboxedEnvironment
2026-03-18 02:43:31 -07:00
merged_input = json . loads ( sys . stdin . buffer . read (). decode ( "utf-8" ))
tmpl = merged_input [ "tmpl" ]
vars_json = merged_input [ "vars" ]
2026-01-18 08:14:27 +01:00
env = SandboxedEnvironment (
trim_blocks = True ,
lstrip_blocks = True ,
extensions = [ jinja2_ext . loopcontrols ],
)
def raise_exception ( message ) :
raise jinja2 . exceptions . TemplateError ( message )
env . filters [ "tojson" ] = lambda x , ensure_ascii = False , indent = None , separators = None , sort_keys = False : json . dumps ( x , ensure_ascii = ensure_ascii , indent = indent , separators = separators , sort_keys = sort_keys )
env . globals [ "strftime_now" ] = lambda format : datetime . now (). strftime ( format )
env . globals [ "raise_exception" ] = raise_exception
template = env . from_string ( tmpl )
result = template . render ( ** vars_json )
2026-03-16 20:40:22 +01:00
sys . stdout . buffer . write ( result . encode ())
2026-01-18 08:14:27 +01:00
) ";
static void test_template_py ( testing & t , const std :: string & name , const std :: string & tmpl , const json & vars , const std :: string & expect ) {
t . test ( name , [ & tmpl , & vars , & expect ]( testing & t ) {
// Prepare arguments
2026-03-18 02:43:31 -07:00
json merged ;
merged [ "tmpl" ] = json ( tmpl );
merged [ "vars" ] = vars ;
2026-01-18 08:14:27 +01:00
#ifdef _WIN32
const char * python_executable = "python.exe" ;
#else
const char * python_executable = "python3" ;
#endif
2026-03-18 02:43:31 -07:00
const char * command_line [] = { python_executable , "-c" , py_script . c_str (), NULL };
2026-01-18 08:14:27 +01:00
struct subprocess_s subprocess ;
int options = subprocess_option_combined_stdout_stderr
| subprocess_option_no_window
| subprocess_option_inherit_environment
| subprocess_option_search_user_path ;
int result = subprocess_create ( command_line , options , & subprocess );
if ( result != 0 ) {
t . log ( "Failed to create subprocess, error code: " + std :: to_string ( result ));
t . assert_true ( "subprocess creation" , false );
return ;
}
2026-03-18 02:43:31 -07:00
FILE * p_stdin = subprocess_stdin ( & subprocess );
// Write input
std :: string input = merged . dump ();
auto written = fwrite ( input . c_str (), 1 , input . size (), p_stdin );
if ( written != input . size ()) {
t . log ( "Failed to write complete input to subprocess stdin" );
t . assert_true ( "subprocess stdin write" , false );
subprocess_destroy ( & subprocess );
return ;
}
fflush ( p_stdin );
fclose ( p_stdin ); // Close stdin to signal EOF to the Python process
subprocess . stdin_file = nullptr ;
2026-01-18 08:14:27 +01:00
// Read output
std :: string output ;
char buffer [ 1024 ];
FILE * p_stdout = subprocess_stdout ( & subprocess );
while ( fgets ( buffer , sizeof ( buffer ), p_stdout )) {
output += buffer ;
}
int process_return ;
subprocess_join ( & subprocess , & process_return );
subprocess_destroy ( & subprocess );
if ( process_return != 0 ) {
t . log ( "Python script failed with exit code: " + std :: to_string ( process_return ));
t . log ( "Output: " + output );
t . assert_true ( "python execution" , false );
return ;
}
if ( ! t . assert_true ( "Template render mismatch" , expect == output )) {
t . log ( "Template: " + json ( tmpl ). dump ());
t . log ( "Expected: " + json ( expect ). dump ());
t . log ( "Python : " + json ( output ). dump ());
}
});
}
static void test_template ( testing & t , const std :: string & name , const std :: string & tmpl , const json & vars , const std :: string & expect ) {
if ( g_python_mode ) {
test_template_py ( t , name , tmpl , vars , expect );
} else {
test_template_cpp ( t , name , tmpl , vars , expect );
}
}
2026-01-16 11:22:06 +01:00
//
// fuzz tests to ensure no crashes occur on malformed inputs
//
constexpr int JINJA_FUZZ_ITERATIONS = 100 ;
// Helper to generate random string
static std :: string random_string ( std :: mt19937 & rng , size_t max_len ) {
static const char charset [] = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_" ;
std :: uniform_int_distribution < size_t > len_dist ( 0 , max_len );
std :: uniform_int_distribution < size_t > char_dist ( 0 , sizeof ( charset ) - 2 );
size_t len = len_dist ( rng );
std :: string result ;
result . reserve ( len );
for ( size_t i = 0 ; i < len ; ++ i ) {
result += charset [ char_dist ( rng )];
}
return result ;
}
// Helper to execute a fuzz test case - returns true if no crash occurred
static bool fuzz_test_template ( const std :: string & tmpl , const json & vars ) {
try {
// printf("Fuzz testing template: %s\n", tmpl.c_str());
jinja :: lexer lexer ;
auto lexer_res = lexer . tokenize ( tmpl );
jinja :: program ast = jinja :: parse_from_tokens ( lexer_res );
jinja :: context ctx ( tmpl );
jinja :: global_from_json ( ctx , vars , true );
jinja :: runtime runtime ( ctx );
const jinja :: value results = runtime . execute ( ast );
runtime . gather_string_parts ( results );
return true ; // success
} catch ( const std :: exception & ) {
return true ; // exception is acceptable, not a crash
} catch (...) {
return true ; // any exception is acceptable, not a crash
}
}
static void test_fuzzing ( testing & t ) {
const int num_iterations = JINJA_FUZZ_ITERATIONS ;
const unsigned int seed = 42 ; // fixed seed for reproducibility
std :: mt19937 rng ( seed );
// Distribution helpers
std :: uniform_int_distribution < int > choice_dist ( 0 , 100 );
std :: uniform_int_distribution < int > int_dist ( - 1000 , 1000 );
std :: uniform_int_distribution < size_t > idx_dist ( 0 , 1000 );
// Template fragments for fuzzing
const std :: vector < std :: string > var_names = {
"x" , "y" , "z" , "arr" , "obj" , "items" , "foo" , "bar" , "undefined_var" ,
"none" , "true" , "false" , "None" , "True" , "False"
};
const std :: vector < std :: string > filters = {
"length" , "first" , "last" , "reverse" , "sort" , "unique" , "join" , "upper" , "lower" ,
"trim" , "default" , "tojson" , "string" , "int" , "float" , "abs" , "list" , "dictsort"
};
const std :: vector < std :: string > builtins = {
"range" , "len" , "dict" , "list" , "join" , "str" , "int" , "float" , "namespace"
};
t . test ( "out of bound array access" , [ & ]( testing & t ) {
for ( int i = 0 ; i < num_iterations ; ++ i ) {
int idx = int_dist ( rng );
std :: string tmpl = "{{ arr[" + std :: to_string ( idx ) + "] }}" ;
json vars = {{ "arr" , json :: array ({ 1 , 2 , 3 })}};
t . assert_true ( "should not crash" , fuzz_test_template ( tmpl , vars ));
}
});
t . test ( "non-existing variables" , [ & ]( testing & t ) {
for ( int i = 0 ; i < num_iterations ; ++ i ) {
std :: string var = random_string ( rng , 20 );
std :: string tmpl = "{{ " + var + " }}" ;
json vars = json :: object (); // empty context
t . assert_true ( "should not crash" , fuzz_test_template ( tmpl , vars ));
}
});
t . test ( "non-existing nested attributes" , [ & ]( testing & t ) {
for ( int i = 0 ; i < num_iterations ; ++ i ) {
std :: string var1 = var_names [ choice_dist ( rng ) % var_names . size ()];
std :: string var2 = random_string ( rng , 10 );
std :: string var3 = random_string ( rng , 10 );
std :: string tmpl = "{{ " + var1 + "." + var2 + "." + var3 + " }}" ;
json vars = {{ var1 , {{ "other" , 123 }}}};
t . assert_true ( "should not crash" , fuzz_test_template ( tmpl , vars ));
}
});
t . test ( "invalid filter arguments" , [ & ]( testing & t ) {
for ( int i = 0 ; i < num_iterations ; ++ i ) {
std :: string filter = filters [ choice_dist ( rng ) % filters . size ()];
int val = int_dist ( rng );
std :: string tmpl = "{{ " + std :: to_string ( val ) + " | " + filter + " }}" ;
json vars = json :: object ();
t . assert_true ( "should not crash" , fuzz_test_template ( tmpl , vars ));
}
});
t . test ( "chained filters on various types" , [ & ]( testing & t ) {
for ( int i = 0 ; i < num_iterations ; ++ i ) {
std :: string f1 = filters [ choice_dist ( rng ) % filters . size ()];
std :: string f2 = filters [ choice_dist ( rng ) % filters . size ()];
std :: string var = var_names [ choice_dist ( rng ) % var_names . size ()];
std :: string tmpl = "{{ " + var + " | " + f1 + " | " + f2 + " }}" ;
json vars = {
{ "x" , 42 },
{ "y" , "hello" },
{ "arr" , json :: array ({ 1 , 2 , 3 })},
{ "obj" , {{ "a" , 1 }, { "b" , 2 }}},
{ "items" , json :: array ({ "a" , "b" , "c" })}
};
t . assert_true ( "should not crash" , fuzz_test_template ( tmpl , vars ));
}
});
t . test ( "invalid builtin calls" , [ & ]( testing & t ) {
for ( int i = 0 ; i < num_iterations ; ++ i ) {
std :: string builtin = builtins [ choice_dist ( rng ) % builtins . size ()];
std :: string arg ;
int arg_type = choice_dist ( rng ) % 4 ;
switch ( arg_type ) {
case 0 : arg = " \" not a number \" " ; break ;
case 1 : arg = "none" ; break ;
case 2 : arg = std :: to_string ( int_dist ( rng )); break ;
case 3 : arg = "[]" ; break ;
}
std :: string tmpl = "{{ " + builtin + "(" + arg + ") }}" ;
json vars = json :: object ();
t . assert_true ( "should not crash" , fuzz_test_template ( tmpl , vars ));
}
});
t . test ( "macro edge cases" , [ & ]( testing & t ) {
// Macro with no args called with args
t . assert_true ( "macro no args with args" , fuzz_test_template (
"{% macro foo() %}hello{% endmacro %}{{ foo(1, 2, 3) }}" ,
json :: object ()
));
// Macro with args called with no args
t . assert_true ( "macro with args no args" , fuzz_test_template (
"{% macro foo(a, b, c) %}{{ a }}{{ b }}{{ c }}{% endmacro %}{{ foo() }}" ,
json :: object ()
));
// Recursive macro reference
t . assert_true ( "recursive macro" , fuzz_test_template (
"{% macro foo(n) %}{% if n > 0 %}{{ foo(n - 1) }}{% endif %}{% endmacro %}{{ foo(5) }}" ,
json :: object ()
));
// Nested macro definitions
for ( int i = 0 ; i < num_iterations / 10 ; ++ i ) {
std :: string tmpl = "{% macro outer() %}{% macro inner() %}x{% endmacro %}{{ inner() }}{% endmacro %}{{ outer() }}" ;
t . assert_true ( "nested macro" , fuzz_test_template ( tmpl , json :: object ()));
}
});
t . test ( "empty and none operations" , [ & ]( testing & t ) {
const std :: vector < std :: string > empty_tests = {
"{{ \"\" | first }}" ,
"{{ \"\" | last }}" ,
"{{ [] | first }}" ,
"{{ [] | last }}" ,
"{{ none.attr }}" ,
"{{ none | length }}" ,
"{{ none | default('fallback') }}" ,
"{{ {} | first }}" ,
"{{ {} | dictsort }}" ,
};
for ( const auto & tmpl : empty_tests ) {
t . assert_true ( "empty/none: " + tmpl , fuzz_test_template ( tmpl , json :: object ()));
}
});
t . test ( "arithmetic edge cases" , [ & ]( testing & t ) {
const std :: vector < std :: string > arith_tests = {
"{{ 1 / 0 }}" ,
"{{ 1 // 0 }}" ,
"{{ 1 % 0 }}" ,
"{{ 999999999999999999 * 999999999999999999 }}" ,
"{{ -999999999999999999 - 999999999999999999 }}" ,
"{{ 1.0 / 0.0 }}" ,
"{{ 0.0 / 0.0 }}" ,
};
for ( const auto & tmpl : arith_tests ) {
t . assert_true ( "arith: " + tmpl , fuzz_test_template ( tmpl , json :: object ()));
}
});
t . test ( "deeply nested structures" , [ & ]( testing & t ) {
// Deeply nested loops
for ( int depth = 1 ; depth <= 10 ; ++ depth ) {
std :: string tmpl ;
for ( int d = 0 ; d < depth ; ++ d ) {
tmpl += "{% for i" + std :: to_string ( d ) + " in arr %}" ;
}
tmpl += "x" ;
for ( int d = 0 ; d < depth ; ++ d ) {
tmpl += "{% endfor %}" ;
}
json vars = {{ "arr" , json :: array ({ 1 , 2 })}};
t . assert_true ( "nested loops depth " + std :: to_string ( depth ), fuzz_test_template ( tmpl , vars ));
}
// Deeply nested conditionals
for ( int depth = 1 ; depth <= 10 ; ++ depth ) {
std :: string tmpl ;
for ( int d = 0 ; d < depth ; ++ d ) {
tmpl += "{% if true %}" ;
}
tmpl += "x" ;
for ( int d = 0 ; d < depth ; ++ d ) {
tmpl += "{% endif %}" ;
}
t . assert_true ( "nested ifs depth " + std :: to_string ( depth ), fuzz_test_template ( tmpl , json :: object ()));
}
});
t . test ( "special characters in strings" , [ & ]( testing & t ) {
const std :: vector < std :: string > special_tests = {
"{{ \" }{% \" }}" ,
"{{ \" }}{{ \" }}" ,
"{{ \" {%%} \" }}" ,
"{{ \"\\ n \\ t \\ r \" }}" ,
"{{ \" ' \\\" ' \" }}" ,
"{{ \" hello \\ x00world \" }}" ,
};
for ( const auto & tmpl : special_tests ) {
t . assert_true ( "special: " + tmpl , fuzz_test_template ( tmpl , json :: object ()));
}
});
t . test ( "random template generation" , [ & ]( testing & t ) {
const std :: vector < std :: string > fragments = {
"{{ x }}" , "{{ y }}" , "{{ arr }}" , "{{ obj }}" ,
"{% if true %}a{% endif %}" ,
"{% if false %}b{% else %}c{% endif %}" ,
"{% for i in arr %}{{ i }}{% endfor %}" ,
"{{ x | length }}" , "{{ x | first }}" , "{{ x | default(0) }}" ,
"{{ x + y }}" , "{{ x - y }}" , "{{ x * y }}" ,
"{{ x == y }}" , "{{ x != y }}" , "{{ x > y }}" ,
"{{ range(3) }}" , "{{ \" hello \" | upper }}" ,
"text" , " " , " \n " ,
};
for ( int i = 0 ; i < num_iterations ; ++ i ) {
std :: string tmpl ;
int num_frags = choice_dist ( rng ) % 10 + 1 ;
for ( int f = 0 ; f < num_frags ; ++ f ) {
tmpl += fragments [ choice_dist ( rng ) % fragments . size ()];
}
json vars = {
{ "x" , int_dist ( rng )},
{ "y" , int_dist ( rng )},
{ "arr" , json :: array ({ 1 , 2 , 3 })},
{ "obj" , {{ "a" , 1 }, { "b" , 2 }}}
};
t . assert_true ( "random template #" + std :: to_string ( i ), fuzz_test_template ( tmpl , vars ));
}
});
t . test ( "malformed templates (should error, not crash)" , [ & ]( testing & t ) {
const std :: vector < std :: string > malformed = {
2026-03-22 17:45:10 +01:00
"" ,
2026-01-16 11:22:06 +01:00
"{{ x" ,
"{% if %}" ,
"{% for %}" ,
"{% for x in %}" ,
"{% endfor %}" ,
"{% endif %}" ,
"{{ | filter }}" ,
"{% if x %}" , // unclosed
"{% for i in x %}" , // unclosed
"{{ x | }}" ,
"{% macro %}{% endmacro %}" ,
"{{{{" ,
"}}}}" ,
"{%%}" ,
"{% set %}" ,
"{% set x %}" ,
};
for ( const auto & tmpl : malformed ) {
t . assert_true ( "malformed: " + tmpl , fuzz_test_template ( tmpl , json :: object ()));
}
2026-03-22 17:45:10 +01:00
std :: string tmpl = "{% for message in messages %}{{ message.role | string }} : {{ message.content if ('content' in message and message.content is not none) }}{% endfor %" ;
while ( tmpl . length () > 0 ) {
t . assert_true ( "malformed: " + tmpl , fuzz_test_template ( tmpl , json :: object ()));
tmpl . pop_back ();
}
2026-01-16 11:22:06 +01:00
});
t . test ( "type coercion edge cases" , [ & ]( testing & t ) {
for ( int i = 0 ; i < num_iterations ; ++ i ) {
int op_choice = choice_dist ( rng ) % 6 ;
std :: string op ;
switch ( op_choice ) {
case 0 : op = "+" ; break ;
case 1 : op = "-" ; break ;
case 2 : op = "*" ; break ;
case 3 : op = "/" ; break ;
case 4 : op = "==" ; break ;
case 5 : op = "~" ; break ; // string concat
}
std :: string left_var = var_names [ choice_dist ( rng ) % var_names . size ()];
std :: string right_var = var_names [ choice_dist ( rng ) % var_names . size ()];
std :: string tmpl = "{{ " + left_var + " " + op + " " + right_var + " }}" ;
json vars = {
{ "x" , 42 },
{ "y" , "hello" },
{ "z" , 3.14 },
{ "arr" , json :: array ({ 1 , 2 , 3 })},
{ "obj" , {{ "a" , 1 }}},
{ "items" , json :: array ()},
{ "foo" , nullptr },
{ "bar" , true }
};
t . assert_true ( "type coercion: " + tmpl , fuzz_test_template ( tmpl , vars ));
}
});
t . test ( "fuzz builtin functions" , [ & ]( testing & t ) {
// pair of (type_name, builtin_name)
std :: vector < std :: pair < std :: string , std :: string >> builtins ;
auto add_fns = [ & ]( std :: string type_name , const jinja :: func_builtins & added ) {
for ( const auto & it : added ) {
builtins . push_back ({ type_name , it . first });
}
};
add_fns ( "global" , jinja :: global_builtins ());
add_fns ( "int" , jinja :: value_int_t ( 0 ). get_builtins ());
add_fns ( "float" , jinja :: value_float_t ( 0.0f ). get_builtins ());
add_fns ( "string" , jinja :: value_string_t (). get_builtins ());
add_fns ( "array" , jinja :: value_array_t (). get_builtins ());
add_fns ( "object" , jinja :: value_object_t (). get_builtins ());
const int max_args = 5 ;
const std :: vector < std :: string > kwarg_names = {
"base" , "attribute" , "default" , "reverse" , "case_sensitive" , "by" , "safe" , "chars" , "separators" , "sort_keys" , "indent" , "ensure_ascii" ,
};
// Generate random argument values
auto gen_random_arg = [ & ]() -> std :: string {
int type = choice_dist ( rng ) % 8 ;
switch ( type ) {
case 0 : return std :: to_string ( int_dist ( rng )); // int
case 1 : return std :: to_string ( int_dist ( rng )) + ".5" ; // float
case 2 : return " \" " + random_string ( rng , 10 ) + " \" " ; // string
case 3 : return "true" ; // bool true
case 4 : return "false" ; // bool false
case 5 : return "none" ; // none
case 6 : return "[1, 2, 3]" ; // array
case 7 : return "{ \" a \" : 1}" ; // object
default : return "0" ;
}
};
for ( int i = 0 ; i < num_iterations ; ++ i ) {
// Pick a random builtin
auto & [ type_name , fn_name ] = builtins [ choice_dist ( rng ) % builtins . size ()];
// Generate random number of args
int num_args = choice_dist ( rng ) % ( max_args + 1 );
std :: string args_str ;
for ( int a = 0 ; a < num_args ; ++ a ) {
if ( a > 0 ) args_str += ", " ;
// Sometimes use keyword args
if ( choice_dist ( rng ) % 3 == 0 && ! kwarg_names . empty ()) {
std :: string kwarg = kwarg_names [ choice_dist ( rng ) % kwarg_names . size ()];
args_str += kwarg + "=" + gen_random_arg ();
} else {
args_str += gen_random_arg ();
}
}
std :: string tmpl ;
if ( type_name == "global" ) {
// Global function call
tmpl = "{{ " + fn_name + "(" + args_str + ") }}" ;
} else {
// Method call on a value
std :: string base_val ;
if ( type_name == "int" ) {
base_val = std :: to_string ( int_dist ( rng ));
} else if ( type_name == "float" ) {
base_val = std :: to_string ( int_dist ( rng )) + ".5" ;
} else if ( type_name == "string" ) {
base_val = " \" test_string \" " ;
} else if ( type_name == "array" ) {
base_val = "[1, 2, 3, \" a \" , \" b \" ]" ;
} else if ( type_name == "object" ) {
base_val = "{ \" x \" : 1, \" y \" : 2}" ;
} else {
base_val = "x" ;
}
tmpl = "{{ " + base_val + "." + fn_name + "(" + args_str + ") }}" ;
}
json vars = {
{ "x" , 42 },
{ "y" , "hello" },
{ "arr" , json :: array ({ 1 , 2 , 3 })},
{ "obj" , {{ "a" , 1 }, { "b" , 2 }}}
};
t . assert_true ( "builtin " + type_name + "." + fn_name + " #" + std :: to_string ( i ), fuzz_test_template ( tmpl , vars ));
}
});
2026-04-09 17:28:33 +08:00
t . test ( "tojson ensure_ascii=true with invalid utf-8" , [ & ]( testing & t ) {
t . assert_true ( "invalid utf-8 does not crash" ,
fuzz_test_template (
"{{ data|tojson(ensure_ascii=true) }}" ,
{{ "data" , std :: string ( "hello \xfe\xff world" )}}
));
});
2026-01-16 11:22:06 +01:00
}