Repository: bojand/ghz
Pull request #265 (2021-03-22 00:44:54)

add timestamp tests


Patch:
diff --git a/go.mod b/go.mod
index 829f549..dac4cc4 100644
--- a/go.mod
+++ b/go.mod
@@ -29,5 +29,6 @@ require (
 	golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208
 	golang.org/x/tools v0.0.0-20200812195022-5ae4c3c160a0
 	google.golang.org/grpc v1.34.0
+	google.golang.org/protobuf v1.25.0 // indirect
 	gopkg.in/go-playground/assert.v1 v1.2.1 // indirect
 )
diff --git a/internal/common.go b/internal/common.go
index 273322b..04dcb8d 100644
--- a/internal/common.go
+++ b/internal/common.go
@@ -8,6 +8,7 @@ import (
 	"google.golang.org/grpc/credentials"
 	"google.golang.org/grpc/reflection"
 
+	"github.com/bojand/ghz/internal/gtime"
 	"github.com/bojand/ghz/internal/helloworld"
 	"github.com/bojand/ghz/internal/sleep"
 	"github.com/bojand/ghz/internal/wrapped"
@@ -129,3 +130,36 @@ func StartWrappedServer(secure bool) (*wrapped.WrappedService, *grpc.Server, err
 
 	return &ws, s, err
 }
+
+// StartTimeServer starts the wrapped test server
+func StartTimeServer(secure bool) (*gtime.TimeService, *grpc.Server, error) {
+	lis, err := net.Listen("tcp", ":0")
+	if err != nil {
+		return nil, nil, err
+	}
+
+	var opts []grpc.ServerOption
+
+	if secure {
+		creds, err := credentials.NewServerTLSFromFile("../testdata/localhost.crt", "../testdata/localhost.key")
+		if err != nil {
+			return nil, nil, err
+		}
+		opts = []grpc.ServerOption{grpc.Creds(creds)}
+	}
+
+	s := grpc.NewServer(opts...)
+
+	gs := gtime.TimeService{}
+	gtime.RegisterTimeServiceServer(s, &gs)
+	reflection.Register(s)
+
+	TestPort = strconv.Itoa(lis.Addr().(*net.TCPAddr).Port)
+	TestLocalhost = "localhost:" + TestPort
+
+	go func() {
+		_ = s.Serve(lis)
+	}()
+
+	return &gs, s, err
+}
diff --git a/internal/gtime/gtime.pb.go b/internal/gtime/gtime.pb.go
new file mode 100644
index 0000000..ce40c23
--- /dev/null
+++ b/internal/gtime/gtime.pb.go
@@ -0,0 +1,271 @@
+// Code generated by protoc-gen-go. DO NOT EDIT.
+// versions:
+// 	protoc-gen-go v1.25.0
+// 	protoc        v3.11.4
+// source: gtime.proto
+
+package gtime
+
+import (
+	proto "github.com/golang/protobuf/proto"
+	duration "github.com/golang/protobuf/ptypes/duration"
+	timestamp "github.com/golang/protobuf/ptypes/timestamp"
+	protoreflect "google.golang.org/protobuf/reflect/protoreflect"
+	protoimpl "google.golang.org/protobuf/runtime/protoimpl"
+	reflect "reflect"
+	sync "sync"
+)
+
+const (
+	// Verify that this generated code is sufficiently up-to-date.
+	_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
+	// Verify that runtime/protoimpl is sufficiently up-to-date.
+	_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
+)
+
+// This is a compile-time assertion that a sufficiently up-to-date version
+// of the legacy proto package is being used.
+const _ = proto.ProtoPackageIsVersion4
+
+type CallRequest struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	Ts     *timestamp.Timestamp `protobuf:"bytes,1,opt,name=ts,proto3" json:"ts,omitempty"`
+	Dur    *duration.Duration   `protobuf:"bytes,2,opt,name=dur,proto3" json:"dur,omitempty"`
+	UserId uint64               `protobuf:"varint,3,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"`
+}
+
+func (x *CallRequest) Reset() {
+	*x = CallRequest{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_gtime_proto_msgTypes[0]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *CallRequest) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*CallRequest) ProtoMessage() {}
+
+func (x *CallRequest) ProtoReflect() protoreflect.Message {
+	mi := &file_gtime_proto_msgTypes[0]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use CallRequest.ProtoReflect.Descriptor instead.
+func (*CallRequest) Descriptor() ([]byte, []int) {
+	return file_gtime_proto_rawDescGZIP(), []int{0}
+}
+
+func (x *CallRequest) GetTs() *timestamp.Timestamp {
+	if x != nil {
+		return x.Ts
+	}
+	return nil
+}
+
+func (x *CallRequest) GetDur() *duration.Duration {
+	if x != nil {
+		return x.Dur
+	}
+	return nil
+}
+
+func (x *CallRequest) GetUserId() uint64 {
+	if x != nil {
+		return x.UserId
+	}
+	return 0
+}
+
+type CallReply struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	Ts      *timestamp.Timestamp `protobuf:"bytes,1,opt,name=ts,proto3" json:"ts,omitempty"`
+	Dur     *duration.Duration   `protobuf:"bytes,2,opt,name=dur,proto3" json:"dur,omitempty"`
+	Message string               `protobuf:"bytes,3,opt,name=message,proto3" json:"message,omitempty"`
+}
+
+func (x *CallReply) Reset() {
+	*x = CallReply{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_gtime_proto_msgTypes[1]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *CallReply) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*CallReply) ProtoMessage() {}
+
+func (x *CallReply) ProtoReflect() protoreflect.Message {
+	mi := &file_gtime_proto_msgTypes[1]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use CallReply.ProtoReflect.Descriptor instead.
+func (*CallReply) Descriptor() ([]byte, []int) {
+	return file_gtime_proto_rawDescGZIP(), []int{1}
+}
+
+func (x *CallReply) GetTs() *timestamp.Timestamp {
+	if x != nil {
+		return x.Ts
+	}
+	return nil
+}
+
+func (x *CallReply) GetDur() *duration.Duration {
+	if x != nil {
+		return x.Dur
+	}
+	return nil
+}
+
+func (x *CallReply) GetMessage() string {
+	if x != nil {
+		return x.Message
+	}
+	return ""
+}
+
+var File_gtime_proto protoreflect.FileDescriptor
+
+var file_gtime_proto_rawDesc = []byte{
+	0x0a, 0x0b, 0x67, 0x74, 0x69, 0x6d, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x05, 0x67,
+	0x74, 0x69, 0x6d, 0x65, 0x1a, 0x1f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f,
+	0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e,
+	0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72,
+	0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x64, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e,
+	0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x7f, 0x0a, 0x0b, 0x43, 0x61, 0x6c, 0x6c, 0x52, 0x65, 0x71,
+	0x75, 0x65, 0x73, 0x74, 0x12, 0x2a, 0x0a, 0x02, 0x74, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b,
+	0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62,
+	0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x02, 0x74, 0x73,
+	0x12, 0x2b, 0x0a, 0x03, 0x64, 0x75, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e,
+	0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e,
+	0x44, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x03, 0x64, 0x75, 0x72, 0x12, 0x17, 0x0a,
+	0x07, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x04, 0x52, 0x06,
+	0x75, 0x73, 0x65, 0x72, 0x49, 0x64, 0x22, 0x7e, 0x0a, 0x09, 0x43, 0x61, 0x6c, 0x6c, 0x52, 0x65,
+	0x70, 0x6c, 0x79, 0x12, 0x2a, 0x0a, 0x02, 0x74, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32,
+	0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75,
+	0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x02, 0x74, 0x73, 0x12,
+	0x2b, 0x0a, 0x03, 0x64, 0x75, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x67,
+	0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x44,
+	0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x03, 0x64, 0x75, 0x72, 0x12, 0x18, 0x0a, 0x07,
+	0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d,
+	0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x32, 0x41, 0x0a, 0x0b, 0x54, 0x69, 0x6d, 0x65, 0x53, 0x65,
+	0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x32, 0x0a, 0x08, 0x54, 0x65, 0x73, 0x74, 0x43, 0x61, 0x6c,
+	0x6c, 0x12, 0x12, 0x2e, 0x67, 0x74, 0x69, 0x6d, 0x65, 0x2e, 0x43, 0x61, 0x6c, 0x6c, 0x52, 0x65,
+	0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x10, 0x2e, 0x67, 0x74, 0x69, 0x6d, 0x65, 0x2e, 0x43, 0x61,
+	0x6c, 0x6c, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x22, 0x00, 0x42, 0x10, 0x5a, 0x0e, 0x69, 0x6e, 0x74,
+	0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x67, 0x74, 0x69, 0x6d, 0x65, 0x62, 0x06, 0x70, 0x72, 0x6f,
+	0x74, 0x6f, 0x33,
+}
+
+var (
+	file_gtime_proto_rawDescOnce sync.Once
+	file_gtime_proto_rawDescData = file_gtime_proto_rawDesc
+)
+
+func file_gtime_proto_rawDescGZIP() []byte {
+	file_gtime_proto_rawDescOnce.Do(func() {
+		file_gtime_proto_rawDescData = protoimpl.X.CompressGZIP(file_gtime_proto_rawDescData)
+	})
+	return file_gtime_proto_rawDescData
+}
+
+var file_gtime_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
+var file_gtime_proto_goTypes = []interface{}{
+	(*CallRequest)(nil),         // 0: gtime.CallRequest
+	(*CallReply)(nil),           // 1: gtime.CallReply
+	(*timestamp.Timestamp)(nil), // 2: google.protobuf.Timestamp
+	(*duration.Duration)(nil),   // 3: google.protobuf.Duration
+}
+var file_gtime_proto_depIdxs = []int32{
+	2, // 0: gtime.CallRequest.ts:type_name -> google.protobuf.Timestamp
+	3, // 1: gtime.CallRequest.dur:type_name -> google.protobuf.Duration
+	2, // 2: gtime.CallReply.ts:type_name -> google.protobuf.Timestamp
+	3, // 3: gtime.CallReply.dur:type_name -> google.protobuf.Duration
+	0, // 4: gtime.TimeService.TestCall:input_type -> gtime.CallRequest
+	1, // 5: gtime.TimeService.TestCall:output_type -> gtime.CallReply
+	5, // [5:6] is the sub-list for method output_type
+	4, // [4:5] is the sub-list for method input_type
+	4, // [4:4] is the sub-list for extension type_name
+	4, // [4:4] is the sub-list for extension extendee
+	0, // [0:4] is the sub-list for field type_name
+}
+
+func init() { file_gtime_proto_init() }
+func file_gtime_proto_init() {
+	if File_gtime_proto != nil {
+		return
+	}
+	if !protoimpl.UnsafeEnabled {
+		file_gtime_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*CallRequest); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+		file_gtime_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*CallReply); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+	}
+	type x struct{}
+	out := protoimpl.TypeBuilder{
+		File: protoimpl.DescBuilder{
+			GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
+			RawDescriptor: file_gtime_proto_rawDesc,
+			NumEnums:      0,
+			NumMessages:   2,
+			NumExtensions: 0,
+			NumServices:   1,
+		},
+		GoTypes:           file_gtime_proto_goTypes,
+		DependencyIndexes: file_gtime_proto_depIdxs,
+		MessageInfos:      file_gtime_proto_msgTypes,
+	}.Build()
+	File_gtime_proto = out.File
+	file_gtime_proto_rawDesc = nil
+	file_gtime_proto_goTypes = nil
+	file_gtime_proto_depIdxs = nil
+}
diff --git a/internal/gtime/gtime_grpc.pb.go b/internal/gtime/gtime_grpc.pb.go
new file mode 100644
index 0000000..5edd8a8
--- /dev/null
+++ b/internal/gtime/gtime_grpc.pb.go
@@ -0,0 +1,101 @@
+// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
+
+package gtime
+
+import (
+	context "context"
+	grpc "google.golang.org/grpc"
+	codes "google.golang.org/grpc/codes"
+	status "google.golang.org/grpc/status"
+)
+
+// This is a compile-time assertion to ensure that this generated file
+// is compatible with the grpc package it is being compiled against.
+// Requires gRPC-Go v1.32.0 or later.
+const _ = grpc.SupportPackageIsVersion7
+
+// TimeServiceClient is the client API for TimeService service.
+//
+// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
+type TimeServiceClient interface {
+	TestCall(ctx context.Context, in *CallRequest, opts ...grpc.CallOption) (*CallReply, error)
+}
+
+type timeServiceClient struct {
+	cc grpc.ClientConnInterface
+}
+
+func NewTimeServiceClient(cc grpc.ClientConnInterface) TimeServiceClient {
+	return &timeServiceClient{cc}
+}
+
+func (c *timeServiceClient) TestCall(ctx context.Context, in *CallRequest, opts ...grpc.CallOption) (*CallReply, error) {
+	out := new(CallReply)
+	err := c.cc.Invoke(ctx, "/gtime.TimeService/TestCall", in, out, opts...)
+	if err != nil {
+		return nil, err
+	}
+	return out, nil
+}
+
+// TimeServiceServer is the server API for TimeService service.
+// All implementations must embed UnimplementedTimeServiceServer
+// for forward compatibility
+type TimeServiceServer interface {
+	TestCall(context.Context, *CallRequest) (*CallReply, error)
+	mustEmbedUnimplementedTimeServiceServer()
+}
+
+// UnimplementedTimeServiceServer must be embedded to have forward compatible implementations.
+type UnimplementedTimeServiceServer struct {
+}
+
+func (UnimplementedTimeServiceServer) TestCall(context.Context, *CallRequest) (*CallReply, error) {
+	return nil, status.Errorf(codes.Unimplemented, "method TestCall not implemented")
+}
+func (UnimplementedTimeServiceServer) mustEmbedUnimplementedTimeServiceServer() {}
+
+// UnsafeTimeServiceServer may be embedded to opt out of forward compatibility for this service.
+// Use of this interface is not recommended, as added methods to TimeServiceServer will
+// result in compilation errors.
+type UnsafeTimeServiceServer interface {
+	mustEmbedUnimplementedTimeServiceServer()
+}
+
+func RegisterTimeServiceServer(s grpc.ServiceRegistrar, srv TimeServiceServer) {
+	s.RegisterService(&TimeService_ServiceDesc, srv)
+}
+
+func _TimeService_TestCall_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+	in := new(CallRequest)
+	if err := dec(in); err != nil {
+		return nil, err
+	}
+	if interceptor == nil {
+		return srv.(TimeServiceServer).TestCall(ctx, in)
+	}
+	info := &grpc.UnaryServerInfo{
+		Server:     srv,
+		FullMethod: "/gtime.TimeService/TestCall",
+	}
+	handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+		return srv.(TimeServiceServer).TestCall(ctx, req.(*CallRequest))
+	}
+	return interceptor(ctx, in, info, handler)
+}
+
+// TimeService_ServiceDesc is the grpc.ServiceDesc for TimeService service.
+// It's only intended for direct use with grpc.RegisterService,
+// and not to be introspected or modified (even as a copy)
+var TimeService_ServiceDesc = grpc.ServiceDesc{
+	ServiceName: "gtime.TimeService",
+	HandlerType: (*TimeServiceServer)(nil),
+	Methods: []grpc.MethodDesc{
+		{
+			MethodName: "TestCall",
+			Handler:    _TimeService_TestCall_Handler,
+		},
+	},
+	Streams:  []grpc.StreamDesc{},
+	Metadata: "gtime.proto",
+}
diff --git a/internal/gtime/server.go b/internal/gtime/server.go
new file mode 100644
index 0000000..ed03d5d
--- /dev/null
+++ b/internal/gtime/server.go
@@ -0,0 +1,26 @@
+package gtime
+
+import (
+	"context"
+	"strconv"
+	"time"
+)
+
+type TimeService struct {
+	UnimplementedTimeServiceServer
+
+	LastTimestamp time.Time
+	LastDuration  time.Duration
+}
+
+func (s *TimeService) TestCall(ctx context.Context, req *CallRequest) (*CallReply, error) {
+
+	s.LastTimestamp = req.GetTs().AsTime()
+	s.LastDuration = req.GetDur().AsDuration()
+
+	return &CallReply{
+		Ts:      req.GetTs(),
+		Dur:     req.GetDur(),
+		Message: strconv.FormatUint(req.GetUserId(), 10),
+	}, nil
+}


