tailscale/tstest/nettest/nettest.go
Brad Fitzpatrick fb96137d79 net/{netx,memnet},all: add netx.DialFunc, move memnet Network impl
This adds netx.DialFunc, unifying a type we have a bazillion other
places, giving it now a nice short name that's clickable in
editors, etc.

That highlighted that my earlier move (03b47a55c7956) of stuff from
nettest into netx moved too much: it also dragged along the memnet
impl, meaning all users of netx.DialFunc who just wanted netx for the
type definition were instead also pulling in all of memnet.

So move the memnet implementation netx.Network into memnet, a package
we already had.

Then use netx.DialFunc in a bunch of places. I'm sure I missed some.
And plenty remain in other repos, to be updated later.

Updates tailscale/corp#27636

Change-Id: I7296cd4591218e8624e214f8c70dab05fb884e95
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2025-04-08 10:07:47 -07:00

119 lines
3.2 KiB
Go

// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
// Package nettest contains additional test helpers related to network state
// that can't go into tstest for circular dependency reasons.
package nettest
import (
"context"
"flag"
"net"
"net/http"
"net/http/httptest"
"sync"
"testing"
"tailscale.com/net/memnet"
"tailscale.com/net/netmon"
"tailscale.com/net/netx"
"tailscale.com/util/testenv"
)
var useMemNet = flag.Bool("use-test-memnet", false, "prefer using in-memory network for tests")
// SkipIfNoNetwork skips the test if it looks like there's no network
// access.
func SkipIfNoNetwork(t testing.TB) {
nm := netmon.NewStatic()
if !nm.InterfaceState().AnyInterfaceUp() {
t.Skip("skipping; test requires network but no interface is up")
}
}
// PreferMemNetwork reports whether the --use-test-memnet flag is set.
func PreferMemNetwork() bool {
return *useMemNet
}
// GetNetwork returns the appropriate Network implementation based on
// whether the --use-test-memnet flag is set.
//
// Each call generates a new network.
func GetNetwork(tb testing.TB) netx.Network {
var n netx.Network
if PreferMemNetwork() {
n = &memnet.Network{}
} else {
n = netx.RealNetwork()
}
detectLeaks := PreferMemNetwork() || !testenv.InParallelTest(tb)
if detectLeaks {
tb.Cleanup(func() {
// TODO: leak detection, making sure no connections
// remain at the end of the test. For real network,
// snapshot conns in pid table before & after.
})
}
return n
}
// NewHTTPServer starts and returns a new [httptest.Server].
// The caller should call Close when finished, to shut it down.
func NewHTTPServer(net netx.Network, handler http.Handler) *httptest.Server {
ts := NewUnstartedHTTPServer(net, handler)
ts.Start()
return ts
}
// NewUnstartedHTTPServer returns a new [httptest.Server] but doesn't start it.
//
// After changing its configuration, the caller should call Start or
// StartTLS.
//
// The caller should call Close when finished, to shut it down.
func NewUnstartedHTTPServer(nw netx.Network, handler http.Handler) *httptest.Server {
s := &httptest.Server{
Config: &http.Server{Handler: handler},
}
ln := nw.NewLocalTCPListener()
s.Listener = &listenerOnAddrOnce{
Listener: ln,
fn: func() {
c := s.Client()
if c == nil {
// This httptest.Server.Start initialization order has been true
// for over 10 years. Let's keep counting on it.
panic("httptest.Server: Client not initialized before Addr called")
}
if c.Transport == nil {
c.Transport = &http.Transport{}
}
tr := c.Transport.(*http.Transport)
if tr.Dial != nil || tr.DialContext != nil {
panic("unexpected non-nil Dial or DialContext in httptest.Server.Client.Transport")
}
tr.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) {
return nw.Dial(ctx, network, addr)
}
},
}
return s
}
// listenerOnAddrOnce is a net.Listener that wraps another net.Listener
// and calls a function the first time its Addr is called.
type listenerOnAddrOnce struct {
net.Listener
once sync.Once
fn func()
}
func (ln *listenerOnAddrOnce) Addr() net.Addr {
ln.once.Do(func() {
ln.fn()
})
return ln.Listener.Addr()
}