testing: add start of an integration test

Only minimal tailscale + tailscaled for now.

And a super minimal in-memory logcatcher.

No control ... yet.

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
This commit is contained in:
Brad Fitzpatrick 2021-04-29 14:44:08 -07:00 committed by Brad Fitzpatrick
parent 08782b92f7
commit 80429b97e5

View File

@ -0,0 +1,175 @@
// Copyright (c) 2021 Tailscale Inc & 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 integration contains Tailscale integration tests.
package integration
import (
"bytes"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"log"
"net/http"
"net/http/httptest"
"os"
"os/exec"
"path"
"path/filepath"
"runtime"
"strings"
"sync"
"testing"
"time"
"go4.org/mem"
"tailscale.com/smallzstd"
"tailscale.com/tstest"
)
func TestIntegration(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("not tested/working on Windows yet")
}
td := t.TempDir()
daemonExe := build(t, td, "tailscale.com/cmd/tailscaled")
cliExe := build(t, td, "tailscale.com/cmd/tailscale")
logc := new(logCatcher)
ts := httptest.NewServer(logc)
defer ts.Close()
socketPath := filepath.Join(td, "tailscale.sock")
dcmd := exec.Command(daemonExe,
"--tun=userspace-networking",
"--state="+filepath.Join(td, "tailscale.state"),
"--socket="+socketPath,
)
dcmd.Env = append(os.Environ(), "TS_LOG_TARGET="+ts.URL)
if err := dcmd.Start(); err != nil {
t.Fatalf("starting tailscaled: %v", err)
}
defer dcmd.Process.Kill()
var json []byte
if err := tstest.WaitFor(20*time.Second, func() (err error) {
json, err = exec.Command(cliExe, "--socket="+socketPath, "status", "--json").CombinedOutput()
if err != nil {
return fmt.Errorf("running tailscale status: %v, %s", err, json)
}
return nil
}); err != nil {
t.Fatal(err)
}
if err := tstest.WaitFor(20*time.Second, func() error {
const sub = `Program starting: `
if !logc.logsContains(mem.S(sub)) {
return fmt.Errorf("log catcher didn't see %#q; got %s", sub, logc.logsString())
}
return nil
}); err != nil {
t.Error(err)
}
dcmd.Process.Signal(os.Interrupt)
ps, err := dcmd.Process.Wait()
if err != nil {
t.Fatalf("tailscaled Wait: %v", err)
}
if ps.ExitCode() != 0 {
t.Errorf("tailscaled ExitCode = %d; want 0", ps.ExitCode())
}
t.Logf("number of HTTP logcatcher requests: %v", logc.numRequests())
}
func build(t *testing.T, outDir, target string) string {
exe := ""
if runtime.GOOS == "windows" {
exe = ".exe"
}
bin := filepath.Join(outDir, path.Base(target)) + exe
errOut, err := exec.Command("go", "build", "-o", bin, target).CombinedOutput()
if err != nil {
t.Fatalf("failed to build %v: %v, %s", target, err, errOut)
}
return bin
}
type logCatcher struct {
mu sync.Mutex
buf bytes.Buffer
gotErr error
reqs int
}
func (lc *logCatcher) logsContains(sub mem.RO) bool {
lc.mu.Lock()
defer lc.mu.Unlock()
return mem.Contains(mem.B(lc.buf.Bytes()), sub)
}
func (lc *logCatcher) numRequests() int {
lc.mu.Lock()
defer lc.mu.Unlock()
return lc.reqs
}
func (lc *logCatcher) logsString() string {
lc.mu.Lock()
defer lc.mu.Unlock()
return lc.buf.String()
}
func (lc *logCatcher) ServeHTTP(w http.ResponseWriter, r *http.Request) {
var body io.Reader = r.Body
if r.Header.Get("Content-Encoding") == "zstd" {
var err error
body, err = smallzstd.NewDecoder(body)
if err != nil {
log.Printf("bad caught zstd: %v", err)
http.Error(w, err.Error(), 400)
return
}
}
bodyBytes, _ := ioutil.ReadAll(body)
type Entry struct {
Logtail struct {
ClientTime time.Time `json:"client_time"`
ServerTime time.Time `json:"server_time"`
Error struct {
BadData string `json:"bad_data"`
} `json:"error"`
} `json:"logtail"`
Text string `json:"text"`
}
var jreq []Entry
var err error
if len(bodyBytes) > 0 && bodyBytes[0] == '[' {
err = json.Unmarshal(bodyBytes, &jreq)
} else {
var ent Entry
err = json.Unmarshal(bodyBytes, &ent)
jreq = append(jreq, ent)
}
lc.mu.Lock()
defer lc.mu.Unlock()
lc.reqs++
if lc.gotErr == nil && err != nil {
lc.gotErr = err
}
if err != nil {
fmt.Fprintf(&lc.buf, "error from %s of %#q: %v\n", r.Method, bodyBytes, err)
} else {
for _, ent := range jreq {
fmt.Fprintf(&lc.buf, "%s\n", strings.TrimSpace(ent.Text))
}
}
w.WriteHeader(200) // must have no content, but not a 204
}