OSDN Git Service

add package
[bytom/vapor.git] / vendor / github.com / hashicorp / go-plugin / client_test.go
diff --git a/vendor/github.com/hashicorp/go-plugin/client_test.go b/vendor/github.com/hashicorp/go-plugin/client_test.go
new file mode 100644 (file)
index 0000000..6807bcc
--- /dev/null
@@ -0,0 +1,942 @@
+package plugin
+
+import (
+       "bytes"
+       "crypto/sha256"
+       "io"
+       "io/ioutil"
+       "net"
+       "os"
+       "path/filepath"
+       "strings"
+       "sync"
+       "testing"
+       "time"
+
+       hclog "github.com/hashicorp/go-hclog"
+)
+
+func TestClient(t *testing.T) {
+       process := helperProcess("mock")
+       c := NewClient(&ClientConfig{Cmd: process, HandshakeConfig: testHandshake})
+       defer c.Kill()
+
+       // Test that it parses the proper address
+       addr, err := c.Start()
+       if err != nil {
+               t.Fatalf("err should be nil, got %s", err)
+       }
+
+       if addr.Network() != "tcp" {
+               t.Fatalf("bad: %#v", addr)
+       }
+
+       if addr.String() != ":1234" {
+               t.Fatalf("bad: %#v", addr)
+       }
+
+       // Test that it exits properly if killed
+       c.Kill()
+
+       if process.ProcessState == nil {
+               t.Fatal("should have process state")
+       }
+
+       // Test that it knows it is exited
+       if !c.Exited() {
+               t.Fatal("should say client has exited")
+       }
+}
+
+// This tests a bug where Kill would start
+func TestClient_killStart(t *testing.T) {
+       // Create a temporary dir to store the result file
+       td, err := ioutil.TempDir("", "plugin")
+       if err != nil {
+               t.Fatalf("err: %s", err)
+       }
+       defer os.RemoveAll(td)
+
+       // Start the client
+       path := filepath.Join(td, "booted")
+       process := helperProcess("bad-version", path)
+       c := NewClient(&ClientConfig{Cmd: process, HandshakeConfig: testHandshake})
+       defer c.Kill()
+
+       // Verify our path doesn't exist
+       if _, err := os.Stat(path); err == nil || !os.IsNotExist(err) {
+               t.Fatalf("bad: %s", err)
+       }
+
+       // Test that it parses the proper address
+       if _, err := c.Start(); err == nil {
+               t.Fatal("expected error")
+       }
+
+       // Verify we started
+       if _, err := os.Stat(path); err != nil {
+               t.Fatalf("bad: %s", err)
+       }
+       if err := os.Remove(path); err != nil {
+               t.Fatalf("bad: %s", err)
+       }
+
+       // Test that Kill does nothing really
+       c.Kill()
+
+       // Test that it knows it is exited
+       if !c.Exited() {
+               t.Fatal("should say client has exited")
+       }
+
+       if process.ProcessState == nil {
+               t.Fatal("should have no process state")
+       }
+
+       // Verify our path doesn't exist
+       if _, err := os.Stat(path); err == nil || !os.IsNotExist(err) {
+               t.Fatalf("bad: %s", err)
+       }
+}
+
+func TestClient_testCleanup(t *testing.T) {
+       // Create a temporary dir to store the result file
+       td, err := ioutil.TempDir("", "plugin")
+       if err != nil {
+               t.Fatalf("err: %s", err)
+       }
+       defer os.RemoveAll(td)
+
+       // Create a path that the helper process will write on cleanup
+       path := filepath.Join(td, "output")
+
+       // Test the cleanup
+       process := helperProcess("cleanup", path)
+       c := NewClient(&ClientConfig{
+               Cmd:             process,
+               HandshakeConfig: testHandshake,
+               Plugins:         testPluginMap,
+       })
+
+       // Grab the client so the process starts
+       if _, err := c.Client(); err != nil {
+               c.Kill()
+               t.Fatalf("err: %s", err)
+       }
+
+       // Kill it gracefully
+       c.Kill()
+
+       // Test for the file
+       if _, err := os.Stat(path); err != nil {
+               t.Fatalf("err: %s", err)
+       }
+}
+
+func TestClient_testInterface(t *testing.T) {
+       process := helperProcess("test-interface")
+       c := NewClient(&ClientConfig{
+               Cmd:             process,
+               HandshakeConfig: testHandshake,
+               Plugins:         testPluginMap,
+       })
+       defer c.Kill()
+
+       // Grab the RPC client
+       client, err := c.Client()
+       if err != nil {
+               t.Fatalf("err should be nil, got %s", err)
+       }
+
+       // Grab the impl
+       raw, err := client.Dispense("test")
+       if err != nil {
+               t.Fatalf("err should be nil, got %s", err)
+       }
+
+       impl, ok := raw.(testInterface)
+       if !ok {
+               t.Fatalf("bad: %#v", raw)
+       }
+
+       result := impl.Double(21)
+       if result != 42 {
+               t.Fatalf("bad: %#v", result)
+       }
+
+       // Kill it
+       c.Kill()
+
+       // Test that it knows it is exited
+       if !c.Exited() {
+               t.Fatal("should say client has exited")
+       }
+}
+
+func TestClient_grpc_servercrash(t *testing.T) {
+       process := helperProcess("test-grpc")
+       c := NewClient(&ClientConfig{
+               Cmd:              process,
+               HandshakeConfig:  testHandshake,
+               Plugins:          testPluginMap,
+               AllowedProtocols: []Protocol{ProtocolGRPC},
+       })
+       defer c.Kill()
+
+       if _, err := c.Start(); err != nil {
+               t.Fatalf("err: %s", err)
+       }
+
+       if v := c.Protocol(); v != ProtocolGRPC {
+               t.Fatalf("bad: %s", v)
+       }
+
+       // Grab the RPC client
+       client, err := c.Client()
+       if err != nil {
+               t.Fatalf("err should be nil, got %s", err)
+       }
+
+       // Grab the impl
+       raw, err := client.Dispense("test")
+       if err != nil {
+               t.Fatalf("err should be nil, got %s", err)
+       }
+
+       _, ok := raw.(testInterface)
+       if !ok {
+               t.Fatalf("bad: %#v", raw)
+       }
+
+       c.process.Kill()
+
+       select {
+       case <-c.doneCtx.Done():
+       case <-time.After(time.Second * 2):
+               t.Fatal("Context was not closed")
+       }
+}
+
+func TestClient_grpc(t *testing.T) {
+       process := helperProcess("test-grpc")
+       c := NewClient(&ClientConfig{
+               Cmd:              process,
+               HandshakeConfig:  testHandshake,
+               Plugins:          testPluginMap,
+               AllowedProtocols: []Protocol{ProtocolGRPC},
+       })
+       defer c.Kill()
+
+       if _, err := c.Start(); err != nil {
+               t.Fatalf("err: %s", err)
+       }
+
+       if v := c.Protocol(); v != ProtocolGRPC {
+               t.Fatalf("bad: %s", v)
+       }
+
+       // Grab the RPC client
+       client, err := c.Client()
+       if err != nil {
+               t.Fatalf("err should be nil, got %s", err)
+       }
+
+       // Grab the impl
+       raw, err := client.Dispense("test")
+       if err != nil {
+               t.Fatalf("err should be nil, got %s", err)
+       }
+
+       impl, ok := raw.(testInterface)
+       if !ok {
+               t.Fatalf("bad: %#v", raw)
+       }
+
+       result := impl.Double(21)
+       if result != 42 {
+               t.Fatalf("bad: %#v", result)
+       }
+
+       // Kill it
+       c.Kill()
+
+       // Test that it knows it is exited
+       if !c.Exited() {
+               t.Fatal("should say client has exited")
+       }
+}
+
+func TestClient_grpcNotAllowed(t *testing.T) {
+       process := helperProcess("test-grpc")
+       c := NewClient(&ClientConfig{
+               Cmd:             process,
+               HandshakeConfig: testHandshake,
+               Plugins:         testPluginMap,
+       })
+       defer c.Kill()
+
+       if _, err := c.Start(); err == nil {
+               t.Fatal("should error")
+       }
+}
+
+func TestClient_cmdAndReattach(t *testing.T) {
+       config := &ClientConfig{
+               Cmd:      helperProcess("start-timeout"),
+               Reattach: &ReattachConfig{},
+       }
+
+       c := NewClient(config)
+       defer c.Kill()
+
+       _, err := c.Start()
+       if err == nil {
+               t.Fatal("err should not be nil")
+       }
+}
+
+func TestClient_reattach(t *testing.T) {
+       process := helperProcess("test-interface")
+       c := NewClient(&ClientConfig{
+               Cmd:             process,
+               HandshakeConfig: testHandshake,
+               Plugins:         testPluginMap,
+       })
+       defer c.Kill()
+
+       // Grab the RPC client
+       _, err := c.Client()
+       if err != nil {
+               t.Fatalf("err should be nil, got %s", err)
+       }
+
+       // Get the reattach configuration
+       reattach := c.ReattachConfig()
+
+       // Create a new client
+       c = NewClient(&ClientConfig{
+               Reattach:        reattach,
+               HandshakeConfig: testHandshake,
+               Plugins:         testPluginMap,
+       })
+
+       // Grab the RPC client
+       client, err := c.Client()
+       if err != nil {
+               t.Fatalf("err should be nil, got %s", err)
+       }
+
+       // Grab the impl
+       raw, err := client.Dispense("test")
+       if err != nil {
+               t.Fatalf("err should be nil, got %s", err)
+       }
+
+       impl, ok := raw.(testInterface)
+       if !ok {
+               t.Fatalf("bad: %#v", raw)
+       }
+
+       result := impl.Double(21)
+       if result != 42 {
+               t.Fatalf("bad: %#v", result)
+       }
+
+       // Kill it
+       c.Kill()
+
+       // Test that it knows it is exited
+       if !c.Exited() {
+               t.Fatal("should say client has exited")
+       }
+}
+
+func TestClient_reattachNoProtocol(t *testing.T) {
+       process := helperProcess("test-interface")
+       c := NewClient(&ClientConfig{
+               Cmd:             process,
+               HandshakeConfig: testHandshake,
+               Plugins:         testPluginMap,
+       })
+       defer c.Kill()
+
+       // Grab the RPC client
+       _, err := c.Client()
+       if err != nil {
+               t.Fatalf("err should be nil, got %s", err)
+       }
+
+       // Get the reattach configuration
+       reattach := c.ReattachConfig()
+       reattach.Protocol = ""
+
+       // Create a new client
+       c = NewClient(&ClientConfig{
+               Reattach:        reattach,
+               HandshakeConfig: testHandshake,
+               Plugins:         testPluginMap,
+       })
+
+       // Grab the RPC client
+       client, err := c.Client()
+       if err != nil {
+               t.Fatalf("err should be nil, got %s", err)
+       }
+
+       // Grab the impl
+       raw, err := client.Dispense("test")
+       if err != nil {
+               t.Fatalf("err should be nil, got %s", err)
+       }
+
+       impl, ok := raw.(testInterface)
+       if !ok {
+               t.Fatalf("bad: %#v", raw)
+       }
+
+       result := impl.Double(21)
+       if result != 42 {
+               t.Fatalf("bad: %#v", result)
+       }
+
+       // Kill it
+       c.Kill()
+
+       // Test that it knows it is exited
+       if !c.Exited() {
+               t.Fatal("should say client has exited")
+       }
+}
+
+func TestClient_reattachGRPC(t *testing.T) {
+       process := helperProcess("test-grpc")
+       c := NewClient(&ClientConfig{
+               Cmd:              process,
+               HandshakeConfig:  testHandshake,
+               Plugins:          testPluginMap,
+               AllowedProtocols: []Protocol{ProtocolGRPC},
+       })
+       defer c.Kill()
+
+       // Grab the RPC client
+       _, err := c.Client()
+       if err != nil {
+               t.Fatalf("err should be nil, got %s", err)
+       }
+
+       // Get the reattach configuration
+       reattach := c.ReattachConfig()
+
+       // Create a new client
+       c = NewClient(&ClientConfig{
+               Reattach:         reattach,
+               HandshakeConfig:  testHandshake,
+               Plugins:          testPluginMap,
+               AllowedProtocols: []Protocol{ProtocolGRPC},
+       })
+
+       // Grab the RPC client
+       client, err := c.Client()
+       if err != nil {
+               t.Fatalf("err should be nil, got %s", err)
+       }
+
+       // Grab the impl
+       raw, err := client.Dispense("test")
+       if err != nil {
+               t.Fatalf("err should be nil, got %s", err)
+       }
+
+       impl, ok := raw.(testInterface)
+       if !ok {
+               t.Fatalf("bad: %#v", raw)
+       }
+
+       result := impl.Double(21)
+       if result != 42 {
+               t.Fatalf("bad: %#v", result)
+       }
+
+       // Kill it
+       c.Kill()
+
+       // Test that it knows it is exited
+       if !c.Exited() {
+               t.Fatal("should say client has exited")
+       }
+}
+
+func TestClient_reattachNotFound(t *testing.T) {
+       // Find a bad pid
+       var pid int = 5000
+       for i := pid; i < 32000; i++ {
+               if _, err := os.FindProcess(i); err != nil {
+                       pid = i
+                       break
+               }
+       }
+
+       // Addr that won't work
+       l, err := net.Listen("tcp", "127.0.0.1:0")
+       if err != nil {
+               t.Fatalf("err: %s", err)
+       }
+       addr := l.Addr()
+       l.Close()
+
+       // Reattach
+       c := NewClient(&ClientConfig{
+               Reattach: &ReattachConfig{
+                       Addr: addr,
+                       Pid:  pid,
+               },
+               HandshakeConfig: testHandshake,
+               Plugins:         testPluginMap,
+       })
+
+       // Start shouldn't error
+       if _, err := c.Start(); err == nil {
+               t.Fatal("should error")
+       } else if err != ErrProcessNotFound {
+               t.Fatalf("err: %s", err)
+       }
+}
+
+func TestClientStart_badVersion(t *testing.T) {
+       config := &ClientConfig{
+               Cmd:             helperProcess("bad-version"),
+               StartTimeout:    50 * time.Millisecond,
+               HandshakeConfig: testHandshake,
+       }
+
+       c := NewClient(config)
+       defer c.Kill()
+
+       _, err := c.Start()
+       if err == nil {
+               t.Fatal("err should not be nil")
+       }
+}
+
+func TestClient_Start_Timeout(t *testing.T) {
+       config := &ClientConfig{
+               Cmd:             helperProcess("start-timeout"),
+               StartTimeout:    50 * time.Millisecond,
+               HandshakeConfig: testHandshake,
+       }
+
+       c := NewClient(config)
+       defer c.Kill()
+
+       _, err := c.Start()
+       if err == nil {
+               t.Fatal("err should not be nil")
+       }
+}
+
+func TestClient_Stderr(t *testing.T) {
+       stderr := new(bytes.Buffer)
+       process := helperProcess("stderr")
+       c := NewClient(&ClientConfig{
+               Cmd:             process,
+               Stderr:          stderr,
+               HandshakeConfig: testHandshake,
+       })
+       defer c.Kill()
+
+       if _, err := c.Start(); err != nil {
+               t.Fatalf("err: %s", err)
+       }
+
+       for !c.Exited() {
+               time.Sleep(10 * time.Millisecond)
+       }
+
+       if !strings.Contains(stderr.String(), "HELLO\n") {
+               t.Fatalf("bad log data: '%s'", stderr.String())
+       }
+
+       if !strings.Contains(stderr.String(), "WORLD\n") {
+               t.Fatalf("bad log data: '%s'", stderr.String())
+       }
+}
+
+func TestClient_StderrJSON(t *testing.T) {
+       stderr := new(bytes.Buffer)
+       process := helperProcess("stderr-json")
+       c := NewClient(&ClientConfig{
+               Cmd:             process,
+               Stderr:          stderr,
+               HandshakeConfig: testHandshake,
+       })
+       defer c.Kill()
+
+       if _, err := c.Start(); err != nil {
+               t.Fatalf("err: %s", err)
+       }
+
+       for !c.Exited() {
+               time.Sleep(10 * time.Millisecond)
+       }
+
+       if !strings.Contains(stderr.String(), "[\"HELLO\"]\n") {
+               t.Fatalf("bad log data: '%s'", stderr.String())
+       }
+
+       if !strings.Contains(stderr.String(), "12345\n") {
+               t.Fatalf("bad log data: '%s'", stderr.String())
+       }
+}
+
+func TestClient_Stdin(t *testing.T) {
+       // Overwrite stdin for this test with a temporary file
+       tf, err := ioutil.TempFile("", "terraform")
+       if err != nil {
+               t.Fatalf("err: %s", err)
+       }
+       defer os.Remove(tf.Name())
+       defer tf.Close()
+
+       if _, err = tf.WriteString("hello"); err != nil {
+               t.Fatalf("error: %s", err)
+       }
+
+       if err = tf.Sync(); err != nil {
+               t.Fatalf("error: %s", err)
+       }
+
+       if _, err = tf.Seek(0, 0); err != nil {
+               t.Fatalf("error: %s", err)
+       }
+
+       oldStdin := os.Stdin
+       defer func() { os.Stdin = oldStdin }()
+       os.Stdin = tf
+
+       process := helperProcess("stdin")
+       c := NewClient(&ClientConfig{Cmd: process, HandshakeConfig: testHandshake})
+       defer c.Kill()
+
+       _, err = c.Start()
+       if err != nil {
+               t.Fatalf("error: %s", err)
+       }
+
+       for {
+               if c.Exited() {
+                       break
+               }
+
+               time.Sleep(50 * time.Millisecond)
+       }
+
+       if !process.ProcessState.Success() {
+               t.Fatal("process didn't exit cleanly")
+       }
+}
+
+func TestClient_SecureConfig(t *testing.T) {
+       // Test failure case
+       secureConfig := &SecureConfig{
+               Checksum: []byte{'1'},
+               Hash:     sha256.New(),
+       }
+       process := helperProcess("test-interface")
+       c := NewClient(&ClientConfig{
+               Cmd:             process,
+               HandshakeConfig: testHandshake,
+               Plugins:         testPluginMap,
+               SecureConfig:    secureConfig,
+       })
+
+       // Grab the RPC client, should error
+       _, err := c.Client()
+       c.Kill()
+       if err != ErrChecksumsDoNotMatch {
+               t.Fatalf("err should be %s, got %s", ErrChecksumsDoNotMatch, err)
+       }
+
+       // Get the checksum of the executable
+       file, err := os.Open(os.Args[0])
+       if err != nil {
+               t.Fatal(err)
+       }
+       defer file.Close()
+
+       hash := sha256.New()
+
+       _, err = io.Copy(hash, file)
+       if err != nil {
+               t.Fatal(err)
+       }
+
+       sum := hash.Sum(nil)
+
+       secureConfig = &SecureConfig{
+               Checksum: sum,
+               Hash:     sha256.New(),
+       }
+
+       c = NewClient(&ClientConfig{
+               Cmd:             process,
+               HandshakeConfig: testHandshake,
+               Plugins:         testPluginMap,
+               SecureConfig:    secureConfig,
+       })
+       defer c.Kill()
+
+       // Grab the RPC client
+       _, err = c.Client()
+       if err != nil {
+               t.Fatalf("err should be nil, got %s", err)
+       }
+}
+
+func TestClient_TLS(t *testing.T) {
+       // Test failure case
+       process := helperProcess("test-interface-tls")
+       cBad := NewClient(&ClientConfig{
+               Cmd:             process,
+               HandshakeConfig: testHandshake,
+               Plugins:         testPluginMap,
+       })
+       defer cBad.Kill()
+
+       // Grab the RPC client
+       clientBad, err := cBad.Client()
+       if err != nil {
+               t.Fatalf("err should be nil, got %s", err)
+       }
+
+       // Grab the impl
+       raw, err := clientBad.Dispense("test")
+       if err == nil {
+               t.Fatal("expected error, got nil")
+       }
+
+       cBad.Kill()
+
+       // Add TLS config to client
+       tlsConfig, err := helperTLSProvider()
+       if err != nil {
+               t.Fatalf("err should be nil, got %s", err)
+       }
+
+       process = helperProcess("test-interface-tls")
+       c := NewClient(&ClientConfig{
+               Cmd:             process,
+               HandshakeConfig: testHandshake,
+               Plugins:         testPluginMap,
+               TLSConfig:       tlsConfig,
+       })
+       defer c.Kill()
+
+       // Grab the RPC client
+       client, err := c.Client()
+       if err != nil {
+               t.Fatalf("err should be nil, got %s", err)
+       }
+
+       // Grab the impl
+       raw, err = client.Dispense("test")
+       if err != nil {
+               t.Fatalf("err should be nil, got %s", err)
+       }
+
+       impl, ok := raw.(testInterface)
+       if !ok {
+               t.Fatalf("bad: %#v", raw)
+       }
+
+       result := impl.Double(21)
+       if result != 42 {
+               t.Fatalf("bad: %#v", result)
+       }
+
+       // Kill it
+       c.Kill()
+
+       // Test that it knows it is exited
+       if !c.Exited() {
+               t.Fatal("should say client has exited")
+       }
+}
+
+func TestClient_TLS_grpc(t *testing.T) {
+       // Add TLS config to client
+       tlsConfig, err := helperTLSProvider()
+       if err != nil {
+               t.Fatalf("err should be nil, got %s", err)
+       }
+
+       process := helperProcess("test-grpc-tls")
+       c := NewClient(&ClientConfig{
+               Cmd:              process,
+               HandshakeConfig:  testHandshake,
+               Plugins:          testPluginMap,
+               TLSConfig:        tlsConfig,
+               AllowedProtocols: []Protocol{ProtocolGRPC},
+       })
+       defer c.Kill()
+
+       // Grab the RPC client
+       client, err := c.Client()
+       if err != nil {
+               t.Fatalf("err should be nil, got %s", err)
+       }
+
+       // Grab the impl
+       raw, err := client.Dispense("test")
+       if err != nil {
+               t.Fatalf("err should be nil, got %s", err)
+       }
+
+       impl, ok := raw.(testInterface)
+       if !ok {
+               t.Fatalf("bad: %#v", raw)
+       }
+
+       result := impl.Double(21)
+       if result != 42 {
+               t.Fatalf("bad: %#v", result)
+       }
+
+       // Kill it
+       c.Kill()
+
+       // Test that it knows it is exited
+       if !c.Exited() {
+               t.Fatal("should say client has exited")
+       }
+}
+
+func TestClient_secureConfigAndReattach(t *testing.T) {
+       config := &ClientConfig{
+               SecureConfig: &SecureConfig{},
+               Reattach:     &ReattachConfig{},
+       }
+
+       c := NewClient(config)
+       defer c.Kill()
+
+       _, err := c.Start()
+       if err != ErrSecureConfigAndReattach {
+               t.Fatalf("err should not be %s, got %s", ErrSecureConfigAndReattach, err)
+       }
+}
+
+func TestClient_ping(t *testing.T) {
+       process := helperProcess("test-interface")
+       c := NewClient(&ClientConfig{
+               Cmd:             process,
+               HandshakeConfig: testHandshake,
+               Plugins:         testPluginMap,
+       })
+       defer c.Kill()
+
+       // Get the client
+       client, err := c.Client()
+       if err != nil {
+               t.Fatalf("err: %s", err)
+       }
+
+       // Ping, should work
+       if err := client.Ping(); err != nil {
+               t.Fatalf("err: %s", err)
+       }
+
+       // Kill it
+       c.Kill()
+       if err := client.Ping(); err == nil {
+               t.Fatal("should error")
+       }
+}
+
+func TestClient_logger(t *testing.T) {
+       t.Run("net/rpc", func(t *testing.T) { testClient_logger(t, "netrpc") })
+       t.Run("grpc", func(t *testing.T) { testClient_logger(t, "grpc") })
+}
+
+func testClient_logger(t *testing.T, proto string) {
+       var buffer bytes.Buffer
+       mutex := new(sync.Mutex)
+       stderr := io.MultiWriter(os.Stderr, &buffer)
+       // Custom hclog.Logger
+       clientLogger := hclog.New(&hclog.LoggerOptions{
+               Name:   "test-logger",
+               Level:  hclog.Trace,
+               Output: stderr,
+               Mutex:  mutex,
+       })
+
+       process := helperProcess("test-interface-logger-" + proto)
+       c := NewClient(&ClientConfig{
+               Cmd:              process,
+               HandshakeConfig:  testHandshake,
+               Plugins:          testPluginMap,
+               Logger:           clientLogger,
+               AllowedProtocols: []Protocol{ProtocolNetRPC, ProtocolGRPC},
+       })
+       defer c.Kill()
+
+       // Grab the RPC client
+       client, err := c.Client()
+       if err != nil {
+               t.Fatalf("err should be nil, got %s", err)
+       }
+
+       // Grab the impl
+       raw, err := client.Dispense("test")
+       if err != nil {
+               t.Fatalf("err should be nil, got %s", err)
+       }
+
+       impl, ok := raw.(testInterface)
+       if !ok {
+               t.Fatalf("bad: %#v", raw)
+       }
+
+       {
+               // Discard everything else, and capture the output we care about
+               mutex.Lock()
+               buffer.Reset()
+               mutex.Unlock()
+               impl.PrintKV("foo", "bar")
+               time.Sleep(100 * time.Millisecond)
+               mutex.Lock()
+               line, err := buffer.ReadString('\n')
+               mutex.Unlock()
+               if err != nil {
+                       t.Fatal(err)
+               }
+               if !strings.Contains(line, "foo=bar") {
+                       t.Fatalf("bad: %q", line)
+               }
+       }
+
+       {
+               // Try an integer type
+               mutex.Lock()
+               buffer.Reset()
+               mutex.Unlock()
+               impl.PrintKV("foo", 12)
+               time.Sleep(100 * time.Millisecond)
+               mutex.Lock()
+               line, err := buffer.ReadString('\n')
+               mutex.Unlock()
+               if err != nil {
+                       t.Fatal(err)
+               }
+               if !strings.Contains(line, "foo=12") {
+                       t.Fatalf("bad: %q", line)
+               }
+       }
+
+       // Kill it
+       c.Kill()
+
+       // Test that it knows it is exited
+       if !c.Exited() {
+               t.Fatal("should say client has exited")
+       }
+}