Skip to content

Commit 89a6ebc

Browse files
sbinetDrew O'Meara
and
Drew O'Meara
committed
pytest: refactor testing infrastructure
This CL also exposes a -regen flag to easily regenerate golden files. Co-authored-by: Drew O'Meara <[email protected]> Signed-off-by: Sebastien Binet <[email protected]>
1 parent 80944be commit 89a6ebc

File tree

1 file changed

+109
-14
lines changed

1 file changed

+109
-14
lines changed

pytest/pytest.go

+109-14
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,14 @@ package pytest
66

77
import (
88
"bytes"
9+
"flag"
10+
"fmt"
911
"io"
1012
"os"
1113
"path"
1214
"path/filepath"
1315
"strings"
16+
"sync/atomic"
1417
"testing"
1518

1619
"github.com/go-python/gpython/compile"
@@ -20,6 +23,8 @@ import (
2023
_ "github.com/go-python/gpython/stdlib"
2124
)
2225

26+
var RegenTestData = flag.Bool("regen", false, "Regenerate golden files from current testdata.")
27+
2328
var gContext = py.NewContext(py.DefaultContextOpts())
2429

2530
// Compile the program in the file prog to code in the module that is returned
@@ -132,58 +137,148 @@ func RunBenchmarks(b *testing.B, testDir string) {
132137

133138
// RunScript runs the provided path to a script.
134139
// RunScript captures the stdout and stderr while executing the script
135-
// and compares it to a golden file:
140+
// and compares it to a golden file, blocking until completion.
136141
//
137142
// RunScript("./testdata/foo.py")
138143
//
139144
// will compare the output with "./testdata/foo_golden.txt".
140145
func RunScript(t *testing.T, fname string) {
146+
147+
RunTestTasks(t, []*Task{
148+
{
149+
PyFile: fname,
150+
},
151+
})
152+
}
153+
154+
// RunTestTasks runs each given task in a newly created py.Context concurrently.
155+
// If a fatal error is encountered, the given testing.T is signaled.
156+
func RunTestTasks(t *testing.T, tasks []*Task) {
157+
onCompleted := make(chan *Task)
158+
159+
numTasks := len(tasks)
160+
for ti := 0; ti < numTasks; ti++ {
161+
task := tasks[ti]
162+
go func() {
163+
err := task.run()
164+
task.Err = err
165+
onCompleted <- task
166+
}()
167+
}
168+
169+
tasks = tasks[:0]
170+
for ti := 0; ti < numTasks; ti++ {
171+
task := <-onCompleted
172+
if task.Err != nil {
173+
t.Error(task.Err)
174+
}
175+
tasks = append(tasks, task)
176+
}
177+
}
178+
179+
var (
180+
taskCounter int32
181+
)
182+
183+
type Task struct {
184+
num int32 // Assigned when this task is run
185+
ID string // unique key identifying this task. If empty, autogenerated from the basename of PyFile
186+
PyFile string // If set, this file pathname is executed in a newly created ctx
187+
PyTask func(ctx py.Context) error // If set, a new created ctx is created and this blocks until completion
188+
GoldFile string // Filename containing the "gold standard" stdout+stderr. If empty, autogenerated from PyFile or ID
189+
Err error // Non-nil if a fatal error is encountered with this task
190+
}
191+
192+
func (task *Task) run() error {
193+
fileBase := ""
194+
141195
opts := py.DefaultContextOpts()
142-
opts.SysArgs = []string{fname}
196+
if task.PyFile != "" {
197+
opts.SysArgs = []string{task.PyFile}
198+
if task.ID == "" {
199+
ext := filepath.Ext(task.PyFile)
200+
fileBase = task.PyFile[0 : len(task.PyFile)-len(ext)]
201+
}
202+
}
203+
204+
task.num = atomic.AddInt32(&taskCounter, 1)
205+
if task.ID == "" {
206+
if fileBase == "" {
207+
task.ID = fmt.Sprintf("task-%04d", atomic.AddInt32(&taskCounter, 1))
208+
} else {
209+
task.ID = strings.TrimPrefix(fileBase, "./")
210+
}
211+
}
212+
213+
if task.GoldFile == "" {
214+
task.GoldFile = fileBase + "_golden.txt"
215+
}
216+
143217
ctx := py.NewContext(opts)
144218
defer ctx.Close()
145219

146220
sys := ctx.Store().MustGetModule("sys")
147221
tmp, err := os.MkdirTemp("", "gpython-pytest-")
148222
if err != nil {
149-
t.Fatal(err)
223+
return err
150224
}
151225
defer os.RemoveAll(tmp)
152226

153227
out, err := os.Create(filepath.Join(tmp, "combined"))
154228
if err != nil {
155-
t.Fatalf("could not create stdout/stderr: %+v", err)
229+
return fmt.Errorf("could not create stdout+stderr output file: %w", err)
156230
}
157231
defer out.Close()
158232

159233
sys.Globals["stdout"] = &py.File{File: out, FileMode: py.FileWrite}
160234
sys.Globals["stderr"] = &py.File{File: out, FileMode: py.FileWrite}
161235

162-
_, err = py.RunFile(ctx, fname, py.CompileOpts{}, nil)
163-
if err != nil {
164-
t.Fatalf("could not run script %q: %+v", fname, err)
236+
if task.PyFile != "" {
237+
_, err := py.RunFile(ctx, task.PyFile, py.CompileOpts{}, nil)
238+
if err != nil {
239+
return fmt.Errorf("could not run target script %q: %w", task.PyFile, err)
240+
}
165241
}
166242

243+
if task.PyTask != nil {
244+
err := task.PyTask(ctx)
245+
if err != nil {
246+
return fmt.Errorf("PyTask %q failed: %w", task.ID, err)
247+
}
248+
}
249+
250+
// Close the ctx explicitly as it may legitimately generate output
251+
ctx.Close()
252+
<-ctx.Done()
253+
167254
err = out.Close()
168255
if err != nil {
169-
t.Fatalf("could not close stdout/stderr: %+v", err)
256+
return fmt.Errorf("could not close output file: %w", err)
170257
}
171258

172259
got, err := os.ReadFile(out.Name())
173260
if err != nil {
174-
t.Fatalf("could not read script output: %+v", err)
261+
return fmt.Errorf("could not read script output file: %w", err)
175262
}
176263

177-
ref := fname[:len(fname)-len(".py")] + "_golden.txt"
178-
want, err := os.ReadFile(ref)
264+
if *RegenTestData {
265+
err := os.WriteFile(task.GoldFile, got, 0644)
266+
if err != nil {
267+
return fmt.Errorf("could not write golden output %q: %w", task.GoldFile, err)
268+
}
269+
}
270+
271+
want, err := os.ReadFile(task.GoldFile)
179272
if err != nil {
180-
t.Fatalf("could not read golden output %q: %+v", ref, err)
273+
return fmt.Errorf("could not read golden output %q: %w", task.GoldFile, err)
181274
}
182275

183276
diff := cmp.Diff(string(want), string(got))
184277
if !bytes.Equal(got, want) {
185-
out := fname[:len(fname)-len(".py")] + ".txt"
278+
out := fileBase + ".txt"
186279
_ = os.WriteFile(out, got, 0644)
187-
t.Fatalf("output differ: -- (-ref +got)\n%s", diff)
280+
return fmt.Errorf("output differ: -- (-ref +got)\n%s", diff)
188281
}
282+
283+
return nil
189284
}

0 commit comments

Comments
 (0)