e2e/ready_test.go

bd5cf5d7d1f34fa780660fd7562b9ffd9746ee27
gitbay/e2e/ready_test.go history · blame · raw

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}