e2e: reserve every port before releasing any of them !149

merged merged by cmc on 2026-09-01 07:08 UTC · krz/gitbay:fix-68 into main

3 files changed, +52 −13

Layout: unified · split

e2e/acme_test.go +2 −2
@@ -22,8 +22,8 @@ func TestACMEServe(t *testing.T) {
22 inst.proc.Process.Kill() 22 inst.proc.Process.Kill()
23 inst.proc.Wait() 23 inst.proc.Wait()
24 24
25 httpsPort := freePort(t) 25 ports := freePorts(t, 2)
26 acmeHTTPPort := freePort(t) 26 httpsPort, acmeHTTPPort := ports[0], ports[1]
27 cfg := fmt.Sprintf(` 27 cfg := fmt.Sprintf(`
28[server] 28[server]
29root = %q 29root = %q
e2e/backup_test.go +2 −2
@@ -69,8 +69,8 @@ func TestAdminBackup(t *testing.T) {
69 if outB, err := exec.Command("tar", "-xzf", archive, "-C", root2).CombinedOutput(); err != nil { 69 if outB, err := exec.Command("tar", "-xzf", archive, "-C", root2).CombinedOutput(); err != nil {
70 t.Fatalf("extract: %v\n%s", err, outB) 70 t.Fatalf("extract: %v\n%s", err, outB)
71 } 71 }
72 port2 := freePort(t) 72 ports := freePorts(t, 2)
73 httpPort2 := freePort(t) 73 port2, httpPort2 := ports[0], ports[1]
74 config2 := filepath.Join(root2, "config.toml") 74 config2 := filepath.Join(root2, "config.toml")
75 cfg := fmt.Sprintf(` 75 cfg := fmt.Sprintf(`
76[server] 76[server]
e2e/ssh_test.go +48 −9
@@ -36,14 +36,36 @@ func buildGitbayd(t *testing.T) string {
36 return bin 36 return bin
37} 37}
38 38
39func freePort(t *testing.T) int { 39// freePorts reserves n distinct ports. A port is chosen by binding :0 and
40// reading back what the kernel assigned, so every listener has to stay open
41// until all of them are picked — closing one before picking the next lets
42// the kernel hand out the same port again, and the instance that asked for
43// three then fails to bind its second listener.
44//
45// Still a narrowing rather than a guarantee: another process can take a port
46// between the close here and the bind in gitbayd. Distinctness within one
47// instance is the part that is ours.
48func freePorts(t *testing.T, n int) []int {
40 t.Helper() 49 t.Helper()
41 ln, err := net.Listen("tcp", "127.0.0.1:0") 50 lns := make([]net.Listener, 0, n)
42 if err != nil { 51 ports := make([]int, 0, n)
43 t.Fatal(err) 52 for i := 0; i < n; i++ {
53 ln, err := net.Listen("tcp", "127.0.0.1:0")
54 if err != nil {
55 t.Fatal(err)
56 }
57 lns = append(lns, ln)
58 ports = append(ports, ln.Addr().(*net.TCPAddr).Port)
44 } 59 }
45 defer ln.Close() 60 for _, ln := range lns {
46 return ln.Addr().(*net.TCPAddr).Port 61 ln.Close()
62 }
63 return ports
64}
65
66func freePort(t *testing.T) int {
67 t.Helper()
68 return freePorts(t, 1)[0]
47} 69}
48 70
49func startInstance(t *testing.T) *instance { 71func startInstance(t *testing.T) *instance {
@@ -53,12 +75,13 @@ func startInstance(t *testing.T) *instance {
53// startInstanceWith appends extra TOML to the instance config. 75// startInstanceWith appends extra TOML to the instance config.
54func startInstanceWith(t *testing.T, extra string) *instance { 76func startInstanceWith(t *testing.T, extra string) *instance {
55 t.Helper() 77 t.Helper()
78 ports := freePorts(t, 3)
56 inst := &instance{ 79 inst := &instance{
57 gitbayd: buildGitbayd(t), 80 gitbayd: buildGitbayd(t),
58 root: t.TempDir(), 81 root: t.TempDir(),
59 port: freePort(t), 82 port: ports[0],
60 httpPort: freePort(t), 83 httpPort: ports[1],
61 gitPort: freePort(t), 84 gitPort: ports[2],
62 sshDir: t.TempDir(), 85 sshDir: t.TempDir(),
63 } 86 }
64 inst.config = filepath.Join(inst.root, "config.toml") 87 inst.config = filepath.Join(inst.root, "config.toml")
@@ -244,3 +267,19 @@ func TestControlPlaneOverBareSSH(t *testing.T) {
244 t.Fatalf("keys remove with spaced arg: exit %d (want 3), stderr %q", code, errOut) 267 t.Fatalf("keys remove with spaced arg: exit %d (want 3), stderr %q", code, errOut)
245 } 268 }
246} 269}
270
271// freePort used to close its listener before returning, so the kernel was
272// free to hand the same port to the next call. An instance asks for three in
273// a row and then fails to bind its second listener, which surfaces as an
274// unrelated test timing out on "gitbayd did not start listening".
275func TestFreePortsAreDistinct(t *testing.T) {
276 for round := 0; round < 50; round++ {
277 seen := map[int]bool{}
278 for _, p := range freePorts(t, 8) {
279 if seen[p] {
280 t.Fatalf("round %d: port %d issued twice in one request", round, p)
281 }
282 seen[p] = true
283 }
284 }
285}