Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 16 additions & 4 deletions builtins.go
Original file line number Diff line number Diff line change
Expand Up @@ -930,7 +930,10 @@ func filterSelect(args []Value, kwargs map[string]Value) (Value, error) {
testName = args[1].AsString()
}

testArgs := args[2:]
var testArgs []Value
if len(args) > 2 {
testArgs = args[2:]
}

var items []Value
for _, item := range list.Items {
Expand Down Expand Up @@ -963,7 +966,10 @@ func filterReject(args []Value, kwargs map[string]Value) (Value, error) {
testName = args[1].AsString()
}

testArgs := args[2:]
var testArgs []Value
if len(args) > 2 {
testArgs = args[2:]
}

var items []Value
for _, item := range list.Items {
Expand Down Expand Up @@ -997,7 +1003,10 @@ func filterSelectattr(args []Value, kwargs map[string]Value) (Value, error) {
testName = printValue(args[2])
}

testArgs := args[3:]
var testArgs []Value
if len(args) > 3 {
testArgs = args[3:]
}

var items []Value
for _, item := range list.Items {
Expand Down Expand Up @@ -1038,7 +1047,10 @@ func filterRejectattr(args []Value, kwargs map[string]Value) (Value, error) {
testName = printValue(args[2])
}

testArgs := args[3:]
var testArgs []Value
if len(args) > 3 {
testArgs = args[3:]
}

var items []Value
for _, item := range list.Items {
Expand Down
141 changes: 141 additions & 0 deletions jinjafix_regression_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
package jinja

import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"os"
"os/exec"
"runtime/debug"
"testing"
"time"
)

type jinjaFixResult struct {
ID string `json:"id"`
Stage string `json:"stage"`
Output string `json:"output,omitempty"`
Error string `json:"error,omitempty"`
}

func jinjaFixEvaluate(source string) (r jinjaFixResult) {
defer func() {
if p := recover(); p != nil {
r = jinjaFixResult{Stage: "panic", Error: fmt.Sprint(p)}
}
}()
template, err := Compile(source)
if err != nil {
return jinjaFixResult{Stage: "compile_error", Error: err.Error()}
}
output, err := template.Render(nil)
if err != nil {
return jinjaFixResult{Stage: "render_error", Error: err.Error()}
}
return jinjaFixResult{Stage: "render", Output: output}
}

// Child processes bound nontermination and stack exhaustion in broken revisions.
func TestJinjaFixWorker(t *testing.T) {
if os.Getenv("JINJAFIX_WORKER") != "1" {
return
}
debug.SetMaxStack(2 << 20)
source, err := io.ReadAll(os.Stdin)
if err != nil {
os.Exit(2)
}
if json.NewEncoder(os.Stdout).Encode(jinjaFixEvaluate(string(source))) != nil {
os.Exit(2)
}
os.Exit(0)
}

func jinjaFixReferenceMissing(found bool) bool {
return !found
}

func TestJinjaFixRegression(t *testing.T) {
var cases []struct {
ID string `json:"id"`
Source string `json:"source"`
GoStage string `json:"go_stage,omitempty"`
Reason string `json:"reason,omitempty"`
}
var python struct {
Cases []jinjaFixResult `json:"cases"`
}
read := func(path string, v any) {
b, err := os.ReadFile(path)
if err != nil {
t.Fatal(err)
}
if err = json.Unmarshal(b, v); err != nil {
t.Fatal(err)
}
}
read("testdata/jinjafix/cases.json", &cases)
read("testdata/jinjafix/python-result.json", &python)
expected := map[string]jinjaFixResult{}
for _, r := range python.Cases {
expected[r.ID] = r
}
var results []jinjaFixResult
for _, c := range cases {
t.Run(c.ID, func(t *testing.T) {
want, ok := expected[c.ID]
if jinjaFixReferenceMissing(ok) {
t.Fatal("missing Python result")
}
if c.GoStage != "" {
if c.Reason == "" {
t.Fatal("undocumented parity exception")
}
want.Stage = c.GoStage
}
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
cmd := exec.CommandContext(ctx, os.Args[0], "-test.run=^TestJinjaFixWorker$")
cmd.Env = append(os.Environ(), "JINJAFIX_WORKER=1", "GOMEMLIMIT=64MiB", "GOTRACEBACK=none")
cmd.Stdin = bytes.NewBufferString(c.Source)
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
err := cmd.Run()
var got jinjaFixResult
switch {
case ctx.Err() != nil:
got = jinjaFixResult{Stage: "timeout", Error: ctx.Err().Error()}
case err != nil:
got = jinjaFixResult{Stage: "process_error", Error: err.Error()}
default:
if err = json.Unmarshal(stdout.Bytes(), &got); err != nil {
t.Fatal(err)
}
}
got.ID = c.ID
results = append(results, got)
if jinjaFixResultsDiffer(got, want) {
t.Errorf("Go: stage=%s output=%q; expected: stage=%s output=%q; error=%s", got.Stage, got.Output, want.Stage, want.Output, got.Error)
}
})
}
if path := os.Getenv("JINJAFIX_RESULT_PATH"); path != "" {
b, err := json.MarshalIndent(results, "", " ")
if err != nil {
t.Fatal(err)
}
if err = os.WriteFile(path, append(b, '\n'), 0644); err != nil {
t.Fatal(err)
}
}
}

func jinjaFixResultsDiffer(got, want jinjaFixResult) bool {
if got.Stage != want.Stage {
return true
}
return want.Stage == "render" && got.Output != want.Output
}
9 changes: 9 additions & 0 deletions testdata/jinjafix/README.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
Reproduce the Python reference:
python3 testdata/jinjafix/reference.py

Run the shared-input Go regression:
go test -count=1 -v -run ^TestJinjaFixRegression$ .

The test commit records Python results and failing Go results from the same
inputs. The fix commit records passing Go results and the full Go test suite.
Worker processes bound panics, stack exhaustion and nontermination.
26 changes: 26 additions & 0 deletions testdata/jinjafix/cases.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
[
{
"id": "B064-01",
"source": "{{ [0,1,2]|select|list }}",
"origin": "testdata/provisioning/5g-network.jinja",
"source_line": 1071
},
{
"id": "B064-02",
"source": "{{ [0,1,2]|reject|list }}",
"origin": "testdata/provisioning/5g-network.jinja",
"source_line": 1075
},
{
"id": "B065-01",
"source": "{{ [{'enabled':true},{'enabled':false}]|selectattr('enabled')|list|length }}",
"origin": "testdata/provisioning/5g-network.jinja",
"source_line": 1079
},
{
"id": "B065-02",
"source": "{{ [{'enabled':true},{'enabled':false}]|rejectattr('enabled')|list|length }}",
"origin": "testdata/provisioning/5g-network.jinja",
"source_line": 1083
}
]
22 changes: 22 additions & 0 deletions testdata/jinjafix/go-after.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
[
{
"id": "B064-01",
"stage": "render",
"output": "[1, 2]"
},
{
"id": "B064-02",
"stage": "render",
"output": "[0]"
},
{
"id": "B065-01",
"stage": "render",
"output": "1"
},
{
"id": "B065-02",
"stage": "render",
"output": "1"
}
]
12 changes: 12 additions & 0 deletions testdata/jinjafix/go-after.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
=== RUN TestJinjaFixRegression
=== RUN TestJinjaFixRegression/B064-01
=== RUN TestJinjaFixRegression/B064-02
=== RUN TestJinjaFixRegression/B065-01
=== RUN TestJinjaFixRegression/B065-02
--- PASS: TestJinjaFixRegression (0.02s)
--- PASS: TestJinjaFixRegression/B064-01 (0.00s)
--- PASS: TestJinjaFixRegression/B064-02 (0.00s)
--- PASS: TestJinjaFixRegression/B065-01 (0.00s)
--- PASS: TestJinjaFixRegression/B065-02 (0.00s)
PASS
ok github.com/ardanlabs/jinja 0.021s
22 changes: 22 additions & 0 deletions testdata/jinjafix/go-before.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
[
{
"id": "B064-01",
"stage": "panic",
"error": "runtime error: slice bounds out of range [2:1]"
},
{
"id": "B064-02",
"stage": "panic",
"error": "runtime error: slice bounds out of range [2:1]"
},
{
"id": "B065-01",
"stage": "panic",
"error": "runtime error: slice bounds out of range [3:2]"
},
{
"id": "B065-02",
"stage": "panic",
"error": "runtime error: slice bounds out of range [3:2]"
}
]
17 changes: 17 additions & 0 deletions testdata/jinjafix/go-before.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
=== RUN TestJinjaFixRegression
=== RUN TestJinjaFixRegression/B064-01
jinjafix_regression_test.go:121: Go: stage=panic output=""; expected: stage=render output="[1, 2]"; error=runtime error: slice bounds out of range [2:1]
=== RUN TestJinjaFixRegression/B064-02
jinjafix_regression_test.go:121: Go: stage=panic output=""; expected: stage=render output="[0]"; error=runtime error: slice bounds out of range [2:1]
=== RUN TestJinjaFixRegression/B065-01
jinjafix_regression_test.go:121: Go: stage=panic output=""; expected: stage=render output="1"; error=runtime error: slice bounds out of range [3:2]
=== RUN TestJinjaFixRegression/B065-02
jinjafix_regression_test.go:121: Go: stage=panic output=""; expected: stage=render output="1"; error=runtime error: slice bounds out of range [3:2]
--- FAIL: TestJinjaFixRegression (0.02s)
--- FAIL: TestJinjaFixRegression/B064-01 (0.00s)
--- FAIL: TestJinjaFixRegression/B064-02 (0.00s)
--- FAIL: TestJinjaFixRegression/B065-01 (0.00s)
--- FAIL: TestJinjaFixRegression/B065-02 (0.00s)
FAIL
FAIL github.com/ardanlabs/jinja 0.020s
FAIL
4 changes: 4 additions & 0 deletions testdata/jinjafix/go-suite-after.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
ok github.com/ardanlabs/jinja 9.429s
? github.com/ardanlabs/jinja/examples/basic [no test files]
? github.com/ardanlabs/jinja/examples/chat [no test files]
? github.com/ardanlabs/jinja/examples/toolcall [no test files]
9 changes: 9 additions & 0 deletions testdata/jinjafix/provenance.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"base_commit": "b1bf3a06adcade12b59a1dfaa2edef6f4a7d6e09",
"selected_fix_commit": "5bebe578b37e036788e6d0115e650681fddbe3a0",
"original_branch": "fast-fix-select-arg-panic",
"corpus_sha256": "e844754b9724b93fd2fc162d3d756cca40a57d6ed07188b02d02b2bcbe834bb9",
"comparison": "Exact successful output; compile/render error stage agreement. Error strings are recorded, not equated.",
"previous_test_commit": "ea3969644f184067e534d5c714ed81254ddfe4e6",
"previous_fix_commit": "6d04c4cbf738b6084fab3744a69575f92c1d51ea"
}
34 changes: 34 additions & 0 deletions testdata/jinjafix/python-result.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
{
"python": "3.14.4",
"jinja2": "3.1.6",
"settings": {
"trim_blocks": true,
"lstrip_blocks": true,
"keep_trailing_newline": true
},
"extensions": [
"fromjson=json.loads"
],
"cases": [
{
"id": "B064-01",
"stage": "render",
"output": "[1, 2]"
},
{
"id": "B064-02",
"stage": "render",
"output": "[0]"
},
{
"id": "B065-01",
"stage": "render",
"output": "1"
},
{
"id": "B065-02",
"stage": "render",
"output": "1"
}
]
}
42 changes: 42 additions & 0 deletions testdata/jinjafix/reference.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
"""Recompute Python results for the exact inputs used by the Go unit test."""
import json
import pathlib
import platform
import sys

import jinja2


def evaluate(source):
env = jinja2.Environment(trim_blocks=True, lstrip_blocks=True,
keep_trailing_newline=True)
# fromjson is a repository extension; tojson remains stock Jinja2.
env.filters['fromjson'] = json.loads
try:
template = env.from_string(source)
except Exception as error:
return dict(stage='compile_error', error=str(error), exception=type(error).__name__)
try:
return dict(stage='render', output=template.render())
except Exception as error:
return dict(stage='render_error', error=str(error), exception=type(error).__name__)


if __name__ == '__main__':
if len(sys.argv) > 1 and sys.argv[1] == '--worker':
print(json.dumps(evaluate(sys.stdin.read()), ensure_ascii=False))
else:
import subprocess
here = pathlib.Path(__file__).resolve().parent
cases = json.loads((here / 'cases.json').read_text())
results = []
for case in cases:
child = subprocess.run([sys.executable, __file__, '--worker'],
input=case['source'], capture_output=True,
text=True, timeout=5, check=True)
results.append(dict(id=case['id'], **json.loads(child.stdout)))
print(json.dumps(dict(python=platform.python_version(), jinja2=jinja2.__version__,
settings=dict(trim_blocks=True, lstrip_blocks=True,
keep_trailing_newline=True),
extensions=['fromjson=json.loads'], cases=results),
ensure_ascii=False, indent=2))
1 change: 1 addition & 0 deletions testdata/jinjafix/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Jinja2==3.1.6