github.com/rogpeppe/go-internal@v1.12.1-0.20240509064211-c8567cf8e95f/cmd/testscript/main.go (about) 1 // Copyright 2018 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 package main 6 7 import ( 8 "errors" 9 "flag" 10 "fmt" 11 "io" 12 "os" 13 "os/exec" 14 "path/filepath" 15 "strings" 16 "sync/atomic" 17 18 "github.com/rogpeppe/go-internal/goproxytest" 19 "github.com/rogpeppe/go-internal/gotooltest" 20 "github.com/rogpeppe/go-internal/testscript" 21 "github.com/rogpeppe/go-internal/txtar" 22 ) 23 24 const ( 25 // goModProxyDir is the special subdirectory in a txtar script's supporting files 26 // within which we expect to find github.com/rogpeppe/go-internal/goproxytest 27 // directories. 28 goModProxyDir = ".gomodproxy" 29 ) 30 31 type envVarsFlag struct { 32 vals []string 33 } 34 35 func (e *envVarsFlag) String() string { 36 return fmt.Sprintf("%v", e.vals) 37 } 38 39 func (e *envVarsFlag) Set(v string) error { 40 e.vals = append(e.vals, v) 41 return nil 42 } 43 44 func main() { 45 os.Exit(main1()) 46 } 47 48 func main1() int { 49 switch err := mainerr(); err { 50 case nil: 51 return 0 52 case flag.ErrHelp: 53 return 2 54 default: 55 fmt.Fprintln(os.Stderr, err) 56 return 1 57 } 58 } 59 60 func mainerr() (retErr error) { 61 fs := flag.NewFlagSet(os.Args[0], flag.ContinueOnError) 62 fs.Usage = func() { 63 mainUsage(os.Stderr) 64 } 65 var envVars envVarsFlag 66 fUpdate := fs.Bool("u", false, "update archive file if a cmp fails") 67 fWork := fs.Bool("work", false, "print temporary work directory and do not remove when done") 68 fContinue := fs.Bool("continue", false, "continue running the script if an error occurs") 69 fVerbose := fs.Bool("v", false, "run tests verbosely") 70 fs.Var(&envVars, "e", "pass through environment variable to script (can appear multiple times)") 71 if err := fs.Parse(os.Args[1:]); err != nil { 72 return err 73 } 74 75 td, err := os.MkdirTemp("", "testscript") 76 if err != nil { 77 return fmt.Errorf("unable to create temp dir: %v", err) 78 } 79 if *fWork { 80 fmt.Fprintf(os.Stderr, "temporary work directory: %v\n", td) 81 } else { 82 defer os.RemoveAll(td) 83 } 84 85 files := fs.Args() 86 if len(files) == 0 { 87 files = []string{"-"} 88 } 89 90 // If we are only reading from stdin, -u cannot be specified. It seems a bit 91 // bizarre to invoke testscript with '-' and a regular file, but hey. In 92 // that case the -u flag will only apply to the regular file and we assume 93 // the user knows it. 94 onlyReadFromStdin := true 95 for _, f := range files { 96 if f != "-" { 97 onlyReadFromStdin = false 98 } 99 } 100 if onlyReadFromStdin && *fUpdate { 101 return fmt.Errorf("cannot use -u when reading from stdin") 102 } 103 104 tr := testRunner{ 105 update: *fUpdate, 106 continueOnError: *fContinue, 107 verbose: *fVerbose, 108 env: envVars.vals, 109 testWork: *fWork, 110 } 111 112 dirNames := make(map[string]int) 113 for _, filename := range files { 114 // TODO make running files concurrent by default? If we do, note we'll need to do 115 // something smarter with the runner stdout and stderr below 116 117 // Derive a name for the directory from the basename of file, making 118 // uniq by adding a numeric suffix in the case we otherwise end 119 // up with multiple files with the same basename 120 dirName := filepath.Base(filename) 121 count := dirNames[dirName] 122 dirNames[dirName] = count + 1 123 if count != 0 { 124 dirName = fmt.Sprintf("%s%d", dirName, count) 125 } 126 127 runDir := filepath.Join(td, dirName) 128 if err := os.Mkdir(runDir, 0o777); err != nil { 129 return fmt.Errorf("failed to create a run directory within %v for %v: %v", td, renderFilename(filename), err) 130 } 131 if err := tr.run(runDir, filename); err != nil { 132 return err 133 } 134 } 135 136 return nil 137 } 138 139 type testRunner struct { 140 // update denotes that the source testscript archive filename should be 141 // updated in the case of any cmp failures. 142 update bool 143 144 // continueOnError indicates that T.FailNow should not panic, allowing the 145 // test script to continue running. Note that T is still marked as failed. 146 continueOnError bool 147 148 // verbose indicates the running of the script should be noisy. 149 verbose bool 150 151 // env is the environment that should be set on top of the base 152 // testscript-defined minimal environment. 153 env []string 154 155 // testWork indicates whether or not temporary working directory trees 156 // should be left behind. Corresponds exactly to the 157 // testscript.Params.TestWork field. 158 testWork bool 159 } 160 161 // run runs the testscript archive located at the path filename, within the 162 // working directory runDir. filename could be "-" in the case of stdin 163 func (tr *testRunner) run(runDir, filename string) error { 164 var ar *txtar.Archive 165 var err error 166 167 mods := filepath.Join(runDir, goModProxyDir) 168 169 if err := os.MkdirAll(mods, 0o777); err != nil { 170 return fmt.Errorf("failed to create goModProxy dir: %v", err) 171 } 172 173 if filename == "-" { 174 byts, err := io.ReadAll(os.Stdin) 175 if err != nil { 176 return fmt.Errorf("failed to read from stdin: %v", err) 177 } 178 ar = txtar.Parse(byts) 179 } else { 180 ar, err = txtar.ParseFile(filename) 181 } 182 183 if err != nil { 184 return fmt.Errorf("failed to txtar parse %v: %v", renderFilename(filename), err) 185 } 186 187 var script, gomodProxy txtar.Archive 188 script.Comment = ar.Comment 189 190 for _, f := range ar.Files { 191 fp := filepath.Clean(filepath.FromSlash(f.Name)) 192 parts := strings.Split(fp, string(os.PathSeparator)) 193 194 if len(parts) > 1 && parts[0] == goModProxyDir { 195 gomodProxy.Files = append(gomodProxy.Files, f) 196 } else { 197 script.Files = append(script.Files, f) 198 } 199 } 200 201 if txtar.Write(&gomodProxy, runDir); err != nil { 202 return fmt.Errorf("failed to write .gomodproxy files: %v", err) 203 } 204 205 scriptFile := filepath.Join(runDir, "script.txtar") 206 207 if err := os.WriteFile(scriptFile, txtar.Format(&script), 0o666); err != nil { 208 return fmt.Errorf("failed to write script for %v: %v", renderFilename(filename), err) 209 } 210 211 p := testscript.Params{ 212 Dir: runDir, 213 UpdateScripts: tr.update, 214 ContinueOnError: tr.continueOnError, 215 } 216 217 if _, err := exec.LookPath("go"); err == nil { 218 if err := gotooltest.Setup(&p); err != nil { 219 return fmt.Errorf("failed to setup go tool for %v run: %v", renderFilename(filename), err) 220 } 221 } 222 223 addSetup := func(f func(env *testscript.Env) error) { 224 origSetup := p.Setup 225 p.Setup = func(env *testscript.Env) error { 226 if origSetup != nil { 227 if err := origSetup(env); err != nil { 228 return err 229 } 230 } 231 return f(env) 232 } 233 } 234 235 if tr.testWork { 236 addSetup(func(env *testscript.Env) error { 237 fmt.Fprintf(os.Stderr, "temporary work directory for %s: %s\n", renderFilename(filename), env.WorkDir) 238 return nil 239 }) 240 } 241 242 if len(gomodProxy.Files) > 0 { 243 srv, err := goproxytest.NewServer(mods, "") 244 if err != nil { 245 return fmt.Errorf("cannot start proxy for %v: %v", renderFilename(filename), err) 246 } 247 defer srv.Close() 248 249 addSetup(func(env *testscript.Env) error { 250 // Add GOPROXY after calling the original setup 251 // so that it overrides any GOPROXY set there. 252 env.Vars = append(env.Vars, 253 "GOPROXY="+srv.URL, 254 "GONOSUMDB=*", 255 ) 256 return nil 257 }) 258 } 259 260 if len(tr.env) > 0 { 261 addSetup(func(env *testscript.Env) error { 262 for _, v := range tr.env { 263 varName := v 264 if i := strings.Index(v, "="); i >= 0 { 265 varName = v[:i] 266 } else { 267 v = fmt.Sprintf("%s=%s", v, os.Getenv(v)) 268 } 269 switch varName { 270 case "": 271 return fmt.Errorf("invalid variable name %q", varName) 272 case "WORK": 273 return fmt.Errorf("cannot override WORK variable") 274 } 275 env.Vars = append(env.Vars, v) 276 } 277 return nil 278 }) 279 } 280 281 r := &runT{ 282 verbose: tr.verbose, 283 } 284 285 func() { 286 defer func() { 287 switch recover() { 288 case nil, skipRun: 289 case failedRun: 290 err = failedRun 291 default: 292 panic(fmt.Errorf("unexpected panic: %v [%T]", err, err)) 293 } 294 }() 295 testscript.RunT(r, p) 296 297 // When continueOnError is true, FailNow does not call panic(failedRun). 298 // We still want err to be set, as the script resulted in a failure. 299 if r.Failed() { 300 err = failedRun 301 } 302 }() 303 304 if err != nil { 305 return fmt.Errorf("error running %v in %v\n", renderFilename(filename), runDir) 306 } 307 308 if tr.update && filename != "-" { 309 // Parse the (potentially) updated scriptFile as an archive, then merge 310 // with the original archive, retaining order. Then write the archive 311 // back to the source file 312 source, err := os.ReadFile(scriptFile) 313 if err != nil { 314 return fmt.Errorf("failed to read from script file %v for -update: %v", scriptFile, err) 315 } 316 updatedAr := txtar.Parse(source) 317 updatedFiles := make(map[string]txtar.File) 318 for _, f := range updatedAr.Files { 319 updatedFiles[f.Name] = f 320 } 321 for i, f := range ar.Files { 322 if newF, ok := updatedFiles[f.Name]; ok { 323 ar.Files[i] = newF 324 } 325 } 326 if err := os.WriteFile(filename, txtar.Format(ar), 0o666); err != nil { 327 return fmt.Errorf("failed to write script back to %v for -update: %v", renderFilename(filename), err) 328 } 329 } 330 331 return nil 332 } 333 334 var ( 335 failedRun = errors.New("failed run") 336 skipRun = errors.New("skip") 337 ) 338 339 // renderFilename renders filename in error messages, taking into account 340 // the filename could be the special "-" (stdin) 341 func renderFilename(filename string) string { 342 if filename == "-" { 343 return "<stdin>" 344 } 345 return filename 346 } 347 348 // runT implements testscript.T and is used in the call to testscript.Run 349 type runT struct { 350 verbose bool 351 failed int32 352 } 353 354 func (r *runT) Skip(is ...interface{}) { 355 panic(skipRun) 356 } 357 358 func (r *runT) Fatal(is ...interface{}) { 359 r.Log(is...) 360 r.FailNow() 361 } 362 363 func (r *runT) Parallel() { 364 // No-op for now; we are currently only running a single script in a 365 // testscript instance. 366 } 367 368 func (r *runT) Log(is ...interface{}) { 369 fmt.Print(is...) 370 } 371 372 func (r *runT) FailNow() { 373 atomic.StoreInt32(&r.failed, 1) 374 panic(failedRun) 375 } 376 377 func (r *runT) Failed() bool { 378 return atomic.LoadInt32(&r.failed) != 0 379 } 380 381 func (r *runT) Run(n string, f func(t testscript.T)) { 382 // For now we we don't top/tail the run of a subtest. We are currently only 383 // running a single script in a testscript instance, which means that we 384 // will only have a single subtest. 385 f(r) 386 } 387 388 func (r *runT) Verbose() bool { 389 return r.verbose 390 }