Safetensors
GGUF
Turkish
llama
Llama-3
instruct
finetune
chatml
gpt4
synthetic data
distillation
function calling
json mode
axolotl
roleplaying
chat
Instructions to use tda45/TdAI with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- llama-cpp-python
How to use tda45/TdAI with llama-cpp-python:
# !pip install llama-cpp-python from llama_cpp import Llama llm = Llama.from_pretrained( repo_id="tda45/TdAI", filename="llama.cpp/models/ggml-vocab-aquila.gguf", )
output = llm( "Once upon a time,", max_tokens=512, echo=True ) print(output)
- Notebooks
- Google Colab
- Kaggle
- Local Apps Settings
- llama.cpp
How to use tda45/TdAI with llama.cpp:
Install (macOS, Linux)
curl -LsSf https://llama.app/install.sh | sh # Start a local OpenAI-compatible server with a web UI: llama serve -hf tda45/TdAI # Run inference directly in the terminal: llama cli -hf tda45/TdAI
Install from WinGet (Windows)
winget install llama.cpp # Start a local OpenAI-compatible server with a web UI: llama serve -hf tda45/TdAI # Run inference directly in the terminal: llama cli -hf tda45/TdAI
Use pre-built binary
# Download pre-built binary from: # https://github.com/ggerganov/llama.cpp/releases # Start a local OpenAI-compatible server with a web UI: ./llama-server -hf tda45/TdAI # Run inference directly in the terminal: ./llama-cli -hf tda45/TdAI
Build from source code
git clone https://github.com/ggerganov/llama.cpp.git cd llama.cpp cmake -B build cmake --build build -j --target llama-server llama-cli # Start a local OpenAI-compatible server with a web UI: ./build/bin/llama-server -hf tda45/TdAI # Run inference directly in the terminal: ./build/bin/llama-cli -hf tda45/TdAI
Use Docker
docker model run hf.co/tda45/TdAI
- LM Studio
- Jan
- Ollama
How to use tda45/TdAI with Ollama:
ollama run hf.co/tda45/TdAI
- Unsloth Studio
How to use tda45/TdAI with Unsloth Studio:
Install Unsloth Studio (macOS, Linux, WSL)
curl -fsSL https://unsloth.ai/install.sh | sh # Run unsloth studio unsloth studio -H 0.0.0.0 -p 8888 # Then open http://localhost:8888 in your browser # Search for tda45/TdAI to start chatting
Install Unsloth Studio (Windows)
irm https://unsloth.ai/install.ps1 | iex # Run unsloth studio unsloth studio -H 0.0.0.0 -p 8888 # Then open http://localhost:8888 in your browser # Search for tda45/TdAI to start chatting
Using HuggingFace Spaces for Unsloth
# No setup required # Open https://huggingface.co/spaces/unsloth/studio in your browser # Search for tda45/TdAI to start chatting
- Atomic Chat new
- Docker Model Runner
How to use tda45/TdAI with Docker Model Runner:
docker model run hf.co/tda45/TdAI
- Lemonade
How to use tda45/TdAI with Lemonade:
Pull the model
# Download Lemonade from https://lemonade-server.ai/ lemonade pull tda45/TdAI
Run and chat with the model
lemonade run user.TdAI-{{QUANT_TAG}}List all available models
lemonade list
| 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); | |
| static void test_hasher(testing & t); | |
| static void test_stats(testing & t); | |
| static void test_fuzzing(testing & t); | |
| static bool g_python_mode = false; | |
| int main(int argc, char *argv[]) { | |
| testing t(std::cout); | |
| t.verbose = true; | |
| // 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); | |
| } | |
| } | |
| 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); | |
| if (!g_python_mode) { | |
| t.test("hasher", test_hasher); | |
| t.test("stats", test_stats); | |
| t.test("fuzzing", test_fuzzing); | |
| } | |
| 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\n2\n3\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" | |
| ); | |
| 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" | |
| ); | |
| 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" | |
| ); | |
| test_template(t, "in operator (element in array)", | |
| "{% if 'x' in items %}found{% endif %}", | |
| {{"items", json::array({"x", "y"})}}, | |
| "found" | |
| ); | |
| 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" | |
| ); | |
| 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" | |
| ); | |
| 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" | |
| ); | |
| } | |
| 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" | |
| ); | |
| test_template(t, "for undefined empty", | |
| "{% for i in items %}{{ i }}{% else %}empty{% endfor %}", | |
| json::object(), | |
| "empty" | |
| ); | |
| 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" | |
| ); | |
| test_template(t, "negative float (not dot notation)", | |
| "{{ -1.0 }}", | |
| json::object(), | |
| "-1.0" | |
| ); | |
| test_template(t, "bracket notation", | |
| "{{ user['name'] }}", | |
| {{"user", {{"name", "Bob"}}}}, | |
| "Bob" | |
| ); | |
| 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" | |
| ); | |
| test_template(t, "array access", | |
| "{{ items[1] }}", | |
| {{"items", json::array({"a", "b", "c"})}}, | |
| "b" | |
| ); | |
| 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')" | |
| ); | |
| test_template(t, "string slice negative step", | |
| "{{ 'abcdef'[::-2] }}", | |
| json::object(), | |
| "fdb" | |
| ); | |
| test_template(t, "string slice negative start and step", | |
| "{{ 'abcdef'[-1:1:-1] }}", | |
| json::object(), | |
| "fedc" | |
| ); | |
| test_template(t, "string slice negative start, stop and step", | |
| "{{ 'abcdef'[-1:-5:-1] }}", | |
| json::object(), | |
| "fedc" | |
| ); | |
| 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" | |
| ); | |
| test_template(t, "string repetition", | |
| "{{ 'ab' * 3 }}", | |
| json::object(), | |
| "ababab" | |
| ); | |
| test_template(t, "reversed string repetition", | |
| "{{ 3 * 'ab' }}", | |
| json::object(), | |
| "ababab" | |
| ); | |
| 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" | |
| ); | |
| 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" | |
| ); | |
| } | |
| static void test_filters(testing & t) { | |
| test_template(t, "upper", | |
| "{{ 'hello'|upper }}", | |
| json::object(), | |
| "HELLO" | |
| ); | |
| test_template(t, "lower", | |
| "{{ 'HELLO'|lower }}", | |
| json::object(), | |
| "hello" | |
| ); | |
| 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'}" | |
| ); | |
| 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\thello\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 (count alias) list", | |
| "{{ items|count }}", | |
| {{"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" | |
| ); | |
| 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" | |
| ); | |
| 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 (d alias) with falsy value", | |
| "{{ ''|d('fallback', true) }}", | |
| json::object(), | |
| "fallback" | |
| ); | |
| test_template(t, "tojson ensure_ascii=true", | |
| "{{ data|tojson(ensure_ascii=true) }}", | |
| {{"data", "\u2713"}}, | |
| "\"\\u2713\"" | |
| ); | |
| 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\nb"} | |
| }}}, | |
| "{\"emoji\": \"\\ud83d\\ude00\", \"line\": \"a\\nb\"}" | |
| ); | |
| 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}" | |
| ); | |
| test_template(t, "indent", | |
| "{{ data|indent(2) }}", | |
| {{ "data", "foo\nbar" }}, | |
| "foo\n bar" | |
| ); | |
| test_template(t, "indent first only", | |
| "{{ data|indent(width=3,first=true) }}", | |
| {{ "data", "foo\nbar" }}, | |
| " foo\n bar" | |
| ); | |
| test_template(t, "indent blank lines and first line", | |
| "{{ data|indent(width=5,blank=true,first=true) }}", | |
| {{ "data", "foo\n\nbar" }}, | |
| " foo\n \n bar" | |
| ); | |
| test_template(t, "indent with default width", | |
| "{{ data|indent() }}", | |
| {{ "data", "foo\nbar" }}, | |
| "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\nbar" }}, | |
| "foo\n>>>>bar" | |
| ); | |
| test_template(t, "chained filters", | |
| "{{ ' HELLO '|trim|lower }}", | |
| json::object(), | |
| "hello" | |
| ); | |
| test_template(t, "int filter on integer is identity", | |
| "{{ value|int }}", | |
| {{"value", 7}}, | |
| "7" | |
| ); | |
| test_template(t, "none to string", | |
| "{{ x|string }}", | |
| {{"x", nullptr}}, | |
| "None" | |
| ); | |
| } | |
| 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" | |
| ); | |
| 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" | |
| ); | |
| } | |
| 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" | |
| ); | |
| 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" | |
| ); | |
| test_template(t, "macro with caller", | |
| "\ | |
| {%- macro nest_dict(o, i, ff='') %}\n\ | |
| {{- caller(ff) }}\n\ | |
| {%- for k, v in o|items %}\n\ | |
| {{- i + k + ': ' }}\n\ | |
| {%- if v is mapping %}\n\ | |
| {{- '{' }}\n\ | |
| {% call(f) nest_dict(v, i + ' ') %}\n\ | |
| {{- 'fail' if ff is undefined }}\n\ | |
| {%- endcall %}\n\ | |
| {{- i + '}' }}\n\ | |
| {% else %}\n\ | |
| {{- v|string }}\n\ | |
| {% endif %}\n\ | |
| {%- endfor %}\n\ | |
| {%- endmacro %}\n\ | |
| {%- call(f) nest_dict({'root1': 1, 'root2': {'nest1': 1, 'nest2': {'nest3': 2}}}, ' ', 'Dict') %}\n\ | |
| {{- 'fail' if ff is defined }}\n\ | |
| {{- f + ' {' }}\n\ | |
| {% endcall %}\n\ | |
| {{- '}' }}", | |
| json::object(), | |
| "Dict {\n root1: 1\n root2: {\n nest1: 1\n nest2: {\n nest3: 2\n }\n }\n}" | |
| ); | |
| } | |
| 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" | |
| ); | |
| 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" | |
| ); | |
| 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" | |
| ); | |
| } | |
| 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" | |
| ); | |
| test_template(t, "string.replace() empty", | |
| "{{ s.replace('', '.') }}", | |
| {{"s", "hello world"}}, | |
| ".h.e.l.l.o. .w.o.r.l.d." | |
| ); | |
| test_template(t, "string.replace() with count", | |
| "{{ s.replace('a', 'X', 2) }}", | |
| {{"s", "banana"}}, | |
| "bXnXna" | |
| ); | |
| 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" | |
| ); | |
| } | |
| 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", | |
| "{{ 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) }}", | |
| {{"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" | |
| ); | |
| test_template(t, "array|map with attribute", | |
| "{% 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"}, {"age", 3}}), | |
| })}}, | |
| "1 2 3 " | |
| ); | |
| 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 %}", | |
| {{"arr", json::array({ | |
| json::array({10, "x"}), | |
| json::array({20, "y"}), | |
| json::array({30, "z"}), | |
| })}}, | |
| "10 20 30 " | |
| ); | |
| 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" | |
| ); | |
| test_template(t, "array|min", | |
| "{{ [tool_calls_count, tool_sep_count]|min }}", | |
| {{"tool_calls_count", 2}, {"tool_sep_count", 1}}, | |
| "1" | |
| ); | |
| test_template(t, "array|max", | |
| "{{ [tool_calls_count, tool_sep_count]|max }}", | |
| {{"tool_calls_count", 2}, {"tool_sep_count", 1}}, | |
| "2" | |
| ); | |
| test_template(t, "array|min attribute", | |
| "{{ items|min(attribute='x') }}", | |
| {{"items", json::array({ | |
| json({{"x", 2}}), | |
| json({{"x", 1}}), | |
| })}}, | |
| "{'x': 1}" | |
| ); | |
| test_template(t, "array|max attribute", | |
| "{{ items|max(attribute='x') }}", | |
| {{"items", json::array({ | |
| json({{"x", 2}}), | |
| json({{"x", 1}}), | |
| })}}, | |
| "{'x': 2}" | |
| ); | |
| // 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" | |
| // ); | |
| 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" | |
| ); | |
| } | |
| 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]}" | |
| ); | |
| 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" | |
| ); | |
| 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" | |
| ); | |
| test_template(t, "undefined|items", | |
| "{{ arr|items|join }}", | |
| json::object(), | |
| "" | |
| ); | |
| } | |
| 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; | |
| } | |
| }); | |
| } | |
| 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); | |
| }); | |
| } | |
| static void test_template_cpp(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) { | |
| 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())); | |
| } | |
| }); | |
| } | |
| // 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 | |
| merged_input = json.loads(sys.stdin.buffer.read().decode("utf-8")) | |
| tmpl = merged_input["tmpl"] | |
| vars_json = merged_input["vars"] | |
| 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) | |
| sys.stdout.buffer.write(result.encode()) | |
| )"; | |
| 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 | |
| json merged; | |
| merged["tmpl"] = json(tmpl); | |
| merged["vars"] = vars; | |
| const char * python_executable = "python.exe"; | |
| const char * python_executable = "python3"; | |
| const char * command_line[] = {python_executable, "-c", py_script.c_str(), NULL}; | |
| 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; | |
| } | |
| 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; | |
| // 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); | |
| } | |
| } | |
| // | |
| // 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 = { | |
| "", | |
| "{{ 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())); | |
| } | |
| 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(); | |
| } | |
| }); | |
| 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)); | |
| } | |
| }); | |
| 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\xffworld")}} | |
| )); | |
| }); | |
| } | |