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) {
2222 inst.proc.Process.Kill()
2323 inst.proc.Wait()
2424
25 httpsPort := freePort(t)
26 acmeHTTPPort := freePort(t)
25 ports := freePorts(t, 2)
26 httpsPort, acmeHTTPPort := ports[0], ports[1]
2727 cfg := fmt.Sprintf(`
2828[server]
2929root = %q
e2e/backup_test.go +2 −2
@@ -69,8 +69,8 @@ func TestAdminBackup(t *testing.T) {
6969 if outB, err := exec.Command("tar", "-xzf", archive, "-C", root2).CombinedOutput(); err != nil {
7070 t.Fatalf("extract: %v\n%s", err, outB)
7171 }
72 port2 := freePort(t)
73 httpPort2 := freePort(t)
72 ports := freePorts(t, 2)
73 port2, httpPort2 := ports[0], ports[1]
7474 config2 := filepath.Join(root2, "config.toml")
7575 cfg := fmt.Sprintf(`
7676[server]
e2e/ssh_test.go +48 −9
@@ -36,14 +36,36 @@ func buildGitbayd(t *testing.T) string {
3636 return bin
3737}
3838
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 {
4049 t.Helper()
41 ln, err := net.Listen("tcp", "127.0.0.1:0")
42 if err != nil {
43 t.Fatal(err)
50 lns := make([]net.Listener, 0, n)
51 ports := make([]int, 0, n)
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)
4459 }
45 defer ln.Close()
46 return ln.Addr().(*net.TCPAddr).Port
60 for _, ln := range lns {
61 ln.Close()
62 }
63 return ports
64}
65
66func freePort(t *testing.T) int {
67 t.Helper()
68 return freePorts(t, 1)[0]
4769}
4870
4971func startInstance(t *testing.T) *instance {
@@ -53,12 +75,13 @@ func startInstance(t *testing.T) *instance {
5375// startInstanceWith appends extra TOML to the instance config.
5476func startInstanceWith(t *testing.T, extra string) *instance {
5577 t.Helper()
78 ports := freePorts(t, 3)
5679 inst := &instance{
5780 gitbayd: buildGitbayd(t),
5881 root: t.TempDir(),
59 port: freePort(t),
60 httpPort: freePort(t),
61 gitPort: freePort(t),
82 port: ports[0],
83 httpPort: ports[1],
84 gitPort: ports[2],
6285 sshDir: t.TempDir(),
6386 }
6487 inst.config = filepath.Join(inst.root, "config.toml")
@@ -244,3 +267,19 @@ func TestControlPlaneOverBareSSH(t *testing.T) {
244267 t.Fatalf("keys remove with spaced arg: exit %d (want 3), stderr %q", code, errOut)
245268 }
246269}
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}