-
Notifications
You must be signed in to change notification settings - Fork 41
Expand file tree
/
Copy pathJinja2TemplateTests.cs
More file actions
146 lines (131 loc) · 6.37 KB
/
Copy pathJinja2TemplateTests.cs
File metadata and controls
146 lines (131 loc) · 6.37 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
// Copyright (c) Zhongkai Fu. All rights reserved.
// https://github.com/zhongkaifu/TensorSharp
//
// This file is part of TensorSharp.
//
// TensorSharp is licensed under the BSD-3-Clause license found in the LICENSE file in the root directory of this source tree.
//
// TensorSharp is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the BSD-3-Clause License for more details.
//
// Unit tests for the lightweight Jinja2 renderer used to evaluate GGUF chat
// templates. These focus on the features the Gemma 4 ("gemma4") template relies
// on that previously crashed or mis-rendered: the block-form set capture
// ({% set var %}...{% endset %}) and the multi-argument range() generator.
// They run without a model.
using System.Collections.Generic;
using TensorSharp.Runtime;
using Xunit;
namespace InferenceWeb.Tests;
public class Jinja2TemplateTests
{
private static string Render(string template, Dictionary<string, object> ctx)
=> new Jinja2Template(template).Render(ctx);
[Fact]
public void SetBlock_CapturesRenderedBody()
{
// {% set x %}...{% endset %} captures the rendered body into x.
string tmpl = "{% set captured %}Hello {{ name }}!{% endset %}[{{ captured }}]";
string outp = Render(tmpl, new Dictionary<string, object> { ["name"] = "World" });
Assert.Equal("[Hello World!]", outp);
}
[Fact]
public void SetBlock_WithTrimFilter_AppliesFilterToCapture()
{
string tmpl = "{% set x | trim %} padded {% endset %}[{{ x }}]";
string outp = Render(tmpl, new Dictionary<string, object>());
Assert.Equal("[padded]", outp);
}
[Fact]
public void SetBlock_HonorsWhitespaceControlInsideBody()
{
// The Gemma 4 template wraps the body in {%- ... -%} trims; the captured
// value must reflect that trimming, not the raw inter-tag whitespace.
string tmpl =
"{%- set captured_content -%}\n" +
" {%- if msg == 'a' -%}AAA{%- else -%}BBB{%- endif -%}\n" +
"{%- endset -%}<{{ captured_content }}>";
Assert.Equal("<AAA>", Render(tmpl, new Dictionary<string, object> { ["msg"] = "a" }));
Assert.Equal("<BBB>", Render(tmpl, new Dictionary<string, object> { ["msg"] = "z" }));
}
[Fact]
public void InlineSet_StillWorks_NotMisparsedAsBlock()
{
// Regression: distinguishing block-set from inline-set must not break the
// common "set var = expr" form (including values that contain '==').
Assert.Equal("3", Render("{% set x = 1 + 2 %}{{ x }}", new Dictionary<string, object>()));
Assert.Equal("True", Render("{% set x = a == a %}{{ x }}",
new Dictionary<string, object> { ["a"] = "k" }));
}
[Fact]
public void Range_SingleArg()
{
Assert.Equal("012", Render("{% for i in range(3) %}{{ i }}{% endfor %}", new Dictionary<string, object>()));
}
[Fact]
public void Range_StartStop()
{
Assert.Equal("234", Render("{% for i in range(2, 5) %}{{ i }}{% endfor %}", new Dictionary<string, object>()));
}
[Fact]
public void Range_StartStopStep_Descending()
{
// The Gemma 4 continuation scan uses range(loop.index0 - 1, -1, -1).
Assert.Equal("3,2,1,0,",
Render("{% for i in range(3, -1, -1) %}{{ i }},{% endfor %}", new Dictionary<string, object>()));
}
[Fact]
public void Range_StartStopStep_PositiveStep()
{
Assert.Equal("0246", Render("{% for i in range(0, 8, 2) %}{{ i }}{% endfor %}", new Dictionary<string, object>()));
}
[Fact]
public void NotFilterPrecedence_DefaultFalse()
{
// The Gemma 4 generation-prompt block hinges on
// "{%- if not enable_thinking | default(false) -%}". Jinja binds the filter
// tighter than 'not', i.e. not (enable_thinking | default(false)). With
// enable_thinking undefined or false the result is true (emit the empty
// <|channel>thought<channel|> priming); with true it is false (skip).
const string tmpl = "{%- if not enable_thinking | default(false) -%}EMIT{%- endif -%}";
Assert.Equal("EMIT", Render(tmpl, new Dictionary<string, object>()));
Assert.Equal("EMIT", Render(tmpl, new Dictionary<string, object> { ["enable_thinking"] = false }));
Assert.Equal("", Render(tmpl, new Dictionary<string, object> { ["enable_thinking"] = true }));
}
[Fact]
public void ListPlusList_Concatenates()
{
// Jinja2 `+` on two sequences concatenates them. This used to fall through
// to Stringify() and render the .NET type name
// ("System.Collections.Generic.List`1[System.Object]").
Assert.Equal("a,b,c",
Render("{{ (['a', 'b'] + ['c']) | join(',') }}", new Dictionary<string, object>()));
}
[Fact]
public void NamespaceListAccumulator_SurvivesJoin()
{
// Muse-Glimmer's render_system_meta() builds its recipient list with the
// namespace accumulator idiom and then joins it. Before list concatenation
// was implemented this emitted
// "# Valid recipients: System.Collections.Generic.List`1[System.Object]
// System.Collections.Generic.List`1[System.Object]."
// into every rendered system prompt.
const string tmpl =
"{%- set ns = namespace(recipients=['\"self\"']) -%}" +
"{%- for t in tools -%}{%- set ns.recipients = ns.recipients + ['\"' + t + '.*\"'] -%}{%- endfor -%}" +
"{%- set ns.recipients = ns.recipients + ['\"user\"'] -%}" +
"{{- '# Valid recipients: ' + ns.recipients | join(', ') + '.' -}}";
Assert.Equal("# Valid recipients: \"self\", \"user\".",
Render(tmpl, new Dictionary<string, object> { ["tools"] = new List<object>() }));
Assert.Equal("# Valid recipients: \"self\", \"weather.*\", \"user\".",
Render(tmpl, new Dictionary<string, object> { ["tools"] = new List<object> { "weather" } }));
}
[Fact]
public void NumericAddition_StaysNumeric()
{
// int + int must not degrade to text, and a float on either side must stay
// arithmetic rather than concatenate.
Assert.Equal("3", Render("{{ 1 + 2 }}", new Dictionary<string, object>()));
Assert.Equal("3.5", Render("{{ 1.5 + 2 }}", new Dictionary<string, object>()));
}
}