Repository: bojand/ghz
Pull request #221 (2020-08-30 00:14:16)

Fix file config API options
- Use `LoadConfig` in API to properly read in config
- Perform correct test duration init and setup in package API functions instead of just in cli


Patch:
diff --git a/cmd/ghz/main.go b/cmd/ghz/main.go
index b38312c..b5445a1 100644
--- a/cmd/ghz/main.go
+++ b/cmd/ghz/main.go
@@ -5,7 +5,6 @@ import (
 	"errors"
 	"fmt"
 	"io/ioutil"
-	"math"
 	"os"
 	"runtime"
 	"strconv"
@@ -218,12 +217,6 @@ func main() {
 		kingpin.FatalIfError(err, "")
 	}
 
-	// init / fix up durations
-	if cfg.X > 0 {
-		cfg.Z = cfg.X
-	} else if cfg.Z > 0 {
-		cfg.N = math.MaxInt32
-	}
 	var logger *zap.SugaredLogger
 
 	options := []runner.Option{runner.WithConfig(&cfg)}
diff --git a/runner/call_template_data.go b/runner/call_template_data.go
index be95f6f..d3fcd08 100644
--- a/runner/call_template_data.go
+++ b/runner/call_template_data.go
@@ -3,7 +3,6 @@ package runner
 import (
 	"bytes"
 	"encoding/json"
-	"math/rand"
 	"text/template"
 	"time"
 
@@ -11,12 +10,6 @@ import (
 	"github.com/jhump/protoreflect/desc"
 )
 
-const charset = "abcdefghijklmnopqrstuvwxyz" +
-	"ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
-
-var seededRand *rand.Rand = rand.New(
-	rand.NewSource(time.Now().UnixNano()))
-
 // call template data
 type callTemplateData struct {
 	WorkerID           string // unique worker ID
@@ -33,34 +26,13 @@ type callTemplateData struct {
 	TimestampUnixMilli int64  // timestamp of the call as unix time in milliseconds
 	TimestampUnixNano  int64  // timestamp of the call as unix time in nanoseconds
 	UUID               string // generated UUIDv4 for each call
-
-	templateFuncs template.FuncMap
-}
-
-var tmplFuncMap = template.FuncMap{
-	"newUUID":      newUUID,
-	"randomString": randomString,
 }
 
 // newCallTemplateData returns new call template data
-func newCallTemplateData(
-	mtd *desc.MethodDescriptor,
-	funcs template.FuncMap,
-	workerID string, reqNum int64) *callTemplateData {
+func newCallTemplateData(mtd *desc.MethodDescriptor, workerID string, reqNum int64) *callTemplateData {
 	now := time.Now()
 	newUUID, _ := uuid.NewRandom()
 
-	fns := make(template.FuncMap, len(funcs)+2)
-	for k, v := range tmplFuncMap {
-		fns[k] = v
-	}
-
-	if len(funcs) > 0 {
-		for k, v := range funcs {
-			fns[k] = v
-		}
-	}
-
 	return &callTemplateData{
 		WorkerID:           workerID,
 		RequestNumber:      reqNum,
@@ -76,12 +48,11 @@ func newCallTemplateData(
 		TimestampUnixMilli: now.UnixNano() / 1000000,
 		TimestampUnixNano:  now.UnixNano(),
 		UUID:               newUUID.String(),
-		templateFuncs:      fns,
 	}
 }
 
 func (td *callTemplateData) execute(data string) (*bytes.Buffer, error) {
-	t := template.Must(template.New("call_template_data").Funcs(td.templateFuncs).Parse(data))
+	t := template.Must(template.New("call_template_data").Parse(data))
 	var tpl bytes.Buffer
 	err := t.Execute(&tpl, td)
 	return &tpl, err
@@ -101,7 +72,7 @@ func (td *callTemplateData) executeData(data string) ([]byte, error) {
 	return []byte{}, nil
 }
 
-func (td *callTemplateData) executeMetadata(metadata string) (map[string]string, error) {
+func (td *callTemplateData) executeMetadata(metadata string) (*map[string]string, error) {
 	var mdMap map[string]string
 
 	if len(metadata) > 0 {
@@ -117,29 +88,5 @@ func (td *callTemplateData) executeMetadata(metadata string) (map[string]string,
 		}
 	}
 
-	return mdMap, nil
-}
-
-func newUUID() string {
-	newUUID, _ := uuid.NewRandom()
-	return newUUID.String()
-}
-
-const maxLen = 16
-const minLen = 2
-
-func stringWithCharset(length int, charset string) string {
-	b := make([]byte, length)
-	for i := range b {
-		b[i] = charset[seededRand.Intn(len(charset))]
-	}
-	return string(b)
-}
-
-func randomString(length int) string {
-	if length <= 0 {
-		length = seededRand.Intn(maxLen-minLen+1) + minLen
-	}
-
-	return stringWithCharset(length, charset)
+	return &mdMap, nil
 }
diff --git a/runner/options.go b/runner/options.go
index 91c294b..e8e81ed 100644
--- a/runner/options.go
+++ b/runner/options.go
@@ -7,11 +7,11 @@ import (
 	"fmt"
 	"io"
 	"io/ioutil"
+	"math"
 	"os"
 	"path/filepath"
 	"runtime"
 	"strings"
-	"text/template"
 	"time"
 
 	"github.com/pkg/errors"
@@ -71,26 +71,22 @@ type RunConfig struct {
 	cpus      int
 	tags      []byte
 	skipFirst int
-
-	funcs template.FuncMap
 }
 
 // Option controls some aspect of run
 type Option func(*RunConfig) error
 
 // WithConfigFromFile uses a configuration JSON file to populate the RunConfig
-//  WithConfig("config.json")
+//  WithConfigFromFile("config.json")
 func WithConfigFromFile(file string) Option {
 	return func(o *RunConfig) error {
-		data, err := ioutil.ReadFile(file)
+		var cfg Config
+		err := LoadConfig(file, &cfg)
 		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)
+			return err
 		}
-		for _, option := range fromConfig(&config) {
+
+		for _, option := range fromConfig(&cfg) {
 			if err := option(o); err != nil {
 				return err
 			}
@@ -103,11 +99,12 @@ func WithConfigFromFile(file string) Option {
 // 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 {
+		var cfg Config
+		if err := json.NewDecoder(reader).Decode(&cfg); err != nil {
 			return fmt.Errorf("unmarshal config: %w", err)
 		}
-		for _, option := range fromConfig(&config) {
+
+		for _, option := range fromConfig(&cfg) {
 			if err := option(o); err != nil {
 				return err
 			}
@@ -118,9 +115,17 @@ func WithConfigFromReader(reader io.Reader) Option {
 
 // WithConfig uses the configuration to populate the RunConfig
 // See also: WithConfigFromFile, WithConfigFromReader
-func WithConfig(c *Config) Option {
+func WithConfig(cfg *Config) Option {
 	return func(o *RunConfig) error {
-		for _, option := range fromConfig(c) {
+
+		// init / fix up durations
+		if cfg.X > 0 {
+			cfg.Z = cfg.X
+		} else if cfg.Z > 0 {
+			cfg.N = math.MaxInt32
+		}
+
+		for _, option := range fromConfig(cfg) {
 			if err := option(o); err != nil {
 				return err
 			}
@@ -549,15 +554,6 @@ func WithLogger(log Logger) Option {
 	}
 }
 
-// WithTemplateFuncs adds additional tempalte functions
-func WithTemplateFuncs(funcMap template.FuncMap) Option {
-	return func(o *RunConfig) error {
-		o.funcs = funcMap
-
-		return nil
-	}
-}
-
 func newConfig(call, host string, options ...Option) (*RunConfig, error) {
 	call = strings.TrimSpace(call)
 	host = strings.TrimSpace(host)
@@ -584,6 +580,11 @@ func newConfig(call, host string, options ...Option) (*RunConfig, error) {
 		}
 	}
 
+	// fix up durations
+	if c.z > 0 {
+		c.n = math.MaxInt32
+	}
+
 	// checks
 	if c.nConns > c.c {
 		return nil, errors.New("Number of connections cannot be greater than concurrency")
@@ -668,6 +669,13 @@ func fromConfig(cfg *Config) []Option {
 	// set up all the options
 	options := make([]Option, 0, 17)
 
+	// init / fix up durations
+	if cfg.X > 0 {
+		cfg.Z = cfg.X
+	} else if cfg.Z > 0 {
+		cfg.N = math.MaxInt32
+	}
+
 	options = append(options,
 		WithProtoFile(cfg.Proto, cfg.ImportPaths),
 		WithProtoset(cfg.Protoset),
diff --git a/runner/reporter.go b/runner/reporter.go
index 473f9dd..ed166af 100644
--- a/runner/reporter.go
+++ b/runner/reporter.go
@@ -240,25 +240,14 @@ func (r *Reporter) Finalize(stopReason StopReason, total time.Duration) *Report
 func latencies(latencies []float64) []LatencyDistribution {
 	pctls := []int{10, 25, 50, 75, 90, 95, 99}
 	data := make([]float64, len(pctls))
-	lt := float64(len(latencies))
-	for i, p := range pctls {
-		ip := (float64(p) / 100.0) * lt
-		di := int(ip)
-
-		// since we're dealing with 0th based ranks we need to
-		// check if ordinal is a whole number that lands on the percentile
-		// if so adjust accordingly
-		if ip == float64(di) {
-			di = di - 1
+	j := 0
+	for i := 0; i < len(latencies) && j < len(pctls); i++ {
+		current := i * 100 / len(latencies)
+		if current >= pctls[j] {
+			data[j] = latencies[i]
+			j++
 		}
-
-		if di < 0 {
-			di = 0
-		}
-
-		data[i] = latencies[di]
 	}
-
 	res := make([]LatencyDistribution, len(pctls))
 	for i := 0; i < len(pctls); i++ {
 		if data[i] > 0 {
diff --git a/runner/worker.go b/runner/worker.go
index f5a8fb6..d371ef3 100644
--- a/runner/worker.go
+++ b/runner/worker.go
@@ -65,7 +65,7 @@ func (w *Worker) makeRequest() error {
 
 	reqNum := atomic.AddInt64(w.reqCounter, 1)
 
-	ctd := newCallTemplateData(w.mtd, w.config.funcs, w.workerID, reqNum)
+	ctd := newCallTemplateData(w.mtd, w.workerID, reqNum)
 
 	var inputs []*dynamic.Message
 	var err error
@@ -88,8 +88,8 @@ func (w *Worker) makeRequest() error {
 	}
 
 	var reqMD *metadata.MD
-	if len(mdMap) > 0 {
-		md := metadata.New(mdMap)
+	if mdMap != nil && len(*mdMap) > 0 {
+		md := metadata.New(*mdMap)
 		reqMD = &md
 	} else {
 		reqMD = &metadata.MD{}
diff --git a/web/ui/package-lock.json b/web/ui/package-lock.json
index ffaab7c..724a3f6 100644
--- a/web/ui/package-lock.json
+++ b/web/ui/package-lock.json
@@ -3953,9 +3953,9 @@
       "dev": true
     },
     "elliptic": {
-      "version": "6.5.3",
-      "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.3.tgz",
-      "integrity": "sha512-IMqzv5wNQf+E6aHeIqATs0tOLeOTwj1QKbRcS3jBbYkl5oLAserA8yJTT7/VyHUYG91PRmPyeQDObKLPpeS4dw==",
+      "version": "6.5.1",
+      "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.1.tgz",
+      "integrity": "sha512-xvJINNLbTeWQjrl6X+7eQCrIy/YPv5XCpKW6kB5mKvtnGILoLDcySuwomfdzt0BMdLNVnuRNTuzKNHj0bva1Cg==",
       "dev": true,
       "requires": {
         "bn.js": "^4.4.0",
diff --git a/www/docs/calldata.md b/www/docs/calldata.md
index 6ee7cc2..c9b98c9 100644
--- a/www/docs/calldata.md
+++ b/www/docs/calldata.md
@@ -53,16 +53,6 @@ type callTemplateData struct {
 }
 ```
 
-**Functions**
-
-There are also two template functions available:
-
-`newUUID` - Generates a new UUID for each invocation.
-
-`randomString` - Generates a new random string for each incovation. Accepts a length parameter. If the argument is `<= 0` then a ranom string is generated with a random length between length of `2` and `16`.
-
-**Examples**
-
 This can be useful to inject variable information into the message data JSON or metadata JSON payloads for each request, such as timestamp or unique request number. For example:
 
 ```sh
@@ -79,19 +69,4 @@ Would result in server getting the following metadata map represented here in JS
 }
 ```
 
-```sh
--d '{"order_id":"{{newUUID}}", "item_id":"{{newUUID}}", "sku":"{{randomString 8 }}", "product_name":"{{ranomdString 0}}"}'
-```
-
-Would result in data with JSON representation:
-
-```json
-{
-  "order_id": "3974e7b3-5946-4df5-bed3-8c3dc9a0be19",
-  "item_id": "cd9c2604-cd9b-43a8-9cbb-d1ad26ca93a4",
-  "sku": "HlFTAxcm",
-  "product_name": "xg3NEC"
-}
-```
-
 See [example calls](examples.md) for some more usage examples.
diff --git a/www/website/package-lock.json b/www/website/package-lock.json
index 5f86bac..355d039 100644
--- a/www/website/package-lock.json
+++ b/www/website/package-lock.json
@@ -7039,9 +7039,9 @@
       "dev": true
     },
     "prismjs": {
-      "version": "1.21.0",
-      "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.21.0.tgz",
-      "integrity": "sha512-uGdSIu1nk3kej2iZsLyDoJ7e9bnPzIgY0naW/HdknGj61zScaprVEVGHrPoXqI+M9sP0NDnTK2jpkvmldpuqDw==",
+      "version": "1.20.0",
+      "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.20.0.tgz",
+      "integrity": "sha512-AEDjSrVNkynnw6A+B1DsFkd6AVdTnp+/WoUixFRULlCLZVRZlVQMVWio/16jv7G1FscUxQxOQhWwApgbnxr6kQ==",
       "dev": true,
       "requires": {
         "clipboard": "^2.0.0"


Test patch:
diff --git a/runner/call_template_data_test.go b/runner/call_template_data_test.go
index 2807468..9523210 100644
--- a/runner/call_template_data_test.go
+++ b/runner/call_template_data_test.go
@@ -1,12 +1,9 @@
 package runner
 
 import (
-	"strings"
 	"testing"
-	"text/template"
 
 	"github.com/bojand/ghz/protodesc"
-	"github.com/google/uuid"
 	"github.com/stretchr/testify/assert"
 )
 
@@ -15,7 +12,7 @@ func TestCallTemplateData_New(t *testing.T) {
 	assert.NoError(t, err)
 	assert.NotNil(t, md)
 
-	ctd := newCallTemplateData(md, nil, "worker_id_123", 100)
+	ctd := newCallTemplateData(md, "worker_id_123", 100)
 
 	assert.NotNil(t, ctd)
 	assert.Equal(t, "worker_id_123", ctd.WorkerID)
@@ -41,7 +38,7 @@ func TestCallTemplateData_ExecuteData(t *testing.T) {
 	assert.NoError(t, err)
 	assert.NotNil(t, md)
 
-	ctd := newCallTemplateData(md, nil, "worker_id_123", 200)
+	ctd := newCallTemplateData(md, "worker_id_123", 200)
 
 	assert.NotNil(t, ctd)
 
@@ -92,7 +89,7 @@ func TestCallTemplateData_ExecuteMetadata(t *testing.T) {
 	assert.NoError(t, err)
 	assert.NotNil(t, md)
 
-	ctd := newCallTemplateData(md, nil, "worker_id_123", 200)
+	ctd := newCallTemplateData(md, "worker_id_123", 200)
 
 	assert.NotNil(t, ctd)
 
@@ -104,17 +101,17 @@ func TestCallTemplateData_ExecuteMetadata(t *testing.T) {
 	}{
 		{"no template",
 			`{"trace_id":"asdf"}`,
-			map[string]string{"trace_id": "asdf"},
+			&map[string]string{"trace_id": "asdf"},
 			false,
 		},
 		{"with template",
 			`{"trace_id":"{{.RequestNumber}} asdf {{.FullyQualifiedName}} {{.MethodName}} {{.ServiceName}} {{.InputName}} {{.OutputName}} {{.IsClientStreaming}} {{.IsServerStreaming}}"}`,
-			map[string]string{"trace_id": "200 asdf helloworld.Greeter.SayHello SayHello Greeter HelloRequest HelloReply false false"},
+			&map[string]string{"trace_id": "200 asdf helloworld.Greeter.SayHello SayHello Greeter HelloRequest HelloReply false false"},
 			false,
 		},
 		{"with unknown action",
 			`{"trace_id":"asdf {{.Something}} {{.MethodName}} bob"}`,
-			map[string]string{"trace_id": "asdf {{.Something}} {{.MethodName}} bob"},
+			&map[string]string{"trace_id": "asdf {{.Something}} {{.MethodName}} bob"},
 			false,
 		},
 	}
@@ -132,140 +129,3 @@ func TestCallTemplateData_ExecuteMetadata(t *testing.T) {
 		})
 	}
 }
-
-func TestCallTemplateData_ExecuteFuncs(t *testing.T) {
-	md, err := protodesc.GetMethodDescFromProto("helloworld.Greeter/SayHello", "../testdata/greeter.proto", []string{})
-	assert.NoError(t, err)
-	assert.NotNil(t, md)
-
-	ctd := newCallTemplateData(md, nil, "worker_id_123", 200)
-
-	assert.NotNil(t, ctd)
-
-	t.Run("newUUID", func(t *testing.T) {
-
-		// no template
-		r, err := ctd.executeData(`{"trace_id":"asdf"}`)
-		assert.NoError(t, err)
-		assert.Equal(t, `{"trace_id":"asdf"}`, string(r))
-
-		rm, err := ctd.executeMetadata(`{"trace_id":"asdf"}`)
-		assert.NoError(t, err)
-		assert.Equal(t, map[string]string{"trace_id": "asdf"}, rm)
-
-		// new uuid
-		r, err = ctd.executeData(`{"trace_id":"{{newUUID}}"}`)
-		assert.NoError(t, err)
-		rs := strings.Replace(string(r), `{"trace_id":"`, "", -1)
-		rs = strings.Replace(rs, `"}`, "", -1)
-		assert.NotEmpty(t, rs)
-		parsed, err := uuid.Parse(rs)
-		assert.NoError(t, err)
-		assert.NotEmpty(t, parsed)
-
-		rm2, err := ctd.executeMetadata(`{"trace_id":"{{newUUID}}"}`)
-		assert.NoError(t, err)
-		rs2 := rm2["trace_id"]
-		assert.NotEmpty(t, rs)
-		assert.NotEqual(t, rs, rs2)
-		parsed, err = uuid.Parse(rs2)
-		assert.NoError(t, err)
-		assert.NotEmpty(t, parsed)
-
-		rm3, err := ctd.executeMetadata(`{"span_id":"{{newUUID}}","trace_id":"{{newUUID}}"}`)
-		assert.NoError(t, err)
-		assert.NotEmpty(t, rs)
-		assert.NotEqual(t, rm3["span_id"], rs2)
-		assert.NotEqual(t, rm3["span_id"], rs)
-		assert.NotEqual(t, rm3["trace_id"], rs2)
-		assert.NotEqual(t, rm3["trace_id"], rs)
-		assert.NotEqual(t, rm3["trace_id"], rm3["span_id"])
-		parsed, err = uuid.Parse(rm3["span_id"])
-		assert.NoError(t, err)
-		assert.NotEmpty(t, parsed)
-		parsed, err = uuid.Parse(rm3["trace_id"])
-		assert.NoError(t, err)
-		assert.NotEmpty(t, parsed)
-	})
-
-	t.Run("randomString", func(t *testing.T) {
-
-		// no template
-		r, err := ctd.executeData(`{"trace_id":"asdf"}`)
-		assert.NoError(t, err)
-		assert.Equal(t, `{"trace_id":"asdf"}`, string(r))
-
-		rm, err := ctd.executeMetadata(`{"trace_id":"asdf"}`)
-		assert.NoError(t, err)
-		assert.Equal(t, map[string]string{"trace_id": "asdf"}, rm)
-
-		// default length when 0
-		r, err = ctd.executeData(`{"trace_id":"{{randomString 0}}"}`)
-		assert.NoError(t, err)
-		rs := strings.Replace(string(r), `{"trace_id":"`, "", -1)
-		rs = strings.Replace(rs, `"}`, "", -1)
-		assert.NotEmpty(t, rs)
-		assert.True(t, len(rs) >= 2)
-		assert.True(t, len(rs) <= 16)
-
-		// default length when -1
-		r, err = ctd.executeData(`{"trace_id":"{{randomString -1}}"}`)
-		assert.NoError(t, err)
-		rs2 := strings.Replace(string(r), `{"trace_id":"`, "", -1)
-		rs2 = strings.Replace(rs2, `"}`, "", -1)
-		assert.NotEmpty(t, rs2)
-		assert.NotEqual(t, rs, rs2)
-		assert.True(t, len(rs2) >= 2)
-		assert.True(t, len(rs2) <= 16)
-
-		// specific length
-		r, err = ctd.executeData(`{"trace_id":"{{randomString 10}}"}`)
-		assert.NoError(t, err)
-		rs = strings.Replace(string(r), `{"trace_id":"`, "", -1)
-		rs = strings.Replace(rs, `"}`, "", -1)
-		assert.NotEmpty(t, rs)
-		assert.Len(t, rs, 10)
-
-		rm, err = ctd.executeMetadata(`{"trace_id":"{{randomString 0}}"}`)
-		assert.NoError(t, err)
-		assert.True(t, len(rm["trace_id"]) >= 2)
-		assert.True(t, len(rm["trace_id"]) <= 16)
-
-		rm, err = ctd.executeMetadata(`{"span_id":"{{randomString -1}}","trace_id":"{{randomString 0}}"}`)
-		assert.NoError(t, err)
-		assert.True(t, len(rm["trace_id"]) >= 2)
-		assert.True(t, len(rm["trace_id"]) <= 16)
-		assert.True(t, len(rm["span_id"]) >= 2)
-		assert.True(t, len(rm["span_id"]) <= 16)
-		assert.NotEqual(t, rm["trace_id"], rm["span_id"])
-
-		rm, err = ctd.executeMetadata(`{"span_id":"{{randomString 12}}","trace_id":"{{randomString 12}}"}`)
-		assert.NoError(t, err)
-		assert.Len(t, rm["trace_id"], 12)
-		assert.Len(t, rm["span_id"], 12)
-		assert.NotEqual(t, rm["trace_id"], rs)
-		assert.NotEqual(t, rm["trace_id"], rs2)
-		assert.NotEqual(t, rm["trace_id"], rm["span_id"])
-	})
-
-	t.Run("custom functions", func(t *testing.T) {
-		ctd = newCallTemplateData(md, template.FuncMap{
-			"getSKU": func() string {
-				return "custom-sku"
-			},
-			"newUUID": func() string {
-				return "custom-uuid"
-			},
-		}, "worker_id_123", 200)
-
-		r, err := ctd.executeData(`{"trace_id":"{{newUUID}}", "span_id":"{{getSKU}}"}`)
-		assert.NoError(t, err)
-		assert.Equal(t, `{"trace_id":"custom-uuid", "span_id":"custom-sku"}`, string(r))
-
-		rm, err := ctd.executeMetadata(`{"span_id":"{{randomString 12}}","trace_id":"{{newUUID}}", "sku":"{{getSKU}}"}`)
-		assert.NoError(t, err)
-		assert.Len(t, rm["span_id"], 12)
-		assert.Equal(t, "custom-uuid", rm["trace_id"])
-		assert.Equal(t, "custom-sku", rm["sku"])
-	})
-}
diff --git a/runner/options_test.go b/runner/options_test.go
index b467ed6..3abf630 100644
--- a/runner/options_test.go
+++ b/runner/options_test.go
@@ -2,6 +2,7 @@ package runner
 
 import (
 	"encoding/json"
+	"math"
 	"os"
 	"runtime"
 	"testing"
@@ -88,7 +89,7 @@ func TestRunConfig_newRunConfig(t *testing.T) {
 		assert.Equal(t, "call", c.call)
 		assert.Equal(t, "localhost:50050", c.host)
 		assert.Equal(t, true, c.insecure)
-		assert.Equal(t, 100, c.n)
+		assert.Equal(t, math.MaxInt32, c.n)
 		assert.Equal(t, 20, c.c)
 		assert.Equal(t, 5, c.qps)
 		assert.Equal(t, 5, c.skipFirst)
@@ -118,7 +119,6 @@ func TestRunConfig_newRunConfig(t *testing.T) {
 			WithConcurrency(20),
 			WithQPS(5),
 			WithSkipFirst(5),
-			WithRunDuration(time.Duration(5*time.Minute)),
 			WithKeepalive(time.Duration(60*time.Second)),
 			WithTimeout(time.Duration(10*time.Second)),
 			WithDialTimeout(time.Duration(30*time.Second)),
@@ -143,7 +143,7 @@ func TestRunConfig_newRunConfig(t *testing.T) {
 		assert.Equal(t, 5, c.qps)
 		assert.Equal(t, 5, c.skipFirst)
 		assert.Equal(t, true, c.binary)
-		assert.Equal(t, time.Duration(5*time.Minute), c.z)
+		assert.Equal(t, time.Duration(0), c.z)
 		assert.Equal(t, time.Duration(60*time.Second), c.keepaliveTime)
 		assert.Equal(t, time.Duration(10*time.Second), c.timeout)
 		assert.Equal(t, time.Duration(30*time.Second), c.dialTimeout)
@@ -185,7 +185,6 @@ func TestRunConfig_newRunConfig(t *testing.T) {
 			WithProtoFile("testdata/data.proto", []string{}),
 			WithCertificate("../testdata/localhost.crt", "../testdata/localhost.key"),
 			WithInsecure(true),
-			WithTotalRequests(100),
 			WithConcurrency(20),
 			WithQPS(5),
 			WithSkipFirst(5),
@@ -208,7 +207,7 @@ func TestRunConfig_newRunConfig(t *testing.T) {
 		assert.Equal(t, true, c.insecure)
 		assert.Equal(t, "../testdata/localhost.crt", c.cert)
 		assert.Equal(t, "../testdata/localhost.key", c.key)
-		assert.Equal(t, 100, c.n)
+		assert.Equal(t, math.MaxInt32, c.n)
 		assert.Equal(t, 20, c.c)
 		assert.Equal(t, 5, c.qps)
 		assert.Equal(t, 5, c.skipFirst)
@@ -382,6 +381,7 @@ func TestRunConfig_newRunConfig(t *testing.T) {
 
 	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))
@@ -390,18 +390,38 @@ func TestRunConfig_newRunConfig(t *testing.T) {
 			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, math.MaxInt32, c.n) // max-duration set so n ignored
 			assert.Equal(t, 50, c.c)
 			assert.Equal(t, 5, c.skipFirst)
-			assert.Equal(t, 12*time.Second, c.z)
+			assert.Equal(t, 7*time.Second, c.z) // max-duration (x) is set to z becomes x
 			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 file 2", func(t *testing.T) {
+			c, err := newConfig("", "",
+				WithConfigFromFile("../testdata/config5.toml"))
+			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, 40, c.c)
+			assert.Equal(t, 100, c.skipFirst)
+			assert.Equal(t, time.Duration(0), c.z)
+			assert.Equal(t, time.Duration(0), 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)
+		})
+
 		t.Run("from reader", func(t *testing.T) {
+			file, _ := os.Open(filename)
+			defer file.Close()
+
 			c, err := newConfig("call", "localhost:50050",
 				WithConfigFromReader(file))
 			assert.Nil(t, err)
@@ -409,32 +429,37 @@ func TestRunConfig_newRunConfig(t *testing.T) {
 			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, math.MaxInt32, c.n) // max-duration set so n ignored
 			assert.Equal(t, 50, c.c)
 			assert.Equal(t, 5, c.skipFirst)
-			assert.Equal(t, 12*time.Second, c.z)
+			assert.Equal(t, 7*time.Second, c.z) // max-duration (x) is set to z becomes x
+			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)
+		})
+
+		t.Run("from reader", func(t *testing.T) {
+			file, _ := os.Open(filename)
+			defer file.Close()
+
+			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, math.MaxInt32, c.n)
+			assert.Equal(t, 50, c.c)
+			assert.Equal(t, 5, c.skipFirst)
+			assert.Equal(t, 7*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, 5, c.skipFirst)
-		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/reporter_test.go b/runner/reporter_test.go
index fe5a7af..9434f2f 100644
--- a/runner/reporter_test.go
+++ b/runner/reporter_test.go
@@ -3,7 +3,6 @@ package runner
 import (
 	"context"
 	"encoding/json"
-	"strconv"
 	"testing"
 	"time"
 
@@ -59,66 +58,3 @@ func TestReport_CorrectDetails(t *testing.T) {
 	assert.Equal(t, ResultDetail{Error: "", Latency: cr1.duration, Status: cr1.status, Timestamp: cr1.timestamp}, report.Details[0])
 	assert.Equal(t, ResultDetail{Error: cr2.err.Error(), Latency: cr2.duration, Status: cr2.status, Timestamp: cr2.timestamp}, report.Details[1])
 }
-
-func TestReport_latencies(t *testing.T) {
-	var tests = []struct {
-		input    []float64
-		expected []LatencyDistribution
-	}{
-		{
-			input: []float64{15, 20, 35, 40, 50},
-			expected: []LatencyDistribution{
-				{Percentage: 10, Latency: 15 * time.Second},
-				{Percentage: 25, Latency: 20 * time.Second},
-				{Percentage: 50, Latency: 35 * time.Second},
-				{Percentage: 75, Latency: 40 * time.Second},
-				{Percentage: 90, Latency: 50 * time.Second},
-				{Percentage: 95, Latency: 50 * time.Second},
-				{Percentage: 99, Latency: 50 * time.Second},
-			},
-		},
-		{
-			input: []float64{3, 6, 7, 8, 8, 10, 13, 15, 16, 20},
-			expected: []LatencyDistribution{
-				{Percentage: 10, Latency: 3 * time.Second},
-				{Percentage: 25, Latency: 7 * time.Second},
-				{Percentage: 50, Latency: 8 * time.Second},
-				{Percentage: 75, Latency: 15 * time.Second},
-				{Percentage: 90, Latency: 16 * time.Second},
-				{Percentage: 95, Latency: 20 * time.Second},
-				{Percentage: 99, Latency: 20 * time.Second},
-			},
-		},
-		{
-			input: []float64{3, 6, 7, 8, 8, 9, 10, 13, 15, 16, 20},
-			expected: []LatencyDistribution{
-				{Percentage: 10, Latency: 6 * time.Second},
-				{Percentage: 25, Latency: 7 * time.Second},
-				{Percentage: 50, Latency: 9 * time.Second},
-				{Percentage: 75, Latency: 15 * time.Second},
-				{Percentage: 90, Latency: 16 * time.Second},
-				{Percentage: 95, Latency: 20 * time.Second},
-				{Percentage: 99, Latency: 20 * time.Second},
-			},
-		},
-		{
-			input: []float64{2.1, 3.2, 4.5, 6.3, 7.4, 8.5, 9.6, 10.7, 13.8, 15.9, 16.11, 18.17, 20.11, 22.34},
-			expected: []LatencyDistribution{
-				{Percentage: 10, Latency: time.Duration(3.2 * float64(time.Second))},
-				{Percentage: 25, Latency: time.Duration(6.3 * float64(time.Second))},
-				{Percentage: 50, Latency: time.Duration(9.6 * float64(time.Second))},
-				{Percentage: 75, Latency: time.Duration(16.11 * float64(time.Second))},
-				{Percentage: 90, Latency: time.Duration(20.11 * float64(time.Second))},
-				{Percentage: 95, Latency: time.Duration(22.34 * float64(time.Second))},
-				{Percentage: 99, Latency: time.Duration(22.34 * float64(time.Second))},
-			},
-		},
-	}
-
-	for i, tt := range tests {
-		t.Run("latencies "+strconv.FormatInt(int64(i), 10), func(t *testing.T) {
-			lats := latencies(tt.input)
-			assert.Equal(t, tt.expected, lats)
-		})
-	}
-}
diff --git a/runner/run_test.go b/runner/run_test.go
index 0c501fa..dfca98c 100644
--- a/runner/run_test.go
+++ b/runner/run_test.go
@@ -1,6 +1,7 @@
 package runner
 
 import (
+	"fmt"
 	"strconv"
 	"sync"
 	"testing"
@@ -159,6 +160,92 @@ func TestRunUnary(t *testing.T) {
 		assert.Equal(t, 1, connCount)
 	})
 
+	t.Run("test n default", func(t *testing.T) {
+		gs.ResetCounters()
+
+		data := make(map[string]interface{})
+		data["name"] = "bob"
+
+		report, err := Run(
+			"helloworld.Greeter.SayHello",
+			internal.TestLocalhost,
+			WithProtoFile("../testdata/greeter.proto", []string{}),
+			WithData(data),
+			WithInsecure(true),
+		)
+
+		assert.NoError(t, err)
+
+		assert.NotNil(t, report)
+
+		assert.Equal(t, 200, int(report.Count))
+		assert.NotZero(t, report.Average)
+		assert.NotZero(t, report.Fastest)
+		assert.NotZero(t, report.Slowest)
+		assert.NotZero(t, report.Rps)
+		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.NotEqual(t, report.Average, report.Slowest)
+		assert.NotEqual(t, report.Average, report.Fastest)
+		assert.NotEqual(t, report.Slowest, report.Fastest)
+
+		count := gs.GetCount(callType)
+		assert.Equal(t, 200, count)
+
+		connCount := gs.GetConnectionCount()
+		assert.Equal(t, 1, connCount)
+	})
+
+	t.Run("test run duration", func(t *testing.T) {
+		gs.ResetCounters()
+
+		data := make(map[string]interface{})
+		data["name"] = "bob"
+
+		report, err := Run(
+			"helloworld.Greeter.SayHello",
+			internal.TestLocalhost,
+			WithProtoFile("../testdata/greeter.proto", []string{}),
+			WithRunDuration(3*time.Second),
+			WithData(data),
+			WithInsecure(true),
+		)
+
+		assert.NoError(t, err)
+
+		assert.NotNil(t, report)
+
+		assert.True(t, int(report.Count) > 200, fmt.Sprintf("%d not > 200", int64(report.Count)))
+		assert.True(t, report.Total.Milliseconds() >= 3000 && report.Total.Milliseconds() < 3100, fmt.Sprintf("duration %d expected value", report.Total.Milliseconds()))
+		assert.NotZero(t, report.Average)
+		assert.NotZero(t, report.Fastest)
+		assert.NotZero(t, report.Slowest)
+		assert.NotZero(t, report.Rps)
+		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, ReasonTimeout, report.EndReason)
+		// assert.Empty(t, report.ErrorDist)
+
+		assert.NotEqual(t, report.Average, report.Slowest)
+		assert.NotEqual(t, report.Average, report.Fastest)
+		assert.NotEqual(t, report.Slowest, report.Fastest)
+
+		// count := gs.GetCount(callType)
+		// assert.Equal(t, int64(report.Count), int64(count))
+
+		connCount := gs.GetConnectionCount()
+		assert.Equal(t, 1, connCount)
+	})
+
 	t.Run("test QPS", func(t *testing.T) {
 		gs.ResetCounters()
 
diff --git a/testdata/config5.toml b/testdata/config5.toml
new file mode 100644
index 0000000..5c953e9
--- /dev/null
+++ b/testdata/config5.toml
@@ -0,0 +1,13 @@
+total = 5000
+concurrency = 40
+skipFirst = 100
+proto = "../../testdata/greeter.proto"
+call = "helloworld.Greeter.SayHello"
+host = "0.0.0.0:50051"
+insecure = true
+
+[data]
+name = "Bob {{.TimestampUnix}}"
+
+[metadata]
+rn = "{{.RequestNumber}}"