Test patch:
diff --git a/runner/run_test.go b/runner/run_test.go
index 11ba396..26a944e 100644
--- a/runner/run_test.go
+++ b/runner/run_test.go
@@ -2526,8 +2526,6 @@ func TestRunBidi(t *testing.T) {
 
 func TestRunUnarySecure(t *testing.T) {
 
-	t.Skip("asdf")
-
 	callType := helloworld.Unary
 
 	gs, s, err := internal.StartServer(true)
@@ -3023,3 +3021,96 @@ func TestRunWrappedUnary(t *testing.T) {
 		assert.Equal(t, report.Average, report.Fastest)
 	})
 }
+
+func TestRunGtimeUnary(t *testing.T) {
+
+	ts, s, err := internal.StartTimeServer(false)
+
+	if err != nil {
+		assert.FailNow(t, err.Error())
+	}
+
+	defer s.Stop()
+
+	t.Run("json string data", func(t *testing.T) {
+		data := `{"ts":"2020-01-20T01:30:30.01Z", "dur":"30s", "user_id":"12"}`
+
+		report, err := Run(
+			"gtime.TimeService.TestCall",
+			internal.TestLocalhost,
+			WithProtoFile("../testdata/gtime.proto", []string{"../testdata"}),
+			WithTotalRequests(1),
+			WithConcurrency(1),
+			WithTimeout(time.Duration(20*time.Second)),
+			WithDialTimeout(time.Duration(20*time.Second)),
+			WithDataFromJSON(data),
+			WithInsecure(true),
+		)
+
+		assert.NoError(t, err)
+
+		assert.NotNil(t, report)
+
+		assert.Equal(t, 1, int(report.Count))
+		assert.NotZero(t, report.Average)
+		assert.NotZero(t, report.Fastest)
+		assert.NotZero(t, report.Slowest)
+		assert.NotZero(t, report.Rps)
+		assert.Empty(t, report.Name)
+		assert.NotEmpty(t, report.Date)
+		assert.NotEmpty(t, report.Options)
+		assert.NotEmpty(t, report.Details)
+		assert.Equal(t, true, report.Options.Insecure)
+		assert.NotEmpty(t, report.LatencyDistribution)
+		assert.Equal(t, ReasonNormalEnd, report.EndReason)
+		assert.Empty(t, report.ErrorDist)
+
+		assert.Equal(t, report.Average, report.Slowest)
+		assert.Equal(t, report.Average, report.Fastest)
+
+		expectedTime, err := time.Parse(time.RFC3339, "2020-01-20T01:30:30.01Z")
+		assert.NoError(t, err)
+
+		expectedDur := time.Duration(30 * time.Second)
+
+		assert.Equal(t, expectedTime, ts.LastTimestamp)
+		assert.Equal(t, expectedDur, ts.LastDuration)
+	})
+
+	t.Run("json file data", func(t *testing.T) {
+
+		report, err := Run(
+			"",
+			internal.TestLocalhost,
+			WithConfigFromFile("../testdata/tsconfig.json"),
+		)
+
+		assert.NoError(t, err)
+		assert.NotNil(t, report)
+
+		assert.Equal(t, 1, int(report.Count))
+		assert.NotZero(t, report.Average)
+		assert.NotZero(t, report.Fastest)
+		assert.NotZero(t, report.Slowest)
+		assert.NotZero(t, report.Rps)
+		assert.Equal(t, "TimeService - TestCall", report.Name)
+		assert.NotEmpty(t, report.Date)
+		assert.NotEmpty(t, report.Options)
+		assert.NotEmpty(t, report.Details)
+		assert.Equal(t, true, report.Options.Insecure)
+		assert.NotEmpty(t, report.LatencyDistribution)
+		assert.Equal(t, ReasonNormalEnd, report.EndReason)
+		assert.Empty(t, report.ErrorDist)
+
+		assert.Equal(t, report.Average, report.Slowest)
+		assert.Equal(t, report.Average, report.Fastest)
+
+		expectedTime, err := time.Parse(time.RFC3339, "2020-01-22T02:30:30.01Z")
+		assert.NoError(t, err)
+
+		expectedDur := time.Duration(40 * time.Second)
+
+		assert.Equal(t, expectedTime, ts.LastTimestamp)
+		assert.Equal(t, expectedDur, ts.LastDuration)
+	})
+}
diff --git a/testdata/google/protobuf/duration.proto b/testdata/google/protobuf/duration.proto
new file mode 100644
index 0000000..99cb102
--- /dev/null
+++ b/testdata/google/protobuf/duration.proto
@@ -0,0 +1,116 @@
+// Protocol Buffers - Google's data interchange format
+// Copyright 2008 Google Inc.  All rights reserved.
+// https://developers.google.com/protocol-buffers/
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+//     * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+//     * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+//     * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+syntax = "proto3";
+
+package google.protobuf;
+
+option csharp_namespace = "Google.Protobuf.WellKnownTypes";
+option cc_enable_arenas = true;
+option go_package = "github.com/golang/protobuf/ptypes/duration";
+option java_package = "com.google.protobuf";
+option java_outer_classname = "DurationProto";
+option java_multiple_files = true;
+option objc_class_prefix = "GPB";
+
+// A Duration represents a signed, fixed-length span of time represented
+// as a count of seconds and fractions of seconds at nanosecond
+// resolution. It is independent of any calendar and concepts like "day"
+// or "month". It is related to Timestamp in that the difference between
+// two Timestamp values is a Duration and it can be added or subtracted
+// from a Timestamp. Range is approximately +-10,000 years.
+//
+// # Examples
+//
+// Example 1: Compute Duration from two Timestamps in pseudo code.
+//
+//     Timestamp start = ...;
+//     Timestamp end = ...;
+//     Duration duration = ...;
+//
+//     duration.seconds = end.seconds - start.seconds;
+//     duration.nanos = end.nanos - start.nanos;
+//
+//     if (duration.seconds < 0 && duration.nanos > 0) {
+//       duration.seconds += 1;
+//       duration.nanos -= 1000000000;
+//     } else if (duration.seconds > 0 && duration.nanos < 0) {
+//       duration.seconds -= 1;
+//       duration.nanos += 1000000000;
+//     }
+//
+// Example 2: Compute Timestamp from Timestamp + Duration in pseudo code.
+//
+//     Timestamp start = ...;
+//     Duration duration = ...;
+//     Timestamp end = ...;
+//
+//     end.seconds = start.seconds + duration.seconds;
+//     end.nanos = start.nanos + duration.nanos;
+//
+//     if (end.nanos < 0) {
+//       end.seconds -= 1;
+//       end.nanos += 1000000000;
+//     } else if (end.nanos >= 1000000000) {
+//       end.seconds += 1;
+//       end.nanos -= 1000000000;
+//     }
+//
+// Example 3: Compute Duration from datetime.timedelta in Python.
+//
+//     td = datetime.timedelta(days=3, minutes=10)
+//     duration = Duration()
+//     duration.FromTimedelta(td)
+//
+// # JSON Mapping
+//
+// In JSON format, the Duration type is encoded as a string rather than an
+// object, where the string ends in the suffix "s" (indicating seconds) and
+// is preceded by the number of seconds, with nanoseconds expressed as
+// fractional seconds. For example, 3 seconds with 0 nanoseconds should be
+// encoded in JSON format as "3s", while 3 seconds and 1 nanosecond should
+// be expressed in JSON format as "3.000000001s", and 3 seconds and 1
+// microsecond should be expressed in JSON format as "3.000001s".
+//
+//
+message Duration {
+  // Signed seconds of the span of time. Must be from -315,576,000,000
+  // to +315,576,000,000 inclusive. Note: these bounds are computed from:
+  // 60 sec/min * 60 min/hr * 24 hr/day * 365.25 days/year * 10000 years
+  int64 seconds = 1;
+
+  // Signed fractions of a second at nanosecond resolution of the span
+  // of time. Durations less than one second are represented with a 0
+  // `seconds` field and a positive or negative `nanos` field. For durations
+  // of one second or more, a non-zero value for the `nanos` field must be
+  // of the same sign as the `seconds` field. Must be from -999,999,999
+  // to +999,999,999 inclusive.
+  int32 nanos = 2;
+}
diff --git a/testdata/google/protobuf/timestamp.proto b/testdata/google/protobuf/timestamp.proto
new file mode 100644
index 0000000..cd35786
--- /dev/null
+++ b/testdata/google/protobuf/timestamp.proto
@@ -0,0 +1,138 @@
+// Protocol Buffers - Google's data interchange format
+// Copyright 2008 Google Inc.  All rights reserved.
+// https://developers.google.com/protocol-buffers/
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+//     * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+//     * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+//     * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+syntax = "proto3";
+
+package google.protobuf;
+
+option csharp_namespace = "Google.Protobuf.WellKnownTypes";
+option cc_enable_arenas = true;
+option go_package = "github.com/golang/protobuf/ptypes/timestamp";
+option java_package = "com.google.protobuf";
+option java_outer_classname = "TimestampProto";
+option java_multiple_files = true;
+option objc_class_prefix = "GPB";
+
+// A Timestamp represents a point in time independent of any time zone or local
+// calendar, encoded as a count of seconds and fractions of seconds at
+// nanosecond resolution. The count is relative to an epoch at UTC midnight on
+// January 1, 1970, in the proleptic Gregorian calendar which extends the
+// Gregorian calendar backwards to year one.
+//
+// All minutes are 60 seconds long. Leap seconds are "smeared" so that no leap
+// second table is needed for interpretation, using a [24-hour linear
+// smear](https://developers.google.com/time/smear).
+//
+// The range is from 0001-01-01T00:00:00Z to 9999-12-31T23:59:59.999999999Z. By
+// restricting to that range, we ensure that we can convert to and from [RFC
+// 3339](https://www.ietf.org/rfc/rfc3339.txt) date strings.
+//
+// # Examples
+//
+// Example 1: Compute Timestamp from POSIX `time()`.
+//
+//     Timestamp timestamp;
+//     timestamp.set_seconds(time(NULL));
+//     timestamp.set_nanos(0);
+//
+// Example 2: Compute Timestamp from POSIX `gettimeofday()`.
+//
+//     struct timeval tv;
+//     gettimeofday(&tv, NULL);
+//
+//     Timestamp timestamp;
+//     timestamp.set_seconds(tv.tv_sec);
+//     timestamp.set_nanos(tv.tv_usec * 1000);
+//
+// Example 3: Compute Timestamp from Win32 `GetSystemTimeAsFileTime()`.
+//
+//     FILETIME ft;
+//     GetSystemTimeAsFileTime(&ft);
+//     UINT64 ticks = (((UINT64)ft.dwHighDateTime) << 32) | ft.dwLowDateTime;
+//
+//     // A Windows tick is 100 nanoseconds. Windows epoch 1601-01-01T00:00:00Z
+//     // is 11644473600 seconds before Unix epoch 1970-01-01T00:00:00Z.
+//     Timestamp timestamp;
+//     timestamp.set_seconds((INT64) ((ticks / 10000000) - 11644473600LL));
+//     timestamp.set_nanos((INT32) ((ticks % 10000000) * 100));
+//
+// Example 4: Compute Timestamp from Java `System.currentTimeMillis()`.
+//
+//     long millis = System.currentTimeMillis();
+//
+//     Timestamp timestamp = Timestamp.newBuilder().setSeconds(millis / 1000)
+//         .setNanos((int) ((millis % 1000) * 1000000)).build();
+//
+//
+// Example 5: Compute Timestamp from current time in Python.
+//
+//     timestamp = Timestamp()
+//     timestamp.GetCurrentTime()
+//
+// # JSON Mapping
+//
+// In JSON format, the Timestamp type is encoded as a string in the
+// [RFC 3339](https://www.ietf.org/rfc/rfc3339.txt) format. That is, the
+// format is "{year}-{month}-{day}T{hour}:{min}:{sec}[.{frac_sec}]Z"
+// where {year} is always expressed using four digits while {month}, {day},
+// {hour}, {min}, and {sec} are zero-padded to two digits each. The fractional
+// seconds, which can go up to 9 digits (i.e. up to 1 nanosecond resolution),
+// are optional. The "Z" suffix indicates the timezone ("UTC"); the timezone
+// is required. A proto3 JSON serializer should always use UTC (as indicated by
+// "Z") when printing the Timestamp type and a proto3 JSON parser should be
+// able to accept both UTC and other timezones (as indicated by an offset).
+//
+// For example, "2017-01-15T01:30:15.01Z" encodes 15.01 seconds past
+// 01:30 UTC on January 15, 2017.
+//
+// In JavaScript, one can convert a Date object to this format using the
+// standard
+// [toISOString()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString)
+// method. In Python, a standard `datetime.datetime` object can be converted
+// to this format using
+// [`strftime`](https://docs.python.org/2/library/time.html#time.strftime) with
+// the time format spec '%Y-%m-%dT%H:%M:%S.%fZ'. Likewise, in Java, one can use
+// the Joda Time's [`ISODateTimeFormat.dateTime()`](
+// http://www.joda.org/joda-time/apidocs/org/joda/time/format/ISODateTimeFormat.html#dateTime%2D%2D
+// ) to obtain a formatter capable of generating timestamps in this format.
+//
+//
+message Timestamp {
+  // Represents seconds of UTC time since Unix epoch
+  // 1970-01-01T00:00:00Z. Must be from 0001-01-01T00:00:00Z to
+  // 9999-12-31T23:59:59Z inclusive.
+  int64 seconds = 1;
+
+  // Non-negative fractions of a second at nanosecond resolution. Negative
+  // second values with fractions must still have non-negative nanos values
+  // that count forward in time. Must be from 0 to 999,999,999
+  // inclusive.
+  int32 nanos = 2;
+}
diff --git a/testdata/gtime.proto b/testdata/gtime.proto
new file mode 100644
index 0000000..b2364bb
--- /dev/null
+++ b/testdata/gtime.proto
@@ -0,0 +1,24 @@
+syntax = "proto3";
+
+package gtime;
+
+option go_package = "internal/gtime";
+
+import "google/protobuf/timestamp.proto";
+import "google/protobuf/duration.proto";
+
+service TimeService {
+    rpc TestCall (CallRequest) returns (CallReply) {}
+}
+
+message CallRequest {
+    google.protobuf.Timestamp ts = 1;
+    google.protobuf.Duration dur = 2;
+    uint64 user_id = 3;
+}
+  
+message CallReply {
+    google.protobuf.Timestamp ts = 1;
+    google.protobuf.Duration dur = 2;
+    string message = 3;
+}
diff --git a/testdata/tsconfig.json b/testdata/tsconfig.json
new file mode 100644
index 0000000..816a24d
--- /dev/null
+++ b/testdata/tsconfig.json
@@ -0,0 +1,14 @@
+{
+    "proto": "../testdata/gtime.proto",
+    "call": "gtime.TimeService.TestCall",
+    "name": "TimeService - TestCall",
+    "total": 1,
+    "concurrency": 1,
+    "connections": 1,
+    "insecure": true,
+    "data": {
+      "ts":"2020-01-22T02:30:30.01Z",
+      "dur": "40s",
+      "user_id": 100
+    }
+  }
\ No newline at end of file