@@ -6,11 +6,14 @@ package pytest
66
77import (
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+
2328var 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".
140145func 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