blob: cc8cc9c6f6d91085e90b83c1b311e06c6a1015c6 [file] [log] [blame]
// 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
}
}