From 58c05f87d7bf73353aa77ae4cacf50af1dc26d4f Mon Sep 17 00:00:00 2001
From: Michelle Noorali <michelle@deis.com>
Date: Wed, 25 Jan 2017 15:22:48 -0500
Subject: [PATCH] feat(*): stream helm test messages to client

---
 .../{test_result.proto => test_run.proto}     |   5 +-
 _proto/hapi/release/test_suite.proto          |  11 +-
 _proto/hapi/services/tiller.proto             |   5 +-
 cmd/helm/release_testing.go                   |  26 ++---
 pkg/helm/client.go                            |  42 +++++--
 pkg/helm/interface.go                         |   2 +-
 pkg/proto/hapi/release/hook.pb.go             |   4 +-
 pkg/proto/hapi/release/test_result.pb.go      |  85 --------------
 pkg/proto/hapi/release/test_run.pb.go         |  94 +++++++++++++++
 pkg/proto/hapi/release/test_suite.pb.go       |  48 +++++---
 pkg/proto/hapi/services/tiller.pb.go          |  87 ++++++++------
 pkg/tiller/environment/environment.go         |   5 +
 pkg/tiller/environment/environment_test.go    |   6 +
 pkg/tiller/release_server.go                  |  20 ++--
 pkg/tiller/release_testing.go                 | 110 +++++++++++++-----
 15 files changed, 342 insertions(+), 208 deletions(-)
 rename _proto/hapi/release/{test_result.proto => test_run.proto} (88%)
 delete mode 100644 pkg/proto/hapi/release/test_result.pb.go
 create mode 100644 pkg/proto/hapi/release/test_run.pb.go

diff --git a/_proto/hapi/release/test_result.proto b/_proto/hapi/release/test_run.proto
similarity index 88%
rename from _proto/hapi/release/test_result.proto
rename to _proto/hapi/release/test_run.proto
index 7b73e5d6d..a441e729f 100644
--- a/_proto/hapi/release/test_result.proto
+++ b/_proto/hapi/release/test_run.proto
@@ -21,7 +21,7 @@ import "google/protobuf/timestamp.proto";
 
 option go_package = "release";
 
