...

Source file src/github.com/rogpeppe/go-internal/cmd/testscript/main.go

Documentation: github.com/rogpeppe/go-internal/cmd/testscript

     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/ioutil"
    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 := ioutil.TempDir("", "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 := ioutil.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 := ioutil.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 := ioutil.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 := ioutil.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  }
   391  

View as plain text