golang / go

The Go programming language
https://go.dev
BSD 3-Clause "New" or "Revised" License
124.03k stars 17.67k forks source link

proposal: x/net/nettest: add MakeLocalPipe to construct local listener and connections #30984

Open mdlayher opened 5 years ago

mdlayher commented 5 years ago

I'm currently implementing my own net.Conn type and am working on tests using nettest.TestConn: https://godoc.org/golang.org/x/net/nettest#TestConn.

The nettest.MakePipe function provides adequate functionality for my needs, but a fair amount of boilerplate is required to create a nettest.MakePipe that can:

As it turns out, there's a pretty useful function to make a local pipe in one of the tests: https://go.googlesource.com/net/+/refs/heads/master/nettest/conntest_test.go

I propose we generalize and export this function for use with net.Listener and net.Conn implementations of any type. I'm happy to accept name suggestions, but my proposed signature and WIP documentation is as follows:

// MakeLocalPipe creates a local net.Listener and points another net.Conn at the
// listener, producing a MakePipe function suitable for use with TestConn.
// 
// The listen function should produce a net.Listener, but should not invoke Accept or
// any other methods on the listener. The dial function receives the address of the
// listener, and should produce a net.Conn pointed at the listener. If dial is nil,
// net.Dial will be invoked with the network and address information from addr.
func MakeLocalPipe(
    listen func() (net.Listener, error),
    dial func(addr net.Addr) (net.Conn, error),
) nettest.MakePipe

Usage is as follows:

Producing a local pipe with a TCP listener and connection, using net.Dial as a default dial function.

nettest.TestConn(t, nettest.MakeLocalPipe(
    func() (net.Listener, error) {
        return net.Listen("tcp", ":0")
    },
    // dial: net.Dial(addr.Network(), addr.String())
    nil,
))

Producing a local pipe with a custom listener and connection implemented outside of the stdlib (https://github.com/mdlayher/vsock)

nettest.TestConn(t, nettest.MakeLocalPipe(
    func() (net.Listener, error) {
        return vsock.Listen(0)
    },
    func(addr net.Addr) (net.Conn, error) {
        a := addr.(*vsock.Addr)
        return vsock.Dial(a.ContextID, a.Port)
    },
))

I don't mind putting this code in my own repository or similar if deemed unnecessary, but it seems like this would be an appropriate addition to nettest, since it enables easy setup of scaffolding for use with nettest.TestConn.

/cc @dsnet @mikioh

mdlayher commented 5 years ago

Spoke with @acln0 and we came up with a possible alternative API that would have similar semantics, but would enable future extensibility as well. Something along the lines of:

type LocalPipe struct {
    // Same semantics, cannot be nil.
    Listen func() (net.Listener, error)

    // Same semantics: if nil, use net.Dial.
    Dial func(addr net.Addr) (net.Conn, error)

    // Room for future extensibility, like perhaps:
    //
    // If not nil, called to clean up any artifacts produced by the net.Listener, e.g.
    // UNIX socket files.
    // Cleanup func(l net.Listener) error
}

// MakePipe produces a MakePipe function using the configuration defined on LocalPipe.
func (lp *LocalPipe) MakePipe() nettest.MakePipe {}

However, we aren't sure that the extensibility would ever be needed.

I think it makes sense to keep the UNIX socket cleanup logic from the original test if net.Listener.Network() is "unix" or "unixgram". I think it also makes sense to require the caller to deal with cleanup of any other socket artifacts from unknown families on their own.

acln0 commented 5 years ago

I am in favor of this proposal. Having implemented this particular scaffolding logic in a number of different places already, it would be pleasant to have a canonical home for it.

With the alternative struct-based construction @mdlayher mentioned, perhaps a direct method is nicer:

func (lp LocalPipe) MakePipe() (c1, c2 net.Conn, close func(), err error)

Then, caller code makes use of the method value like so:

package vsock_test

func TestConn(t *testing.T) {
    vsocklp := netttest.LocalPipe{
        Listen: func() (net.Listener, error) {
            return vsock.Listen(0)
        },
        Dial: func(addr net.Addr) (net.Conn, error) {
            a := addr.(*vsock.Addr)
            return vsock.Dial(a.ContextID, a.Port)
        },
    }
    nettest.TestConn(t, vsocklp.MakePipe)
}
rsc commented 5 years ago

Thoughts, @mikioh?

rsc commented 5 years ago

Once @mikioh and @mdlayher agree here, this can be accepted.

rsc commented 5 years ago

Ping, @mikioh and @mdlayher. Note that the crypto/tls package tests had what amounts to a copy of the nettest/conntest_test.go TestTestConn body, and we had some reliability problems with it on some systems. I submitted a few CLs to deflake tests (https://golang.org/cl/184157), by not assuming that every dial would succeed. I don't know if that's necessary in general, but if it is then that may influence the API design.

mdlayher commented 5 years ago

I'm happy to submit the code I have been using for this. I haven't heard anything from @mikioh in a while.

I can't say I've run into tests which sometimes fail to dial, but assuming both of my proposed hook functions continue to return errors, I wouldn't be opposed to adding a retry mechanism.