Skip to content

Commit 50575e1

Browse files
authored
fix: use fake local network for port-forward tests (#11119)
Fixes #10979 Testing code that listens on a specific port has created a long battle with flakes. Previous attempts to deal with this include opening a listener on a port chosen by the OS, then closing the listener, noting the port and starting the test with that port. This still flakes, notably in macOS which has a proclivity to reuse ports quickly. Instead of fighting with the chaos that is an OS networking stack, this PR fakes the host networking in tests. I've taken a small step here, only faking out the Listen() calls that port-forward makes, but I think over time we should be transitioning all networking the CLI does to an abstract interface so we can fake it. This allows us to run in parallel without flakes and presents an opportunity to test error paths as well.
1 parent 37f6b38 commit 50575e1

File tree

4 files changed

+191
-106
lines changed

4 files changed

+191
-106
lines changed

cli/clibase/cmd.go

+2
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,7 @@ type Invocation struct {
189189
Stderr io.Writer
190190
Stdin io.Reader
191191
Logger slog.Logger
192+
Net Net
192193

193194
// testing
194195
signalNotifyContext func(parent context.Context, signals ...os.Signal) (ctx context.Context, stop context.CancelFunc)
@@ -203,6 +204,7 @@ func (inv *Invocation) WithOS() *Invocation {
203204
i.Stdin = os.Stdin
204205
i.Args = os.Args[1:]
205206
i.Environ = ParseEnviron(os.Environ(), "")
207+
i.Net = osNet{}
206208
})
207209
}
208210

cli/clibase/net.go

+50
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package clibase
2+
3+
import (
4+
"net"
5+
"strconv"
6+
7+
"github.com/pion/udp"
8+
"golang.org/x/xerrors"
9+
)
10+
11+
// Net abstracts CLI commands interacting with the operating system networking.
12+
//
13+
// At present, it covers opening local listening sockets, since doing this
14+
// in testing is a challenge without flakes, since it's hard to pick a port we
15+
// know a priori will be free.
16+
type Net interface {
17+
// Listen has the same semantics as `net.Listen` but also supports `udp`
18+
Listen(network, address string) (net.Listener, error)
19+
}
20+
21+
// osNet is an implementation that call the real OS for networking.
22+
type osNet struct{}
23+
24+
func (osNet) Listen(network, address string) (net.Listener, error) {
25+
switch network {
26+
case "tcp", "tcp4", "tcp6", "unix", "unixpacket":
27+
return net.Listen(network, address)
28+
case "udp":
29+
host, port, err := net.SplitHostPort(address)
30+
if err != nil {
31+
return nil, xerrors.Errorf("split %q: %w", address, err)
32+
}
33+
34+
var portInt int
35+
portInt, err = strconv.Atoi(port)
36+
if err != nil {
37+
return nil, xerrors.Errorf("parse port %v from %q as int: %w", port, address, err)
38+
}
39+
40+
// Use pion here so that we get a stream-style net.Conn listener, instead
41+
// of a packet-oriented connection that can read and write to multiple
42+
// addresses.
43+
return udp.Listen(network, &net.UDPAddr{
44+
IP: net.ParseIP(host),
45+
Port: portInt,
46+
})
47+
default:
48+
return nil, xerrors.Errorf("unknown listen network %q", network)
49+
}
50+
}

cli/portforward.go

+7-29
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import (
1212
"sync"
1313
"syscall"
1414

15-
"github.com/pion/udp"
1615
"golang.org/x/xerrors"
1716

1817
"cdr.dev/slog"
@@ -121,6 +120,7 @@ func (r *RootCmd) portForward() *clibase.Cmd {
121120
wg = new(sync.WaitGroup)
122121
listeners = make([]net.Listener, len(specs))
123122
closeAllListeners = func() {
123+
logger.Debug(ctx, "closing all listeners")
124124
for _, l := range listeners {
125125
if l == nil {
126126
continue
@@ -134,6 +134,7 @@ func (r *RootCmd) portForward() *clibase.Cmd {
134134
for i, spec := range specs {
135135
l, err := listenAndPortForward(ctx, inv, conn, wg, spec, logger)
136136
if err != nil {
137+
logger.Error(ctx, "failed to listen", slog.F("spec", spec), slog.Error(err))
137138
return err
138139
}
139140
listeners[i] = l
@@ -151,8 +152,10 @@ func (r *RootCmd) portForward() *clibase.Cmd {
151152

152153
select {
153154
case <-ctx.Done():
155+
logger.Debug(ctx, "command context expired waiting for signal", slog.Error(ctx.Err()))
154156
closeErr = ctx.Err()
155-
case <-sigs:
157+
case sig := <-sigs:
158+
logger.Debug(ctx, "received signal", slog.F("signal", sig))
156159
_, _ = fmt.Fprintln(inv.Stderr, "\nReceived signal, closing all listeners and active connections")
157160
}
158161

@@ -161,6 +164,7 @@ func (r *RootCmd) portForward() *clibase.Cmd {
161164
}()
162165

163166
conn.AwaitReachable(ctx)
167+
logger.Debug(ctx, "read to accept connections to forward")
164168
_, _ = fmt.Fprintln(inv.Stderr, "Ready!")
165169
wg.Wait()
166170
return closeErr
@@ -198,33 +202,7 @@ func listenAndPortForward(
198202
logger = logger.With(slog.F("network", spec.listenNetwork), slog.F("address", spec.listenAddress))
199203
_, _ = fmt.Fprintf(inv.Stderr, "Forwarding '%v://%v' locally to '%v://%v' in the workspace\n", spec.listenNetwork, spec.listenAddress, spec.dialNetwork, spec.dialAddress)
200204

201-
var (
202-
l net.Listener
203-
err error
204-
)
205-
switch spec.listenNetwork {
206-
case "tcp":
207-
l, err = net.Listen(spec.listenNetwork, spec.listenAddress)
208-
case "udp":
209-
var host, port string
210-
host, port, err = net.SplitHostPort(spec.listenAddress)
211-
if err != nil {
212-
return nil, xerrors.Errorf("split %q: %w", spec.listenAddress, err)
213-
}
214-
215-
var portInt int
216-
portInt, err = strconv.Atoi(port)
217-
if err != nil {
218-
return nil, xerrors.Errorf("parse port %v from %q as int: %w", port, spec.listenAddress, err)
219-
}
220-
221-
l, err = udp.Listen(spec.listenNetwork, &net.UDPAddr{
222-
IP: net.ParseIP(host),
223-
Port: portInt,
224-
})
225-
default:
226-
return nil, xerrors.Errorf("unknown listen network %q", spec.listenNetwork)
227-
}
205+
l, err := inv.Net.Listen(spec.listenNetwork, spec.listenAddress)
228206
if err != nil {
229207
return nil, xerrors.Errorf("listen '%v://%v': %w", spec.listenNetwork, spec.listenAddress, err)
230208
}

0 commit comments

Comments
 (0)