Source file src/cmd/go/script_test.go

     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  // Script-driven tests.
     6  // See testdata/script/README for an overview.
     7  
     8  //go:generate go test cmd/go -v -run=TestScript/README --fixreadme
     9  
    10  package main_test
    11  
    12  import (
    13  	"bufio"
    14  	"bytes"
    15  	"context"
    16  	"flag"
    17  	"internal/testenv"
    18  	"internal/txtar"
    19  	"net/url"
    20  	"os"
    21  	"path/filepath"
    22  	"runtime"
    23  	"strings"
    24  	"testing"
    25  	"time"
    26  
    27  	"cmd/go/internal/cfg"
    28  	"cmd/go/internal/gover"
    29  	"cmd/go/internal/script"
    30  	"cmd/go/internal/script/scripttest"
    31  	"cmd/go/internal/vcweb/vcstest"
    32  )
    33  
    34  var testSum = flag.String("testsum", "", `may be tidy, listm, or listall. If set, TestScript generates a go.sum file at the beginning of each test and updates test files if they pass.`)
    35  
    36  // TestScript runs the tests in testdata/script/*.txt.
    37  func TestScript(t *testing.T) {
    38  	testenv.MustHaveGoBuild(t)
    39  	testenv.SkipIfShortAndSlow(t)
    40  
    41  	srv, err := vcstest.NewServer()
    42  	if err != nil {
    43  		t.Fatal(err)
    44  	}
    45  	t.Cleanup(func() {
    46  		if err := srv.Close(); err != nil {
    47  			t.Fatal(err)
    48  		}
    49  	})
    50  	certFile, err := srv.WriteCertificateFile()
    51  	if err != nil {
    52  		t.Fatal(err)
    53  	}
    54  
    55  	StartProxy()
    56  
    57  	var (
    58  		ctx         = context.Background()
    59  		gracePeriod = 100 * time.Millisecond
    60  	)
    61  	if deadline, ok := t.Deadline(); ok {
    62  		timeout := time.Until(deadline)
    63  
    64  		// If time allows, increase the termination grace period to 5% of the
    65  		// remaining time.
    66  		if gp := timeout / 20; gp > gracePeriod {
    67  			gracePeriod = gp
    68  		}
    69  
    70  		// When we run commands that execute subprocesses, we want to reserve two
    71  		// grace periods to clean up. We will send the first termination signal when
    72  		// the context expires, then wait one grace period for the process to
    73  		// produce whatever useful output it can (such as a stack trace). After the
    74  		// first grace period expires, we'll escalate to os.Kill, leaving the second
    75  		// grace period for the test function to record its output before the test
    76  		// process itself terminates.
    77  		timeout -= 2 * gracePeriod
    78  
    79  		var cancel context.CancelFunc
    80  		ctx, cancel = context.WithTimeout(ctx, timeout)
    81  		t.Cleanup(cancel)
    82  	}
    83  
    84  	env, err := scriptEnv(srv, certFile)
    85  	if err != nil {
    86  		t.Fatal(err)
    87  	}
    88  	engine := &script.Engine{
    89  		Conds: scriptConditions(),
    90  		Cmds:  scriptCommands(quitSignal(), gracePeriod),
    91  		Quiet: !testing.Verbose(),
    92  	}
    93  
    94  	t.Run("README", func(t *testing.T) {
    95  		checkScriptReadme(t, engine, env)
    96  	})
    97  
    98  	files, err := filepath.Glob("testdata/script/*.txt")
    99  	if err != nil {
   100  		t.Fatal(err)
   101  	}
   102  	for _, file := range files {
   103  		file := file
   104  		name := strings.TrimSuffix(filepath.Base(file), ".txt")
   105  		t.Run(name, func(t *testing.T) {
   106  			t.Parallel()
   107  			StartProxy()
   108  
   109  			workdir, err := os.MkdirTemp(testTmpDir, name)
   110  			if err != nil {
   111  				t.Fatal(err)
   112  			}
   113  			if !*testWork {
   114  				defer removeAll(workdir)
   115  			}
   116  
   117  			s, err := script.NewState(tbContext(ctx, t), workdir, env)
   118  			if err != nil {
   119  				t.Fatal(err)
   120  			}
   121  
   122  			// Unpack archive.
   123  			a, err := txtar.ParseFile(file)
   124  			if err != nil {
   125  				t.Fatal(err)
   126  			}
   127  			initScriptDirs(t, s)
   128  			if err := s.ExtractFiles(a); err != nil {
   129  				t.Fatal(err)
   130  			}
   131  
   132  			t.Log(time.Now().UTC().Format(time.RFC3339))
   133  			work, _ := s.LookupEnv("WORK")
   134  			t.Logf("$WORK=%s", work)
   135  
   136  			// With -testsum, if a go.mod file is present in the test's initial
   137  			// working directory, run 'go mod tidy'.
   138  			if *testSum != "" {
   139  				if updateSum(t, engine, s, a) {
   140  					defer func() {
   141  						if t.Failed() {
   142  							return
   143  						}
   144  						data := txtar.Format(a)
   145  						if err := os.WriteFile(file, data, 0666); err != nil {
   146  							t.Errorf("rewriting test file: %v", err)
   147  						}
   148  					}()
   149  				}
   150  			}
   151  
   152  			// Note: Do not use filepath.Base(file) here:
   153  			// editors that can jump to file:line references in the output
   154  			// will work better seeing the full path relative to cmd/go
   155  			// (where the "go test" command is usually run).
   156  			scripttest.Run(t, engine, s, file, bytes.NewReader(a.Comment))
   157  		})
   158  	}
   159  }
   160  
   161  // testingTBKey is the Context key for a testing.TB.
   162  type testingTBKey struct{}
   163  
   164  // tbContext returns a Context derived from ctx and associated with t.
   165  func tbContext(ctx context.Context, t testing.TB) context.Context {
   166  	return context.WithValue(ctx, testingTBKey{}, t)
   167  }
   168  
   169  // tbFromContext returns the testing.TB associated with ctx, if any.
   170  func tbFromContext(ctx context.Context) (testing.TB, bool) {
   171  	t := ctx.Value(testingTBKey{})
   172  	if t == nil {
   173  		return nil, false
   174  	}
   175  	return t.(testing.TB), true
   176  }
   177  
   178  // initScriptState creates the initial directory structure in s for unpacking a
   179  // cmd/go script.
   180  func initScriptDirs(t testing.TB, s *script.State) {
   181  	must := func(err error) {
   182  		if err != nil {
   183  			t.Helper()
   184  			t.Fatal(err)
   185  		}
   186  	}
   187  
   188  	work := s.Getwd()
   189  	must(s.Setenv("WORK", work))
   190  
   191  	must(os.MkdirAll(filepath.Join(work, "tmp"), 0777))
   192  	must(s.Setenv(tempEnvName(), filepath.Join(work, "tmp")))
   193  
   194  	gopath := filepath.Join(work, "gopath")
   195  	must(s.Setenv("GOPATH", gopath))
   196  	gopathSrc := filepath.Join(gopath, "src")
   197  	must(os.MkdirAll(gopathSrc, 0777))
   198  	must(s.Chdir(gopathSrc))
   199  }
   200  
   201  func scriptEnv(srv *vcstest.Server, srvCertFile string) ([]string, error) {
   202  	httpURL, err := url.Parse(srv.HTTP.URL)
   203  	if err != nil {
   204  		return nil, err
   205  	}
   206  	httpsURL, err := url.Parse(srv.HTTPS.URL)
   207  	if err != nil {
   208  		return nil, err
   209  	}
   210  	env := []string{
   211  		pathEnvName() + "=" + testBin + string(filepath.ListSeparator) + os.Getenv(pathEnvName()),
   212  		homeEnvName() + "=/no-home",
   213  		"CCACHE_DISABLE=1", // ccache breaks with non-existent HOME
   214  		"GOARCH=" + runtime.GOARCH,
   215  		"TESTGO_GOHOSTARCH=" + goHostArch,
   216  		"GOCACHE=" + testGOCACHE,
   217  		"GOCOVERDIR=" + os.Getenv("GOCOVERDIR"),
   218  		"GODEBUG=" + os.Getenv("GODEBUG"),
   219  		"GOEXE=" + cfg.ExeSuffix,
   220  		"GOEXPERIMENT=" + os.Getenv("GOEXPERIMENT"),
   221  		"GOOS=" + runtime.GOOS,
   222  		"TESTGO_GOHOSTOS=" + goHostOS,
   223  		"GOPROXY=" + proxyURL,
   224  		"GOPRIVATE=",
   225  		"GOROOT=" + testGOROOT,
   226  		"GOROOT_FINAL=" + testGOROOT_FINAL, // causes spurious rebuilds and breaks the "stale" built-in if not propagated
   227  		"GOTRACEBACK=system",
   228  		"TESTGONETWORK=panic", // allow only local connections by default; the [net] condition resets this
   229  		"TESTGO_GOROOT=" + testGOROOT,
   230  		"TESTGO_EXE=" + testGo,
   231  		"TESTGO_VCSTEST_HOST=" + httpURL.Host,
   232  		"TESTGO_VCSTEST_TLS_HOST=" + httpsURL.Host,
   233  		"TESTGO_VCSTEST_CERT=" + srvCertFile,
   234  		"TESTGONETWORK=panic", // cleared by the [net] condition
   235  		"GOSUMDB=" + testSumDBVerifierKey,
   236  		"GONOPROXY=",
   237  		"GONOSUMDB=",
   238  		"GOVCS=*:all",
   239  		"devnull=" + os.DevNull,
   240  		"goversion=" + gover.Local(),
   241  		"CMDGO_TEST_RUN_MAIN=true",
   242  		"HGRCPATH=",
   243  		"GOTOOLCHAIN=auto",
   244  		"newline=\n",
   245  	}
   246  
   247  	if testenv.Builder() != "" || os.Getenv("GIT_TRACE_CURL") == "1" {
   248  		// To help diagnose https://go.dev/issue/52545,
   249  		// enable tracing for Git HTTPS requests.
   250  		env = append(env,
   251  			"GIT_TRACE_CURL=1",
   252  			"GIT_TRACE_CURL_NO_DATA=1",
   253  			"GIT_REDACT_COOKIES=o,SSO,GSSO_Uberproxy")
   254  	}
   255  	if testing.Short() {
   256  		// VCS commands are always somewhat slow: they either require access to external hosts,
   257  		// or they require our intercepted vcs-test.golang.org to regenerate the repository.
   258  		// Require all tests that use VCS commands to be skipped in short mode.
   259  		env = append(env, "TESTGOVCS=panic")
   260  	}
   261  
   262  	if os.Getenv("CGO_ENABLED") != "" || runtime.GOOS != goHostOS || runtime.GOARCH != goHostArch {
   263  		// If the actual CGO_ENABLED might not match the cmd/go default, set it
   264  		// explicitly in the environment. Otherwise, leave it unset so that we also
   265  		// cover the default behaviors.
   266  		env = append(env, "CGO_ENABLED="+cgoEnabled)
   267  	}
   268  
   269  	for _, key := range extraEnvKeys {
   270  		if val, ok := os.LookupEnv(key); ok {
   271  			env = append(env, key+"="+val)
   272  		}
   273  	}
   274  
   275  	return env, nil
   276  }
   277  
   278  var extraEnvKeys = []string{
   279  	"SYSTEMROOT",         // must be preserved on Windows to find DLLs; golang.org/issue/25210
   280  	"WINDIR",             // must be preserved on Windows to be able to run PowerShell command; golang.org/issue/30711
   281  	"LD_LIBRARY_PATH",    // must be preserved on Unix systems to find shared libraries
   282  	"LIBRARY_PATH",       // allow override of non-standard static library paths
   283  	"C_INCLUDE_PATH",     // allow override non-standard include paths
   284  	"CC",                 // don't lose user settings when invoking cgo
   285  	"GO_TESTING_GOTOOLS", // for gccgo testing
   286  	"GCCGO",              // for gccgo testing
   287  	"GCCGOTOOLDIR",       // for gccgo testing
   288  }
   289  
   290  // updateSum runs 'go mod tidy', 'go list -mod=mod -m all', or
   291  // 'go list -mod=mod all' in the test's current directory if a file named
   292  // "go.mod" is present after the archive has been extracted. updateSum modifies
   293  // archive and returns true if go.mod or go.sum were changed.
   294  func updateSum(t testing.TB, e *script.Engine, s *script.State, archive *txtar.Archive) (rewrite bool) {
   295  	gomodIdx, gosumIdx := -1, -1
   296  	for i := range archive.Files {
   297  		switch archive.Files[i].Name {
   298  		case "go.mod":
   299  			gomodIdx = i
   300  		case "go.sum":
   301  			gosumIdx = i
   302  		}
   303  	}
   304  	if gomodIdx < 0 {
   305  		return false
   306  	}
   307  
   308  	var cmd string
   309  	switch *testSum {
   310  	case "tidy":
   311  		cmd = "go mod tidy"
   312  	case "listm":
   313  		cmd = "go list -m -mod=mod all"
   314  	case "listall":
   315  		cmd = "go list -mod=mod all"
   316  	default:
   317  		t.Fatalf(`unknown value for -testsum %q; may be "tidy", "listm", or "listall"`, *testSum)
   318  	}
   319  
   320  	log := new(strings.Builder)
   321  	err := e.Execute(s, "updateSum", bufio.NewReader(strings.NewReader(cmd)), log)
   322  	if log.Len() > 0 {
   323  		t.Logf("%s", log)
   324  	}
   325  	if err != nil {
   326  		t.Fatal(err)
   327  	}
   328  
   329  	newGomodData, err := os.ReadFile(s.Path("go.mod"))
   330  	if err != nil {
   331  		t.Fatalf("reading go.mod after -testsum: %v", err)
   332  	}
   333  	if !bytes.Equal(newGomodData, archive.Files[gomodIdx].Data) {
   334  		archive.Files[gomodIdx].Data = newGomodData
   335  		rewrite = true
   336  	}
   337  
   338  	newGosumData, err := os.ReadFile(s.Path("go.sum"))
   339  	if err != nil && !os.IsNotExist(err) {
   340  		t.Fatalf("reading go.sum after -testsum: %v", err)
   341  	}
   342  	switch {
   343  	case os.IsNotExist(err) && gosumIdx >= 0:
   344  		// go.sum was deleted.
   345  		rewrite = true
   346  		archive.Files = append(archive.Files[:gosumIdx], archive.Files[gosumIdx+1:]...)
   347  	case err == nil && gosumIdx < 0:
   348  		// go.sum was created.
   349  		rewrite = true
   350  		gosumIdx = gomodIdx + 1
   351  		archive.Files = append(archive.Files, txtar.File{})
   352  		copy(archive.Files[gosumIdx+1:], archive.Files[gosumIdx:])
   353  		archive.Files[gosumIdx] = txtar.File{Name: "go.sum", Data: newGosumData}
   354  	case err == nil && gosumIdx >= 0 && !bytes.Equal(newGosumData, archive.Files[gosumIdx].Data):
   355  		// go.sum was changed.
   356  		rewrite = true
   357  		archive.Files[gosumIdx].Data = newGosumData
   358  	}
   359  	return rewrite
   360  }
   361  

View as plain text