Skip to content

Commit b978661

Browse files
Bryan C. Millsgopherbot
Bryan C. Mills
authored andcommitted
cmd/godoc: streamline test subprocesses
- Use testenv.Command to obtain Cmd instances that terminate (with useful goroutine dumps!) before the test's timeout, and remove arbitrary hard-coded timeouts. - Execute the test binary itself as cmd/godoc instead of invoking (and cleaning up after) 'go build'. - Use context cancellation to reduce the number of ad-hoc goroutines and channels needed by the tests and to provide stronger invariants on process cleanup. For golang/go#50014 Change-Id: I19ae4d10da691db233c79734799ae074ffdf6a03 Reviewed-on: https://go-review.googlesource.com/c/tools/+/377836 Run-TryBot: Bryan Mills <bcmills@google.com> Reviewed-by: Joedian Reid <joedian@golang.org> Reviewed-by: Dmitri Shuralyov <dmitshur@golang.org> TryBot-Result: Gopher Robot <gobot@golang.org> gopls-CI: kokoro <noreply+kokoro@google.com> Auto-Submit: Bryan Mills <bcmills@google.com>
1 parent 932ec22 commit b978661

File tree

1 file changed

+87
-102
lines changed

1 file changed

+87
-102
lines changed

cmd/godoc/godoc_test.go

+87-102
Original file line numberDiff line numberDiff line change
@@ -2,64 +2,62 @@
22
// Use of this source code is governed by a BSD-style
33
// license that can be found in the LICENSE file.
44

5-
package main_test
5+
package main
66

77
import (
88
"bytes"
9+
"context"
910
"fmt"
1011
"go/build"
1112
"io/ioutil"
1213
"net"
1314
"net/http"
1415
"os"
1516
"os/exec"
16-
"path/filepath"
1717
"regexp"
1818
"runtime"
1919
"strings"
20+
"sync"
2021
"testing"
2122
"time"
2223

2324
"golang.org/x/tools/go/packages/packagestest"
2425
"golang.org/x/tools/internal/testenv"
2526
)
2627

