Skip to content

Commit

Permalink
net/http/httptest: Add NewUnstartedPipeServer
Browse files Browse the repository at this point in the history
Adds a new server initialization function to create a net.Pipe based
listener for a httptest.Server.

Fixes golang#14200.
  • Loading branch information
emcfarlane committed Oct 6, 2023
1 parent dcd018b commit bbbd206
Show file tree
Hide file tree
Showing 3 changed files with 127 additions and 0 deletions.
93 changes: 93 additions & 0 deletions src/net/http/httptest/pipelistener.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
// Copyright 2023 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package httptest

import (
"context"
"errors"
"net"
"sync"
)

// A pipeListener is a net.Listener implementation that serves
// in-memory pipes.
type pipeListener struct {
addr pipeAddr

close sync.Once
done chan struct{}
ch chan net.Conn
}

func newPipeListener(addr string) *pipeListener {
return &pipeListener{
addr: pipeAddr(addr),
done: make(chan struct{}),
c: make(chan net.Conn),
}
}

func (lis *pipeListener) Accept() (net.Conn, error) {
aerr := func(err error) error {
return &net.OpError{
Op: "accept",
Net: lis.addr.Network(),
Addr: lis.addr,
Err: err,
}
}
select {
case <-lis.done:
return nil, aerr(errors.New("closed"))
case s := <-lis.ch:
return s, nil
}
}

func (lis *pipeListener) Close() (err error) {
err = errors.New("closed")
lis.close.Do(func() {
close(lis.done)
err = nil
})
return
}

func (lis *pipeListener) Addr() net.Addr {
return pipeAddr(lis.addr)
}

// Dial "dials" the listener, creating a pipe. It returns the client's
// end of the connection and causes a waiting Accept call to return
// the server's end.
func (lis *pipeListener) DialContext(ctx context.Context, network, addr string) (net.Conn, error) {
derr := func(err error) error {
return &net.OpError{
Op: "dial",
Net: lis.addr.Network(),
Err: err,
}
}
s, c := net.Pipe()
select {
case <-ctx.Done():
return nil, derr(ctx.Err())
case lis.ch <- s:
return c, nil
case <-lis.done:
return nil, derr(errors.New("closed"))
}
return c, nil
}

type pipeAddr string

func (pipeAddr) Network() string {
return "pipe"
}

func (addr pipeAddr) String() string {
return string(addr)
}
29 changes: 29 additions & 0 deletions src/net/http/httptest/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,35 @@ func NewUnstartedServer(handler http.Handler) *Server {
}
}

// NewUnstartedPipeServer returns a new Server that listens on a
// unique in-memory pipe.
//
// The provided addr argument is used as the address of the
// net.Listener that serves the pipes. It may be safely left blank in
// most cases, in which case DefaultRemoteAddr is used.
//
// If non-basic features of either HTTP or a network connection are
// needed, such as redirects or timeouts, the standard Server should
// be used.
//
// After changing its configuration, the caller should call Start or
// StartTLS.
//
// The caller should call Close when finished, to shut it down.
func NewUnstartedPipeServer(addr string, handler http.Handler) *Server {
if addr == "" {
addr = DefaultRemoteAddr
}
pipelistener := newPipeListener(addr)
return &Server{
Listener: pipelistener,
Config: &http.Server{Handler: handler},
client: &http.Client{Transport: &http.Transport{
DialContext: pipelistener.DialContext,
}},
}
}

// Start starts a server from NewUnstartedServer.
func (s *Server) Start() {
if s.URL != "" {
Expand Down
5 changes: 5 additions & 0 deletions src/net/http/httptest/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@ var newServers = map[string]newServerFunc{
ts.StartTLS()
return ts
},
"NewUnstartedPipeServer": func(h http.Handler) *Server {
ts := NewUnstartedPipeServer("", h)
ts.Start()
return ts
},
}

func TestServer(t *testing.T) {
Expand Down

0 comments on commit bbbd206

Please sign in to comment.