| // Copyright 2020 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. |
| |
| package gocommand_test |
| |
| import ( |
| "context" |
| "fmt" |
| "io/fs" |
| "os" |
| "os/exec" |
| "path/filepath" |
| "strings" |
| "testing" |
| "time" |
| |
| "golang.org/x/sync/errgroup" |
| "golang.org/x/tools/internal/gocommand" |
| "golang.org/x/tools/internal/testenv" |
| ) |
| |
| func TestGoVersion(t *testing.T) { |
| testenv.NeedsTool(t, "go") |
| |
| inv := gocommand.Invocation{ |
| Verb: "version", |
| } |
| gocmdRunner := &gocommand.Runner{} |
| if _, err := gocmdRunner.Run(context.Background(), inv); err != nil { |
| t.Error(err) |
| } |
| } |
| |
| // This is not a test of go/packages at all: it's a test of whether it |
| // is possible to delete the directory used by go list once it has |
| // finished. It is intended to evaluate the hypothesis (to explain |
| // issue #71544) that the go command, on Windows, occasionally fails |
| // to release all its handles to the temporary directory even when it |
| // should have finished. |
| // |
| // If this test ever fails, the combination of the gocommand package |
| // and the go command itself has a bug; this has been observed (#73503). |
| func TestRmdirAfterGoList_Runner(t *testing.T) { |
| t.Skip("flaky; see https://github.com/golang/go/issues/73736#issuecomment-2885407104") |
| |
| testRmdirAfterGoList(t, func(ctx context.Context, dir string) { |
| var runner gocommand.Runner |
| stdout, stderr, friendlyErr, err := runner.RunRaw(ctx, gocommand.Invocation{ |
| Verb: "list", |
| Args: []string{"-json", "example.com/p"}, |
| WorkingDir: dir, |
| }) |
| if ctx.Err() != nil { |
| return // don't report error if canceled |
| } |
| if err != nil || friendlyErr != nil { |
| t.Fatalf("go list failed: %v, %v (stdout=%s stderr=%s)", |
| err, friendlyErr, stdout, stderr) |
| } |
| }) |
| } |
| |
| // TestRmdirAfterGoList_Direct is a variant of |
| // TestRmdirAfterGoList_Runner that executes go list directly, to |
| // control for the substantial logic of the gocommand package. |
| // |
| // It has two variants: the first does not set WaitDelay; the second |
| // sets it to 30s. If the first variant ever fails, the go command |
| // itself has a bug; as of May 2025 this has never been observed. |
| // |
| // If the second variant fails, it indicates that the WaitDelay |
| // mechanism is responsible for causing Wait to return before the |
| // child process has naturally finished. This is to confirm the |
| // hypothesis at https://github.com/golang/go/issues/73736#issuecomment-2885407104. |
| func TestRmdirAfterGoList_Direct(t *testing.T) { |
| for _, delay := range []time.Duration{0, 30 * time.Second} { |
| t.Run(delay.String(), func(t *testing.T) { |
| testRmdirAfterGoList(t, func(ctx context.Context, dir string) { |
| cmd := exec.Command("go", "list", "-json", "example.com/p") |
| cmd.Dir = dir |
| cmd.Stdout = new(strings.Builder) |
| cmd.Stderr = new(strings.Builder) |
| cmd.WaitDelay = delay |
| err := cmd.Run() |
| if ctx.Err() != nil { |
| return // don't report error if canceled |
| } |
| if err != nil { |
| t.Fatalf("go list failed: %v (stdout=%s stderr=%s)", |
| err, cmd.Stdout, cmd.Stderr) |
| } |
| }) |
| }) |
| } |
| } |
| |
| func testRmdirAfterGoList(t *testing.T, f func(ctx context.Context, dir string)) { |
| testenv.NeedsExec(t) |
| |
| dir := t.TempDir() |
| if err := os.Mkdir(filepath.Join(dir, "p"), 0777); err != nil { |
| t.Fatalf("mkdir p: %v", err) |
| } |
| |
| // Create a go.mod file and 100 trivial Go files for the go command to read. |
| if err := os.WriteFile(filepath.Join(dir, "go.mod"), []byte("module example.com"), 0666); err != nil { |
| t.Fatal(err) |
| } |
| for i := range 100 { |
| filename := filepath.Join(dir, fmt.Sprintf("p/%d.go", i)) |
| if err := os.WriteFile(filename, []byte("package p"), 0666); err != nil { |
| t.Fatal(err) |
| } |
| } |
| |
| t0 := time.Now() |
| g, ctx := errgroup.WithContext(context.Background()) |
| for range 10 { |
| g.Go(func() error { |
| f(ctx, dir) |
| // Return an error so that concurrent invocations are canceled. |
| return fmt.Errorf("oops") |
| }) |
| } |
| g.Wait() // ignore error (expected) |
| |
| t.Logf("10 concurrent executions (9 canceled) took %v", time.Since(t0)) |
| |
| // This is the critical operation. |
| if err := os.RemoveAll(dir); err != nil { |
| t.Errorf("failed to remove temp dir: %v", err) |
| // List the contents of the directory, for clues. |
| filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error { |
| t.Log(path, d, err) |
| return nil |
| }) // ignore error |
| } |
| } |