Repository: bojand/ghz
Pull request #178 (2020-04-14 20:05:41)

Add WithConfig options for runner
I prefer to use config files for configuring calls, but also like to use this as a library. Thus I added WithConfig* options for the runner. I thought I'd create a PR for it in case this is something more people would like to use.


Patch:
diff --git a/cmd/ghz/config.go b/cmd/ghz/config.go
deleted file mode 100644
index 94e0b0e..0000000
--- a/cmd/ghz/config.go
+++ /dev/null
@@ -1,180 +0,0 @@
-package main
-
-import (
-	"encoding/json"
-	"errors"
-	"strings"
-	"time"
-)
-
-// Duration is our duration with TOML support
-type Duration time.Duration
-
-// UnmarshalText is our custom unmarshaller with TOML support
-func (d *Duration) UnmarshalText(text []byte) error {
-	dur, err := time.ParseDuration(string(text))
-	if err != nil {
-		return err
-	}
-
-	*d = Duration(dur)
-	return nil
-}
-
-// MarshalText implements encoding.TextMarshaler
-func (d Duration) MarshalText() ([]byte, error) {
-	return []byte(time.Duration(d).String()), nil
-}
-
-func (d Duration) String() string {
-	return time.Duration(d).String()
-}
-
-// config for the run.
-type config struct {
-	Proto             string             `json:"proto" toml:"proto" yaml:"proto"`
-	Protoset          string             `json:"protoset" toml:"protoset" yaml:"protoset"`
-	Call              string             `json:"call" toml:"call" yaml:"call"`
-	RootCert          string             `json:"cacert" toml:"cacert" yaml:"cacert"`
-	Cert              string             `json:"cert" toml:"cert" yaml:"cert"`
-	Key               string             `json:"key" toml:"key" yaml:"key"`
-	SkipTLSVerify     bool               `json:"skipTLS" toml:"skipTLS" yaml:"skipTLS"`
-	CName             string             `json:"cname" toml:"cname" yaml:"cname"`
-	Authority         string             `json:"authority" toml:"authority" yaml:"authority"`
-	Insecure          bool               `json:"insecure,omitempty" toml:"insecure,omitempty" yaml:"insecure,omitempty"`
-	N                 uint               `json:"total" toml:"total" yaml:"total" default:"200"`
-	C                 uint               `json:"concurrency" toml:"concurrency" yaml:"concurrency" default:"50"`
-	Connections       uint               `json:"connections" toml:"connections" yaml:"connections" default:"1"`
-	QPS               uint               `json:"qps" toml:"qps" yaml:"qps"`
-	Z                 Duration           `json:"duration" toml:"duration" yaml:"duration"`
-	ZStop             string             `json:"duration-stop" toml:"duration-stop" yaml:"duration-stop" default:"close"`
-	X                 Duration           `json:"max-duration" toml:"max-duration" yaml:"max-duration"`
-	Timeout           Duration           `json:"timeout" toml:"timeout" yaml:"timeout" default:"20s"`
-	Data              interface{}        `json:"data,omitempty" toml:"data,omitempty" yaml:"data,omitempty"`
-	DataPath          string             `json:"data-file" toml:"data-file" yaml:"data-file"`
-	BinData           []byte             `json:"-" toml:"-" yaml:"-"`
-	BinDataPath       string             `json:"binary-file" toml:"binary-file" yaml:"binary-file"`
-	Metadata          *map[string]string `json:"metadata,omitempty" toml:"metadata,omitempty" yaml:"metadata,omitempty"`
-	MetadataPath      string             `json:"metadata-file" toml:"metadata-file" yaml:"metadata-file"`
-	SI                Duration           `json:"stream-interval" toml:"stream-interval" yaml:"stream-interval"`
-	Output            string             `json:"output" toml:"output" yaml:"output"`
-	Format            string             `json:"format" toml:"format" yaml:"format" default:"summary"`
-	DialTimeout       Duration           `json:"connect-timeout" toml:"connect-timeout" yaml:"connect-timeout" default:"10s"`
-	KeepaliveTime     Duration           `json:"keepalive" toml:"keepalive" yaml:"keepalive"`
-	CPUs              uint               `json:"cpus" toml:"cpus" yaml:"cpus"`
-	ImportPaths       []string           `json:"import-paths,omitempty" toml:"import-paths,omitempty" yaml:"import-paths,omitempty"`
-	Name              string             `json:"name,omitempty" toml:"name,omitempty" yaml:"name,omitempty"`
-	Tags              *map[string]string `json:"tags,omitempty" toml:"tags,omitempty" yaml:"tags,omitempty"`
-	ReflectMetadata   *map[string]string `json:"reflect-metadata,omitempty" toml:"reflect-metadata,omitempty" yaml:"reflect-metadata,omitempty"`
-	Debug             string             `json:"debug,omitempty" toml:"debug,omitempty" yaml:"debug,omitempty"`
-	Host              string             `json:"host" toml:"host" yaml:"host"`
-	EnableCompression bool               `json:"enable-compression,omitempty" toml:"enable-compression,omitempty" yaml:"enable-compression,omitempty"`
-}
-
-// UnmarshalJSON is our custom implementation to handle the Duration fields
-// and validate data
-func (c *config) UnmarshalJSON(data []byte) error {
-	type Alias config
-	aux := &struct {
-		Z       string `json:"duration"`
-		X       string `json:"max-duration"`
-		SI      string `json:"stream-interval"`
-		Timeout string `json:"timeout"`
-		*Alias
-	}{
-		Alias: (*Alias)(c),
-	}
-	if err := json.Unmarshal(data, &aux); err != nil {
-		return err
-	}
-
-	if aux.Data != nil {
-		err := checkData(aux.Data)
-		if err != nil {
-			return err
-		}
-	}
-
-	if aux.Z != "" {
-		zd, err := time.ParseDuration(aux.Z)
-		if err != nil {
-			return nil
-		}
-
-		c.Z = Duration(zd)
-	}
-
-	aux.ZStop = strings.ToLower(aux.ZStop)
-	if aux.ZStop != "close" && aux.ZStop != "ignore" && aux.ZStop != "wait" {
-		aux.ZStop = "close"
-	}
-
-	if aux.X != "" {
-		xd, err := time.ParseDuration(aux.X)
-		if err != nil {
-			return nil
-		}
-
-		c.X = Duration(xd)
-	}
-
-	if aux.SI != "" {
-		sid, err := time.ParseDuration(aux.SI)
-		if err != nil {
-			return nil
-		}
-
-		c.SI = Duration(sid)
-	}
-
-	if aux.Timeout != "" {
-		td, err := time.ParseDuration(aux.Timeout)
-		if err != nil {
-			return nil
-		}
-
-		c.Timeout = Duration(td)
-	}
-
-	return nil
-}
-
-// MarshalJSON is our custom implementation to handle the Duration fields
-func (c config) MarshalJSON() ([]byte, error) {
-	type Alias config
-	return json.Marshal(&struct {
-		*Alias
-		Z       string `json:"duration"`
-		X       string `json:"max-duration"`
-		SI      string `json:"stream-interval"`
-		Timeout string `json:"timeout"`
-	}{
-		Alias:   (*Alias)(&c),
-		Z:       c.Z.String(),
-		X:       c.X.String(),
-		SI:      c.SI.String(),
-		Timeout: c.Timeout.String(),
-	})
-}
-
-func checkData(data interface{}) error {
-	_, isObjData := data.(map[string]interface{})
-	if !isObjData {
-		arrData, isArrData := data.([]interface{})
-		if !isArrData {
-			return errors.New("Unsupported type for Data")
-		}
-		if len(arrData) == 0 {
-			return errors.New("Data array must not be empty")
-		}
-		for _, elem := range arrData {
-			_, isObjData = elem.(map[string]interface{})
-			if !isObjData {
-				return errors.New("Data array contains unsupported type")
-			}
-		}
-
-	}
-
-	return nil
-}
diff --git a/cmd/ghz/main.go b/cmd/ghz/main.go
index 1067c96..20c2d97 100644
--- a/cmd/ghz/main.go
+++ b/cmd/ghz/main.go
@@ -10,14 +10,14 @@ import (
 	"runtime"
 	"strconv"
 	"strings"
-	"time"
 
 	"github.com/alecthomas/kingpin"
-	"github.com/bojand/ghz/printer"
-	"github.com/bojand/ghz/runner"
 	"github.com/jinzhu/configor"
 	"go.uber.org/zap"
 	"go.uber.org/zap/zapcore"
+
+	"github.com/bojand/ghz/printer"
+	"github.com/bojand/ghz/runner"
 )
 
 var (
@@ -194,7 +194,7 @@ func main() {
 
 	cfgPath := strings.TrimSpace(*cPath)
 
-	var cfg config
+	var cfg runner.Config
 
 	if cfgPath != "" {
 		err := configor.Load(&cfg, cfgPath)
@@ -202,7 +202,7 @@ func main() {
 
 		args := os.Args[1:]
 		if len(args) > 1 {
-			var cmdCfg config
+			var cmdCfg runner.Config
 			err = createConfigFromArgs(&cmdCfg)
 			kingpin.FatalIfError(err, "")
 
@@ -221,60 +221,9 @@ func main() {
 	} else if cfg.Z > 0 {
 		cfg.N = math.MaxInt32
 	}
-
-	// set up all the options
-	options := make([]runner.Option, 0, 15)
-
-	options = append(options,
-		runner.WithProtoFile(cfg.Proto, cfg.ImportPaths),
-		runner.WithProtoset(cfg.Protoset),
-		runner.WithRootCertificate(cfg.RootCert),
-		runner.WithCertificate(cfg.Cert, cfg.Key),
-		runner.WithServerNameOverride(cfg.CName),
-		runner.WithSkipTLSVerify(cfg.SkipTLSVerify),
-		runner.WithInsecure(cfg.Insecure),
-		runner.WithAuthority(cfg.Authority),
-		runner.WithConcurrency(cfg.C),
-		runner.WithTotalRequests(cfg.N),
-		runner.WithQPS(cfg.QPS),
-		runner.WithTimeout(time.Duration(cfg.Timeout)),
-		runner.WithRunDuration(time.Duration(cfg.Z)),
-		runner.WithDurationStopAction(cfg.ZStop),
-		runner.WithDialTimeout(time.Duration(cfg.DialTimeout)),
-		runner.WithKeepalive(time.Duration(cfg.KeepaliveTime)),
-		runner.WithName(cfg.Name),
-		runner.WithCPUs(cfg.CPUs),
-		runner.WithMetadata(cfg.Metadata),
-		runner.WithTags(cfg.Tags),
-		runner.WithStreamInterval(time.Duration(cfg.SI)),
-		runner.WithReflectionMetadata(cfg.ReflectMetadata),
-		runner.WithConnections(cfg.Connections),
-		runner.WithEnableCompression(cfg.EnableCompression),
-	)
-
-	if strings.TrimSpace(cfg.MetadataPath) != "" {
-		options = append(options, runner.WithMetadataFromFile(strings.TrimSpace(cfg.MetadataPath)))
-	}
-
-	// data
-	if dataStr, ok := cfg.Data.(string); ok && dataStr == "@" {
-		options = append(options, runner.WithDataFromReader(os.Stdin))
-	} else if strings.TrimSpace(cfg.DataPath) != "" {
-		options = append(options, runner.WithDataFromFile(strings.TrimSpace(cfg.DataPath)))
-	} else {
-		options = append(options, runner.WithData(cfg.Data))
-	}
-
-	// or binary data
-	if len(cfg.BinData) > 0 {
-		options = append(options, runner.WithBinaryData(cfg.BinData))
-	}
-	if len(cfg.BinDataPath) > 0 {
-		options = append(options, runner.WithBinaryDataFromFile(cfg.BinDataPath))
-	}
-
 	var logger *zap.SugaredLogger
 
+	options := []runner.Option{runner.WithConfig(&cfg)}
 	if len(cfg.Debug) > 0 {
 		var err error
 		logger, err = createLogger(cfg.Debug)
@@ -349,7 +298,7 @@ func handleError(err error) {
 	}
 }
 
-func createConfigFromArgs(cfg *config) error {
+func createConfigFromArgs(cfg *runner.Config) error {
 	if cfg == nil {
 		return errors.New("config cannot be nil")
 	}
@@ -415,9 +364,9 @@ func createConfigFromArgs(cfg *config) error {
 	cfg.N = *n
 	cfg.C = *c
 	cfg.QPS = *q
-	cfg.Z = Duration(*z)
-	cfg.X = Duration(*x)
-	cfg.Timeout = Duration(*t)
+	cfg.Z = runner.Duration(*z)
+	cfg.X = runner.Duration(*x)
+	cfg.Timeout = runner.Duration(*t)
 	cfg.ZStop = *zstop
 	cfg.Data = dataObj
 	cfg.DataPath = *dataPath
@@ -425,13 +374,13 @@ func createConfigFromArgs(cfg *config) error {
 	cfg.BinDataPath = *binPath
 	cfg.Metadata = &metadata
 	cfg.MetadataPath = *mdPath
-	cfg.SI = Duration(*si)
+	cfg.SI = runner.Duration(*si)
 	cfg.Output = *output
 	cfg.Format = *format
 	cfg.ImportPaths = iPaths
 	cfg.Connections = *conns
-	cfg.DialTimeout = Duration(*ct)
-	cfg.KeepaliveTime = Duration(*kt)
+	cfg.DialTimeout = runner.Duration(*ct)
+	cfg.KeepaliveTime = runner.Duration(*kt)
 	cfg.CPUs = *cpus
 	cfg.Name = *name
 	cfg.Tags = &tagsMap
@@ -442,7 +391,7 @@ func createConfigFromArgs(cfg *config) error {
 	return nil
 }
 
-func mergeConfig(dest *config, src *config) error {
+func mergeConfig(dest *runner.Config, src *runner.Config) error {
 	if src == nil || dest == nil {
 		return errors.New("config cannot be nil")
 	}
diff --git a/go.sum b/go.sum
index 3f9bf3a..6409a9d 100644
--- a/go.sum
+++ b/go.sum
@@ -139,12 +139,8 @@ github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyC
 github.com/valyala/fasttemplate v1.0.1 h1:tY9CJiPnMXf1ERmG2EyK7gNUd+c6RKGD0IfU8WdUSz8=
 github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
 go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
-go.uber.org/atomic v1.4.0 h1:cxzIVoETapQEqDhQu3QfnvXAV4AlzcvUCxkVUFw3+EU=
-go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
 go.uber.org/atomic v1.5.0 h1:OI5t8sDa1Or+q8AeE+yKeB/SDYioSHAgcVljj9JIETY=
 go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
-go.uber.org/multierr v1.2.0 h1:6I+W7f5VwC5SV9dNrZ3qXrDB9mD0dyGOi/ZJmYw03T4=
-go.uber.org/multierr v1.2.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
 go.uber.org/multierr v1.3.0 h1:sFPn2GLc3poCkfrpIXGhBD2X0CMIo4Q/zSULXrj/+uc=
 go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4=
 go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee h1:0mgffUl7nfd+FpvXMVz4IDEaUSmT1ysygQC7qYo7sG4=
diff --git a/internal/common.go b/internal/common.go
index 01f6f3a..273322b 100644
--- a/internal/common.go
+++ b/internal/common.go
@@ -10,6 +10,7 @@ import (
 
 	"github.com/bojand/ghz/internal/helloworld"
 	"github.com/bojand/ghz/internal/sleep"
+	"github.com/bojand/ghz/internal/wrapped"
 )
 
 // TestPort is the port.
@@ -95,3 +96,36 @@ func StartSleepServer(secure bool) (*sleep.SleepService, *grpc.Server, error) {
 
 	return &ss, s, err
 }
+
+// StartWrappedServer starts the wrapped test server
+func StartWrappedServer(secure bool) (*wrapped.WrappedService, *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...)
+
+	ws := wrapped.WrappedService{}
+	wrapped.RegisterWrappedServiceServer(s, &ws)
+	reflection.Register(s)
+
+	TestPort = strconv.Itoa(lis.Addr().(*net.TCPAddr).Port)
+	TestLocalhost = "localhost:" + TestPort
+
+	go func() {
+		_ = s.Serve(lis)
+	}()
+
+	return &ws, s, err
+}
diff --git a/internal/wrapped/server.go b/internal/wrapped/server.go
new file mode 100644
index 0000000..3ca4527
--- /dev/null
+++ b/internal/wrapped/server.go
@@ -0,0 +1,13 @@
+package wrapped
+
+import (
+	"context"
+
+	wrappers "github.com/golang/protobuf/ptypes/wrappers"
+)
+
+type WrappedService struct{}
+
+func (s *WrappedService) GetMessage(ctx context.Context, req *wrappers.StringValue) (*wrappers.StringValue, error) {
+	return &wrappers.StringValue{Value: "Hello: " + req.GetValue()}, nil
+}
diff --git a/internal/wrapped/wrapped.pb.go b/internal/wrapped/wrapped.pb.go
new file mode 100644
index 0000000..d3eb982
--- /dev/null
+++ b/internal/wrapped/wrapped.pb.go
@@ -0,0 +1,121 @@
+// Code generated by protoc-gen-go. DO NOT EDIT.
+// source: wrapped.proto
+
+package wrapped
+
+import (
+	context "context"
+	fmt "fmt"
+	proto "github.com/golang/protobuf/proto"
+	wrappers "github.com/golang/protobuf/ptypes/wrappers"
+	grpc "google.golang.org/grpc"
+	codes "google.golang.org/grpc/codes"
+	status "google.golang.org/grpc/status"
+	math "math"
+)
+
+// Reference imports to suppress errors if they are not otherwise used.
+var _ = proto.Marshal
+var _ = fmt.Errorf
+var _ = math.Inf
+
+// This is a compile-time assertion to ensure that this generated file
+// is compatible with the proto package it is being compiled against.
+// A compilation error at this line likely means your copy of the
+// proto package needs to be updated.
+const _ = proto.ProtoPackageIsVersion3 // please upgrade the proto package
+
+func init() { proto.RegisterFile("wrapped.proto", fileDescriptor_a9aa7f80a2b4490d) }
+
+var fileDescriptor_a9aa7f80a2b4490d = []byte{
+	// 135 bytes of a gzipped FileDescriptorProto
+	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0xe2, 0x2d, 0x2f, 0x4a, 0x2c,
+	0x28, 0x48, 0x4d, 0xd1, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0x62, 0x87, 0x72, 0xa5, 0xe4, 0xd2,
+	0xf3, 0xf3, 0xd3, 0x73, 0x52, 0xf5, 0xc1, 0xc2, 0x49, 0xa5, 0x69, 0xfa, 0x10, 0x89, 0xa2, 0x62,
+	0x88, 0x42, 0xa3, 0x28, 0x2e, 0xbe, 0x70, 0x88, 0xd2, 0xe0, 0xd4, 0xa2, 0xb2, 0xcc, 0xe4, 0x54,
+	0x21, 0x0f, 0x2e, 0x2e, 0xf7, 0xd4, 0x12, 0xdf, 0xd4, 0xe2, 0xe2, 0xc4, 0xf4, 0x54, 0x21, 0x19,
+	0x3d, 0x88, 0x01, 0x7a, 0x30, 0x03, 0xf4, 0x82, 0x4b, 0x8a, 0x32, 0xf3, 0xd2, 0xc3, 0x12, 0x73,
+	0x4a, 0x53, 0xa5, 0xf0, 0xca, 0x3a, 0x09, 0x45, 0x09, 0x64, 0xe6, 0x95, 0xa4, 0x16, 0xe5, 0x25,
+	0xe6, 0x40, 0xad, 0x4d, 0x49, 0x62, 0x03, 0xab, 0x34, 0x06, 0x04, 0x00, 0x00, 0xff, 0xff, 0xfd,
+	0xf7, 0x3c, 0x4d, 0xb0, 0x00, 0x00, 0x00,
+}
+
+// Reference imports to suppress errors if they are not otherwise used.
+var _ context.Context
+var _ grpc.ClientConn
+
+// This is a compile-time assertion to ensure that this generated file
+// is compatible with the grpc package it is being compiled against.
+const _ = grpc.SupportPackageIsVersion4
+
+// WrappedServiceClient is the client API for WrappedService service.
+//
+// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream.
+type WrappedServiceClient interface {
+	GetMessage(ctx context.Context, in *wrappers.StringValue, opts ...grpc.CallOption) (*wrappers.StringValue, error)
+}
+
+type wrappedServiceClient struct {
+	cc *grpc.ClientConn
+}
+
+func NewWrappedServiceClient(cc *grpc.ClientConn) WrappedServiceClient {
+	return &wrappedServiceClient{cc}
+}
+
+func (c *wrappedServiceClient) GetMessage(ctx context.Context, in *wrappers.StringValue, opts ...grpc.CallOption) (*wrappers.StringValue, error) {
+	out := new(wrappers.StringValue)
+	err := c.cc.Invoke(ctx, "/wrapped.WrappedService/GetMessage", in, out, opts...)
+	if err != nil {
+		return nil, err
+	}
+	return out, nil
+}
+
+// WrappedServiceServer is the server API for WrappedService service.
+type WrappedServiceServer interface {
+	GetMessage(context.Context, *wrappers.StringValue) (*wrappers.StringValue, error)
+}
+
+// UnimplementedWrappedServiceServer can be embedded to have forward compatible implementations.
+type UnimplementedWrappedServiceServer struct {
+}
+
+func (*UnimplementedWrappedServiceServer) GetMessage(ctx context.Context, req *wrappers.StringValue) (*wrappers.StringValue, error) {
+	return nil, status.Errorf(codes.Unimplemented, "method GetMessage not implemented")
+}
+
+func RegisterWrappedServiceServer(s *grpc.Server, srv WrappedServiceServer) {
+	s.RegisterService(&_WrappedService_serviceDesc, srv)
+}
+
+func _WrappedService_GetMessage_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+	in := new(wrappers.StringValue)
+	if err := dec(in); err != nil {
+		return nil, err
+	}
+	if interceptor == nil {
+		return srv.(WrappedServiceServer).GetMessage(ctx, in)
+	}
+	info := &grpc.UnaryServerInfo{
+		Server:     srv,
+		FullMethod: "/wrapped.WrappedService/GetMessage",
+	}
+	handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+		return srv.(WrappedServiceServer).GetMessage(ctx, req.(*wrappers.StringValue))
+	}
+	return interceptor(ctx, in, info, handler)
+}
+
+var _WrappedService_serviceDesc = grpc.ServiceDesc{
+	ServiceName: "wrapped.WrappedService",
+	HandlerType: (*WrappedServiceServer)(nil),
+	Methods: []grpc.MethodDesc{
+		{
+			MethodName: "GetMessage",
+			Handler:    _WrappedService_GetMessage_Handler,
+		},
+	},
+	Streams:  []grpc.StreamDesc{},
+	Metadata: "wrapped.proto",
+}
diff --git a/runner/config.go b/runner/config.go
new file mode 100644
index 0000000..eaf0e86
--- /dev/null
+++ b/runner/config.go
@@ -0,0 +1,180 @@
+package runner
+
+import (
+	"encoding/json"
+	"errors"
+	"strings"
+	"time"
+)
+
+// Duration is our duration with TOML support
+type Duration time.Duration
+
+// UnmarshalText is our custom unmarshaller with TOML support
+func (d *Duration) UnmarshalText(text []byte) error {
+	dur, err := time.ParseDuration(string(text))
+	if err != nil {
+		return err
+	}
+
+	*d = Duration(dur)
+	return nil
+}
+
+// MarshalText implements encoding.TextMarshaler
+func (d Duration) MarshalText() ([]byte, error) {
+	return []byte(time.Duration(d).String()), nil
+}
+
+func (d Duration) String() string {
+	return time.Duration(d).String()
+}
+
+// Config for the run.
+type Config struct {
+	Proto           string             `json:"proto" toml:"proto" yaml:"proto"`
+	Protoset        string             `json:"protoset" toml:"protoset" yaml:"protoset"`
+	Call            string             `json:"call" toml:"call" yaml:"call" required:"true"`
+	RootCert        string             `json:"cacert" toml:"cacert" yaml:"cacert"`
+	Cert            string             `json:"cert" toml:"cert" yaml:"cert"`
+	Key             string             `json:"key" toml:"key" yaml:"key"`
+	SkipTLSVerify   bool               `json:"skipTLS" toml:"skipTLS" yaml:"skipTLS"`
+	CName           string             `json:"cname" toml:"cname" yaml:"cname"`
+	Authority       string             `json:"authority" toml:"authority" yaml:"authority"`
+	Insecure        bool               `json:"insecure,omitempty" toml:"insecure,omitempty" yaml:"insecure,omitempty"`
+	N               uint               `json:"total" toml:"total" yaml:"total" default:"200"`
+	C               uint               `json:"concurrency" toml:"concurrency" yaml:"concurrency" default:"50"`
+	Connections     uint               `json:"connections" toml:"connections" yaml:"connections" default:"1"`
+	QPS             uint               `json:"qps" toml:"qps" yaml:"qps"`
+	Z               Duration           `json:"duration" toml:"duration" yaml:"duration"`
+	ZStop             string             `json:"duration-stop" toml:"duration-stop" yaml:"duration-stop" default:"close"`
+	X               Duration           `json:"max-duration" toml:"max-duration" yaml:"max-duration"`
+	Timeout         Duration           `json:"timeout" toml:"timeout" yaml:"timeout" default:"20s"`
+	Data            interface{}        `json:"data,omitempty" toml:"data,omitempty" yaml:"data,omitempty"`
+	DataPath        string             `json:"data-file" toml:"data-file" yaml:"data-file"`
+	BinData         []byte             `json:"-" toml:"-" yaml:"-"`
+	BinDataPath     string             `json:"binary-file" toml:"binary-file" yaml:"binary-file"`
+	Metadata        *map[string]string `json:"metadata,omitempty" toml:"metadata,omitempty" yaml:"metadata,omitempty"`
+	MetadataPath    string             `json:"metadata-file" toml:"metadata-file" yaml:"metadata-file"`
+	SI              Duration           `json:"stream-interval" toml:"stream-interval" yaml:"stream-interval"`
+	Output          string             `json:"output" toml:"output" yaml:"output"`
+	Format          string             `json:"format" toml:"format" yaml:"format" default:"summary"`
+	DialTimeout     Duration           `json:"connect-timeout" toml:"connect-timeout" yaml:"connect-timeout" default:"10s"`
+	KeepaliveTime   Duration           `json:"keepalive" toml:"keepalive" yaml:"keepalive"`
+	CPUs            uint               `json:"cpus" toml:"cpus" yaml:"cpus"`
+	ImportPaths     []string           `json:"import-paths,omitempty" toml:"import-paths,omitempty" yaml:"import-paths,omitempty"`
+	Name            string             `json:"name,omitempty" toml:"name,omitempty" yaml:"name,omitempty"`
+	Tags            *map[string]string `json:"tags,omitempty" toml:"tags,omitempty" yaml:"tags,omitempty"`
+	ReflectMetadata *map[string]string `json:"reflect-metadata,omitempty" toml:"reflect-metadata,omitempty" yaml:"reflect-metadata,omitempty"`
+	Debug             string             `json:"debug,omitempty" toml:"debug,omitempty" yaml:"debug,omitempty"`
+	Host            string             `json:"host" toml:"host" yaml:"host"`
+	EnableCompression bool               `json:"enable-compression,omitempty" toml:"enable-compression,omitempty" yaml:"enable-compression,omitempty"`
+}
+
+// UnmarshalJSON is our custom implementation to handle the Duration fields
+// and validate data
+func (c *Config) UnmarshalJSON(data []byte) error {
+	type Alias Config
+	aux := &struct {
+		Z       string `json:"duration"`
+		X       string `json:"max-duration"`
+		SI      string `json:"stream-interval"`
+		Timeout string `json:"timeout"`
+		*Alias
+	}{
+		Alias: (*Alias)(c),
+	}
+	if err := json.Unmarshal(data, &aux); err != nil {
+		return err
+	}
+
+	if aux.Data != nil {
+		err := checkData(aux.Data)
+		if err != nil {
+			return err
+		}
+	}
+
+	if aux.Z != "" {
+		zd, err := time.ParseDuration(aux.Z)
+		if err != nil {
+			return nil
+		}
+
+		c.Z = Duration(zd)
+	}
+
+	aux.ZStop = strings.ToLower(aux.ZStop)
+	if aux.ZStop != "close" && aux.ZStop != "ignore" && aux.ZStop != "wait" {
+		aux.ZStop = "close"
+	}
+
+	if aux.X != "" {
+		xd, err := time.ParseDuration(aux.X)
+		if err != nil {
+			return nil
+		}
+
+		c.X = Duration(xd)
+	}
+
+	if aux.SI != "" {
+		sid, err := time.ParseDuration(aux.SI)
+		if err != nil {
+			return nil
+		}
+
+		c.SI = Duration(sid)
+	}
+
+	if aux.Timeout != "" {
+		td, err := time.ParseDuration(aux.Timeout)
+		if err != nil {
+			return nil
+		}
+
+		c.Timeout = Duration(td)
+	}
+
+	return nil
+}
+
+// MarshalJSON is our custom implementation to handle the Duration fields
+func (c Config) MarshalJSON() ([]byte, error) {
+	type Alias Config
+	return json.Marshal(&struct {
+		*Alias
+		Z       string `json:"duration"`
+		X       string `json:"max-duration"`
+		SI      string `json:"stream-interval"`
+		Timeout string `json:"timeout"`
+	}{
+		Alias:   (*Alias)(&c),
+		Z:       c.Z.String(),
+		X:       c.X.String(),
+		SI:      c.SI.String(),
+		Timeout: c.Timeout.String(),
+	})
+}
+
+func checkData(data interface{}) error {
+	_, isObjData := data.(map[string]interface{})
+	if !isObjData {
+		arrData, isArrData := data.([]interface{})
+		if !isArrData {
+			return errors.New("Unsupported type for Data")
+		}
+		if len(arrData) == 0 {
+			return errors.New("Data array must not be empty")
+		}
+		for _, elem := range arrData {
+			_, isObjData = elem.(map[string]interface{})
+			if !isObjData {
+				return errors.New("Data array contains unsupported type")
+			}
+		}
+
+	}
+
+	return nil
+}
diff --git a/runner/options.go b/runner/options.go
index fd6d995..42d688d 100644
--- a/runner/options.go
+++ b/runner/options.go
@@ -7,6 +7,7 @@ import (
 	"fmt"
 	"io"
 	"io/ioutil"
+	"os"
 	"path/filepath"
 	"runtime"
 	"strings"
@@ -73,6 +74,57 @@ type RunConfig struct {
 // Option controls some aspect of run
 type Option func(*RunConfig) error
 
+// WithConfigFromFile uses a configuration JSON file to populate the RunConfig
+//  WithConfig("config.json")
+func WithConfigFromFile(file string) Option {
+	return func(o *RunConfig) error {
+		data, err := ioutil.ReadFile(file)
+		if err != nil {
+			return fmt.Errorf("read config from file: %w", err)
+		}
+		var config Config
+		if err := json.Unmarshal(data, &config); err != nil {
+			return fmt.Errorf("unmarshal config: %w", err)
+		}
+		for _, option := range fromConfig(&config) {
+			if err := option(o); err != nil {
+				return err
+			}
+		}
+		return nil
+	}
+}
+
+// WithConfigFromReader uses a reader containing JSON data to populate the RunConfig
+// See also: WithConfigFromFile
+func WithConfigFromReader(reader io.Reader) Option {
+	return func(o *RunConfig) error {
+		var config Config
+		if err := json.NewDecoder(reader).Decode(&config); err != nil {
+			return fmt.Errorf("unmarshal config: %w", err)
+		}
+		for _, option := range fromConfig(&config) {
+			if err := option(o); err != nil {
+				return err
+			}
+		}
+		return nil
+	}
+}
+
+// WithConfig uses the configuration to populate the RunConfig
+// See also: WithConfigFromFile, WithConfigFromReader
+func WithConfig(c *Config) Option {
+	return func(o *RunConfig) error {
+		for _, option := range fromConfig(c) {
+			if err := option(o); err != nil {
+				return err
+			}
+		}
+		return nil
+	}
+}
+
 // WithCertificate specifies the certificate options for the run
 //	WithCertificate("client.crt", "client.key")
 func WithCertificate(cert, key string) Option {
@@ -584,3 +636,65 @@ func createClientTransportCredentials(skipVerify bool, cacertFile, clientCertFil
 
 	return credentials.NewTLS(&tlsConf), nil
 }
+
+func fromConfig(cfg *Config) []Option {
+	// set up all the options
+	options := make([]Option, 0, 17)
+
+	options = append(options,
+		WithProtoFile(cfg.Proto, cfg.ImportPaths),
+		WithProtoset(cfg.Protoset),
+		WithRootCertificate(cfg.RootCert),
+		WithCertificate(cfg.Cert, cfg.Key),
+		WithServerNameOverride(cfg.CName),
+		WithSkipTLSVerify(cfg.SkipTLSVerify),
+		WithInsecure(cfg.Insecure),
+		WithAuthority(cfg.Authority),
+		WithConcurrency(cfg.C),
+		WithTotalRequests(cfg.N),
+		WithQPS(cfg.QPS),
+		WithTimeout(time.Duration(cfg.Timeout)),
+		WithRunDuration(time.Duration(cfg.Z)),
+		WithDialTimeout(time.Duration(cfg.DialTimeout)),
+		WithKeepalive(time.Duration(cfg.KeepaliveTime)),
+		WithName(cfg.Name),
+		WithCPUs(cfg.CPUs),
+		WithMetadata(cfg.Metadata),
+		WithTags(cfg.Tags),
+		WithStreamInterval(time.Duration(cfg.SI)),
+		WithReflectionMetadata(cfg.ReflectMetadata),
+		WithConnections(cfg.Connections),
+		WithEnableCompression(cfg.EnableCompression),
+		WithDurationStopAction(cfg.ZStop),
+		func(o *RunConfig) error {
+			o.call = cfg.Call
+			return nil
+		},
+		func(o *RunConfig) error {
+			o.host = cfg.Host
+			return nil
+		},
+	)
+
+	if strings.TrimSpace(cfg.MetadataPath) != "" {
+		options = append(options, WithMetadataFromFile(strings.TrimSpace(cfg.MetadataPath)))
+	}
+
+	// data
+	if dataStr, ok := cfg.Data.(string); ok && dataStr == "@" {
+		options = append(options, WithDataFromReader(os.Stdin))
+	} else if strings.TrimSpace(cfg.DataPath) != "" {
+		options = append(options, WithDataFromFile(strings.TrimSpace(cfg.DataPath)))
+	} else {
+		options = append(options, WithData(cfg.Data))
+	}
+
+	// or binary data
+	if len(cfg.BinData) > 0 {
+		options = append(options, WithBinaryData(cfg.BinData))
+	}
+	if len(cfg.BinDataPath) > 0 {
+		options = append(options, WithBinaryDataFromFile(cfg.BinDataPath))
+	}
+	return options
+}
diff --git a/www/docs/examples.md b/www/docs/examples.md
index 2962a69..1323772 100644
--- a/www/docs/examples.md
+++ b/www/docs/examples.md
@@ -3,7 +3,22 @@ id: examples
 title: Examples
 ---
 
-A simple insecure unary call:
+- [A simple insecure unary call](#simple-unary)
+- [Server reflection](#server-reflection)
+- [Metadata using template variables](#metadata-template)
+- [Binary data](#binary-data)
+- [Variable data for unary calls](#variable-data)
+- [Custom parameters](#custom-parameters)
+- [Protoset](#protoset)
+- [Config file](#config)
+- [Debug logging](#debug)
+- [Client streaming](#client-stream)
+- [Server streaming](#server-stream)
+- [Well Known Types](#wkt)
+
+
+<a name="simple-unary">
+### A simple insecure unary call:
 
 ```sh
 ghz --insecure \
@@ -13,6 +28,9 @@ ghz --insecure \
   0.0.0.0:50051
 ```
 
+<a name="server-reflection">
+### Server reflection
+
 Or same test using [server reflection](https://github.com/grpc/grpc/blob/master/doc/server-reflection.md) (just omit `-proto` option):
 
 ```sh
@@ -22,6 +40,9 @@ ghz --insecure \
   0.0.0.0:50051
 ```
 
+<a name="metadata-template">
+### Metadata using template variables
+
 A simple unary call with metadata using template actions:
 
 ```sh
@@ -33,6 +54,9 @@ ghz --insecure \
   0.0.0.0:50051
 ```
 
+<a name="binary-data">
+### Binary data
+
 Using binary data file (see [writing a message](https://developers.google.com/protocol-buffers/docs/gotutorial#writing-a-message)):
 
 ```sh
@@ -50,6 +74,9 @@ ghz --proto ./greeter.proto \
   0.0.0.0:50051 < ./hello_request_data.bin
 ```
 
+<a name="variable-data">
+### Variable data for unary calls
+
 Round-robin of messages for unary call:
 
 ```sh
@@ -60,6 +87,9 @@ ghz --insecure \
   0.0.0.0:50051
 ```
 
+<a name="custom-parameters">
+### Custom parameters
+
 Custom number of requests and concurrency:
 
 ```sh
@@ -94,6 +124,9 @@ ghz --proto ./greeter.proto \
   0.0.0.0:50051
 ```
 
+<a name="protoset">
+### Protoset
+
 If a single object is given for data it is sent as every message.
 
 We can also use `.protoset` files which can bundle multiple protocol buffer files into one binary file.
@@ -122,6 +155,9 @@ Alternatively `ghz` can be used with [Prototool](https://github.com/uber/prototo
 ghz --protoset $(prototool descriptor-set --include-imports --tmp) ...
 ```
 
+<a name="config">
+### Config file
+
 Finally we can specify all settings, including the target host, conveniently in a JSON or TOML config file.
 
 ```sh
@@ -134,6 +170,9 @@ Config file settings can be combined with command line arguments. CLI options ov
 ghz --config ./config.json -c 20 -n 1000
 ```
 
+<a name="debug">
+### Debug logging
+
 With debug logging enabled:
 
 ```sh
@@ -145,6 +184,9 @@ ghz --insecure \
   0.0.0.0:50051
 ```
 
+<a name="client-stream">
+### Client streaming
+
 Client streaming with metadata:
 
 ```sh
@@ -156,6 +198,9 @@ ghz --insecure \
   0.0.0.0:50051
 ```
 
+<a name="server-stream">
+### Server streaming
+
 Server streaming with metadata:
 
 ```sh
@@ -165,4 +210,35 @@ ghz --insecure \
   -d '{"lo":{"latitude":400000000,"longitude":-750000000},"hi":{"latitude":420000000,"longitude":-730000000}}' \
   -m '{"trace_id":"{{.RequestNumber}}", "timestamp":"{{.TimestampUnixNano}}"}' \
   0.0.0.0:50051
-```
\ No newline at end of file
+```
+
+<a name="wkt">
+### Well Known Types
+
+[Well known types](https://developers.google.com/protocol-buffers/docs/reference/google.protobuf) can be used:
+
+Example proto:
+
+```proto
+syntax = "proto3";
+
+package wrapped;
+
+option go_package = "internal/wrapped";
+
+import "google/protobuf/wrappers.proto";
+
+service WrappedService {
+  rpc GetMessage (google.protobuf.StringValue) returns (google.protobuf.StringValue);
+}
+```
+
+We can test the call:
+
+```sh
+ghz --insecure \
+  --proto ./testdata/wrapped.proto \
+  --call wrapped.WrappedService.GetMessage \
+  -d '"asdf"' \
+  0.0.0.0:50051
+```


Test patch:
diff --git a/runner/data_test.go b/runner/data_test.go
index 796e96e..33592e2 100644
--- a/runner/data_test.go
+++ b/runner/data_test.go
@@ -2,6 +2,7 @@ package runner
 
 import (
 	"encoding/json"
+	"fmt"
 	"testing"
 
 	"github.com/golang/protobuf/proto"
@@ -45,6 +46,14 @@ func TestData_createPayloads(t *testing.T) {
 	assert.NoError(t, err)
 	assert.NotNil(t, mtdTestUnaryTwo)
 
+	mtdWrapped, err := protodesc.GetMethodDescFromProto(
+		"wrapped.WrappedService.GetMessage",
+		"../testdata/wrapped.proto",
+		[]string{"../testdata"})
+
+	assert.NoError(t, err)
+	assert.NotNil(t, mtdUnary)
+
 	t.Run("get empty when empty", func(t *testing.T) {
 		inputs, err := createPayloadsFromJSON("", mtdUnary)
 		assert.NoError(t, err)
@@ -204,6 +213,18 @@ func TestData_createPayloads(t *testing.T) {
 		assert.NotNil(t, inputs[0])
 	})
 
+	t.Run("create data with well-known wrapped type", func(t *testing.T) {
+		jsonData := `"foo"`
+
+		inputs, err := createPayloadsFromJSON(jsonData, mtdWrapped)
+		assert.NoError(t, err)
+		fmt.Println(err)
+		fmt.Println(inputs)
+		assert.NotNil(t, inputs)
+		assert.Len(t, inputs, 1)
+		assert.EqualValues(t, "foo", inputs[0].GetFieldByName("value"))
+	})
+
 	t.Run("create slice from single message binary data", func(t *testing.T) {
 		msg1 := &helloworld.HelloRequest{}
 		msg1.Name = "bob"
diff --git a/runner/options_test.go b/runner/options_test.go
index 862e5b7..633fb90 100644
--- a/runner/options_test.go
+++ b/runner/options_test.go
@@ -1,6 +1,7 @@
 package runner
 
 import (
+	"encoding/json"
 	"os"
 	"runtime"
 	"testing"
@@ -367,4 +368,59 @@ func TestRunConfig_newRunConfig(t *testing.T) {
 		assert.Error(t, err)
 		assert.Nil(t, c)
 	})
+
+	t.Run("with config", func(t *testing.T) {
+		filename := "../testdata/config.json"
+		t.Run("from file", func(t *testing.T) {
+			c, err := newConfig("", "",
+				WithConfigFromFile(filename))
+			assert.Nil(t, err)
+			assert.Equal(t, "helloworld.Greeter.SayHello", c.call)
+			assert.Equal(t, "0.0.0.0:50051", c.host)
+			assert.Equal(t, "../../testdata/greeter.proto", c.proto)
+			assert.Equal(t, []string{"../../testdata", "."}, c.importPaths)
+			assert.Equal(t, 5000, c.n)
+			assert.Equal(t, 50, c.c)
+			assert.Equal(t, 12*time.Second, c.z)
+			assert.Equal(t, 500*time.Millisecond, c.streamInterval)
+			assert.Equal(t, []byte(`{"name":"Bob {{.TimestampUnix}}"}`), c.data)
+			assert.Equal(t, []byte(`{"rn":"{{.RequestNumber}}"}`), c.metadata)
+			assert.Equal(t, true, c.insecure)
+		})
+		file, _ := os.Open(filename)
+		defer file.Close()
+		t.Run("from reader", func(t *testing.T) {
+			c, err := newConfig("call", "localhost:50050",
+				WithConfigFromReader(file))
+			assert.Nil(t, err)
+			assert.Equal(t, "helloworld.Greeter.SayHello", c.call)
+			assert.Equal(t, "0.0.0.0:50051", c.host)
+			assert.Equal(t, "../../testdata/greeter.proto", c.proto)
+			assert.Equal(t, []string{"../../testdata", "."}, c.importPaths)
+			assert.Equal(t, 5000, c.n)
+			assert.Equal(t, 50, c.c)
+			assert.Equal(t, 12*time.Second, c.z)
+			assert.Equal(t, 500*time.Millisecond, c.streamInterval)
+			assert.Equal(t, []byte(`{"name":"Bob {{.TimestampUnix}}"}`), c.data)
+			assert.Equal(t, []byte(`{"rn":"{{.RequestNumber}}"}`), c.metadata)
+			assert.Equal(t, true, c.insecure)
+		})
+		file, _ = os.Open(filename)
+		var config Config
+		_ = json.NewDecoder(file).Decode(&config)
+		c, err := newConfig("call", "localhost:50050",
+			WithConfig(&config))
+		assert.Nil(t, err)
+		assert.Equal(t, "helloworld.Greeter.SayHello", c.call)
+		assert.Equal(t, "0.0.0.0:50051", c.host)
+		assert.Equal(t, "../../testdata/greeter.proto", c.proto)
+		assert.Equal(t, []string{"../../testdata", "."}, c.importPaths)
+		assert.Equal(t, 5000, c.n)
+		assert.Equal(t, 50, c.c)
+		assert.Equal(t, 12*time.Second, c.z)
+		assert.Equal(t, 500*time.Millisecond, c.streamInterval)
+		assert.Equal(t, []byte(`{"name":"Bob {{.TimestampUnix}}"}`), c.data)
+		assert.Equal(t, []byte(`{"rn":"{{.RequestNumber}}"}`), c.metadata)
+		assert.Equal(t, true, c.insecure)
+	})
 }
diff --git a/runner/run_test.go b/runner/run_test.go
index 88fb7d1..4c7aaf7 100644
--- a/runner/run_test.go
+++ b/runner/run_test.go
@@ -1059,3 +1059,84 @@ func TestRunUnaryDurationStop(t *testing.T) {
 		assert.Equal(t, 6, report.StatusCodeDist["OK"])
 	})
 }
+
+func TestRunWrappedUnary(t *testing.T) {
+
+	_, s, err := internal.StartWrappedServer(false)
+
+	if err != nil {
+		assert.FailNow(t, err.Error())
+	}
+
+	defer s.Stop()
+
+	t.Run("json string data", func(t *testing.T) {
+		report, err := Run(
+			"wrapped.WrappedService.GetMessage",
+			internal.TestLocalhost,
+			WithProtoFile("../testdata/wrapped.proto", []string{"../testdata"}),
+			WithTotalRequests(1),
+			WithConcurrency(1),
+			WithTimeout(time.Duration(20*time.Second)),
+			WithDialTimeout(time.Duration(20*time.Second)),
+			WithDataFromJSON(`"foo"`),
+			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)
+	})
+
+	t.Run("json string data from file", func(t *testing.T) {
+		report, err := Run(
+			"wrapped.WrappedService.GetMessage",
+			internal.TestLocalhost,
+			WithProtoFile("../testdata/wrapped.proto", []string{"../testdata"}),
+			WithTotalRequests(1),
+			WithConcurrency(1),
+			WithTimeout(time.Duration(20*time.Second)),
+			WithDialTimeout(time.Duration(20*time.Second)),
+			WithDataFromFile(`../testdata/wrapped_data.json`),
+			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)
+	})
+}
diff --git a/testdata/google/protobuf/wrappers.proto b/testdata/google/protobuf/wrappers.proto
new file mode 100644
index 0000000..b7dd90e
--- /dev/null
+++ b/testdata/google/protobuf/wrappers.proto
@@ -0,0 +1,123 @@
+// 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.
+
+// Wrappers for primitive (non-message) types. These types are useful
+// for embedding primitives in the `google.protobuf.Any` type and for places
+// where we need to distinguish between the absence of a primitive
+// typed field and its default value.
+//
+// These wrappers have no meaningful use within repeated fields as they lack
+// the ability to detect presence on individual elements.
+// These wrappers have no meaningful use within a map or a oneof since
+// individual entries of a map or fields of a oneof can already detect presence.
+
+syntax = "proto3";
+
+package google.protobuf;
+
+option csharp_namespace = "Google.Protobuf.WellKnownTypes";
+option cc_enable_arenas = true;
+option go_package = "github.com/golang/protobuf/ptypes/wrappers";
+option java_package = "com.google.protobuf";
+option java_outer_classname = "WrappersProto";
+option java_multiple_files = true;
+option objc_class_prefix = "GPB";
+
+// Wrapper message for `double`.
+//
+// The JSON representation for `DoubleValue` is JSON number.
+message DoubleValue {
+  // The double value.
+  double value = 1;
+}
+
+// Wrapper message for `float`.
+//
+// The JSON representation for `FloatValue` is JSON number.
+message FloatValue {
+  // The float value.
+  float value = 1;
+}
+
+// Wrapper message for `int64`.
+//
+// The JSON representation for `Int64Value` is JSON string.
+message Int64Value {
+  // The int64 value.
+  int64 value = 1;
+}
+
+// Wrapper message for `uint64`.
+//
+// The JSON representation for `UInt64Value` is JSON string.
+message UInt64Value {
+  // The uint64 value.
+  uint64 value = 1;
+}
+
+// Wrapper message for `int32`.
+//
+// The JSON representation for `Int32Value` is JSON number.
+message Int32Value {
+  // The int32 value.
+  int32 value = 1;
+}
+
+// Wrapper message for `uint32`.
+//
+// The JSON representation for `UInt32Value` is JSON number.
+message UInt32Value {
+  // The uint32 value.
+  uint32 value = 1;
+}
+
+// Wrapper message for `bool`.
+//
+// The JSON representation for `BoolValue` is JSON `true` and `false`.
+message BoolValue {
+  // The bool value.
+  bool value = 1;
+}
+
+// Wrapper message for `string`.
+//
+// The JSON representation for `StringValue` is JSON string.
+message StringValue {
+  // The string value.
+  string value = 1;
+}
+
+// Wrapper message for `bytes`.
+//
+// The JSON representation for `BytesValue` is JSON string.
+message BytesValue {
+  // The bytes value.
+  bytes value = 1;
+}
\ No newline at end of file
diff --git a/testdata/wrapped.proto b/testdata/wrapped.proto
new file mode 100644
index 0000000..42ffbbb
--- /dev/null
+++ b/testdata/wrapped.proto
@@ -0,0 +1,11 @@
+syntax = "proto3";
+
+package wrapped;
+
+option go_package = "internal/wrapped";
+
+import "google/protobuf/wrappers.proto";
+
+service WrappedService {
+  rpc GetMessage (google.protobuf.StringValue) returns (google.protobuf.StringValue);
+}
\ No newline at end of file
diff --git a/testdata/wrapped_data.json b/testdata/wrapped_data.json
new file mode 100644
index 0000000..7e9668e
--- /dev/null
+++ b/testdata/wrapped_data.json
@@ -0,0 +1,1 @@
+"foo"
\ No newline at end of file