| @@ -36,14 +36,36 @@ func buildGitbayd(t *testing.T) string { |
| 36 | 36 | return bin |
| 37 | 37 | } |
| 38 | 38 | |
| 39 | | func 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. |
| 48 | func freePorts(t *testing.T, n int) []int { |
| 40 | 49 | 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) |
| 44 | 59 | } |
| 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 | |
| 66 | func freePort(t *testing.T) int { |
| 67 | t.Helper() |
| 68 | return freePorts(t, 1)[0] |
| 47 | 69 | } |
| 48 | 70 | |
| 49 | 71 | func startInstance(t *testing.T) *instance { |
| @@ -53,12 +75,13 @@ func startInstance(t *testing.T) *instance { |
| 53 | 75 | // startInstanceWith appends extra TOML to the instance config. |
| 54 | 76 | func startInstanceWith(t *testing.T, extra string) *instance { |
| 55 | 77 | t.Helper() |
| 78 | ports := freePorts(t, 3) |
| 56 | 79 | inst := &instance{ |
| 57 | 80 | gitbayd: buildGitbayd(t), |
| 58 | 81 | 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], |
| 62 | 85 | sshDir: t.TempDir(), |
| 63 | 86 | } |
| 64 | 87 | inst.config = filepath.Join(inst.root, "config.toml") |
| @@ -244,3 +267,19 @@ func TestControlPlaneOverBareSSH(t *testing.T) { |
| 244 | 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". |
| 275 | func 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 | } |