MCPcopy
hub / github.com/flosch/pongo2 / TestBugIfchangedSharedState

Function TestBugIfchangedSharedState

pongo2_issues_test.go:82–151  ·  view source on GitHub ↗
(t *testing.T)

Source from the content-addressed store, hash-verified

80}
81
82func TestBugIfchangedSharedState(t *testing.T) {
83 // Bug: tagIfchangedNode.lastValues and lastContent were stored on the
84 // AST node, which is shared across all concurrent executions.
85 // This caused two problems:
86 // 1. Data race: concurrent writes to lastValues/lastContent
87 // 2. Semantic bug: ifchanged compares against state from a DIFFERENT
88 // execution, so the first item might be suppressed if a previous
89 // execution ended with the same value.
90 //
91 // Each template execution must have independent ifchanged state.
92
93 tpl, err := pongo2.FromString(`{% for item in items %}{% ifchanged %}{{ item }}{% endifchanged %}{% endfor %}`)
94 if err != nil {
95 t.Fatalf("failed to parse template: %v", err)
96 }
97
98 ctx := pongo2.Context{"items": []string{"a", "a", "b", "b", "c"}}
99 const expected = "abc"
100
101 // First: verify sequential executions produce consistent results.
102 // With shared state, the second execution starts with lastContent="c"
103 // from the first execution, so "a" would be correctly shown (different
104 // from "c"), but the pattern breaks in more complex scenarios.
105 // Use a case where it definitely breaks: items starting with the same
106 // value the previous execution ended with.
107 tplSameEnd, err := pongo2.FromString(`{% for item in items %}{% ifchanged %}{{ item }}{% endifchanged %}{% endfor %}`)
108 if err != nil {
109 t.Fatalf("failed to parse template: %v", err)
110 }
111
112 // Items end with "x", so next execution starting with "x" would skip it
113 ctxEndsX := pongo2.Context{"items": []string{"a", "b", "x"}}
114 ctxStartsX := pongo2.Context{"items": []string{"x", "y", "z"}}
115
116 // First execution ends with lastContent="x"
117 result1, err := tplSameEnd.Execute(ctxEndsX)
118 if err != nil {
119 t.Fatalf("execution 1: unexpected error: %v", err)
120 }
121 if result1 != "abx" {
122 t.Errorf("execution 1: got %q, want %q", result1, "abx")
123 }
124
125 // Second execution should output "x" even though previous ended with "x"
126 result2, err := tplSameEnd.Execute(ctxStartsX)
127 if err != nil {
128 t.Fatalf("execution 2: unexpected error: %v", err)
129 }
130 if result2 != "xyz" {
131 t.Errorf("execution 2: got %q, want %q (ifchanged state leaked from previous execution)", result2, "xyz")
132 }
133
134 // Also verify concurrent executions produce correct results.
135 var wg2 sync.WaitGroup
136 for i := 0; i < 20; i++ {
137 wg2.Add(1)
138 go func() {
139 defer wg2.Done()

Callers

nothing calls this directly

Calls 2

FromStringMethod · 0.80
ExecuteMethod · 0.65

Tested by

no test coverage detected

Used in the wild real call sites across dependent graphs

searching dependent graphs…