27-
// buildGodoc builds the godoc executable.
28-
// It returns its path, and a cleanup function.
29-
//
30-
// TODO(adonovan): opt: do this at most once, and do the cleanup
31-
// exactly once. How though? There's no atexit.
32-
func buildGodoc(t *testing.T) (bin string, cleanup func()) {
33-
t.Helper()
34-
35-
if runtime.GOARCH == "arm" {
36-
t.Skip("skipping test on arm platforms; too slow")
37-
}
38-
if runtime.GOOS == "android" {
39-
t.Skipf("the dependencies are not available on android")
28+
func TestMain(m *testing.M) {
29+
if os.Getenv("GODOC_TEST_IS_GODOC") != "" {
30+
main()
31+
os.Exit(0)
4032
}
41-
testenv.NeedsTool(t, "go")
4233

43-
tmp, err := ioutil.TempDir("", "godoc-regtest-")
44-
if err != nil {
45-
t.Fatal(err)
46-
}
47-
defer func() {
48-
if cleanup == nil { // probably, go build failed.
49-
os.RemoveAll(tmp)
50-
}
51-
}()
34+
// Inform subprocesses that they should run the cmd/godoc main instead of
35+
// running tests. It's a close approximation to building and running the real
36+
// command, and much less complicated and expensive to build and clean up.
37+
os.Setenv("GODOC_TEST_IS_GODOC", "1")
5238

53-
bin = filepath.Join(tmp, "godoc")
54-
if runtime.GOOS == "windows" {
55-
bin += ".exe"
56-
}
57-
cmd := exec.Command("go", "build", "-o", bin)
58-
if err := cmd.Run(); err != nil {
59-
t.Fatalf("Building godoc: %v", err)
39+
os.Exit(m.Run())
40+
}
41+
42+
var exe struct {
43+
path string
44+
err error
45+
once sync.Once
46+
}
47+
48+
func godocPath(t *testing.T) string {
49+
switch runtime.GOOS {
50+
case "js", "ios":
51+
t.Skipf("skipping test that requires exec")
6052
}
6153

62-
return bin, func() { os.RemoveAll(tmp) }
54+
exe.once.Do(func() {
55+
exe.path, exe.err = os.Executable()
56+
})
57+
if exe.err != nil {
58+
t.Fatal(exe.err)
59+
}
60+
return exe.path
6361
}
6462

6563
func serverAddress(t *testing.T) string {
@@ -74,60 +72,42 @@ func serverAddress(t *testing.T) string {
7472
return ln.Addr().String()
7573
}
7674

77-
func waitForServerReady(t *testing.T, cmd *exec.Cmd, addr string) {
78-
ch := make(chan error, 1)
79-
go func() { ch <- fmt.Errorf("server exited early: %v", cmd.Wait()) }()
80-
go waitForServer(t, ch,
75+
func waitForServerReady(t *testing.T, ctx context.Context, cmd *exec.Cmd, addr string) {
76+
waitForServer(t, ctx,
8177
fmt.Sprintf("http://%v/", addr),
8278
"Go Documentation Server",
83-
15*time.Second,
8479
false)
85-
if err := <-ch; err != nil {
86-
t.Skipf("skipping due to https://go.dev/issue/50014: %v", err)
87-
}
8880
}
8981

90-
func waitForSearchReady(t *testing.T, cmd *exec.Cmd, addr string) {
91-
ch := make(chan error, 1)
92-
go func() { ch <- fmt.Errorf("server exited early: %v", cmd.Wait()) }()
93-
go waitForServer(t, ch,
82+
func waitForSearchReady(t *testing.T, ctx context.Context, cmd *exec.Cmd, addr string) {
83+
waitForServer(t, ctx,
9484
fmt.Sprintf("http://%v/search?q=FALLTHROUGH", addr),
9585
"The list of tokens.",
96-
2*time.Minute,
9786
false)
98-
if err := <-ch; err != nil {
99-
t.Skipf("skipping due to https://go.dev/issue/50014: %v", err)
100-
}
10187
}
10288

103-
func waitUntilScanComplete(t *testing.T, addr string) {
104-
ch := make(chan error)
105-
go waitForServer(t, ch,
89+
func waitUntilScanComplete(t *testing.T, ctx context.Context, addr string) {
90+
waitForServer(t, ctx,
10691
fmt.Sprintf("http://%v/pkg", addr),
10792
"Scan is not yet complete",
108-
2*time.Minute,
10993
// setting reverse as true, which means this waits
11094
// until the string is not returned in the response anymore
111-
true,
112-
)
113-
if err := <-ch; err != nil {
114-
t.Skipf("skipping due to https://go.dev/issue/50014: %v", err)
115-
}
95+
true)
11696
}
11797

118-
const pollInterval = 200 * time.Millisecond
98+
const pollInterval = 50 * time.Millisecond
11999

120-
// waitForServer waits for server to meet the required condition.
121-
// It sends a single error value to ch, unless the test has failed.
122-
// The error value is nil if the required condition was met within
123-
// timeout, or non-nil otherwise.
124-
func waitForServer(t *testing.T, ch chan<- error, url, match string, timeout time.Duration, reverse bool) {
125-
deadline := time.Now().Add(timeout)
126-
for time.Now().Before(deadline) {
127-
time.Sleep(pollInterval)
128-
if t.Failed() {
129-
return
100+
// waitForServer waits for server to meet the required condition,
101+
// failing the test if ctx is canceled before that occurs.
102+
func waitForServer(t *testing.T, ctx context.Context, url, match string, reverse bool) {
103+
start := time.Now()
104+
for {
105+
if ctx.Err() != nil {
106+
t.Helper()
107+
t.Fatalf("server failed to respond in %v", time.Since(start))
130108
}
109+
110+
time.Sleep(pollInterval)
131111
res, err := http.Get(url)
132112
if err != nil {
133113
continue
@@ -140,11 +120,9 @@ func waitForServer(t *testing.T, ch chan<- error, url, match string, timeout tim
140120
switch {
141121
case !reverse && bytes.Contains(body, []byte(match)),
142122
reverse && !bytes.Contains(body, []byte(match)):
143-
ch <- nil
144123
return
145124
}
146125
}
147-
ch <- fmt.Errorf("server failed to respond in %v", timeout)
148126
}
149127

150128
// hasTag checks whether a given release tag is contained in the current version
@@ -158,24 +136,18 @@ func hasTag(t string) bool {
158136
return false
159137
}
160138

161-
func killAndWait(cmd *exec.Cmd) {
162-
cmd.Process.Kill()
163-
cmd.Process.Wait()
164-
}
165-
166139
func TestURL(t *testing.T) {
167140
if runtime.GOOS == "plan9" {
168141
t.Skip("skipping on plan9; fails to start up quickly enough")
169142
}
170-
bin, cleanup := buildGodoc(t)
171-
defer cleanup()
143+
bin := godocPath(t)
172144

173145
testcase := func(url string, contents string) func(t *testing.T) {
174146
return func(t *testing.T) {
175147
stdout, stderr := new(bytes.Buffer), new(bytes.Buffer)
176148

177149
args := []string{fmt.Sprintf("-url=%s", url)}
178-
cmd := exec.Command(bin, args...)
150+
cmd := testenv.Command(t, bin, args...)
179151
cmd.Stdout = stdout
180152
cmd.Stderr = stderr
181153
cmd.Args[0] = "godoc"
@@ -205,8 +177,8 @@ func TestURL(t *testing.T) {
205177

206178
// Basic integration test for godoc HTTP interface.
207179
func TestWeb(t *testing.T) {
208-
bin, cleanup := buildGodoc(t)
209-
defer cleanup()
180+
bin := godocPath(t)
181+
210182
for _, x := range packagestest.All {
211183
t.Run(x.Name(), func(t *testing.T) {
212184
testWeb(t, x, bin, false)
@@ -217,17 +189,19 @@ func TestWeb(t *testing.T) {
217189
// Basic integration test for godoc HTTP interface.
218190
func TestWebIndex(t *testing.T) {
219191
if testing.Short() {
220-
t.Skip("skipping test in -short mode")
192+
t.Skip("skipping slow test in -short mode")
221193
}
222-
bin, cleanup := buildGodoc(t)
223-
defer cleanup()
194+
bin := godocPath(t)
224195
testWeb(t, packagestest.GOPATH, bin, true)
225196
}
226197

227198
// Basic integration test for godoc HTTP interface.
228199
func testWeb(t *testing.T, x packagestest.Exporter, bin string, withIndex bool) {
229-
if runtime.GOOS == "plan9" {
230-
t.Skip("skipping on plan9; fails to start up quickly enough")
200+
switch runtime.GOOS {
201+
case "plan9":
202+
t.Skip("skipping on plan9: fails to start up quickly enough")
203+
case "android", "ios":
204+
t.Skip("skipping on mobile: lacks GOROOT/api in test environment")
231205
}
232206

233207
// Write a fake GOROOT/GOPATH with some third party packages.
@@ -256,23 +230,39 @@ package a; import _ "godoc.test/repo2/a"; const Name = "repo1a"`,
256230
if withIndex {
257231
args = append(args, "-index", "-index_interval=-1s")
258232
}
259-
cmd := exec.Command(bin, args...)
233+
cmd := testenv.Command(t, bin, args...)
260234
cmd.Dir = e.Config.Dir
261235
cmd.Env = e.Config.Env
262-
cmd.Stdout = os.Stderr
263-
cmd.Stderr = os.Stderr
236+
cmdOut := new(strings.Builder)
237+
cmd.Stdout = cmdOut
238+
cmd.Stderr = cmdOut
264239
cmd.Args[0] = "godoc"
265240

266241
if err := cmd.Start(); err != nil {
267242
t.Fatalf("failed to start godoc: %s", err)
268243
}
269-
defer killAndWait(cmd)
244+
ctx, cancel := context.WithCancel(context.Background())
245+
go func() {
246+
err := cmd.Wait()
247+
t.Logf("%v: %v", cmd, err)
248+
cancel()
249+
}()
250+
defer func() {
251+
// Shut down the server cleanly if possible.
252+
if runtime.GOOS == "windows" {
253+
cmd.Process.Kill() // Windows doesn't support os.Interrupt.
254+
} else {
255+
cmd.Process.Signal(os.Interrupt)
256+
}
257+
<-ctx.Done()
258+
t.Logf("server output:\n%s", cmdOut)
259+
}()
270260

271261
if withIndex {
272-
waitForSearchReady(t, cmd, addr)
262+
waitForSearchReady(t, ctx, cmd, addr)
273263
} else {
274-
waitForServerReady(t, cmd, addr)
275-
waitUntilScanComplete(t, addr)
264+
waitForServerReady(t, ctx, cmd, addr)
265+
waitUntilScanComplete(t, ctx, addr)
276266
}
277267

278268
tests := []struct {
@@ -454,22 +444,17 @@ func TestNoMainModule(t *testing.T) {
454444
if runtime.GOOS == "plan9" {
455445
t.Skip("skipping on plan9; for consistency with other tests that build godoc binary")
456446
}
457-
bin, cleanup := buildGodoc(t)
458-
defer cleanup()
459-
tempDir, err := ioutil.TempDir("", "godoc-test-")
460-
if err != nil {
461-
t.Fatal(err)
462-
}
463-
defer os.RemoveAll(tempDir)
447+
bin := godocPath(t)
448+
tempDir := t.TempDir()
464449

465450
// Run godoc in an empty directory with module mode explicitly on,
466451
// so that 'go env GOMOD' reports os.DevNull.
467-
cmd := exec.Command(bin, "-url=/")
452+
cmd := testenv.Command(t, bin, "-url=/")
468453
cmd.Dir = tempDir
469454
cmd.Env = append(os.Environ(), "GO111MODULE=on")
470455
var stderr bytes.Buffer
471456
cmd.Stderr = &stderr
472-
err = cmd.Run()
457+
err := cmd.Run()
473458
if err != nil {
474459
t.Fatalf("godoc command failed: %v\nstderr=%q", err, stderr.String())
475460
}

0 commit comments

Comments
 (0)