From e370323a9ae19ec5b3d34c91097d6bd98375b086 Mon Sep 17 00:00:00 2001
From: Xuan Son Nguyen <son@huggingface.co>
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 7adb302ffbb2..6e13bcbf8c6d 100644
--- a/tests/test-jinja.cpp
+++ b/tests/test-jinja.cpp
@@ -4,6 +4,7 @@
 #include <cstdlib>
 
 #include <nlohmann/json.hpp>
+#include <sheredom/subprocess.h>
 
 #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 <thichthat@gmail.com>
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 <sigbjorn.skjaeret@scala.com>
---
 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 6e13bcbf8c6d..ac20ade76044 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 <son@huggingface.co>
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 ac20ade76044..d09af2b97f1e 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 <son@huggingface.co>
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 d09af2b97f1e..35234fc38d4e 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 <son@huggingface.co>
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 35234fc38d4e..2c5afb845ec1 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"}),
