The test port allocation had a TOCTOU race where GetFreePort() would open a listener, grab the port number, then immediately close it. When called repeatedly, the OS could recycle a just-released port, causing two services (e.g. Filer and S3) to be assigned the same port. Replace per-call GetFreePort() with batch AllocatePorts() that holds all listeners open until every port is obtained, matching the pattern already used in test/volume_server/framework/cluster.go.
93 lines
2.2 KiB
Go
93 lines
2.2 KiB
Go
package testutil
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net"
|
|
"net/http"
|
|
"os/exec"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func HasDocker() bool {
|
|
cmd := exec.Command("docker", "version")
|
|
return cmd.Run() == nil
|
|
}
|
|
|
|
// MustFreePortPair is a convenience wrapper for tests that only need a single pair.
|
|
// Prefer MustAllocatePorts when allocating multiple pairs to guarantee uniqueness.
|
|
func MustFreePortPair(t *testing.T, name string) (int, int) {
|
|
ports := MustAllocatePorts(t, 2)
|
|
return ports[0], ports[1]
|
|
}
|
|
|
|
// MustAllocatePorts allocates count unique free ports atomically.
|
|
// All listeners are held open until every port is obtained, preventing
|
|
// the OS from recycling a port between successive allocations.
|
|
func MustAllocatePorts(t *testing.T, count int) []int {
|
|
t.Helper()
|
|
ports, err := AllocatePorts(count)
|
|
if err != nil {
|
|
t.Fatalf("Failed to allocate %d free ports: %v", count, err)
|
|
}
|
|
return ports
|
|
}
|
|
|
|
// AllocatePorts allocates count unique free ports atomically.
|
|
func AllocatePorts(count int) ([]int, error) {
|
|
listeners := make([]net.Listener, 0, count)
|
|
ports := make([]int, 0, count)
|
|
for i := 0; i < count; i++ {
|
|
l, err := net.Listen("tcp", "0.0.0.0:0")
|
|
if err != nil {
|
|
for _, ll := range listeners {
|
|
_ = ll.Close()
|
|
}
|
|
return nil, err
|
|
}
|
|
listeners = append(listeners, l)
|
|
ports = append(ports, l.Addr().(*net.TCPAddr).Port)
|
|
}
|
|
for _, l := range listeners {
|
|
_ = l.Close()
|
|
}
|
|
return ports, nil
|
|
}
|
|
|
|
func WaitForService(url string, timeout time.Duration) bool {
|
|
client := &http.Client{Timeout: 2 * time.Second}
|
|
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
|
defer cancel()
|
|
|
|
ticker := time.NewTicker(500 * time.Millisecond)
|
|
defer ticker.Stop()
|
|
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
return false
|
|
case <-ticker.C:
|
|
resp, err := client.Get(url)
|
|
if err == nil {
|
|
resp.Body.Close()
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func WaitForPort(port int, timeout time.Duration) bool {
|
|
deadline := time.Now().Add(timeout)
|
|
address := fmt.Sprintf("127.0.0.1:%d", port)
|
|
for time.Now().Before(deadline) {
|
|
conn, err := net.DialTimeout("tcp", address, 500*time.Millisecond)
|
|
if err == nil {
|
|
_ = conn.Close()
|
|
return true
|
|
}
|
|
time.Sleep(100 * time.Millisecond)
|
|
}
|
|
return false
|
|
}
|