diff options
Diffstat (limited to 'src/cmd/go/script_test.go')
-rw-r--r-- | src/cmd/go/script_test.go | 360 |
1 files changed, 360 insertions, 0 deletions
diff --git a/src/cmd/go/script_test.go b/src/cmd/go/script_test.go new file mode 100644 index 0000000..624c5bf --- /dev/null +++ b/src/cmd/go/script_test.go @@ -0,0 +1,360 @@ +// Copyright 2018 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Script-driven tests. +// See testdata/script/README for an overview. + +//go:generate go test cmd/go -v -run=TestScript/README --fixreadme + +package main_test + +import ( + "bufio" + "bytes" + "context" + "flag" + "internal/testenv" + "internal/txtar" + "net/url" + "os" + "path/filepath" + "runtime" + "strings" + "testing" + "time" + + "cmd/go/internal/cfg" + "cmd/go/internal/gover" + "cmd/go/internal/script" + "cmd/go/internal/script/scripttest" + "cmd/go/internal/vcweb/vcstest" +) + +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.`) + +// TestScript runs the tests in testdata/script/*.txt. +func TestScript(t *testing.T) { + testenv.MustHaveGoBuild(t) + testenv.SkipIfShortAndSlow(t) + + srv, err := vcstest.NewServer() + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { + if err := srv.Close(); err != nil { + t.Fatal(err) + } + }) + certFile, err := srv.WriteCertificateFile() + if err != nil { + t.Fatal(err) + } + + StartProxy() + + var ( + ctx = context.Background() + gracePeriod = 100 * time.Millisecond + ) + if deadline, ok := t.Deadline(); ok { + timeout := time.Until(deadline) + + // If time allows, increase the termination grace period to 5% of the + // remaining time. + if gp := timeout / 20; gp > gracePeriod { + gracePeriod = gp + } + + // When we run commands that execute subprocesses, we want to reserve two + // grace periods to clean up. We will send the first termination signal when + // the context expires, then wait one grace period for the process to + // produce whatever useful output it can (such as a stack trace). After the + // first grace period expires, we'll escalate to os.Kill, leaving the second + // grace period for the test function to record its output before the test + // process itself terminates. + timeout -= 2 * gracePeriod + + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(ctx, timeout) + t.Cleanup(cancel) + } + + env, err := scriptEnv(srv, certFile) + if err != nil { + t.Fatal(err) + } + engine := &script.Engine{ + Conds: scriptConditions(), + Cmds: scriptCommands(quitSignal(), gracePeriod), + Quiet: !testing.Verbose(), + } + + t.Run("README", func(t *testing.T) { + checkScriptReadme(t, engine, env) + }) + + files, err := filepath.Glob("testdata/script/*.txt") + if err != nil { + t.Fatal(err) + } + for _, file := range files { + file := file + name := strings.TrimSuffix(filepath.Base(file), ".txt") + t.Run(name, func(t *testing.T) { + t.Parallel() + StartProxy() + + workdir, err := os.MkdirTemp(testTmpDir, name) + if err != nil { + t.Fatal(err) + } + if !*testWork { + defer removeAll(workdir) + } + + s, err := script.NewState(tbContext(ctx, t), workdir, env) + if err != nil { + t.Fatal(err) + } + + // Unpack archive. + a, err := txtar.ParseFile(file) + if err != nil { + t.Fatal(err) + } + initScriptDirs(t, s) + if err := s.ExtractFiles(a); err != nil { + t.Fatal(err) + } + + t.Log(time.Now().UTC().Format(time.RFC3339)) + work, _ := s.LookupEnv("WORK") + t.Logf("$WORK=%s", work) + + // With -testsum, if a go.mod file is present in the test's initial + // working directory, run 'go mod tidy'. + if *testSum != "" { + if updateSum(t, engine, s, a) { + defer func() { + if t.Failed() { + return + } + data := txtar.Format(a) + if err := os.WriteFile(file, data, 0666); err != nil { + t.Errorf("rewriting test file: %v", err) + } + }() + } + } + + // Note: Do not use filepath.Base(file) here: + // editors that can jump to file:line references in the output + // will work better seeing the full path relative to cmd/go + // (where the "go test" command is usually run). + scripttest.Run(t, engine, s, file, bytes.NewReader(a.Comment)) + }) + } +} + +// testingTBKey is the Context key for a testing.TB. +type testingTBKey struct{} + +// tbContext returns a Context derived from ctx and associated with t. +func tbContext(ctx context.Context, t testing.TB) context.Context { + return context.WithValue(ctx, testingTBKey{}, t) +} + +// tbFromContext returns the testing.TB associated with ctx, if any. +func tbFromContext(ctx context.Context) (testing.TB, bool) { + t := ctx.Value(testingTBKey{}) + if t == nil { + return nil, false + } + return t.(testing.TB), true +} + +// initScriptState creates the initial directory structure in s for unpacking a +// cmd/go script. +func initScriptDirs(t testing.TB, s *script.State) { + must := func(err error) { + if err != nil { + t.Helper() + t.Fatal(err) + } + } + + work := s.Getwd() + must(s.Setenv("WORK", work)) + + must(os.MkdirAll(filepath.Join(work, "tmp"), 0777)) + must(s.Setenv(tempEnvName(), filepath.Join(work, "tmp"))) + + gopath := filepath.Join(work, "gopath") + must(s.Setenv("GOPATH", gopath)) + gopathSrc := filepath.Join(gopath, "src") + must(os.MkdirAll(gopathSrc, 0777)) + must(s.Chdir(gopathSrc)) +} + +func scriptEnv(srv *vcstest.Server, srvCertFile string) ([]string, error) { + httpURL, err := url.Parse(srv.HTTP.URL) + if err != nil { + return nil, err + } + httpsURL, err := url.Parse(srv.HTTPS.URL) + if err != nil { + return nil, err + } + env := []string{ + pathEnvName() + "=" + testBin + string(filepath.ListSeparator) + os.Getenv(pathEnvName()), + homeEnvName() + "=/no-home", + "CCACHE_DISABLE=1", // ccache breaks with non-existent HOME + "GOARCH=" + runtime.GOARCH, + "TESTGO_GOHOSTARCH=" + goHostArch, + "GOCACHE=" + testGOCACHE, + "GOCOVERDIR=" + os.Getenv("GOCOVERDIR"), + "GODEBUG=" + os.Getenv("GODEBUG"), + "GOEXE=" + cfg.ExeSuffix, + "GOEXPERIMENT=" + os.Getenv("GOEXPERIMENT"), + "GOOS=" + runtime.GOOS, + "TESTGO_GOHOSTOS=" + goHostOS, + "GOPROXY=" + proxyURL, + "GOPRIVATE=", + "GOROOT=" + testGOROOT, + "GOROOT_FINAL=" + testGOROOT_FINAL, // causes spurious rebuilds and breaks the "stale" built-in if not propagated + "GOTRACEBACK=system", + "TESTGONETWORK=panic", // allow only local connections by default; the [net] condition resets this + "TESTGO_GOROOT=" + testGOROOT, + "TESTGO_EXE=" + testGo, + "TESTGO_VCSTEST_HOST=" + httpURL.Host, + "TESTGO_VCSTEST_TLS_HOST=" + httpsURL.Host, + "TESTGO_VCSTEST_CERT=" + srvCertFile, + "TESTGONETWORK=panic", // cleared by the [net] condition + "GOSUMDB=" + testSumDBVerifierKey, + "GONOPROXY=", + "GONOSUMDB=", + "GOVCS=*:all", + "devnull=" + os.DevNull, + "goversion=" + gover.Local(), + "CMDGO_TEST_RUN_MAIN=true", + "HGRCPATH=", + "GOTOOLCHAIN=auto", + "newline=\n", + } + + if testenv.Builder() != "" || os.Getenv("GIT_TRACE_CURL") == "1" { + // To help diagnose https://go.dev/issue/52545, + // enable tracing for Git HTTPS requests. + env = append(env, + "GIT_TRACE_CURL=1", + "GIT_TRACE_CURL_NO_DATA=1", + "GIT_REDACT_COOKIES=o,SSO,GSSO_Uberproxy") + } + if testing.Short() { + // VCS commands are always somewhat slow: they either require access to external hosts, + // or they require our intercepted vcs-test.golang.org to regenerate the repository. + // Require all tests that use VCS commands to be skipped in short mode. + env = append(env, "TESTGOVCS=panic") + } + + if os.Getenv("CGO_ENABLED") != "" || runtime.GOOS != goHostOS || runtime.GOARCH != goHostArch { + // If the actual CGO_ENABLED might not match the cmd/go default, set it + // explicitly in the environment. Otherwise, leave it unset so that we also + // cover the default behaviors. + env = append(env, "CGO_ENABLED="+cgoEnabled) + } + + for _, key := range extraEnvKeys { + if val, ok := os.LookupEnv(key); ok { + env = append(env, key+"="+val) + } + } + + return env, nil +} + +var extraEnvKeys = []string{ + "SYSTEMROOT", // must be preserved on Windows to find DLLs; golang.org/issue/25210 + "WINDIR", // must be preserved on Windows to be able to run PowerShell command; golang.org/issue/30711 + "LD_LIBRARY_PATH", // must be preserved on Unix systems to find shared libraries + "LIBRARY_PATH", // allow override of non-standard static library paths + "C_INCLUDE_PATH", // allow override non-standard include paths + "CC", // don't lose user settings when invoking cgo + "GO_TESTING_GOTOOLS", // for gccgo testing + "GCCGO", // for gccgo testing + "GCCGOTOOLDIR", // for gccgo testing +} + +// updateSum runs 'go mod tidy', 'go list -mod=mod -m all', or +// 'go list -mod=mod all' in the test's current directory if a file named +// "go.mod" is present after the archive has been extracted. updateSum modifies +// archive and returns true if go.mod or go.sum were changed. +func updateSum(t testing.TB, e *script.Engine, s *script.State, archive *txtar.Archive) (rewrite bool) { + gomodIdx, gosumIdx := -1, -1 + for i := range archive.Files { + switch archive.Files[i].Name { + case "go.mod": + gomodIdx = i + case "go.sum": + gosumIdx = i + } + } + if gomodIdx < 0 { + return false + } + + var cmd string + switch *testSum { + case "tidy": + cmd = "go mod tidy" + case "listm": + cmd = "go list -m -mod=mod all" + case "listall": + cmd = "go list -mod=mod all" + default: + t.Fatalf(`unknown value for -testsum %q; may be "tidy", "listm", or "listall"`, *testSum) + } + + log := new(strings.Builder) + err := e.Execute(s, "updateSum", bufio.NewReader(strings.NewReader(cmd)), log) + if log.Len() > 0 { + t.Logf("%s", log) + } + if err != nil { + t.Fatal(err) + } + + newGomodData, err := os.ReadFile(s.Path("go.mod")) + if err != nil { + t.Fatalf("reading go.mod after -testsum: %v", err) + } + if !bytes.Equal(newGomodData, archive.Files[gomodIdx].Data) { + archive.Files[gomodIdx].Data = newGomodData + rewrite = true + } + + newGosumData, err := os.ReadFile(s.Path("go.sum")) + if err != nil && !os.IsNotExist(err) { + t.Fatalf("reading go.sum after -testsum: %v", err) + } + switch { + case os.IsNotExist(err) && gosumIdx >= 0: + // go.sum was deleted. + rewrite = true + archive.Files = append(archive.Files[:gosumIdx], archive.Files[gosumIdx+1:]...) + case err == nil && gosumIdx < 0: + // go.sum was created. + rewrite = true + gosumIdx = gomodIdx + 1 + archive.Files = append(archive.Files, txtar.File{}) + copy(archive.Files[gosumIdx+1:], archive.Files[gosumIdx:]) + archive.Files[gosumIdx] = txtar.File{Name: "go.sum", Data: newGosumData} + case err == nil && gosumIdx >= 0 && !bytes.Equal(newGosumData, archive.Files[gosumIdx].Data): + // go.sum was changed. + rewrite = true + archive.Files[gosumIdx].Data = newGosumData + } + return rewrite +} |