-message TestResult {
+message TestRun {
     enum Status {
         UNKNOWN = 0;
         SUCCESS = 1;
@@ -31,5 +31,6 @@ message TestResult {
     string name = 1;
     Status status = 2;
     string info = 3;
-    google.protobuf.Timestamp last_run = 4;
+    google.protobuf.Timestamp started_at = 4;
+    google.protobuf.Timestamp completed_at = 5;
 }
diff --git a/_proto/hapi/release/test_suite.proto b/_proto/hapi/release/test_suite.proto
index 1ff5eca07..2f6feb08c 100644
--- a/_proto/hapi/release/test_suite.proto
+++ b/_proto/hapi/release/test_suite.proto
@@ -17,15 +17,18 @@ syntax = "proto3";
 package hapi.release;
 
 import "google/protobuf/timestamp.proto";
-import "hapi/release/test_result.proto";
+import "hapi/release/test_run.proto";
 
 option go_package = "release";
 
 // TestSuite comprises of the last run of the pre-defined test suite of a release version
 message TestSuite {
-        // LastRun indicates the date/time this test was last run.
-        google.protobuf.Timestamp last_run = 1;
+        // StartedAt indicates the date/time this test suite was kicked off
+        google.protobuf.Timestamp started_at = 1;
+
+        // CompletedAt indicates the date/time this test suite was completed
+        google.protobuf.Timestamp completed_at = 2;
 
         // Results are the results of each segment of the test
-        repeated hapi.release.TestResult results = 2;
+        repeated hapi.release.TestRun results = 3;
 }
diff --git a/_proto/hapi/services/tiller.proto b/_proto/hapi/services/tiller.proto
index df663acc1..572e39be0 100644
--- a/_proto/hapi/services/tiller.proto
+++ b/_proto/hapi/services/tiller.proto
@@ -22,7 +22,6 @@ import "hapi/release/release.proto";
 import "hapi/release/info.proto";
 import "hapi/release/status.proto";
 import "hapi/version/version.proto";
-import "hapi/release/test_suite.proto";
 
 option go_package = "services";
 
@@ -82,7 +81,7 @@ service ReleaseService {
 
     //TODO: move this to a test release service or rename to RunReleaseTest
     // TestRelease runs the tests for a given release
-    rpc RunReleaseTest(TestReleaseRequest) returns (TestReleaseResponse) {
+    rpc RunReleaseTest(TestReleaseRequest) returns (stream TestReleaseResponse) {
     }
 }
 
@@ -322,5 +321,5 @@ message TestReleaseRequest {
 // TestReleaseResponse
 message TestReleaseResponse {
 	// TODO: change to repeated hapi.release.Release.Test results = 1; (for stream)
-	hapi.release.TestSuite result = 1;
+	string msg = 1;
 }
diff --git a/cmd/helm/release_testing.go b/cmd/helm/release_testing.go
index 796198bf8..e4c8e2c44 100644
--- a/cmd/helm/release_testing.go
+++ b/cmd/helm/release_testing.go
@@ -20,11 +20,9 @@ import (
 	"fmt"
 	"io"
 
-	"github.com/gosuri/uitable"
 	"github.com/spf13/cobra"
 
 	"k8s.io/helm/pkg/helm"
-	//"k8s.io/helm/pkg/proto/hapi/release"
 )
 
 const releaseTestDesc = `
@@ -69,20 +67,20 @@ func newReleaseTestCmd(c helm.Interface, out io.Writer) *cobra.Command {
 	return cmd
 }
 
-func (t *releaseTestCmd) run() error {
-	res, err := t.client.ReleaseTest(t.name, helm.ReleaseTestTimeout(t.timeout))
-	if err != nil {
-		return prettyError(err)
-	}
+func (t *releaseTestCmd) run() (err error) {
+	c, errc := t.client.RunReleaseTest(t.name, helm.ReleaseTestTimeout(t.timeout))
 
-	table := uitable.New()
-	table.MaxColWidth = 50
-	table.AddRow("NAME", "Result", "Info")
-	//TODO: change Result to Suite
-	for _, r := range res.Result.Results {
-		table.AddRow(r.Name, r.Status, r.Info)
+	for {
+		select {
+		case err := <-errc:
+			return prettyError(err)
+		case res, ok := <-c:
+			if !ok {
+				break
+			}
+			fmt.Fprintf(t.out, res.Msg+"\n")
+		}
 	}
 
-	fmt.Fprintln(t.out, table.String()) //TODO: or no tests found
 	return nil
 }
diff --git a/pkg/helm/client.go b/pkg/helm/client.go
index 489d78ba9..771974bb7 100644
--- a/pkg/helm/client.go
+++ b/pkg/helm/client.go
@@ -17,6 +17,8 @@ limitations under the License.
 package helm // import "k8s.io/helm/pkg/helm"
 
 import (
+	"io"
+
 	"golang.org/x/net/context"
 	"google.golang.org/grpc"
 
@@ -244,8 +246,8 @@ func (h *Client) ReleaseHistory(rlsName string, opts ...HistoryOption) (*rls.Get
 	return h.history(ctx, req)
 }
 
-// ReleaseTest executes a pre-defined test on a release
-func (h *Client) ReleaseTest(rlsName string, opts ...ReleaseTestOption) (*rls.TestReleaseResponse, error) {
+//ReleaseTest executes a pre-defined test on a release
+func (h *Client) RunReleaseTest(rlsName string, opts ...ReleaseTestOption) (<-chan *rls.TestReleaseResponse, <-chan error) {
 	for _, opt := range opts {
 		opt(&h.opts)
 	}
@@ -371,13 +373,39 @@ func (h *Client) history(ctx context.Context, req *rls.GetHistoryRequest) (*rls.
 }
 
 // Executes tiller.TestRelease RPC.
-func (h *Client) test(ctx context.Context, req *rls.TestReleaseRequest) (*rls.TestReleaseResponse, error) {
+func (h *Client) test(ctx context.Context, req *rls.TestReleaseRequest) (<-chan *rls.TestReleaseResponse, <-chan error) {
+	errc := make(chan error, 1)
 	c, err := grpc.Dial(h.opts.host, grpc.WithInsecure())
 	if err != nil {
-		return nil, err
+		errc <- err
+		return nil, errc
 	}
-	defer c.Close()
 
-	rlc := rls.NewReleaseServiceClient(c)
-	return rlc.RunReleaseTest(ctx, req)
+	ch := make(chan *rls.TestReleaseResponse, 1)
+	go func() {
+		defer close(errc)
+		defer close(ch)
+		defer c.Close()
+
+		rlc := rls.NewReleaseServiceClient(c)
+		s, err := rlc.RunReleaseTest(ctx, req)
+		if err != nil {
+			errc <- err
+			return
+		}
+
+		for {
+			msg, err := s.Recv()
+			if err == io.EOF {
+				return
+			}
+			if err != nil {
+				errc <- err
+				return
+			}
+			ch <- msg
+		}
+	}()
+
+	return ch, errc
 }
diff --git a/pkg/helm/interface.go b/pkg/helm/interface.go
index 84af3aaab..bff110b34 100644
--- a/pkg/helm/interface.go
+++ b/pkg/helm/interface.go
@@ -34,5 +34,5 @@ type Interface interface {
 	ReleaseContent(rlsName string, opts ...ContentOption) (*rls.GetReleaseContentResponse, error)
 	ReleaseHistory(rlsName string, opts ...HistoryOption) (*rls.GetHistoryResponse, error)
 	GetVersion(opts ...VersionOption) (*rls.GetVersionResponse, error)
-	ReleaseTest(rlsName string, opts ...ReleaseTestOption) (*rls.TestReleaseResponse, error)
+	RunReleaseTest(rlsName string, opts ...ReleaseTestOption) (<-chan *rls.TestReleaseResponse, <-chan error)
 }
diff --git a/pkg/proto/hapi/release/hook.pb.go b/pkg/proto/hapi/release/hook.pb.go
index 45b0533f2..c90e0b59e 100644
--- a/pkg/proto/hapi/release/hook.pb.go
+++ b/pkg/proto/hapi/release/hook.pb.go
@@ -10,7 +10,7 @@ It is generated from these files:
 	hapi/release/info.proto
 	hapi/release/release.proto
 	hapi/release/status.proto
-	hapi/release/test_result.proto
+	hapi/release/test_run.proto
 	hapi/release/test_suite.proto
 
 It has these top-level messages:
@@ -18,7 +18,7 @@ It has these top-level messages:
 	Info
 	Release
 	Status
-	TestResult
+	TestRun
 	TestSuite
 */
 package release
diff --git a/pkg/proto/hapi/release/test_result.pb.go b/pkg/proto/hapi/release/test_result.pb.go
deleted file mode 100644
index 691b66abb..000000000
--- a/pkg/proto/hapi/release/test_result.pb.go
+++ /dev/null
@@ -1,85 +0,0 @@
-// Code generated by protoc-gen-go.
-// source: hapi/release/test_result.proto
-// DO NOT EDIT!
-
-package release
-
-import proto "github.com/golang/protobuf/proto"
-import fmt "fmt"
-import math "math"
-import google_protobuf "github.com/golang/protobuf/ptypes/timestamp"
-
-// Reference imports to suppress errors if they are not otherwise used.
-var _ = proto.Marshal
-var _ = fmt.Errorf
-var _ = math.Inf
-
-type TestResult_Status int32
-
-const (
-	TestResult_UNKNOWN TestResult_Status = 0
-	TestResult_SUCCESS TestResult_Status = 1
-	TestResult_FAILURE TestResult_Status = 2
-)
-
-var TestResult_Status_name = map[int32]string{
-	0: "UNKNOWN",
-	1: "SUCCESS",
-	2: "FAILURE",
-}
-var TestResult_Status_value = map[string]int32{
-	"UNKNOWN": 0,
-	"SUCCESS": 1,
-	"FAILURE": 2,
-}
-
-func (x TestResult_Status) String() string {
-	return proto.EnumName(TestResult_Status_name, int32(x))
-}
-func (TestResult_Status) EnumDescriptor() ([]byte, []int) { return fileDescriptor4, []int{0, 0} }
-
-type TestResult struct {
-	Name    string                     `protobuf:"bytes,1,opt,name=name" json:"name,omitempty"`
-	Status  TestResult_Status          `protobuf:"varint,2,opt,name=status,enum=hapi.release.TestResult_Status" json:"status,omitempty"`
-	Info    string                     `protobuf:"bytes,3,opt,name=info" json:"info,omitempty"`
-	LastRun *google_protobuf.Timestamp `protobuf:"bytes,4,opt,name=last_run,json=lastRun" json:"last_run,omitempty"`
-}
-
-func (m *TestResult) Reset()                    { *m = TestResult{} }
-func (m *TestResult) String() string            { return proto.CompactTextString(m) }
-func (*TestResult) ProtoMessage()               {}
-func (*TestResult) Descriptor() ([]byte, []int) { return fileDescriptor4, []int{0} }
-
-func (m *TestResult) GetLastRun() *google_protobuf.Timestamp {
-	if m != nil {
-		return m.LastRun
-	}
-	return nil
-}
-
-func init() {
-	proto.RegisterType((*TestResult)(nil), "hapi.release.TestResult")
-	proto.RegisterEnum("hapi.release.TestResult_Status", TestResult_Status_name, TestResult_Status_value)
-}
-
-func init() { proto.RegisterFile("hapi/release/test_result.proto", fileDescriptor4) }
-
-var fileDescriptor4 = []byte{
-	// 244 bytes of a gzipped FileDescriptorProto
-	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0x4c, 0x8e, 0x41, 0x4b, 0xc3, 0x30,
-	0x18, 0x86, 0xcd, 0x1c, 0xad, 0xcb, 0x44, 0x4a, 0x4e, 0x65, 0x07, 0x57, 0x76, 0xea, 0x29, 0x81,
-	0x89, 0x78, 0xd6, 0x31, 0x41, 0x94, 0x0a, 0xe9, 0x8a, 0xe0, 0x45, 0x32, 0xf8, 0x36, 0x0b, 0x6d,
-	0x53, 0x9a, 0x2f, 0x3f, 0xd5, 0xff, 0x23, 0x49, 0x5a, 0xf4, 0xf6, 0xbd, 0xbc, 0x6f, 0x9e, 0x3c,
-	0xf4, 0xf6, 0x5b, 0xf5, 0xb5, 0x18, 0xa0, 0x01, 0x65, 0x40, 0x20, 0x18, 0xfc, 0x1a, 0xc0, 0xd8,
-	0x06, 0x79, 0x3f, 0x68, 0xd4, 0xec, 0xda, 0xf5, 0x7c, 0xec, 0x57, 0xeb, 0xb3, 0xd6, 0xe7, 0x06,
-	0x84, 0xef, 0x8e, 0xf6, 0x24, 0xb0, 0x6e, 0xc1, 0xa0, 0x6a, 0xfb, 0x30, 0xdf, 0xfc, 0x10, 0x4a,
-	0x0f, 0x60, 0x50, 0x7a, 0x06, 0x63, 0x74, 0xde, 0xa9, 0x16, 0x52, 0x92, 0x91, 0x7c, 0x21, 0xfd,
-	0xcd, 0x1e, 0x68, 0x64, 0x50, 0xa1, 0x35, 0xe9, 0x2c, 0x23, 0xf9, 0xcd, 0x76, 0xcd, 0xff, 0x7f,
-	0xc1, 0xff, 0x5e, 0xf3, 0xd2, 0xcf, 0xe4, 0x38, 0x77, 0xb0, 0xba, 0x3b, 0xe9, 0xf4, 0x32, 0xc0,
-	0xdc, 0xcd, 0xee, 0xe9, 0x55, 0xa3, 0x9c, 0xb3, 0xed, 0xd2, 0x79, 0x46, 0xf2, 0xe5, 0x76, 0xc5,
-	0x83, 0x23, 0x9f, 0x1c, 0xf9, 0x61, 0x72, 0x94, 0xb1, 0xdb, 0x4a, 0xdb, 0x6d, 0x04, 0x8d, 0x02,
-	0x9c, 0x2d, 0x69, 0x5c, 0x15, 0xaf, 0xc5, 0xfb, 0x47, 0x91, 0x5c, 0xb8, 0x50, 0x56, 0xbb, 0xdd,
-	0xbe, 0x2c, 0x13, 0xe2, 0xc2, 0xf3, 0xe3, 0xcb, 0x5b, 0x25, 0xf7, 0xc9, 0xec, 0x69, 0xf1, 0x19,
-	0x8f, 0x82, 0xc7, 0xc8, 0x83, 0xef, 0x7e, 0x03, 0x00, 0x00, 0xff, 0xff, 0x4c, 0x44, 0x22, 0xbb,
-	0x3a, 0x01, 0x00, 0x00,
-}
diff --git a/pkg/proto/hapi/release/test_run.pb.go b/pkg/proto/hapi/release/test_run.pb.go
new file mode 100644
index 000000000..51b3e72f9
--- /dev/null
+++ b/pkg/proto/hapi/release/test_run.pb.go
@@ -0,0 +1,94 @@
+// Code generated by protoc-gen-go.
+// source: hapi/release/test_run.proto
+// DO NOT EDIT!
+
+package release
+
+import proto "github.com/golang/protobuf/proto"
+import fmt "fmt"
+import math "math"
+import google_protobuf "github.com/golang/protobuf/ptypes/timestamp"
+
+// Reference imports to suppress errors if they are not otherwise used.
+var _ = proto.Marshal
+var _ = fmt.Errorf
+var _ = math.Inf
+
+type TestRun_Status int32
+
+const (
+	TestRun_UNKNOWN TestRun_Status = 0
+	TestRun_SUCCESS TestRun_Status = 1
+	TestRun_FAILURE TestRun_Status = 2
+)
+
+var TestRun_Status_name = map[int32]string{
+	0: "UNKNOWN",
+	1: "SUCCESS",
+	2: "FAILURE",
+}
+var TestRun_Status_value = map[string]int32{
+	"UNKNOWN": 0,
+	"SUCCESS": 1,
+	"FAILURE": 2,
+}
+
+func (x TestRun_Status) String() string {
+	return proto.EnumName(TestRun_Status_name, int32(x))
+}
+func (TestRun_Status) EnumDescriptor() ([]byte, []int) { return fileDescriptor4, []int{0, 0} }
+
+type TestRun struct {
+	Name        string                     `protobuf:"bytes,1,opt,name=name" json:"name,omitempty"`
+	Status      TestRun_Status             `protobuf:"varint,2,opt,name=status,enum=hapi.release.TestRun_Status" json:"status,omitempty"`
+	Info        string                     `protobuf:"bytes,3,opt,name=info" json:"info,omitempty"`
+	StartedAt   *google_protobuf.Timestamp `protobuf:"bytes,4,opt,name=started_at,json=startedAt" json:"started_at,omitempty"`
+	CompletedAt *google_protobuf.Timestamp `protobuf:"bytes,5,opt,name=completed_at,json=completedAt" json:"completed_at,omitempty"`
+}
+
+func (m *TestRun) Reset()                    { *m = TestRun{} }
+func (m *TestRun) String() string            { return proto.CompactTextString(m) }
+func (*TestRun) ProtoMessage()               {}
+func (*TestRun) Descriptor() ([]byte, []int) { return fileDescriptor4, []int{0} }
+
+func (m *TestRun) GetStartedAt() *google_protobuf.Timestamp {
+	if m != nil {
+		return m.StartedAt
+	}
+	return nil
+}
+
+func (m *TestRun) GetCompletedAt() *google_protobuf.Timestamp {
+	if m != nil {
+		return m.CompletedAt
+	}
+	return nil
+}
+
+func init() {
+	proto.RegisterType((*TestRun)(nil), "hapi.release.TestRun")
+	proto.RegisterEnum("hapi.release.TestRun_Status", TestRun_Status_name, TestRun_Status_value)
+}
+
+func init() { proto.RegisterFile("hapi/release/test_run.proto", fileDescriptor4) }
+
+var fileDescriptor4 = []byte{
+	// 265 bytes of a gzipped FileDescriptorProto
+	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0x84, 0x8f, 0x41, 0x4b, 0xfb, 0x40,
+	0x14, 0xc4, 0xff, 0xc9, 0xbf, 0x26, 0x64, 0x53, 0x24, 0xec, 0x29, 0x54, 0xc1, 0xd0, 0x53, 0x4e,
+	0xbb, 0x50, 0xbd, 0x78, 0xf0, 0x10, 0x4b, 0x05, 0x51, 0x22, 0x6c, 0x1a, 0x04, 0x2f, 0x65, 0xab,
+	0xaf, 0x35, 0x90, 0x64, 0x43, 0xf6, 0xe5, 0x8b, 0xf8, 0x89, 0x65, 0x93, 0xad, 0x78, 0xf3, 0xf6,
+	0x86, 0xf9, 0xcd, 0x30, 0x8f, 0x5c, 0x7c, 0xca, 0xae, 0xe2, 0x3d, 0xd4, 0x20, 0x35, 0x70, 0x04,
+	0x8d, 0xbb, 0x7e, 0x68, 0x59, 0xd7, 0x2b, 0x54, 0x74, 0x6e, 0x4c, 0x66, 0xcd, 0xc5, 0xd5, 0x51,
+	0xa9, 0x63, 0x0d, 0x7c, 0xf4, 0xf6, 0xc3, 0x81, 0x63, 0xd5, 0x80, 0x46, 0xd9, 0x74, 0x13, 0xbe,
+	0xfc, 0x72, 0x89, 0xbf, 0x05, 0x8d, 0x62, 0x68, 0x29, 0x25, 0xb3, 0x56, 0x36, 0x10, 0x3b, 0x89,
+	0x93, 0x06, 0x62, 0xbc, 0xe9, 0x0d, 0xf1, 0x34, 0x4a, 0x1c, 0x74, 0xec, 0x26, 0x4e, 0x7a, 0xbe,
+	0xba, 0x64, 0xbf, 0xfb, 0x99, 0x8d, 0xb2, 0x62, 0x64, 0x84, 0x65, 0x4d, 0x53, 0xd5, 0x1e, 0x54,
+	0xfc, 0x7f, 0x6a, 0x32, 0x37, 0xbd, 0x25, 0x44, 0xa3, 0xec, 0x11, 0x3e, 0x76, 0x12, 0xe3, 0x59,
+	0xe2, 0xa4, 0xe1, 0x6a, 0xc1, 0xa6, 0x7d, 0xec, 0xb4, 0x8f, 0x6d, 0x4f, 0xfb, 0x44, 0x60, 0xe9,
+	0x0c, 0xe9, 0x1d, 0x99, 0xbf, 0xab, 0xa6, 0xab, 0xc1, 0x86, 0xcf, 0xfe, 0x0c, 0x87, 0x3f, 0x7c,
+	0x86, 0x4b, 0x4e, 0xbc, 0x69, 0x1f, 0x0d, 0x89, 0x5f, 0xe6, 0x4f, 0xf9, 0xcb, 0x6b, 0x1e, 0xfd,
+	0x33, 0xa2, 0x28, 0xd7, 0xeb, 0x4d, 0x51, 0x44, 0x8e, 0x11, 0x0f, 0xd9, 0xe3, 0x73, 0x29, 0x36,
+	0x91, 0x7b, 0x1f, 0xbc, 0xf9, 0xf6, 0xc1, 0xbd, 0x37, 0x96, 0x5f, 0x7f, 0x07, 0x00, 0x00, 0xff,
+	0xff, 0x8d, 0xb9, 0xce, 0x57, 0x74, 0x01, 0x00, 0x00,
+}
diff --git a/pkg/proto/hapi/release/test_suite.pb.go b/pkg/proto/hapi/release/test_suite.pb.go
index bc6357cbd..27fe45ac5 100644
--- a/pkg/proto/hapi/release/test_suite.pb.go
+++ b/pkg/proto/hapi/release/test_suite.pb.go
@@ -16,10 +16,12 @@ var _ = math.Inf
 
 // TestSuite comprises of the last run of the pre-defined test suite of a release version
 type TestSuite struct {
-	// LastRun indicates the date/time this test was last run.
-	LastRun *google_protobuf.Timestamp `protobuf:"bytes,1,opt,name=last_run,json=lastRun" json:"last_run,omitempty"`
+	// StartedAt indicates the date/time this test suite was kicked off
+	StartedAt *google_protobuf.Timestamp `protobuf:"bytes,1,opt,name=started_at,json=startedAt" json:"started_at,omitempty"`
+	// CompletedAt indicates the date/time this test suite was completed
+	CompletedAt *google_protobuf.Timestamp `protobuf:"bytes,2,opt,name=completed_at,json=completedAt" json:"completed_at,omitempty"`
 	// Results are the results of each segment of the test
-	Results []*TestResult `protobuf:"bytes,2,rep,name=results" json:"results,omitempty"`
+	Results []*TestRun `protobuf:"bytes,3,rep,name=results" json:"results,omitempty"`
 }
 
 func (m *TestSuite) Reset()                    { *m = TestSuite{} }
@@ -27,14 +29,21 @@ func (m *TestSuite) String() string            { return proto.CompactTextString(
 func (*TestSuite) ProtoMessage()               {}
 func (*TestSuite) Descriptor() ([]byte, []int) { return fileDescriptor5, []int{0} }
 
-func (m *TestSuite) GetLastRun() *google_protobuf.Timestamp {
+func (m *TestSuite) GetStartedAt() *google_protobuf.Timestamp {
 	if m != nil {
-		return m.LastRun
+		return m.StartedAt
 	}
 	return nil
 }
 
-func (m *TestSuite) GetResults() []*TestResult {
+func (m *TestSuite) GetCompletedAt() *google_protobuf.Timestamp {
+	if m != nil {
+		return m.CompletedAt
+	}
+	return nil
+}
+
+func (m *TestSuite) GetResults() []*TestRun {
 	if m != nil {
 		return m.Results
 	}
@@ -48,17 +57,18 @@ func init() {
 func init() { proto.RegisterFile("hapi/release/test_suite.proto", fileDescriptor5) }
 
 var fileDescriptor5 = []byte{
-	// 183 bytes of a gzipped FileDescriptorProto
-	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0x64, 0x8e, 0xc1, 0x8a, 0x83, 0x30,
-	0x14, 0x45, 0x71, 0x06, 0xc6, 0x31, 0xce, 0xca, 0x95, 0x08, 0xd3, 0x4a, 0x57, 0xae, 0x5e, 0xc0,
-	0xd2, 0x1f, 0xe8, 0x27, 0xa4, 0xae, 0xba, 0x29, 0x11, 0x5e, 0xad, 0x10, 0x8d, 0xf8, 0x5e, 0xfa,
-	0xfd, 0x25, 0x46, 0xa1, 0xd0, 0xf5, 0x39, 0xdc, 0x73, 0xc5, 0xff, 0x43, 0x4f, 0xbd, 0x9c, 0xd1,
-	0xa0, 0x26, 0x94, 0x8c, 0xc4, 0x37, 0x72, 0x3d, 0x23, 0x4c, 0xb3, 0x65, 0x9b, 0xfd, 0x79, 0x0c,
-	0x2b, 0x2e, 0xf6, 0x9d, 0xb5, 0x9d, 0x41, 0xb9, 0xb0, 0xd6, 0xdd, 0x25, 0xf7, 0x03, 0x12, 0xeb,
-	0x61, 0x0a, 0x7a, 0xb1, 0xfb, 0x5c, 0x9b, 0x91, 0x9c, 0xe1, 0xc0, 0x0f, 0x4f, 0x91, 0x34, 0x48,
-	0x7c, 0xf1, 0x85, 0xec, 0x24, 0x7e, 0x8d, 0xf6, 0x86, 0x1b, 0xf3, 0xa8, 0x8c, 0xaa, 0xb4, 0x2e,
-	0x20, 0x04, 0x60, 0x0b, 0x40, 0xb3, 0x05, 0x54, 0xec, 0x5d, 0xe5, 0xc6, 0xac, 0x16, 0x71, 0xd8,
-	0xa4, 0xfc, 0xab, 0xfc, 0xae, 0xd2, 0x3a, 0x87, 0xf7, 0x93, 0xe0, 0x03, 0x6a, 0x11, 0xd4, 0x26,
-	0x9e, 0x93, 0x6b, 0xbc, 0xe2, 0xf6, 0x67, 0xd9, 0x3e, 0xbe, 0x02, 0x00, 0x00, 0xff, 0xff, 0x05,
-	0x00, 0xf5, 0xbb, 0xf9, 0x00, 0x00, 0x00,
+	// 207 bytes of a gzipped FileDescriptorProto
+	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0x84, 0x8f, 0xc1, 0x4a, 0x86, 0x40,
+	0x14, 0x85, 0x31, 0x21, 0x71, 0x74, 0x35, 0x10, 0x88, 0x11, 0x49, 0x2b, 0x57, 0x33, 0x60, 0xab,
+	0x16, 0x2d, 0xec, 0x11, 0xcc, 0x55, 0x1b, 0x19, 0xeb, 0x66, 0xc2, 0xe8, 0x0c, 0x73, 0xef, 0xbc,
+	0x5a, 0xcf, 0x17, 0xea, 0x18, 0x41, 0x8b, 0x7f, 0xfd, 0x7d, 0xe7, 0x9c, 0x7b, 0xd9, 0xdd, 0x97,
+	0xb2, 0xb3, 0x74, 0xa0, 0x41, 0x21, 0x48, 0x02, 0xa4, 0x01, 0xfd, 0x4c, 0x20, 0xac, 0x33, 0x64,
+	0x78, 0xbe, 0x61, 0x11, 0x70, 0x79, 0x3f, 0x19, 0x33, 0x69, 0x90, 0x3b, 0x1b, 0xfd, 0xa7, 0xa4,
+	0x79, 0x01, 0x24, 0xb5, 0xd8, 0x43, 0x2f, 0x6f, 0xff, 0xb7, 0x39, 0xbf, 0x1e, 0xf0, 0xe1, 0x3b,
+	0x62, 0x69, 0x0f, 0x48, 0xaf, 0x5b, 0x3f, 0x7f, 0x62, 0x0c, 0x49, 0x39, 0x82, 0x8f, 0x41, 0x51,
+	0x11, 0x55, 0x51, 0x9d, 0x35, 0xa5, 0x38, 0x06, 0xc4, 0x39, 0x20, 0xfa, 0x73, 0xa0, 0x4b, 0x83,
+	0xdd, 0x12, 0x7f, 0x66, 0xf9, 0xbb, 0x59, 0xac, 0x86, 0x10, 0xbe, 0xba, 0x18, 0xce, 0x7e, 0xfd,
+	0x96, 0xb8, 0x64, 0x89, 0x03, 0xf4, 0x9a, 0xb0, 0x88, 0xab, 0xb8, 0xce, 0x9a, 0x1b, 0xf1, 0xf7,
+	0x4b, 0xb1, 0xdd, 0xd8, 0xf9, 0xb5, 0x3b, 0xad, 0x97, 0xf4, 0x2d, 0x09, 0x6c, 0xbc, 0xde, 0xcb,
+	0x1f, 0x7f, 0x02, 0x00, 0x00, 0xff, 0xff, 0x8c, 0x59, 0x65, 0x4f, 0x37, 0x01, 0x00, 0x00,
 }
diff --git a/pkg/proto/hapi/services/tiller.pb.go b/pkg/proto/hapi/services/tiller.pb.go
index bfe6cb43d..883d5a194 100644
--- a/pkg/proto/hapi/services/tiller.pb.go
+++ b/pkg/proto/hapi/services/tiller.pb.go
@@ -42,7 +42,6 @@ import hapi_release5 "k8s.io/helm/pkg/proto/hapi/release"
 import hapi_release2 "k8s.io/helm/pkg/proto/hapi/release"
 import hapi_release1 "k8s.io/helm/pkg/proto/hapi/release"
 import hapi_version "k8s.io/helm/pkg/proto/hapi/version"
-import hapi_release4 "k8s.io/helm/pkg/proto/hapi/release"
 
 import (
 	context "golang.org/x/net/context"
@@ -507,7 +506,7 @@ func (*TestReleaseRequest) Descriptor() ([]byte, []int) { return fileDescriptor0
 // TestReleaseResponse
 type TestReleaseResponse struct {
 	// TODO: change to repeated hapi.release.Release.Test results = 1; (for stream)
-	Result *hapi_release4.TestSuite `protobuf:"bytes,1,opt,name=result" json:"result,omitempty"`
+	Msg string `protobuf:"bytes,1,opt,name=msg" json:"msg,omitempty"`
 }
 
 func (m *TestReleaseResponse) Reset()                    { *m = TestReleaseResponse{} }
@@ -515,13 +514,6 @@ func (m *TestReleaseResponse) String() string            { return proto.CompactT
 func (*TestReleaseResponse) ProtoMessage()               {}
 func (*TestReleaseResponse) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{20} }
 
-func (m *TestReleaseResponse) GetResult() *hapi_release4.TestSuite {
-	if m != nil {
-		return m.Result
-	}
-	return nil
-}
-
 func init() {
 	proto.RegisterType((*ListReleasesRequest)(nil), "hapi.services.tiller.ListReleasesRequest")
 	proto.RegisterType((*ListSort)(nil), "hapi.services.tiller.ListSort")
@@ -582,7 +574,7 @@ type ReleaseServiceClient interface {
 	GetHistory(ctx context.Context, in *GetHistoryRequest, opts ...grpc.CallOption) (*GetHistoryResponse, error)
 	// TODO: move this to a test release service or rename to RunReleaseTest
 	// TestRelease runs the tests for a given release
-	RunReleaseTest(ctx context.Context, in *TestReleaseRequest, opts ...grpc.CallOption) (*TestReleaseResponse, error)
+	RunReleaseTest(ctx context.Context, in *TestReleaseRequest, opts ...grpc.CallOption) (ReleaseService_RunReleaseTestClient, error)
 }
 
 type releaseServiceClient struct {
@@ -697,13 +689,36 @@ func (c *releaseServiceClient) GetHistory(ctx context.Context, in *GetHistoryReq
 	return out, nil
 }
 
-func (c *releaseServiceClient) RunReleaseTest(ctx context.Context, in *TestReleaseRequest, opts ...grpc.CallOption) (*TestReleaseResponse, error) {
-	out := new(TestReleaseResponse)
-	err := grpc.Invoke(ctx, "/hapi.services.tiller.ReleaseService/RunReleaseTest", in, out, c.cc, opts...)
+func (c *releaseServiceClient) RunReleaseTest(ctx context.Context, in *TestReleaseRequest, opts ...grpc.CallOption) (ReleaseService_RunReleaseTestClient, error) {
+	stream, err := grpc.NewClientStream(ctx, &_ReleaseService_serviceDesc.Streams[1], c.cc, "/hapi.services.tiller.ReleaseService/RunReleaseTest", opts...)
 	if err != nil {
 		return nil, err
 	}
-	return out, nil
+	x := &releaseServiceRunReleaseTestClient{stream}
+	if err := x.ClientStream.SendMsg(in); err != nil {
+		return nil, err
+	}
+	if err := x.ClientStream.CloseSend(); err != nil {
+		return nil, err
+	}
+	return x, nil
+}
+
+type ReleaseService_RunReleaseTestClient interface {
+	Recv() (*TestReleaseResponse, error)
+	grpc.ClientStream
+}
+
+type releaseServiceRunReleaseTestClient struct {
+	grpc.ClientStream
+}
+
+func (x *releaseServiceRunReleaseTestClient) Recv() (*TestReleaseResponse, error) {
+	m := new(TestReleaseResponse)
+	if err := x.ClientStream.RecvMsg(m); err != nil {
+		return nil, err
+	}
+	return m, nil
 }
 
 // Server API for ReleaseService service
@@ -732,7 +747,7 @@ type ReleaseServiceServer interface {
 	GetHistory(context.Context, *GetHistoryRequest) (*GetHistoryResponse, error)
 	// TODO: move this to a test release service or rename to RunReleaseTest
 	// TestRelease runs the tests for a given release
-	RunReleaseTest(context.Context, *TestReleaseRequest) (*TestReleaseResponse, error)
+	RunReleaseTest(*TestReleaseRequest, ReleaseService_RunReleaseTestServer) error
 }
 
 func RegisterReleaseServiceServer(s *grpc.Server, srv ReleaseServiceServer) {
@@ -904,22 +919,25 @@ func _ReleaseService_GetHistory_Handler(srv interface{}, ctx context.Context, de
 	return interceptor(ctx, in, info, handler)
 }
 
-func _ReleaseService_RunReleaseTest_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
-	in := new(TestReleaseRequest)
-	if err := dec(in); err != nil {
-		return nil, err
-	}
-	if interceptor == nil {
-		return srv.(ReleaseServiceServer).RunReleaseTest(ctx, in)
-	}
-	info := &grpc.UnaryServerInfo{
-		Server:     srv,
-		FullMethod: "/hapi.services.tiller.ReleaseService/RunReleaseTest",
-	}
-	handler := func(ctx context.Context, req interface{}) (interface{}, error) {
-		return srv.(ReleaseServiceServer).RunReleaseTest(ctx, req.(*TestReleaseRequest))
+func _ReleaseService_RunReleaseTest_Handler(srv interface{}, stream grpc.ServerStream) error {
+	m := new(TestReleaseRequest)
+	if err := stream.RecvMsg(m); err != nil {
+		return err
 	}
-	return interceptor(ctx, in, info, handler)
+	return srv.(ReleaseServiceServer).RunReleaseTest(m, &releaseServiceRunReleaseTestServer{stream})
+}
+
+type ReleaseService_RunReleaseTestServer interface {
+	Send(*TestReleaseResponse) error
+	grpc.ServerStream
+}
+
+type releaseServiceRunReleaseTestServer struct {
+	grpc.ServerStream
+}
+
+func (x *releaseServiceRunReleaseTestServer) Send(m *TestReleaseResponse) error {
+	return x.ServerStream.SendMsg(m)
 }
 
 var _ReleaseService_serviceDesc = grpc.ServiceDesc{
@@ -958,10 +976,6 @@ var _ReleaseService_serviceDesc = grpc.ServiceDesc{
 			MethodName: "GetHistory",
 			Handler:    _ReleaseService_GetHistory_Handler,
 		},
-		{
-			MethodName: "RunReleaseTest",
-			Handler:    _ReleaseService_RunReleaseTest_Handler,
-		},
 	},
 	Streams: []grpc.StreamDesc{
 		{
@@ -969,6 +983,11 @@ var _ReleaseService_serviceDesc = grpc.ServiceDesc{
 			Handler:       _ReleaseService_ListReleases_Handler,
 			ServerStreams: true,
 		},
+		{
+			StreamName:    "RunReleaseTest",
+			Handler:       _ReleaseService_RunReleaseTest_Handler,
+			ServerStreams: true,
+		},
 	},
 	Metadata: fileDescriptor0,
 }
diff --git a/pkg/tiller/environment/environment.go b/pkg/tiller/environment/environment.go
index 5ddff63cc..010d14060 100644
--- a/pkg/tiller/environment/environment.go
+++ b/pkg/tiller/environment/environment.go
@@ -137,6 +137,7 @@ type KubeClient interface {
 	Update(namespace string, originalReader, modifiedReader io.Reader, recreate bool, timeout int64, shouldWait bool) error
 
 	Build(namespace string, reader io.Reader) (kube.Result, error)
+
 	//TODO: insert description
 	WaitAndGetCompletedPodStatus(namespace string, reader io.Reader, timeout time.Duration) (api.PodPhase, error)
 }
@@ -184,6 +185,10 @@ func (p *PrintingKubeClient) Build(ns string, reader io.Reader) (kube.Result, er
 	return []*resource.Info{}, nil
 }
 
+func (p *PrintingKubeClient) WaitAndGetCompletedPodStatus(namespace string, reader io.Reader, timeout time.Duration) (api.PodPhase, error) {
+	return "", nil
+}
+
 // Environment provides the context for executing a client request.
 //
 // All services in a context are concurrency safe.
diff --git a/pkg/tiller/environment/environment_test.go b/pkg/tiller/environment/environment_test.go
index a6621e5e7..7544a3938 100644
--- a/pkg/tiller/environment/environment_test.go
+++ b/pkg/tiller/environment/environment_test.go
@@ -20,10 +20,12 @@ import (
 	"bytes"
 	"io"
 	"testing"
+	"time"
 
 	"k8s.io/helm/pkg/chartutil"
 	"k8s.io/helm/pkg/kube"
 	"k8s.io/helm/pkg/proto/hapi/chart"
+	"k8s.io/kubernetes/pkg/api"
 	"k8s.io/kubernetes/pkg/kubectl/resource"
 )
 
@@ -56,6 +58,10 @@ func (k *mockKubeClient) Build(ns string, reader io.Reader) (kube.Result, error)
 	return []*resource.Info{}, nil
 }
 
+func (k *mockKubeClient) WaitAndGetCompletedPodStatus(namespace string, reader io.Reader, timeout time.Duration) (api.PodPhase, error) {
+	return "", nil
+}
+
 var _ Engine = &mockEngine{}
 var _ KubeClient = &mockKubeClient{}
 var _ KubeClient = &PrintingKubeClient{}
diff --git a/pkg/tiller/release_server.go b/pkg/tiller/release_server.go
index 40e9fa75f..28d127ac0 100644
--- a/pkg/tiller/release_server.go
+++ b/pkg/tiller/release_server.go
@@ -1066,27 +1066,27 @@ func validateManifest(c environment.KubeClient, ns string, manifest []byte) erro
 }
 
 // RunTestRelease runs a pre-defined test on a given release
-func (s *ReleaseServer) RunReleaseTest(c ctx.Context, req *services.TestReleaseRequest) (*services.TestReleaseResponse, error) {
+func (s *ReleaseServer) RunReleaseTest(req *services.TestReleaseRequest, stream services.ReleaseService_RunReleaseTestServer) error {
 
-	res := &services.TestReleaseResponse{}
 	if !ValidName.MatchString(req.Name) {
-		return nil, errMissingRelease
+		return errMissingRelease
 	}
 
 	// finds the non-deleted release with the given name
-	r, err := s.env.Releases.Last(req.Name)
+	rel, err := s.env.Releases.Last(req.Name)
 	if err != nil {
-		return nil, err
+		return err
 	}
 
+	tests, err := prepareTests(rel.Hooks, rel.Name)
 	kubeCli := s.env.KubeClient
-	testSuite, err := runReleaseTestSuite(r.Hooks, kubeCli, r.Name, r.Namespace, req.Timeout)
+
+	testSuite, err := runReleaseTests(tests, rel, kubeCli, stream, req.Timeout)
 	if err != nil {
-		return nil, err
+		return err
 	}
 
-	r.TestSuite = testSuite
-	res.Result = testSuite
+	rel.TestSuite = testSuite
 
-	return res, nil
+	return nil
 }
diff --git a/pkg/tiller/release_testing.go b/pkg/tiller/release_testing.go
index 64fffc094..90379887a 100644
--- a/pkg/tiller/release_testing.go
+++ b/pkg/tiller/release_testing.go
@@ -26,43 +26,55 @@ import (
 	"k8s.io/kubernetes/pkg/api"
 
 	"k8s.io/helm/pkg/proto/hapi/release"
+	"k8s.io/helm/pkg/proto/hapi/services"
 	"k8s.io/helm/pkg/tiller/environment"
 	"k8s.io/helm/pkg/timeconv"
 )
 
-// change name to runReleaseTestSuite
-func runReleaseTestSuite(hooks []*release.Hook, kube environment.KubeClient, name, namespace string, timeout int64) (*release.TestSuite, error) {
+//TODO: testSuiteRunner.Run()
+//struct testSuiteRunner {
+//suite *release.TestSuite,
+//tests []string,
+//kube environemtn.KubeClient,
+//timeout int64
+////stream or output channel
+//}
 
-	suite := &release.TestSuite{}
-	suite.LastRun = timeconv.Now()
-	results := []*release.TestResult{}
+func runReleaseTests(tests []string, rel *release.Release, kube environment.KubeClient, stream services.ReleaseService_RunReleaseTestServer, timeout int64) (*release.TestSuite, error) {
+	results := []*release.TestRun{}
 
-	tests, err := prepareTests(hooks, name)
-	if err != nil {
-		return suite, err
-	}
+	//TODO: add results to test suite
+	suite := &release.TestSuite{}
+	suite.StartedAt = timeconv.Now()
 
 	for _, h := range tests {
 		var sh simpleHead
 		err := yaml.Unmarshal([]byte(h), &sh)
 		if err != nil {
-			//handle err better
 			return nil, err
 		}
-		ts := &release.TestResult{Name: sh.Metadata.Name}
 
-		// should this be lower? should we even be saving time to hook?
-		// TODO: should be start time really
-		ts.LastRun = timeconv.Now()
+		if sh.Kind != "Pod" {
+			return nil, fmt.Errorf("%s is not a pod", sh.Metadata.Name)
+		}
+
+		ts := &release.TestRun{Name: sh.Metadata.Name}
+		ts.StartedAt = timeconv.Now()
+		if err := streamRunning(ts.Name, stream); err != nil {
+			return nil, err
+		}
 
 		resourceCreated := true
 		b := bytes.NewBufferString(h)
-		if err := kube.Create(namespace, b); err != nil {
-			log.Printf("Could not create %s(%s): %v", ts.Name, sh.Kind, err)
-			ts.Info = err.Error()
-			//TODO: status option should be constant not random int
-			ts.Status = 2
+		if err := kube.Create(rel.Namespace, b); err != nil {
 			resourceCreated = false
+			msg := fmt.Sprintf("ERROR: %s", err)
+			log.Printf(msg)
+			ts.Info = err.Error()
+			ts.Status = release.TestRun_FAILURE
+			if streamErr := streamMessage(msg, stream); streamErr != nil {
+				return nil, err
+			}
 		}
 
 		status := api.PodUnknown
@@ -70,31 +82,41 @@ func runReleaseTestSuite(hooks []*release.Hook, kube environment.KubeClient, nam
 		if resourceCreated {
 			b.Reset()
 			b.WriteString(h)
-			status, err = kube.WaitAndGetCompletedPodStatus(namespace, b, time.Duration(timeout)*time.Second)
+			status, err = kube.WaitAndGetCompletedPodStatus(rel.Namespace, b, time.Duration(timeout)*time.Second)
 			if err != nil {
-				log.Printf("Error getting status for %s(%s): %s", ts.Name, sh.Kind, err)
-				ts.Info = err.Error()
-				ts.Status = 0
 				resourceCleanExit = false
+				log.Printf("Error getting status for pod %s: %s", ts.Name, err)
+				ts.Info = err.Error()
+				ts.Status = release.TestRun_UNKNOWN
+				if streamErr := streamFailed(ts.Name, stream); streamErr != nil {
+					return nil, err
+				}
 			}
 		}
 
 		// TODO: maybe better suited as a switch statement and include
 		//      PodUnknown, PodFailed, PodRunning, and PodPending scenarios
 		if resourceCreated && resourceCleanExit && status == api.PodSucceeded {
-			ts.Status = 1
+			ts.Status = release.TestRun_SUCCESS
+			if streamErr := streamSuccess(ts.Name, stream); streamErr != nil {
+				return nil, streamErr
+			}
 		} else if resourceCreated && resourceCleanExit && status == api.PodFailed {
-			ts.Status = 2
+			ts.Status = release.TestRun_FAILURE
+			if streamErr := streamFailed(ts.Name, stream); streamErr != nil {
+				return nil, err
+			}
 		}
 
 		results = append(results, ts)
-		log.Printf("Test %s(%s) complete", ts.Name, sh.Kind)
+		log.Printf("Test %s completed", ts.Name)
 
 		//TODO: recordTests() - add test results to configmap with standardized name
 	}
 
 	suite.Results = results
-	log.Printf("Finished running test suite for %s", name)
+	//TODO: delete flag
+	log.Printf("Finished running test suite for %s", rel.Name)
 
 	return suite, nil
 }
@@ -145,3 +167,37 @@ func prepareTests(hooks []*release.Hook, releaseName string) ([]string, error) {
 	}
 	return tests, nil
 }
+
+func streamRunning(name string, stream services.ReleaseService_RunReleaseTestServer) error {
+	msg := "RUNNING: " + name
+	if err := streamMessage(msg, stream); err != nil {
+		return err
+	}
+	return nil
+}
+
+func streamFailed(name string, stream services.ReleaseService_RunReleaseTestServer) error {
+	msg := fmt.Sprintf("FAILED: %s, run `kubectl logs %s` for more info", name, name)
+	if err := streamMessage(msg, stream); err != nil {
+		return err
+	}
+	return nil
+}
+
+func streamSuccess(name string, stream services.ReleaseService_RunReleaseTestServer) error {
+	msg := fmt.Sprintf("PASSED: %s", name)
+	if err := streamMessage(msg, stream); err != nil {
+		return err
+	}
+	return nil
+}
+
+func streamMessage(msg string, stream services.ReleaseService_RunReleaseTestServer) error {
+	resp := &services.TestReleaseResponse{Msg: msg}
+	// TODO: handle err better
+	if err := stream.Send(resp); err != nil {
+		return err
+	}
+
+	return nil
+}
-- 
GitLab