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}