1#include <string>
   2#include <iostream>
   3#include <random>
   4#include <cstdlib>
   5
   6#include <nlohmann/json.hpp>
   7#include <sheredom/subprocess.h>
   8
   9#include "jinja/runtime.h"
  10#include "jinja/parser.h"
  11#include "jinja/lexer.h"
  12#include "jinja/utils.h"
  13
  14#include "testing.h"
  15
  16using json = nlohmann::ordered_json;
  17
  18static void test_template(testing & t, const std::string & name, const std::string & tmpl, const json & vars, const std::string & expect);
  19
  20static void test_whitespace_control(testing & t);
  21static void test_conditionals(testing & t);
  22static void test_loops(testing & t);
  23static void test_expressions(testing & t);
  24static void test_set_statement(testing & t);
  25static void test_filters(testing & t);
  26static void test_literals(testing & t);
  27static void test_comments(testing & t);
  28static void test_macros(testing & t);
  29static void test_namespace(testing & t);
  30static void test_tests(testing & t);
  31static void test_string_methods(testing & t);
  32static void test_array_methods(testing & t);
  33static void test_object_methods(testing & t);
  34static void test_hasher(testing & t);
  35static void test_fuzzing(testing & t);
  36
  37static bool g_python_mode = false;
  38
  39int main(int argc, char *argv[]) {
  40    testing t(std::cout);
  41    t.verbose = true;
  42
  43    // usage: test-jinja [-py] [filter_regex]
  44    //  -py : enable python mode (use python jinja2 for rendering expected output)
  45    //        only use this for cross-checking, not for correctness
  46    //        note: the implementation of this flag is basic, only intented to be used by maintainers
  47
  48    for (int i = 1; i < argc; i++) {
  49        std::string arg = argv[i];
  50        if (arg == "-py") {
  51            g_python_mode = true;
  52        } else {
  53            t.set_filter(arg);
  54        }
  55    }
  56
  57    t.test("whitespace control", test_whitespace_control);
  58    t.test("conditionals", test_conditionals);
  59    t.test("loops", test_loops);
  60    t.test("expressions", test_expressions);
  61    t.test("set statement", test_set_statement);
  62    t.test("filters", test_filters);
  63    t.test("literals", test_literals);
  64    t.test("comments", test_comments);
  65    t.test("macros", test_macros);
  66    t.test("namespace", test_namespace);
  67    t.test("tests", test_tests);
  68    t.test("string methods", test_string_methods);
  69    t.test("array methods", test_array_methods);
  70    t.test("object methods", test_object_methods);
  71    if (!g_python_mode) {
  72        t.test("hasher", test_hasher);
  73        t.test("fuzzing", test_fuzzing);
  74    }
  75
  76    return t.summary();
  77}
  78
  79static void test_whitespace_control(testing & t) {
  80    test_template(t, "trim_blocks removes newline after tag",
  81        "{% if true %}\n"
  82        "hello\n"
  83        "{% endif %}\n",
  84        json::object(),
  85        "hello\n"
  86    );
  87
  88    test_template(t, "lstrip_blocks removes leading whitespace",
  89        "    {% if true %}\n"
  90        "    hello\n"
  91        "    {% endif %}\n",
  92        json::object(),
  93        "    hello\n"
  94    );
  95
  96    test_template(t, "for loop with trim_blocks",
  97        "{% for i in items %}\n"
  98        "{{ i }}\n"
  99        "{% endfor %}\n",
 100        {{"items", json::array({1, 2, 3})}},
 101        "1\n2\n3\n"
 102    );
 103
 104    test_template(t, "explicit strip both",
 105        "  {%- if true -%}  \n"
 106        "hello\n"
 107        "  {%- endif -%}  \n",
 108        json::object(),
 109        "hello"
 110    );
 111
 112    test_template(t, "expression whitespace control",
 113        "  {{- 'hello' -}}  \n",
 114        json::object(),
 115        "hello"
 116    );
 117
 118    test_template(t, "inline block no newline",
 119        "{% if true %}yes{% endif %}",
 120        json::object(),
 121        "yes"
 122    );
 123}
 124
 125static void test_conditionals(testing & t) {
 126    test_template(t, "if true",
 127        "{% if cond %}yes{% endif %}",
 128        {{"cond", true}},
 129        "yes"
 130    );
 131
 132    test_template(t, "if false",
 133        "{% if cond %}yes{% endif %}",
 134        {{"cond", false}},
 135        ""
 136    );
 137
 138    test_template(t, "if else",
 139        "{% if cond %}yes{% else %}no{% endif %}",
 140        {{"cond", false}},
 141        "no"
 142    );
 143
 144    test_template(t, "if elif else",
 145        "{% if a %}A{% elif b %}B{% else %}C{% endif %}",
 146        {{"a", false}, {"b", true}},
 147        "B"
 148    );
 149
 150    test_template(t, "nested if",
 151        "{% if outer %}{% if inner %}both{% endif %}{% endif %}",
 152        {{"outer", true}, {"inner", true}},
 153        "both"
 154    );
 155
 156    test_template(t, "comparison operators",
 157        "{% if x > 5 %}big{% endif %}",
 158        {{"x", 10}},
 159        "big"
 160    );
 161
 162    test_template(t, "object comparison",
 163        "{% if {0: 1, none: 2, 1.0: 3, '0': 4, true: 5} == {false: 1, none: 2, 1: 5, '0': 4} %}equal{% endif %}",
 164        json::object(),
 165        "equal"
 166    );
 167
 168    test_template(t, "array comparison",
 169        "{% if [0, 1.0, false] == [false, 1, 0.0] %}equal{% endif %}",
 170        json::object(),
 171        "equal"
 172    );
 173
 174    test_template(t, "logical and",
 175        "{% if a and b %}both{% endif %}",
 176        {{"a", true}, {"b", true}},
 177        "both"
 178    );
 179
 180    test_template(t, "logical or",
 181        "{% if a or b %}either{% endif %}",
 182        {{"a", false}, {"b", true}},
 183        "either"
 184    );
 185
 186    test_template(t, "logical not",
 187        "{% if not a %}negated{% endif %}",
 188        {{"a", false}},
 189        "negated"
 190    );
 191
 192    test_template(t, "in operator (element in array)",
 193        "{% if 'x' in items %}found{% endif %}",
 194        {{"items", json::array({"x", "y"})}},
 195        "found"
 196    );
 197
 198    test_template(t, "in operator (substring)",
 199        "{% if 'bc' in 'abcd' %}found{% endif %}",
 200        json::object(),
 201        "found"
 202    );
 203
 204    test_template(t, "in operator (object key)",
 205        "{% if 'key' in obj %}found{% endif %}",
 206        {{"obj", {{"key", 1}, {"other", 2}}}},
 207        "found"
 208    );
 209
 210    test_template(t, "is defined",
 211        "{% if x is defined %}yes{% else %}no{% endif %}",
 212        {{"x", 1}},
 213        "yes"
 214    );
 215
 216    test_template(t, "is not defined",
 217        "{% if y is not defined %}yes{% else %}no{% endif %}",
 218        json::object(),
 219        "yes"
 220    );
 221
 222    test_template(t, "is undefined falsy",
 223        "{{ 'yes' if not y else 'no' }}",
 224        json::object(),
 225        "yes"
 226    );
 227
 228    test_template(t, "is undefined attribute falsy",
 229        "{{ 'yes' if not y.x else 'no' }}",
 230        {{"y", true}},
 231        "yes"
 232    );
 233
 234    test_template(t, "is undefined key falsy",
 235        "{{ 'yes' if not y['x'] else 'no' }}",
 236        {{"y", {{}}}},
 237        "yes"
 238    );
 239
 240    test_template(t, "is empty array falsy",
 241        "{{ 'yes' if not y else 'no' }}",
 242        {{"y", json::array()}},
 243        "yes"
 244    );
 245
 246    test_template(t, "is empty object falsy",
 247        "{{ 'yes' if not y else 'no' }}",
 248        {{"y", json::object()}},
 249        "yes"
 250    );
 251
 252    test_template(t, "is empty string falsy",
 253        "{{ 'yes' if not y else 'no' }}",
 254        {{"y", ""}},
 255        "yes"
 256    );
 257
 258    test_template(t, "is 0 falsy",
 259        "{{ 'yes' if not y else 'no' }}",
 260        {{"y", 0}},
 261        "yes"
 262    );
 263
 264    test_template(t, "is 0.0 falsy",
 265        "{{ 'yes' if not y else 'no' }}",
 266        {{"y", 0.0}},
 267        "yes"
 268    );
 269
 270    test_template(t, "is non-empty array truthy",
 271        "{{ 'yes' if y else 'no' }}",
 272        {{"y", json::array({""})}},
 273        "yes"
 274    );
 275
 276    test_template(t, "is non-empty object truthy",
 277        "{{ 'yes' if y else 'no' }}",
 278        {{"y", {"x", false}}},
 279        "yes"
 280    );
 281
 282    test_template(t, "is non-empty string truthy",
 283        "{{ 'yes' if y else 'no' }}",
 284        {{"y", "0"}},
 285        "yes"
 286    );
 287
 288    test_template(t, "is 1 truthy",
 289        "{{ 'yes' if y else 'no' }}",
 290        {{"y", 1}},
 291        "yes"
 292    );
 293
 294    test_template(t, "is 1.0 truthy",
 295        "{{ 'yes' if y else 'no' }}",
 296        {{"y", 1.0}},
 297        "yes"
 298    );
 299}
 300
 301static void test_loops(testing & t) {
 302    test_template(t, "simple for",
 303        "{% for i in items %}{{ i }}{% endfor %}",
 304        {{"items", json::array({1, 2, 3})}},
 305        "123"
 306    );
 307
 308    test_template(t, "loop.index",
 309        "{% for i in items %}{{ loop.index }}{% endfor %}",
 310        {{"items", json::array({"a", "b", "c"})}},
 311        "123"
 312    );
 313
 314    test_template(t, "loop.index0",
 315        "{% for i in items %}{{ loop.index0 }}{% endfor %}",
 316        {{"items", json::array({"a", "b", "c"})}},
 317        "012"
 318    );
 319
 320    test_template(t, "loop.first and loop.last",
 321        "{% for i in items %}{% if loop.first %}[{% endif %}{{ i }}{% if loop.last %}]{% endif %}{% endfor %}",
 322        {{"items", json::array({1, 2, 3})}},
 323        "[123]"
 324    );
 325
 326    test_template(t, "loop.length",
 327        "{% for i in items %}{{ loop.length }}{% endfor %}",
 328        {{"items", json::array({"a", "b"})}},
 329        "22"
 330    );
 331
 332    test_template(t, "for over dict items",
 333        "{% for k, v in data.items() %}{{ k }}={{ v }} {% endfor %}",
 334        {{"data", {{"x", 1}, {"y", 2}}}},
 335        "x=1 y=2 "
 336    );
 337
 338    test_template(t, "for else empty",
 339        "{% for i in items %}{{ i }}{% else %}empty{% endfor %}",
 340        {{"items", json::array()}},
 341        "empty"
 342    );
 343
 344    test_template(t, "for undefined empty",
 345        "{% for i in items %}{{ i }}{% else %}empty{% endfor %}",
 346        json::object(),
 347        "empty"
 348    );
 349
 350    test_template(t, "nested for",
 351        "{% for i in a %}{% for j in b %}{{ i }}{{ j }}{% endfor %}{% endfor %}",
 352        {{"a", json::array({1, 2})}, {"b", json::array({"x", "y"})}},
 353        "1x1y2x2y"
 354    );
 355
 356    test_template(t, "for with range",
 357        "{% for i in range(3) %}{{ i }}{% endfor %}",
 358        json::object(),
 359        "012"
 360    );
 361}
 362
 363static void test_expressions(testing & t) {
 364    test_template(t, "simple variable",
 365        "{{ x }}",
 366        {{"x", 42}},
 367        "42"
 368    );
 369
 370    test_template(t, "dot notation",
 371        "{{ user.name }}",
 372        {{"user", {{"name", "Bob"}}}},
 373        "Bob"
 374    );
 375
 376    test_template(t, "negative float (not dot notation)",
 377        "{{ -1.0 }}",
 378        json::object(),
 379        "-1.0"
 380    );
 381
 382    test_template(t, "bracket notation",
 383        "{{ user['name'] }}",
 384        {{"user", {{"name", "Bob"}}}},
 385        "Bob"
 386    );
 387
 388    test_template(t, "array access",
 389        "{{ items[1] }}",
 390        {{"items", json::array({"a", "b", "c"})}},
 391        "b"
 392    );
 393
 394    test_template(t, "array negative access",
 395        "{{ items[-1] }}",
 396        {{"items", json::array({"a", "b", "c"})}},
 397        "c"
 398    );
 399
 400    test_template(t, "array slice",
 401        "{{ items[1:-1]|string }}",
 402        {{"items", json::array({"a", "b", "c"})}},
 403        "['b']"
 404    );
 405
 406    test_template(t, "array slice step",
 407        "{{ items[::2]|string }}",
 408        {{"items", json::array({"a", "b", "c"})}},
 409        "['a', 'c']"
 410    );
 411
 412    test_template(t, "tuple slice",
 413        "{{ ('a', 'b', 'c')[::-1]|string }}",
 414        json::object(),
 415        "('c', 'b', 'a')"
 416    );
 417
 418    test_template(t, "arithmetic",
 419        "{{ (a + b) * c }}",
 420        {{"a", 2}, {"b", 3}, {"c", 4}},
 421        "20"
 422    );
 423
 424    test_template(t, "string concat ~",
 425        "{{ 'hello' ~ ' ' ~ 'world' }}",
 426        json::object(),
 427        "hello world"
 428    );
 429
 430    test_template(t, "ternary",
 431        "{{ 'yes' if cond else 'no' }}",
 432        {{"cond", true}},
 433        "yes"
 434    );
 435}
 436
 437static void test_set_statement(testing & t) {
 438    test_template(t, "simple set",
 439        "{% set x = 5 %}{{ x }}",
 440        json::object(),
 441        "5"
 442    );
 443
 444    test_template(t, "set with expression",
 445        "{% set x = a + b %}{{ x }}",
 446        {{"a", 10}, {"b", 20}},
 447        "30"
 448    );
 449
 450    test_template(t, "set list",
 451        "{% set items = [1, 2, 3] %}{{ items|length }}",
 452        json::object(),
 453        "3"
 454    );
 455
 456    test_template(t, "set dict",
 457        "{% set d = {'a': 1} %}{{ d.a }}",
 458        json::object(),
 459        "1"
 460    );
 461
 462    test_template(t, "set dict with mixed type keys",
 463        "{% 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] }}",
 464        json::object(),
 465        "37"
 466    );
 467
 468    test_template(t, "print dict with mixed type keys",
 469        "{% set d = {0: 1, none: 2, 1.0: 3, '0': 4, (0, 0): 5, true: 6} %}{{ d|string }}",
 470        json::object(),
 471        "{0: 1, None: 2, 1.0: 6, '0': 4, (0, 0): 5}"
 472    );
 473
 474    test_template(t, "print array with mixed types",
 475        "{% set d = [0, none, 1.0, '0', true, (0, 0)] %}{{ d|string }}",
 476        json::object(),
 477        "[0, None, 1.0, '0', True, (0, 0)]"
 478    );
 479
 480    test_template(t, "object member assignment with mixed key types",
 481        "{% set d = namespace() %}{% set d.a = 123 %}{{ d['a'] == 123 }}",
 482        json::object(),
 483        "True"
 484    );
 485
 486    test_template(t, "tuple unpacking",
 487        "{% set t = (1, 2, 3) %}{% set a, b, c = t %}{{ a + b + c }}",
 488        json::object(),
 489        "6"
 490    );
 491}
 492
 493static void test_filters(testing & t) {
 494    test_template(t, "upper",
 495        "{{ 'hello'|upper }}",
 496        json::object(),
 497        "HELLO"
 498    );
 499
 500    test_template(t, "lower",
 501        "{{ 'HELLO'|lower }}",
 502        json::object(),
 503        "hello"
 504    );
 505
 506    test_template(t, "capitalize",
 507        "{{ 'heLlo World'|capitalize }}",
 508        json::object(),
 509        "Hello world"
 510    );
 511
 512    test_template(t, "title",
 513        "{{ 'hello world'|title }}",
 514        json::object(),
 515        "Hello World"
 516    );
 517
 518    test_template(t, "trim",
 519        "{{ '  \r\n\thello\t\n\r  '|trim }}",
 520        json::object(),
 521        "hello"
 522    );
 523
 524    test_template(t, "trim chars",
 525        "{{ 'xyxhelloxyx'|trim('xy') }}",
 526        json::object(),
 527        "hello"
 528    );
 529
 530    test_template(t, "length string",
 531        "{{ 'hello'|length }}",
 532        json::object(),
 533        "5"
 534    );
 535
 536    test_template(t, "replace",
 537        "{{ 'hello world'|replace('world', 'jinja') }}",
 538        json::object(),
 539        "hello jinja"
 540    );
 541
 542    test_template(t, "length list",
 543        "{{ items|length }}",
 544        {{"items", json::array({1, 2, 3})}},
 545        "3"
 546    );
 547
 548    test_template(t, "first",
 549        "{{ items|first }}",
 550        {{"items", json::array({10, 20, 30})}},
 551        "10"
 552    );
 553
 554    test_template(t, "last",
 555        "{{ items|last }}",
 556        {{"items", json::array({10, 20, 30})}},
 557        "30"
 558    );
 559
 560    test_template(t, "reverse",
 561        "{% for i in items|reverse %}{{ i }}{% endfor %}",
 562        {{"items", json::array({1, 2, 3})}},
 563        "321"
 564    );
 565
 566    test_template(t, "sort",
 567        "{% for i in items|sort %}{{ i }}{% endfor %}",
 568        {{"items", json::array({3, 1, 2})}},
 569        "123"
 570    );
 571
 572    test_template(t, "sort reverse",
 573        "{% for i in items|sort(true) %}{{ i }}{% endfor %}",
 574        {{"items", json::array({3, 1, 2})}},
 575        "321"
 576    );
 577
 578    test_template(t, "sort with attribute",
 579        "{{ items|sort(attribute='name')|join(attribute='age') }}",
 580        {{"items", json::array({
 581            json({{"name", "c"}, {"age", 3}}),
 582            json({{"name", "a"}, {"age", 1}}),
 583            json({{"name", "b"}, {"age", 2}}),
 584        })}},
 585        "123"
 586    );
 587
 588    test_template(t, "sort with numeric attribute",
 589        "{{ items|sort(attribute=0)|join(attribute=1) }}",
 590        {{"items", json::array({
 591            json::array({3, "z"}),
 592            json::array({1, "x"}),
 593            json::array({2, "y"}),
 594        })}},
 595        "xyz"
 596    );
 597
 598    test_template(t, "join",
 599        "{{ items|join(', ') }}",
 600        {{"items", json::array({"a", "b", "c"})}},
 601        "a, b, c"
 602    );
 603
 604    test_template(t, "join default separator",
 605        "{{ items|join }}",
 606        {{"items", json::array({"x", "y", "z"})}},
 607        "xyz"
 608    );
 609
 610    test_template(t, "abs",
 611        "{{ -5|abs }}",
 612        json::object(),
 613        "5"
 614    );
 615
 616    test_template(t, "int from string",
 617        "{{ '42'|int }}",
 618        json::object(),
 619        "42"
 620    );
 621
 622    test_template(t, "int from string with default",
 623        "{{ ''|int(1) }}",
 624        json::object(),
 625        "1"
 626    );
 627
 628    test_template(t, "int from string with base",
 629        "{{ '11'|int(base=2) }}",
 630        json::object(),
 631        "3"
 632    );
 633
 634    test_template(t, "float from string",
 635        "{{ '3.14'|float }}",
 636        json::object(),
 637        "3.14"
 638    );
 639
 640    test_template(t, "default with value",
 641        "{{ x|default('fallback') }}",
 642        {{"x", "actual"}},
 643        "actual"
 644    );
 645
 646    test_template(t, "default without value",
 647        "{{ y|default('fallback') }}",
 648        json::object(),
 649        "fallback"
 650    );
 651
 652    test_template(t, "default with falsy value",
 653        "{{ ''|default('fallback', true) }}",
 654        json::object(),
 655        "fallback"
 656    );
 657
 658    test_template(t, "tojson ensure_ascii=true",
 659        "{{ data|tojson(ensure_ascii=true) }}",
 660        {{"data", "\u2713"}},
 661        "\"\\u2713\""
 662    );
 663
 664    test_template(t, "tojson sort_keys=true",
 665        "{{ data|tojson(sort_keys=true) }}",
 666        {{"data", {{"b", 2}, {"a", 1}}}},
 667        "{\"a\": 1, \"b\": 2}"
 668    );
 669
 670    test_template(t, "tojson",
 671        "{{ data|tojson }}",
 672        {{"data", {{"a", 1}, {"b", json::array({1, 2})}}}},
 673        "{\"a\": 1, \"b\": [1, 2]}"
 674    );
 675
 676    test_template(t, "tojson indent=4",
 677        "{{ data|tojson(indent=4) }}",
 678        {{"data", {{"a", 1}, {"b", json::array({1, 2})}}}},
 679        "{\n    \"a\": 1,\n    \"b\": [\n        1,\n        2\n    ]\n}"
 680    );
 681
 682    test_template(t, "tojson separators=(',',':')",
 683        "{{ data|tojson(separators=(',',':')) }}",
 684        {{"data", {{"a", 1}, {"b", json::array({1, 2})}}}},
 685        "{\"a\":1,\"b\":[1,2]}"
 686    );
 687
 688    test_template(t, "tojson separators=(',',': ') indent=2",
 689        "{{ data|tojson(separators=(',',': '), indent=2) }}",
 690        {{"data", {{"a", 1}, {"b", json::array({1, 2})}}}},
 691        "{\n  \"a\": 1,\n  \"b\": [\n    1,\n    2\n  ]\n}"
 692    );
 693
 694    test_template(t, "chained filters",
 695        "{{ '  HELLO  '|trim|lower }}",
 696        json::object(),
 697        "hello"
 698    );
 699
 700    test_template(t, "none to string",
 701        "{{ x|string }}",
 702        {{"x", nullptr}},
 703        "None"
 704    );
 705}
 706
 707static void test_literals(testing & t) {
 708    test_template(t, "integer",
 709        "{{ 42 }}",
 710        json::object(),
 711        "42"
 712    );
 713
 714    test_template(t, "float",
 715        "{{ 3.14 }}",
 716        json::object(),
 717        "3.14"
 718    );
 719
 720    test_template(t, "string",
 721        "{{ 'hello' }}",
 722        json::object(),
 723        "hello"
 724    );
 725
 726    test_template(t, "boolean true",
 727        "{{ true }}",
 728        json::object(),
 729        "True"
 730    );
 731
 732    test_template(t, "boolean false",
 733        "{{ false }}",
 734        json::object(),
 735        "False"
 736    );
 737
 738    test_template(t, "none",
 739        "{% if x is none %}null{% endif %}",
 740        {{"x", nullptr}},
 741        "null"
 742    );
 743
 744    test_template(t, "list literal",
 745        "{% for i in [1, 2, 3] %}{{ i }}{% endfor %}",
 746        json::object(),
 747        "123"
 748    );
 749
 750    test_template(t, "dict literal",
 751        "{% set d = {'a': 1} %}{{ d.a }}",
 752        json::object(),
 753        "1"
 754    );
 755
 756    test_template(t, "integer|abs",
 757        "{{ -42 | abs }}",
 758        json::object(),
 759        "42"
 760    );
 761
 762    test_template(t, "integer|float",
 763        "{{ 42 | float }}",
 764        json::object(),
 765        "42.0"
 766    );
 767
 768    test_template(t, "integer|tojson",
 769        "{{ 42 | tojson }}",
 770        json::object(),
 771        "42"
 772    );
 773
 774    test_template(t, "float|abs",
 775        "{{ -3.14 | abs }}",
 776        json::object(),
 777        "3.14"
 778    );
 779
 780    test_template(t, "float|int",
 781        "{{ 3.14 | int }}",
 782        json::object(),
 783        "3"
 784    );
 785
 786    test_template(t, "float|tojson",
 787        "{{ 3.14 | tojson }}",
 788        json::object(),
 789        "3.14"
 790    );
 791
 792    test_template(t, "string|tojson",
 793        "{{ 'hello' | tojson }}",
 794        json::object(),
 795        "\"hello\""
 796    );
 797
 798    test_template(t, "boolean|int",
 799        "{{ true | int }}",
 800        json::object(),
 801        "1"
 802    );
 803
 804    test_template(t, "boolean|float",
 805        "{{ true | float }}",
 806        json::object(),
 807        "1.0"
 808    );
 809
 810    test_template(t, "boolean|tojson",
 811        "{{ true | tojson }}",
 812        json::object(),
 813        "true"
 814    );
 815}
 816
 817static void test_comments(testing & t) {
 818    test_template(t, "inline comment",
 819        "before{# comment #}after",
 820        json::object(),
 821        "beforeafter"
 822    );
 823
 824    test_template(t, "comment ignores code",
 825        "{% set x = 1 %}{# {% set x = 999 %} #}{{ x }}",
 826        json::object(),
 827        "1"
 828    );
 829}
 830
 831static void test_macros(testing & t) {
 832    test_template(t, "simple macro",
 833        "{% macro greet(name) %}Hello {{ name }}{% endmacro %}{{ greet('World') }}",
 834        json::object(),
 835        "Hello World"
 836    );
 837
 838    test_template(t, "macro default arg",
 839        "{% macro greet(name='Guest') %}Hi {{ name }}{% endmacro %}{{ greet() }}",
 840        json::object(),
 841        "Hi Guest"
 842    );
 843}
 844
 845static void test_namespace(testing & t) {
 846    test_template(t, "namespace counter",
 847        "{% set ns = namespace(count=0) %}{% for i in range(3) %}{% set ns.count = ns.count + 1 %}{% endfor %}{{ ns.count }}",
 848        json::object(),
 849        "3"
 850    );
 851}
 852
 853static void test_tests(testing & t) {
 854    test_template(t, "is odd",
 855        "{% if 3 is odd %}yes{% endif %}",
 856        json::object(),
 857        "yes"
 858    );
 859
 860    test_template(t, "is even",
 861        "{% if 4 is even %}yes{% endif %}",
 862        json::object(),
 863        "yes"
 864    );
 865
 866    test_template(t, "is false",
 867        "{{ 'yes' if x is false }}",
 868        {{"x", false}},
 869        "yes"
 870    );
 871
 872    test_template(t, "is true",
 873        "{{ 'yes' if x is true }}",
 874        {{"x", true}},
 875        "yes"
 876    );
 877
 878    test_template(t, "string is false",
 879        "{{ 'yes' if x is false else 'no' }}",
 880        {{"x", ""}},
 881        "no"
 882    );
 883
 884    test_template(t, "is divisibleby",
 885        "{{ 'yes' if x is divisibleby(2) }}",
 886        {{"x", 2}},
 887        "yes"
 888    );
 889
 890    test_template(t, "is eq",
 891        "{{ 'yes' if 3 is eq(3) }}",
 892        json::object(),
 893        "yes"
 894    );
 895
 896    test_template(t, "is not equalto",
 897        "{{ 'yes' if 3 is not equalto(4) }}",
 898        json::object(),
 899        "yes"
 900    );
 901
 902    test_template(t, "is ge",
 903        "{{ 'yes' if 3 is ge(3) }}",
 904        json::object(),
 905        "yes"
 906    );
 907
 908    test_template(t, "is gt",
 909        "{{ 'yes' if 3 is gt(2) }}",
 910        json::object(),
 911        "yes"
 912    );
 913
 914    test_template(t, "is greaterthan",
 915        "{{ 'yes' if 3 is greaterthan(2) }}",
 916        json::object(),
 917        "yes"
 918    );
 919
 920    test_template(t, "is lt",
 921        "{{ 'yes' if 2 is lt(3) }}",
 922        json::object(),
 923        "yes"
 924    );
 925
 926    test_template(t, "is lessthan",
 927        "{{ 'yes' if 2 is lessthan(3) }}",
 928        json::object(),
 929        "yes"
 930    );
 931
 932    test_template(t, "is ne",
 933        "{{ 'yes' if 2 is ne(3) }}",
 934        json::object(),
 935        "yes"
 936    );
 937
 938    test_template(t, "is lower",
 939        "{{ 'yes' if 'lowercase' is lower }}",
 940        json::object(),
 941        "yes"
 942    );
 943
 944    test_template(t, "is upper",
 945        "{{ 'yes' if 'UPPERCASE' is upper }}",
 946        json::object(),
 947        "yes"
 948    );
 949
 950    test_template(t, "is sameas",
 951        "{{ 'yes' if x is sameas(false) }}",
 952        {{"x", false}},
 953        "yes"
 954    );
 955
 956    test_template(t, "is boolean",
 957        "{{ 'yes' if x is boolean }}",
 958        {{"x", true}},
 959        "yes"
 960    );
 961
 962    test_template(t, "is callable",
 963        "{{ 'yes' if ''.strip is callable }}",
 964        json::object(),
 965        "yes"
 966    );
 967
 968    test_template(t, "is escaped",
 969        "{{ 'yes' if 'foo'|safe is escaped }}",
 970        json::object(),
 971        "yes"
 972    );
 973
 974    test_template(t, "is filter",
 975        "{{ 'yes' if 'trim' is filter }}",
 976        json::object(),
 977        "yes"
 978    );
 979
 980    test_template(t, "is float",
 981        "{{ 'yes' if x is float }}",
 982        {{"x", 1.1}},
 983        "yes"
 984    );
 985
 986    test_template(t, "is integer",
 987        "{{ 'yes' if x is integer }}",
 988        {{"x", 1}},
 989        "yes"
 990    );
 991
 992    test_template(t, "is sequence",
 993        "{{ 'yes' if x is sequence }}",
 994        {{"x", json::array({1, 2, 3})}},
 995        "yes"
 996    );
 997
 998    test_template(t, "is test",
 999        "{{ 'yes' if 'sequence' is test }}",
1000        json::object(),
1001        "yes"
1002    );
1003
1004    test_template(t, "is undefined",
1005        "{{ 'yes' if x is undefined }}",
1006        json::object(),
1007        "yes"
1008    );
1009
1010    test_template(t, "is none",
1011        "{% if x is none %}yes{% endif %}",
1012        {{"x", nullptr}},
1013        "yes"
1014    );
1015
1016    test_template(t, "is string",
1017        "{% if x is string %}yes{% endif %}",
1018        {{"x", "hello"}},
1019        "yes"
1020    );
1021
1022    test_template(t, "is number",
1023        "{% if x is number %}yes{% endif %}",
1024        {{"x", 42}},
1025        "yes"
1026    );
1027
1028    test_template(t, "is iterable",
1029        "{% if x is iterable %}yes{% endif %}",
1030        {{"x", json::array({1, 2, 3})}},
1031        "yes"
1032    );
1033
1034    test_template(t, "is mapping",
1035        "{% if x is mapping %}yes{% endif %}",
1036        {{"x", {{"a", 1}}}},
1037        "yes"
1038    );
1039
1040    test_template(t, "undefined is sequence",
1041        "{{ 'yes' if x is sequence }}",
1042        json::object(),
1043        "yes"
1044    );
1045
1046    test_template(t, "undefined is iterable",
1047        "{{ 'yes' if x is iterable }}",
1048        json::object(),
1049        "yes"
1050    );
1051
1052    test_template(t, "is in (array, true)",
1053        "{{ 'yes' if 2 is in([1, 2, 3]) }}",
1054        json::object(),
1055        "yes"
1056    );
1057
1058    test_template(t, "is in (array, false)",
1059        "{{ 'yes' if 5 is in([1, 2, 3]) else 'no' }}",
1060        json::object(),
1061        "no"
1062    );
1063
1064    test_template(t, "is in (string)",
1065        "{{ 'yes' if 'bc' is in('abcde') }}",
1066        json::object(),
1067        "yes"
1068    );
1069
1070    test_template(t, "is in (object keys)",
1071        "{{ 'yes' if 'a' is in(obj) }}",
1072        {{"obj", {{"a", 1}, {"b", 2}}}},
1073        "yes"
1074    );
1075
1076    test_template(t, "reject with in test",
1077        "{{ items | reject('in', skip) | join(', ') }}",
1078        {{"items", json::array({"a", "b", "c", "d"})}, {"skip", json::array({"b", "d"})}},
1079        "a, c"
1080    );
1081
1082    test_template(t, "select with in test",
1083        "{{ items | select('in', keep) | join(', ') }}",
1084        {{"items", json::array({"a", "b", "c", "d"})}, {"keep", json::array({"b", "c"})}},
1085        "b, c"
1086    );
1087}
1088
1089static void test_string_methods(testing & t) {
1090    test_template(t, "string.upper()",
1091        "{{ s.upper() }}",
1092        {{"s", "hello"}},
1093        "HELLO"
1094    );
1095
1096    test_template(t, "string.lower()",
1097        "{{ s.lower() }}",
1098        {{"s", "HELLO"}},
1099        "hello"
1100    );
1101
1102    test_template(t, "string.strip()",
1103        "[{{ s.strip() }}]",
1104        {{"s", "  hello  "}},
1105        "[hello]"
1106    );
1107
1108    test_template(t, "string.lstrip()",
1109        "[{{ s.lstrip() }}]",
1110        {{"s", "   hello"}},
1111        "[hello]"
1112    );
1113
1114    test_template(t, "string.rstrip()",
1115        "[{{ s.rstrip() }}]",
1116        {{"s", "hello   "}},
1117        "[hello]"
1118    );
1119
1120    test_template(t, "string.title()",
1121        "{{ s.title() }}",
1122        {{"s", "hello world"}},
1123        "Hello World"
1124    );
1125
1126    test_template(t, "string.capitalize()",
1127        "{{ s.capitalize() }}",
1128        {{"s", "heLlo World"}},
1129        "Hello world"
1130    );
1131
1132    test_template(t, "string.startswith() true",
1133        "{% if s.startswith('hel') %}yes{% endif %}",
1134        {{"s", "hello"}},
1135        "yes"
1136    );
1137
1138    test_template(t, "string.startswith() false",
1139        "{% if s.startswith('xyz') %}yes{% else %}no{% endif %}",
1140        {{"s", "hello"}},
1141        "no"
1142    );
1143
1144    test_template(t, "string.endswith() true",
1145        "{% if s.endswith('lo') %}yes{% endif %}",
1146        {{"s", "hello"}},
1147        "yes"
1148    );
1149
1150    test_template(t, "string.endswith() false",
1151        "{% if s.endswith('xyz') %}yes{% else %}no{% endif %}",
1152        {{"s", "hello"}},
1153        "no"
1154    );
1155
1156    test_template(t, "string.split() with sep",
1157        "{{ s.split(',')|join('-') }}",
1158        {{"s", "a,b,c"}},
1159        "a-b-c"
1160    );
1161
1162    test_template(t, "string.split() with maxsplit",
1163        "{{ s.split(',', 1)|join('-') }}",
1164        {{"s", "a,b,c"}},
1165        "a-b,c"
1166    );
1167
1168    test_template(t, "string.rsplit() with sep",
1169        "{{ s.rsplit(',')|join('-') }}",
1170        {{"s", "a,b,c"}},
1171        "a-b-c"
1172    );
1173
1174    test_template(t, "string.rsplit() with maxsplit",
1175        "{{ s.rsplit(',', 1)|join('-') }}",
1176        {{"s", "a,b,c"}},
1177        "a,b-c"
1178    );
1179
1180    test_template(t, "string.replace() basic",
1181        "{{ s.replace('world', 'jinja') }}",
1182        {{"s", "hello world"}},
1183        "hello jinja"
1184    );
1185
1186    test_template(t, "string.replace() with count",
1187        "{{ s.replace('a', 'X', 2) }}",
1188        {{"s", "banana"}},
1189        "bXnXna"
1190    );
1191
1192    test_template(t, "undefined|capitalize",
1193        "{{ arr|capitalize }}",
1194        json::object(),
1195        ""
1196    );
1197
1198    test_template(t, "undefined|title",
1199        "{{ arr|title }}",
1200        json::object(),
1201        ""
1202    );
1203
1204    test_template(t, "undefined|truncate",
1205        "{{ arr|truncate(9) }}",
1206        json::object(),
1207        ""
1208    );
1209
1210    test_template(t, "undefined|upper",
1211        "{{ arr|upper }}",
1212        json::object(),
1213        ""
1214    );
1215
1216    test_template(t, "undefined|lower",
1217        "{{ arr|lower }}",
1218        json::object(),
1219        ""
1220    );
1221
1222    test_template(t, "undefined|replace",
1223        "{{ arr|replace('a', 'b') }}",
1224        json::object(),
1225        ""
1226    );
1227
1228    test_template(t, "undefined|trim",
1229        "{{ arr|trim }}",
1230        json::object(),
1231        ""
1232    );
1233
1234    test_template(t, "undefined|wordcount",
1235        "{{ arr|wordcount }}",
1236        json::object(),
1237        "0"
1238    );
1239}
1240
1241static void test_array_methods(testing & t) {
1242    test_template(t, "array|selectattr by attribute",
1243        "{% for item in items|selectattr('active') %}{{ item.name }} {% endfor %}",
1244        {{"items", json::array({
1245            {{"name", "a"}, {"active", true}},
1246            {{"name", "b"}, {"active", false}},
1247            {{"name", "c"}, {"active", true}}
1248        })}},
1249        "a c "
1250    );
1251
1252    test_template(t, "array|selectattr with operator",
1253        "{% for item in items|selectattr('value', 'equalto', 5) %}{{ item.name }} {% endfor %}",
1254        {{"items", json::array({
1255            {{"name", "a"}, {"value", 3}},
1256            {{"name", "b"}, {"value", 5}},
1257            {{"name", "c"}, {"value", 5}}
1258        })}},
1259        "b c "
1260    );
1261
1262    test_template(t, "array|tojson",
1263        "{{ arr|tojson }}",
1264        {{"arr", json::array({1, 2, 3})}},
1265        "[1, 2, 3]"
1266    );
1267
1268    test_template(t, "array|tojson with strings",
1269        "{{ arr|tojson }}",
1270        {{"arr", json::array({"a", "b", "c"})}},
1271        "[\"a\", \"b\", \"c\"]"
1272    );
1273
1274    test_template(t, "array|tojson nested",
1275        "{{ arr|tojson }}",
1276        {{"arr", json::array({json::array({1, 2}), json::array({3, 4})})}},
1277        "[[1, 2], [3, 4]]"
1278    );
1279
1280    test_template(t, "array|last",
1281        "{{ arr|last }}",
1282        {{"arr", json::array({10, 20, 30})}},
1283        "30"
1284    );
1285
1286    test_template(t, "array|last single element",
1287        "{{ arr|last }}",
1288        {{"arr", json::array({42})}},
1289        "42"
1290    );
1291
1292    test_template(t, "array|join with separator",
1293        "{{ arr|join(', ') }}",
1294        {{"arr", json::array({"a", "b", "c"})}},
1295        "a, b, c"
1296    );
1297
1298    test_template(t, "array|join with custom separator",
1299        "{{ arr|join(' | ') }}",
1300        {{"arr", json::array({1, 2, 3})}},
1301        "1 | 2 | 3"
1302    );
1303
1304    test_template(t, "array|join default separator",
1305        "{{ arr|join }}",
1306        {{"arr", json::array({"x", "y", "z"})}},
1307        "xyz"
1308    );
1309
1310    test_template(t, "array|join attribute",
1311        "{{ arr|join(attribute='age') }}",
1312        {{"arr", json::array({
1313            json({{"name", "a"}, {"age", 1}}),
1314            json({{"name", "b"}, {"age", 2}}),
1315            json({{"name", "c"}, {"age", 3}}),
1316        })}},
1317        "123"
1318    );
1319
1320    test_template(t, "array|join numeric attribute",
1321        "{{ arr|join(attribute=-1) }}",
1322        {{"arr", json::array({json::array({1}), json::array({2}), json::array({3})})}},
1323        "123"
1324    );
1325
1326    test_template(t, "array.pop() last",
1327        "{{ arr.pop() }}-{{ arr|join(',') }}",
1328        {{"arr", json::array({"a", "b", "c"})}},
1329        "c-a,b"
1330    );
1331
1332    test_template(t, "array.pop() with index",
1333        "{{ arr.pop(0) }}-{{ arr|join(',') }}",
1334        {{"arr", json::array({"a", "b", "c"})}},
1335        "a-b,c"
1336    );
1337
1338    test_template(t, "array.append()",
1339        "{% set _ = arr.append('d') %}{{ arr|join(',') }}",
1340        {{"arr", json::array({"a", "b", "c"})}},
1341        "a,b,c,d"
1342    );
1343
1344    test_template(t, "array|map with attribute",
1345        "{% for v in arr|map(attribute='age') %}{{ v }} {% endfor %}",
1346        {{"arr", json::array({
1347            json({{"name", "a"}, {"age", 1}}),
1348            json({{"name", "b"}, {"age", 2}}),
1349            json({{"name", "c"}, {"age", 3}}),
1350        })}},
1351        "1 2 3 "
1352    );
1353
1354    test_template(t, "array|map with attribute default",
1355        "{% for v in arr|map(attribute='age', default=3) %}{{ v }} {% endfor %}",
1356        {{"arr", json::array({
1357            json({{"name", "a"}, {"age", 1}}),
1358            json({{"name", "b"}, {"age", 2}}),
1359            json({{"name", "c"}}),
1360        })}},
1361        "1 2 3 "
1362    );
1363
1364    test_template(t, "array|map without attribute default",
1365        "{% for v in arr|map(attribute='age') %}{{ v }} {% endfor %}",
1366        {{"arr", json::array({
1367            json({{"name", "a"}, {"age", 1}}),
1368            json({{"name", "b"}, {"age", 2}}),
1369            json({{"name", "c"}}),
1370        })}},
1371        "1 2  "
1372    );
1373
1374    test_template(t, "array|map with numeric attribute",
1375        "{% for v in arr|map(attribute=0) %}{{ v }} {% endfor %}",
1376        {{"arr", json::array({
1377            json::array({10, "x"}),
1378            json::array({20, "y"}),
1379            json::array({30, "z"}),
1380        })}},
1381        "10 20 30 "
1382    );
1383
1384    test_template(t, "array|map with negative attribute",
1385        "{% for v in arr|map(attribute=-1) %}{{ v }} {% endfor %}",
1386        {{"arr", json::array({
1387            json::array({10, "x"}),
1388            json::array({20, "y"}),
1389            json::array({30, "z"}),
1390        })}},
1391        "x y z "
1392    );
1393
1394    test_template(t, "array|map with filter",
1395        "{{ arr|map('int')|sum }}",
1396        {{"arr", json::array({"1", "2", "3"})}},
1397        "6"
1398    );
1399
1400    // not used by any chat templates
1401    // test_template(t, "array.insert()",
1402    //     "{% set _ = arr.insert(1, 'x') %}{{ arr|join(',') }}",
1403    //     {{"arr", json::array({"a", "b", "c"})}},
1404    //     "a,x,b,c"
1405    // );
1406
1407    test_template(t, "undefined|select",
1408        "{% for item in items|select('odd') %}{{ item.name }} {% endfor %}",
1409        json::object(),
1410        ""
1411    );
1412
1413    test_template(t, "undefined|selectattr",
1414        "{% for item in items|selectattr('active') %}{{ item.name }} {% endfor %}",
1415        json::object(),
1416        ""
1417    );
1418
1419    test_template(t, "undefined|reject",
1420        "{% for item in items|reject('even') %}{{ item.name }} {% endfor %}",
1421        json::object(),
1422        ""
1423    );
1424
1425    test_template(t, "undefined|rejectattr",
1426        "{% for item in items|rejectattr('active') %}{{ item.name }} {% endfor %}",
1427        json::object(),
1428        ""
1429    );
1430
1431    test_template(t, "undefined|list",
1432        "{{ arr|list|string }}",
1433        json::object(),
1434        "[]"
1435    );
1436
1437    test_template(t, "undefined|string",
1438        "{{ arr|string }}",
1439        json::object(),
1440        ""
1441    );
1442
1443    test_template(t, "undefined|first",
1444        "{{ arr|first }}",
1445        json::object(),
1446        ""
1447    );
1448
1449    test_template(t, "undefined|last",
1450        "{{ arr|last }}",
1451        json::object(),
1452        ""
1453    );
1454
1455    test_template(t, "undefined|length",
1456        "{{ arr|length }}",
1457        json::object(),
1458        "0"
1459    );
1460
1461    test_template(t, "undefined|join",
1462        "{{ arr|join }}",
1463        json::object(),
1464        ""
1465    );
1466
1467    test_template(t, "undefined|sort",
1468        "{{ arr|sort|string }}",
1469        json::object(),
1470        "[]"
1471    );
1472
1473    test_template(t, "undefined|reverse",
1474        "{{ arr|reverse|join }}",
1475        json::object(),
1476        ""
1477    );
1478
1479    test_template(t, "undefined|map",
1480        "{% for v in arr|map(attribute='age') %}{{ v }} {% endfor %}",
1481        json::object(),
1482        ""
1483    );
1484
1485    test_template(t, "undefined|min",
1486        "{{ arr|min }}",
1487        json::object(),
1488        ""
1489    );
1490
1491    test_template(t, "undefined|max",
1492        "{{ arr|max }}",
1493        json::object(),
1494        ""
1495    );
1496
1497    test_template(t, "undefined|unique",
1498        "{{ arr|unique|join }}",
1499        json::object(),
1500        ""
1501    );
1502
1503    test_template(t, "undefined|sum",
1504        "{{ arr|sum }}",
1505        json::object(),
1506        "0"
1507    );
1508}
1509
1510static void test_object_methods(testing & t) {
1511    test_template(t, "object.get() existing key",
1512        "{{ obj.get('a') }}",
1513        {{"obj", {{"a", 1}, {"b", 2}}}},
1514        "1"
1515    );
1516
1517    test_template(t, "object.get() missing key",
1518        "[{{ obj.get('c') is none }}]",
1519        {{"obj", {{"a", 1}}}},
1520        "[True]"
1521    );
1522
1523    test_template(t, "object.get() missing key with default",
1524        "{{ obj.get('c', 'default') }}",
1525        {{"obj", {{"a", 1}}}},
1526        "default"
1527    );
1528
1529    test_template(t, "object.items()",
1530        "{% for k, v in obj.items() %}{{ k }}={{ v }} {% endfor %}",
1531        {{"obj", {{"x", 1}, {"y", 2}}}},
1532        "x=1 y=2 "
1533    );
1534
1535    test_template(t, "object.keys()",
1536        "{% for k in obj.keys() %}{{ k }} {% endfor %}",
1537        {{"obj", {{"a", 1}, {"b", 2}}}},
1538        "a b "
1539    );
1540
1541    test_template(t, "object.values()",
1542        "{% for v in obj.values() %}{{ v }} {% endfor %}",
1543        {{"obj", {{"a", 1}, {"b", 2}}}},
1544        "1 2 "
1545    );
1546
1547    test_template(t, "dictsort ascending by key",
1548        "{% for k, v in obj|dictsort %}{{ k }}={{ v }} {% endfor %}",
1549        {{"obj", {{"z", 2}, {"a", 3}, {"m", 1}}}},
1550        "a=3 m=1 z=2 "
1551    );
1552
1553    test_template(t, "dictsort descending by key",
1554        "{% for k, v in obj|dictsort(reverse=true) %}{{ k }}={{ v }} {% endfor %}",
1555        {{"obj", {{"a", 1}, {"b", 2}, {"c", 3}}}},
1556        "c=3 b=2 a=1 "
1557    );
1558
1559    test_template(t, "dictsort by value",
1560        "{% for k, v in obj|dictsort(by='value') %}{{ k }}={{ v }} {% endfor %}",
1561        {{"obj", {{"a", 3}, {"b", 1}, {"c", 2}}}},
1562        "b=1 c=2 a=3 "
1563    );
1564
1565    test_template(t, "dictsort case sensitive",
1566        "{% for k, v in obj|dictsort(case_sensitive=true) %}{{ k }}={{ v }} {% endfor %}",
1567        {{"obj", {{"a", 1}, {"A", 1}, {"b", 2}, {"B", 2}, {"c", 3}}}},
1568        "A=1 B=2 a=1 b=2 c=3 "
1569    );
1570
1571    test_template(t, "object|tojson",
1572        "{{ obj|tojson }}",
1573        {{"obj", {{"name", "test"}, {"value", 42}}}},
1574        "{\"name\": \"test\", \"value\": 42}"
1575    );
1576
1577    test_template(t, "nested object|tojson",
1578        "{{ obj|tojson }}",
1579        {{"obj", {{"outer", {{"inner", "value"}}}}}},
1580        "{\"outer\": {\"inner\": \"value\"}}"
1581    );
1582
1583    test_template(t, "array in object|tojson",
1584        "{{ obj|tojson }}",
1585        {{"obj", {{"items", json::array({1, 2, 3})}}}},
1586        "{\"items\": [1, 2, 3]}"
1587    );
1588
1589    test_template(t, "object attribute and key access",
1590        "{{ obj.keys()|join(',') }} vs {{ obj['keys'] }} vs {{ obj.test }}",
1591        {{"obj", {{"keys", "value"}, {"test", "attr_value"}}}},
1592        "keys,test vs value vs attr_value"
1593    );
1594
1595    test_template(t, "env should not have object methods",
1596        "{{ keys is undefined }} {{ obj.keys is defined }}",
1597        {{"obj", {{"a", "b"}}}},
1598        "True True"
1599    );
1600
1601    test_template(t, "expression as object key",
1602        "{% set d = {'ab': 123} %}{{ d['a' + 'b'] == 123 }}",
1603        json::object(),
1604        "True"
1605    );
1606
1607    test_template(t, "numeric as object key (template: Seed-OSS)",
1608        "{% set d = {1: 'a', 2: 'b'} %}{{ d[1] == 'a' and d[2] == 'b' }}",
1609        json::object(),
1610        "True"
1611    );
1612
1613    test_template(t, "undefined|items",
1614        "{{ arr|items|join }}",
1615        json::object(),
1616        ""
1617    );
1618}
1619
1620static void test_hasher(testing & t) {
1621    static const std::vector<std::pair<size_t, size_t>> chunk_sizes = {
1622        {1, 2},
1623        {1, 16},
1624        {8, 1},
1625        {1, 1024},
1626        {5, 512},
1627        {16, 256},
1628        {45, 122},
1629        {70, 634},
1630    };
1631
1632    static auto random_bytes = [](size_t length) -> std::string {
1633        std::string data;
1634        data.resize(length);
1635        for (size_t i = 0; i < length; ++i) {
1636            data[i] = static_cast<char>(rand() % 256);
1637        }
1638        return data;
1639    };
1640
1641    t.test("state unchanged with empty input", [](testing & t) {
1642        jinja::hasher hasher;
1643        hasher.update("some data");
1644        size_t initial_state = hasher.digest();
1645        hasher.update("", 0);
1646        size_t final_state = hasher.digest();
1647        t.assert_true("Hasher state should remain unchanged", initial_state == final_state);
1648    });
1649
1650    t.test("different inputs produce different hashes", [](testing & t) {
1651        jinja::hasher hasher1;
1652        hasher1.update("data one");
1653        size_t hash1 = hasher1.digest();
1654
1655        jinja::hasher hasher2;
1656        hasher2.update("data two");
1657        size_t hash2 = hasher2.digest();
1658
1659        t.assert_true("Different inputs should produce different hashes", hash1 != hash2);
1660    });
1661
1662    t.test("same inputs produce same hashes", [](testing & t) {
1663        jinja::hasher hasher1;
1664        hasher1.update("consistent data");
1665        size_t hash1 = hasher1.digest();
1666
1667        jinja::hasher hasher2;
1668        hasher2.update("consistent data");
1669        size_t hash2 = hasher2.digest();
1670
1671        t.assert_true("Same inputs should produce same hashes", hash1 == hash2);
1672    });
1673
1674    t.test("property: update(a ~ b) == update(a).update(b)", [](testing & t) {
1675        for (const auto & [size1, size2] : chunk_sizes) {
1676            std::string data1 = random_bytes(size1);
1677            std::string data2 = random_bytes(size2);
1678
1679            jinja::hasher hasher1;
1680            hasher1.update(data1);
1681            hasher1.update(data2);
1682            size_t hash1 = hasher1.digest();
1683
1684            jinja::hasher hasher2;
1685            hasher2.update(data1 + data2);
1686            size_t hash2 = hasher2.digest();
1687
1688            t.assert_true(
1689                "Hashing in multiple updates should match single update (" + std::to_string(size1) + ", " + std::to_string(size2) + ")",
1690                hash1 == hash2);
1691        }
1692    });
1693
1694    t.test("property: update(a ~ b) == update(a).update(b) with more update passes", [](testing & t) {
1695        static const std::vector<size_t> sizes = {3, 732, 131, 13, 17, 256, 436, 99, 4};
1696
1697        jinja::hasher hasher1;
1698        jinja::hasher hasher2;
1699
1700        std::string combined_data;
1701        for (size_t size : sizes) {
1702            std::string data = random_bytes(size);
1703            hasher1.update(data);
1704            combined_data += data;
1705        }
1706
1707        hasher2.update(combined_data);
1708        size_t hash1 = hasher1.digest();
1709        size_t hash2 = hasher2.digest();
1710        t.assert_true(
1711            "Hashing in multiple updates should match single update with many chunks",
1712            hash1 == hash2);
1713    });
1714
1715    t.test("property: non associativity of update", [](testing & t) {
1716        for (const auto & [size1, size2] : chunk_sizes) {
1717            std::string data1 = random_bytes(size1);
1718            std::string data2 = random_bytes(size2);
1719
1720            jinja::hasher hasher1;
1721            hasher1.update(data1);
1722            hasher1.update(data2);
1723            size_t hash1 = hasher1.digest();
1724
1725            jinja::hasher hasher2;
1726            hasher2.update(data2);
1727            hasher2.update(data1);
1728            size_t hash2 = hasher2.digest();
1729
1730            t.assert_true(
1731                "Hashing order should matter (" + std::to_string(size1) + ", " + std::to_string(size2) + ")",
1732                hash1 != hash2);
1733        }
1734    });
1735
1736    t.test("property: different lengths produce different hashes (padding block size)", [](testing & t) {
1737        std::string random_data = random_bytes(64);
1738
1739        jinja::hasher hasher1;
1740        hasher1.update(random_data);
1741        size_t hash1 = hasher1.digest();
1742
1743        for (int i = 0; i < 16; ++i) {
1744            random_data.push_back('A');  // change length
1745            jinja::hasher hasher2;
1746            hasher2.update(random_data);
1747            size_t hash2 = hasher2.digest();
1748
1749            t.assert_true("Different lengths should produce different hashes (length " + std::to_string(random_data.size()) + ")", hash1 != hash2);
1750
1751            hash1 = hash2;
1752        }
1753    });
1754}
1755
1756static void test_template_cpp(testing & t, const std::string & name, const std::string & tmpl, const json & vars, const std::string & expect) {
1757    t.test(name, [&tmpl, &vars, &expect](testing & t) {
1758        jinja::lexer lexer;
1759        auto lexer_res = lexer.tokenize(tmpl);
1760
1761        jinja::program ast = jinja::parse_from_tokens(lexer_res);
1762
1763        jinja::context ctx(tmpl);
1764        jinja::global_from_json(ctx, vars, true);
1765
1766        jinja::runtime runtime(ctx);
1767
1768        try {
1769            const jinja::value results = runtime.execute(ast);
1770            auto parts = runtime.gather_string_parts(results);
1771
1772            std::string rendered;
1773            for (const auto & part : parts->as_string().parts) {
1774                rendered += part.val;
1775            }
1776
1777            if (!t.assert_true("Template render mismatch", expect == rendered)) {
1778                t.log("Template: " + json(tmpl).dump());
1779                t.log("Expected: " + json(expect).dump());
1780                t.log("Actual  : " + json(rendered).dump());
1781            }
1782        } catch (const jinja::not_implemented_exception & e) {
1783            // TODO @ngxson : remove this when the test framework supports skipping tests
1784            t.log("Skipped: " + std::string(e.what()));
1785        }
1786    });
1787}
1788
1789// keep this in-sync with https://github.com/huggingface/transformers/blob/main/src/transformers/utils/chat_template_utils.py
1790// note: we use SandboxedEnvironment instead of ImmutableSandboxedEnvironment to allow usage of in-place array methods like append() and pop()
1791static std::string py_script = R"(
1792import jinja2
1793import jinja2.ext as jinja2_ext
1794import json
1795import sys
1796from datetime import datetime
1797from jinja2.sandbox import SandboxedEnvironment
1798
1799tmpl = json.loads(sys.argv[1])
1800vars_json = json.loads(sys.argv[2])
1801
1802env = SandboxedEnvironment(
1803    trim_blocks=True,
1804    lstrip_blocks=True,
1805    extensions=[jinja2_ext.loopcontrols],
1806)
1807
1808def raise_exception(message):
1809    raise jinja2.exceptions.TemplateError(message)
1810
1811env.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)
1812env.globals["strftime_now"] = lambda format: datetime.now().strftime(format)
1813env.globals["raise_exception"] = raise_exception
1814
1815template = env.from_string(tmpl)
1816result = template.render(**vars_json)
1817print(result, end='')
1818)";
1819
1820static void test_template_py(testing & t, const std::string & name, const std::string & tmpl, const json & vars, const std::string & expect) {
1821    t.test(name, [&tmpl, &vars, &expect](testing & t) {
1822        // Prepare arguments
1823        std::string tmpl_json = json(tmpl).dump();
1824        std::string vars_json = vars.dump();
1825
1826#ifdef _WIN32
1827        const char * python_executable = "python.exe";
1828#else
1829        const char * python_executable = "python3";
1830#endif
1831
1832        const char * command_line[] = {python_executable, "-c", py_script.c_str(), tmpl_json.c_str(), vars_json.c_str(), NULL};
1833
1834        struct subprocess_s subprocess;
1835        int options = subprocess_option_combined_stdout_stderr
1836                    | subprocess_option_no_window
1837                    | subprocess_option_inherit_environment
1838                    | subprocess_option_search_user_path;
1839        int result = subprocess_create(command_line, options, &subprocess);
1840
1841        if (result != 0) {
1842            t.log("Failed to create subprocess, error code: " + std::to_string(result));
1843            t.assert_true("subprocess creation", false);
1844            return;
1845        }
1846
1847        // Read output
1848        std::string output;
1849        char buffer[1024];
1850        FILE * p_stdout = subprocess_stdout(&subprocess);
1851        while (fgets(buffer, sizeof(buffer), p_stdout)) {
1852            output += buffer;
1853        }
1854
1855        int process_return;
1856        subprocess_join(&subprocess, &process_return);
1857        subprocess_destroy(&subprocess);
1858
1859        if (process_return != 0) {
1860            t.log("Python script failed with exit code: " + std::to_string(process_return));
1861            t.log("Output: " + output);
1862            t.assert_true("python execution", false);
1863            return;
1864        }
1865
1866        if (!t.assert_true("Template render mismatch", expect == output)) {
1867            t.log("Template: " + json(tmpl).dump());
1868            t.log("Expected: " + json(expect).dump());
1869            t.log("Python  : " + json(output).dump());
1870        }
1871    });
1872}
1873
1874static void test_template(testing & t, const std::string & name, const std::string & tmpl, const json & vars, const std::string & expect) {
1875    if (g_python_mode) {
1876        test_template_py(t, name, tmpl, vars, expect);
1877    } else {
1878        test_template_cpp(t, name, tmpl, vars, expect);
1879    }
1880}
1881
1882//
1883// fuzz tests to ensure no crashes occur on malformed inputs
1884//
1885
1886constexpr int JINJA_FUZZ_ITERATIONS = 100;
1887
1888// Helper to generate random string
1889static std::string random_string(std::mt19937 & rng, size_t max_len) {
1890    static const char charset[] = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_";
1891    std::uniform_int_distribution<size_t> len_dist(0, max_len);
1892    std::uniform_int_distribution<size_t> char_dist(0, sizeof(charset) - 2);
1893    size_t len = len_dist(rng);
1894    std::string result;
1895    result.reserve(len);
1896    for (size_t i = 0; i < len; ++i) {
1897        result += charset[char_dist(rng)];
1898    }
1899    return result;
1900}
1901
1902// Helper to execute a fuzz test case - returns true if no crash occurred
1903static bool fuzz_test_template(const std::string & tmpl, const json & vars) {
1904    try {
1905        // printf("Fuzz testing template: %s\n", tmpl.c_str());
1906        jinja::lexer lexer;
1907        auto lexer_res = lexer.tokenize(tmpl);
1908        jinja::program ast = jinja::parse_from_tokens(lexer_res);
1909        jinja::context ctx(tmpl);
1910        jinja::global_from_json(ctx, vars, true);
1911        jinja::runtime runtime(ctx);
1912        const jinja::value results = runtime.execute(ast);
1913        runtime.gather_string_parts(results);
1914        return true; // success
1915    } catch (const std::exception &) {
1916        return true; // exception is acceptable, not a crash
1917    } catch (...) {
1918        return true; // any exception is acceptable, not a crash
1919    }
1920}
1921
1922static void test_fuzzing(testing & t) {
1923    const int num_iterations = JINJA_FUZZ_ITERATIONS;
1924    const unsigned int seed = 42; // fixed seed for reproducibility
1925    std::mt19937 rng(seed);
1926
1927    // Distribution helpers
1928    std::uniform_int_distribution<int> choice_dist(0, 100);
1929    std::uniform_int_distribution<int> int_dist(-1000, 1000);
1930    std::uniform_int_distribution<size_t> idx_dist(0, 1000);
1931
1932    // Template fragments for fuzzing
1933    const std::vector<std::string> var_names = {
1934        "x", "y", "z", "arr", "obj", "items", "foo", "bar", "undefined_var",
1935        "none", "true", "false", "None", "True", "False"
1936    };
1937    const std::vector<std::string> filters = {
1938        "length", "first", "last", "reverse", "sort", "unique", "join", "upper", "lower",
1939        "trim", "default", "tojson", "string", "int", "float", "abs", "list", "dictsort"
1940    };
1941    const std::vector<std::string> builtins = {
1942        "range", "len", "dict", "list", "join", "str", "int", "float", "namespace"
1943    };
1944
1945    t.test("out of bound array access", [&](testing & t) {
1946        for (int i = 0; i < num_iterations; ++i) {
1947            int idx = int_dist(rng);
1948            std::string tmpl = "{{ arr[" + std::to_string(idx) + "] }}";
1949            json vars = {{"arr", json::array({1, 2, 3})}};
1950            t.assert_true("should not crash", fuzz_test_template(tmpl, vars));
1951        }
1952    });
1953
1954    t.test("non-existing variables", [&](testing & t) {
1955        for (int i = 0; i < num_iterations; ++i) {
1956            std::string var = random_string(rng, 20);
1957            std::string tmpl = "{{ " + var + " }}";
1958            json vars = json::object(); // empty context
1959            t.assert_true("should not crash", fuzz_test_template(tmpl, vars));
1960        }
1961    });
1962
1963    t.test("non-existing nested attributes", [&](testing & t) {
1964        for (int i = 0; i < num_iterations; ++i) {
1965            std::string var1 = var_names[choice_dist(rng) % var_names.size()];
1966            std::string var2 = random_string(rng, 10);
1967            std::string var3 = random_string(rng, 10);
1968            std::string tmpl = "{{ " + var1 + "." + var2 + "." + var3 + " }}";
1969            json vars = {{var1, {{"other", 123}}}};
1970            t.assert_true("should not crash", fuzz_test_template(tmpl, vars));
1971        }
1972    });
1973
1974    t.test("invalid filter arguments", [&](testing & t) {
1975        for (int i = 0; i < num_iterations; ++i) {
1976            std::string filter = filters[choice_dist(rng) % filters.size()];
1977            int val = int_dist(rng);
1978            std::string tmpl = "{{ " + std::to_string(val) + " | " + filter + " }}";
1979            json vars = json::object();
1980            t.assert_true("should not crash", fuzz_test_template(tmpl, vars));
1981        }
1982    });
1983
1984    t.test("chained filters on various types", [&](testing & t) {
1985        for (int i = 0; i < num_iterations; ++i) {
1986            std::string f1 = filters[choice_dist(rng) % filters.size()];
1987            std::string f2 = filters[choice_dist(rng) % filters.size()];
1988            std::string var = var_names[choice_dist(rng) % var_names.size()];
1989            std::string tmpl = "{{ " + var + " | " + f1 + " | " + f2 + " }}";
1990            json vars = {
1991                {"x", 42},
1992                {"y", "hello"},
1993                {"arr", json::array({1, 2, 3})},
1994                {"obj", {{"a", 1}, {"b", 2}}},
1995                {"items", json::array({"a", "b", "c"})}
1996            };
1997            t.assert_true("should not crash", fuzz_test_template(tmpl, vars));
1998        }
1999    });
2000
2001    t.test("invalid builtin calls", [&](testing & t) {
2002        for (int i = 0; i < num_iterations; ++i) {
2003            std::string builtin = builtins[choice_dist(rng) % builtins.size()];
2004            std::string arg;
2005            int arg_type = choice_dist(rng) % 4;
2006            switch (arg_type) {
2007                case 0: arg = "\"not a number\""; break;
2008                case 1: arg = "none"; break;
2009                case 2: arg = std::to_string(int_dist(rng)); break;
2010                case 3: arg = "[]"; break;
2011            }
2012            std::string tmpl = "{{ " + builtin + "(" + arg + ") }}";
2013            json vars = json::object();
2014            t.assert_true("should not crash", fuzz_test_template(tmpl, vars));
2015        }
2016    });
2017
2018    t.test("macro edge cases", [&](testing & t) {
2019        // Macro with no args called with args
2020        t.assert_true("macro no args with args", fuzz_test_template(
2021            "{% macro foo() %}hello{% endmacro %}{{ foo(1, 2, 3) }}",
2022            json::object()
2023        ));
2024
2025        // Macro with args called with no args
2026        t.assert_true("macro with args no args", fuzz_test_template(
2027            "{% macro foo(a, b, c) %}{{ a }}{{ b }}{{ c }}{% endmacro %}{{ foo() }}",
2028            json::object()
2029        ));
2030
2031        // Recursive macro reference
2032        t.assert_true("recursive macro", fuzz_test_template(
2033            "{% macro foo(n) %}{% if n > 0 %}{{ foo(n - 1) }}{% endif %}{% endmacro %}{{ foo(5) }}",
2034            json::object()
2035        ));
2036
2037        // Nested macro definitions
2038        for (int i = 0; i < num_iterations / 10; ++i) {
2039            std::string tmpl = "{% macro outer() %}{% macro inner() %}x{% endmacro %}{{ inner() }}{% endmacro %}{{ outer() }}";
2040            t.assert_true("nested macro", fuzz_test_template(tmpl, json::object()));
2041        }
2042    });
2043
2044    t.test("empty and none operations", [&](testing & t) {
2045        const std::vector<std::string> empty_tests = {
2046            "{{ \"\" | first }}",
2047            "{{ \"\" | last }}",
2048            "{{ [] | first }}",
2049            "{{ [] | last }}",
2050            "{{ none.attr }}",
2051            "{{ none | length }}",
2052            "{{ none | default('fallback') }}",
2053            "{{ {} | first }}",
2054            "{{ {} | dictsort }}",
2055        };
2056        for (const auto & tmpl : empty_tests) {
2057            t.assert_true("empty/none: " + tmpl, fuzz_test_template(tmpl, json::object()));
2058        }
2059    });
2060
2061    t.test("arithmetic edge cases", [&](testing & t) {
2062        const std::vector<std::string> arith_tests = {
2063            "{{ 1 / 0 }}",
2064            "{{ 1 // 0 }}",
2065            "{{ 1 % 0 }}",
2066            "{{ 999999999999999999 * 999999999999999999 }}",
2067            "{{ -999999999999999999 - 999999999999999999 }}",
2068            "{{ 1.0 / 0.0 }}",
2069            "{{ 0.0 / 0.0 }}",
2070        };
2071        for (const auto & tmpl : arith_tests) {
2072            t.assert_true("arith: " + tmpl, fuzz_test_template(tmpl, json::object()));
2073        }
2074    });
2075
2076    t.test("deeply nested structures", [&](testing & t) {
2077        // Deeply nested loops
2078        for (int depth = 1; depth <= 10; ++depth) {
2079            std::string tmpl;
2080            for (int d = 0; d < depth; ++d) {
2081                tmpl += "{% for i" + std::to_string(d) + " in arr %}";
2082            }
2083            tmpl += "x";
2084            for (int d = 0; d < depth; ++d) {
2085                tmpl += "{% endfor %}";
2086            }
2087            json vars = {{"arr", json::array({1, 2})}};
2088            t.assert_true("nested loops depth " + std::to_string(depth), fuzz_test_template(tmpl, vars));
2089        }
2090
2091        // Deeply nested conditionals
2092        for (int depth = 1; depth <= 10; ++depth) {
2093            std::string tmpl;
2094            for (int d = 0; d < depth; ++d) {
2095                tmpl += "{% if true %}";
2096            }
2097            tmpl += "x";
2098            for (int d = 0; d < depth; ++d) {
2099                tmpl += "{% endif %}";
2100            }
2101            t.assert_true("nested ifs depth " + std::to_string(depth), fuzz_test_template(tmpl, json::object()));
2102        }
2103    });
2104
2105    t.test("special characters in strings", [&](testing & t) {
2106        const std::vector<std::string> special_tests = {
2107            "{{ \"}{%\" }}",
2108            "{{ \"}}{{\" }}",
2109            "{{ \"{%%}\" }}",
2110            "{{ \"\\n\\t\\r\" }}",
2111            "{{ \"'\\\"'\" }}",
2112            "{{ \"hello\\x00world\" }}",
2113        };
2114        for (const auto & tmpl : special_tests) {
2115            t.assert_true("special: " + tmpl, fuzz_test_template(tmpl, json::object()));
2116        }
2117    });
2118
2119    t.test("random template generation", [&](testing & t) {
2120        const std::vector<std::string> fragments = {
2121            "{{ x }}", "{{ y }}", "{{ arr }}", "{{ obj }}",
2122            "{% if true %}a{% endif %}",
2123            "{% if false %}b{% else %}c{% endif %}",
2124            "{% for i in arr %}{{ i }}{% endfor %}",
2125            "{{ x | length }}", "{{ x | first }}", "{{ x | default(0) }}",
2126            "{{ x + y }}", "{{ x - y }}", "{{ x * y }}",
2127            "{{ x == y }}", "{{ x != y }}", "{{ x > y }}",
2128            "{{ range(3) }}", "{{ \"hello\" | upper }}",
2129            "text", " ", "\n",
2130        };
2131
2132        for (int i = 0; i < num_iterations; ++i) {
2133            std::string tmpl;
2134            int num_frags = choice_dist(rng) % 10 + 1;
2135            for (int f = 0; f < num_frags; ++f) {
2136                tmpl += fragments[choice_dist(rng) % fragments.size()];
2137            }
2138            json vars = {
2139                {"x", int_dist(rng)},
2140                {"y", int_dist(rng)},
2141                {"arr", json::array({1, 2, 3})},
2142                {"obj", {{"a", 1}, {"b", 2}}}
2143            };
2144            t.assert_true("random template #" + std::to_string(i), fuzz_test_template(tmpl, vars));
2145        }
2146    });
2147
2148    t.test("malformed templates (should error, not crash)", [&](testing & t) {
2149        const std::vector<std::string> malformed = {
2150            "{{ x",
2151            "{% if %}",
2152            "{% for %}",
2153            "{% for x in %}",
2154            "{% endfor %}",
2155            "{% endif %}",
2156            "{{ | filter }}",
2157            "{% if x %}", // unclosed
2158            "{% for i in x %}", // unclosed
2159            "{{ x | }}",
2160            "{% macro %}{% endmacro %}",
2161            "{{{{",
2162            "}}}}",
2163            "{%%}",
2164            "{% set %}",
2165            "{% set x %}",
2166        };
2167        for (const auto & tmpl : malformed) {
2168            t.assert_true("malformed: " + tmpl, fuzz_test_template(tmpl, json::object()));
2169        }
2170    });
2171
2172    t.test("type coercion edge cases", [&](testing & t) {
2173        for (int i = 0; i < num_iterations; ++i) {
2174            int op_choice = choice_dist(rng) % 6;
2175            std::string op;
2176            switch (op_choice) {
2177                case 0: op = "+"; break;
2178                case 1: op = "-"; break;
2179                case 2: op = "*"; break;
2180                case 3: op = "/"; break;
2181                case 4: op = "=="; break;
2182                case 5: op = "~"; break; // string concat
2183            }
2184
2185            std::string left_var = var_names[choice_dist(rng) % var_names.size()];
2186            std::string right_var = var_names[choice_dist(rng) % var_names.size()];
2187            std::string tmpl = "{{ " + left_var + " " + op + " " + right_var + " }}";
2188
2189            json vars = {
2190                {"x", 42},
2191                {"y", "hello"},
2192                {"z", 3.14},
2193                {"arr", json::array({1, 2, 3})},
2194                {"obj", {{"a", 1}}},
2195                {"items", json::array()},
2196                {"foo", nullptr},
2197                {"bar", true}
2198            };
2199            t.assert_true("type coercion: " + tmpl, fuzz_test_template(tmpl, vars));
2200        }
2201    });
2202
2203    t.test("fuzz builtin functions", [&](testing & t) {
2204        // pair of (type_name, builtin_name)
2205        std::vector<std::pair<std::string, std::string>> builtins;
2206        auto add_fns = [&](std::string type_name, const jinja::func_builtins & added) {
2207            for (const auto & it : added) {
2208                builtins.push_back({type_name, it.first});
2209            }
2210        };
2211        add_fns("global", jinja::global_builtins());
2212        add_fns("int",    jinja::value_int_t(0).get_builtins());
2213        add_fns("float",  jinja::value_float_t(0.0f).get_builtins());
2214        add_fns("string", jinja::value_string_t().get_builtins());
2215        add_fns("array",  jinja::value_array_t().get_builtins());
2216        add_fns("object", jinja::value_object_t().get_builtins());
2217
2218        const int max_args = 5;
2219        const std::vector<std::string> kwarg_names = {
2220            "base", "attribute", "default", "reverse", "case_sensitive", "by", "safe", "chars", "separators", "sort_keys", "indent", "ensure_ascii",
2221        };
2222
2223        // Generate random argument values
2224        auto gen_random_arg = [&]() -> std::string {
2225            int type = choice_dist(rng) % 8;
2226            switch (type) {
2227                case 0: return std::to_string(int_dist(rng));           // int
2228                case 1: return std::to_string(int_dist(rng)) + ".5";    // float
2229                case 2: return "\"" + random_string(rng, 10) + "\"";    // string
2230                case 3: return "true";                                   // bool true
2231                case 4: return "false";                                  // bool false
2232                case 5: return "none";                                   // none
2233                case 6: return "[1, 2, 3]";                              // array
2234                case 7: return "{\"a\": 1}";                             // object
2235                default: return "0";
2236            }
2237        };
2238
2239        for (int i = 0; i < num_iterations; ++i) {
2240            // Pick a random builtin
2241            auto & [type_name, fn_name] = builtins[choice_dist(rng) % builtins.size()];
2242
2243            // Generate random number of args
2244            int num_args = choice_dist(rng) % (max_args + 1);
2245            std::string args_str;
2246            for (int a = 0; a < num_args; ++a) {
2247                if (a > 0) args_str += ", ";
2248                // Sometimes use keyword args
2249                if (choice_dist(rng) % 3 == 0 && !kwarg_names.empty()) {
2250                    std::string kwarg = kwarg_names[choice_dist(rng) % kwarg_names.size()];
2251                    args_str += kwarg + "=" + gen_random_arg();
2252                } else {
2253                    args_str += gen_random_arg();
2254                }
2255            }
2256
2257            std::string tmpl;
2258            if (type_name == "global") {
2259                // Global function call
2260                tmpl = "{{ " + fn_name + "(" + args_str + ") }}";
2261            } else {
2262                // Method call on a value
2263                std::string base_val;
2264                if (type_name == "int") {
2265                    base_val = std::to_string(int_dist(rng));
2266                } else if (type_name == "float") {
2267                    base_val = std::to_string(int_dist(rng)) + ".5";
2268                } else if (type_name == "string") {
2269                    base_val = "\"test_string\"";
2270                } else if (type_name == "array") {
2271                    base_val = "[1, 2, 3, \"a\", \"b\"]";
2272                } else if (type_name == "object") {
2273                    base_val = "{\"x\": 1, \"y\": 2}";
2274                } else {
2275                    base_val = "x";
2276                }
2277                tmpl = "{{ " + base_val + "." + fn_name + "(" + args_str + ") }}";
2278            }
2279
2280            json vars = {
2281                {"x", 42},
2282                {"y", "hello"},
2283                {"arr", json::array({1, 2, 3})},
2284                {"obj", {{"a", 1}, {"b", 2}}}
2285            };
2286
2287            t.assert_true("builtin " + type_name + "." + fn_name + " #" + std::to_string(i), fuzz_test_template(tmpl, vars));
2288        }
2289    });
2290}