mirror of
https://github.com/semihalev/twig.git
synced 2026-03-14 13:55:46 +01:00
- Fixed macro parameter parsing with improved tokenization handling - Added support for proper module.function() syntax in templates - Enhanced filter handling within macro variables - Added support for variable interpolation in macro body - Added comprehensive test suite for macros with various scenarios - Improved overall coverage from 43.8% to 45.7% 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
325 lines
8.9 KiB
Go
325 lines
8.9 KiB
Go
package twig
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// TestSimpleControlStructures tests simple control structures (if, for)
|
|
func TestSimpleControlStructures(t *testing.T) {
|
|
engine := New()
|
|
|
|
tests := []struct {
|
|
name string
|
|
source string
|
|
context map[string]interface{}
|
|
expected string
|
|
}{
|
|
// If statements
|
|
{
|
|
name: "Simple if (true)",
|
|
source: "{% if true %}yes{% endif %}",
|
|
context: nil,
|
|
expected: "yes",
|
|
},
|
|
{
|
|
name: "Simple if (false)",
|
|
source: "{% if false %}yes{% endif %}",
|
|
context: nil,
|
|
expected: "",
|
|
},
|
|
{
|
|
name: "If-else (true)",
|
|
source: "{% if true %}yes{% else %}no{% endif %}",
|
|
context: nil,
|
|
expected: "yes",
|
|
},
|
|
{
|
|
name: "If-else (false)",
|
|
source: "{% if false %}yes{% else %}no{% endif %}",
|
|
context: nil,
|
|
expected: "no",
|
|
},
|
|
{
|
|
name: "If-elseif-else (first true)",
|
|
source: "{% if true %}1{% elseif true %}2{% else %}3{% endif %}",
|
|
context: nil,
|
|
expected: "1",
|
|
},
|
|
{
|
|
name: "If-elseif-else (second true)",
|
|
source: "{% if false %}1{% elseif true %}2{% else %}3{% endif %}",
|
|
context: nil,
|
|
expected: "2",
|
|
},
|
|
{
|
|
name: "If-elseif-else (all false)",
|
|
source: "{% if false %}1{% elseif false %}2{% else %}3{% endif %}",
|
|
context: nil,
|
|
expected: "3",
|
|
},
|
|
{
|
|
name: "Nested if statements",
|
|
source: "{% if true %}outer{% if false %}inner-if{% else %}inner-else{% endif %}{% endif %}",
|
|
context: nil,
|
|
expected: "outerinner-else",
|
|
},
|
|
{
|
|
name: "If with variable condition",
|
|
source: "{% if value %}yes{% else %}no{% endif %}",
|
|
context: map[string]interface{}{"value": true},
|
|
expected: "yes",
|
|
},
|
|
{
|
|
name: "If with complex condition (and)",
|
|
source: "{% if value and otherValue %}yes{% else %}no{% endif %}",
|
|
context: map[string]interface{}{"value": true, "otherValue": true},
|
|
expected: "yes",
|
|
},
|
|
{
|
|
name: "If with complex condition (or)",
|
|
source: "{% if value or otherValue %}yes{% else %}no{% endif %}",
|
|
context: map[string]interface{}{"value": false, "otherValue": true},
|
|
expected: "yes",
|
|
},
|
|
{
|
|
name: "If with is test",
|
|
source: "{% if value is defined %}yes{% else %}no{% endif %}",
|
|
context: map[string]interface{}{"value": "something"},
|
|
expected: "yes",
|
|
},
|
|
{
|
|
name: "If with complex condition (parentheses)",
|
|
source: "{% if (a or b) and c %}yes{% else %}no{% endif %}",
|
|
context: map[string]interface{}{"a": true, "b": false, "c": true},
|
|
expected: "yes",
|
|
},
|
|
|
|
// For loops
|
|
{
|
|
name: "For loop with array",
|
|
source: "{% for item in items %}{{ item }}{% endfor %}",
|
|
context: map[string]interface{}{"items": []string{"a", "b", "c"}},
|
|
expected: "abc",
|
|
},
|
|
{
|
|
name: "For loop with array literal",
|
|
source: "{% for item in ['a', 'b', 'c'] %}{{ item }}{% endfor %}",
|
|
context: nil,
|
|
expected: "abc",
|
|
},
|
|
{
|
|
name: "For loop with map",
|
|
source: "{% for key, value in data %}{{ key }}:{{ value }};{% endfor %}",
|
|
context: map[string]interface{}{"data": map[string]interface{}{"a": 1, "b": 2, "c": 3}},
|
|
expected: "a:1;b:2;c:3;", // This is just a reference; actual map iteration order is checked separately
|
|
},
|
|
{
|
|
name: "For loop with loop variable",
|
|
source: "{% for item in items %}{{ loop.index }}:{{ item }};{% endfor %}",
|
|
context: map[string]interface{}{"items": []string{"a", "b", "c"}},
|
|
expected: "1:a;2:b;3:c;",
|
|
},
|
|
{
|
|
name: "Nested for loops",
|
|
source: "{% for i in items %}{% for j in items %}({{ i }},{{ j }}){% endfor %}{% endfor %}",
|
|
context: map[string]interface{}{"items": []string{"a", "b"}},
|
|
expected: "(a,a)(a,b)(b,a)(b,b)",
|
|
},
|
|
{
|
|
name: "For loop with if condition",
|
|
source: "{% for item in items %}{% if loop.first %}first:{% endif %}{{ item }}{% endfor %}",
|
|
context: map[string]interface{}{"items": []string{"a", "b", "c"}},
|
|
expected: "first:abc",
|
|
},
|
|
{
|
|
name: "For loop with else (non-empty)",
|
|
source: "{% for item in items %}{{ item }}{% else %}empty{% endfor %}",
|
|
context: map[string]interface{}{"items": []string{"a", "b", "c"}},
|
|
expected: "abc",
|
|
},
|
|
{
|
|
name: "For loop with else (empty)",
|
|
source: "{% for item in items %}{{ item }}{% else %}empty{% endfor %}",
|
|
context: map[string]interface{}{"items": []string{}},
|
|
expected: "empty",
|
|
},
|
|
{
|
|
name: "For loop with array access",
|
|
source: "{% for i in range(0, 2) %}{{ items[i] }}{% endfor %}",
|
|
context: map[string]interface{}{"items": []string{"a", "b", "c"}},
|
|
expected: "abc",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
err := engine.RegisterString("test", tt.source)
|
|
if err != nil {
|
|
t.Fatalf("Error registering template: %v", err)
|
|
}
|
|
|
|
result, err := engine.Render("test", tt.context)
|
|
if err != nil {
|
|
t.Fatalf("Error rendering template: %v", err)
|
|
}
|
|
|
|
// Special handling for map iteration which is non-deterministic in Go
|
|
if tt.name == "For loop with map" {
|
|
// Check that the result contains all the expected key-value pairs
|
|
expectedPairs := []string{"a:1;", "b:2;", "c:3;"}
|
|
for _, pair := range expectedPairs {
|
|
if !strings.Contains(result, pair) {
|
|
t.Errorf("Expected result to contain %q, but got: %q", pair, result)
|
|
}
|
|
}
|
|
} else if result != tt.expected {
|
|
t.Errorf("Expected: %q, Got: %q", tt.expected, result)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestRangeFunctionInForLoop tests the range function directly in a for loop
|
|
func TestRangeFunctionInForLoop(t *testing.T) {
|
|
engine := New()
|
|
|
|
// Test the actual rendering
|
|
tests := []struct {
|
|
name string
|
|
source string
|
|
expected string
|
|
}{
|
|
{
|
|
name: "Simple range function in for loop",
|
|
source: "{% for i in range(1, 3) %}{{ i }}{% endfor %}",
|
|
expected: "123",
|
|
},
|
|
{
|
|
name: "Range function with step in for loop",
|
|
source: "{% for i in range(1, 9, 2) %}{{ i }}{% endfor %}",
|
|
expected: "13579",
|
|
},
|
|
{
|
|
name: "Range function with loop variable",
|
|
source: "{% for i in range(1, 3) %}{{ loop.index }}:{{ i }};{% endfor %}",
|
|
expected: "1:1;2:2;3:3;",
|
|
},
|
|
{
|
|
name: "Range function with direct negative start",
|
|
source: "{% for i in range(-3, 0) %}{{ i }}{% endfor %}",
|
|
expected: "-3-2-10",
|
|
},
|
|
{
|
|
name: "Range function with parenthesized negative start",
|
|
source: "{% for i in range((-5), 0) %}{{ i }}{% endfor %}",
|
|
expected: "-5-4-3-2-10",
|
|
},
|
|
{
|
|
name: "Range with negative end value",
|
|
source: "{% for i in range(0, -3, -1) %}{{ i }}{% endfor %}",
|
|
expected: "0-1-2-3",
|
|
},
|
|
{
|
|
name: "Range with complex negative literals",
|
|
source: "{% for i in range((-10), (-5)) %}{{ i }}{% endfor %}",
|
|
expected: "-10-9-8-7-6-5",
|
|
},
|
|
{
|
|
name: "Range with arithmetic expressions for bounds",
|
|
source: "{% for i in range(0-5, 0) %}{{ i }}{% endfor %}",
|
|
expected: "-5-4-3-2-10",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
err := engine.RegisterString("test", tt.source)
|
|
if err != nil {
|
|
t.Fatalf("Error registering template: %v", err)
|
|
}
|
|
|
|
result, err := engine.Render("test", nil)
|
|
if err != nil {
|
|
t.Fatalf("Error rendering template: %v", err)
|
|
}
|
|
|
|
if result != tt.expected {
|
|
t.Errorf("Expected: %q, Got: %q", tt.expected, result)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestConditionalExpressions tests conditional expressions (ternary)
|
|
func TestConditionalExpressions(t *testing.T) {
|
|
engine := New()
|
|
|
|
tests := []struct {
|
|
name string
|
|
source string
|
|
context map[string]interface{}
|
|
expected string
|
|
}{
|
|
{
|
|
name: "Simple ternary (true)",
|
|
source: "{{ true ? 'yes' : 'no' }}",
|
|
context: nil,
|
|
expected: "yes",
|
|
},
|
|
{
|
|
name: "Simple ternary (false)",
|
|
source: "{{ false ? 'yes' : 'no' }}",
|
|
context: nil,
|
|
expected: "no",
|
|
},
|
|
{
|
|
name: "Ternary with variable",
|
|
source: "{{ value ? 'yes' : 'no' }}",
|
|
context: map[string]interface{}{"value": true},
|
|
expected: "yes",
|
|
},
|
|
{
|
|
name: "Ternary with complex condition",
|
|
source: "{{ (a or b) and c ? 'yes' : 'no' }}",
|
|
context: map[string]interface{}{"a": true, "b": false, "c": true},
|
|
expected: "yes",
|
|
},
|
|
{
|
|
name: "Nested ternary (outer true)",
|
|
source: "{{ true ? (true ? '1' : '2') : '3' }}",
|
|
context: nil,
|
|
expected: "1",
|
|
},
|
|
{
|
|
name: "Nested ternary (outer false)",
|
|
source: "{{ false ? '1' : (true ? '2' : '3') }}",
|
|
context: nil,
|
|
expected: "2",
|
|
},
|
|
{
|
|
name: "Ternary with arithmetic",
|
|
source: "{{ 5 > 2 ? 5 + 3 : 2 - 1 }}",
|
|
context: nil,
|
|
expected: "8",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
err := engine.RegisterString("test", tt.source)
|
|
if err != nil {
|
|
t.Fatalf("Error registering template: %v", err)
|
|
}
|
|
|
|
result, err := engine.Render("test", tt.context)
|
|
if err != nil {
|
|
t.Fatalf("Error rendering template: %v", err)
|
|
}
|
|
|
|
if result != tt.expected {
|
|
t.Errorf("Expected: %q, Got: %q", tt.expected, result)
|
|
}
|
|
})
|
|
}
|
|
}
|