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