--- /dev/null
+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")
+ }
+}