veyron/lib/testutil/integration: an integration test environment.
Add a debug binary integration test in Go.
Change-Id: Id4bf8bf5fb241be0bb19134121c1ce31152628c0
diff --git a/lib/testutil/integration/util.go b/lib/testutil/integration/util.go
index b535576..6a6d9b8 100644
--- a/lib/testutil/integration/util.go
+++ b/lib/testutil/integration/util.go
@@ -2,20 +2,317 @@
import (
"bufio"
+ "bytes"
"fmt"
+ "io"
"io/ioutil"
"os"
"os/exec"
"path"
"path/filepath"
"strings"
+ "syscall"
+ "testing"
"time"
"veyron.io/veyron/veyron/lib/expect"
"veyron.io/veyron/veyron/lib/modules"
"veyron.io/veyron/veyron/lib/modules/core"
+ tsecurity "veyron.io/veyron/veyron/lib/testutil/security"
+ "veyron.io/veyron/veyron2/security"
)
+// TestEnvironment represents a test environment. You should obtain
+// an instance with NewTestEnvironment. Typically, an end-to-end
+// test will begin with:
+// func TestFoo(t *testing.T) {
+// env := integration.NewTestEnvironment(t)
+// defer env.Cleanup()
+//
+// ...
+// }
+type TestEnvironment interface {
+ // Cleanup cleans up the environment and deletes all its artifacts.
+ Cleanup()
+
+ // BuildGoPkg expects a Go package path that identifies a "main"
+ // package and returns a TestBinary representing the newly built
+ // binary.
+ BuildGoPkg(path string) TestBinary
+
+ // RootMT returns the endpoint to the root mounttable for this test
+ // environment.
+ RootMT() string
+
+ // Principal returns the security principal of this environment.
+ Principal() security.Principal
+
+ // DebugShell drops the user into a debug shell. If there is no
+ // controlling TTY, DebugShell will emit a warning message and take no
+ // futher action.
+ DebugShell()
+
+ // TempFile creates a temporary file. Temporary files will be deleted
+ // by Cleanup.
+ TempFile() *os.File
+}
+
+type TestBinary interface {
+ // Start starts the given binary with the given arguments.
+ Start(args ...string) Invocation
+
+ // Path returns the path to the binary.
+ Path() string
+}
+
+type Invocation interface {
+ Stdin() io.Writer
+ Stdout() io.Reader
+ Stderr() io.Reader
+
+ // Output reads the invocation's stdout until EOF and then returns what
+ // was read as a string.
+ Output() string
+
+ // ErrorOutput reads the invocation's stderr until EOF and then returns
+ // what was read as a string.
+ ErrorOutput() string
+
+ // Wait waits for this invocation to finish. If either stdout or stderr
+ // is non-nil, any remaining unread output from those sources will be
+ // written to the corresponding writer.
+ Wait(stdout, stderr io.Writer)
+}
+
+type integrationTestEnvironment struct {
+ // The testing framework.
+ t *testing.T
+
+ // The shell to use to start commands.
+ shell *modules.Shell
+
+ // The environment's root security principal.
+ principal security.Principal
+
+ // Maps path to TestBinary.
+ builtBinaries map[string]*integrationTestBinary
+
+ mtHandle *modules.Handle
+ mtEndpoint string
+
+ tempFiles []*os.File
+}
+
+type integrationTestBinary struct {
+ // The environment to which this binary belongs.
+ env *integrationTestEnvironment
+
+ // The path to the binary.
+ path string
+
+ // The cleanup function to run when the binary exits.
+ cleanupFunc func()
+}
+
+type integrationTestBinaryInvocation struct {
+ // The environment to which this invocation belongs.
+ env *integrationTestEnvironment
+
+ // The handle to the process that was run when this invocation was started.
+ handle *modules.Handle
+}
+
+func (i *integrationTestBinaryInvocation) Stdin() io.Writer {
+ return (*i.handle).Stdin()
+}
+
+func (i *integrationTestBinaryInvocation) Stdout() io.Reader {
+ return (*i.handle).Stdout()
+}
+
+func readerToString(t *testing.T, r io.Reader) string {
+ buf := bytes.Buffer{}
+ _, err := buf.ReadFrom(r)
+ if err != nil {
+ t.Fatalf("ReadFrom() failed: %v", err)
+ }
+ return buf.String()
+}
+
+func (i *integrationTestBinaryInvocation) Output() string {
+ return readerToString(i.env.t, i.Stdout())
+}
+
+func (i *integrationTestBinaryInvocation) Stderr() io.Reader {
+ return (*i.handle).Stderr()
+}
+
+func (i *integrationTestBinaryInvocation) ErrorOutput() string {
+ return readerToString(i.env.t, i.Stderr())
+}
+
+func (i *integrationTestBinaryInvocation) Wait(stdout, stderr io.Writer) {
+ if err := (*i.handle).Shutdown(stdout, stderr); err != nil {
+ i.env.t.Fatalf("Shutdown() failed: %v", err)
+ }
+}
+
+func (b *integrationTestBinary) cleanup() {
+ binaryDir := path.Dir(b.path)
+ b.env.t.Logf("cleaning up %s", binaryDir)
+ if err := os.RemoveAll(binaryDir); err != nil {
+ b.env.t.Logf("WARNING: RemoveAll(%s) failed (%v)", binaryDir, err)
+ }
+}
+
+func (b *integrationTestBinary) Path() string {
+ return b.path
+}
+
+func (b *integrationTestBinary) Start(args ...string) Invocation {
+ b.env.t.Logf("starting %s %s", b.Path(), strings.Join(args, " "))
+ handle, err := b.env.shell.Start("exec", nil, append([]string{b.Path()}, args...)...)
+ if err != nil {
+ b.env.t.Fatalf("Start(%v, %v) failed: %v", b.Path(), strings.Join(args, ", "), err)
+ }
+ return &integrationTestBinaryInvocation{
+ env: b.env,
+ handle: &handle,
+ }
+}
+
+func (e *integrationTestEnvironment) RootMT() string {
+ return e.mtEndpoint
+}
+
+func (e *integrationTestEnvironment) Principal() security.Principal {
+ return e.principal
+}
+
+func (e *integrationTestEnvironment) Cleanup() {
+ for _, binary := range e.builtBinaries {
+ binary.cleanupFunc()
+ }
+
+ for _, tempFile := range e.tempFiles {
+ e.t.Logf("cleaning up %s", tempFile.Name())
+ if err := tempFile.Close(); err != nil {
+ e.t.Logf("WARNING: Close(%s) failed", tempFile, err)
+ }
+ if err := os.Remove(tempFile.Name()); err != nil {
+ e.t.Logf("WARNING: Remove(%s) failed: %v", tempFile.Name(), err)
+ }
+ }
+
+ if err := e.shell.Cleanup(os.Stdout, os.Stderr); err != nil {
+ e.t.Fatalf("WARNING: could not clean up shell (%v)", err)
+ }
+}
+
+func (e *integrationTestEnvironment) DebugShell() {
+ // Get the current working directory.
+ cwd, err := os.Getwd()
+ if err != nil {
+ e.t.Fatalf("Getwd() failed: %v", err)
+ }
+
+ // Transfer stdin, stdout, and stderr to the new process
+ // and also set target directory for the shell to start in.
+ dev := "/dev/tty"
+ fd, err := syscall.Open(dev, 0, 0)
+ if err != nil {
+ e.t.Logf("WARNING: Open(%v) failed, not going to create a debug shell: %v", dev, err)
+ return
+ }
+ attr := os.ProcAttr{
+ Files: []*os.File{os.NewFile(uintptr(fd), "/dev/tty"), os.Stdout, os.Stderr},
+ Dir: cwd,
+ }
+
+ // Start up a new shell.
+ fmt.Printf(">> Starting a new interactive shell\n")
+ fmt.Printf("Hit CTRL-D to resume the test\n")
+ if len(e.builtBinaries) > 0 {
+ fmt.Println("Built binaries:")
+ for _, value := range e.builtBinaries {
+ fmt.Println(value.Path())
+ }
+ }
+ fmt.Println("Root mounttable endpoint:", e.RootMT())
+
+ shellPath := "/bin/sh"
+ proc, err := os.StartProcess(shellPath, []string{}, &attr)
+ if err != nil {
+ e.t.Fatalf("StartProcess(%v) failed: %v", shellPath, err)
+ }
+
+ // Wait until user exits the shell
+ state, err := proc.Wait()
+ if err != nil {
+ e.t.Fatalf("Wait(%v) failed: %v", shellPath, err)
+ }
+
+ fmt.Printf("<< Exited shell: %s\n", state.String())
+}
+
+func (e *integrationTestEnvironment) BuildGoPkg(binary_path string) TestBinary {
+ e.t.Logf("building %s...", binary_path)
+ if cached_binary := e.builtBinaries[binary_path]; cached_binary != nil {
+ e.t.Logf("using cached binary for %s at %s.", binary_path, cached_binary.Path())
+ return cached_binary
+ }
+ built_path, cleanup, err := BuildPkgs([]string{binary_path})
+ if err != nil {
+ e.t.Fatalf("BuildPkgs() failed: %v", err)
+ return nil
+ }
+ output_path := path.Join(built_path, path.Base(binary_path))
+ e.t.Logf("done building %s, written to %s.", binary_path, output_path)
+ binary := &integrationTestBinary{
+ env: e,
+ path: output_path,
+ cleanupFunc: cleanup,
+ }
+ e.builtBinaries[binary_path] = binary
+ return binary
+}
+
+func (e *integrationTestEnvironment) TempFile() *os.File {
+ f, err := ioutil.TempFile("", "")
+ if err != nil {
+ e.t.Fatalf("TempFile() failed: %v", err)
+ }
+ e.t.Logf("created temporary file at %s", f.Name())
+ e.tempFiles = append(e.tempFiles, f)
+ return f
+}
+
+func NewTestEnvironment(t *testing.T) TestEnvironment {
+ t.Log("creating root principal")
+ principal := tsecurity.NewPrincipal("root")
+ shell, err := modules.NewShell(principal)
+ if err != nil {
+ t.Fatalf("NewShell() failed: %v", err)
+ }
+
+ t.Log("starting root mounttable...")
+ mtHandle, mtEndpoint, err := StartRootMT(shell)
+ if err != nil {
+ t.Fatalf("StartRootMT() failed: %v", err)
+ }
+ t.Logf("mounttable available at %s", mtEndpoint)
+
+ return &integrationTestEnvironment{
+ t: t,
+ principal: principal,
+ builtBinaries: make(map[string]*integrationTestBinary),
+ shell: shell,
+ mtHandle: &mtHandle,
+ mtEndpoint: mtEndpoint,
+ tempFiles: []*os.File{},
+ }
+}
+
// BuildPkgs returns a path to a directory that contains the built
// binaries for the given set of packages and a function that should
// be invoked to clean up the build artifacts. Note that the clients
diff --git a/tools/debug/testdata/integration_test.go b/tools/debug/testdata/integration_test.go
new file mode 100644
index 0000000..86be0c9
--- /dev/null
+++ b/tools/debug/testdata/integration_test.go
@@ -0,0 +1,107 @@
+package testdata
+
+import (
+ "fmt"
+ "os"
+ "path/filepath"
+ "strconv"
+ "strings"
+ "testing"
+
+ "veyron.io/veyron/veyron/lib/modules"
+ "veyron.io/veyron/veyron/lib/testutil/integration"
+
+ _ "veyron.io/veyron/veyron/profiles/static"
+)
+
+func TestHelperProcess(t *testing.T) {
+ modules.DispatchInTest()
+}
+
+func TestDebugGlob(t *testing.T) {
+ env := integration.NewTestEnvironment(t)
+ defer env.Cleanup()
+
+ binary := env.BuildGoPkg("veyron.io/veyron/veyron/tools/debug")
+ inv := binary.Start("glob", env.RootMT()+"/__debug/*")
+
+ var want string
+ for _, entry := range []string{"logs", "pprof", "stats", "vtrace"} {
+ want += env.RootMT() + "/__debug/" + entry + "\n"
+ }
+ if got := inv.Output(); got != want {
+ t.Fatalf("unexpected output, want %s, got %s", want, got)
+ }
+}
+
+func TestDebugGlobLogs(t *testing.T) {
+ env := integration.NewTestEnvironment(t)
+ defer env.Cleanup()
+
+ fileName := filepath.Base(env.TempFile().Name())
+ binary := env.BuildGoPkg("veyron.io/veyron/veyron/tools/debug")
+ output := binary.Start("glob", env.RootMT()+"/__debug/logs/*").Output()
+
+ // The output should contain the filename.
+ want := "/logs/" + fileName
+ if !strings.Contains(output, want) {
+ t.Fatalf("output should contain %s but did not\n%s", want, output)
+ }
+}
+
+func TestReadHostname(t *testing.T) {
+ env := integration.NewTestEnvironment(t)
+ defer env.Cleanup()
+
+ path := env.RootMT() + "/__debug/stats/system/hostname"
+ binary := env.BuildGoPkg("veyron.io/veyron/veyron/tools/debug")
+ got := binary.Start("stats", "read", path).Output()
+ hostname, err := os.Hostname()
+ if err != nil {
+ t.Fatalf("Hostname() failed: %v", err)
+ }
+ if want := path + ": " + hostname + "\n"; got != want {
+ t.Fatalf("unexpected output, want %s, got %s", want, got)
+ }
+}
+
+func TestLogSize(t *testing.T) {
+ env := integration.NewTestEnvironment(t)
+ defer env.Cleanup()
+
+ binary := env.BuildGoPkg("veyron.io/veyron/veyron/tools/debug")
+ f := env.TempFile()
+ testLogData := []byte("This is a test log file")
+ f.Write(testLogData)
+
+ // Check to ensure the file size is accurate
+ str := strings.TrimSpace(binary.Start("logs", "size", env.RootMT()+"/__debug/logs/"+filepath.Base(f.Name())).Output())
+ got, err := strconv.Atoi(str)
+ if err != nil {
+ t.Fatalf("Atoi(\"%q\") failed", str)
+ }
+ want := len(testLogData)
+ if got != want {
+ t.Fatalf("unexpected output, want %d, got %d", got, want)
+ }
+}
+
+func TestStatsRead(t *testing.T) {
+ env := integration.NewTestEnvironment(t)
+ defer env.Cleanup()
+
+ binary := env.BuildGoPkg("veyron.io/veyron/veyron/tools/debug")
+ file := env.TempFile()
+ testLogData := []byte("This is a test log file\n")
+ file.Write(testLogData)
+ logName := filepath.Base(file.Name())
+ runCount := 12
+ for i := 0; i < runCount; i++ {
+ binary.Start("logs", "read", env.RootMT()+"/__debug/logs/"+logName).Wait(nil, nil)
+ }
+ got := binary.Start("stats", "read", env.RootMT()+"/__debug/stats/ipc/server/routing-id/*/methods/ReadLog/latency-ms").Output()
+ want := fmt.Sprintf("Count: %d", runCount)
+ if !strings.Contains(got, want) {
+ t.Fatalf("expected output to contain %s, but did not\n", want, got)
+ }
+}