(t *testing.T)
| 80 | } |
| 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() |
nothing calls this directly
no test coverage detected
searching dependent graphs…