Use each port only once during e2e tests.
The previous port allocation strategy could lead to collisions
in rare circumstances.
Closes
package testutil
import (
+ "errors"
"fmt"
"io/ioutil"
"net"
"os"
"runtime"
"strings"
+ "sync/atomic"
"testing"
)
+// dirty hack to prevent port collisions
+var lastPort int64 = 30000
+
// TempDir provides a test with a temporary directory (under os.TempDir())
// returning the absolute path to the directory and a remove() function
// that should be deferred immediately after calling TempDir(t) to recursively
@@ -23,25 +28,47 @@ func TempDir(t *testing.T) (tmpDir string, remove func()) {
return tmpDir, func() { _ = os.RemoveAll(tmpDir) }
}
-// RandomPorts generates len(p) random ports and assigns them to elements of p.
-func RandomPorts(p []int) (err error) {
- for i := range p {
- l, err := net.Listen("tcp", "127.0.0.1:0")
+// ReservePort reserves a port, as long as this process is the only one
+// on the system opening ports. Introduced to avoid port collisions in
+// our end-to-end tests. A total hack, and will probably need replacing
+// one day.
+func ReservePort() (int, error) {
+ for {
+ port := atomic.AddInt64(&lastPort, 1)
+ if port > 65535 {
+ return 0, errors.New("port allocation failed")
+ }
+ ln, err := net.Listen("tcp4", fmt.Sprintf("127.0.0.1:%d", port))
if err != nil {
- return err
+ if oe, ok := err.(*net.OpError); ok {
+ if oe.Timeout() {
+ return 0, err
+ }
+ }
+ continue
}
- defer func() {
- e := l.Close()
+ for {
+ err, ok := ln.Close().(*net.OpError)
if err == nil {
- err = e
+ return int(port), nil
+ }
+ if ok && err.Temporary() {
+ continue
+ }
+ return 0, err
+ }
+ }
}
- }()
- addr, err := net.ResolveTCPAddr("tcp", l.Addr().String())
+// RandomPorts reserves len(p) ports and assigns them to elements of p.
+// It calls ReservePort repeatedly.
+func RandomPorts(p []int) (err error) {
+ for i := range p {
+ port, err := ReservePort()
if err != nil {
return err
}
- p[i] = addr.Port
+ p[i] = port
}
return nil
}
