@@ -78,3 +78,74 @@ func TestBugCycleSharedState(t *testing.T) {
7878 }
7979 wg .Wait ()
8080}
81+
82+ func 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 ()
140+ result , err := tpl .Execute (ctx )
141+ if err != nil {
142+ t .Errorf ("concurrent: unexpected error: %v" , err )
143+ return
144+ }
145+ if result != expected {
146+ t .Errorf ("concurrent: got %q, want %q" , result , expected )
147+ }
148+ }()
149+ }
150+ wg2 .Wait ()
151+ }
0 commit comments