logtail: run HTTP tests in-memory with memnet + synctest
TestEncodeAndUploadMessages waited on the default 2s FlushDelay, making the logtail package the slowest non-integration test in the tree (~2s real time). Switch the shared harness from an httptest.Server-on-loopback to a memnet.Listener-backed *http.Server and run the tests inside synctest.Test, so fake time advances the flush timer instantly. Drops the net/http/httptest dependency from these tests. Combined with the TestMain non-localhost dial guard added in the previous commit, no test in this package can accidentally reach the real log.tailscale.com server. Whole package now runs in ~7ms. Updates tailscale/corp#28679 Change-Id: Ie0e7a6a79641384ed0eecb99d767e17cda8bb944 Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
This commit is contained in:
committed by
Brad Fitzpatrick
parent
5b06e32f33
commit
1e68a11721
+47
-42
@@ -11,7 +11,6 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
@@ -20,6 +19,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/go-json-experiment/json/jsontext"
|
"github.com/go-json-experiment/json/jsontext"
|
||||||
|
"tailscale.com/net/memnet"
|
||||||
"tailscale.com/tstest"
|
"tailscale.com/tstest"
|
||||||
"tailscale.com/tstime"
|
"tailscale.com/tstime"
|
||||||
"tailscale.com/util/eventbus/eventbustest"
|
"tailscale.com/util/eventbus/eventbustest"
|
||||||
@@ -30,6 +30,7 @@ import (
|
|||||||
// test in this package. Config.BaseURL defaults to https://log.tailscale.com
|
// test in this package. Config.BaseURL defaults to https://log.tailscale.com
|
||||||
// and Config.HTTPC defaults to http.DefaultClient, so a test that forgets to
|
// and Config.HTTPC defaults to http.DefaultClient, so a test that forgets to
|
||||||
// override either can otherwise silently hit the real logtail server.
|
// override either can otherwise silently hit the real logtail server.
|
||||||
|
// Tests that need an HTTP server should use memnet (see newTestLogtailServer).
|
||||||
func TestMain(m *testing.M) {
|
func TestMain(m *testing.M) {
|
||||||
tr := http.DefaultTransport.(*http.Transport)
|
tr := http.DefaultTransport.(*http.Transport)
|
||||||
orig := tr.DialContext
|
orig := tr.DialContext
|
||||||
@@ -38,25 +39,19 @@ func TestMain(m *testing.M) {
|
|||||||
if err == nil && (host == "127.0.0.1" || host == "::1" || host == "localhost") {
|
if err == nil && (host == "127.0.0.1" || host == "::1" || host == "localhost") {
|
||||||
return orig(ctx, network, addr)
|
return orig(ctx, network, addr)
|
||||||
}
|
}
|
||||||
return nil, fmt.Errorf("logtail tests: refusing to dial non-localhost address %q; use httptest.Server or a custom Config.HTTPC", addr)
|
return nil, fmt.Errorf("logtail tests: refusing to dial non-localhost address %q; use memnet or a custom Config.HTTPC", addr)
|
||||||
}
|
}
|
||||||
os.Exit(m.Run())
|
os.Exit(m.Run())
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestFastShutdown(t *testing.T) {
|
func TestFastShutdown(t *testing.T) { synctest.Test(t, synctestFastShutdown) }
|
||||||
|
|
||||||
|
func synctestFastShutdown(t *testing.T) {
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
cancel()
|
cancel()
|
||||||
|
|
||||||
testServ := httptest.NewServer(http.HandlerFunc(
|
_, logger := newTestLogtailServer(t)
|
||||||
func(w http.ResponseWriter, r *http.Request) {}))
|
if err := logger.Shutdown(ctx); err != nil {
|
||||||
defer testServ.Close()
|
|
||||||
|
|
||||||
logger := NewLogger(Config{
|
|
||||||
BaseURL: testServ.URL,
|
|
||||||
Bus: eventbustest.NewBus(t),
|
|
||||||
}, t.Logf)
|
|
||||||
err := logger.Shutdown(ctx)
|
|
||||||
if err != nil {
|
|
||||||
t.Error(err)
|
t.Error(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -65,49 +60,60 @@ func TestFastShutdown(t *testing.T) {
|
|||||||
const logLines = 3
|
const logLines = 3
|
||||||
|
|
||||||
type LogtailTestServer struct {
|
type LogtailTestServer struct {
|
||||||
srv *httptest.Server // Log server
|
|
||||||
uploaded chan []byte
|
uploaded chan []byte
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewLogtailTestHarness(t *testing.T) (*LogtailTestServer, *Logger) {
|
// newTestLogtailServer wires up an in-memory HTTP server (via memnet) and a
|
||||||
ts := LogtailTestServer{}
|
// *Logger whose HTTPC dials it. Lives inside the caller's synctest bubble so
|
||||||
|
// the default FlushDelay and any other fake timers advance automatically.
|
||||||
|
func newTestLogtailServer(t *testing.T) (*LogtailTestServer, *Logger) {
|
||||||
|
ts := &LogtailTestServer{
|
||||||
|
// max channel backlog = 1 "started" + #logLines x "log line" + 1 "closed"
|
||||||
|
uploaded: make(chan []byte, 2+logLines),
|
||||||
|
}
|
||||||
|
|
||||||
// max channel backlog = 1 "started" + #logLines x "log line" + 1 "closed"
|
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
ts.uploaded = make(chan []byte, 2+logLines)
|
body, err := io.ReadAll(r.Body)
|
||||||
|
if err != nil {
|
||||||
|
t.Error("failed to read HTTP request")
|
||||||
|
}
|
||||||
|
ts.uploaded <- body
|
||||||
|
})
|
||||||
|
|
||||||
ts.srv = httptest.NewServer(http.HandlerFunc(
|
ln := memnet.Listen("logtail-test:0")
|
||||||
func(w http.ResponseWriter, r *http.Request) {
|
httpsrv := &http.Server{Handler: handler}
|
||||||
body, err := io.ReadAll(r.Body)
|
go httpsrv.Serve(ln)
|
||||||
if err != nil {
|
t.Cleanup(func() {
|
||||||
t.Error("failed to read HTTP request")
|
httpsrv.Close()
|
||||||
}
|
ln.Close()
|
||||||
ts.uploaded <- body
|
})
|
||||||
}))
|
|
||||||
|
|
||||||
t.Cleanup(ts.srv.Close)
|
|
||||||
|
|
||||||
logger := NewLogger(Config{
|
logger := NewLogger(Config{
|
||||||
BaseURL: ts.srv.URL,
|
BaseURL: "http://" + ln.Addr().String(),
|
||||||
Bus: eventbustest.NewBus(t),
|
Bus: eventbustest.NewBus(t),
|
||||||
|
HTTPC: &http.Client{
|
||||||
|
Transport: &http.Transport{DialContext: ln.Dial},
|
||||||
|
},
|
||||||
}, t.Logf)
|
}, t.Logf)
|
||||||
|
|
||||||
// There is always an initial "logtail started" message
|
// There is always an initial "logtail started" message.
|
||||||
body := <-ts.uploaded
|
body := <-ts.uploaded
|
||||||
if !strings.Contains(string(body), "started") {
|
if !strings.Contains(string(body), "started") {
|
||||||
t.Errorf("unknown start logging statement: %q", string(body))
|
t.Errorf("unknown start logging statement: %q", string(body))
|
||||||
}
|
}
|
||||||
|
return ts, logger
|
||||||
return &ts, logger
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDrainPendingMessages(t *testing.T) {
|
func TestDrainPendingMessages(t *testing.T) { synctest.Test(t, synctestDrainPendingMessages) }
|
||||||
ts, logger := NewLogtailTestHarness(t)
|
|
||||||
|
func synctestDrainPendingMessages(t *testing.T) {
|
||||||
|
ts, logger := newTestLogtailServer(t)
|
||||||
|
|
||||||
for range logLines {
|
for range logLines {
|
||||||
logger.Write([]byte("log line"))
|
logger.Write([]byte("log line"))
|
||||||
}
|
}
|
||||||
|
|
||||||
// all of the "log line" messages usually arrive at once, but poll if needed.
|
// All the "log line" messages usually arrive at once, but poll if needed.
|
||||||
var body strings.Builder
|
var body strings.Builder
|
||||||
for i := 0; i <= logLines; i++ {
|
for i := 0; i <= logLines; i++ {
|
||||||
body.WriteString(string(<-ts.uploaded))
|
body.WriteString(string(<-ts.uploaded))
|
||||||
@@ -115,17 +121,17 @@ func TestDrainPendingMessages(t *testing.T) {
|
|||||||
if count == logLines {
|
if count == logLines {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
// if we never find count == logLines, the test will eventually time out.
|
|
||||||
}
|
}
|
||||||
|
|
||||||
err := logger.Shutdown(context.Background())
|
if err := logger.Shutdown(context.Background()); err != nil {
|
||||||
if err != nil {
|
|
||||||
t.Error(err)
|
t.Error(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestEncodeAndUploadMessages(t *testing.T) {
|
func TestEncodeAndUploadMessages(t *testing.T) { synctest.Test(t, synctestEncodeAndUploadMessages) }
|
||||||
ts, logger := NewLogtailTestHarness(t)
|
|
||||||
|
func synctestEncodeAndUploadMessages(t *testing.T) {
|
||||||
|
ts, logger := newTestLogtailServer(t)
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
@@ -166,8 +172,7 @@ func TestEncodeAndUploadMessages(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
err := logger.Shutdown(context.Background())
|
if err := logger.Shutdown(context.Background()); err != nil {
|
||||||
if err != nil {
|
|
||||||
t.Error(err)
|
t.Error(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user