From e370323a9ae19ec5b3d34c91097d6bd98375b086 Mon Sep 17 00:00:00 2001 From: Xuan Son Nguyen Date: Sun, 18 Jan 2026 00:10:08 +0100 Subject: [PATCH 1/5] tests : add test-jinja -py option or cross-checking --- tests/test-jinja.cpp | 102 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 98 insertions(+), 4 deletions(-) diff --git a/tests/test-jinja.cpp b/tests/test-jinja.cpp index 7adb302ffbb..6e13bcbf8c6 100644 --- a/tests/test-jinja.cpp +++ b/tests/test-jinja.cpp @@ -4,6 +4,7 @@ #include #include +#include #include "jinja/runtime.h" #include "jinja/parser.h" @@ -31,12 +32,24 @@ static void test_array_methods(testing & t); static void test_object_methods(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; - if (argc >= 2) { - t.set_filter(argv[1]); + // 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); @@ -53,7 +66,9 @@ int main(int argc, char *argv[]) { t.test("string methods", test_string_methods); t.test("array methods", test_array_methods); t.test("object methods", test_object_methods); - t.test("fuzzing", test_fuzzing); + if (!g_python_mode) { + t.test("fuzzing", test_fuzzing); + } return t.summary(); } @@ -1065,7 +1080,7 @@ static void test_object_methods(testing & t) { ); } -static void test_template(testing & t, const std::string & name, const std::string & tmpl, const json & vars, const std::string & expect) { +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); @@ -1098,6 +1113,85 @@ static void test_template(testing & t, const std::string & name, const std::stri }); } +static std::string py_script = R"( +import jinja2 +import json +import sys + +tmpl = json.loads(sys.argv[1]) +vars_json = json.loads(sys.argv[2]) + +env = jinja2.Environment( + trim_blocks=True, + lstrip_blocks=True, +) +template = env.from_string(tmpl) +result = template.render(**vars_json) +print(result, end='') +)"; + +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 + std::string tmpl_json = json(tmpl).dump(); + std::string vars_json = vars.dump(); + +#ifdef _WIN32 + const char * python_executable = "python.exe"; +#else + const char * python_executable = "python3"; +#endif + + const char * command_line[] = {python_executable, "-c", py_script.c_str(), tmpl_json.c_str(), vars_json.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; + } + + // 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 // From 8787de5b9d3741cc65526eec6621e25393eadf7f Mon Sep 17 00:00:00 2001 From: Xuan-Son Nguyen Date: Sun, 18 Jan 2026 00:32:50 +0100 Subject: [PATCH 2/5] Update tests/test-jinja.cpp MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sigbjørn Skjæret --- tests/test-jinja.cpp | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/tests/test-jinja.cpp b/tests/test-jinja.cpp index 6e13bcbf8c6..ac20ade7604 100644 --- a/tests/test-jinja.cpp +++ b/tests/test-jinja.cpp @@ -1115,16 +1115,23 @@ static void test_template_cpp(testing & t, const std::string & name, const std:: 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 ImmutableSandboxedEnvironment tmpl = json.loads(sys.argv[1]) vars_json = json.loads(sys.argv[2]) -env = jinja2.Environment( +env = ImmutableSandboxedEnvironment( trim_blocks=True, lstrip_blocks=True, + extensions=[jinja2_ext.loopcontrols], ) +environment.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) +environment.globals["strftime_now"] = lambda format: datetime.now().strftime(format) +environment.globals["raise_exception"] = lambda message: raise jinja2.exceptions.TemplateError(message) template = env.from_string(tmpl) result = template.render(**vars_json) print(result, end='') From 1d7a9eb0692180e32ce257deb167e874930396f6 Mon Sep 17 00:00:00 2001 From: Xuan Son Nguyen Date: Sun, 18 Jan 2026 00:36:00 +0100 Subject: [PATCH 3/5] fix + add source --- tests/test-jinja.cpp | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/tests/test-jinja.cpp b/tests/test-jinja.cpp index ac20ade7604..d09af2b97f1 100644 --- a/tests/test-jinja.cpp +++ b/tests/test-jinja.cpp @@ -1113,6 +1113,7 @@ static void test_template_cpp(testing & t, const std::string & name, const std:: }); } +// keep this in-sync with https://github.com/huggingface/transformers/blob/main/src/transformers/utils/chat_template_utils.py static std::string py_script = R"( import jinja2 import jinja2.ext as jinja2_ext @@ -1129,9 +1130,14 @@ env = ImmutableSandboxedEnvironment( lstrip_blocks=True, extensions=[jinja2_ext.loopcontrols], ) -environment.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) -environment.globals["strftime_now"] = lambda format: datetime.now().strftime(format) -environment.globals["raise_exception"] = lambda message: raise jinja2.exceptions.TemplateError(message) + +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) print(result, end='') From 7b54480c7418f00bf217b0bc944486ff80f1696a Mon Sep 17 00:00:00 2001 From: Xuan Son Nguyen Date: Sun, 18 Jan 2026 00:41:48 +0100 Subject: [PATCH 4/5] SandboxedEnvironment --- tests/test-jinja.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/test-jinja.cpp b/tests/test-jinja.cpp index d09af2b97f1..35234fc38d4 100644 --- a/tests/test-jinja.cpp +++ b/tests/test-jinja.cpp @@ -1114,18 +1114,19 @@ static void test_template_cpp(testing & t, const std::string & name, const std:: } // 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 ImmutableSandboxedEnvironment +from jinja2.sandbox import SandboxedEnvironment tmpl = json.loads(sys.argv[1]) vars_json = json.loads(sys.argv[2]) -env = ImmutableSandboxedEnvironment( +env = SandboxedEnvironment( trim_blocks=True, lstrip_blocks=True, extensions=[jinja2_ext.loopcontrols], From 7c2d857556f48dc413b8685334308e3112d54d5f Mon Sep 17 00:00:00 2001 From: Xuan Son Nguyen Date: Sun, 18 Jan 2026 00:46:57 +0100 Subject: [PATCH 5/5] fix array.map case --- tests/test-jinja.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test-jinja.cpp b/tests/test-jinja.cpp index 35234fc38d4..2c5afb845ec 100644 --- a/tests/test-jinja.cpp +++ b/tests/test-jinja.cpp @@ -973,7 +973,7 @@ static void test_array_methods(testing & t) { ); test_template(t, "array.map() with attribute", - "{% for v in arr.map('age') %}{{ v }} {% endfor %}", + "{% for v in arr|map(attribute='age') %}{{ v }} {% endfor %}", {{"arr", json::array({ json({{"name", "a"}, {"age", 1}}), json({{"name", "b"}, {"age", 2}}), @@ -983,7 +983,7 @@ static void test_array_methods(testing & t) { ); test_template(t, "array.map() with numeric attribute", - "{% for v in arr.map(0) %}{{ v }} {% endfor %}", + "{% for v in arr|map(attribute=0) %}{{ v }} {% endfor %}", {{"arr", json::array({ json::array({10, "x"}), json::array({20, "y"}),