e2e/ready_test.go
106 lines · 2564 bytes
1package e2e
2
3import (
4 "bytes"
5 "fmt"
6 "net"
7 "os"
8 "runtime"
9 "strings"
10 "sync"
11 "testing"
12 "time"
13)
14
15// startupWait bounds how long a gitbayd start may take before its
16// listeners answer. CI runs these tests in parallel on a four-core runner
17// beside other builds, where a start has taken longer than ten seconds;
18// the wait ends early if the daemon exits.
19const startupWait = 60 * time.Second
20
21// tailBuffer keeps the last max bytes written to it, for a failure message.
22type tailBuffer struct {
23 mu sync.Mutex
24 max int
25 buf []byte
26}
27
28func (b *tailBuffer) Write(p []byte) (int, error) {
29 b.mu.Lock()
30 defer b.mu.Unlock()
31 b.buf = append(b.buf, p...)
32 if over := len(b.buf) - b.max; over > 0 {
33 b.buf = b.buf[over:]
34 }
35 return len(p), nil
36}
37
38func (b *tailBuffer) String() string {
39 b.mu.Lock()
40 defer b.mu.Unlock()
41 return string(bytes.TrimSpace(b.buf))
42}
43
44// exited reports whether the process has ended without reaping it, so the
45// test can still Wait on it. It reads /proc on Linux, where CI runs, and
46// reports false elsewhere.
47func exited(pid int) bool {
48 if runtime.GOOS != "linux" {
49 return false
50 }
51 stat, err := os.ReadFile(fmt.Sprintf("/proc/%d/stat", pid))
52 if err != nil {
53 return true
54 }
55 // The state follows the command name, which is in parentheses.
56 s := string(stat)
57 if i := strings.LastIndexByte(s, ')'); i >= 0 && i+2 < len(s) {
58 return s[i+2] == 'Z'
59 }
60 return false
61}
62
63func dialable(port int) bool {
64 conn, err := net.DialTimeout("tcp", fmt.Sprintf("127.0.0.1:%d", port), 200*time.Millisecond)
65 if err != nil {
66 return false
67 }
68 conn.Close()
69 return true
70}
71
72// waitListening waits until every port accepts a connection. It fails at
73// once if the instance's daemon exits, and on either failure names the
74// ports that never answered and shows the end of the daemon's stderr.
75func (i *instance) waitListening(t *testing.T, ports ...int) {
76 t.Helper()
77 deadline := time.Now().Add(startupWait)
78 pending := ports
79 for {
80 var still []int
81 for _, p := range pending {
82 if !dialable(p) {
83 still = append(still, p)
84 }
85 }
86 if len(still) == 0 {
87 return
88 }
89 pending = still
90 why := ""
91 switch {
92 case i.proc != nil && i.proc.Process != nil && exited(i.proc.Process.Pid):
93 why = "gitbayd exited"
94 case time.Now().After(deadline):
95 why = fmt.Sprintf("gitbayd still not listening after %s", startupWait)
96 }
97 if why != "" {
98 log := "(not captured)"
99 if i.stderr != nil {
100 log = i.stderr.String()
101 }
102 t.Fatalf("%s on %v; its stderr ends:\n%s", why, pending, log)
103 }
104 time.Sleep(50 * time.Millisecond)
105 }
106}