From b68492dc0ba4356be5c2cef50cd401ac37287f63 Mon Sep 17 00:00:00 2001 From: Gregory Albouy Date: Sat, 15 Oct 2022 18:19:20 +0200 Subject: [PATCH 01/53] refactor(configparse): implement Representation.Unmarshal - ParseRepresentation -> UnmarshalRepresentation: change signature so it matches json.Unmarshal style - implement Representation.Unmarshal: alias to UnmarshalRepresentation - update JSON accordingly --- configparse/json.go | 9 ++++---- configparse/parse.go | 55 +++++++++++++++++--------------------------- 2 files changed, 25 insertions(+), 39 deletions(-) diff --git a/configparse/json.go b/configparse/json.go index ed052c9..6313a49 100644 --- a/configparse/json.go +++ b/configparse/json.go @@ -7,16 +7,15 @@ import ( // JSON reads input bytes as JSON and unmarshals it into a runner.ConfigGlobal. func JSON(in []byte) (runner.Config, error) { parser := JSONParser{} - - var repr Representation + repr := Representation{} if err := parser.Parse(in, &repr); err != nil { return runner.Config{}, err } - cfg, err := ParseRepresentation(repr) - if err != nil { + cfg := runner.DefaultConfig() + if err := repr.Unmarshal(&cfg); err != nil { return runner.Config{}, err } - return cfg.Override(runner.DefaultConfig()), nil + return cfg, nil } diff --git a/configparse/parse.go b/configparse/parse.go index 66761c7..f735708 100644 --- a/configparse/parse.go +++ b/configparse/parse.go @@ -44,32 +44,27 @@ type Representation struct { } `yaml:"tests" json:"tests"` } -// ParseRepresentation parses an input raw config as a runner.ConfigGlobal and returns -// a parsed Config or the first non-nil error occurring in the process. -func ParseRepresentation(repr Representation) (runner.Config, error) { //nolint:gocognit // acceptable complexity for a parsing func - cfg := runner.Config{} - assignedFields := []string{} - - addField := func(field string) { - assignedFields = append(assignedFields, field) - } - - abort := func(err error) (runner.Config, error) { - return runner.Config{}, err - } +// Unmarshal parses the Representation receiver as a runner.Config +// and stores any non-nil field value into the corresponding field +// of dst. +func (repr Representation) Unmarshal(dst *runner.Config) error { + return UnmarshalRepresentation(repr, dst) +} +// Unmarshal parses the given Representation as a runner.Config +// and stores any non-nil field value into the corresponding field +// of dst. +func UnmarshalRepresentation(repr Representation, cfg *runner.Config) error { //nolint:gocognit // acceptable complexity for a parsing func if method := repr.Request.Method; method != nil { cfg.Request.Method = *method - addField(runner.ConfigFieldMethod) } if rawURL := repr.Request.URL; rawURL != nil { - parsedURL, err := parseAndBuildURL(*repr.Request.URL, repr.Request.QueryParams) + parsedURL, err := parseAndBuildURL(*rawURL, repr.Request.QueryParams) if err != nil { - return abort(err) + return err } cfg.Request.URL = parsedURL - addField(runner.ConfigFieldURL) } if header := repr.Request.Header; header != nil { @@ -78,7 +73,6 @@ func ParseRepresentation(repr Representation) (runner.Config, error) { //nolint: httpHeader[key] = val } cfg.Request.Header = httpHeader - addField(runner.ConfigFieldHeader) } if body := repr.Request.Body; body != nil { @@ -86,49 +80,43 @@ func ParseRepresentation(repr Representation) (runner.Config, error) { //nolint: Type: body.Type, Content: []byte(body.Content), } - addField(runner.ConfigFieldBody) } if requests := repr.Runner.Requests; requests != nil { cfg.Runner.Requests = *requests - addField(runner.ConfigFieldRequests) } if concurrency := repr.Runner.Concurrency; concurrency != nil { cfg.Runner.Concurrency = *concurrency - addField(runner.ConfigFieldConcurrency) } if interval := repr.Runner.Interval; interval != nil { parsedInterval, err := parseOptionalDuration(*interval) if err != nil { - return abort(err) + return err } cfg.Runner.Interval = parsedInterval - addField(runner.ConfigFieldInterval) } if requestTimeout := repr.Runner.RequestTimeout; requestTimeout != nil { parsedTimeout, err := parseOptionalDuration(*requestTimeout) if err != nil { - return abort(err) + return err } cfg.Runner.RequestTimeout = parsedTimeout - addField(runner.ConfigFieldRequestTimeout) } if globalTimeout := repr.Runner.GlobalTimeout; globalTimeout != nil { parsedGlobalTimeout, err := parseOptionalDuration(*globalTimeout) if err != nil { - return abort(err) + return err } cfg.Runner.GlobalTimeout = parsedGlobalTimeout - addField(runner.ConfigFieldGlobalTimeout) } testSuite := repr.Tests if len(testSuite) == 0 { - return cfg.WithFields(assignedFields...), nil + return nil } cases := make([]runner.TestCase, len(testSuite)) @@ -143,22 +131,22 @@ func ParseRepresentation(repr Representation) (runner.Config, error) { //nolint: fieldPath("predicate"): t.Predicate, fieldPath("target"): t.Target, }); err != nil { - return abort(err) + return err } field := runner.MetricsField(*t.Field) if err := field.Validate(); err != nil { - return abort(fmt.Errorf("%s: %s", fieldPath("field"), err)) + return fmt.Errorf("%s: %s", fieldPath("field"), err) } predicate := runner.TestPredicate(*t.Predicate) if err := predicate.Validate(); err != nil { - return abort(fmt.Errorf("%s: %s", fieldPath("predicate"), err)) + return fmt.Errorf("%s: %s", fieldPath("predicate"), err) } target, err := parseMetricValue(field, fmt.Sprint(t.Target)) if err != nil { - return abort(fmt.Errorf("%s: %s", fieldPath("target"), err)) + return fmt.Errorf("%s: %s", fieldPath("target"), err) } cases[i] = runner.TestCase{ @@ -169,9 +157,8 @@ func ParseRepresentation(repr Representation) (runner.Config, error) { //nolint: } } cfg.Tests = cases - addField(runner.ConfigFieldTests) - return cfg.WithFields(assignedFields...), nil + return nil } // helpers From e7745df6c6e51957e59f02ae47be71ae1e91e954 Mon Sep 17 00:00:00 2001 From: Gregory Albouy Date: Sat, 15 Oct 2022 18:47:50 +0200 Subject: [PATCH 02/53] feat: remove Config methods that became irrelevant - remove Config.Override, Config.WithFields, Config.Equal - remove related tests - update tests relying on removed Config methods --- configparse/json_test.go | 48 +++---- runner/internal/config/config.go | 97 +------------- runner/internal/config/config_test.go | 176 -------------------------- 3 files changed, 20 insertions(+), 301 deletions(-) diff --git a/configparse/json_test.go b/configparse/json_test.go index 142c98d..adc2105 100644 --- a/configparse/json_test.go +++ b/configparse/json_test.go @@ -3,7 +3,6 @@ package configparse_test import ( "encoding/json" "errors" - "net/url" "testing" "github.com/benchttp/engine/configparse" @@ -18,18 +17,18 @@ func TestJSON(t *testing.T) { } testcases := []struct { - name string - input []byte - expConfig runner.Config - expError error + name string + input []byte + isValidConfig func(runner.Config) bool + expError error }{ { name: "returns error if input json has bad keys", input: baseInput.assign(object{ "badkey": "marcel-patulacci", }).json(), - expConfig: runner.Config{}, - expError: errors.New(`invalid field ("badkey"): does not exist`), + isValidConfig: func(cfg runner.Config) bool { return true }, + expError: errors.New(`invalid field ("badkey"): does not exist`), }, { name: "returns error if input json has bad values", @@ -38,24 +37,23 @@ func TestJSON(t *testing.T) { "concurrency": "bad value", // want int }, }).json(), - expConfig: runner.Config{}, - expError: errors.New(`wrong type for field runner.concurrency: want int, got string`), + isValidConfig: func(runner.Config) bool { return true }, + expError: errors.New(`wrong type for field runner.concurrency: want int, got string`), }, { name: "unmarshals JSON config and merges it with default", input: baseInput.assign(object{ "runner": object{"concurrency": 3}, }).json(), - expConfig: runner.Config{ - Request: runner.RequestConfig{ - URL: mustParseURL("https://example.com"), - }, - Runner: runner.RecorderConfig{ - Concurrency: 3, - }, - }. - WithFields("url", "concurrency"). - Override(runner.DefaultConfig()), + isValidConfig: func(cfg runner.Config) bool { + defaultConfig := runner.DefaultConfig() + + isInputValueParsed := cfg.Runner.Concurrency == 3 + isMergedWithDefault := cfg.Request.Method == defaultConfig.Request.Method && + cfg.Runner.GlobalTimeout == defaultConfig.Runner.GlobalTimeout + + return isInputValueParsed && isMergedWithDefault + }, expError: nil, }, } @@ -63,8 +61,8 @@ func TestJSON(t *testing.T) { for _, tc := range testcases { t.Run(tc.name, func(t *testing.T) { gotConfig, gotError := configparse.JSON(tc.input) - if !gotConfig.Equal(tc.expConfig) { - t.Errorf("unexpected config:\nexp %+v\ngot %+v", tc.expConfig, gotConfig) + if !tc.isValidConfig(gotConfig) { + t.Errorf("unexpected config:\n%+v", gotConfig) } if !sameErrors(gotError, tc.expError) { @@ -95,14 +93,6 @@ func (o object) assign(other object) object { return newObject } -func mustParseURL(rawURL string) *url.URL { - u, err := url.Parse(rawURL) - if err != nil { - panic(err) - } - return u -} - func sameErrors(a, b error) bool { if a == nil && b == nil { return true diff --git a/runner/internal/config/config.go b/runner/internal/config/config.go index 4076333..5a6b56a 100644 --- a/runner/internal/config/config.go +++ b/runner/internal/config/config.go @@ -7,7 +7,6 @@ import ( "fmt" "net/http" "net/url" - "reflect" "time" "github.com/benchttp/engine/runner/internal/tests" @@ -79,34 +78,12 @@ type Runner struct { GlobalTimeout time.Duration } -type set map[string]struct{} - -func (set set) add(values ...string) { - for _, v := range values { - set[v] = struct{}{} - } -} - // Global represents the global configuration of the runner. // It must be validated using Global.Validate before usage. type Global struct { Request Request Runner Runner - - Tests []tests.Case - - assignedFields set -} - -// WithField returns a new Global with the input fields marked as set. -// Accepted options are limited to existing Fields, other values are -// silently ignored. -func (cfg Global) WithFields(fields ...string) Global { - if cfg.assignedFields == nil { - cfg.assignedFields = set{} - } - cfg.assignedFields.add(fields...) - return cfg + Tests []tests.Case } // String implements fmt.Stringer. It returns an indented JSON representation @@ -116,78 +93,6 @@ func (cfg Global) String() string { return string(b) } -// Equal returns true if cfg and c are equal configurations. -func (cfg Global) Equal(c Global) bool { - cfg.assignedFields = nil - c.assignedFields = nil - return reflect.DeepEqual(cfg, c) -} - -// Override returns a new Config by overriding the values of base -// with the values from the Config receiver. -// Only fields previously specified by the receiver via Config.WithFields -// are replaced. -// All other values from base are preserved. -// -// The following example is equivalent to defaultConfig with the concurrency -// value from myConfig: -// -// myConfig. -// WithFields(FieldConcurrency). -// Override(defaultConfig) -// -// The following example is equivalent to defaultConfig, as no field as been -// tagged via WithFields by the receiver: -// -// myConfig.Override(defaultConfig) -func (cfg Global) Override(base Global) Global { - for field := range cfg.assignedFields { - switch field { - case FieldMethod: - base.Request.Method = cfg.Request.Method - case FieldURL: - base.Request.URL = cfg.Request.URL - case FieldHeader: - base.overrideHeader(cfg.Request.Header) - case FieldBody: - base.Request.Body = cfg.Request.Body - case FieldRequests: - base.Runner.Requests = cfg.Runner.Requests - case FieldConcurrency: - base.Runner.Concurrency = cfg.Runner.Concurrency - case FieldInterval: - base.Runner.Interval = cfg.Runner.Interval - case FieldRequestTimeout: - base.Runner.RequestTimeout = cfg.Runner.RequestTimeout - case FieldGlobalTimeout: - base.Runner.GlobalTimeout = cfg.Runner.GlobalTimeout - case FieldTests: - base.Tests = cfg.Tests - } - } - return base -} - -// overrideHeader overrides cfg's Request.Header with the values from newHeader. -// For every key in newHeader: -// -// - If it's not present in cfg.Request.Header, it is added. -// -// - If it's already present in cfg.Request.Header, the value is replaced. -// -// - All other keys in cfg.Request.Header are left untouched. -func (cfg *Global) overrideHeader(newHeader http.Header) { - if newHeader == nil { - return - } - if cfg.Request.Header == nil { - cfg.Request.Header = http.Header{} - } - for k, v := range newHeader { - cfg.Request.Header[k] = v - } -} - // Validate returns a non-nil InvalidConfigError if any of its fields // does not meet the requirements. func (cfg Global) Validate() error { //nolint:gocognit diff --git a/runner/internal/config/config_test.go b/runner/internal/config/config_test.go index 6d43ebe..6218614 100644 --- a/runner/internal/config/config_test.go +++ b/runner/internal/config/config_test.go @@ -8,7 +8,6 @@ import ( "net/url" "reflect" "testing" - "time" "github.com/benchttp/engine/runner/internal/config" ) @@ -70,144 +69,6 @@ func TestGlobal_Validate(t *testing.T) { }) } -func TestGlobal_Override(t *testing.T) { - t.Run("do not override unspecified fields", func(t *testing.T) { - baseCfg := config.Global{} - nextCfg := config.Global{ - Request: config.Request{ - Body: config.RequestBody{}, - }.WithURL("http://a.b?p=2"), - Runner: config.Runner{ - Requests: 1, - Concurrency: 2, - RequestTimeout: 3 * time.Second, - GlobalTimeout: 4 * time.Second, - }, - } - - if gotCfg := nextCfg.Override(baseCfg); !gotCfg.Equal(baseCfg) { - t.Errorf("overrode unexpected fields:\nexp %#v\ngot %#v", baseCfg, gotCfg) - } - }) - - t.Run("override specified fields", func(t *testing.T) { - fields := []string{ - config.FieldMethod, - config.FieldURL, - config.FieldRequests, - config.FieldConcurrency, - config.FieldRequestTimeout, - config.FieldGlobalTimeout, - config.FieldBody, - } - - baseCfg := config.Global{} - nextCfg := config.Global{ - Request: config.Request{ - Body: validBody, - }.WithURL("http://a.b?p=2"), - Runner: config.Runner{ - Requests: 1, - Concurrency: 2, - RequestTimeout: 3 * time.Second, - GlobalTimeout: 4 * time.Second, - }, - }.WithFields(fields...) - - if gotCfg := nextCfg.Override(baseCfg); !gotCfg.Equal(nextCfg) { - t.Errorf("did not override expected fields:\nexp %v\ngot %v", nextCfg, gotCfg) - t.Log(fields) - } - }) - - t.Run("override header selectively", func(t *testing.T) { - testcases := []struct { - label string - oldHeader http.Header - newHeader http.Header - expHeader http.Header - }{ - { - label: "erase overridden keys", - oldHeader: http.Header{"key": []string{"oldval"}}, - newHeader: http.Header{"key": []string{"newval"}}, - expHeader: http.Header{"key": []string{"newval"}}, - }, - { - label: "do not erase not overridden keys", - oldHeader: http.Header{"key": []string{"oldval"}}, - newHeader: http.Header{}, - expHeader: http.Header{"key": []string{"oldval"}}, - }, - { - label: "add new keys", - oldHeader: http.Header{"key0": []string{"oldval"}}, - newHeader: http.Header{"key1": []string{"newval"}}, - expHeader: http.Header{ - "key0": []string{"oldval"}, - "key1": []string{"newval"}, - }, - }, - { - label: "erase only overridden keys", - oldHeader: http.Header{ - "key0": []string{"oldval0", "oldval1"}, - "key1": []string{"oldval0", "oldval1"}, - }, - newHeader: http.Header{ - "key1": []string{"newval0", "newval1"}, - "key2": []string{"newval0", "newval1"}, - }, - expHeader: http.Header{ - "key0": []string{"oldval0", "oldval1"}, - "key1": []string{"newval0", "newval1"}, - "key2": []string{"newval0", "newval1"}, - }, - }, - { - label: "nil new header does nothing", - oldHeader: http.Header{"key": []string{"val"}}, - newHeader: nil, - expHeader: http.Header{"key": []string{"val"}}, - }, - { - label: "replace nil old header", - oldHeader: nil, - newHeader: http.Header{"key": []string{"val"}}, - expHeader: http.Header{"key": []string{"val"}}, - }, - { - label: "nil over nil is nil", - oldHeader: nil, - newHeader: nil, - expHeader: nil, - }, - } - - for _, tc := range testcases { - t.Run(tc.label, func(t *testing.T) { - baseCfg := config.Global{ - Request: config.Request{ - Header: tc.oldHeader, - }, - } - - nextCfg := config.Global{ - Request: config.Request{ - Header: tc.newHeader, - }, - }.WithFields(config.FieldHeader) - - gotCfg := nextCfg.Override(baseCfg) - - if gotHeader := gotCfg.Request.Header; !reflect.DeepEqual(gotHeader, tc.expHeader) { - t.Errorf("\nexp %#v\ngot %#v", tc.expHeader, gotHeader) - } - }) - } - }) -} - func TestRequest_WithURL(t *testing.T) { t.Run("set empty url if invalid", func(t *testing.T) { cfg := config.Global{Request: config.Request{}.WithURL("abc")} @@ -297,43 +158,6 @@ func TestRequest_Value(t *testing.T) { }) } -func TestGlobal_Equal(t *testing.T) { - t.Run("returns false for different configs", func(t *testing.T) { - base := config.Default() - diff := base - diff.Runner.Requests = base.Runner.Requests + 1 - - if base.Equal(diff) { - t.Error("exp unequal configs") - } - }) - - t.Run("ignores set fields", func(t *testing.T) { - base := config.Default() - same := base.WithFields(config.FieldRequests) - - if !base.Equal(same) { - t.Error("exp equal configs") - } - }) - - t.Run("does not alter configs", func(t *testing.T) { - baseA := config.Default().WithFields(config.FieldRequests) - copyA := config.Default().WithFields(config.FieldRequests) - baseB := config.Default().WithFields(config.FieldURL) - copyB := config.Default().WithFields(config.FieldURL) - - baseA.Equal(baseB) - - if !reflect.DeepEqual(baseA, copyA) { - t.Error("altered receiver config") - } - if !reflect.DeepEqual(baseB, copyB) { - t.Error("altered parameter config") - } - }) -} - // helpers // findErrorOrFail fails t if no error in src matches msg. From b2990bea10a1062cc4ef7630ae70eade8e73ca7c Mon Sep 17 00:00:00 2001 From: Gregory Albouy Date: Tue, 18 Oct 2022 23:51:39 +0200 Subject: [PATCH 03/53] chore: remove unused IsConfigField --- runner/internal/config/field.go | 33 ---------------------------- runner/internal/config/field_test.go | 24 -------------------- runner/runner.go | 17 ++------------ 3 files changed, 2 insertions(+), 72 deletions(-) delete mode 100644 runner/internal/config/field.go delete mode 100644 runner/internal/config/field_test.go diff --git a/runner/internal/config/field.go b/runner/internal/config/field.go deleted file mode 100644 index c23c831..0000000 --- a/runner/internal/config/field.go +++ /dev/null @@ -1,33 +0,0 @@ -package config - -const ( - FieldMethod = "method" - FieldURL = "url" - FieldHeader = "header" - FieldBody = "body" - FieldRequests = "requests" - FieldConcurrency = "concurrency" - FieldInterval = "interval" - FieldRequestTimeout = "requestTimeout" - FieldGlobalTimeout = "globalTimeout" - FieldTests = "tests" -) - -// FieldsUsage is a record of all available config fields and their usage. -var FieldsUsage = map[string]string{ - FieldMethod: "HTTP request method", - FieldURL: "HTTP request url", - FieldHeader: "HTTP request header", - FieldBody: "HTTP request body", - FieldRequests: "Number of requests to run, use duration as exit condition if omitted", - FieldConcurrency: "Number of connections to run concurrently", - FieldInterval: "Minimum duration between two non concurrent requests", - FieldRequestTimeout: "Timeout for each HTTP request", - FieldGlobalTimeout: "Max duration of test", - FieldTests: "Test suite", -} - -func IsField(v string) bool { - _, exists := FieldsUsage[v] - return exists -} diff --git a/runner/internal/config/field_test.go b/runner/internal/config/field_test.go deleted file mode 100644 index ad53a23..0000000 --- a/runner/internal/config/field_test.go +++ /dev/null @@ -1,24 +0,0 @@ -package config_test - -import ( - "testing" - - "github.com/drykit-go/testx" - - "github.com/benchttp/engine/runner/internal/config" -) - -func TestIsField(t *testing.T) { - testx.Table(config.IsField).Cases([]testx.Case{ - {In: config.FieldMethod, Exp: true}, - {In: config.FieldURL, Exp: true}, - {In: config.FieldHeader, Exp: true}, - {In: config.FieldBody, Exp: true}, - {In: config.FieldRequests, Exp: true}, - {In: config.FieldConcurrency, Exp: true}, - {In: config.FieldInterval, Exp: true}, - {In: config.FieldRequestTimeout, Exp: true}, - {In: config.FieldGlobalTimeout, Exp: true}, - {In: "notafield", Exp: false}, - }).Run(t) -} diff --git a/runner/runner.go b/runner/runner.go index a68db93..38817c5 100644 --- a/runner/runner.go +++ b/runner/runner.go @@ -41,24 +41,11 @@ const ( StatusCanceled = recorder.StatusCanceled StatusTimeout = recorder.StatusTimeout StatusDone = recorder.StatusDone - - ConfigFieldMethod = config.FieldMethod - ConfigFieldURL = config.FieldURL - ConfigFieldHeader = config.FieldHeader - ConfigFieldBody = config.FieldBody - ConfigFieldRequests = config.FieldRequests - ConfigFieldConcurrency = config.FieldConcurrency - ConfigFieldInterval = config.FieldInterval - ConfigFieldRequestTimeout = config.FieldRequestTimeout - ConfigFieldGlobalTimeout = config.FieldGlobalTimeout - ConfigFieldTests = config.FieldTests ) var ( - DefaultConfig = config.Default - ConfigFieldsUsage = config.FieldsUsage - NewRequestBody = config.NewRequestBody - IsConfigField = config.IsField + DefaultConfig = config.Default + NewRequestBody = config.NewRequestBody ErrCanceled = recorder.ErrCanceled ) From 474dec488ce6c02390164e9c4bf623f94b2b100f Mon Sep 17 00:00:00 2001 From: Gregory Albouy Date: Tue, 18 Oct 2022 23:52:19 +0200 Subject: [PATCH 04/53] refactor(configparse): rename Representation.Unmarshal Unmarshal -> ParseInto --- configparse/json.go | 2 +- configparse/parse.go | 34 ++++++++++++++-------------------- 2 files changed, 15 insertions(+), 21 deletions(-) diff --git a/configparse/json.go b/configparse/json.go index 6313a49..1a1d718 100644 --- a/configparse/json.go +++ b/configparse/json.go @@ -13,7 +13,7 @@ func JSON(in []byte) (runner.Config, error) { } cfg := runner.DefaultConfig() - if err := repr.Unmarshal(&cfg); err != nil { + if err := repr.ParseInto(&cfg); err != nil { return runner.Config{}, err } diff --git a/configparse/parse.go b/configparse/parse.go index f735708..2b14215 100644 --- a/configparse/parse.go +++ b/configparse/parse.go @@ -10,10 +10,11 @@ import ( "github.com/benchttp/engine/runner" ) -// Representation is a raw data model for runner config files. +// Representation is a raw data model for formatted runner config (json, yaml). // It serves as a receiver for unmarshaling processes and for that reason // its types are kept simple (certain types are incompatible with certain // unmarshalers). +// It exposes a method Unmarshal to convert its values into a runner.Config. type Representation struct { Extends *string `yaml:"extends" json:"extends"` @@ -44,19 +45,12 @@ type Representation struct { } `yaml:"tests" json:"tests"` } -// Unmarshal parses the Representation receiver as a runner.Config +// ParseInto parses the Representation receiver as a runner.Config // and stores any non-nil field value into the corresponding field // of dst. -func (repr Representation) Unmarshal(dst *runner.Config) error { - return UnmarshalRepresentation(repr, dst) -} - -// Unmarshal parses the given Representation as a runner.Config -// and stores any non-nil field value into the corresponding field -// of dst. -func UnmarshalRepresentation(repr Representation, cfg *runner.Config) error { //nolint:gocognit // acceptable complexity for a parsing func +func (repr Representation) ParseInto(dst *runner.Config) error { //nolint:gocognit // acceptable complexity for a parsing func if method := repr.Request.Method; method != nil { - cfg.Request.Method = *method + dst.Request.Method = *method } if rawURL := repr.Request.URL; rawURL != nil { @@ -64,7 +58,7 @@ func UnmarshalRepresentation(repr Representation, cfg *runner.Config) error { // if err != nil { return err } - cfg.Request.URL = parsedURL + dst.Request.URL = parsedURL } if header := repr.Request.Header; header != nil { @@ -72,22 +66,22 @@ func UnmarshalRepresentation(repr Representation, cfg *runner.Config) error { // for key, val := range header { httpHeader[key] = val } - cfg.Request.Header = httpHeader + dst.Request.Header = httpHeader } if body := repr.Request.Body; body != nil { - cfg.Request.Body = runner.RequestBody{ + dst.Request.Body = runner.RequestBody{ Type: body.Type, Content: []byte(body.Content), } } if requests := repr.Runner.Requests; requests != nil { - cfg.Runner.Requests = *requests + dst.Runner.Requests = *requests } if concurrency := repr.Runner.Concurrency; concurrency != nil { - cfg.Runner.Concurrency = *concurrency + dst.Runner.Concurrency = *concurrency } if interval := repr.Runner.Interval; interval != nil { @@ -95,7 +89,7 @@ func UnmarshalRepresentation(repr Representation, cfg *runner.Config) error { // if err != nil { return err } - cfg.Runner.Interval = parsedInterval + dst.Runner.Interval = parsedInterval } if requestTimeout := repr.Runner.RequestTimeout; requestTimeout != nil { @@ -103,7 +97,7 @@ func UnmarshalRepresentation(repr Representation, cfg *runner.Config) error { // if err != nil { return err } - cfg.Runner.RequestTimeout = parsedTimeout + dst.Runner.RequestTimeout = parsedTimeout } if globalTimeout := repr.Runner.GlobalTimeout; globalTimeout != nil { @@ -111,7 +105,7 @@ func UnmarshalRepresentation(repr Representation, cfg *runner.Config) error { // if err != nil { return err } - cfg.Runner.GlobalTimeout = parsedGlobalTimeout + dst.Runner.GlobalTimeout = parsedGlobalTimeout } testSuite := repr.Tests @@ -156,7 +150,7 @@ func UnmarshalRepresentation(repr Representation, cfg *runner.Config) error { // Target: target, } } - cfg.Tests = cases + dst.Tests = cases return nil } From 28b0fc26b7452cc3804ef92d8a9a01761edf06dc Mon Sep 17 00:00:00 2001 From: Gregory Albouy Date: Tue, 18 Oct 2022 23:53:01 +0200 Subject: [PATCH 05/53] deps: remove unused dependencies - run go mod tidy --- go.mod | 3 --- go.sum | 6 ------ 2 files changed, 9 deletions(-) diff --git a/go.mod b/go.mod index 4de6a8f..23faa4d 100644 --- a/go.mod +++ b/go.mod @@ -3,9 +3,6 @@ module github.com/benchttp/engine go 1.17 require ( - github.com/drykit-go/testx v1.2.0 golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 gopkg.in/yaml.v3 v3.0.1 ) - -require github.com/drykit-go/cond v0.1.0 // indirect diff --git a/go.sum b/go.sum index 12452b0..12011ba 100644 --- a/go.sum +++ b/go.sum @@ -1,9 +1,3 @@ -github.com/drykit-go/cond v0.1.0 h1:y7MNxREQLT83vGfcfSKjyFPLC/ZDjYBNp6KuaVVjOg4= -github.com/drykit-go/cond v0.1.0/go.mod h1:7MXBFjjaB5ZCEB8Q4w2euNOaWuTqf7NjOFZAyV1Jpfg= -github.com/drykit-go/strcase v0.2.0/go.mod h1:cWK0/az2f09UPIbJ42Sb8Iqdv01uENrFX+XXKGjPo+8= -github.com/drykit-go/testx v0.1.0/go.mod h1:qGXb49a8CzQ82crBeCVW8R3kGU1KRgWHnI+Q6CNVbz8= -github.com/drykit-go/testx v1.2.0 h1:UsH+tFd24z3Xu+mwvwPY+9eBEg9CUyMsUeMYyUprG0o= -github.com/drykit-go/testx v1.2.0/go.mod h1:qTzXJgnAg8n31woklBzNTaWzLMJrnFk93x/aeaIpc20= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 h1:uVc8UZUe6tr40fFVnUP5Oj+veunVezqYl9z7DYw9xzw= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= From c2812216e4046d8c2cc60335bbe8d15137eb5e6d Mon Sep 17 00:00:00 2001 From: Gregory Albouy Date: Wed, 19 Oct 2022 00:59:42 +0200 Subject: [PATCH 06/53] refactor(runner): use *http.Request for Config.Request --- configparse/parse.go | 44 +++++++-- runner/internal/config/config.go | 67 +------------- runner/internal/config/config_test.go | 123 ++------------------------ runner/internal/config/default.go | 17 ++-- runner/runner.go | 13 +-- 5 files changed, 59 insertions(+), 205 deletions(-) diff --git a/configparse/parse.go b/configparse/parse.go index 2b14215..0ee8fbd 100644 --- a/configparse/parse.go +++ b/configparse/parse.go @@ -1,7 +1,10 @@ package configparse import ( + "bytes" + "errors" "fmt" + "io" "net/http" "net/url" "strconv" @@ -48,17 +51,29 @@ type Representation struct { // ParseInto parses the Representation receiver as a runner.Config // and stores any non-nil field value into the corresponding field // of dst. -func (repr Representation) ParseInto(dst *runner.Config) error { //nolint:gocognit // acceptable complexity for a parsing func +func (repr Representation) ParseInto(dst *runner.Config) error { + if err := repr.parseRequestInto(dst); err != nil { + return err + } + if err := repr.parseRunnerInto(dst); err != nil { + return err + } + return repr.parseTestsInto(dst) +} + +func (repr Representation) parseRequestInto(dst *runner.Config) error { + req := &http.Request{} + if method := repr.Request.Method; method != nil { - dst.Request.Method = *method + req.Method = *method } if rawURL := repr.Request.URL; rawURL != nil { parsedURL, err := parseAndBuildURL(*rawURL, repr.Request.QueryParams) if err != nil { - return err + return fmt.Errorf(`configparse: invalid url: %q`, *rawURL) } - dst.Request.URL = parsedURL + req.URL = parsedURL } if header := repr.Request.Header; header != nil { @@ -66,16 +81,23 @@ func (repr Representation) ParseInto(dst *runner.Config) error { //nolint:gocogn for key, val := range header { httpHeader[key] = val } - dst.Request.Header = httpHeader + req.Header = httpHeader } if body := repr.Request.Body; body != nil { - dst.Request.Body = runner.RequestBody{ - Type: body.Type, - Content: []byte(body.Content), + switch body.Type { + case "raw": + req.Body = io.NopCloser(bytes.NewReader([]byte(body.Content))) + default: + return errors.New(`configparse: request.body.type: only "raw" accepted`) } } + *dst.Request = *req + return nil +} + +func (repr Representation) parseRunnerInto(dst *runner.Config) error { if requests := repr.Runner.Requests; requests != nil { dst.Runner.Requests = *requests } @@ -108,6 +130,10 @@ func (repr Representation) ParseInto(dst *runner.Config) error { //nolint:gocogn dst.Runner.GlobalTimeout = parsedGlobalTimeout } + return nil +} + +func (repr Representation) parseTestsInto(dst *runner.Config) error { testSuite := repr.Tests if len(testSuite) == 0 { return nil @@ -150,8 +176,8 @@ func (repr Representation) ParseInto(dst *runner.Config) error { //nolint:gocogn Target: target, } } - dst.Tests = cases + dst.Tests = cases return nil } diff --git a/runner/internal/config/config.go b/runner/internal/config/config.go index 5a6b56a..efce833 100644 --- a/runner/internal/config/config.go +++ b/runner/internal/config/config.go @@ -1,74 +1,15 @@ package config import ( - "bytes" "encoding/json" "errors" "fmt" "net/http" - "net/url" "time" "github.com/benchttp/engine/runner/internal/tests" ) -// RequestBody represents a request body associated with a type. -// The type affects the way the content is processed. -// If Type == "file", Content is read as a filepath to be resolved. -// If Type == "raw", Content is attached as-is. -// -// Note: only "raw" is supported at the moment. -type RequestBody struct { - Type string - Content []byte -} - -// NewRequestBody returns a Body initialized with the given type and content. -// For now, the only valid value for type is "raw". -func NewRequestBody(typ, content string) RequestBody { - return RequestBody{Type: typ, Content: []byte(content)} -} - -// Request contains the confing options relative to a single request. -type Request struct { - Method string - URL *url.URL - Header http.Header - Body RequestBody -} - -// Value generates a *http.Request based on Request and returns it -// or any non-nil error that occurred. -func (r Request) Value() (*http.Request, error) { - if r.URL == nil { - return nil, errors.New("empty url") - } - rawURL := r.URL.String() - if _, err := url.ParseRequestURI(rawURL); err != nil { - return nil, errors.New("bad url") - } - - req, err := http.NewRequest(r.Method, rawURL, bytes.NewReader(r.Body.Content)) - if err != nil { - return nil, err - } - req.Header = r.Header - return req, nil -} - -// WithURL sets the current Request with the parsed *url.URL from rawURL -// and returns it. Any errors is discarded as a Config can be invalid -// until Config.Validate is called. The url is always non-nil. -func (r Request) WithURL(rawURL string) Request { - // ignore err: a Config can be invalid at this point - urlURL, _ := url.ParseRequestURI(rawURL) - if urlURL == nil { - urlURL = &url.URL{} - } - r.URL = urlURL - return r -} - // Runner contains options relative to the runner. type Runner struct { Requests int @@ -81,7 +22,7 @@ type Runner struct { // Global represents the global configuration of the runner. // It must be validated using Global.Validate before usage. type Global struct { - Request Request + Request *http.Request Runner Runner Tests []tests.Case } @@ -101,10 +42,8 @@ func (cfg Global) Validate() error { //nolint:gocognit errs = append(errs, err) } - if cfg.Request.URL == nil { - appendError(errors.New("url: missing")) - } else if _, err := url.ParseRequestURI(cfg.Request.URL.String()); err != nil { - appendError(fmt.Errorf("url (%q): invalid", cfg.Request.URL.String())) + if cfg.Request == nil { + appendError(errors.New("unexpected nil request")) } if cfg.Runner.Requests < 1 && cfg.Runner.Requests != -1 { diff --git a/runner/internal/config/config_test.go b/runner/internal/config/config_test.go index 6218614..a8d3c20 100644 --- a/runner/internal/config/config_test.go +++ b/runner/internal/config/config_test.go @@ -1,25 +1,17 @@ package config_test import ( - "bytes" "errors" - "io" "net/http" - "net/url" - "reflect" "testing" "github.com/benchttp/engine/runner/internal/config" ) -var validBody = config.NewRequestBody("raw", `{"key0": "val0", "key1": "val1"}`) - func TestGlobal_Validate(t *testing.T) { t.Run("return nil if config is valid", func(t *testing.T) { cfg := config.Global{ - Request: config.Request{ - Body: validBody, - }.WithURL("https://github.com/benchttp/"), + Request: validRequest(), Runner: config.Runner{ Requests: 5, Concurrency: 5, @@ -35,9 +27,7 @@ func TestGlobal_Validate(t *testing.T) { t.Run("return cumulated errors if config is invalid", func(t *testing.T) { cfg := config.Global{ - Request: config.Request{ - Body: config.RequestBody{}, - }.WithURL("abc"), + Request: nil, Runner: config.Runner{ Requests: -5, Concurrency: -5, @@ -58,7 +48,7 @@ func TestGlobal_Validate(t *testing.T) { } errs := errInvalid.Errors - findErrorOrFail(t, errs, `url (""): invalid`) + findErrorOrFail(t, errs, `unexpected nil request`) findErrorOrFail(t, errs, `requests (-5): want >= 0`) findErrorOrFail(t, errs, `concurrency (-5): want > 0 and <= requests (-5)`) findErrorOrFail(t, errs, `interval (-5): want >= 0`) @@ -69,97 +59,16 @@ func TestGlobal_Validate(t *testing.T) { }) } -func TestRequest_WithURL(t *testing.T) { - t.Run("set empty url if invalid", func(t *testing.T) { - cfg := config.Global{Request: config.Request{}.WithURL("abc")} - if got := cfg.Request.URL; !reflect.DeepEqual(got, &url.URL{}) { - t.Errorf("exp empty *url.URL, got %v", got) - } - }) - - t.Run("set parsed url", func(t *testing.T) { - var ( - rawURL = "http://benchttp.app?cool=true" - expURL, _ = url.ParseRequestURI(rawURL) - gotURL = config.Request{}.WithURL(rawURL).URL - ) - - if !reflect.DeepEqual(gotURL, expURL) { - t.Errorf("\nexp %v\ngot %v", expURL, gotURL) - } - }) -} - -func TestRequest_Value(t *testing.T) { - testcases := []struct { - label string - in config.Request - expMsg string - }{ - { - label: "return error if url is empty", - in: config.Request{}, - expMsg: "empty url", - }, - { - label: "return error if url is invalid", - in: config.Request{URL: &url.URL{Scheme: ""}}, - expMsg: "bad url", - }, - { - label: "return error if NewRequest fails", - in: config.Request{Method: "é", URL: &url.URL{Scheme: "http"}}, - expMsg: `net/http: invalid method "é"`, - }, - } - - for _, tc := range testcases { - t.Run(tc.label, func(t *testing.T) { - gotReq, gotErr := tc.in.Value() - if gotErr == nil { - t.Fatal("exp error, got nil") - } - - if gotMsg := gotErr.Error(); gotMsg != tc.expMsg { - t.Errorf("\nexp %q\ngot %q", tc.expMsg, gotMsg) - } +// helpers - if gotReq != nil { - t.Errorf("exp nil, got %v", gotReq) - } - }) +func validRequest() *http.Request { + req, err := http.NewRequest("GET", "https://a.b#c?d=e&f=g", nil) + if err != nil { + panic(err) } - - t.Run("return request with added headers", func(t *testing.T) { - in := config.Request{ - Method: "POST", - Header: http.Header{"key": []string{"val"}}, - Body: config.RequestBody{Content: []byte("abc")}, - }.WithURL("http://a.b") - - expReq, err := http.NewRequest( - in.Method, - in.URL.String(), - bytes.NewReader(in.Body.Content), - ) - if err != nil { - t.Fatal(err) - } - expReq.Header = in.Header - - gotReq, gotErr := in.Value() - if gotErr != nil { - t.Fatal(err) - } - - if !sameRequests(gotReq, expReq) { - t.Errorf("\nexp %#v\ngot %#v", expReq, gotReq) - } - }) + return req } -// helpers - // findErrorOrFail fails t if no error in src matches msg. func findErrorOrFail(t *testing.T, src []error, msg string) { t.Helper() @@ -170,17 +79,3 @@ func findErrorOrFail(t *testing.T, src []error, msg string) { } t.Errorf("missing error: %v", msg) } - -func sameRequests(a, b *http.Request) bool { - if a == nil || b == nil { - return a == b - } - - ab, _ := io.ReadAll(a.Body) - bb, _ := io.ReadAll(b.Body) - - return a.Method == b.Method && - a.URL.String() == b.URL.String() && - bytes.Equal(ab, bb) && - reflect.DeepEqual(a.Header, b.Header) -} diff --git a/runner/internal/config/default.go b/runner/internal/config/default.go index b5299de..9bae34f 100644 --- a/runner/internal/config/default.go +++ b/runner/internal/config/default.go @@ -1,18 +1,13 @@ package config import ( + "fmt" "net/http" - "net/url" "time" ) var defaultConfig = Global{ - Request: Request{ - Method: "GET", - URL: &url.URL{}, - Header: http.Header{}, - Body: RequestBody{}, - }, + Request: defaultRequest(), Runner: Runner{ Concurrency: 10, Requests: 100, @@ -26,3 +21,11 @@ var defaultConfig = Global{ func Default() Global { return defaultConfig } + +func defaultRequest() *http.Request { + req, err := http.NewRequest("GET", "", nil) + if err != nil { + panic(fmt.Sprintf("benchttp/runner: %s", err)) + } + return req +} diff --git a/runner/runner.go b/runner/runner.go index 38817c5..10467bc 100644 --- a/runner/runner.go +++ b/runner/runner.go @@ -13,8 +13,6 @@ import ( type ( Config = config.Global - RequestConfig = config.Request - RequestBody = config.RequestBody RecorderConfig = config.Runner InvalidConfigError = config.InvalidConfigError @@ -44,8 +42,7 @@ const ( ) var ( - DefaultConfig = config.Default - NewRequestBody = config.NewRequestBody + DefaultConfig = config.Default ErrCanceled = recorder.ErrCanceled ) @@ -65,19 +62,13 @@ func (r *Runner) Run(ctx context.Context, cfg config.Global) (*Report, error) { return nil, err } - // Generate http request from input config - rq, err := cfg.Request.Value() - if err != nil { - return nil, err - } - // Create and attach request recorder r.recorder = recorder.New(recorderConfig(cfg, r.onRecordingProgress)) startTime := time.Now() // Run request recorder - records, err := r.recorder.Record(ctx, rq) + records, err := r.recorder.Record(ctx, cfg.Request) if err != nil { return nil, err } From 106281a56f39f8caa1dd4099b1380f2e2465950e Mon Sep 17 00:00:00 2001 From: Gregory Albouy Date: Wed, 19 Oct 2022 01:16:58 +0200 Subject: [PATCH 07/53] refactor(runner): flatten packages runner, config, report --- configparse/json.go | 2 +- runner/{internal/config => }/config.go | 18 ++++++++--------- runner/{internal/config => }/config_test.go | 16 +++++++-------- runner/{internal/config => }/default.go | 10 +++++----- runner/{internal/config => }/error.go | 4 ++-- runner/{internal/config => }/error_test.go | 6 +++--- runner/{internal/report => }/report.go | 11 +++++------ runner/runner.go | 22 ++++----------------- 8 files changed, 37 insertions(+), 52 deletions(-) rename runner/{internal/config => }/config.go (81%) rename runner/{internal/config => }/config_test.go (86%) rename runner/{internal/config => }/default.go (72%) rename runner/{internal/config => }/error.go (85%) rename runner/{internal/config => }/error_test.go (79%) rename runner/{internal/report => }/report.go (80%) diff --git a/configparse/json.go b/configparse/json.go index 1a1d718..7297947 100644 --- a/configparse/json.go +++ b/configparse/json.go @@ -4,7 +4,7 @@ import ( "github.com/benchttp/engine/runner" ) -// JSON reads input bytes as JSON and unmarshals it into a runner.ConfigGlobal. +// JSON reads input bytes as JSON and unmarshals it into a runner.Config. func JSON(in []byte) (runner.Config, error) { parser := JSONParser{} repr := Representation{} diff --git a/runner/internal/config/config.go b/runner/config.go similarity index 81% rename from runner/internal/config/config.go rename to runner/config.go index efce833..30158bb 100644 --- a/runner/internal/config/config.go +++ b/runner/config.go @@ -1,4 +1,4 @@ -package config +package runner import ( "encoding/json" @@ -10,8 +10,8 @@ import ( "github.com/benchttp/engine/runner/internal/tests" ) -// Runner contains options relative to the runner. -type Runner struct { +// RunnerConfig contains options relative to the runner. +type RunnerConfig struct { Requests int Concurrency int Interval time.Duration @@ -19,24 +19,24 @@ type Runner struct { GlobalTimeout time.Duration } -// Global represents the global configuration of the runner. -// It must be validated using Global.Validate before usage. -type Global struct { +// Config represents the global configuration of the runner. +// It must be validated using Config.Validate before usage. +type Config struct { Request *http.Request - Runner Runner + Runner RunnerConfig Tests []tests.Case } // String implements fmt.Stringer. It returns an indented JSON representation // of Config for debugging purposes. -func (cfg Global) String() string { +func (cfg Config) String() string { b, _ := json.MarshalIndent(cfg, "", " ") return string(b) } // Validate returns a non-nil InvalidConfigError if any of its fields // does not meet the requirements. -func (cfg Global) Validate() error { //nolint:gocognit +func (cfg Config) Validate() error { //nolint:gocognit errs := []error{} appendError := func(err error) { errs = append(errs, err) diff --git a/runner/internal/config/config_test.go b/runner/config_test.go similarity index 86% rename from runner/internal/config/config_test.go rename to runner/config_test.go index a8d3c20..c3a2734 100644 --- a/runner/internal/config/config_test.go +++ b/runner/config_test.go @@ -1,18 +1,18 @@ -package config_test +package runner_test import ( "errors" "net/http" "testing" - "github.com/benchttp/engine/runner/internal/config" + "github.com/benchttp/engine/runner" ) -func TestGlobal_Validate(t *testing.T) { +func TestConfig_Validate(t *testing.T) { t.Run("return nil if config is valid", func(t *testing.T) { - cfg := config.Global{ + cfg := runner.Config{ Request: validRequest(), - Runner: config.Runner{ + Runner: runner.RunnerConfig{ Requests: 5, Concurrency: 5, Interval: 5, @@ -26,9 +26,9 @@ func TestGlobal_Validate(t *testing.T) { }) t.Run("return cumulated errors if config is invalid", func(t *testing.T) { - cfg := config.Global{ + cfg := runner.Config{ Request: nil, - Runner: config.Runner{ + Runner: runner.RunnerConfig{ Requests: -5, Concurrency: -5, Interval: -5, @@ -42,7 +42,7 @@ func TestGlobal_Validate(t *testing.T) { t.Fatal("invalid configuration considered valid") } - var errInvalid *config.InvalidConfigError + var errInvalid *runner.InvalidConfigError if !errors.As(err, &errInvalid) { t.Fatalf("unexpected error type: %T", err) } diff --git a/runner/internal/config/default.go b/runner/default.go similarity index 72% rename from runner/internal/config/default.go rename to runner/default.go index 9bae34f..84b896d 100644 --- a/runner/internal/config/default.go +++ b/runner/default.go @@ -1,4 +1,4 @@ -package config +package runner import ( "fmt" @@ -6,9 +6,9 @@ import ( "time" ) -var defaultConfig = Global{ +var defaultConfig = Config{ Request: defaultRequest(), - Runner: Runner{ + Runner: RunnerConfig{ Concurrency: 10, Requests: 100, Interval: 0 * time.Second, @@ -17,8 +17,8 @@ var defaultConfig = Global{ }, } -// Default returns a default config that is safe to use. -func Default() Global { +// DefaultConfig returns a default config that is safe to use. +func DefaultConfig() Config { return defaultConfig } diff --git a/runner/internal/config/error.go b/runner/error.go similarity index 85% rename from runner/internal/config/error.go rename to runner/error.go index a69cf82..145f72c 100644 --- a/runner/internal/config/error.go +++ b/runner/error.go @@ -1,8 +1,8 @@ -package config +package runner import "strings" -// InvalidConfigError is the errors returned by Global.Validate +// InvalidConfigError is the errors returned by Config.Validate // when values are missing or invalid. type InvalidConfigError struct { Errors []error diff --git a/runner/internal/config/error_test.go b/runner/error_test.go similarity index 79% rename from runner/internal/config/error_test.go rename to runner/error_test.go index 8ff4421..f8c3af2 100644 --- a/runner/internal/config/error_test.go +++ b/runner/error_test.go @@ -1,14 +1,14 @@ -package config_test +package runner_test import ( "errors" "testing" - "github.com/benchttp/engine/runner/internal/config" + "github.com/benchttp/engine/runner" ) func TestInvalidConfigError_Error(t *testing.T) { - e := config.InvalidConfigError{ + e := runner.InvalidConfigError{ Errors: []error{ errors.New("error 0"), errors.New("error 1\nwith new line"), diff --git a/runner/internal/report/report.go b/runner/report.go similarity index 80% rename from runner/internal/report/report.go rename to runner/report.go index 8f972c9..cd75bf9 100644 --- a/runner/internal/report/report.go +++ b/runner/report.go @@ -1,9 +1,8 @@ -package report +package runner import ( "time" - "github.com/benchttp/engine/runner/internal/config" "github.com/benchttp/engine/runner/internal/metrics" "github.com/benchttp/engine/runner/internal/tests" ) @@ -17,14 +16,14 @@ type Report struct { // Metadata contains contextual information about a run. type Metadata struct { - Config config.Global + Config Config FinishedAt time.Time TotalDuration time.Duration } -// New returns an initialized *Report. -func New( - cfg config.Global, +// newReport returns an initialized *Report. +func newReport( + cfg Config, d time.Duration, m metrics.Aggregate, t tests.SuiteResult, diff --git a/runner/runner.go b/runner/runner.go index 10467bc..74db536 100644 --- a/runner/runner.go +++ b/runner/runner.go @@ -4,23 +4,15 @@ import ( "context" "time" - "github.com/benchttp/engine/runner/internal/config" "github.com/benchttp/engine/runner/internal/metrics" "github.com/benchttp/engine/runner/internal/recorder" - "github.com/benchttp/engine/runner/internal/report" "github.com/benchttp/engine/runner/internal/tests" ) type ( - Config = config.Global - RecorderConfig = config.Runner - InvalidConfigError = config.InvalidConfigError - RecordingProgress = recorder.Progress RecordingStatus = recorder.Status - Report = report.Report - MetricsAggregate = metrics.Aggregate MetricsField = metrics.Field MetricsValue = metrics.Value @@ -30,8 +22,6 @@ type ( TestPredicate = tests.Predicate TestSuiteResults = tests.SuiteResult TestCaseResult = tests.CaseResult - - ReportMetadata = report.Metadata ) const ( @@ -41,11 +31,7 @@ const ( StatusDone = recorder.StatusDone ) -var ( - DefaultConfig = config.Default - - ErrCanceled = recorder.ErrCanceled -) +var ErrCanceled = recorder.ErrCanceled type Runner struct { recorder *recorder.Recorder @@ -56,7 +42,7 @@ func New(onRecordingProgress func(RecordingProgress)) *Runner { return &Runner{onRecordingProgress: onRecordingProgress} } -func (r *Runner) Run(ctx context.Context, cfg config.Global) (*Report, error) { +func (r *Runner) Run(ctx context.Context, cfg Config) (*Report, error) { // Validate input config if err := cfg.Validate(); err != nil { return nil, err @@ -79,7 +65,7 @@ func (r *Runner) Run(ctx context.Context, cfg config.Global) (*Report, error) { testResults := tests.Run(agg, cfg.Tests) - return report.New(cfg, duration, agg, testResults), nil + return newReport(cfg, duration, agg, testResults), nil } // Progress returns the current progress of the recording. @@ -94,7 +80,7 @@ func (r *Runner) Progress() RecordingProgress { // recorderConfig returns a runner.RequesterConfig generated from cfg. func recorderConfig( - cfg config.Global, + cfg Config, onRecordingProgress func(recorder.Progress), ) recorder.Config { return recorder.Config{ From f7bd59978654b16fb9276f3b7be1209019355175 Mon Sep 17 00:00:00 2001 From: Gregory Albouy Date: Wed, 19 Oct 2022 02:53:10 +0200 Subject: [PATCH 08/53] refactor(configparse): minor param renaming --- configparse/parse.go | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/configparse/parse.go b/configparse/parse.go index 0ee8fbd..101c1b1 100644 --- a/configparse/parse.go +++ b/configparse/parse.go @@ -62,10 +62,12 @@ func (repr Representation) ParseInto(dst *runner.Config) error { } func (repr Representation) parseRequestInto(dst *runner.Config) error { - req := &http.Request{} + if dst.Request == nil { + dst.Request = &http.Request{} + } if method := repr.Request.Method; method != nil { - req.Method = *method + dst.Request.Method = *method } if rawURL := repr.Request.URL; rawURL != nil { @@ -73,7 +75,7 @@ func (repr Representation) parseRequestInto(dst *runner.Config) error { if err != nil { return fmt.Errorf(`configparse: invalid url: %q`, *rawURL) } - req.URL = parsedURL + dst.Request.URL = parsedURL } if header := repr.Request.Header; header != nil { @@ -81,19 +83,18 @@ func (repr Representation) parseRequestInto(dst *runner.Config) error { for key, val := range header { httpHeader[key] = val } - req.Header = httpHeader + dst.Request.Header = httpHeader } if body := repr.Request.Body; body != nil { switch body.Type { case "raw": - req.Body = io.NopCloser(bytes.NewReader([]byte(body.Content))) + dst.Request.Body = io.NopCloser(bytes.NewReader([]byte(body.Content))) default: return errors.New(`configparse: request.body.type: only "raw" accepted`) } } - *dst.Request = *req return nil } From 74447a6b789b4ec2f74b3dd70c6c93a09fc4ab39 Mon Sep 17 00:00:00 2001 From: Gregory Albouy Date: Wed, 19 Oct 2022 03:19:50 +0200 Subject: [PATCH 09/53] refactor(runner): nuke Config, flatten Runner fields --- configparse/json.go | 10 +-- configparse/json_test.go | 22 +++---- configparse/parse.go | 20 +++--- runner/config.go | 77 ----------------------- runner/default.go | 23 ++++--- runner/error.go | 6 +- runner/error_test.go | 4 +- runner/report.go | 4 +- runner/runner.go | 76 ++++++++++++++++++---- runner/{config_test.go => runner_test.go} | 41 ++++++------ 10 files changed, 127 insertions(+), 156 deletions(-) delete mode 100644 runner/config.go rename runner/{config_test.go => runner_test.go} (70%) diff --git a/configparse/json.go b/configparse/json.go index 7297947..897c44f 100644 --- a/configparse/json.go +++ b/configparse/json.go @@ -4,17 +4,17 @@ import ( "github.com/benchttp/engine/runner" ) -// JSON reads input bytes as JSON and unmarshals it into a runner.Config. -func JSON(in []byte) (runner.Config, error) { +// JSON reads input bytes as JSON and unmarshals it into a runner.Runner. +func JSON(in []byte) (runner.Runner, error) { parser := JSONParser{} repr := Representation{} if err := parser.Parse(in, &repr); err != nil { - return runner.Config{}, err + return runner.Runner{}, err } - cfg := runner.DefaultConfig() + cfg := runner.DefaultRunner() if err := repr.ParseInto(&cfg); err != nil { - return runner.Config{}, err + return runner.Runner{}, err } return cfg, nil diff --git a/configparse/json_test.go b/configparse/json_test.go index adc2105..4d76c10 100644 --- a/configparse/json_test.go +++ b/configparse/json_test.go @@ -19,7 +19,7 @@ func TestJSON(t *testing.T) { testcases := []struct { name string input []byte - isValidConfig func(runner.Config) bool + isValidRunner func(runner.Runner) bool expError error }{ { @@ -27,7 +27,7 @@ func TestJSON(t *testing.T) { input: baseInput.assign(object{ "badkey": "marcel-patulacci", }).json(), - isValidConfig: func(cfg runner.Config) bool { return true }, + isValidRunner: func(cfg runner.Runner) bool { return true }, expError: errors.New(`invalid field ("badkey"): does not exist`), }, { @@ -37,7 +37,7 @@ func TestJSON(t *testing.T) { "concurrency": "bad value", // want int }, }).json(), - isValidConfig: func(runner.Config) bool { return true }, + isValidRunner: func(runner.Runner) bool { return true }, expError: errors.New(`wrong type for field runner.concurrency: want int, got string`), }, { @@ -45,12 +45,12 @@ func TestJSON(t *testing.T) { input: baseInput.assign(object{ "runner": object{"concurrency": 3}, }).json(), - isValidConfig: func(cfg runner.Config) bool { - defaultConfig := runner.DefaultConfig() + isValidRunner: func(r runner.Runner) bool { + defaultRunner := runner.DefaultRunner() - isInputValueParsed := cfg.Runner.Concurrency == 3 - isMergedWithDefault := cfg.Request.Method == defaultConfig.Request.Method && - cfg.Runner.GlobalTimeout == defaultConfig.Runner.GlobalTimeout + isInputValueParsed := r.Concurrency == 3 + isMergedWithDefault := r.Request.Method == defaultRunner.Request.Method && + r.GlobalTimeout == defaultRunner.GlobalTimeout return isInputValueParsed && isMergedWithDefault }, @@ -60,9 +60,9 @@ func TestJSON(t *testing.T) { for _, tc := range testcases { t.Run(tc.name, func(t *testing.T) { - gotConfig, gotError := configparse.JSON(tc.input) - if !tc.isValidConfig(gotConfig) { - t.Errorf("unexpected config:\n%+v", gotConfig) + gotRunner, gotError := configparse.JSON(tc.input) + if !tc.isValidRunner(gotRunner) { + t.Errorf("unexpected config:\n%+v", gotRunner) } if !sameErrors(gotError, tc.expError) { diff --git a/configparse/parse.go b/configparse/parse.go index 101c1b1..a33e8e8 100644 --- a/configparse/parse.go +++ b/configparse/parse.go @@ -48,10 +48,10 @@ type Representation struct { } `yaml:"tests" json:"tests"` } -// ParseInto parses the Representation receiver as a runner.Config +// ParseInto parses the Representation receiver as a runner.Runner // and stores any non-nil field value into the corresponding field // of dst. -func (repr Representation) ParseInto(dst *runner.Config) error { +func (repr Representation) ParseInto(dst *runner.Runner) error { if err := repr.parseRequestInto(dst); err != nil { return err } @@ -61,7 +61,7 @@ func (repr Representation) ParseInto(dst *runner.Config) error { return repr.parseTestsInto(dst) } -func (repr Representation) parseRequestInto(dst *runner.Config) error { +func (repr Representation) parseRequestInto(dst *runner.Runner) error { if dst.Request == nil { dst.Request = &http.Request{} } @@ -98,13 +98,13 @@ func (repr Representation) parseRequestInto(dst *runner.Config) error { return nil } -func (repr Representation) parseRunnerInto(dst *runner.Config) error { +func (repr Representation) parseRunnerInto(dst *runner.Runner) error { if requests := repr.Runner.Requests; requests != nil { - dst.Runner.Requests = *requests + dst.Requests = *requests } if concurrency := repr.Runner.Concurrency; concurrency != nil { - dst.Runner.Concurrency = *concurrency + dst.Concurrency = *concurrency } if interval := repr.Runner.Interval; interval != nil { @@ -112,7 +112,7 @@ func (repr Representation) parseRunnerInto(dst *runner.Config) error { if err != nil { return err } - dst.Runner.Interval = parsedInterval + dst.Interval = parsedInterval } if requestTimeout := repr.Runner.RequestTimeout; requestTimeout != nil { @@ -120,7 +120,7 @@ func (repr Representation) parseRunnerInto(dst *runner.Config) error { if err != nil { return err } - dst.Runner.RequestTimeout = parsedTimeout + dst.RequestTimeout = parsedTimeout } if globalTimeout := repr.Runner.GlobalTimeout; globalTimeout != nil { @@ -128,13 +128,13 @@ func (repr Representation) parseRunnerInto(dst *runner.Config) error { if err != nil { return err } - dst.Runner.GlobalTimeout = parsedGlobalTimeout + dst.GlobalTimeout = parsedGlobalTimeout } return nil } -func (repr Representation) parseTestsInto(dst *runner.Config) error { +func (repr Representation) parseTestsInto(dst *runner.Runner) error { testSuite := repr.Tests if len(testSuite) == 0 { return nil diff --git a/runner/config.go b/runner/config.go deleted file mode 100644 index 30158bb..0000000 --- a/runner/config.go +++ /dev/null @@ -1,77 +0,0 @@ -package runner - -import ( - "encoding/json" - "errors" - "fmt" - "net/http" - "time" - - "github.com/benchttp/engine/runner/internal/tests" -) - -// RunnerConfig contains options relative to the runner. -type RunnerConfig struct { - Requests int - Concurrency int - Interval time.Duration - RequestTimeout time.Duration - GlobalTimeout time.Duration -} - -// Config represents the global configuration of the runner. -// It must be validated using Config.Validate before usage. -type Config struct { - Request *http.Request - Runner RunnerConfig - Tests []tests.Case -} - -// String implements fmt.Stringer. It returns an indented JSON representation -// of Config for debugging purposes. -func (cfg Config) String() string { - b, _ := json.MarshalIndent(cfg, "", " ") - return string(b) -} - -// Validate returns a non-nil InvalidConfigError if any of its fields -// does not meet the requirements. -func (cfg Config) Validate() error { //nolint:gocognit - errs := []error{} - appendError := func(err error) { - errs = append(errs, err) - } - - if cfg.Request == nil { - appendError(errors.New("unexpected nil request")) - } - - if cfg.Runner.Requests < 1 && cfg.Runner.Requests != -1 { - appendError(fmt.Errorf("requests (%d): want >= 0", cfg.Runner.Requests)) - } - - if cfg.Runner.Concurrency < 1 || cfg.Runner.Concurrency > cfg.Runner.Requests { - appendError(fmt.Errorf( - "concurrency (%d): want > 0 and <= requests (%d)", - cfg.Runner.Concurrency, cfg.Runner.Requests, - )) - } - - if cfg.Runner.Interval < 0 { - appendError(fmt.Errorf("interval (%d): want >= 0", cfg.Runner.Interval)) - } - - if cfg.Runner.RequestTimeout < 1 { - appendError(fmt.Errorf("requestTimeout (%d): want > 0", cfg.Runner.RequestTimeout)) - } - - if cfg.Runner.GlobalTimeout < 1 { - appendError(fmt.Errorf("globalTimeout (%d): want > 0", cfg.Runner.GlobalTimeout)) - } - - if len(errs) > 0 { - return &InvalidConfigError{errs} - } - - return nil -} diff --git a/runner/default.go b/runner/default.go index 84b896d..32ca603 100644 --- a/runner/default.go +++ b/runner/default.go @@ -6,20 +6,19 @@ import ( "time" ) -var defaultConfig = Config{ - Request: defaultRequest(), - Runner: RunnerConfig{ - Concurrency: 10, - Requests: 100, - Interval: 0 * time.Second, - RequestTimeout: 5 * time.Second, - GlobalTimeout: 30 * time.Second, - }, +// DefaultRunner returns a default Runner that is safe to use. +func DefaultRunner() Runner { + return defaultRunner } -// DefaultConfig returns a default config that is safe to use. -func DefaultConfig() Config { - return defaultConfig +var defaultRunner = Runner{ + Request: defaultRequest(), + + Concurrency: 10, + Requests: 100, + Interval: 0 * time.Second, + RequestTimeout: 5 * time.Second, + GlobalTimeout: 30 * time.Second, } func defaultRequest() *http.Request { diff --git a/runner/error.go b/runner/error.go index 145f72c..98f82ba 100644 --- a/runner/error.go +++ b/runner/error.go @@ -2,14 +2,14 @@ package runner import "strings" -// InvalidConfigError is the errors returned by Config.Validate +// InvalidRunnerError is the errors returned by Config.Validate // when values are missing or invalid. -type InvalidConfigError struct { +type InvalidRunnerError struct { Errors []error } // Error returns the joined errors of InvalidConfigError as a string. -func (e *InvalidConfigError) Error() string { +func (e *InvalidRunnerError) Error() string { const sep = "\n - " var b strings.Builder diff --git a/runner/error_test.go b/runner/error_test.go index f8c3af2..8b1aad7 100644 --- a/runner/error_test.go +++ b/runner/error_test.go @@ -7,8 +7,8 @@ import ( "github.com/benchttp/engine/runner" ) -func TestInvalidConfigError_Error(t *testing.T) { - e := runner.InvalidConfigError{ +func TestInvalidRunnerError(t *testing.T) { + e := runner.InvalidRunnerError{ Errors: []error{ errors.New("error 0"), errors.New("error 1\nwith new line"), diff --git a/runner/report.go b/runner/report.go index cd75bf9..a0d959d 100644 --- a/runner/report.go +++ b/runner/report.go @@ -16,14 +16,14 @@ type Report struct { // Metadata contains contextual information about a run. type Metadata struct { - Config Config + Config Runner FinishedAt time.Time TotalDuration time.Duration } // newReport returns an initialized *Report. func newReport( - cfg Config, + cfg Runner, d time.Duration, m metrics.Aggregate, t tests.SuiteResult, diff --git a/runner/runner.go b/runner/runner.go index 74db536..e421911 100644 --- a/runner/runner.go +++ b/runner/runner.go @@ -2,6 +2,9 @@ package runner import ( "context" + "errors" + "fmt" + "net/http" "time" "github.com/benchttp/engine/runner/internal/metrics" @@ -34,6 +37,16 @@ const ( var ErrCanceled = recorder.ErrCanceled type Runner struct { + Request *http.Request + + Requests int + Concurrency int + Interval time.Duration + RequestTimeout time.Duration + GlobalTimeout time.Duration + + Tests []tests.Case + recorder *recorder.Recorder onRecordingProgress func(RecordingProgress) } @@ -42,14 +55,14 @@ func New(onRecordingProgress func(RecordingProgress)) *Runner { return &Runner{onRecordingProgress: onRecordingProgress} } -func (r *Runner) Run(ctx context.Context, cfg Config) (*Report, error) { +func (r *Runner) Run(ctx context.Context, cfg Runner) (*Report, error) { // Validate input config if err := cfg.Validate(); err != nil { return nil, err } // Create and attach request recorder - r.recorder = recorder.New(recorderConfig(cfg, r.onRecordingProgress)) + r.recorder = recorder.New(r.recorderConfig()) startTime := time.Now() @@ -79,16 +92,55 @@ func (r *Runner) Progress() RecordingProgress { } // recorderConfig returns a runner.RequesterConfig generated from cfg. -func recorderConfig( - cfg Config, - onRecordingProgress func(recorder.Progress), -) recorder.Config { +func (r *Runner) recorderConfig() recorder.Config { return recorder.Config{ - Requests: cfg.Runner.Requests, - Concurrency: cfg.Runner.Concurrency, - Interval: cfg.Runner.Interval, - RequestTimeout: cfg.Runner.RequestTimeout, - GlobalTimeout: cfg.Runner.GlobalTimeout, - OnProgress: onRecordingProgress, + Requests: r.Requests, + Concurrency: r.Concurrency, + Interval: r.Interval, + RequestTimeout: r.RequestTimeout, + GlobalTimeout: r.GlobalTimeout, + OnProgress: r.onRecordingProgress, + } +} + +// Validate returns a non-nil InvalidConfigError if any of its fields +// does not meet the requirements. +func (r Runner) Validate() error { //nolint:gocognit + errs := []error{} + appendError := func(err error) { + errs = append(errs, err) } + + if r.Request == nil { + appendError(errors.New("unexpected nil request")) + } + + if r.Requests < 1 && r.Requests != -1 { + appendError(fmt.Errorf("requests (%d): want >= 0", r.Requests)) + } + + if r.Concurrency < 1 || r.Concurrency > r.Requests { + appendError(fmt.Errorf( + "concurrency (%d): want > 0 and <= requests (%d)", + r.Concurrency, r.Requests, + )) + } + + if r.Interval < 0 { + appendError(fmt.Errorf("interval (%d): want >= 0", r.Interval)) + } + + if r.RequestTimeout < 1 { + appendError(fmt.Errorf("requestTimeout (%d): want > 0", r.RequestTimeout)) + } + + if r.GlobalTimeout < 1 { + appendError(fmt.Errorf("globalTimeout (%d): want > 0", r.GlobalTimeout)) + } + + if len(errs) > 0 { + return &InvalidRunnerError{errs} + } + + return nil } diff --git a/runner/config_test.go b/runner/runner_test.go similarity index 70% rename from runner/config_test.go rename to runner/runner_test.go index c3a2734..ebf1a2a 100644 --- a/runner/config_test.go +++ b/runner/runner_test.go @@ -8,41 +8,38 @@ import ( "github.com/benchttp/engine/runner" ) -func TestConfig_Validate(t *testing.T) { +func TestRunner_Validate(t *testing.T) { t.Run("return nil if config is valid", func(t *testing.T) { - cfg := runner.Config{ - Request: validRequest(), - Runner: runner.RunnerConfig{ - Requests: 5, - Concurrency: 5, - Interval: 5, - RequestTimeout: 5, - GlobalTimeout: 5, - }, + brunner := runner.Runner{ + Request: validRequest(), + Requests: 5, + Concurrency: 5, + Interval: 5, + RequestTimeout: 5, + GlobalTimeout: 5, } - if err := cfg.Validate(); err != nil { + + if err := brunner.Validate(); err != nil { t.Errorf("unexpected error: %v", err) } }) t.Run("return cumulated errors if config is invalid", func(t *testing.T) { - cfg := runner.Config{ - Request: nil, - Runner: runner.RunnerConfig{ - Requests: -5, - Concurrency: -5, - Interval: -5, - RequestTimeout: -5, - GlobalTimeout: -5, - }, + brunner := runner.Runner{ + Request: nil, + Requests: -5, + Concurrency: -5, + Interval: -5, + RequestTimeout: -5, + GlobalTimeout: -5, } - err := cfg.Validate() + err := brunner.Validate() if err == nil { t.Fatal("invalid configuration considered valid") } - var errInvalid *runner.InvalidConfigError + var errInvalid *runner.InvalidRunnerError if !errors.As(err, &errInvalid) { t.Fatalf("unexpected error type: %T", err) } From dec31d91348a39521730ab0212347646fae73913 Mon Sep 17 00:00:00 2001 From: Gregory Albouy Date: Sat, 22 Oct 2022 23:18:36 +0200 Subject: [PATCH 10/53] refactor(runner): use httptest in unit tests --- runner/runner_test.go | 28 ++++++++++------------------ 1 file changed, 10 insertions(+), 18 deletions(-) diff --git a/runner/runner_test.go b/runner/runner_test.go index ebf1a2a..e6fe4d5 100644 --- a/runner/runner_test.go +++ b/runner/runner_test.go @@ -2,7 +2,7 @@ package runner_test import ( "errors" - "net/http" + "net/http/httptest" "testing" "github.com/benchttp/engine/runner" @@ -11,7 +11,7 @@ import ( func TestRunner_Validate(t *testing.T) { t.Run("return nil if config is valid", func(t *testing.T) { brunner := runner.Runner{ - Request: validRequest(), + Request: httptest.NewRequest("GET", "https://a.b/#c?d=e&f=g", nil), Requests: 5, Concurrency: 5, Interval: 5, @@ -45,12 +45,12 @@ func TestRunner_Validate(t *testing.T) { } errs := errInvalid.Errors - findErrorOrFail(t, errs, `unexpected nil request`) - findErrorOrFail(t, errs, `requests (-5): want >= 0`) - findErrorOrFail(t, errs, `concurrency (-5): want > 0 and <= requests (-5)`) - findErrorOrFail(t, errs, `interval (-5): want >= 0`) - findErrorOrFail(t, errs, `requestTimeout (-5): want > 0`) - findErrorOrFail(t, errs, `globalTimeout (-5): want > 0`) + assertError(t, errs, `unexpected nil request`) + assertError(t, errs, `requests (-5): want >= 0`) + assertError(t, errs, `concurrency (-5): want > 0 and <= requests (-5)`) + assertError(t, errs, `interval (-5): want >= 0`) + assertError(t, errs, `requestTimeout (-5): want > 0`) + assertError(t, errs, `globalTimeout (-5): want > 0`) t.Logf("got error:\n%v", errInvalid) }) @@ -58,16 +58,8 @@ func TestRunner_Validate(t *testing.T) { // helpers -func validRequest() *http.Request { - req, err := http.NewRequest("GET", "https://a.b#c?d=e&f=g", nil) - if err != nil { - panic(err) - } - return req -} - -// findErrorOrFail fails t if no error in src matches msg. -func findErrorOrFail(t *testing.T, src []error, msg string) { +// assertError fails t if no error in src matches msg. +func assertError(t *testing.T, src []error, msg string) { t.Helper() for _, err := range src { if err.Error() == msg { From 1ebb2f47b30b7868a1ff8f212ee462a153ff0b53 Mon Sep 17 00:00:00 2001 From: Gregory Albouy Date: Sat, 22 Oct 2022 23:22:51 +0200 Subject: [PATCH 11/53] chore: remove obsolete env file --- .env.development | 1 - 1 file changed, 1 deletion(-) delete mode 100644 .env.development diff --git a/.env.development b/.env.development deleted file mode 100644 index 98da8de..0000000 --- a/.env.development +++ /dev/null @@ -1 +0,0 @@ -SERVER_PORT=8080 From 5c6172f3f66100ac8a3bc1c910786d836a094770 Mon Sep 17 00:00:00 2001 From: Gregory Albouy Date: Sat, 22 Oct 2022 23:23:29 +0200 Subject: [PATCH 12/53] chore: clean up .gitignore --- .gitignore | 9 --------- 1 file changed, 9 deletions(-) diff --git a/.gitignore b/.gitignore index 575893a..a960199 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,3 @@ -# Env files -.env - -# Binary files -/bin - # IDE files /.vscode /.idea @@ -12,6 +6,3 @@ /.benchttp.yml /.benchttp.yaml /.benchttp.json - -# Benchttp reports -/benchttp.report.*.json From bc00fb0729e5a7366ab9c1b467062bbe5cd81c71 Mon Sep 17 00:00:00 2001 From: Gregory Albouy Date: Sat, 22 Oct 2022 23:31:37 +0200 Subject: [PATCH 13/53] chore: remove deprecated linters --- .golangci.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index 786a2a0..04d396e 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -131,7 +131,6 @@ linters: disable-all: true enable: - bodyclose # enforce resp.Body.Close() - - deadcode - dupl # duplicate code - errcheck - exportloopref @@ -145,10 +144,8 @@ linters: - prealloc # enforce capacity allocation when possible - revive # golint enhancement - staticcheck # go vet enhancement - - structcheck # unused struct fields - testpackage # checks on tests (*_test) - thelper # enforce t.Helper() - - varcheck # unused global var and const - wastedassign fast: false From e8a9255beb49daeaa0168ad1605bedc109295644 Mon Sep 17 00:00:00 2001 From: Gregory Albouy Date: Sun, 23 Oct 2022 16:56:41 +0200 Subject: [PATCH 14/53] refactor(runner): remove unnecessary constructor --- runner/report.go | 4 ++-- runner/runner.go | 19 ++++++++----------- 2 files changed, 10 insertions(+), 13 deletions(-) diff --git a/runner/report.go b/runner/report.go index a0d959d..f1cde3e 100644 --- a/runner/report.go +++ b/runner/report.go @@ -23,7 +23,7 @@ type Metadata struct { // newReport returns an initialized *Report. func newReport( - cfg Runner, + r Runner, d time.Duration, m metrics.Aggregate, t tests.SuiteResult, @@ -32,7 +32,7 @@ func newReport( Metrics: m, Tests: t, Metadata: Metadata{ - Config: cfg, + Config: r, FinishedAt: time.Now(), // TODO: change, unreliable TotalDuration: d, }, diff --git a/runner/runner.go b/runner/runner.go index e421911..ff052b9 100644 --- a/runner/runner.go +++ b/runner/runner.go @@ -47,17 +47,14 @@ type Runner struct { Tests []tests.Case - recorder *recorder.Recorder - onRecordingProgress func(RecordingProgress) -} + OnProgress func(RecordingProgress) -func New(onRecordingProgress func(RecordingProgress)) *Runner { - return &Runner{onRecordingProgress: onRecordingProgress} + recorder *recorder.Recorder } -func (r *Runner) Run(ctx context.Context, cfg Runner) (*Report, error) { +func (r *Runner) Run(ctx context.Context) (*Report, error) { // Validate input config - if err := cfg.Validate(); err != nil { + if err := r.Validate(); err != nil { return nil, err } @@ -67,7 +64,7 @@ func (r *Runner) Run(ctx context.Context, cfg Runner) (*Report, error) { startTime := time.Now() // Run request recorder - records, err := r.recorder.Record(ctx, cfg.Request) + records, err := r.recorder.Record(ctx, r.Request) if err != nil { return nil, err } @@ -76,9 +73,9 @@ func (r *Runner) Run(ctx context.Context, cfg Runner) (*Report, error) { agg := metrics.NewAggregate(records) - testResults := tests.Run(agg, cfg.Tests) + testResults := tests.Run(agg, r.Tests) - return newReport(cfg, duration, agg, testResults), nil + return newReport(*r, duration, agg, testResults), nil } // Progress returns the current progress of the recording. @@ -99,7 +96,7 @@ func (r *Runner) recorderConfig() recorder.Config { Interval: r.Interval, RequestTimeout: r.RequestTimeout, GlobalTimeout: r.GlobalTimeout, - OnProgress: r.onRecordingProgress, + OnProgress: r.OnProgress, } } From 78e6d8dce9b77e2b6060b0bef7c44dadeec73ac0 Mon Sep 17 00:00:00 2001 From: Gregory Albouy Date: Sun, 23 Oct 2022 16:56:55 +0200 Subject: [PATCH 15/53] docs: update docs --- README.md | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 07a2a00..db4215b 100644 --- a/README.md +++ b/README.md @@ -46,12 +46,15 @@ import ( ) func main(t *testing.T) { - // Set runner configuration - config := runner.DefaultConfig() - config.Request = config.Request.WithURL("https://example.com") + // Set the request to send + req, _ := http.NewRequest("GET", "http://localhost:3000", nil) - // Instantiate runner and run benchmark - report, _ := runner.New(nil).Run(context.Background(), config) + // Configure the runner + rnr := runner.DefaultRunner() + rnr.Request = req + + // Run benchmark, get report + report, _ := rnr.Run(context.Background()) fmt.Println(report.Metrics.ResponseTimes.Mean) } @@ -67,7 +70,6 @@ import ( "fmt" "github.com/benchttp/engine/configparse" - "github.com/benchttp/engine/runner" ) func main() { @@ -75,12 +77,12 @@ func main() { jsonConfig := []byte(` { "request": { - "url": "https://example.com" + "url": "http://localhost:9999" } }`) - config, _ := configparse.JSON(jsonConfig) - report, _ := runner.New(nil).Run(context.Background(), config) + runner, _ := configparse.JSON(jsonConfig) + report, _ := runner.Run(context.Background()) fmt.Println(report.Metrics.ResponseTimes.Mean) } From 540d07161e1619d6ac8360754be4168f2adf4725 Mon Sep 17 00:00:00 2001 From: Gregory Albouy Date: Sun, 23 Oct 2022 18:21:14 +0200 Subject: [PATCH 16/53] refactor(configparse): JSON: use pointer destination param --- configparse/json.go | 15 ++++----------- configparse/json_test.go | 5 +++-- 2 files changed, 7 insertions(+), 13 deletions(-) diff --git a/configparse/json.go b/configparse/json.go index 897c44f..b48a5ac 100644 --- a/configparse/json.go +++ b/configparse/json.go @@ -5,17 +5,10 @@ import ( ) // JSON reads input bytes as JSON and unmarshals it into a runner.Runner. -func JSON(in []byte) (runner.Runner, error) { - parser := JSONParser{} +func JSON(in []byte, dst *runner.Runner) error { repr := Representation{} - if err := parser.Parse(in, &repr); err != nil { - return runner.Runner{}, err + if err := (JSONParser{}).Parse(in, &repr); err != nil { + return err } - - cfg := runner.DefaultRunner() - if err := repr.ParseInto(&cfg); err != nil { - return runner.Runner{}, err - } - - return cfg, nil + return repr.ParseInto(dst) } diff --git a/configparse/json_test.go b/configparse/json_test.go index 4d76c10..6e14a14 100644 --- a/configparse/json_test.go +++ b/configparse/json_test.go @@ -60,11 +60,12 @@ func TestJSON(t *testing.T) { for _, tc := range testcases { t.Run(tc.name, func(t *testing.T) { - gotRunner, gotError := configparse.JSON(tc.input) + gotRunner := runner.DefaultRunner() + gotError := configparse.JSON(tc.input, &gotRunner) + if !tc.isValidRunner(gotRunner) { t.Errorf("unexpected config:\n%+v", gotRunner) } - if !sameErrors(gotError, tc.expError) { t.Errorf("unexpected error:\nexp %v,\ngot %v", tc.expError, gotError) } From 680fc0b9f8407b65abd35fc60c15c5326120d139 Mon Sep 17 00:00:00 2001 From: Gregory Albouy Date: Sun, 23 Oct 2022 18:57:07 +0200 Subject: [PATCH 17/53] refactor: rename module and packages - module github.com/benchttp/engine -> github.com/benchttp/sdk - package runner -> benchttp --- .golangci.yml | 2 +- Makefile | 2 +- README.md | 26 +++++++++---------- {runner => benchttp}/default.go | 4 +-- {runner => benchttp}/error.go | 2 +- {runner => benchttp}/error_test.go | 6 ++--- .../internal/metrics/aggregate.go | 4 +-- .../internal/metrics/aggregate_test.go | 6 ++--- .../internal/metrics/compare.go | 0 .../internal/metrics/field.go | 2 +- .../internal/metrics/field_test.go | 2 +- .../internal/metrics/metrics.go | 2 +- .../internal/metrics/metrics_test.go | 4 +-- .../internal/metrics/timestats/sort.go | 0 .../internal/metrics/timestats/timestats.go | 0 .../metrics/timestats/timestats_test.go | 2 +- .../internal/recorder/error.go | 0 .../internal/recorder/httputil.go | 0 .../internal/recorder/progress.go | 0 .../internal/recorder/recorder.go | 2 +- .../recorder/recorder_internal_test.go | 2 +- .../internal/recorder/tracer.go | 0 .../internal/recorder/tracer_internal_test.go | 0 .../internal/reflectpath/resolver.go | 0 .../internal/reflectpath/type.go | 0 .../internal/reflectpath/value.go | 0 .../internal/tests/predicate.go | 4 +-- .../internal/tests/predicate_test.go | 4 +-- {runner => benchttp}/internal/tests/tests.go | 2 +- .../internal/tests/tests_test.go | 6 ++--- {runner => benchttp}/report.go | 6 ++--- {runner => benchttp}/runner.go | 8 +++--- {runner => benchttp}/runner_test.go | 14 +++++----- configparse/json.go | 8 +++--- configparse/json_test.go | 16 ++++++------ configparse/parse.go | 26 +++++++++---------- configparse/parser_json_test.go | 2 +- configparse/parser_yaml_test.go | 2 +- go.mod | 2 +- internal/dispatcher/dispatcher_test.go | 2 +- 40 files changed, 84 insertions(+), 86 deletions(-) rename {runner => benchttp}/default.go (88%) rename {runner => benchttp}/error.go (96%) rename {runner => benchttp}/error_test.go (80%) rename {runner => benchttp}/internal/metrics/aggregate.go (96%) rename {runner => benchttp}/internal/metrics/aggregate_test.go (94%) rename {runner => benchttp}/internal/metrics/compare.go (100%) rename {runner => benchttp}/internal/metrics/field.go (94%) rename {runner => benchttp}/internal/metrics/field_test.go (96%) rename {runner => benchttp}/internal/metrics/metrics.go (97%) rename {runner => benchttp}/internal/metrics/metrics_test.go (96%) rename {runner => benchttp}/internal/metrics/timestats/sort.go (100%) rename {runner => benchttp}/internal/metrics/timestats/timestats.go (100%) rename {runner => benchttp}/internal/metrics/timestats/timestats_test.go (97%) rename {runner => benchttp}/internal/recorder/error.go (100%) rename {runner => benchttp}/internal/recorder/httputil.go (100%) rename {runner => benchttp}/internal/recorder/progress.go (100%) rename {runner => benchttp}/internal/recorder/recorder.go (98%) rename {runner => benchttp}/internal/recorder/recorder_internal_test.go (99%) rename {runner => benchttp}/internal/recorder/tracer.go (100%) rename {runner => benchttp}/internal/recorder/tracer_internal_test.go (100%) rename {runner => benchttp}/internal/reflectpath/resolver.go (100%) rename {runner => benchttp}/internal/reflectpath/type.go (100%) rename {runner => benchttp}/internal/reflectpath/value.go (100%) rename {runner => benchttp}/internal/tests/predicate.go (91%) rename {runner => benchttp}/internal/tests/predicate_test.go (94%) rename {runner => benchttp}/internal/tests/tests.go (95%) rename {runner => benchttp}/internal/tests/tests_test.go (95%) rename {runner => benchttp}/report.go (84%) rename {runner => benchttp}/runner.go (94%) rename {runner => benchttp}/runner_test.go (86%) diff --git a/.golangci.yml b/.golangci.yml index 04d396e..d9e410a 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -111,7 +111,7 @@ linters-settings: extra-rules: true goimports: - local-prefixes: github.com/benchttp/engine + local-prefixes: github.com/benchttp/sdk misspell: locale: US diff --git a/Makefile b/Makefile index 5f4e1e0..0ed0f52 100644 --- a/Makefile +++ b/Makefile @@ -41,5 +41,5 @@ test-cov: .PHONY: docs docs: - @echo "\033[4mhttp://localhost:9995/pkg/github.com/benchttp/engine/\033[0m" + @echo "\033[4mhttp://localhost:9995/pkg/github.com/benchttp/sdk/\033[0m" @godoc -http=localhost:9995 diff --git a/README.md b/README.md index db4215b..0d19487 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,16 @@

benchttp/engine

- + Github Worklow Status Code coverage - - Go Report Card + + Go Report Card
- + Go package Reference - + Latest version

@@ -28,7 +28,7 @@ Go1.17 environment or higher is required. Install. ```txt -go get github.com/benchttp/engine +go get github.com/benchttp/sdk ``` ## Usage @@ -42,19 +42,19 @@ import ( "context" "fmt" - "github.com/benchttp/engine/runner" + "github.com/benchttp/sdk/benchttp" ) func main(t *testing.T) { // Set the request to send - req, _ := http.NewRequest("GET", "http://localhost:3000", nil) + request, _ := http.NewRequest("GET", "http://localhost:3000", nil) // Configure the runner - rnr := runner.DefaultRunner() - rnr.Request = req + runner := runner.DefaultRunner() + runner.Request = request // Run benchmark, get report - report, _ := rnr.Run(context.Background()) + report, _ := runner.Run(context.Background()) fmt.Println(report.Metrics.ResponseTimes.Mean) } @@ -69,7 +69,7 @@ import ( "context" "fmt" - "github.com/benchttp/engine/configparse" + "github.com/benchttp/sdk/configparse" ) func main() { @@ -88,7 +88,7 @@ func main() { } ``` -📄 Please refer to [our Wiki](https://github.com/benchttp/engine/wiki/IO-Structures) for exhaustive `Config` and `Report` structures (and more!) +📄 Please refer to [our Wiki](https://github.com/benchttp/sdk/wiki/IO-Structures) for exhaustive `Runner` and `Report` structures (and more!) ## Development diff --git a/runner/default.go b/benchttp/default.go similarity index 88% rename from runner/default.go rename to benchttp/default.go index 32ca603..545cf15 100644 --- a/runner/default.go +++ b/benchttp/default.go @@ -1,4 +1,4 @@ -package runner +package benchttp import ( "fmt" @@ -24,7 +24,7 @@ var defaultRunner = Runner{ func defaultRequest() *http.Request { req, err := http.NewRequest("GET", "", nil) if err != nil { - panic(fmt.Sprintf("benchttp/runner: %s", err)) + panic(fmt.Sprintf("benchttp: %s", err)) } return req } diff --git a/runner/error.go b/benchttp/error.go similarity index 96% rename from runner/error.go rename to benchttp/error.go index 98f82ba..19ceeac 100644 --- a/runner/error.go +++ b/benchttp/error.go @@ -1,4 +1,4 @@ -package runner +package benchttp import "strings" diff --git a/runner/error_test.go b/benchttp/error_test.go similarity index 80% rename from runner/error_test.go rename to benchttp/error_test.go index 8b1aad7..cdbb1c9 100644 --- a/runner/error_test.go +++ b/benchttp/error_test.go @@ -1,14 +1,14 @@ -package runner_test +package benchttp_test import ( "errors" "testing" - "github.com/benchttp/engine/runner" + "github.com/benchttp/sdk/benchttp" ) func TestInvalidRunnerError(t *testing.T) { - e := runner.InvalidRunnerError{ + e := benchttp.InvalidRunnerError{ Errors: []error{ errors.New("error 0"), errors.New("error 1\nwith new line"), diff --git a/runner/internal/metrics/aggregate.go b/benchttp/internal/metrics/aggregate.go similarity index 96% rename from runner/internal/metrics/aggregate.go rename to benchttp/internal/metrics/aggregate.go index 7a40929..b44723e 100644 --- a/runner/internal/metrics/aggregate.go +++ b/benchttp/internal/metrics/aggregate.go @@ -3,8 +3,8 @@ package metrics import ( "time" - "github.com/benchttp/engine/runner/internal/metrics/timestats" - "github.com/benchttp/engine/runner/internal/recorder" + "github.com/benchttp/sdk/benchttp/internal/metrics/timestats" + "github.com/benchttp/sdk/benchttp/internal/recorder" ) type TimeStats = timestats.TimeStats diff --git a/runner/internal/metrics/aggregate_test.go b/benchttp/internal/metrics/aggregate_test.go similarity index 94% rename from runner/internal/metrics/aggregate_test.go rename to benchttp/internal/metrics/aggregate_test.go index 5883a20..a2af956 100644 --- a/runner/internal/metrics/aggregate_test.go +++ b/benchttp/internal/metrics/aggregate_test.go @@ -5,9 +5,9 @@ import ( "testing" "time" - "github.com/benchttp/engine/runner/internal/metrics" - "github.com/benchttp/engine/runner/internal/metrics/timestats" - "github.com/benchttp/engine/runner/internal/recorder" + "github.com/benchttp/sdk/benchttp/internal/metrics" + "github.com/benchttp/sdk/benchttp/internal/metrics/timestats" + "github.com/benchttp/sdk/benchttp/internal/recorder" ) func TestNewAggregate(t *testing.T) { diff --git a/runner/internal/metrics/compare.go b/benchttp/internal/metrics/compare.go similarity index 100% rename from runner/internal/metrics/compare.go rename to benchttp/internal/metrics/compare.go diff --git a/runner/internal/metrics/field.go b/benchttp/internal/metrics/field.go similarity index 94% rename from runner/internal/metrics/field.go rename to benchttp/internal/metrics/field.go index e1a8d73..2228ab2 100644 --- a/runner/internal/metrics/field.go +++ b/benchttp/internal/metrics/field.go @@ -3,7 +3,7 @@ package metrics import ( "errors" - "github.com/benchttp/engine/internal/errorutil" + "github.com/benchttp/sdk/internal/errorutil" ) // ErrUnknownField occurs when a Field is used with an invalid path. diff --git a/runner/internal/metrics/field_test.go b/benchttp/internal/metrics/field_test.go similarity index 96% rename from runner/internal/metrics/field_test.go rename to benchttp/internal/metrics/field_test.go index f3a32c0..41cea87 100644 --- a/runner/internal/metrics/field_test.go +++ b/benchttp/internal/metrics/field_test.go @@ -3,7 +3,7 @@ package metrics_test import ( "testing" - "github.com/benchttp/engine/runner/internal/metrics" + "github.com/benchttp/sdk/benchttp/internal/metrics" ) func TestField_Type(t *testing.T) { diff --git a/runner/internal/metrics/metrics.go b/benchttp/internal/metrics/metrics.go similarity index 97% rename from runner/internal/metrics/metrics.go rename to benchttp/internal/metrics/metrics.go index 61e946d..5eb51ea 100644 --- a/runner/internal/metrics/metrics.go +++ b/benchttp/internal/metrics/metrics.go @@ -3,7 +3,7 @@ package metrics import ( "strings" - "github.com/benchttp/engine/runner/internal/reflectpath" + "github.com/benchttp/sdk/benchttp/internal/reflectpath" ) // Value is a concrete metric value, e.g. 120 or 3 * time.Second. diff --git a/runner/internal/metrics/metrics_test.go b/benchttp/internal/metrics/metrics_test.go similarity index 96% rename from runner/internal/metrics/metrics_test.go rename to benchttp/internal/metrics/metrics_test.go index b9ff933..b1cab65 100644 --- a/runner/internal/metrics/metrics_test.go +++ b/benchttp/internal/metrics/metrics_test.go @@ -4,8 +4,8 @@ import ( "testing" "time" - "github.com/benchttp/engine/runner/internal/metrics" - "github.com/benchttp/engine/runner/internal/metrics/timestats" + "github.com/benchttp/sdk/benchttp/internal/metrics" + "github.com/benchttp/sdk/benchttp/internal/metrics/timestats" ) func TestMetric_Compare(t *testing.T) { diff --git a/runner/internal/metrics/timestats/sort.go b/benchttp/internal/metrics/timestats/sort.go similarity index 100% rename from runner/internal/metrics/timestats/sort.go rename to benchttp/internal/metrics/timestats/sort.go diff --git a/runner/internal/metrics/timestats/timestats.go b/benchttp/internal/metrics/timestats/timestats.go similarity index 100% rename from runner/internal/metrics/timestats/timestats.go rename to benchttp/internal/metrics/timestats/timestats.go diff --git a/runner/internal/metrics/timestats/timestats_test.go b/benchttp/internal/metrics/timestats/timestats_test.go similarity index 97% rename from runner/internal/metrics/timestats/timestats_test.go rename to benchttp/internal/metrics/timestats/timestats_test.go index e3854f4..65c0537 100644 --- a/runner/internal/metrics/timestats/timestats_test.go +++ b/benchttp/internal/metrics/timestats/timestats_test.go @@ -4,7 +4,7 @@ import ( "testing" "time" - "github.com/benchttp/engine/runner/internal/metrics/timestats" + "github.com/benchttp/sdk/benchttp/internal/metrics/timestats" ) func TestCompute(t *testing.T) { diff --git a/runner/internal/recorder/error.go b/benchttp/internal/recorder/error.go similarity index 100% rename from runner/internal/recorder/error.go rename to benchttp/internal/recorder/error.go diff --git a/runner/internal/recorder/httputil.go b/benchttp/internal/recorder/httputil.go similarity index 100% rename from runner/internal/recorder/httputil.go rename to benchttp/internal/recorder/httputil.go diff --git a/runner/internal/recorder/progress.go b/benchttp/internal/recorder/progress.go similarity index 100% rename from runner/internal/recorder/progress.go rename to benchttp/internal/recorder/progress.go diff --git a/runner/internal/recorder/recorder.go b/benchttp/internal/recorder/recorder.go similarity index 98% rename from runner/internal/recorder/recorder.go rename to benchttp/internal/recorder/recorder.go index 1e9be56..54fc0ac 100644 --- a/runner/internal/recorder/recorder.go +++ b/benchttp/internal/recorder/recorder.go @@ -7,7 +7,7 @@ import ( "sync" "time" - "github.com/benchttp/engine/internal/dispatcher" + "github.com/benchttp/sdk/internal/dispatcher" ) const ( diff --git a/runner/internal/recorder/recorder_internal_test.go b/benchttp/internal/recorder/recorder_internal_test.go similarity index 99% rename from runner/internal/recorder/recorder_internal_test.go rename to benchttp/internal/recorder/recorder_internal_test.go index bc79fe8..9ef9bf8 100644 --- a/runner/internal/recorder/recorder_internal_test.go +++ b/benchttp/internal/recorder/recorder_internal_test.go @@ -9,7 +9,7 @@ import ( "testing" "time" - "github.com/benchttp/engine/internal/dispatcher" + "github.com/benchttp/sdk/internal/dispatcher" ) var errTest = errors.New("test-generated error") diff --git a/runner/internal/recorder/tracer.go b/benchttp/internal/recorder/tracer.go similarity index 100% rename from runner/internal/recorder/tracer.go rename to benchttp/internal/recorder/tracer.go diff --git a/runner/internal/recorder/tracer_internal_test.go b/benchttp/internal/recorder/tracer_internal_test.go similarity index 100% rename from runner/internal/recorder/tracer_internal_test.go rename to benchttp/internal/recorder/tracer_internal_test.go diff --git a/runner/internal/reflectpath/resolver.go b/benchttp/internal/reflectpath/resolver.go similarity index 100% rename from runner/internal/reflectpath/resolver.go rename to benchttp/internal/reflectpath/resolver.go diff --git a/runner/internal/reflectpath/type.go b/benchttp/internal/reflectpath/type.go similarity index 100% rename from runner/internal/reflectpath/type.go rename to benchttp/internal/reflectpath/type.go diff --git a/runner/internal/reflectpath/value.go b/benchttp/internal/reflectpath/value.go similarity index 100% rename from runner/internal/reflectpath/value.go rename to benchttp/internal/reflectpath/value.go diff --git a/runner/internal/tests/predicate.go b/benchttp/internal/tests/predicate.go similarity index 91% rename from runner/internal/tests/predicate.go rename to benchttp/internal/tests/predicate.go index d8146d0..17e508f 100644 --- a/runner/internal/tests/predicate.go +++ b/benchttp/internal/tests/predicate.go @@ -3,8 +3,8 @@ package tests import ( "errors" - "github.com/benchttp/engine/internal/errorutil" - "github.com/benchttp/engine/runner/internal/metrics" + "github.com/benchttp/sdk/benchttp/internal/metrics" + "github.com/benchttp/sdk/internal/errorutil" ) var ErrUnknownPredicate = errors.New("tests: unknown predicate") diff --git a/runner/internal/tests/predicate_test.go b/benchttp/internal/tests/predicate_test.go similarity index 94% rename from runner/internal/tests/predicate_test.go rename to benchttp/internal/tests/predicate_test.go index f9aee16..b70ce87 100644 --- a/runner/internal/tests/predicate_test.go +++ b/benchttp/internal/tests/predicate_test.go @@ -4,8 +4,8 @@ import ( "testing" "time" - "github.com/benchttp/engine/runner/internal/metrics" - "github.com/benchttp/engine/runner/internal/tests" + "github.com/benchttp/sdk/benchttp/internal/metrics" + "github.com/benchttp/sdk/benchttp/internal/tests" ) func TestPredicate(t *testing.T) { diff --git a/runner/internal/tests/tests.go b/benchttp/internal/tests/tests.go similarity index 95% rename from runner/internal/tests/tests.go rename to benchttp/internal/tests/tests.go index a3e3f36..3fb03cf 100644 --- a/runner/internal/tests/tests.go +++ b/benchttp/internal/tests/tests.go @@ -3,7 +3,7 @@ package tests import ( "fmt" - "github.com/benchttp/engine/runner/internal/metrics" + "github.com/benchttp/sdk/benchttp/internal/metrics" ) type Case struct { diff --git a/runner/internal/tests/tests_test.go b/benchttp/internal/tests/tests_test.go similarity index 95% rename from runner/internal/tests/tests_test.go rename to benchttp/internal/tests/tests_test.go index a358601..6c58019 100644 --- a/runner/internal/tests/tests_test.go +++ b/benchttp/internal/tests/tests_test.go @@ -5,9 +5,9 @@ import ( "testing" "time" - "github.com/benchttp/engine/runner/internal/metrics" - "github.com/benchttp/engine/runner/internal/metrics/timestats" - "github.com/benchttp/engine/runner/internal/tests" + "github.com/benchttp/sdk/benchttp/internal/metrics" + "github.com/benchttp/sdk/benchttp/internal/metrics/timestats" + "github.com/benchttp/sdk/benchttp/internal/tests" ) func TestRun(t *testing.T) { diff --git a/runner/report.go b/benchttp/report.go similarity index 84% rename from runner/report.go rename to benchttp/report.go index f1cde3e..da11c31 100644 --- a/runner/report.go +++ b/benchttp/report.go @@ -1,10 +1,10 @@ -package runner +package benchttp import ( "time" - "github.com/benchttp/engine/runner/internal/metrics" - "github.com/benchttp/engine/runner/internal/tests" + "github.com/benchttp/sdk/benchttp/internal/metrics" + "github.com/benchttp/sdk/benchttp/internal/tests" ) // Report represents a run result as exported by the runner. diff --git a/runner/runner.go b/benchttp/runner.go similarity index 94% rename from runner/runner.go rename to benchttp/runner.go index ff052b9..ef28f32 100644 --- a/runner/runner.go +++ b/benchttp/runner.go @@ -1,4 +1,4 @@ -package runner +package benchttp import ( "context" @@ -7,9 +7,9 @@ import ( "net/http" "time" - "github.com/benchttp/engine/runner/internal/metrics" - "github.com/benchttp/engine/runner/internal/recorder" - "github.com/benchttp/engine/runner/internal/tests" + "github.com/benchttp/sdk/benchttp/internal/metrics" + "github.com/benchttp/sdk/benchttp/internal/recorder" + "github.com/benchttp/sdk/benchttp/internal/tests" ) type ( diff --git a/runner/runner_test.go b/benchttp/runner_test.go similarity index 86% rename from runner/runner_test.go rename to benchttp/runner_test.go index e6fe4d5..09bbc1c 100644 --- a/runner/runner_test.go +++ b/benchttp/runner_test.go @@ -1,16 +1,16 @@ -package runner_test +package benchttp_test import ( "errors" "net/http/httptest" "testing" - "github.com/benchttp/engine/runner" + "github.com/benchttp/sdk/benchttp" ) func TestRunner_Validate(t *testing.T) { t.Run("return nil if config is valid", func(t *testing.T) { - brunner := runner.Runner{ + runner := benchttp.Runner{ Request: httptest.NewRequest("GET", "https://a.b/#c?d=e&f=g", nil), Requests: 5, Concurrency: 5, @@ -19,13 +19,13 @@ func TestRunner_Validate(t *testing.T) { GlobalTimeout: 5, } - if err := brunner.Validate(); err != nil { + if err := runner.Validate(); err != nil { t.Errorf("unexpected error: %v", err) } }) t.Run("return cumulated errors if config is invalid", func(t *testing.T) { - brunner := runner.Runner{ + runner := benchttp.Runner{ Request: nil, Requests: -5, Concurrency: -5, @@ -34,12 +34,12 @@ func TestRunner_Validate(t *testing.T) { GlobalTimeout: -5, } - err := brunner.Validate() + err := runner.Validate() if err == nil { t.Fatal("invalid configuration considered valid") } - var errInvalid *runner.InvalidRunnerError + var errInvalid *benchttp.InvalidRunnerError if !errors.As(err, &errInvalid) { t.Fatalf("unexpected error type: %T", err) } diff --git a/configparse/json.go b/configparse/json.go index b48a5ac..c255f24 100644 --- a/configparse/json.go +++ b/configparse/json.go @@ -1,11 +1,9 @@ package configparse -import ( - "github.com/benchttp/engine/runner" -) +import "github.com/benchttp/sdk/benchttp" -// JSON reads input bytes as JSON and unmarshals it into a runner.Runner. -func JSON(in []byte, dst *runner.Runner) error { +// JSON reads input bytes as JSON and unmarshals it into a benchttp.Runner. +func JSON(in []byte, dst *benchttp.Runner) error { repr := Representation{} if err := (JSONParser{}).Parse(in, &repr); err != nil { return err diff --git a/configparse/json_test.go b/configparse/json_test.go index 6e14a14..0809be9 100644 --- a/configparse/json_test.go +++ b/configparse/json_test.go @@ -5,8 +5,8 @@ import ( "errors" "testing" - "github.com/benchttp/engine/configparse" - "github.com/benchttp/engine/runner" + "github.com/benchttp/sdk/benchttp" + "github.com/benchttp/sdk/configparse" ) func TestJSON(t *testing.T) { @@ -19,7 +19,7 @@ func TestJSON(t *testing.T) { testcases := []struct { name string input []byte - isValidRunner func(runner.Runner) bool + isValidRunner func(benchttp.Runner) bool expError error }{ { @@ -27,7 +27,7 @@ func TestJSON(t *testing.T) { input: baseInput.assign(object{ "badkey": "marcel-patulacci", }).json(), - isValidRunner: func(cfg runner.Runner) bool { return true }, + isValidRunner: func(cfg benchttp.Runner) bool { return true }, expError: errors.New(`invalid field ("badkey"): does not exist`), }, { @@ -37,7 +37,7 @@ func TestJSON(t *testing.T) { "concurrency": "bad value", // want int }, }).json(), - isValidRunner: func(runner.Runner) bool { return true }, + isValidRunner: func(benchttp.Runner) bool { return true }, expError: errors.New(`wrong type for field runner.concurrency: want int, got string`), }, { @@ -45,8 +45,8 @@ func TestJSON(t *testing.T) { input: baseInput.assign(object{ "runner": object{"concurrency": 3}, }).json(), - isValidRunner: func(r runner.Runner) bool { - defaultRunner := runner.DefaultRunner() + isValidRunner: func(r benchttp.Runner) bool { + defaultRunner := benchttp.DefaultRunner() isInputValueParsed := r.Concurrency == 3 isMergedWithDefault := r.Request.Method == defaultRunner.Request.Method && @@ -60,7 +60,7 @@ func TestJSON(t *testing.T) { for _, tc := range testcases { t.Run(tc.name, func(t *testing.T) { - gotRunner := runner.DefaultRunner() + gotRunner := benchttp.DefaultRunner() gotError := configparse.JSON(tc.input, &gotRunner) if !tc.isValidRunner(gotRunner) { diff --git a/configparse/parse.go b/configparse/parse.go index a33e8e8..54e3938 100644 --- a/configparse/parse.go +++ b/configparse/parse.go @@ -10,7 +10,7 @@ import ( "strconv" "time" - "github.com/benchttp/engine/runner" + "github.com/benchttp/sdk/benchttp" ) // Representation is a raw data model for formatted runner config (json, yaml). @@ -48,10 +48,10 @@ type Representation struct { } `yaml:"tests" json:"tests"` } -// ParseInto parses the Representation receiver as a runner.Runner +// ParseInto parses the Representation receiver as a benchttp.Runner // and stores any non-nil field value into the corresponding field // of dst. -func (repr Representation) ParseInto(dst *runner.Runner) error { +func (repr Representation) ParseInto(dst *benchttp.Runner) error { if err := repr.parseRequestInto(dst); err != nil { return err } @@ -61,7 +61,7 @@ func (repr Representation) ParseInto(dst *runner.Runner) error { return repr.parseTestsInto(dst) } -func (repr Representation) parseRequestInto(dst *runner.Runner) error { +func (repr Representation) parseRequestInto(dst *benchttp.Runner) error { if dst.Request == nil { dst.Request = &http.Request{} } @@ -98,7 +98,7 @@ func (repr Representation) parseRequestInto(dst *runner.Runner) error { return nil } -func (repr Representation) parseRunnerInto(dst *runner.Runner) error { +func (repr Representation) parseRunnerInto(dst *benchttp.Runner) error { if requests := repr.Runner.Requests; requests != nil { dst.Requests = *requests } @@ -134,13 +134,13 @@ func (repr Representation) parseRunnerInto(dst *runner.Runner) error { return nil } -func (repr Representation) parseTestsInto(dst *runner.Runner) error { +func (repr Representation) parseTestsInto(dst *benchttp.Runner) error { testSuite := repr.Tests if len(testSuite) == 0 { return nil } - cases := make([]runner.TestCase, len(testSuite)) + cases := make([]benchttp.TestCase, len(testSuite)) for i, t := range testSuite { fieldPath := func(caseField string) string { return fmt.Sprintf("tests[%d].%s", i, caseField) @@ -155,12 +155,12 @@ func (repr Representation) parseTestsInto(dst *runner.Runner) error { return err } - field := runner.MetricsField(*t.Field) + field := benchttp.MetricsField(*t.Field) if err := field.Validate(); err != nil { return fmt.Errorf("%s: %s", fieldPath("field"), err) } - predicate := runner.TestPredicate(*t.Predicate) + predicate := benchttp.TestPredicate(*t.Predicate) if err := predicate.Validate(); err != nil { return fmt.Errorf("%s: %s", fieldPath("predicate"), err) } @@ -170,7 +170,7 @@ func (repr Representation) parseTestsInto(dst *runner.Runner) error { return fmt.Errorf("%s: %s", fieldPath("target"), err) } - cases[i] = runner.TestCase{ + cases[i] = benchttp.TestCase{ Name: *t.Name, Field: field, Predicate: predicate, @@ -217,11 +217,11 @@ func parseOptionalDuration(raw string) (time.Duration, error) { } func parseMetricValue( - field runner.MetricsField, + field benchttp.MetricsField, inputValue string, -) (runner.MetricsValue, error) { +) (benchttp.MetricsValue, error) { fieldType := field.Type() - handleError := func(v interface{}, err error) (runner.MetricsValue, error) { + handleError := func(v interface{}, err error) (benchttp.MetricsValue, error) { if err != nil { return nil, fmt.Errorf( "value %q is incompatible with field %s (want %s)", diff --git a/configparse/parser_json_test.go b/configparse/parser_json_test.go index 51a4914..85deaae 100644 --- a/configparse/parser_json_test.go +++ b/configparse/parser_json_test.go @@ -3,7 +3,7 @@ package configparse_test import ( "testing" - "github.com/benchttp/engine/configparse" + "github.com/benchttp/sdk/configparse" ) func TestJSONParser(t *testing.T) { diff --git a/configparse/parser_yaml_test.go b/configparse/parser_yaml_test.go index e80439a..4b6ca8b 100644 --- a/configparse/parser_yaml_test.go +++ b/configparse/parser_yaml_test.go @@ -7,7 +7,7 @@ import ( "gopkg.in/yaml.v3" - "github.com/benchttp/engine/configparse" + "github.com/benchttp/sdk/configparse" ) func TestYAMLParser(t *testing.T) { diff --git a/go.mod b/go.mod index 23faa4d..06d8eb9 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/benchttp/engine +module github.com/benchttp/sdk go 1.17 diff --git a/internal/dispatcher/dispatcher_test.go b/internal/dispatcher/dispatcher_test.go index 6b5d9bd..953dad7 100644 --- a/internal/dispatcher/dispatcher_test.go +++ b/internal/dispatcher/dispatcher_test.go @@ -9,7 +9,7 @@ import ( "testing" "time" - "github.com/benchttp/engine/internal/dispatcher" + "github.com/benchttp/sdk/internal/dispatcher" ) func TestNew(t *testing.T) { From 5ab6aca5e2fbc8f84d3fb64f4639281176af10c7 Mon Sep 17 00:00:00 2001 From: Gregory Albouy Date: Sun, 23 Oct 2022 21:07:32 +0200 Subject: [PATCH 18/53] refactor: remove Makefile, use shell scripts --- .github/workflows/ci.yml | 4 ++-- Makefile | 45 ---------------------------------------- README.md | 10 ++++----- script/coverage | 3 +++ script/doc | 11 ++++++++++ script/lint | 3 +++ script/test | 8 +++++++ 7 files changed, 32 insertions(+), 52 deletions(-) delete mode 100644 Makefile create mode 100755 script/coverage create mode 100755 script/doc create mode 100755 script/lint create mode 100755 script/test diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3b682a6..f41d910 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,7 +18,7 @@ jobs: go-version: 1.17 - name: Install coverage tool - run: go get github.com/ory/go-acc + run: go install github.com/ory/go-acc@v0.2.8 # Check #1: Lint - name: Lint @@ -28,7 +28,7 @@ jobs: # Check #2: Test & generate coverage report - name: Test & coverage - run: make test-cov + run: ./script/coverage - name: Upload coverage report uses: codecov/codecov-action@v1.0.2 diff --git a/Makefile b/Makefile deleted file mode 100644 index 0ed0f52..0000000 --- a/Makefile +++ /dev/null @@ -1,45 +0,0 @@ -# Default command - -.PHONY: default -default: - @make check - -# Check code - -.PHONY: check -check: - @make lint - @make tests - -.PHONY: lint -lint: - @golangci-lint run - -.PHONY: tests -tests: - @go test -race -timeout 10s ./... - -TEST_FUNC=^.*$$ -ifdef t -TEST_FUNC=$(t) -endif -TEST_PKG=./... -ifdef p -TEST_PKG=./$(p) -endif - -.PHONY: test -test: - @go test -race -timeout 10s -run $(TEST_FUNC) $(TEST_PKG) - - -.PHONY: test-cov -test-cov: - @go-acc ./... - -# Docs - -.PHONY: docs -docs: - @echo "\033[4mhttp://localhost:9995/pkg/github.com/benchttp/sdk/\033[0m" - @godoc -http=localhost:9995 diff --git a/README.md b/README.md index 0d19487..6ed0c1b 100644 --- a/README.md +++ b/README.md @@ -99,8 +99,8 @@ func main() { ### Main commands -| Command | Description | -| ------------ | ----------------------------------- | -| `make lint` | Runs lint on the codebase | -| `make tests` | Runs tests suites from all packages | -| `make check` | Runs both lint and tests | +| Command | Description | +| --------------- | ------------------------------------------------- | +| `./script/lint` | Runs lint on the codebase | +| `./script/test` | Runs tests suites from all packages | +| `./script/doc` | Serves Go doc for this module at `localhost:9995` | diff --git a/script/coverage b/script/coverage new file mode 100755 index 0000000..677aed3 --- /dev/null +++ b/script/coverage @@ -0,0 +1,3 @@ +#!/bin/bash + +go-acc ./... -- -race -timeout=10s diff --git a/script/doc b/script/doc new file mode 100755 index 0000000..715e407 --- /dev/null +++ b/script/doc @@ -0,0 +1,11 @@ +#!/bin/bash + +domain="localhost" +port="9995" +addr="${domain}:${port}" + +main() { + echo "http://${addr}/pkg/github.com/benchttp/sdk/" + godoc -http="${addr}" +} +main diff --git a/script/lint b/script/lint new file mode 100755 index 0000000..3d13e8d --- /dev/null +++ b/script/lint @@ -0,0 +1,3 @@ +#!/bin/bash + +golangci-lint run diff --git a/script/test b/script/test new file mode 100755 index 0000000..5ae7c1c --- /dev/null +++ b/script/test @@ -0,0 +1,8 @@ +#!/bin/bash + +timeout="10s" + +main() { + go test -race -timeout "${timeout}" "${@:1}" ./... +} +main "${@}" From 3de371e54939695f2fe853c885c649fcbb765603 Mon Sep 17 00:00:00 2001 From: Gregory Albouy Date: Sun, 23 Oct 2022 22:01:03 +0200 Subject: [PATCH 19/53] chore(benchttp): remove Runner.Progress method Irrelevant method, use Runner.OnProgress instead. --- benchttp/runner.go | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/benchttp/runner.go b/benchttp/runner.go index ef28f32..0af562b 100644 --- a/benchttp/runner.go +++ b/benchttp/runner.go @@ -78,16 +78,6 @@ func (r *Runner) Run(ctx context.Context) (*Report, error) { return newReport(*r, duration, agg, testResults), nil } -// Progress returns the current progress of the recording. -// r.Run must have been called before, otherwise it returns -// a zero RecorderProgress. -func (r *Runner) Progress() RecordingProgress { - if r.recorder == nil { - return RecordingProgress{} - } - return r.recorder.Progress() -} - // recorderConfig returns a runner.RequesterConfig generated from cfg. func (r *Runner) recorderConfig() recorder.Config { return recorder.Config{ From 50dd455adbe300ac0e785ebf602f2b34ffbca2e2 Mon Sep 17 00:00:00 2001 From: Gregory Albouy Date: Sun, 23 Oct 2022 22:34:25 +0200 Subject: [PATCH 20/53] refactor(benchttp): DefaultRunner - simplify with less declarations - move into fie runner.go - more accurate docs --- benchttp/default.go | 30 ------------------------------ benchttp/runner.go | 14 +++++++++++++- benchttp/runner_test.go | 12 ++++++------ configparse/json_test.go | 39 ++++++++++++++------------------------- 4 files changed, 33 insertions(+), 62 deletions(-) delete mode 100644 benchttp/default.go diff --git a/benchttp/default.go b/benchttp/default.go deleted file mode 100644 index 545cf15..0000000 --- a/benchttp/default.go +++ /dev/null @@ -1,30 +0,0 @@ -package benchttp - -import ( - "fmt" - "net/http" - "time" -) - -// DefaultRunner returns a default Runner that is safe to use. -func DefaultRunner() Runner { - return defaultRunner -} - -var defaultRunner = Runner{ - Request: defaultRequest(), - - Concurrency: 10, - Requests: 100, - Interval: 0 * time.Second, - RequestTimeout: 5 * time.Second, - GlobalTimeout: 30 * time.Second, -} - -func defaultRequest() *http.Request { - req, err := http.NewRequest("GET", "", nil) - if err != nil { - panic(fmt.Sprintf("benchttp: %s", err)) - } - return req -} diff --git a/benchttp/runner.go b/benchttp/runner.go index 0af562b..f5f09d1 100644 --- a/benchttp/runner.go +++ b/benchttp/runner.go @@ -52,6 +52,18 @@ type Runner struct { recorder *recorder.Recorder } +// DefaultRunner returns a default Runner that is ready to use, +// except for Runner.Request that still needs to be set. +func DefaultRunner() Runner { + return Runner{ + Concurrency: 10, + Requests: 100, + Interval: 0 * time.Second, + RequestTimeout: 5 * time.Second, + GlobalTimeout: 30 * time.Second, + } +} + func (r *Runner) Run(ctx context.Context) (*Report, error) { // Validate input config if err := r.Validate(); err != nil { @@ -99,7 +111,7 @@ func (r Runner) Validate() error { //nolint:gocognit } if r.Request == nil { - appendError(errors.New("unexpected nil request")) + appendError(errors.New("Runner.Request must not be nil")) } if r.Requests < 1 && r.Requests != -1 { diff --git a/benchttp/runner_test.go b/benchttp/runner_test.go index 09bbc1c..9600b57 100644 --- a/benchttp/runner_test.go +++ b/benchttp/runner_test.go @@ -45,12 +45,12 @@ func TestRunner_Validate(t *testing.T) { } errs := errInvalid.Errors - assertError(t, errs, `unexpected nil request`) - assertError(t, errs, `requests (-5): want >= 0`) - assertError(t, errs, `concurrency (-5): want > 0 and <= requests (-5)`) - assertError(t, errs, `interval (-5): want >= 0`) - assertError(t, errs, `requestTimeout (-5): want > 0`) - assertError(t, errs, `globalTimeout (-5): want > 0`) + assertError(t, errs, "Runner.Request must not be nil") + assertError(t, errs, "requests (-5): want >= 0") + assertError(t, errs, "concurrency (-5): want > 0 and <= requests (-5)") + assertError(t, errs, "interval (-5): want >= 0") + assertError(t, errs, "requestTimeout (-5): want > 0") + assertError(t, errs, "globalTimeout (-5): want > 0") t.Logf("got error:\n%v", errInvalid) }) diff --git a/configparse/json_test.go b/configparse/json_test.go index 0809be9..4707c98 100644 --- a/configparse/json_test.go +++ b/configparse/json_test.go @@ -10,16 +10,17 @@ import ( ) func TestJSON(t *testing.T) { + const testURL = "https://example.com" baseInput := object{ "request": object{ - "url": "https://example.com", + "url": testURL, }, } testcases := []struct { name string input []byte - isValidRunner func(benchttp.Runner) bool + isValidRunner func(base, got benchttp.Runner) bool expError error }{ { @@ -27,7 +28,7 @@ func TestJSON(t *testing.T) { input: baseInput.assign(object{ "badkey": "marcel-patulacci", }).json(), - isValidRunner: func(cfg benchttp.Runner) bool { return true }, + isValidRunner: func(_, _ benchttp.Runner) bool { return true }, expError: errors.New(`invalid field ("badkey"): does not exist`), }, { @@ -37,22 +38,16 @@ func TestJSON(t *testing.T) { "concurrency": "bad value", // want int }, }).json(), - isValidRunner: func(benchttp.Runner) bool { return true }, + isValidRunner: func(_, _ benchttp.Runner) bool { return true }, expError: errors.New(`wrong type for field runner.concurrency: want int, got string`), }, { - name: "unmarshals JSON config and merges it with default", - input: baseInput.assign(object{ - "runner": object{"concurrency": 3}, - }).json(), - isValidRunner: func(r benchttp.Runner) bool { - defaultRunner := benchttp.DefaultRunner() - - isInputValueParsed := r.Concurrency == 3 - isMergedWithDefault := r.Request.Method == defaultRunner.Request.Method && - r.GlobalTimeout == defaultRunner.GlobalTimeout - - return isInputValueParsed && isMergedWithDefault + name: "unmarshals JSON config and merges it with base runner", + input: baseInput.json(), + isValidRunner: func(base, got benchttp.Runner) bool { + isParsed := got.Request.URL.String() == testURL + isMerged := got.GlobalTimeout == base.GlobalTimeout + return isParsed && isMerged }, expError: nil, }, @@ -63,8 +58,8 @@ func TestJSON(t *testing.T) { gotRunner := benchttp.DefaultRunner() gotError := configparse.JSON(tc.input, &gotRunner) - if !tc.isValidRunner(gotRunner) { - t.Errorf("unexpected config:\n%+v", gotRunner) + if !tc.isValidRunner(benchttp.DefaultRunner(), gotRunner) { + t.Errorf("unexpected runner:\n%+v", gotRunner) } if !sameErrors(gotError, tc.expError) { t.Errorf("unexpected error:\nexp %v,\ngot %v", tc.expError, gotError) @@ -95,11 +90,5 @@ func (o object) assign(other object) object { } func sameErrors(a, b error) bool { - if a == nil && b == nil { - return true - } - if a == nil || b == nil { - return false - } - return a.Error() == b.Error() + return (a == nil && b == nil) || !(a == nil || b == nil) || a.Error() == b.Error() } From aa2d6587d44665734020eed8eb9cc39272e5cd5c Mon Sep 17 00:00:00 2001 From: Gregory Albouy Date: Sun, 23 Oct 2022 23:00:44 +0200 Subject: [PATCH 21/53] feat(benchttp): implement request setters for Runner Allows convenient chaining, e.g. when using de default runner: DefaultRunner().WithNewRequest("", "http://a.b", nil).Run(ctx) - Runner.WithRequest - Runner.WithNewRequest --- benchttp/runner.go | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/benchttp/runner.go b/benchttp/runner.go index f5f09d1..2bb14c2 100644 --- a/benchttp/runner.go +++ b/benchttp/runner.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "io" "net/http" "time" @@ -64,6 +65,23 @@ func DefaultRunner() Runner { } } +// WithRequest attaches the given HTTP request to the Runner. +func (r *Runner) WithRequest(req *http.Request) *Runner { + r.Request = req + return r +} + +// WithNewRequest calls http.NewRequest with the given parameters +// and attaches the result to the Runner. If the call to http.NewRequest +// returns a non-nil error, it panics with the content of that error. +func (r *Runner) WithNewRequest(method, uri string, body io.Reader) *Runner { + req, err := http.NewRequest(method, uri, body) + if err != nil { + panic(err) + } + return r.WithRequest(req) +} + func (r *Runner) Run(ctx context.Context) (*Report, error) { // Validate input config if err := r.Validate(); err != nil { From 5a8f0fbbe58d5406821dcf2f97d11b44907fb7ad Mon Sep 17 00:00:00 2001 From: Gregory Albouy Date: Sun, 23 Oct 2022 23:00:56 +0200 Subject: [PATCH 22/53] docs: update README --- README.md | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 6ed0c1b..eaf4ad0 100644 --- a/README.md +++ b/README.md @@ -45,16 +45,11 @@ import ( "github.com/benchttp/sdk/benchttp" ) -func main(t *testing.T) { - // Set the request to send - request, _ := http.NewRequest("GET", "http://localhost:3000", nil) - - // Configure the runner - runner := runner.DefaultRunner() - runner.Request = request - - // Run benchmark, get report - report, _ := runner.Run(context.Background()) +func main() { + report, _ := benchttp. + DefaultRunner(). // Default runner with safe configuration + WithNewRequest("GET", "http://localhost:3000", nil). // Attach request + Run(context.Background()) // Run benchmark, retrieve report fmt.Println(report.Metrics.ResponseTimes.Mean) } @@ -69,6 +64,7 @@ import ( "context" "fmt" + "github.com/benchttp/sdk/benchttp" "github.com/benchttp/sdk/configparse" ) @@ -77,11 +73,17 @@ func main() { jsonConfig := []byte(` { "request": { - "url": "http://localhost:9999" + "url": "http://localhost:3000" } }`) - runner, _ := configparse.JSON(jsonConfig) + // Instantiate a base Runner (here the default with a safe configuration) + runner := benchttp.DefaultRunner() + + // Parse the json configuration into the Runner + _ = configparse.JSON(jsonConfig, &runner) + + // Run benchmark, retrieve report report, _ := runner.Run(context.Background()) fmt.Println(report.Metrics.ResponseTimes.Mean) From 998e2385079d67472f61eecee94ec1bdfd8bf1c2 Mon Sep 17 00:00:00 2001 From: Gregory Albouy Date: Fri, 28 Oct 2022 16:42:14 +0200 Subject: [PATCH 23/53] faet(runner): use value rexeivers --- benchttp/runner.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/benchttp/runner.go b/benchttp/runner.go index 2bb14c2..3141b6f 100644 --- a/benchttp/runner.go +++ b/benchttp/runner.go @@ -66,7 +66,7 @@ func DefaultRunner() Runner { } // WithRequest attaches the given HTTP request to the Runner. -func (r *Runner) WithRequest(req *http.Request) *Runner { +func (r Runner) WithRequest(req *http.Request) Runner { r.Request = req return r } @@ -74,7 +74,7 @@ func (r *Runner) WithRequest(req *http.Request) *Runner { // WithNewRequest calls http.NewRequest with the given parameters // and attaches the result to the Runner. If the call to http.NewRequest // returns a non-nil error, it panics with the content of that error. -func (r *Runner) WithNewRequest(method, uri string, body io.Reader) *Runner { +func (r Runner) WithNewRequest(method, uri string, body io.Reader) Runner { req, err := http.NewRequest(method, uri, body) if err != nil { panic(err) @@ -82,7 +82,7 @@ func (r *Runner) WithNewRequest(method, uri string, body io.Reader) *Runner { return r.WithRequest(req) } -func (r *Runner) Run(ctx context.Context) (*Report, error) { +func (r Runner) Run(ctx context.Context) (*Report, error) { // Validate input config if err := r.Validate(); err != nil { return nil, err @@ -105,11 +105,11 @@ func (r *Runner) Run(ctx context.Context) (*Report, error) { testResults := tests.Run(agg, r.Tests) - return newReport(*r, duration, agg, testResults), nil + return newReport(r, duration, agg, testResults), nil } // recorderConfig returns a runner.RequesterConfig generated from cfg. -func (r *Runner) recorderConfig() recorder.Config { +func (r Runner) recorderConfig() recorder.Config { return recorder.Config{ Requests: r.Requests, Concurrency: r.Concurrency, From a3b1225b4300c998b44da7f07a46e99e1426971d Mon Sep 17 00:00:00 2001 From: Gregory Albouy Date: Fri, 28 Oct 2022 16:42:28 +0200 Subject: [PATCH 24/53] test(runner) write unit tests for setters --- benchttp/runner_test.go | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/benchttp/runner_test.go b/benchttp/runner_test.go index 9600b57..db60da1 100644 --- a/benchttp/runner_test.go +++ b/benchttp/runner_test.go @@ -8,6 +8,37 @@ import ( "github.com/benchttp/sdk/benchttp" ) +func TestRunner_WithRequest(t *testing.T) { + request := httptest.NewRequest("POST", "https://example.com", nil) + runner := benchttp.DefaultRunner().WithRequest(request) + + if runner.Request != request { + t.Error("request not set") + } +} + +func TestRunner_WithNewRequest(t *testing.T) { + t.Run("attach valid request", func(t *testing.T) { + const method = "POST" + const urlString = "https://example.com" + + runner := benchttp.DefaultRunner().WithNewRequest(method, urlString, nil) + + if runner.Request.Method != method || runner.Request.URL.String() != urlString { + t.Error("request incorrectly seet") + } + }) + + t.Run("panic for bad request params", func(t *testing.T) { + defer func() { + if r := recover(); r == nil { + t.Error("did not panic") + } + }() + _ = benchttp.DefaultRunner().WithNewRequest("ù", "", nil) + }) +} + func TestRunner_Validate(t *testing.T) { t.Run("return nil if config is valid", func(t *testing.T) { runner := benchttp.Runner{ From 83a221351151a220ea5151cf1c5fbb8a4cdab749 Mon Sep 17 00:00:00 2001 From: Gregory Albouy Date: Sun, 6 Nov 2022 12:47:47 +0100 Subject: [PATCH 25/53] feat: create package benchttptest - implement AssertEqualRunners, EqualRunners, DiffRunner - Unit test exposed functions --- benchttptest/benchttptest.go | 2 + benchttptest/compare.go | 143 ++++++++++++++++++++++++++ benchttptest/compare_test.go | 189 +++++++++++++++++++++++++++++++++++ go.mod | 2 + go.sum | 2 + 5 files changed, 338 insertions(+) create mode 100644 benchttptest/benchttptest.go create mode 100644 benchttptest/compare.go create mode 100644 benchttptest/compare_test.go diff --git a/benchttptest/benchttptest.go b/benchttptest/benchttptest.go new file mode 100644 index 0000000..190a73b --- /dev/null +++ b/benchttptest/benchttptest.go @@ -0,0 +1,2 @@ +// Package benchttptest proovides utilities for benchttp testing. +package benchttptest diff --git a/benchttptest/compare.go b/benchttptest/compare.go new file mode 100644 index 0000000..70708d6 --- /dev/null +++ b/benchttptest/compare.go @@ -0,0 +1,143 @@ +package benchttptest + +import ( + "bytes" + "crypto/tls" + "io" + "net/http" + "net/url" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + + "github.com/benchttp/sdk/benchttp" +) + +// RunnerCmpOptions is the cmp.Options used to compare benchttp.Runner. +// By default, it ignores unexported fields and includes RequestCmpOptions. +var RunnerCmpOptions = cmp.Options{ + cmpopts.IgnoreUnexported(benchttp.Runner{}), + RequestCmpOptions, +} + +// RequestCmpOptions is the cmp.Options used to compare *http.Request. +// It behaves as follows: +// +// - Nil and empty values are considered equal +// +// - Fields that depend on how the request was created are ignored +// to avoid false negatives when comparing requests created in different +// ways (http.NewRequest vs httptest.NewRequest vs &http.Request{}) +// +// - Function fields are ignored +// +// - Body is ignored: it is compared separately +var RequestCmpOptions = cmp.Options{ + cmp.Transformer("Request", instantiateNilRequest), + cmp.Transformer("Request.Header", instantiateNilHeader), + cmp.Transformer("Request.URL", stringifyURL), + cmpopts.IgnoreUnexported(http.Request{}, tls.ConnectionState{}), + cmpopts.IgnoreFields(http.Request{}, unreliableRequestFields...), +} + +var unreliableRequestFields = []string{ + // These fields are automatically set by NewRequest constructor + // from packages http and httptest, as a consequence they can + // trigger false positives when comparing requests that were + // created differently. + "Proto", "ProtoMajor", "ProtoMinor", "ContentLength", + "Host", "RemoteAddr", "RequestURI", "TLS", "Cancel", + + // Function fields cannot be reliably compared + "GetBody", + + // Body field can't be read without altering the Request, causing + // cmp-go to panic. We perform a custom comparison instead. + "Body", +} + +// AssertEqualRunners fails t and shows a diff if a and b are not equal, +// as determined by RunnerCmpOptions. +func AssertEqualRunners(t *testing.T, x, y benchttp.Runner) { + t.Helper() + if !EqualRunners(x, y) { + t.Error(DiffRunner(x, y)) + } +} + +// EqualRunners returns true if x and y are equal, as determined by +// RunnerCmpOptions. +func EqualRunners(x, y benchttp.Runner) bool { + return cmp.Equal(x, y, RunnerCmpOptions) && + compareRequestBody(x.Request, y.Request) +} + +// DiffRunner returns a string showing the diff between x and y, +// as determined by RunnerCmpOptions. +func DiffRunner(x, y benchttp.Runner) string { + b := strings.Builder{} + b.WriteString(cmp.Diff(x, y, RunnerCmpOptions)) + if x.Request != nil && y.Request != nil { + xbody := nopreadBody(x.Request) + ybody := nopreadBody(y.Request) + if !bytes.Equal(xbody, ybody) { + b.WriteString("Request.Body: ") + b.WriteString(cmp.Diff(string(xbody), string(ybody))) + } + } + return b.String() +} + +// helpers + +func instantiateNilRequest(r *http.Request) *http.Request { + if r == nil { + return &http.Request{} + } + return r +} + +func instantiateNilHeader(h http.Header) http.Header { + if h == nil { + return http.Header{} + } + return h +} + +func stringifyURL(u *url.URL) string { + if u == nil { + return "" + } + return u.String() +} + +func compareRequestBody(a, b *http.Request) bool { + ba, bb := nopreadBody(a), nopreadBody(b) + return bytes.Equal(ba, bb) +} + +func nopreadBody(r *http.Request) []byte { + if r == nil || r.Body == nil { + return []byte{} + } + + bbuf := bytes.Buffer{} + + if _, err := io.Copy(&bbuf, r.Body); err != nil { + panic("benchttptest: error reading Request.Body: " + err.Error()) + } + + if r.GetBody != nil { + newbody, err := r.GetBody() + if err != nil { + panic("benchttptest: Request.GetBody error: " + err.Error()) + } + r.Body = newbody + } else { + r.Body = io.NopCloser(&bbuf) + } + + return bbuf.Bytes() +} diff --git a/benchttptest/compare_test.go b/benchttptest/compare_test.go new file mode 100644 index 0000000..7df633e --- /dev/null +++ b/benchttptest/compare_test.go @@ -0,0 +1,189 @@ +package benchttptest_test + +import ( + "bytes" + "io" + "net/http" + "net/http/httptest" + "net/url" + "strings" + "testing" + + "github.com/benchttp/sdk/benchttp" + "github.com/benchttp/sdk/benchttptest" +) + +func TestAssertEqualRunners(t *testing.T) { + for _, tc := range []struct { + name string + pass bool + a, b benchttp.Runner + }{ + { + name: "pass if runners are equal", + pass: true, + a: benchttp.Runner{Requests: 1}, + b: benchttp.Runner{Requests: 1}, + }, + { + name: "fail if runners are not equal", + pass: false, + a: benchttp.Runner{Requests: 1}, + b: benchttp.Runner{Requests: 2}, + }, + } { + t.Run(tc.name, func(t *testing.T) { + tt := &testing.T{} + + benchttptest.AssertEqualRunners(tt, tc.a, tc.b) + if tt.Failed() == tc.pass { + t.Fail() + } + }) + } +} + +func TestEqualRunners(t *testing.T) { + for _, tc := range []struct { + name string + want bool + a, b benchttp.Runner + }{ + { + name: "equal runners", + want: true, + a: benchttp.Runner{Requests: 1}, + b: benchttp.Runner{Requests: 1}, + }, + { + name: "different runners", + want: false, + a: benchttp.Runner{Requests: 1}, + b: benchttp.Runner{Requests: 2}, + }, + { + name: "consider zero requests equal", + want: true, + a: benchttp.Runner{Request: nil}, + b: benchttp.Runner{Request: &http.Request{}}, + }, + { + name: "consider zero request headers equal", + want: true, + a: benchttp.Runner{Request: &http.Request{Header: nil}}, + b: benchttp.Runner{Request: &http.Request{Header: http.Header{}}}, + }, + { + name: "consider zero request bodies equal", + want: true, + a: benchttp.Runner{Request: &http.Request{Body: nil}}, + b: benchttp.Runner{Request: &http.Request{Body: http.NoBody}}, + }, + { + name: "zero request vs non zero request", + want: false, + a: benchttp.Runner{Request: &http.Request{Method: "GET"}}, + b: benchttp.Runner{Request: nil}, + }, + { + name: "different request field values", + want: false, + a: benchttp.Runner{Request: &http.Request{Method: "GET"}}, + b: benchttp.Runner{Request: &http.Request{Method: "POST"}}, + }, + { + name: "ignore unreliable request fields", + want: true, + a: benchttp.Runner{ + Request: httptest.NewRequest( // sets Proto, ContentLength, ... + "POST", + "https://example.com", + nil, + ), + }, + b: benchttp.Runner{ + Request: &http.Request{ + Method: "POST", + URL: mustParseRequestURI("https://example.com"), + }, + }, + }, + { + name: "equal request bodies", + want: true, + a: benchttp.Runner{ + Request: &http.Request{ + Body: io.NopCloser(strings.NewReader("hello")), + }, + }, + b: benchttp.Runner{ + Request: &http.Request{ + Body: io.NopCloser(strings.NewReader("hello")), + }, + }, + }, + { + name: "different request bodies", + want: false, + a: benchttp.Runner{ + Request: &http.Request{ + Body: io.NopCloser(strings.NewReader("hello")), + }, + }, + b: benchttp.Runner{ + Request: &http.Request{ + Body: io.NopCloser(strings.NewReader("world")), + }, + }, + }, + } { + t.Run(tc.name, func(t *testing.T) { + if benchttptest.EqualRunners(tc.a, tc.b) != tc.want { + t.Error(benchttptest.DiffRunner(tc.a, tc.b)) + } + }) + } + + t.Run("restore request body", func(t *testing.T) { + a := benchttp.Runner{ + Request: httptest.NewRequest( + "POST", + "https://example.com", + strings.NewReader("hello"), + ), + } + b := benchttp.Runner{ + Request: &http.Request{ + Method: "POST", + URL: mustParseRequestURI("https://example.com"), + Body: io.NopCloser(bytes.NewReader([]byte("hello"))), + }, + } + + _ = benchttptest.EqualRunners(a, b) + + ba, bb := mustRead(a.Request.Body), mustRead(b.Request.Body) + want := []byte("hello") + if !bytes.Equal(want, ba) || !bytes.Equal(want, bb) { + t.Fail() + } + }) +} + +// helpers + +func mustParseRequestURI(s string) *url.URL { + u, err := url.ParseRequestURI(s) + if err != nil { + panic(err) + } + return u +} + +func mustRead(r io.Reader) []byte { + b, err := io.ReadAll(r) + if err != nil { + panic("mustRead: " + err.Error()) + } + return b +} diff --git a/go.mod b/go.mod index 06d8eb9..b514887 100644 --- a/go.mod +++ b/go.mod @@ -6,3 +6,5 @@ require ( golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 gopkg.in/yaml.v3 v3.0.1 ) + +require github.com/google/go-cmp v0.5.9 diff --git a/go.sum b/go.sum index 12011ba..cb4a9ff 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 h1:uVc8UZUe6tr40fFVnUP5Oj+veunVezqYl9z7DYw9xzw= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= From dc0f35c570775615d5c955da5023ccd211a1b435 Mon Sep 17 00:00:00 2001 From: Gregory Albouy Date: Fri, 28 Oct 2022 23:46:58 +0200 Subject: [PATCH 26/53] feat(configparse): import and flatten configfile from benchttp/cli --- configparse/error.go | 26 ++ configparse/file.go | 115 +++++++ configparse/file_test.go | 288 ++++++++++++++++++ configparse/find.go | 24 ++ configparse/find_test.go | 39 +++ configparse/parser.go | 33 ++ .../testdata/extends/extends-circular-0.yml | 1 + .../testdata/extends/extends-circular-1.yml | 1 + .../testdata/extends/extends-circular-2.yml | 1 + .../extends/extends-circular-self.yml | 1 + .../testdata/extends/extends-valid-child.yml | 4 + .../testdata/extends/extends-valid-parent.yml | 3 + .../nest-0/nest-1/extends-valid-nested.yml | 4 + configparse/testdata/invalid/badext.yams | 2 + configparse/testdata/invalid/badfields.json | 7 + configparse/testdata/invalid/badfields.yml | 4 + configparse/testdata/valid/benchttp-zeros.yml | 3 + configparse/testdata/valid/benchttp.json | 44 +++ configparse/testdata/valid/benchttp.yaml | 35 +++ configparse/testdata/valid/benchttp.yml | 32 ++ 20 files changed, 667 insertions(+) create mode 100644 configparse/error.go create mode 100644 configparse/file.go create mode 100644 configparse/file_test.go create mode 100644 configparse/find.go create mode 100644 configparse/find_test.go create mode 100644 configparse/parser.go create mode 100644 configparse/testdata/extends/extends-circular-0.yml create mode 100644 configparse/testdata/extends/extends-circular-1.yml create mode 100644 configparse/testdata/extends/extends-circular-2.yml create mode 100644 configparse/testdata/extends/extends-circular-self.yml create mode 100644 configparse/testdata/extends/extends-valid-child.yml create mode 100644 configparse/testdata/extends/extends-valid-parent.yml create mode 100644 configparse/testdata/extends/nest-0/nest-1/extends-valid-nested.yml create mode 100644 configparse/testdata/invalid/badext.yams create mode 100644 configparse/testdata/invalid/badfields.json create mode 100644 configparse/testdata/invalid/badfields.yml create mode 100644 configparse/testdata/valid/benchttp-zeros.yml create mode 100644 configparse/testdata/valid/benchttp.json create mode 100644 configparse/testdata/valid/benchttp.yaml create mode 100644 configparse/testdata/valid/benchttp.yml diff --git a/configparse/error.go b/configparse/error.go new file mode 100644 index 0000000..4a053e4 --- /dev/null +++ b/configparse/error.go @@ -0,0 +1,26 @@ +package configparse + +import ( + "errors" +) + +var ( + // ErrFileNotFound signals a config file not found. + ErrFileNotFound = errors.New("file not found") + + // ErrFileRead signals an error trying to read a config file. + // It can be due to a corrupted file or an invalid permission + // for instance. + ErrFileRead = errors.New("invalid file") + + // ErrFileExt signals an unsupported extension for the config file. + ErrFileExt = errors.New("invalid extension") + + // ErrFileParse signals an error parsing a retrieved config file. + // It is returned if it contains an unexpected field or an unexpected + // value for a field. + ErrFileParse = errors.New("parsing error: invalid config file") + + // ErrFileCircular signals a circular reference in the config file. + ErrFileCircular = errors.New("circular reference detected") +) diff --git a/configparse/file.go b/configparse/file.go new file mode 100644 index 0000000..ae891ea --- /dev/null +++ b/configparse/file.go @@ -0,0 +1,115 @@ +package configparse + +import ( + "errors" + "os" + "path/filepath" + + "github.com/benchttp/sdk/benchttp" + + "github.com/benchttp/sdk/internal/errorutil" +) + +// Parse parses given filename as a benchttp runner configuration +// into a runner.Runner and stores the retrieved values into *dst. +// It returns the first error occurring in the process, which can be +// any of the values declared in the package. +func Parse(filename string, dst *benchttp.Runner) (err error) { + reprs, err := parseFileRecursive(filename, []Representation{}, set{}) + if err != nil { + return + } + return parseAndMergeConfigs(reprs, dst) +} + +// set is a collection of unique string values. +type set map[string]bool + +// add adds v to the receiver. If v is already set, it returns a non-nil +// error instead. +func (s set) add(v string) error { + if _, exists := s[v]; exists { + return errors.New("value already set") + } + s[v] = true + return nil +} + +// parseFileRecursive parses a config file and its parent found from key +// "extends" recursively until the root config file is reached. +// It returns the list of all parsed configs or the first non-nil error +// occurring in the process. +func parseFileRecursive( + filename string, + reprs []Representation, + seen set, +) ([]Representation, error) { + // avoid infinite recursion caused by circular reference + if err := seen.add(filename); err != nil { + return reprs, ErrFileCircular + } + + // parse current file, append parsed config + repr, err := parseFile(filename) + if err != nil { + return reprs, err + } + reprs = append(reprs, repr) + + // root config reached: stop now and return the parsed configs + if repr.Extends == nil { + return reprs, nil + } + + // config has parent: resolve its path and parse it recursively + parentPath := filepath.Join(filepath.Dir(filename), *repr.Extends) + return parseFileRecursive(parentPath, reprs, seen) +} + +// parseFile parses a single config file and returns the result as an +// Representation and an appropriate error predeclared in the package. +func parseFile(filename string) (repr Representation, err error) { + b, err := os.ReadFile(filename) + switch { + case err == nil: + case errors.Is(err, os.ErrNotExist): + return repr, errorutil.WithDetails(ErrFileNotFound, filename) + default: + return repr, errorutil.WithDetails(ErrFileRead, filename, err) + } + + ext := extension(filepath.Ext(filename)) + parser, err := newParser(ext) + if err != nil { + return repr, errorutil.WithDetails(ErrFileExt, ext, err) + } + + if err = parser.Parse(b, &repr); err != nil { + return repr, errorutil.WithDetails(ErrFileParse, filename, err) + } + + return repr, nil +} + +// parseAndMergeConfigs iterates backwards over reprs, parses them as +// runner.Runner, merges them successively and finally stores the result +// into dst. +// It returns the merged result or the first non-nil error occurring in the +// process. +func parseAndMergeConfigs(reprs []Representation, dst *benchttp.Runner) error { + if len(reprs) == 0 { // supposedly catched upstream, should not occur + return errors.New( + "an unacceptable error occurred parsing the config file, " + + "please visit https://github.com/benchttp/runner/issues/new " + + "and insult us properly", + ) + } + + for i := len(reprs) - 1; i >= 0; i-- { + if err := reprs[i].ParseInto(dst); err != nil { + return errorutil.WithDetails(ErrFileParse, err) + } + } + + return nil +} diff --git a/configparse/file_test.go b/configparse/file_test.go new file mode 100644 index 0000000..8c3eeb7 --- /dev/null +++ b/configparse/file_test.go @@ -0,0 +1,288 @@ +package configparse_test + +import ( + "bytes" + "errors" + "fmt" + "io" + "net/http" + "net/http/httptest" + "net/url" + "path/filepath" + "reflect" + "testing" + "time" + + "github.com/benchttp/sdk/benchttp" + "github.com/benchttp/sdk/configparse" +) + +const ( + validConfigPath = "./testdata" + validURL = "http://localhost:9999?fib=30&delay=200ms" // value from testdata files +) + +var supportedExt = []string{ + ".yml", + ".yaml", + ".json", +} + +// TestParse ensures the config file is open, read, and correctly parsed. +func TestParse(t *testing.T) { + t.Run("return file errors early", func(t *testing.T) { + testcases := []struct { + label string + path string + expErr error + }{ + { + label: "not found", + path: configPath("invalid/bad path"), + expErr: configparse.ErrFileNotFound, + }, + { + label: "unsupported extension", + path: configPath("invalid/badext.yams"), + expErr: configparse.ErrFileExt, + }, + { + label: "yaml invalid fields", + path: configPath("invalid/badfields.yml"), + expErr: configparse.ErrFileParse, + }, + { + label: "json invalid fields", + path: configPath("invalid/badfields.json"), + expErr: configparse.ErrFileParse, + }, + { + label: "self reference", + path: configPath("extends/extends-circular-self.yml"), + expErr: configparse.ErrFileCircular, + }, + { + label: "circular reference", + path: configPath("extends/extends-circular-0.yml"), + expErr: configparse.ErrFileCircular, + }, + } + + for _, tc := range testcases { + t.Run(tc.label, func(t *testing.T) { + runner := benchttp.Runner{} + gotErr := configparse.Parse(tc.path, &runner) + + if gotErr == nil { + t.Fatal("exp non-nil error, got nil") + } + + if !errors.Is(gotErr, tc.expErr) { + t.Errorf("\nexp %v\ngot %v", tc.expErr, gotErr) + } + + if !sameConfig(runner, benchttp.Runner{}) { + t.Errorf("\nexp empty config\ngot %v", runner) + } + }) + } + }) + + t.Run("happy path for all extensions", func(t *testing.T) { + for _, ext := range supportedExt { + expCfg := newExpConfig() + fname := configPath("valid/benchttp" + ext) + + gotCfg := benchttp.Runner{} + if err := configparse.Parse(fname, &gotCfg); err != nil { + // critical error, stop the test + t.Fatal(err) + } + + if sameConfig(gotCfg, benchttp.Runner{}) { + t.Error("received an empty configuration") + } + + if !sameConfig(gotCfg, expCfg) { + t.Errorf("unexpected parsed config for %s file:\nexp %#v\ngot %#v", ext, expCfg, gotCfg) + } + + } + }) + + t.Run("override input config", func(t *testing.T) { + runner := benchttp.Runner{} + runner.Request = httptest.NewRequest("POST", "https://overriden.com", nil) + runner.GlobalTimeout = 10 * time.Millisecond + + fname := configPath("valid/benchttp-zeros.yml") + + if err := configparse.Parse(fname, &runner); err != nil { + t.Fatal(err) + } + + const ( + expMethod = "POST" // from input config + expGlobalTimeout = 42 * time.Millisecond // from read file + ) + + if gotMethod := runner.Request.Method; gotMethod != expMethod { + t.Errorf( + "did not keep input values that are not set: "+ + "exp Request.Method == %s, got %s", + expMethod, gotMethod, + ) + } + + if gotGlobalTimeout := runner.GlobalTimeout; gotGlobalTimeout != expGlobalTimeout { + t.Errorf( + "did not override input values that are set: "+ + "exp Runner.GlobalTimeout == %v, got %v", + expGlobalTimeout, gotGlobalTimeout, + ) + } + + t.Log(runner) + }) + + t.Run("extend config files", func(t *testing.T) { + testcases := []struct { + label string + cfname string + cfpath string + }{ + { + label: "same directory", + cfname: "child", + cfpath: configPath("extends/extends-valid-child.yml"), + }, + { + label: "nested directory", + cfname: "nested", + cfpath: configPath("extends/nest-0/nest-1/extends-valid-nested.yml"), + }, + } + + for _, tc := range testcases { + t.Run(tc.label, func(t *testing.T) { + var runner benchttp.Runner + if err := configparse.Parse(tc.cfpath, &runner); err != nil { + t.Fatal(err) + } + + var ( + expMethod = "POST" + expURL = fmt.Sprintf("http://%s.config", tc.cfname) + ) + + if gotMethod := runner.Request.Method; gotMethod != expMethod { + t.Errorf("method: exp %s, got %s", expMethod, gotMethod) + } + + if gotURL := runner.Request.URL.String(); gotURL != expURL { + t.Errorf("url: exp %s, got %s", expURL, gotURL) + } + }) + } + }) +} + +// helpers + +// newExpConfig returns the expected runner.ConfigConfig result after parsing +// one of the config files in testdataConfigPath. +func newExpConfig() benchttp.Runner { + request := httptest.NewRequest( + "POST", + validURL, + bytes.NewReader([]byte(`{"key0":"val0","key1":"val1"}`)), + ) + request.Header = http.Header{ + "key0": []string{"val0", "val1"}, + "key1": []string{"val0"}, + } + return benchttp.Runner{ + Request: request, + + Requests: 100, + Concurrency: 1, + Interval: 50 * time.Millisecond, + RequestTimeout: 2 * time.Second, + GlobalTimeout: 60 * time.Second, + + Tests: []benchttp.TestCase{ + { + Name: "minimum response time", + Field: "ResponseTimes.Min", + Predicate: "GT", + Target: 80 * time.Millisecond, + }, + { + Name: "maximum response time", + Field: "ResponseTimes.Max", + Predicate: "LTE", + Target: 120 * time.Millisecond, + }, + { + Name: "100% availability", + Field: "RequestFailureCount", + Predicate: "EQ", + Target: 0, + }, + }, + } +} + +func sameConfig(a, b benchttp.Runner) bool { + if a.Request == nil || b.Request == nil { + return a.Request == nil && b.Request == nil + } + return sameURL(a.Request.URL, b.Request.URL) && + sameHeader(a.Request.Header, b.Request.Header) && + sameBody(a.Request.Body, b.Request.Body) +} + +// sameURL returns true if a and b are the same *url.URL, taking into account +// the undeterministic nature of their RawQuery. +func sameURL(a, b *url.URL) bool { + // check query params equality via Query() rather than RawQuery + if !reflect.DeepEqual(a.Query(), b.Query()) { + return false + } + + // temporarily set RawQuery to a determined value + for _, u := range []*url.URL{a, b} { + defer setTempValue(&u.RawQuery, "replaced by test")() + } + + // we can now rely on deep equality check + return reflect.DeepEqual(a, b) +} + +func sameHeader(a, b http.Header) bool { + return reflect.DeepEqual(a, b) + // if len(a) != len(b) { + // return false + // } + // for k, values := range a { + // if len(values) != len() + // } +} + +func sameBody(a, b io.ReadCloser) bool { + return reflect.DeepEqual(a, b) +} + +// setTempValue sets *ptr to val and returns a restore func that sets *ptr +// back to its previous value. +func setTempValue(ptr *string, val string) (restore func()) { + previousValue := *ptr + *ptr = val + return func() { + *ptr = previousValue + } +} + +func configPath(name string) string { + return filepath.Join(validConfigPath, name) +} diff --git a/configparse/find.go b/configparse/find.go new file mode 100644 index 0000000..a4f1bcc --- /dev/null +++ b/configparse/find.go @@ -0,0 +1,24 @@ +package configparse + +import "os" + +var DefaultPaths = []string{ + "./.benchttp.yml", + "./.benchttp.yaml", + "./.benchttp.json", +} + +// Find returns the first name that matches a file path. +// If input paths is empty, it uses DefaultPaths. +// If no match is found, it returns an empty string. +func Find(paths ...string) string { + if len(paths) == 0 { + paths = DefaultPaths + } + for _, path := range paths { + if _, err := os.Stat(path); err == nil { // err IS nil: file exists + return path + } + } + return "" +} diff --git a/configparse/find_test.go b/configparse/find_test.go new file mode 100644 index 0000000..1bf45b4 --- /dev/null +++ b/configparse/find_test.go @@ -0,0 +1,39 @@ +package configparse_test + +import ( + "testing" + + "github.com/benchttp/sdk/configparse" +) + +var ( + goodFileYML = configPath("valid/benchttp.yml") + goodFileJSON = configPath("valid/benchttp.json") + badFile = configPath("does-not-exist.json") +) + +func TestFind(t *testing.T) { + t.Run("return first existing file form input", func(t *testing.T) { + files := []string{badFile, goodFileYML, goodFileJSON} + + if got := configparse.Find(files...); got != goodFileYML { + t.Errorf("did not retrieve good file: exp %s, got %s", goodFileYML, got) + } + }) + + t.Run("return first existing file from defaults", func(t *testing.T) { + configparse.DefaultPaths = []string{badFile, goodFileYML, goodFileJSON} + + if got := configparse.Find(); got != goodFileYML { + t.Errorf("did not retrieve good file: exp %s, got %s", goodFileYML, got) + } + }) + + t.Run("return empty string when no match", func(t *testing.T) { + files := []string{badFile} + + if got := configparse.Find(files...); got != "" { + t.Errorf("retrieved unexpected file: %s", got) + } + }) +} diff --git a/configparse/parser.go b/configparse/parser.go new file mode 100644 index 0000000..585f7d2 --- /dev/null +++ b/configparse/parser.go @@ -0,0 +1,33 @@ +package configparse + +import ( + "errors" +) + +type extension string + +const ( + extYML extension = ".yml" + extYAML extension = ".yaml" + extJSON extension = ".json" +) + +// configParser exposes a method parse to read bytes as a raw config. +type configParser interface { + // parse parses a raw bytes input as a raw config and stores + // the resulting value into dst. + Parse(in []byte, dst *Representation) error +} + +// newParser returns an appropriate parser according to ext, or a non-nil +// error if ext is not an expected extension. +func newParser(ext extension) (configParser, error) { + switch ext { + case extYML, extYAML: + return YAMLParser{}, nil + case extJSON: + return JSONParser{}, nil + default: + return nil, errors.New("unsupported config format") + } +} diff --git a/configparse/testdata/extends/extends-circular-0.yml b/configparse/testdata/extends/extends-circular-0.yml new file mode 100644 index 0000000..55c7ac7 --- /dev/null +++ b/configparse/testdata/extends/extends-circular-0.yml @@ -0,0 +1 @@ +extends: ./extends-circular-1.yml diff --git a/configparse/testdata/extends/extends-circular-1.yml b/configparse/testdata/extends/extends-circular-1.yml new file mode 100644 index 0000000..f451260 --- /dev/null +++ b/configparse/testdata/extends/extends-circular-1.yml @@ -0,0 +1 @@ +extends: ./extends-circular-2.yml diff --git a/configparse/testdata/extends/extends-circular-2.yml b/configparse/testdata/extends/extends-circular-2.yml new file mode 100644 index 0000000..b862fa4 --- /dev/null +++ b/configparse/testdata/extends/extends-circular-2.yml @@ -0,0 +1 @@ +extends: ./extends-circular-0.yml diff --git a/configparse/testdata/extends/extends-circular-self.yml b/configparse/testdata/extends/extends-circular-self.yml new file mode 100644 index 0000000..2fa66ac --- /dev/null +++ b/configparse/testdata/extends/extends-circular-self.yml @@ -0,0 +1 @@ +extends: ./extends-circular-self.yml diff --git a/configparse/testdata/extends/extends-valid-child.yml b/configparse/testdata/extends/extends-valid-child.yml new file mode 100644 index 0000000..a344080 --- /dev/null +++ b/configparse/testdata/extends/extends-valid-child.yml @@ -0,0 +1,4 @@ +extends: ./extends-valid-parent.yml + +request: + url: http://child.config diff --git a/configparse/testdata/extends/extends-valid-parent.yml b/configparse/testdata/extends/extends-valid-parent.yml new file mode 100644 index 0000000..7f3b136 --- /dev/null +++ b/configparse/testdata/extends/extends-valid-parent.yml @@ -0,0 +1,3 @@ +request: + method: POST + url: http://parent.config diff --git a/configparse/testdata/extends/nest-0/nest-1/extends-valid-nested.yml b/configparse/testdata/extends/nest-0/nest-1/extends-valid-nested.yml new file mode 100644 index 0000000..7810890 --- /dev/null +++ b/configparse/testdata/extends/nest-0/nest-1/extends-valid-nested.yml @@ -0,0 +1,4 @@ +extends: ../../extends-valid-parent.yml + +request: + url: http://nested.config diff --git a/configparse/testdata/invalid/badext.yams b/configparse/testdata/invalid/badext.yams new file mode 100644 index 0000000..f7aba57 --- /dev/null +++ b/configparse/testdata/invalid/badext.yams @@ -0,0 +1,2 @@ +request: + url: https://benchttp.app diff --git a/configparse/testdata/invalid/badfields.json b/configparse/testdata/invalid/badfields.json new file mode 100644 index 0000000..9b45468 --- /dev/null +++ b/configparse/testdata/invalid/badfields.json @@ -0,0 +1,7 @@ +{ + "runner": { + "requests": [123], + "concurrency": "123" + }, + "notafield": 123 +} diff --git a/configparse/testdata/invalid/badfields.yml b/configparse/testdata/invalid/badfields.yml new file mode 100644 index 0000000..6899cd8 --- /dev/null +++ b/configparse/testdata/invalid/badfields.yml @@ -0,0 +1,4 @@ +runner: + requests: [123] # error: invalid type for field requests + concurrency: "123" # error: invalid type for field concurrency +notafield: 123 # error: field does not exist diff --git a/configparse/testdata/valid/benchttp-zeros.yml b/configparse/testdata/valid/benchttp-zeros.yml new file mode 100644 index 0000000..38b1bdb --- /dev/null +++ b/configparse/testdata/valid/benchttp-zeros.yml @@ -0,0 +1,3 @@ +runner: + requests: 0 + globalTimeout: 42ms diff --git a/configparse/testdata/valid/benchttp.json b/configparse/testdata/valid/benchttp.json new file mode 100644 index 0000000..714506f --- /dev/null +++ b/configparse/testdata/valid/benchttp.json @@ -0,0 +1,44 @@ +{ + "request": { + "method": "POST", + "url": "http://localhost:9999?delay=200ms", + "queryParams": { + "fib": "30" + }, + "header": { + "key0": ["val0", "val1"], + "key1": ["val0"] + }, + "body": { + "type": "raw", + "content": "{\"key0\":\"val0\",\"key1\":\"val1\"}" + } + }, + "runner": { + "requests": 100, + "concurrency": 1, + "interval": "50ms", + "requestTimeout": "2s", + "globalTimeout": "60s" + }, + "tests": [ + { + "name": "minimum response time", + "field": "ResponseTimes.Min", + "predicate": "GT", + "target": "80ms" + }, + { + "name": "maximum response time", + "field": "ResponseTimes.Max", + "predicate": "LTE", + "target": "120ms" + }, + { + "name": "100% availability", + "field": "RequestFailureCount", + "predicate": "EQ", + "target": "0" + } + ] +} diff --git a/configparse/testdata/valid/benchttp.yaml b/configparse/testdata/valid/benchttp.yaml new file mode 100644 index 0000000..2ee790c --- /dev/null +++ b/configparse/testdata/valid/benchttp.yaml @@ -0,0 +1,35 @@ +x-custom: &data + method: POST + url: http://localhost:9999?delay=200ms + +request: + <<: *data + queryParams: + fib: 30 + header: + key0: [val0, val1] + key1: [val0] + body: + type: raw + content: '{"key0":"val0","key1":"val1"}' + +runner: + requests: 100 + concurrency: 1 + interval: 50ms + requestTimeout: 2s + globalTimeout: 60s + +tests: + - name: minimum response time + field: ResponseTimes.Min + predicate: GT + target: 80ms + - name: maximum response time + field: ResponseTimes.Max + predicate: LTE + target: 120ms + - name: 100% availability + field: RequestFailureCount + predicate: EQ + target: 0 diff --git a/configparse/testdata/valid/benchttp.yml b/configparse/testdata/valid/benchttp.yml new file mode 100644 index 0000000..27a2fc9 --- /dev/null +++ b/configparse/testdata/valid/benchttp.yml @@ -0,0 +1,32 @@ +request: + method: POST + url: http://localhost:9999?delay=200ms + queryParams: + fib: 30 + header: + key0: [val0, val1] + key1: [val0] + body: + type: raw + content: '{"key0":"val0","key1":"val1"}' + +runner: + requests: 100 + concurrency: 1 + interval: 50ms + requestTimeout: 2s + globalTimeout: 60s + +tests: + - name: minimum response time + field: ResponseTimes.Min + predicate: GT + target: 80ms + - name: maximum response time + field: ResponseTimes.Max + predicate: LTE + target: 120ms + - name: 100% availability + field: RequestFailureCount + predicate: EQ + target: 0 From e137eb0a323b50b30ca8fbba62b8c2a702c8ef72 Mon Sep 17 00:00:00 2001 From: Gregory Albouy Date: Sat, 29 Oct 2022 08:41:59 +0200 Subject: [PATCH 27/53] refactor: rename package configparse -> configio --- README.md | 6 ++--- {configparse => configio}/error.go | 2 +- {configparse => configio}/file.go | 2 +- {configparse => configio}/file_test.go | 24 +++++++++---------- {configparse => configio}/find.go | 2 +- {configparse => configio}/find_test.go | 12 +++++----- {configparse => configio}/json.go | 2 +- {configparse => configio}/json_test.go | 6 ++--- {configparse => configio}/parse.go | 6 ++--- {configparse => configio}/parser.go | 2 +- {configparse => configio}/parser_json.go | 2 +- {configparse => configio}/parser_json_test.go | 8 +++---- {configparse => configio}/parser_yaml.go | 2 +- {configparse => configio}/parser_yaml_test.go | 8 +++---- .../testdata/extends/extends-circular-0.yml | 0 .../testdata/extends/extends-circular-1.yml | 0 .../testdata/extends/extends-circular-2.yml | 0 .../extends/extends-circular-self.yml | 0 .../testdata/extends/extends-valid-child.yml | 0 .../testdata/extends/extends-valid-parent.yml | 0 .../nest-0/nest-1/extends-valid-nested.yml | 0 .../testdata/invalid/badext.yams | 0 .../testdata/invalid/badfields.json | 0 .../testdata/invalid/badfields.yml | 0 .../testdata/valid/benchttp-zeros.yml | 0 .../testdata/valid/benchttp.json | 0 .../testdata/valid/benchttp.yaml | 0 .../testdata/valid/benchttp.yml | 0 28 files changed, 42 insertions(+), 42 deletions(-) rename {configparse => configio}/error.go (97%) rename {configparse => configio}/file.go (99%) rename {configparse => configio}/file_test.go (92%) rename {configparse => configio}/find.go (96%) rename {configparse => configio}/find_test.go (70%) rename {configparse => configio}/json.go (93%) rename {configparse => configio}/json_test.go (94%) rename {configparse => configio}/parse.go (97%) rename {configparse => configio}/parser.go (97%) rename {configparse => configio}/parser_json.go (98%) rename {configparse => configio}/parser_json_test.go (90%) rename {configparse => configio}/parser_yaml.go (99%) rename {configparse => configio}/parser_yaml_test.go (93%) rename {configparse => configio}/testdata/extends/extends-circular-0.yml (100%) rename {configparse => configio}/testdata/extends/extends-circular-1.yml (100%) rename {configparse => configio}/testdata/extends/extends-circular-2.yml (100%) rename {configparse => configio}/testdata/extends/extends-circular-self.yml (100%) rename {configparse => configio}/testdata/extends/extends-valid-child.yml (100%) rename {configparse => configio}/testdata/extends/extends-valid-parent.yml (100%) rename {configparse => configio}/testdata/extends/nest-0/nest-1/extends-valid-nested.yml (100%) rename {configparse => configio}/testdata/invalid/badext.yams (100%) rename {configparse => configio}/testdata/invalid/badfields.json (100%) rename {configparse => configio}/testdata/invalid/badfields.yml (100%) rename {configparse => configio}/testdata/valid/benchttp-zeros.yml (100%) rename {configparse => configio}/testdata/valid/benchttp.json (100%) rename {configparse => configio}/testdata/valid/benchttp.yaml (100%) rename {configparse => configio}/testdata/valid/benchttp.yml (100%) diff --git a/README.md b/README.md index eaf4ad0..5641f4a 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ func main() { } ``` -### Usage with JSON config via `configparse` +### Usage with JSON config via `configio` ```go package main @@ -65,7 +65,7 @@ import ( "fmt" "github.com/benchttp/sdk/benchttp" - "github.com/benchttp/sdk/configparse" + "github.com/benchttp/sdk/configio" ) func main() { @@ -81,7 +81,7 @@ func main() { runner := benchttp.DefaultRunner() // Parse the json configuration into the Runner - _ = configparse.JSON(jsonConfig, &runner) + _ = configio.JSON(jsonConfig, &runner) // Run benchmark, retrieve report report, _ := runner.Run(context.Background()) diff --git a/configparse/error.go b/configio/error.go similarity index 97% rename from configparse/error.go rename to configio/error.go index 4a053e4..258bcff 100644 --- a/configparse/error.go +++ b/configio/error.go @@ -1,4 +1,4 @@ -package configparse +package configio import ( "errors" diff --git a/configparse/file.go b/configio/file.go similarity index 99% rename from configparse/file.go rename to configio/file.go index ae891ea..46fbdfc 100644 --- a/configparse/file.go +++ b/configio/file.go @@ -1,4 +1,4 @@ -package configparse +package configio import ( "errors" diff --git a/configparse/file_test.go b/configio/file_test.go similarity index 92% rename from configparse/file_test.go rename to configio/file_test.go index 8c3eeb7..4b34d6f 100644 --- a/configparse/file_test.go +++ b/configio/file_test.go @@ -1,4 +1,4 @@ -package configparse_test +package configio_test import ( "bytes" @@ -14,7 +14,7 @@ import ( "time" "github.com/benchttp/sdk/benchttp" - "github.com/benchttp/sdk/configparse" + "github.com/benchttp/sdk/configio" ) const ( @@ -39,39 +39,39 @@ func TestParse(t *testing.T) { { label: "not found", path: configPath("invalid/bad path"), - expErr: configparse.ErrFileNotFound, + expErr: configio.ErrFileNotFound, }, { label: "unsupported extension", path: configPath("invalid/badext.yams"), - expErr: configparse.ErrFileExt, + expErr: configio.ErrFileExt, }, { label: "yaml invalid fields", path: configPath("invalid/badfields.yml"), - expErr: configparse.ErrFileParse, + expErr: configio.ErrFileParse, }, { label: "json invalid fields", path: configPath("invalid/badfields.json"), - expErr: configparse.ErrFileParse, + expErr: configio.ErrFileParse, }, { label: "self reference", path: configPath("extends/extends-circular-self.yml"), - expErr: configparse.ErrFileCircular, + expErr: configio.ErrFileCircular, }, { label: "circular reference", path: configPath("extends/extends-circular-0.yml"), - expErr: configparse.ErrFileCircular, + expErr: configio.ErrFileCircular, }, } for _, tc := range testcases { t.Run(tc.label, func(t *testing.T) { runner := benchttp.Runner{} - gotErr := configparse.Parse(tc.path, &runner) + gotErr := configio.Parse(tc.path, &runner) if gotErr == nil { t.Fatal("exp non-nil error, got nil") @@ -94,7 +94,7 @@ func TestParse(t *testing.T) { fname := configPath("valid/benchttp" + ext) gotCfg := benchttp.Runner{} - if err := configparse.Parse(fname, &gotCfg); err != nil { + if err := configio.Parse(fname, &gotCfg); err != nil { // critical error, stop the test t.Fatal(err) } @@ -117,7 +117,7 @@ func TestParse(t *testing.T) { fname := configPath("valid/benchttp-zeros.yml") - if err := configparse.Parse(fname, &runner); err != nil { + if err := configio.Parse(fname, &runner); err != nil { t.Fatal(err) } @@ -166,7 +166,7 @@ func TestParse(t *testing.T) { for _, tc := range testcases { t.Run(tc.label, func(t *testing.T) { var runner benchttp.Runner - if err := configparse.Parse(tc.cfpath, &runner); err != nil { + if err := configio.Parse(tc.cfpath, &runner); err != nil { t.Fatal(err) } diff --git a/configparse/find.go b/configio/find.go similarity index 96% rename from configparse/find.go rename to configio/find.go index a4f1bcc..3c0654d 100644 --- a/configparse/find.go +++ b/configio/find.go @@ -1,4 +1,4 @@ -package configparse +package configio import "os" diff --git a/configparse/find_test.go b/configio/find_test.go similarity index 70% rename from configparse/find_test.go rename to configio/find_test.go index 1bf45b4..5467c45 100644 --- a/configparse/find_test.go +++ b/configio/find_test.go @@ -1,9 +1,9 @@ -package configparse_test +package configio_test import ( "testing" - "github.com/benchttp/sdk/configparse" + "github.com/benchttp/sdk/configio" ) var ( @@ -16,15 +16,15 @@ func TestFind(t *testing.T) { t.Run("return first existing file form input", func(t *testing.T) { files := []string{badFile, goodFileYML, goodFileJSON} - if got := configparse.Find(files...); got != goodFileYML { + if got := configio.Find(files...); got != goodFileYML { t.Errorf("did not retrieve good file: exp %s, got %s", goodFileYML, got) } }) t.Run("return first existing file from defaults", func(t *testing.T) { - configparse.DefaultPaths = []string{badFile, goodFileYML, goodFileJSON} + configio.DefaultPaths = []string{badFile, goodFileYML, goodFileJSON} - if got := configparse.Find(); got != goodFileYML { + if got := configio.Find(); got != goodFileYML { t.Errorf("did not retrieve good file: exp %s, got %s", goodFileYML, got) } }) @@ -32,7 +32,7 @@ func TestFind(t *testing.T) { t.Run("return empty string when no match", func(t *testing.T) { files := []string{badFile} - if got := configparse.Find(files...); got != "" { + if got := configio.Find(files...); got != "" { t.Errorf("retrieved unexpected file: %s", got) } }) diff --git a/configparse/json.go b/configio/json.go similarity index 93% rename from configparse/json.go rename to configio/json.go index c255f24..8c4b608 100644 --- a/configparse/json.go +++ b/configio/json.go @@ -1,4 +1,4 @@ -package configparse +package configio import "github.com/benchttp/sdk/benchttp" diff --git a/configparse/json_test.go b/configio/json_test.go similarity index 94% rename from configparse/json_test.go rename to configio/json_test.go index 4707c98..db2d1d6 100644 --- a/configparse/json_test.go +++ b/configio/json_test.go @@ -1,4 +1,4 @@ -package configparse_test +package configio_test import ( "encoding/json" @@ -6,7 +6,7 @@ import ( "testing" "github.com/benchttp/sdk/benchttp" - "github.com/benchttp/sdk/configparse" + "github.com/benchttp/sdk/configio" ) func TestJSON(t *testing.T) { @@ -56,7 +56,7 @@ func TestJSON(t *testing.T) { for _, tc := range testcases { t.Run(tc.name, func(t *testing.T) { gotRunner := benchttp.DefaultRunner() - gotError := configparse.JSON(tc.input, &gotRunner) + gotError := configio.JSON(tc.input, &gotRunner) if !tc.isValidRunner(benchttp.DefaultRunner(), gotRunner) { t.Errorf("unexpected runner:\n%+v", gotRunner) diff --git a/configparse/parse.go b/configio/parse.go similarity index 97% rename from configparse/parse.go rename to configio/parse.go index 54e3938..7404b23 100644 --- a/configparse/parse.go +++ b/configio/parse.go @@ -1,4 +1,4 @@ -package configparse +package configio import ( "bytes" @@ -73,7 +73,7 @@ func (repr Representation) parseRequestInto(dst *benchttp.Runner) error { if rawURL := repr.Request.URL; rawURL != nil { parsedURL, err := parseAndBuildURL(*rawURL, repr.Request.QueryParams) if err != nil { - return fmt.Errorf(`configparse: invalid url: %q`, *rawURL) + return fmt.Errorf(`configio: invalid url: %q`, *rawURL) } dst.Request.URL = parsedURL } @@ -91,7 +91,7 @@ func (repr Representation) parseRequestInto(dst *benchttp.Runner) error { case "raw": dst.Request.Body = io.NopCloser(bytes.NewReader([]byte(body.Content))) default: - return errors.New(`configparse: request.body.type: only "raw" accepted`) + return errors.New(`configio: request.body.type: only "raw" accepted`) } } diff --git a/configparse/parser.go b/configio/parser.go similarity index 97% rename from configparse/parser.go rename to configio/parser.go index 585f7d2..af2f673 100644 --- a/configparse/parser.go +++ b/configio/parser.go @@ -1,4 +1,4 @@ -package configparse +package configio import ( "errors" diff --git a/configparse/parser_json.go b/configio/parser_json.go similarity index 98% rename from configparse/parser_json.go rename to configio/parser_json.go index ff7c64b..ff3d020 100644 --- a/configparse/parser_json.go +++ b/configio/parser_json.go @@ -1,4 +1,4 @@ -package configparse +package configio import ( "bytes" diff --git a/configparse/parser_json_test.go b/configio/parser_json_test.go similarity index 90% rename from configparse/parser_json_test.go rename to configio/parser_json_test.go index 85deaae..460577b 100644 --- a/configparse/parser_json_test.go +++ b/configio/parser_json_test.go @@ -1,9 +1,9 @@ -package configparse_test +package configio_test import ( "testing" - "github.com/benchttp/sdk/configparse" + "github.com/benchttp/sdk/configio" ) func TestJSONParser(t *testing.T) { @@ -38,8 +38,8 @@ func TestJSONParser(t *testing.T) { for _, tc := range testcases { t.Run(tc.label, func(t *testing.T) { var ( - parser configparse.JSONParser - rawcfg configparse.Representation + parser configio.JSONParser + rawcfg configio.Representation ) gotErr := parser.Parse(tc.in, &rawcfg) diff --git a/configparse/parser_yaml.go b/configio/parser_yaml.go similarity index 99% rename from configparse/parser_yaml.go rename to configio/parser_yaml.go index 160cc7b..6b13bc1 100644 --- a/configparse/parser_yaml.go +++ b/configio/parser_yaml.go @@ -1,4 +1,4 @@ -package configparse +package configio import ( "bytes" diff --git a/configparse/parser_yaml_test.go b/configio/parser_yaml_test.go similarity index 93% rename from configparse/parser_yaml_test.go rename to configio/parser_yaml_test.go index 4b6ca8b..6eacb9e 100644 --- a/configparse/parser_yaml_test.go +++ b/configio/parser_yaml_test.go @@ -1,4 +1,4 @@ -package configparse_test +package configio_test import ( "errors" @@ -7,7 +7,7 @@ import ( "gopkg.in/yaml.v3" - "github.com/benchttp/sdk/configparse" + "github.com/benchttp/sdk/configio" ) func TestYAMLParser(t *testing.T) { @@ -65,8 +65,8 @@ func TestYAMLParser(t *testing.T) { for _, tc := range testcases { t.Run(tc.label, func(t *testing.T) { var ( - parser configparse.YAMLParser - rawcfg configparse.Representation + parser configio.YAMLParser + rawcfg configio.Representation yamlErr *yaml.TypeError ) diff --git a/configparse/testdata/extends/extends-circular-0.yml b/configio/testdata/extends/extends-circular-0.yml similarity index 100% rename from configparse/testdata/extends/extends-circular-0.yml rename to configio/testdata/extends/extends-circular-0.yml diff --git a/configparse/testdata/extends/extends-circular-1.yml b/configio/testdata/extends/extends-circular-1.yml similarity index 100% rename from configparse/testdata/extends/extends-circular-1.yml rename to configio/testdata/extends/extends-circular-1.yml diff --git a/configparse/testdata/extends/extends-circular-2.yml b/configio/testdata/extends/extends-circular-2.yml similarity index 100% rename from configparse/testdata/extends/extends-circular-2.yml rename to configio/testdata/extends/extends-circular-2.yml diff --git a/configparse/testdata/extends/extends-circular-self.yml b/configio/testdata/extends/extends-circular-self.yml similarity index 100% rename from configparse/testdata/extends/extends-circular-self.yml rename to configio/testdata/extends/extends-circular-self.yml diff --git a/configparse/testdata/extends/extends-valid-child.yml b/configio/testdata/extends/extends-valid-child.yml similarity index 100% rename from configparse/testdata/extends/extends-valid-child.yml rename to configio/testdata/extends/extends-valid-child.yml diff --git a/configparse/testdata/extends/extends-valid-parent.yml b/configio/testdata/extends/extends-valid-parent.yml similarity index 100% rename from configparse/testdata/extends/extends-valid-parent.yml rename to configio/testdata/extends/extends-valid-parent.yml diff --git a/configparse/testdata/extends/nest-0/nest-1/extends-valid-nested.yml b/configio/testdata/extends/nest-0/nest-1/extends-valid-nested.yml similarity index 100% rename from configparse/testdata/extends/nest-0/nest-1/extends-valid-nested.yml rename to configio/testdata/extends/nest-0/nest-1/extends-valid-nested.yml diff --git a/configparse/testdata/invalid/badext.yams b/configio/testdata/invalid/badext.yams similarity index 100% rename from configparse/testdata/invalid/badext.yams rename to configio/testdata/invalid/badext.yams diff --git a/configparse/testdata/invalid/badfields.json b/configio/testdata/invalid/badfields.json similarity index 100% rename from configparse/testdata/invalid/badfields.json rename to configio/testdata/invalid/badfields.json diff --git a/configparse/testdata/invalid/badfields.yml b/configio/testdata/invalid/badfields.yml similarity index 100% rename from configparse/testdata/invalid/badfields.yml rename to configio/testdata/invalid/badfields.yml diff --git a/configparse/testdata/valid/benchttp-zeros.yml b/configio/testdata/valid/benchttp-zeros.yml similarity index 100% rename from configparse/testdata/valid/benchttp-zeros.yml rename to configio/testdata/valid/benchttp-zeros.yml diff --git a/configparse/testdata/valid/benchttp.json b/configio/testdata/valid/benchttp.json similarity index 100% rename from configparse/testdata/valid/benchttp.json rename to configio/testdata/valid/benchttp.json diff --git a/configparse/testdata/valid/benchttp.yaml b/configio/testdata/valid/benchttp.yaml similarity index 100% rename from configparse/testdata/valid/benchttp.yaml rename to configio/testdata/valid/benchttp.yaml diff --git a/configparse/testdata/valid/benchttp.yml b/configio/testdata/valid/benchttp.yml similarity index 100% rename from configparse/testdata/valid/benchttp.yml rename to configio/testdata/valid/benchttp.yml From 346ef659790ea041786f2be0b197ca2ba7693d0b Mon Sep 17 00:00:00 2001 From: Gregory Albouy Date: Sat, 29 Oct 2022 11:18:23 +0200 Subject: [PATCH 28/53] refactor(configio): adapt namings to new package name --- configio/file.go | 12 ++++----- configio/file_test.go | 58 ++++++++++++++++--------------------------- configio/find.go | 4 +-- configio/find_test.go | 8 +++--- configio/json.go | 7 +++--- configio/json_test.go | 2 +- configio/parse.go | 4 +-- 7 files changed, 40 insertions(+), 55 deletions(-) diff --git a/configio/file.go b/configio/file.go index 46fbdfc..316d7b5 100644 --- a/configio/file.go +++ b/configio/file.go @@ -10,16 +10,16 @@ import ( "github.com/benchttp/sdk/internal/errorutil" ) -// Parse parses given filename as a benchttp runner configuration +// UnmarshalFile parses given filename as a benchttp runner configuration // into a runner.Runner and stores the retrieved values into *dst. // It returns the first error occurring in the process, which can be // any of the values declared in the package. -func Parse(filename string, dst *benchttp.Runner) (err error) { +func UnmarshalFile(filename string, dst *benchttp.Runner) (err error) { reprs, err := parseFileRecursive(filename, []Representation{}, set{}) if err != nil { return } - return parseAndMergeConfigs(reprs, dst) + return parseAndMergeReprs(reprs, dst) } // set is a collection of unique string values. @@ -91,12 +91,12 @@ func parseFile(filename string) (repr Representation, err error) { return repr, nil } -// parseAndMergeConfigs iterates backwards over reprs, parses them as +// parseAndMergeReprs iterates backwards over reprs, parses them as // runner.Runner, merges them successively and finally stores the result // into dst. // It returns the merged result or the first non-nil error occurring in the // process. -func parseAndMergeConfigs(reprs []Representation, dst *benchttp.Runner) error { +func parseAndMergeReprs(reprs []Representation, dst *benchttp.Runner) error { if len(reprs) == 0 { // supposedly catched upstream, should not occur return errors.New( "an unacceptable error occurred parsing the config file, " + @@ -106,7 +106,7 @@ func parseAndMergeConfigs(reprs []Representation, dst *benchttp.Runner) error { } for i := len(reprs) - 1; i >= 0; i-- { - if err := reprs[i].ParseInto(dst); err != nil { + if err := reprs[i].Into(dst); err != nil { return errorutil.WithDetails(ErrFileParse, err) } } diff --git a/configio/file_test.go b/configio/file_test.go index 4b34d6f..d7a14cc 100644 --- a/configio/file_test.go +++ b/configio/file_test.go @@ -4,7 +4,6 @@ import ( "bytes" "errors" "fmt" - "io" "net/http" "net/http/httptest" "net/url" @@ -28,8 +27,7 @@ var supportedExt = []string{ ".json", } -// TestParse ensures the config file is open, read, and correctly parsed. -func TestParse(t *testing.T) { +func TestUnmarshalFile(t *testing.T) { t.Run("return file errors early", func(t *testing.T) { testcases := []struct { label string @@ -71,7 +69,7 @@ func TestParse(t *testing.T) { for _, tc := range testcases { t.Run(tc.label, func(t *testing.T) { runner := benchttp.Runner{} - gotErr := configio.Parse(tc.path, &runner) + gotErr := configio.UnmarshalFile(tc.path, &runner) if gotErr == nil { t.Fatal("exp non-nil error, got nil") @@ -81,7 +79,7 @@ func TestParse(t *testing.T) { t.Errorf("\nexp %v\ngot %v", tc.expErr, gotErr) } - if !sameConfig(runner, benchttp.Runner{}) { + if !sameRunner(runner, benchttp.Runner{}) { t.Errorf("\nexp empty config\ngot %v", runner) } }) @@ -90,34 +88,33 @@ func TestParse(t *testing.T) { t.Run("happy path for all extensions", func(t *testing.T) { for _, ext := range supportedExt { - expCfg := newExpConfig() - fname := configPath("valid/benchttp" + ext) + filename := configPath("valid/benchttp" + ext) + runner := benchttp.Runner{} - gotCfg := benchttp.Runner{} - if err := configio.Parse(fname, &gotCfg); err != nil { + if err := configio.UnmarshalFile(filename, &runner); err != nil { // critical error, stop the test t.Fatal(err) } - if sameConfig(gotCfg, benchttp.Runner{}) { + if sameRunner(runner, benchttp.Runner{}) { t.Error("received an empty configuration") } - if !sameConfig(gotCfg, expCfg) { - t.Errorf("unexpected parsed config for %s file:\nexp %#v\ngot %#v", ext, expCfg, gotCfg) + exp := expectedRunner() + if !sameRunner(runner, exp) { + t.Errorf("unexpected parsed config for %s file:\nexp %#v\ngot %#v", ext, exp, runner) } } }) t.Run("override input config", func(t *testing.T) { + filename := configPath("valid/benchttp-zeros.yml") runner := benchttp.Runner{} runner.Request = httptest.NewRequest("POST", "https://overriden.com", nil) runner.GlobalTimeout = 10 * time.Millisecond - fname := configPath("valid/benchttp-zeros.yml") - - if err := configio.Parse(fname, &runner); err != nil { + if err := configio.UnmarshalFile(filename, &runner); err != nil { t.Fatal(err) } @@ -166,7 +163,7 @@ func TestParse(t *testing.T) { for _, tc := range testcases { t.Run(tc.label, func(t *testing.T) { var runner benchttp.Runner - if err := configio.Parse(tc.cfpath, &runner); err != nil { + if err := configio.UnmarshalFile(tc.cfpath, &runner); err != nil { t.Fatal(err) } @@ -189,9 +186,9 @@ func TestParse(t *testing.T) { // helpers -// newExpConfig returns the expected runner.ConfigConfig result after parsing -// one of the config files in testdataConfigPath. -func newExpConfig() benchttp.Runner { +// expectedRunner returns the expected benchttp.Runner after unmarhsaling +// one of the valid config files in testdata. +func expectedRunner() benchttp.Runner { request := httptest.NewRequest( "POST", validURL, @@ -233,13 +230,13 @@ func newExpConfig() benchttp.Runner { } } -func sameConfig(a, b benchttp.Runner) bool { +func sameRunner(a, b benchttp.Runner) bool { if a.Request == nil || b.Request == nil { return a.Request == nil && b.Request == nil } return sameURL(a.Request.URL, b.Request.URL) && - sameHeader(a.Request.Header, b.Request.Header) && - sameBody(a.Request.Body, b.Request.Body) + reflect.DeepEqual(a.Request.Header, b.Request.Header) && + reflect.DeepEqual(a.Request.Body, b.Request.Body) } // sameURL returns true if a and b are the same *url.URL, taking into account @@ -252,27 +249,14 @@ func sameURL(a, b *url.URL) bool { // temporarily set RawQuery to a determined value for _, u := range []*url.URL{a, b} { - defer setTempValue(&u.RawQuery, "replaced by test")() + restore := setTempValue(&u.RawQuery, "replaced by test") + defer restore() } // we can now rely on deep equality check return reflect.DeepEqual(a, b) } -func sameHeader(a, b http.Header) bool { - return reflect.DeepEqual(a, b) - // if len(a) != len(b) { - // return false - // } - // for k, values := range a { - // if len(values) != len() - // } -} - -func sameBody(a, b io.ReadCloser) bool { - return reflect.DeepEqual(a, b) -} - // setTempValue sets *ptr to val and returns a restore func that sets *ptr // back to its previous value. func setTempValue(ptr *string, val string) (restore func()) { diff --git a/configio/find.go b/configio/find.go index 3c0654d..2f15ec0 100644 --- a/configio/find.go +++ b/configio/find.go @@ -8,10 +8,10 @@ var DefaultPaths = []string{ "./.benchttp.json", } -// Find returns the first name that matches a file path. +// FindFile returns the first name that matches a file path. // If input paths is empty, it uses DefaultPaths. // If no match is found, it returns an empty string. -func Find(paths ...string) string { +func FindFile(paths ...string) string { if len(paths) == 0 { paths = DefaultPaths } diff --git a/configio/find_test.go b/configio/find_test.go index 5467c45..82c1240 100644 --- a/configio/find_test.go +++ b/configio/find_test.go @@ -12,11 +12,11 @@ var ( badFile = configPath("does-not-exist.json") ) -func TestFind(t *testing.T) { +func TestFindFile(t *testing.T) { t.Run("return first existing file form input", func(t *testing.T) { files := []string{badFile, goodFileYML, goodFileJSON} - if got := configio.Find(files...); got != goodFileYML { + if got := configio.FindFile(files...); got != goodFileYML { t.Errorf("did not retrieve good file: exp %s, got %s", goodFileYML, got) } }) @@ -24,7 +24,7 @@ func TestFind(t *testing.T) { t.Run("return first existing file from defaults", func(t *testing.T) { configio.DefaultPaths = []string{badFile, goodFileYML, goodFileJSON} - if got := configio.Find(); got != goodFileYML { + if got := configio.FindFile(); got != goodFileYML { t.Errorf("did not retrieve good file: exp %s, got %s", goodFileYML, got) } }) @@ -32,7 +32,7 @@ func TestFind(t *testing.T) { t.Run("return empty string when no match", func(t *testing.T) { files := []string{badFile} - if got := configio.Find(files...); got != "" { + if got := configio.FindFile(files...); got != "" { t.Errorf("retrieved unexpected file: %s", got) } }) diff --git a/configio/json.go b/configio/json.go index 8c4b608..811391d 100644 --- a/configio/json.go +++ b/configio/json.go @@ -2,11 +2,12 @@ package configio import "github.com/benchttp/sdk/benchttp" -// JSON reads input bytes as JSON and unmarshals it into a benchttp.Runner. -func JSON(in []byte, dst *benchttp.Runner) error { +// UnmarshalJSONRunner parses the JSON-encoded data and stores the result +// in the benchttp.Runner pointed to by dst. +func UnmarshalJSONRunner(in []byte, dst *benchttp.Runner) error { repr := Representation{} if err := (JSONParser{}).Parse(in, &repr); err != nil { return err } - return repr.ParseInto(dst) + return repr.Into(dst) } diff --git a/configio/json_test.go b/configio/json_test.go index db2d1d6..e5b5bea 100644 --- a/configio/json_test.go +++ b/configio/json_test.go @@ -56,7 +56,7 @@ func TestJSON(t *testing.T) { for _, tc := range testcases { t.Run(tc.name, func(t *testing.T) { gotRunner := benchttp.DefaultRunner() - gotError := configio.JSON(tc.input, &gotRunner) + gotError := configio.UnmarshalJSONRunner(tc.input, &gotRunner) if !tc.isValidRunner(benchttp.DefaultRunner(), gotRunner) { t.Errorf("unexpected runner:\n%+v", gotRunner) diff --git a/configio/parse.go b/configio/parse.go index 7404b23..0c5f009 100644 --- a/configio/parse.go +++ b/configio/parse.go @@ -48,10 +48,10 @@ type Representation struct { } `yaml:"tests" json:"tests"` } -// ParseInto parses the Representation receiver as a benchttp.Runner +// Into parses the Representation receiver as a benchttp.Runner // and stores any non-nil field value into the corresponding field // of dst. -func (repr Representation) ParseInto(dst *benchttp.Runner) error { +func (repr Representation) Into(dst *benchttp.Runner) error { if err := repr.parseRequestInto(dst); err != nil { return err } From 6e9e98af795e4db43da1d04d73e852083fbb8ef4 Mon Sep 17 00:00:00 2001 From: Gregory Albouy Date: Sat, 29 Oct 2022 11:29:40 +0200 Subject: [PATCH 29/53] refactor(configio): move FindFile in file.go --- configio/file.go | 21 +++++++++++++++++++ configio/file_test.go | 47 ++++++++++++++++++++++++++++++++++--------- configio/find.go | 24 ---------------------- configio/find_test.go | 39 ----------------------------------- 4 files changed, 58 insertions(+), 73 deletions(-) delete mode 100644 configio/find.go delete mode 100644 configio/find_test.go diff --git a/configio/file.go b/configio/file.go index 316d7b5..d296672 100644 --- a/configio/file.go +++ b/configio/file.go @@ -10,6 +10,27 @@ import ( "github.com/benchttp/sdk/internal/errorutil" ) +var DefaultPaths = []string{ + "./.benchttp.yml", + "./.benchttp.yaml", + "./.benchttp.json", +} + +// FindFile returns the first name that matches a file path. +// If input paths is empty, it uses DefaultPaths. +// If no match is found, it returns an empty string. +func FindFile(paths ...string) string { + if len(paths) == 0 { + paths = DefaultPaths + } + for _, path := range paths { + if _, err := os.Stat(path); err == nil { // err IS nil: file exists + return path + } + } + return "" +} + // UnmarshalFile parses given filename as a benchttp runner configuration // into a runner.Runner and stores the retrieved values into *dst. // It returns the first error occurring in the process, which can be diff --git a/configio/file_test.go b/configio/file_test.go index d7a14cc..2b92d6d 100644 --- a/configio/file_test.go +++ b/configio/file_test.go @@ -16,18 +16,45 @@ import ( "github.com/benchttp/sdk/configio" ) -const ( - validConfigPath = "./testdata" - validURL = "http://localhost:9999?fib=30&delay=200ms" // value from testdata files -) +func TestFindFile(t *testing.T) { + var ( + fileYAML = configPath("valid/benchttp.yaml") + fileJSON = configPath("valid/benchttp.json") + nofile = configPath("does-not-exist.json") + ) + + t.Run("return first existing file from input", func(t *testing.T) { + files := []string{nofile, fileYAML, fileJSON} + + if got := configio.FindFile(files...); got != fileYAML { + t.Errorf("did not retrieve good file: exp %s, got %s", fileYAML, got) + } + }) -var supportedExt = []string{ - ".yml", - ".yaml", - ".json", + t.Run("return first existing file from defaults", func(t *testing.T) { + configio.DefaultPaths = []string{nofile, fileYAML, fileJSON} + + if got := configio.FindFile(); got != fileYAML { + t.Errorf("did not retrieve good file: exp %s, got %s", fileYAML, got) + } + }) + + t.Run("return empty string when no match", func(t *testing.T) { + files := []string{nofile} + + if got := configio.FindFile(files...); got != "" { + t.Errorf("retrieved unexpected file: %s", got) + } + }) } func TestUnmarshalFile(t *testing.T) { + supportedExt := []string{ + ".yml", + ".yaml", + ".json", + } + t.Run("return file errors early", func(t *testing.T) { testcases := []struct { label string @@ -191,7 +218,7 @@ func TestUnmarshalFile(t *testing.T) { func expectedRunner() benchttp.Runner { request := httptest.NewRequest( "POST", - validURL, + "http://localhost:9999?fib=30&delay=200ms", bytes.NewReader([]byte(`{"key0":"val0","key1":"val1"}`)), ) request.Header = http.Header{ @@ -268,5 +295,5 @@ func setTempValue(ptr *string, val string) (restore func()) { } func configPath(name string) string { - return filepath.Join(validConfigPath, name) + return filepath.Join("testdata", name) } diff --git a/configio/find.go b/configio/find.go deleted file mode 100644 index 2f15ec0..0000000 --- a/configio/find.go +++ /dev/null @@ -1,24 +0,0 @@ -package configio - -import "os" - -var DefaultPaths = []string{ - "./.benchttp.yml", - "./.benchttp.yaml", - "./.benchttp.json", -} - -// FindFile returns the first name that matches a file path. -// If input paths is empty, it uses DefaultPaths. -// If no match is found, it returns an empty string. -func FindFile(paths ...string) string { - if len(paths) == 0 { - paths = DefaultPaths - } - for _, path := range paths { - if _, err := os.Stat(path); err == nil { // err IS nil: file exists - return path - } - } - return "" -} diff --git a/configio/find_test.go b/configio/find_test.go deleted file mode 100644 index 82c1240..0000000 --- a/configio/find_test.go +++ /dev/null @@ -1,39 +0,0 @@ -package configio_test - -import ( - "testing" - - "github.com/benchttp/sdk/configio" -) - -var ( - goodFileYML = configPath("valid/benchttp.yml") - goodFileJSON = configPath("valid/benchttp.json") - badFile = configPath("does-not-exist.json") -) - -func TestFindFile(t *testing.T) { - t.Run("return first existing file form input", func(t *testing.T) { - files := []string{badFile, goodFileYML, goodFileJSON} - - if got := configio.FindFile(files...); got != goodFileYML { - t.Errorf("did not retrieve good file: exp %s, got %s", goodFileYML, got) - } - }) - - t.Run("return first existing file from defaults", func(t *testing.T) { - configio.DefaultPaths = []string{badFile, goodFileYML, goodFileJSON} - - if got := configio.FindFile(); got != goodFileYML { - t.Errorf("did not retrieve good file: exp %s, got %s", goodFileYML, got) - } - }) - - t.Run("return empty string when no match", func(t *testing.T) { - files := []string{badFile} - - if got := configio.FindFile(files...); got != "" { - t.Errorf("retrieved unexpected file: %s", got) - } - }) -} From 1784b2b7c1d47a9f0f3140df3cee215aa5743a55 Mon Sep 17 00:00:00 2001 From: Gregory Albouy Date: Sat, 29 Oct 2022 13:33:45 +0200 Subject: [PATCH 30/53] feat(configio): Decoders and Unmarshal funcs Rethink exposed API so it matches the native json lib. - replace Parse -> Decode in all declarations and file names - expose top-level unmarshaling & decoding functions: - UnmarshalJSON, UnmarshalYAML: unmarshal bytes into *Representation - UnmarshalJSONRunner, UnmarshalYAMLRunner: same into *benchttp.Runner - NewJSONDecoder, NewYAMLDecoder: same as json.NewDecoder - DecoderOf: returns appropriate Decoder given an extension --- README.md | 2 +- configio/decoder.go | 35 ++++++++ configio/file.go | 6 +- configio/json.go | 88 ++++++++++++++++++- configio/json_test.go | 60 ++++++++++++- configio/parser.go | 33 ------- configio/parser_json.go | 65 -------------- configio/parser_json_test.go | 63 ------------- configio/{parser_yaml.go => yaml.go} | 55 +++++++++--- .../{parser_yaml_test.go => yaml_test.go} | 14 +-- 10 files changed, 233 insertions(+), 188 deletions(-) create mode 100644 configio/decoder.go delete mode 100644 configio/parser.go delete mode 100644 configio/parser_json.go delete mode 100644 configio/parser_json_test.go rename configio/{parser_yaml.go => yaml.go} (63%) rename configio/{parser_yaml_test.go => yaml_test.go} (87%) diff --git a/README.md b/README.md index 5641f4a..00fc294 100644 --- a/README.md +++ b/README.md @@ -81,7 +81,7 @@ func main() { runner := benchttp.DefaultRunner() // Parse the json configuration into the Runner - _ = configio.JSON(jsonConfig, &runner) + _ = configio.UnmarshalJSONRunner(jsonConfig, &runner) // Run benchmark, retrieve report report, _ := runner.Run(context.Background()) diff --git a/configio/decoder.go b/configio/decoder.go new file mode 100644 index 0000000..0a93511 --- /dev/null +++ b/configio/decoder.go @@ -0,0 +1,35 @@ +package configio + +import ( + "bytes" + "errors" + + "github.com/benchttp/sdk/benchttp" +) + +type Decoder interface { + Decode(dst *Representation) error + DecodeRunner(dst *benchttp.Runner) error +} + +type Extension string + +const ( + ExtYML Extension = ".yml" + ExtYAML Extension = ".yaml" + ExtJSON Extension = ".json" +) + +// DecoderOf returns the appropriate Decoder for the given extension, +// or a non-nil error if ext is not an expected extension. +func DecoderOf(ext Extension, in []byte) (Decoder, error) { + r := bytes.NewReader(in) + switch ext { + case ExtYML, ExtYAML: + return NewYAMLDecoder(r), nil + case ExtJSON: + return NewJSONDecoder(r), nil + default: + return nil, errors.New("unsupported config format") + } +} diff --git a/configio/file.go b/configio/file.go index d296672..fb5c667 100644 --- a/configio/file.go +++ b/configio/file.go @@ -99,13 +99,13 @@ func parseFile(filename string) (repr Representation, err error) { return repr, errorutil.WithDetails(ErrFileRead, filename, err) } - ext := extension(filepath.Ext(filename)) - parser, err := newParser(ext) + ext := Extension(filepath.Ext(filename)) + dec, err := DecoderOf(ext, b) if err != nil { return repr, errorutil.WithDetails(ErrFileExt, ext, err) } - if err = parser.Parse(b, &repr); err != nil { + if err = dec.Decode(&repr); err != nil { return repr, errorutil.WithDetails(ErrFileParse, filename, err) } diff --git a/configio/json.go b/configio/json.go index 811391d..debd94e 100644 --- a/configio/json.go +++ b/configio/json.go @@ -1,13 +1,97 @@ package configio -import "github.com/benchttp/sdk/benchttp" +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "io" + "regexp" + + "github.com/benchttp/sdk/benchttp" +) + +// UnmarshalJSON parses the JSON-encoded data and stores the result +// in the Representation pointed to by dst. +func UnmarshalJSON(in []byte, dst *Representation) error { + dec := NewJSONDecoder(bytes.NewReader(in)) + return dec.Decode(dst) +} // UnmarshalJSONRunner parses the JSON-encoded data and stores the result // in the benchttp.Runner pointed to by dst. func UnmarshalJSONRunner(in []byte, dst *benchttp.Runner) error { + dec := NewJSONDecoder(bytes.NewReader(in)) + return dec.DecodeRunner(dst) +} + +// JSONDecoder implements Decoder +type JSONDecoder struct{ r io.Reader } + +func NewJSONDecoder(r io.Reader) JSONDecoder { + return JSONDecoder{r: r} +} + +// Decode reads the next JSON-encoded value from its input +// and stores it in the Representation pointed to by dst. +func (d JSONDecoder) Decode(dst *Representation) error { + decoder := json.NewDecoder(d.r) + decoder.DisallowUnknownFields() + return d.handleError(decoder.Decode(dst)) +} + +// Decode reads the next JSON-encoded value from its input +// and stores it in the benchttp.Runner pointed to by dst. +func (d JSONDecoder) DecodeRunner(dst *benchttp.Runner) error { repr := Representation{} - if err := (JSONParser{}).Parse(in, &repr); err != nil { + if err := d.Decode(&repr); err != nil { return err } return repr.Into(dst) } + +// handleError handles an error from package json, +// transforms it into a user-friendly standardized format +// and returns the resulting error. +func (d JSONDecoder) handleError(err error) error { + if err == nil { + return nil + } + + // handle syntax error + var errSyntax *json.SyntaxError + if errors.As(err, &errSyntax) { + return fmt.Errorf("syntax error near %d: %w", errSyntax.Offset, err) + } + + // handle type error + var errType *json.UnmarshalTypeError + if errors.As(err, &errType) { + return fmt.Errorf( + "wrong type for field %s: want %s, got %s", + errType.Field, errType.Type, errType.Value, + ) + } + + // handle unknown field error + if field := d.parseUnknownFieldError(err.Error()); field != "" { + return fmt.Errorf(`invalid field ("%s"): does not exist`, field) + } + + return err +} + +// parseJSONUnknownFieldError parses the raw string as a json error +// from an unknown field and returns the field name. +// If the raw string is not an unknown field error, it returns "". +func (d JSONDecoder) parseUnknownFieldError(raw string) (field string) { + unknownFieldRgx := regexp.MustCompile( + // raw output example: + // json: unknown field "notafield" + `json: unknown field "(\S+)"`, + ) + if matches := unknownFieldRgx.FindStringSubmatch(raw); len(matches) >= 2 { + return matches[1] + } + return "" +} diff --git a/configio/json_test.go b/configio/json_test.go index e5b5bea..c04bba2 100644 --- a/configio/json_test.go +++ b/configio/json_test.go @@ -1,6 +1,7 @@ package configio_test import ( + "bytes" "encoding/json" "errors" "testing" @@ -9,7 +10,7 @@ import ( "github.com/benchttp/sdk/configio" ) -func TestJSON(t *testing.T) { +func TestMarshalJSON(t *testing.T) { const testURL = "https://example.com" baseInput := object{ "request": object{ @@ -68,6 +69,61 @@ func TestJSON(t *testing.T) { } } +func TestJSONDecoder(t *testing.T) { + t.Run("return expected errors", func(t *testing.T) { + testcases := []struct { + label string + in []byte + exp string + }{ + { + label: "syntax error", + in: []byte("{\n \"runner\": {},\n}\n"), + exp: "syntax error near 19: invalid character '}' looking for beginning of object key string", + }, + { + label: "unknown field", + in: []byte("{\n \"notafield\": 123\n}\n"), + exp: `invalid field ("notafield"): does not exist`, + }, + { + label: "wrong type", + in: []byte("{\n \"runner\": {\n \"requests\": [123]\n }\n}\n"), + exp: "wrong type for field runner.requests: want int, got array", + }, + { + label: "valid config", + in: []byte("{\n \"runner\": {\n \"requests\": 123\n }\n}\n"), + exp: "", + }, + } + + for _, tc := range testcases { + t.Run(tc.label, func(t *testing.T) { + runner := benchttp.Runner{} + decoder := configio.NewJSONDecoder(bytes.NewReader(tc.in)) + + gotErr := decoder.DecodeRunner(&runner) + + if tc.exp == "" { + if gotErr != nil { + t.Fatalf("unexpected error: %v", gotErr) + } + return + } + if gotErr.Error() != tc.exp { + t.Errorf( + "unexpected error message:\nexp %s\ngot %v", + tc.exp, gotErr, + ) + } + }) + } + }) +} + +// helpers + type object map[string]interface{} func (o object) json() []byte { @@ -90,5 +146,5 @@ func (o object) assign(other object) object { } func sameErrors(a, b error) bool { - return (a == nil && b == nil) || !(a == nil || b == nil) || a.Error() == b.Error() + return (a == nil && b == nil) || (a != nil && b != nil) || a.Error() == b.Error() } diff --git a/configio/parser.go b/configio/parser.go deleted file mode 100644 index af2f673..0000000 --- a/configio/parser.go +++ /dev/null @@ -1,33 +0,0 @@ -package configio - -import ( - "errors" -) - -type extension string - -const ( - extYML extension = ".yml" - extYAML extension = ".yaml" - extJSON extension = ".json" -) - -// configParser exposes a method parse to read bytes as a raw config. -type configParser interface { - // parse parses a raw bytes input as a raw config and stores - // the resulting value into dst. - Parse(in []byte, dst *Representation) error -} - -// newParser returns an appropriate parser according to ext, or a non-nil -// error if ext is not an expected extension. -func newParser(ext extension) (configParser, error) { - switch ext { - case extYML, extYAML: - return YAMLParser{}, nil - case extJSON: - return JSONParser{}, nil - default: - return nil, errors.New("unsupported config format") - } -} diff --git a/configio/parser_json.go b/configio/parser_json.go deleted file mode 100644 index ff3d020..0000000 --- a/configio/parser_json.go +++ /dev/null @@ -1,65 +0,0 @@ -package configio - -import ( - "bytes" - "encoding/json" - "errors" - "fmt" - "regexp" -) - -// JSONParser implements configParser. -type JSONParser struct{} - -// Parse decodes a raw JSON input in strict mode (unknown fields disallowed) -// and stores the resulting value into dst. -func (p JSONParser) Parse(in []byte, dst *Representation) error { - decoder := json.NewDecoder(bytes.NewReader(in)) - decoder.DisallowUnknownFields() - return p.handleError(decoder.Decode(dst)) -} - -// handleError handle a json raw error, transforms it into a user-friendly -// standardized format and returns the resulting error. -func (p JSONParser) handleError(err error) error { - if err == nil { - return nil - } - - // handle syntax error - var errSyntax *json.SyntaxError - if errors.As(err, &errSyntax) { - return fmt.Errorf("syntax error near %d: %w", errSyntax.Offset, err) - } - - // handle type error - var errType *json.UnmarshalTypeError - if errors.As(err, &errType) { - return fmt.Errorf( - "wrong type for field %s: want %s, got %s", - errType.Field, errType.Type, errType.Value, - ) - } - - // handle unknown field error - if field := p.parseUnknownFieldError(err.Error()); field != "" { - return fmt.Errorf(`invalid field ("%s"): does not exist`, field) - } - - return err -} - -// parseUnknownFieldError parses the raw string as a json error -// from an unknown field and returns the field name. -// If the raw string is not an unknown field error, it returns "". -func (p JSONParser) parseUnknownFieldError(raw string) (field string) { - unknownFieldRgx := regexp.MustCompile( - // raw output example: - // json: unknown field "notafield" - `json: unknown field "(\S+)"`, - ) - if matches := unknownFieldRgx.FindStringSubmatch(raw); len(matches) >= 2 { - return matches[1] - } - return "" -} diff --git a/configio/parser_json_test.go b/configio/parser_json_test.go deleted file mode 100644 index 460577b..0000000 --- a/configio/parser_json_test.go +++ /dev/null @@ -1,63 +0,0 @@ -package configio_test - -import ( - "testing" - - "github.com/benchttp/sdk/configio" -) - -func TestJSONParser(t *testing.T) { - t.Run("return expected errors", func(t *testing.T) { - testcases := []struct { - label string - in []byte - exp string - }{ - { - label: "syntax error", - in: []byte("{\n \"runner\": {},\n}\n"), - exp: "syntax error near 19: invalid character '}' looking for beginning of object key string", - }, - { - label: "unknown field", - in: []byte("{\n \"notafield\": 123\n}\n"), - exp: `invalid field ("notafield"): does not exist`, - }, - { - label: "wrong type", - in: []byte("{\n \"runner\": {\n \"requests\": [123]\n }\n}\n"), - exp: "wrong type for field runner.requests: want int, got array", - }, - { - label: "valid config", - in: []byte("{\n \"runner\": {\n \"requests\": 123\n }\n}\n"), - exp: "", - }, - } - - for _, tc := range testcases { - t.Run(tc.label, func(t *testing.T) { - var ( - parser configio.JSONParser - rawcfg configio.Representation - ) - - gotErr := parser.Parse(tc.in, &rawcfg) - - if tc.exp == "" { - if gotErr != nil { - t.Fatalf("unexpected error: %v", gotErr) - } - return - } - - if gotErr.Error() != tc.exp { - t.Errorf( - "unexpected error messages:\nexp %s\ngot %v", - tc.exp, gotErr, - ) - } - }) - } - }) -} diff --git a/configio/parser_yaml.go b/configio/yaml.go similarity index 63% rename from configio/parser_yaml.go rename to configio/yaml.go index 6b13bc1..d1130c3 100644 --- a/configio/parser_yaml.go +++ b/configio/yaml.go @@ -4,25 +4,56 @@ import ( "bytes" "errors" "fmt" + "io" "regexp" "gopkg.in/yaml.v3" + + "github.com/benchttp/sdk/benchttp" ) -// YAMLParser implements configParser. -type YAMLParser struct{} +// UnmarshalYAML parses the YAML-encoded data and stores the result +// in the Representation pointed to by dst. +func UnmarshalYAML(in []byte, dst *Representation) error { + dec := NewYAMLDecoder(bytes.NewReader(in)) + return dec.Decode(dst) +} + +// UnmarshalYAMLRunner parses the YAML-encoded data and stores the result +// in the benchttp.Runner pointed to by dst. +func UnmarshalYAMLRunner(in []byte, dst *benchttp.Runner) error { + dec := NewYAMLDecoder(bytes.NewReader(in)) + return dec.DecodeRunner(dst) +} + +// YAMLDecoder implements Decoder +type YAMLDecoder struct{ r io.Reader } -// Parse decodes a raw yaml input in strict mode (unknown fields disallowed) -// and stores the resulting value into dst. -func (p YAMLParser) Parse(in []byte, dst *Representation) error { - decoder := yaml.NewDecoder(bytes.NewReader(in)) +func NewYAMLDecoder(r io.Reader) YAMLDecoder { + return YAMLDecoder{r: r} +} + +// Decode reads the next YAML-encoded value from its input +// and stores it in the Representation pointed to by dst. +func (d YAMLDecoder) Decode(dst *Representation) error { + decoder := yaml.NewDecoder(d.r) decoder.KnownFields(true) - return p.handleError(decoder.Decode(dst)) + return d.handleError(decoder.Decode(dst)) +} + +// Decode reads the next YAML-encoded value from its input +// and stores it in the benchttp.Runner pointed to by dst. +func (d YAMLDecoder) DecodeRunner(dst *benchttp.Runner) error { + repr := Representation{} + if err := d.Decode(&repr); err != nil { + return err + } + return repr.Into(dst) } // handleError handles a raw yaml decoder.Decode error, filters it, // and return the resulting error. -func (p YAMLParser) handleError(err error) error { +func (d YAMLDecoder) handleError(err error) error { // yaml.TypeError errors require special handling, other errors // (nil included) can be returned as is. var typeError *yaml.TypeError @@ -38,10 +69,10 @@ func (p YAMLParser) handleError(err error) error { // It is a wanted behavior but prevents the usage of custom aliases. // To work around this we allow an exception for that rule with fields // starting with x- (inspired by docker compose api). - if p.isCustomFieldError(msg) { + if d.isCustomFieldError(msg) { continue } - filtered.Errors = append(filtered.Errors, p.prettyErrorMessage(msg)) + filtered.Errors = append(filtered.Errors, d.prettyErrorMessage(msg)) } if len(filtered.Errors) != 0 { @@ -53,7 +84,7 @@ func (p YAMLParser) handleError(err error) error { // isCustomFieldError returns true if the raw error message is due // to an allowed custom field. -func (p YAMLParser) isCustomFieldError(raw string) bool { +func (d YAMLDecoder) isCustomFieldError(raw string) bool { customFieldRgx := regexp.MustCompile( // raw output example: // line 9: field x-my-alias not found in type struct { ... } @@ -65,7 +96,7 @@ func (p YAMLParser) isCustomFieldError(raw string) bool { // prettyErrorMessage transforms a raw Decode error message into a more // user-friendly one by removing noisy information and returns the resulting // value. -func (p YAMLParser) prettyErrorMessage(raw string) string { +func (d YAMLDecoder) prettyErrorMessage(raw string) string { // field not found error fieldNotFoundRgx := regexp.MustCompile( // raw output example (type unmarshaledConfig is entirely exposed): diff --git a/configio/parser_yaml_test.go b/configio/yaml_test.go similarity index 87% rename from configio/parser_yaml_test.go rename to configio/yaml_test.go index 6eacb9e..2f191cf 100644 --- a/configio/parser_yaml_test.go +++ b/configio/yaml_test.go @@ -1,16 +1,18 @@ package configio_test import ( + "bytes" "errors" "reflect" "testing" "gopkg.in/yaml.v3" + "github.com/benchttp/sdk/benchttp" "github.com/benchttp/sdk/configio" ) -func TestYAMLParser(t *testing.T) { +func TestYAMLDecoder(t *testing.T) { t.Run("return expected errors", func(t *testing.T) { testcases := []struct { label string @@ -64,13 +66,10 @@ func TestYAMLParser(t *testing.T) { for _, tc := range testcases { t.Run(tc.label, func(t *testing.T) { - var ( - parser configio.YAMLParser - rawcfg configio.Representation - yamlErr *yaml.TypeError - ) + runner := benchttp.Runner{} + decoder := configio.NewYAMLDecoder(bytes.NewReader(tc.in)) - gotErr := parser.Parse(tc.in, &rawcfg) + gotErr := decoder.DecodeRunner(&runner) if tc.expErr == nil { if gotErr != nil { @@ -79,6 +78,7 @@ func TestYAMLParser(t *testing.T) { return } + var yamlErr *yaml.TypeError if !errors.As(gotErr, &yamlErr) && tc.expErr != nil { t.Fatalf("unexpected error: %v", gotErr) } From fcfc433ed1ce9186eae077cf4c75637cfafc3ea1 Mon Sep 17 00:00:00 2001 From: Gregory Albouy Date: Sat, 29 Oct 2022 14:24:55 +0200 Subject: [PATCH 31/53] refactor(configio): error handling Clarity over humor --- configio/file.go | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/configio/file.go b/configio/file.go index fb5c667..dc22d23 100644 --- a/configio/file.go +++ b/configio/file.go @@ -2,6 +2,7 @@ package configio import ( "errors" + "fmt" "os" "path/filepath" @@ -117,13 +118,11 @@ func parseFile(filename string) (repr Representation, err error) { // into dst. // It returns the merged result or the first non-nil error occurring in the // process. +// +// The input Representation slice must never be nil or empty, otherwise it panics. func parseAndMergeReprs(reprs []Representation, dst *benchttp.Runner) error { if len(reprs) == 0 { // supposedly catched upstream, should not occur - return errors.New( - "an unacceptable error occurred parsing the config file, " + - "please visit https://github.com/benchttp/runner/issues/new " + - "and insult us properly", - ) + panicInternal("parseAndMergeReprs", "nil or empty []Representation") } for i := len(reprs) - 1; i >= 0; i-- { @@ -131,6 +130,14 @@ func parseAndMergeReprs(reprs []Representation, dst *benchttp.Runner) error { return errorutil.WithDetails(ErrFileParse, err) } } - return nil } + +func panicInternal(funcname, detail string) { + const reportURL = "https://github.com/benchttp/sdk/issues/new" + source := fmt.Sprintf("configio.%s", funcname) + panic(fmt.Sprintf( + "%s: unexpected internal error: %s, please file an issue at %s", + source, detail, reportURL, + )) +} From 88eab2431516920e18e6e27c5a8721168086f812 Mon Sep 17 00:00:00 2001 From: Gregory Albouy Date: Sat, 29 Oct 2022 18:03:02 +0200 Subject: [PATCH 32/53] test(configio): refactor TestFindFile, TestUnmarshalFile - make testdata more robust easy to reason about using a dedicated package - use table testing - use benchttptest for runner comparison - extract assertXxx helpers --- configio/file_test.go | 321 ++++++------------ configio/parse.go | 2 +- .../testdata/extends/extends-circular-0.yml | 1 - .../testdata/extends/extends-circular-1.yml | 1 - .../testdata/extends/extends-circular-2.yml | 1 - .../extends/extends-circular-self.yml | 1 - .../testdata/extends/extends-valid-child.yml | 4 - .../testdata/extends/extends-valid-parent.yml | 3 - .../nest-0/nest-1/extends-valid-nested.yml | 4 - configio/testdata/invalid/badext.yams | 2 - .../testdata/invalid/extends/circular-0.yml | 1 + .../testdata/invalid/extends/circular-1.yml | 1 + .../testdata/invalid/extends/circular-2.yml | 1 + .../invalid/extends/circular-self.yml | 1 + configio/testdata/invalid/extension.yams | 2 + .../invalid/{badfields.json => fields.json} | 0 .../invalid/{badfields.yml => fields.yml} | 0 configio/testdata/testdata.go | 158 +++++++++ configio/testdata/valid/benchttp-zeros.yml | 3 - configio/testdata/valid/extends/child.yml | 5 + .../valid/extends/nest-0/nest-1/child.yml | 5 + configio/testdata/valid/extends/parent.yml | 5 + .../valid/{benchttp.json => full.json} | 10 +- .../valid/{benchttp.yaml => full.yaml} | 8 +- .../testdata/valid/{benchttp.yml => full.yml} | 8 +- configio/testdata/valid/partial.yml | 6 + go.mod | 1 + 27 files changed, 299 insertions(+), 256 deletions(-) delete mode 100644 configio/testdata/extends/extends-circular-0.yml delete mode 100644 configio/testdata/extends/extends-circular-1.yml delete mode 100644 configio/testdata/extends/extends-circular-2.yml delete mode 100644 configio/testdata/extends/extends-circular-self.yml delete mode 100644 configio/testdata/extends/extends-valid-child.yml delete mode 100644 configio/testdata/extends/extends-valid-parent.yml delete mode 100644 configio/testdata/extends/nest-0/nest-1/extends-valid-nested.yml delete mode 100644 configio/testdata/invalid/badext.yams create mode 100644 configio/testdata/invalid/extends/circular-0.yml create mode 100644 configio/testdata/invalid/extends/circular-1.yml create mode 100644 configio/testdata/invalid/extends/circular-2.yml create mode 100644 configio/testdata/invalid/extends/circular-self.yml create mode 100644 configio/testdata/invalid/extension.yams rename configio/testdata/invalid/{badfields.json => fields.json} (100%) rename configio/testdata/invalid/{badfields.yml => fields.yml} (100%) create mode 100644 configio/testdata/testdata.go delete mode 100644 configio/testdata/valid/benchttp-zeros.yml create mode 100644 configio/testdata/valid/extends/child.yml create mode 100644 configio/testdata/valid/extends/nest-0/nest-1/child.yml create mode 100644 configio/testdata/valid/extends/parent.yml rename configio/testdata/valid/{benchttp.json => full.json} (76%) rename configio/testdata/valid/{benchttp.yaml => full.yaml} (76%) rename configio/testdata/valid/{benchttp.yml => full.yml} (75%) create mode 100644 configio/testdata/valid/partial.yml diff --git a/configio/file_test.go b/configio/file_test.go index 2b92d6d..86bba93 100644 --- a/configio/file_test.go +++ b/configio/file_test.go @@ -1,94 +1,98 @@ package configio_test import ( - "bytes" "errors" - "fmt" - "net/http" "net/http/httptest" - "net/url" - "path/filepath" - "reflect" "testing" "time" "github.com/benchttp/sdk/benchttp" + "github.com/benchttp/sdk/benchttptest" "github.com/benchttp/sdk/configio" + "github.com/benchttp/sdk/configio/testdata" ) func TestFindFile(t *testing.T) { var ( - fileYAML = configPath("valid/benchttp.yaml") - fileJSON = configPath("valid/benchttp.json") - nofile = configPath("does-not-exist.json") + fileYAML = testdata.ValidFullYAML().Path + fileJSON = testdata.ValidFullJSON().Path + nofile = testdata.InvalidPath().Path ) - t.Run("return first existing file from input", func(t *testing.T) { - files := []string{nofile, fileYAML, fileJSON} - - if got := configio.FindFile(files...); got != fileYAML { - t.Errorf("did not retrieve good file: exp %s, got %s", fileYAML, got) - } - }) - - t.Run("return first existing file from defaults", func(t *testing.T) { - configio.DefaultPaths = []string{nofile, fileYAML, fileJSON} - - if got := configio.FindFile(); got != fileYAML { - t.Errorf("did not retrieve good file: exp %s, got %s", fileYAML, got) - } - }) - - t.Run("return empty string when no match", func(t *testing.T) { - files := []string{nofile} + testcases := []struct { + name string + inputPaths []string + defaultPaths []string + exp string + }{ + { + name: "return first existing input path", + inputPaths: []string{nofile, fileYAML, fileJSON}, + defaultPaths: []string{}, + exp: fileYAML, + }, + { + name: "return first existing default path if no input", + inputPaths: []string{}, + defaultPaths: []string{nofile, fileYAML, fileJSON}, + exp: fileYAML, + }, + { + name: "return empty string if no file found", + inputPaths: []string{nofile}, + defaultPaths: []string{}, + exp: "", + }, + } - if got := configio.FindFile(files...); got != "" { - t.Errorf("retrieved unexpected file: %s", got) - } - }) + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + if len(tc.defaultPaths) > 0 { + configio.DefaultPaths = tc.defaultPaths + } + got := configio.FindFile(tc.inputPaths...) + if got != tc.exp { + t.Errorf("exp %q, got %q", tc.exp, got) + } + }) + } } func TestUnmarshalFile(t *testing.T) { - supportedExt := []string{ - ".yml", - ".yaml", - ".json", - } - t.Run("return file errors early", func(t *testing.T) { testcases := []struct { label string - path string + file testdata.ConfigFile expErr error }{ { label: "not found", - path: configPath("invalid/bad path"), + file: testdata.InvalidPath(), expErr: configio.ErrFileNotFound, }, { label: "unsupported extension", - path: configPath("invalid/badext.yams"), + file: testdata.InvalidExtension(), expErr: configio.ErrFileExt, }, { label: "yaml invalid fields", - path: configPath("invalid/badfields.yml"), + file: testdata.InvalidFieldsYML(), expErr: configio.ErrFileParse, }, { label: "json invalid fields", - path: configPath("invalid/badfields.json"), + file: testdata.InvalidFieldsJSON(), expErr: configio.ErrFileParse, }, { label: "self reference", - path: configPath("extends/extends-circular-self.yml"), + file: testdata.InvalidExtendsSelf(), expErr: configio.ErrFileCircular, }, { label: "circular reference", - path: configPath("extends/extends-circular-0.yml"), + file: testdata.InvalidExtendsCircular(), expErr: configio.ErrFileCircular, }, } @@ -96,116 +100,74 @@ func TestUnmarshalFile(t *testing.T) { for _, tc := range testcases { t.Run(tc.label, func(t *testing.T) { runner := benchttp.Runner{} - gotErr := configio.UnmarshalFile(tc.path, &runner) - - if gotErr == nil { - t.Fatal("exp non-nil error, got nil") - } - - if !errors.Is(gotErr, tc.expErr) { - t.Errorf("\nexp %v\ngot %v", tc.expErr, gotErr) - } + err := configio.UnmarshalFile(tc.file.Path, &runner) - if !sameRunner(runner, benchttp.Runner{}) { - t.Errorf("\nexp empty config\ngot %v", runner) - } + assertStaticError(t, tc.expErr, err) + benchttptest.AssertEqualRunners(t, tc.file.Runner, runner) }) } }) - t.Run("happy path for all extensions", func(t *testing.T) { - for _, ext := range supportedExt { - filename := configPath("valid/benchttp" + ext) - runner := benchttp.Runner{} - - if err := configio.UnmarshalFile(filename, &runner); err != nil { - // critical error, stop the test - t.Fatal(err) - } - - if sameRunner(runner, benchttp.Runner{}) { - t.Error("received an empty configuration") - } - - exp := expectedRunner() - if !sameRunner(runner, exp) { - t.Errorf("unexpected parsed config for %s file:\nexp %#v\ngot %#v", ext, exp, runner) - } + t.Run("happy path all extensions", func(t *testing.T) { + for _, tc := range []struct { + name string + file testdata.ConfigFile + }{ + {name: "full json", file: testdata.ValidFullJSON()}, + {name: "full yaml", file: testdata.ValidFullYAML()}, + {name: "full yml", file: testdata.ValidFullYML()}, + } { + t.Run(tc.name, func(t *testing.T) { + runner := benchttp.Runner{} + err := configio.UnmarshalFile(tc.file.Path, &runner) + mustAssertNilError(t, err) + benchttptest.AssertEqualRunners(t, tc.file.Runner, runner) + }) } }) - t.Run("override input config", func(t *testing.T) { - filename := configPath("valid/benchttp-zeros.yml") - runner := benchttp.Runner{} - runner.Request = httptest.NewRequest("POST", "https://overriden.com", nil) - runner.GlobalTimeout = 10 * time.Millisecond - - if err := configio.UnmarshalFile(filename, &runner); err != nil { - t.Fatal(err) + t.Run("override dst with set config values", func(t *testing.T) { + cfg := testdata.ValidPartial() + runner := benchttp.Runner{ + Request: httptest.NewRequest("GET", "http://a.b", nil), // overridden + GlobalTimeout: 1 * time.Second, // overridden } - const ( - expMethod = "POST" // from input config - expGlobalTimeout = 42 * time.Millisecond // from read file - ) + err := configio.UnmarshalFile(cfg.Path, &runner) - if gotMethod := runner.Request.Method; gotMethod != expMethod { - t.Errorf( - "did not keep input values that are not set: "+ - "exp Request.Method == %s, got %s", - expMethod, gotMethod, - ) - } - - if gotGlobalTimeout := runner.GlobalTimeout; gotGlobalTimeout != expGlobalTimeout { - t.Errorf( - "did not override input values that are set: "+ - "exp Runner.GlobalTimeout == %v, got %v", - expGlobalTimeout, gotGlobalTimeout, - ) - } - - t.Log(runner) + mustAssertNilError(t, err) + benchttptest.AssertEqualRunners(t, cfg.Runner, runner) }) - t.Run("extend config files", func(t *testing.T) { - testcases := []struct { - label string - cfname string - cfpath string - }{ - { - label: "same directory", - cfname: "child", - cfpath: configPath("extends/extends-valid-child.yml"), - }, - { - label: "nested directory", - cfname: "nested", - cfpath: configPath("extends/nest-0/nest-1/extends-valid-nested.yml"), - }, - } + t.Run("keep dst values not set in config", func(t *testing.T) { + const keptConcurrency = 5 // not set in config file - for _, tc := range testcases { - t.Run(tc.label, func(t *testing.T) { - var runner benchttp.Runner - if err := configio.UnmarshalFile(tc.cfpath, &runner); err != nil { - t.Fatal(err) - } + cfg := testdata.ValidPartial() + exp := cfg.Runner + exp.Concurrency = keptConcurrency + dst := benchttp.Runner{Concurrency: keptConcurrency} - var ( - expMethod = "POST" - expURL = fmt.Sprintf("http://%s.config", tc.cfname) - ) + err := configio.UnmarshalFile(cfg.Path, &dst) - if gotMethod := runner.Request.Method; gotMethod != expMethod { - t.Errorf("method: exp %s, got %s", expMethod, gotMethod) - } + mustAssertNilError(t, err) + benchttptest.AssertEqualRunners(t, exp, dst) + }) - if gotURL := runner.Request.URL.String(); gotURL != expURL { - t.Errorf("url: exp %s, got %s", expURL, gotURL) - } + t.Run("extend config files", func(t *testing.T) { + for _, tc := range []struct { + name string + cfg testdata.ConfigFile + }{ + {name: "same directory", cfg: testdata.ValidExtends()}, + {name: "nested directory", cfg: testdata.ValidExtendsNested()}, + } { + t.Run(tc.name, func(t *testing.T) { + dst := benchttp.Runner{} + err := configio.UnmarshalFile(tc.cfg.Path, &dst) + + mustAssertNilError(t, err) + benchttptest.AssertEqualRunners(t, tc.cfg.Runner, dst) }) } }) @@ -213,87 +175,16 @@ func TestUnmarshalFile(t *testing.T) { // helpers -// expectedRunner returns the expected benchttp.Runner after unmarhsaling -// one of the valid config files in testdata. -func expectedRunner() benchttp.Runner { - request := httptest.NewRequest( - "POST", - "http://localhost:9999?fib=30&delay=200ms", - bytes.NewReader([]byte(`{"key0":"val0","key1":"val1"}`)), - ) - request.Header = http.Header{ - "key0": []string{"val0", "val1"}, - "key1": []string{"val0"}, +func mustAssertNilError(t *testing.T, err error) { + t.Helper() + if err != nil { + t.Fatalf("exp nil error, got %v", err) } - return benchttp.Runner{ - Request: request, - - Requests: 100, - Concurrency: 1, - Interval: 50 * time.Millisecond, - RequestTimeout: 2 * time.Second, - GlobalTimeout: 60 * time.Second, - - Tests: []benchttp.TestCase{ - { - Name: "minimum response time", - Field: "ResponseTimes.Min", - Predicate: "GT", - Target: 80 * time.Millisecond, - }, - { - Name: "maximum response time", - Field: "ResponseTimes.Max", - Predicate: "LTE", - Target: 120 * time.Millisecond, - }, - { - Name: "100% availability", - Field: "RequestFailureCount", - Predicate: "EQ", - Target: 0, - }, - }, - } -} - -func sameRunner(a, b benchttp.Runner) bool { - if a.Request == nil || b.Request == nil { - return a.Request == nil && b.Request == nil - } - return sameURL(a.Request.URL, b.Request.URL) && - reflect.DeepEqual(a.Request.Header, b.Request.Header) && - reflect.DeepEqual(a.Request.Body, b.Request.Body) } -// sameURL returns true if a and b are the same *url.URL, taking into account -// the undeterministic nature of their RawQuery. -func sameURL(a, b *url.URL) bool { - // check query params equality via Query() rather than RawQuery - if !reflect.DeepEqual(a.Query(), b.Query()) { - return false +func assertStaticError(t *testing.T, exp, got error) { + t.Helper() + if !errors.Is(got, exp) { + t.Errorf("unexpected error:\nexp %v\ngot %v", exp, got) } - - // temporarily set RawQuery to a determined value - for _, u := range []*url.URL{a, b} { - restore := setTempValue(&u.RawQuery, "replaced by test") - defer restore() - } - - // we can now rely on deep equality check - return reflect.DeepEqual(a, b) -} - -// setTempValue sets *ptr to val and returns a restore func that sets *ptr -// back to its previous value. -func setTempValue(ptr *string, val string) (restore func()) { - previousValue := *ptr - *ptr = val - return func() { - *ptr = previousValue - } -} - -func configPath(name string) string { - return filepath.Join("testdata", name) } diff --git a/configio/parse.go b/configio/parse.go index 0c5f009..28adf71 100644 --- a/configio/parse.go +++ b/configio/parse.go @@ -78,7 +78,7 @@ func (repr Representation) parseRequestInto(dst *benchttp.Runner) error { dst.Request.URL = parsedURL } - if header := repr.Request.Header; header != nil { + if header := repr.Request.Header; len(header) != 0 { httpHeader := http.Header{} for key, val := range header { httpHeader[key] = val diff --git a/configio/testdata/extends/extends-circular-0.yml b/configio/testdata/extends/extends-circular-0.yml deleted file mode 100644 index 55c7ac7..0000000 --- a/configio/testdata/extends/extends-circular-0.yml +++ /dev/null @@ -1 +0,0 @@ -extends: ./extends-circular-1.yml diff --git a/configio/testdata/extends/extends-circular-1.yml b/configio/testdata/extends/extends-circular-1.yml deleted file mode 100644 index f451260..0000000 --- a/configio/testdata/extends/extends-circular-1.yml +++ /dev/null @@ -1 +0,0 @@ -extends: ./extends-circular-2.yml diff --git a/configio/testdata/extends/extends-circular-2.yml b/configio/testdata/extends/extends-circular-2.yml deleted file mode 100644 index b862fa4..0000000 --- a/configio/testdata/extends/extends-circular-2.yml +++ /dev/null @@ -1 +0,0 @@ -extends: ./extends-circular-0.yml diff --git a/configio/testdata/extends/extends-circular-self.yml b/configio/testdata/extends/extends-circular-self.yml deleted file mode 100644 index 2fa66ac..0000000 --- a/configio/testdata/extends/extends-circular-self.yml +++ /dev/null @@ -1 +0,0 @@ -extends: ./extends-circular-self.yml diff --git a/configio/testdata/extends/extends-valid-child.yml b/configio/testdata/extends/extends-valid-child.yml deleted file mode 100644 index a344080..0000000 --- a/configio/testdata/extends/extends-valid-child.yml +++ /dev/null @@ -1,4 +0,0 @@ -extends: ./extends-valid-parent.yml - -request: - url: http://child.config diff --git a/configio/testdata/extends/extends-valid-parent.yml b/configio/testdata/extends/extends-valid-parent.yml deleted file mode 100644 index 7f3b136..0000000 --- a/configio/testdata/extends/extends-valid-parent.yml +++ /dev/null @@ -1,3 +0,0 @@ -request: - method: POST - url: http://parent.config diff --git a/configio/testdata/extends/nest-0/nest-1/extends-valid-nested.yml b/configio/testdata/extends/nest-0/nest-1/extends-valid-nested.yml deleted file mode 100644 index 7810890..0000000 --- a/configio/testdata/extends/nest-0/nest-1/extends-valid-nested.yml +++ /dev/null @@ -1,4 +0,0 @@ -extends: ../../extends-valid-parent.yml - -request: - url: http://nested.config diff --git a/configio/testdata/invalid/badext.yams b/configio/testdata/invalid/badext.yams deleted file mode 100644 index f7aba57..0000000 --- a/configio/testdata/invalid/badext.yams +++ /dev/null @@ -1,2 +0,0 @@ -request: - url: https://benchttp.app diff --git a/configio/testdata/invalid/extends/circular-0.yml b/configio/testdata/invalid/extends/circular-0.yml new file mode 100644 index 0000000..38b6d95 --- /dev/null +++ b/configio/testdata/invalid/extends/circular-0.yml @@ -0,0 +1 @@ +extends: ./circular-1.yml diff --git a/configio/testdata/invalid/extends/circular-1.yml b/configio/testdata/invalid/extends/circular-1.yml new file mode 100644 index 0000000..11a1535 --- /dev/null +++ b/configio/testdata/invalid/extends/circular-1.yml @@ -0,0 +1 @@ +extends: ./circular-2.yml diff --git a/configio/testdata/invalid/extends/circular-2.yml b/configio/testdata/invalid/extends/circular-2.yml new file mode 100644 index 0000000..c20965b --- /dev/null +++ b/configio/testdata/invalid/extends/circular-2.yml @@ -0,0 +1 @@ +extends: ./circular-0.yml diff --git a/configio/testdata/invalid/extends/circular-self.yml b/configio/testdata/invalid/extends/circular-self.yml new file mode 100644 index 0000000..3345ad3 --- /dev/null +++ b/configio/testdata/invalid/extends/circular-self.yml @@ -0,0 +1 @@ +extends: ./circular-self.yml diff --git a/configio/testdata/invalid/extension.yams b/configio/testdata/invalid/extension.yams new file mode 100644 index 0000000..79c97a3 --- /dev/null +++ b/configio/testdata/invalid/extension.yams @@ -0,0 +1,2 @@ +request: + url: https://localhost:3000/not-read diff --git a/configio/testdata/invalid/badfields.json b/configio/testdata/invalid/fields.json similarity index 100% rename from configio/testdata/invalid/badfields.json rename to configio/testdata/invalid/fields.json diff --git a/configio/testdata/invalid/badfields.yml b/configio/testdata/invalid/fields.yml similarity index 100% rename from configio/testdata/invalid/badfields.yml rename to configio/testdata/invalid/fields.yml diff --git a/configio/testdata/testdata.go b/configio/testdata/testdata.go new file mode 100644 index 0000000..376305d --- /dev/null +++ b/configio/testdata/testdata.go @@ -0,0 +1,158 @@ +package testdata + +import ( + "bytes" + "net/http" + "net/http/httptest" + "path/filepath" + "time" + + "github.com/benchttp/sdk/benchttp" +) + +// ConfigFile represents a testdata configuration file. +type ConfigFile struct { + // Path is the relative file path from configio. + Path string + // Runner is the expected benchttp.Runner. + Runner benchttp.Runner +} + +// ValidFullJSON returns a valid full configuration file. +func ValidFullJSON() ConfigFile { + return validConfig("full.json", kindFull) +} + +func ValidFullYAML() ConfigFile { + return validConfig("full.yaml", kindFull) +} + +func ValidFullYML() ConfigFile { + return validConfig("full.yml", kindFull) +} + +func ValidPartial() ConfigFile { + return validConfig("partial.yml", kindPartial) +} + +func ValidExtends() ConfigFile { + return validConfig("extends/child.yml", kindExtended) +} + +func ValidExtendsNested() ConfigFile { + return validConfig("extends/nest-0/nest-1/child.yml", kindExtended) +} + +func InvalidPath() ConfigFile { + return invalidConfig("does-not-exist.json") +} + +func InvalidFieldsJSON() ConfigFile { + return invalidConfig("fields.json") +} + +func InvalidFieldsYML() ConfigFile { + return invalidConfig("fields.yml") +} + +func InvalidExtension() ConfigFile { + return invalidConfig("extension.yams") +} + +func InvalidExtendsCircular() ConfigFile { + return invalidConfig("extends/circular-0.yml") +} + +func InvalidExtendsSelf() ConfigFile { + return invalidConfig("extends/circular-self.yml") +} + +type kind uint8 + +const ( + kindFull kind = iota + kindPartial + kindExtended +) + +func validConfig(name string, k kind) ConfigFile { + return ConfigFile{ + Path: filepath.Join("testdata", "valid", name), + Runner: runnerOf(k), + } +} + +func invalidConfig(name string) ConfigFile { + return ConfigFile{ + Path: filepath.Join("testdata", "invalid", name), + Runner: benchttp.Runner{}, + } +} + +func runnerOf(k kind) benchttp.Runner { + switch k { + case kindFull: + return fullRunner() + case kindPartial: + return partialRunner() + case kindExtended: + return extendedRunner() + default: + panic("invalid kind") + } +} + +// fullRunner returns the expected runner from full configurations +func fullRunner() benchttp.Runner { + request := httptest.NewRequest( + "POST", + "http://localhost:3000/benchttp?param0=value0¶m1=value1", + bytes.NewReader([]byte(`{"key0":"val0","key1":"val1"}`)), + ) + request.Header = http.Header{ + "key0": []string{"val0", "val1"}, + "key1": []string{"val0"}, + } + return benchttp.Runner{ + Request: request, + + Requests: 100, + Concurrency: 1, + Interval: 50 * time.Millisecond, + RequestTimeout: 2 * time.Second, + GlobalTimeout: 60 * time.Second, + + Tests: []benchttp.TestCase{ + { + Name: "maximum response time", + Field: "ResponseTimes.Max", + Predicate: "LTE", + Target: 120 * time.Millisecond, + }, + { + Name: "100% availability", + Field: "RequestFailureCount", + Predicate: "EQ", + Target: 0, + }, + }, + } +} + +// partialRunner returns the expected runner from partial configurations +func partialRunner() benchttp.Runner { + return benchttp.Runner{ + Request: httptest.NewRequest("GET", "http://localhost:3000/partial", nil), + GlobalTimeout: 42 * time.Second, + } +} + +// extendedRunner returns the expected runner from extending configurations. +func extendedRunner() benchttp.Runner { + return benchttp.Runner{ + // child override + Request: httptest.NewRequest("PUT", "http://localhost:3000/child", nil), + // parent kept value + GlobalTimeout: 42 * time.Second, + } +} diff --git a/configio/testdata/valid/benchttp-zeros.yml b/configio/testdata/valid/benchttp-zeros.yml deleted file mode 100644 index 38b1bdb..0000000 --- a/configio/testdata/valid/benchttp-zeros.yml +++ /dev/null @@ -1,3 +0,0 @@ -runner: - requests: 0 - globalTimeout: 42ms diff --git a/configio/testdata/valid/extends/child.yml b/configio/testdata/valid/extends/child.yml new file mode 100644 index 0000000..0bc8912 --- /dev/null +++ b/configio/testdata/valid/extends/child.yml @@ -0,0 +1,5 @@ +extends: ./parent.yml + +request: + method: PUT + url: http://localhost:3000/child diff --git a/configio/testdata/valid/extends/nest-0/nest-1/child.yml b/configio/testdata/valid/extends/nest-0/nest-1/child.yml new file mode 100644 index 0000000..a298cac --- /dev/null +++ b/configio/testdata/valid/extends/nest-0/nest-1/child.yml @@ -0,0 +1,5 @@ +extends: ../../parent.yml + +request: + method: PUT + url: http://localhost:3000/child diff --git a/configio/testdata/valid/extends/parent.yml b/configio/testdata/valid/extends/parent.yml new file mode 100644 index 0000000..7c88117 --- /dev/null +++ b/configio/testdata/valid/extends/parent.yml @@ -0,0 +1,5 @@ +request: + url: http://localhost:3000/parent # overridden + +runner: + globalTimeout: 42s # kept diff --git a/configio/testdata/valid/benchttp.json b/configio/testdata/valid/full.json similarity index 76% rename from configio/testdata/valid/benchttp.json rename to configio/testdata/valid/full.json index 714506f..5f46337 100644 --- a/configio/testdata/valid/benchttp.json +++ b/configio/testdata/valid/full.json @@ -1,9 +1,9 @@ { "request": { "method": "POST", - "url": "http://localhost:9999?delay=200ms", + "url": "http://localhost:3000/benchttp?param0=value0", "queryParams": { - "fib": "30" + "param1": "value1" }, "header": { "key0": ["val0", "val1"], @@ -22,12 +22,6 @@ "globalTimeout": "60s" }, "tests": [ - { - "name": "minimum response time", - "field": "ResponseTimes.Min", - "predicate": "GT", - "target": "80ms" - }, { "name": "maximum response time", "field": "ResponseTimes.Max", diff --git a/configio/testdata/valid/benchttp.yaml b/configio/testdata/valid/full.yaml similarity index 76% rename from configio/testdata/valid/benchttp.yaml rename to configio/testdata/valid/full.yaml index 2ee790c..50d64a9 100644 --- a/configio/testdata/valid/benchttp.yaml +++ b/configio/testdata/valid/full.yaml @@ -1,11 +1,11 @@ x-custom: &data method: POST - url: http://localhost:9999?delay=200ms + url: http://localhost:3000/benchttp?param0=value0 request: <<: *data queryParams: - fib: 30 + param1: value1 header: key0: [val0, val1] key1: [val0] @@ -21,10 +21,6 @@ runner: globalTimeout: 60s tests: - - name: minimum response time - field: ResponseTimes.Min - predicate: GT - target: 80ms - name: maximum response time field: ResponseTimes.Max predicate: LTE diff --git a/configio/testdata/valid/benchttp.yml b/configio/testdata/valid/full.yml similarity index 75% rename from configio/testdata/valid/benchttp.yml rename to configio/testdata/valid/full.yml index 27a2fc9..981512e 100644 --- a/configio/testdata/valid/benchttp.yml +++ b/configio/testdata/valid/full.yml @@ -1,8 +1,8 @@ request: method: POST - url: http://localhost:9999?delay=200ms + url: http://localhost:3000/benchttp?param0=value0 queryParams: - fib: 30 + param1: value1 header: key0: [val0, val1] key1: [val0] @@ -18,10 +18,6 @@ runner: globalTimeout: 60s tests: - - name: minimum response time - field: ResponseTimes.Min - predicate: GT - target: 80ms - name: maximum response time field: ResponseTimes.Max predicate: LTE diff --git a/configio/testdata/valid/partial.yml b/configio/testdata/valid/partial.yml new file mode 100644 index 0000000..4969794 --- /dev/null +++ b/configio/testdata/valid/partial.yml @@ -0,0 +1,6 @@ +request: + method: GET + url: http://localhost:3000/partial + +runner: + globalTimeout: 42s diff --git a/go.mod b/go.mod index b514887..6ac6d6f 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/benchttp/sdk go 1.17 require ( + github.com/google/go-cmp v0.5.9 golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 gopkg.in/yaml.v3 v3.0.1 ) From cd83b037e563ac4ce5955eeb1d3d41e240af1930 Mon Sep 17 00:00:00 2001 From: Gregory Albouy Date: Sun, 30 Oct 2022 18:19:35 +0100 Subject: [PATCH 33/53] refactor(configio): move testdata to internal Do not expose testdata. --- configio/file_test.go | 2 +- .../{ => internal}/testdata/invalid/extends/circular-0.yml | 0 .../{ => internal}/testdata/invalid/extends/circular-1.yml | 0 .../{ => internal}/testdata/invalid/extends/circular-2.yml | 0 .../testdata/invalid/extends/circular-self.yml | 0 configio/{ => internal}/testdata/invalid/extension.yams | 0 configio/{ => internal}/testdata/invalid/fields.json | 0 configio/{ => internal}/testdata/invalid/fields.yml | 0 configio/{ => internal}/testdata/testdata.go | 6 ++++-- configio/{ => internal}/testdata/valid/extends/child.yml | 0 .../testdata/valid/extends/nest-0/nest-1/child.yml | 0 configio/{ => internal}/testdata/valid/extends/parent.yml | 0 configio/{ => internal}/testdata/valid/full.json | 0 configio/{ => internal}/testdata/valid/full.yaml | 0 configio/{ => internal}/testdata/valid/full.yml | 0 configio/{ => internal}/testdata/valid/partial.yml | 0 16 files changed, 5 insertions(+), 3 deletions(-) rename configio/{ => internal}/testdata/invalid/extends/circular-0.yml (100%) rename configio/{ => internal}/testdata/invalid/extends/circular-1.yml (100%) rename configio/{ => internal}/testdata/invalid/extends/circular-2.yml (100%) rename configio/{ => internal}/testdata/invalid/extends/circular-self.yml (100%) rename configio/{ => internal}/testdata/invalid/extension.yams (100%) rename configio/{ => internal}/testdata/invalid/fields.json (100%) rename configio/{ => internal}/testdata/invalid/fields.yml (100%) rename configio/{ => internal}/testdata/testdata.go (95%) rename configio/{ => internal}/testdata/valid/extends/child.yml (100%) rename configio/{ => internal}/testdata/valid/extends/nest-0/nest-1/child.yml (100%) rename configio/{ => internal}/testdata/valid/extends/parent.yml (100%) rename configio/{ => internal}/testdata/valid/full.json (100%) rename configio/{ => internal}/testdata/valid/full.yaml (100%) rename configio/{ => internal}/testdata/valid/full.yml (100%) rename configio/{ => internal}/testdata/valid/partial.yml (100%) diff --git a/configio/file_test.go b/configio/file_test.go index 86bba93..5d244fb 100644 --- a/configio/file_test.go +++ b/configio/file_test.go @@ -9,7 +9,7 @@ import ( "github.com/benchttp/sdk/benchttp" "github.com/benchttp/sdk/benchttptest" "github.com/benchttp/sdk/configio" - "github.com/benchttp/sdk/configio/testdata" + "github.com/benchttp/sdk/configio/internal/testdata" ) func TestFindFile(t *testing.T) { diff --git a/configio/testdata/invalid/extends/circular-0.yml b/configio/internal/testdata/invalid/extends/circular-0.yml similarity index 100% rename from configio/testdata/invalid/extends/circular-0.yml rename to configio/internal/testdata/invalid/extends/circular-0.yml diff --git a/configio/testdata/invalid/extends/circular-1.yml b/configio/internal/testdata/invalid/extends/circular-1.yml similarity index 100% rename from configio/testdata/invalid/extends/circular-1.yml rename to configio/internal/testdata/invalid/extends/circular-1.yml diff --git a/configio/testdata/invalid/extends/circular-2.yml b/configio/internal/testdata/invalid/extends/circular-2.yml similarity index 100% rename from configio/testdata/invalid/extends/circular-2.yml rename to configio/internal/testdata/invalid/extends/circular-2.yml diff --git a/configio/testdata/invalid/extends/circular-self.yml b/configio/internal/testdata/invalid/extends/circular-self.yml similarity index 100% rename from configio/testdata/invalid/extends/circular-self.yml rename to configio/internal/testdata/invalid/extends/circular-self.yml diff --git a/configio/testdata/invalid/extension.yams b/configio/internal/testdata/invalid/extension.yams similarity index 100% rename from configio/testdata/invalid/extension.yams rename to configio/internal/testdata/invalid/extension.yams diff --git a/configio/testdata/invalid/fields.json b/configio/internal/testdata/invalid/fields.json similarity index 100% rename from configio/testdata/invalid/fields.json rename to configio/internal/testdata/invalid/fields.json diff --git a/configio/testdata/invalid/fields.yml b/configio/internal/testdata/invalid/fields.yml similarity index 100% rename from configio/testdata/invalid/fields.yml rename to configio/internal/testdata/invalid/fields.yml diff --git a/configio/testdata/testdata.go b/configio/internal/testdata/testdata.go similarity index 95% rename from configio/testdata/testdata.go rename to configio/internal/testdata/testdata.go index 376305d..a07e8d3 100644 --- a/configio/testdata/testdata.go +++ b/configio/internal/testdata/testdata.go @@ -75,16 +75,18 @@ const ( kindExtended ) +var basePath = filepath.Join("internal", "testdata") + func validConfig(name string, k kind) ConfigFile { return ConfigFile{ - Path: filepath.Join("testdata", "valid", name), + Path: filepath.Join(basePath, "valid", name), Runner: runnerOf(k), } } func invalidConfig(name string) ConfigFile { return ConfigFile{ - Path: filepath.Join("testdata", "invalid", name), + Path: filepath.Join(basePath, "invalid", name), Runner: benchttp.Runner{}, } } diff --git a/configio/testdata/valid/extends/child.yml b/configio/internal/testdata/valid/extends/child.yml similarity index 100% rename from configio/testdata/valid/extends/child.yml rename to configio/internal/testdata/valid/extends/child.yml diff --git a/configio/testdata/valid/extends/nest-0/nest-1/child.yml b/configio/internal/testdata/valid/extends/nest-0/nest-1/child.yml similarity index 100% rename from configio/testdata/valid/extends/nest-0/nest-1/child.yml rename to configio/internal/testdata/valid/extends/nest-0/nest-1/child.yml diff --git a/configio/testdata/valid/extends/parent.yml b/configio/internal/testdata/valid/extends/parent.yml similarity index 100% rename from configio/testdata/valid/extends/parent.yml rename to configio/internal/testdata/valid/extends/parent.yml diff --git a/configio/testdata/valid/full.json b/configio/internal/testdata/valid/full.json similarity index 100% rename from configio/testdata/valid/full.json rename to configio/internal/testdata/valid/full.json diff --git a/configio/testdata/valid/full.yaml b/configio/internal/testdata/valid/full.yaml similarity index 100% rename from configio/testdata/valid/full.yaml rename to configio/internal/testdata/valid/full.yaml diff --git a/configio/testdata/valid/full.yml b/configio/internal/testdata/valid/full.yml similarity index 100% rename from configio/testdata/valid/full.yml rename to configio/internal/testdata/valid/full.yml diff --git a/configio/testdata/valid/partial.yml b/configio/internal/testdata/valid/partial.yml similarity index 100% rename from configio/testdata/valid/partial.yml rename to configio/internal/testdata/valid/partial.yml From 783aa1484cca7fd1d5d2b0c04ba0596c732954bb Mon Sep 17 00:00:00 2001 From: Gregory Albouy Date: Sun, 30 Oct 2022 22:42:23 +0100 Subject: [PATCH 34/53] test(configio): add cases for empty paths --- configio/file_test.go | 10 ++++++++++ .../internal/testdata/invalid/extends/empty-path.yml | 1 + configio/internal/testdata/testdata.go | 4 ++++ 3 files changed, 15 insertions(+) create mode 100644 configio/internal/testdata/invalid/extends/empty-path.yml diff --git a/configio/file_test.go b/configio/file_test.go index 5d244fb..1f637f2 100644 --- a/configio/file_test.go +++ b/configio/file_test.go @@ -65,6 +65,11 @@ func TestUnmarshalFile(t *testing.T) { file testdata.ConfigFile expErr error }{ + { + label: "empty path", + file: testdata.ConfigFile{Path: ""}, + expErr: configio.ErrFileNotFound, + }, { label: "not found", file: testdata.InvalidPath(), @@ -95,6 +100,11 @@ func TestUnmarshalFile(t *testing.T) { file: testdata.InvalidExtendsCircular(), expErr: configio.ErrFileCircular, }, + { + label: "empty reference", + file: testdata.InvalidExtendsEmpty(), + expErr: configio.ErrFileNotFound, + }, } for _, tc := range testcases { diff --git a/configio/internal/testdata/invalid/extends/empty-path.yml b/configio/internal/testdata/invalid/extends/empty-path.yml new file mode 100644 index 0000000..b1f06eb --- /dev/null +++ b/configio/internal/testdata/invalid/extends/empty-path.yml @@ -0,0 +1 @@ +extends: "" diff --git a/configio/internal/testdata/testdata.go b/configio/internal/testdata/testdata.go index a07e8d3..4be6a8a 100644 --- a/configio/internal/testdata/testdata.go +++ b/configio/internal/testdata/testdata.go @@ -67,6 +67,10 @@ func InvalidExtendsSelf() ConfigFile { return invalidConfig("extends/circular-self.yml") } +func InvalidExtendsEmpty() ConfigFile { + return invalidConfig("extends/empty.yml") +} + type kind uint8 const ( From 215f02861289ee8b8a271a74c7c78bb3e824a219 Mon Sep 17 00:00:00 2001 From: Gregory Albouy Date: Sun, 30 Oct 2022 23:53:35 +0100 Subject: [PATCH 35/53] refactor(configio): refactor UnmarshalFile - use dedicated data structure (file) - rethink namings - move functions to appropriate places --- configio/error.go | 10 ++++ configio/file.go | 123 +++++++++++++++++++--------------------------- configio/parse.go | 19 +++++++ 3 files changed, 79 insertions(+), 73 deletions(-) diff --git a/configio/error.go b/configio/error.go index 258bcff..d3d46b4 100644 --- a/configio/error.go +++ b/configio/error.go @@ -2,6 +2,7 @@ package configio import ( "errors" + "fmt" ) var ( @@ -24,3 +25,12 @@ var ( // ErrFileCircular signals a circular reference in the config file. ErrFileCircular = errors.New("circular reference detected") ) + +func panicInternal(funcname, detail string) { + const reportURL = "https://github.com/benchttp/sdk/issues/new" + source := fmt.Sprintf("configio.%s", funcname) + panic(fmt.Sprintf( + "%s: unexpected internal error: %s, please file an issue at %s", + source, detail, reportURL, + )) +} diff --git a/configio/file.go b/configio/file.go index dc22d23..6856ff7 100644 --- a/configio/file.go +++ b/configio/file.go @@ -2,7 +2,6 @@ package configio import ( "errors" - "fmt" "os" "path/filepath" @@ -11,6 +10,8 @@ import ( "github.com/benchttp/sdk/internal/errorutil" ) +// DefaultPaths is the default list of paths looked up by FindFile when +// called without parameters. var DefaultPaths = []string{ "./.benchttp.yml", "./.benchttp.yaml", @@ -36,108 +37,84 @@ func FindFile(paths ...string) string { // into a runner.Runner and stores the retrieved values into *dst. // It returns the first error occurring in the process, which can be // any of the values declared in the package. -func UnmarshalFile(filename string, dst *benchttp.Runner) (err error) { - reprs, err := parseFileRecursive(filename, []Representation{}, set{}) +func UnmarshalFile(filename string, dst *benchttp.Runner) error { + f, err := file{path: filename}.decodeAll() if err != nil { - return + return err } - return parseAndMergeReprs(reprs, dst) + return f.reprs().mergeInto(dst) } -// set is a collection of unique string values. -type set map[string]bool - -// add adds v to the receiver. If v is already set, it returns a non-nil -// error instead. -func (s set) add(v string) error { - if _, exists := s[v]; exists { - return errors.New("value already set") - } - s[v] = true - return nil +// file represents a config file +type file struct { + prev *file + path string + repr Representation } -// parseFileRecursive parses a config file and its parent found from key -// "extends" recursively until the root config file is reached. -// It returns the list of all parsed configs or the first non-nil error -// occurring in the process. -func parseFileRecursive( - filename string, - reprs []Representation, - seen set, -) ([]Representation, error) { - // avoid infinite recursion caused by circular reference - if err := seen.add(filename); err != nil { - return reprs, ErrFileCircular +// decodeAll reads f.path as a file and decodes it into f.repr. +// If the decoded file references another file, the operation +// is repeated recursively until root file is reached. +func (f file) decodeAll() (file, error) { + if err := f.decode(); err != nil { + return file{}, err } - // parse current file, append parsed config - repr, err := parseFile(filename) - if err != nil { - return reprs, err + if isRoot := f.repr.Extends == nil; isRoot { + return f, nil } - reprs = append(reprs, repr) - // root config reached: stop now and return the parsed configs - if repr.Extends == nil { - return reprs, nil + nextPath := filepath.Join(filepath.Dir(f.path), *f.repr.Extends) + if f.seen(nextPath) { + return file{}, errorutil.WithDetails(ErrFileCircular, nextPath) } - // config has parent: resolve its path and parse it recursively - parentPath := filepath.Join(filepath.Dir(filename), *repr.Extends) - return parseFileRecursive(parentPath, reprs, seen) + return f.extend(nextPath).decodeAll() } -// parseFile parses a single config file and returns the result as an -// Representation and an appropriate error predeclared in the package. -func parseFile(filename string) (repr Representation, err error) { - b, err := os.ReadFile(filename) +// decode reads f.path as a file and decodes it into f.repr. +func (f *file) decode() (err error) { + b, err := os.ReadFile(f.path) switch { case err == nil: case errors.Is(err, os.ErrNotExist): - return repr, errorutil.WithDetails(ErrFileNotFound, filename) + return errorutil.WithDetails(ErrFileNotFound, f.path) default: - return repr, errorutil.WithDetails(ErrFileRead, filename, err) + return errorutil.WithDetails(ErrFileRead, f.path, err) } - ext := Extension(filepath.Ext(filename)) + ext := Extension(filepath.Ext(f.path)) dec, err := DecoderOf(ext, b) if err != nil { - return repr, errorutil.WithDetails(ErrFileExt, ext, err) + return errorutil.WithDetails(ErrFileExt, ext, err) } - if err = dec.Decode(&repr); err != nil { - return repr, errorutil.WithDetails(ErrFileParse, filename, err) + if err = dec.Decode(&f.repr); err != nil { + return errorutil.WithDetails(ErrFileParse, f.path, err) } - return repr, nil + return nil } -// parseAndMergeReprs iterates backwards over reprs, parses them as -// runner.Runner, merges them successively and finally stores the result -// into dst. -// It returns the merged result or the first non-nil error occurring in the -// process. -// -// The input Representation slice must never be nil or empty, otherwise it panics. -func parseAndMergeReprs(reprs []Representation, dst *benchttp.Runner) error { - if len(reprs) == 0 { // supposedly catched upstream, should not occur - panicInternal("parseAndMergeReprs", "nil or empty []Representation") - } +func (f file) extend(nextPath string) file { + return file{prev: &f, path: nextPath} +} - for i := len(reprs) - 1; i >= 0; i-- { - if err := reprs[i].Into(dst); err != nil { - return errorutil.WithDetails(ErrFileParse, err) - } +// seen returns true if the given path has already been decoded +// by the receiver or any of its ancestors. +func (f file) seen(p string) bool { + if f.path == "" || p == "" { + panicInternal("file.seen", "empty f.path or p") } - return nil + return f.path == p || (f.prev != nil && f.prev.seen(p)) } -func panicInternal(funcname, detail string) { - const reportURL = "https://github.com/benchttp/sdk/issues/new" - source := fmt.Sprintf("configio.%s", funcname) - panic(fmt.Sprintf( - "%s: unexpected internal error: %s, please file an issue at %s", - source, detail, reportURL, - )) +// reprs returns a slice of Representation, starting with the receiver +// and ending with the last child. +func (f file) reprs() representations { + reprs := []Representation{f.repr} + if f.prev != nil { + reprs = append(reprs, f.prev.reprs()...) + } + return reprs } diff --git a/configio/parse.go b/configio/parse.go index 28adf71..c668f0d 100644 --- a/configio/parse.go +++ b/configio/parse.go @@ -11,6 +11,7 @@ import ( "time" "github.com/benchttp/sdk/benchttp" + "github.com/benchttp/sdk/internal/errorutil" ) // Representation is a raw data model for formatted runner config (json, yaml). @@ -248,3 +249,21 @@ func requireConfigFields(fields map[string]interface{}) error { } return nil } + +type representations []Representation + +// mergeInto successively parses the given representations into dst. +// +// The input Representation slice must never be nil or empty, otherwise it panics. +func (reprs representations) mergeInto(dst *benchttp.Runner) error { + if len(reprs) == 0 { // supposedly catched upstream, should not occur + panicInternal("parseAndMergeReprs", "nil or empty []Representation") + } + + for _, repr := range reprs { + if err := repr.Into(dst); err != nil { + return errorutil.WithDetails(ErrFileParse, err) + } + } + return nil +} From 0b90a8492f87f2f2b02e9d426982df61b964d239 Mon Sep 17 00:00:00 2001 From: Gregory Albouy Date: Mon, 31 Oct 2022 00:16:21 +0100 Subject: [PATCH 36/53] refactor(configio) rename file parse.go -> representation.go --- configio/{parse.go => representation.go} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename configio/{parse.go => representation.go} (100%) diff --git a/configio/parse.go b/configio/representation.go similarity index 100% rename from configio/parse.go rename to configio/representation.go From 1dbd54e2ff157221da55f37b949d7d6bf27451c8 Mon Sep 17 00:00:00 2001 From: Gregory Albouy Date: Tue, 1 Nov 2022 11:43:11 +0100 Subject: [PATCH 37/53] refactor(configio): Extension -> Format --- configio/decoder.go | 27 +++++++++++++-------------- configio/file.go | 18 ++++++++++++++---- 2 files changed, 27 insertions(+), 18 deletions(-) diff --git a/configio/decoder.go b/configio/decoder.go index 0a93511..0014f70 100644 --- a/configio/decoder.go +++ b/configio/decoder.go @@ -2,7 +2,7 @@ package configio import ( "bytes" - "errors" + "fmt" "github.com/benchttp/sdk/benchttp" ) @@ -12,24 +12,23 @@ type Decoder interface { DecodeRunner(dst *benchttp.Runner) error } -type Extension string +type Format string const ( - ExtYML Extension = ".yml" - ExtYAML Extension = ".yaml" - ExtJSON Extension = ".json" + FormatJSON Format = "json" + FormatYAML Format = "yaml" ) -// DecoderOf returns the appropriate Decoder for the given extension, -// or a non-nil error if ext is not an expected extension. -func DecoderOf(ext Extension, in []byte) (Decoder, error) { +// DecoderOf returns the appropriate Decoder for the given Format. +// It panics if the format is not a Format declared in configio. +func DecoderOf(format Format, in []byte) Decoder { r := bytes.NewReader(in) - switch ext { - case ExtYML, ExtYAML: - return NewYAMLDecoder(r), nil - case ExtJSON: - return NewJSONDecoder(r), nil + switch format { + case FormatYAML: + return NewYAMLDecoder(r) + case FormatJSON: + return NewJSONDecoder(r) default: - return nil, errors.New("unsupported config format") + panic(fmt.Sprintf("configio.DecoderOf: unexpected format: %q", format)) } } diff --git a/configio/file.go b/configio/file.go index 6856ff7..f0aff7d 100644 --- a/configio/file.go +++ b/configio/file.go @@ -83,19 +83,29 @@ func (f *file) decode() (err error) { return errorutil.WithDetails(ErrFileRead, f.path, err) } - ext := Extension(filepath.Ext(f.path)) - dec, err := DecoderOf(ext, b) + ext, err := f.format() if err != nil { - return errorutil.WithDetails(ErrFileExt, ext, err) + return err } - if err = dec.Decode(&f.repr); err != nil { + if err := DecoderOf(ext, b).Decode(&f.repr); err != nil { return errorutil.WithDetails(ErrFileParse, f.path, err) } return nil } +func (f file) format() (Format, error) { + switch ext := filepath.Ext(f.path); ext { + case ".yml", ".yaml": + return FormatYAML, nil + case ".json": + return FormatJSON, nil + default: + return "", errorutil.WithDetails(ErrFileExt, ext, f.path) + } +} + func (f file) extend(nextPath string) file { return file{prev: &f, path: nextPath} } From a63a7f87a83c874aaedbe2ef3c74b5a54a774375 Mon Sep 17 00:00:00 2001 From: Gregory Albouy Date: Sat, 5 Nov 2022 13:09:56 +0100 Subject: [PATCH 38/53] feat(configio): implement Builder --- configio/builder.go | 161 +++++++++++++++++++++++++++++++++++++ configio/builder_test.go | 148 ++++++++++++++++++++++++++++++++++ configio/representation.go | 4 + 3 files changed, 313 insertions(+) create mode 100644 configio/builder.go create mode 100644 configio/builder_test.go diff --git a/configio/builder.go b/configio/builder.go new file mode 100644 index 0000000..ad15021 --- /dev/null +++ b/configio/builder.go @@ -0,0 +1,161 @@ +package configio + +import ( + "io" + "net/http" + "net/url" + "time" + + "github.com/benchttp/sdk/benchttp" +) + +type Builder struct { + // TODO: benchmark this vs []func(*benchttp.Runner) + modifier func(*benchttp.Runner) +} + +func (b *Builder) WriteJSON(in []byte) error { + return b.decodeAndWrite(in, FormatJSON) +} + +func (b *Builder) WriteYAML(in []byte) error { + return b.decodeAndWrite(in, FormatYAML) +} + +func (b *Builder) decodeAndWrite(in []byte, format Format) error { + repr := Representation{} + if err := DecoderOf(format, in).Decode(&repr); err != nil { + return err + } + if err := repr.validate(); err != nil { + return err + } + b.pipe(func(dst *benchttp.Runner) { + _ = repr.Into(dst) + }) + return nil +} + +func (b *Builder) Runner() benchttp.Runner { + runner := benchttp.Runner{} + b.into(&runner) + return runner +} + +func (b *Builder) Into(dst *benchttp.Runner) { + b.into(dst) +} + +func (b *Builder) into(dst *benchttp.Runner) { + if b.modifier == nil { + return + } + b.modifier(dst) +} + +// setters + +func (b *Builder) SetRequest(r *http.Request) { + b.pipe(func(runner *benchttp.Runner) { + runner.Request = r + }) +} + +func (b *Builder) SetRequestMethod(v string) { + b.pipe(func(runner *benchttp.Runner) { + if runner.Request == nil { + runner.Request = &http.Request{} + } + runner.Request.Method = v + }) +} + +func (b *Builder) SetRequestURL(v *url.URL) { + b.pipe(func(runner *benchttp.Runner) { + if runner.Request == nil { + runner.Request = &http.Request{} + } + runner.Request.URL = v + }) +} + +func (b *Builder) SetRequestHeader(v http.Header) { + b.SetRequestHeaderFunc(func(_ http.Header) http.Header { + return v + }) +} + +func (b *Builder) SetRequestHeaderFunc(f func(prev http.Header) http.Header) { + b.pipe(func(runner *benchttp.Runner) { + if runner.Request == nil { + runner.Request = &http.Request{} + } + runner.Request.Header = f(runner.Request.Header) + }) +} + +func (b *Builder) SetRequestBody(v io.ReadCloser) { + b.pipe(func(runner *benchttp.Runner) { + if runner.Request == nil { + runner.Request = &http.Request{} + } + runner.Request.Body = v + }) +} + +func (b *Builder) SetRequests(v int) { + b.pipe(func(runner *benchttp.Runner) { + runner.Requests = v + }) +} + +func (b *Builder) SetConcurrency(v int) { + b.pipe(func(runner *benchttp.Runner) { + runner.Concurrency = v + }) +} + +func (b *Builder) SetInterval(v time.Duration) { + b.pipe(func(runner *benchttp.Runner) { + runner.Interval = v + }) +} + +func (b *Builder) SetRequestTimeout(v time.Duration) { + b.pipe(func(runner *benchttp.Runner) { + runner.RequestTimeout = v + }) +} + +func (b *Builder) SetGlobalTimeout(v time.Duration) { + b.pipe(func(runner *benchttp.Runner) { + runner.GlobalTimeout = v + }) +} + +func (b *Builder) SetTests(v []benchttp.TestCase) { + b.pipe(func(runner *benchttp.Runner) { + runner.Tests = v + }) +} + +func (b *Builder) AddTests(v ...benchttp.TestCase) { + b.pipe(func(runner *benchttp.Runner) { + runner.Tests = append(runner.Tests, v...) + }) +} + +func (b *Builder) pipe(modifier func(runner *benchttp.Runner)) { + if modifier == nil { + panicInternal("Builder.pipe", "call with nil modifier") + } + if b.modifier == nil { + b.modifier = modifier + return + } + applyPreviousModifier := b.modifier + b.modifier = func(dst *benchttp.Runner) { + applyPreviousModifier(dst) + modifier(dst) + } +} diff --git a/configio/builder_test.go b/configio/builder_test.go new file mode 100644 index 0000000..6dc95ee --- /dev/null +++ b/configio/builder_test.go @@ -0,0 +1,148 @@ +package configio_test + +import ( + "io" + "net/http" + "net/http/httptest" + "net/url" + "strings" + "testing" + "time" + + "github.com/benchttp/sdk/benchttp" + "github.com/benchttp/sdk/benchttptest" + "github.com/benchttp/sdk/configio" +) + +func TestBuilder_WriteJSON(t *testing.T) { + in := []byte(`{"runner":{"requests": 5}}`) + dest := benchttp.Runner{Requests: 0, Concurrency: 2} + want := benchttp.Runner{Requests: 5, Concurrency: 2} + + b := configio.Builder{} + if err := b.WriteJSON(in); err != nil { + t.Fatal(err) + } + b.Into(&dest) + + benchttptest.AssertEqualRunners(t, want, dest) +} + +func TestBuilder_WriteYAML(t *testing.T) { + in := []byte(`runner: { requests: 5 }`) + dest := benchttp.Runner{Requests: 0, Concurrency: 2} + want := benchttp.Runner{Requests: 5, Concurrency: 2} + + b := configio.Builder{} + if err := b.WriteYAML(in); err != nil { + t.Fatal(err) + } + b.Into(&dest) + + benchttptest.AssertEqualRunners(t, want, dest) +} + +func TestBuilder_Set(t *testing.T) { + t.Run("basic fields", func(t *testing.T) { + want := benchttp.Runner{ + Requests: 5, + Concurrency: 2, + Interval: 10 * time.Millisecond, + RequestTimeout: 1 * time.Second, + GlobalTimeout: 10 * time.Second, + } + + b := configio.Builder{} + b.SetRequests(want.Requests) + b.SetConcurrency(-1) + b.SetConcurrency(want.Concurrency) + b.SetInterval(want.Interval) + b.SetRequestTimeout(want.RequestTimeout) + b.SetGlobalTimeout(want.GlobalTimeout) + + benchttptest.AssertEqualRunners(t, want, b.Runner()) + }) + + t.Run("request", func(t *testing.T) { + want := benchttp.Runner{ + Request: httptest.NewRequest("GET", "https://example.com", nil), + } + + b := configio.Builder{} + b.SetRequest(want.Request) + + benchttptest.AssertEqualRunners(t, want, b.Runner()) + }) + + t.Run("request fields", func(t *testing.T) { + want := benchttp.Runner{ + Request: &http.Request{ + Method: "PUT", + URL: mustParseRequestURI("https://example.com"), + Header: http.Header{ + "API_KEY": []string{"abc"}, + "Accept": []string{"text/html", "application/json"}, + }, + Body: readcloser("hello"), + }, + } + + b := configio.Builder{} + b.SetRequestMethod(want.Request.Method) + b.SetRequestURL(want.Request.URL) + b.SetRequestHeader(http.Header{"API_KEY": []string{"abc"}}) + b.SetRequestHeaderFunc(func(prev http.Header) http.Header { + prev.Add("Accept", "text/html") + prev.Add("Accept", "application/json") + return prev + }) + b.SetRequestBody(readcloser("hello")) + + benchttptest.AssertEqualRunners(t, want, b.Runner()) + }) + + t.Run("test cases", func(t *testing.T) { + want := benchttp.Runner{ + Tests: []benchttp.TestCase{ + { + Name: "maximum response time", + Field: "ResponseTimes.Max", + Predicate: "LT", + Target: 100 * time.Millisecond, + }, + { + Name: "similar response times", + Field: "ResponseTimes.StdDev", + Predicate: "LTE", + Target: 20 * time.Millisecond, + }, + { + Name: "100% availability", + Field: "RequestFailureCount", + Predicate: "EQ", + Target: 0, + }, + }, + } + + b := configio.Builder{} + b.SetTests([]benchttp.TestCase{want.Tests[0]}) + b.AddTests(want.Tests[1:]...) + + benchttptest.AssertEqualRunners(t, want, b.Runner()) + }) +} + +// helpers + +func mustParseRequestURI(s string) *url.URL { + u, err := url.ParseRequestURI(s) + if err != nil { + panic("mustParseRequestURI: " + err.Error()) + } + return u +} + +func readcloser(s string) io.ReadCloser { + return io.NopCloser(strings.NewReader(s)) +} diff --git a/configio/representation.go b/configio/representation.go index c668f0d..e13f2db 100644 --- a/configio/representation.go +++ b/configio/representation.go @@ -49,6 +49,10 @@ type Representation struct { } `yaml:"tests" json:"tests"` } +func (repr Representation) validate() error { + return repr.Into(&benchttp.Runner{}) +} + // Into parses the Representation receiver as a benchttp.Runner // and stores any non-nil field value into the corresponding field // of dst. From 5e9268fde8a97f65265987e6a56d9d9fc87497fb Mon Sep 17 00:00:00 2001 From: Gregory Albouy Date: Sun, 6 Nov 2022 17:44:16 +0100 Subject: [PATCH 39/53] doc(configio): write ExampleBuilder example --- configio/example_test.go | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 configio/example_test.go diff --git a/configio/example_test.go b/configio/example_test.go new file mode 100644 index 0000000..4efe5b2 --- /dev/null +++ b/configio/example_test.go @@ -0,0 +1,40 @@ +package configio_test + +import ( + "fmt" + "time" + + "github.com/benchttp/sdk/benchttp" + "github.com/benchttp/sdk/configio" +) + +var jsonConfig = []byte( + `{"request": {"method": "GET", "url": "https://example.com"}}`, +) + +var yamlConfig = []byte( + `{request: {method: PUT}, runner: {requests: 42}}`, +) + +func ExampleBuilder() { + runner := benchttp.Runner{Requests: -1, Concurrency: 3} + + b := configio.Builder{} + _ = b.WriteJSON(jsonConfig) + _ = b.WriteYAML(yamlConfig) + b.SetInterval(100 * time.Millisecond) + + b.Into(&runner) + + // Output: + // PUT + // https://example.com + // 42 + // 3 + // 100ms + fmt.Println(runner.Request.Method) + fmt.Println(runner.Request.URL) + fmt.Println(runner.Requests) + fmt.Println(runner.Concurrency) + fmt.Println(runner.Interval) +} From 731547e2925a323b1c568c3c440e0aab637c58fd Mon Sep 17 00:00:00 2001 From: Gregory Albouy Date: Sun, 6 Nov 2022 19:08:54 +0100 Subject: [PATCH 40/53] tmp: benchmark pipe vs append implementations for configio.Builder --- configio/builder_append.go | 151 +++++++++++++++++++++++++++++ configio/builder_append_test.go | 131 +++++++++++++++++++++++++ configio/builder_benchmark_test.go | 75 ++++++++++++++ 3 files changed, 357 insertions(+) create mode 100644 configio/builder_append.go create mode 100644 configio/builder_append_test.go create mode 100644 configio/builder_benchmark_test.go diff --git a/configio/builder_append.go b/configio/builder_append.go new file mode 100644 index 0000000..d067cd1 --- /dev/null +++ b/configio/builder_append.go @@ -0,0 +1,151 @@ +package configio + +import ( + "io" + "net/http" + "net/url" + "time" + + "github.com/benchttp/sdk/benchttp" +) + +type Builder_append struct { // nolint:revive + modifiers []func(*benchttp.Runner) +} + +func (b *Builder_append) WriteJSON(in []byte) error { + return b.decodeAndWrite(in, FormatJSON) +} + +func (b *Builder_append) WriteYAML(in []byte) error { + return b.decodeAndWrite(in, FormatYAML) +} + +func (b *Builder_append) decodeAndWrite(in []byte, format Format) error { + repr := Representation{} + if err := DecoderOf(format, in).Decode(&repr); err != nil { + return err + } + if err := repr.validate(); err != nil { + return err + } + b.append(func(dst *benchttp.Runner) { + _ = repr.Into(dst) + }) + return nil +} + +func (b *Builder_append) Runner() benchttp.Runner { + runner := benchttp.Runner{} + b.into(&runner) + return runner +} + +func (b *Builder_append) Into(dst *benchttp.Runner) { + b.into(dst) +} + +func (b *Builder_append) into(dst *benchttp.Runner) { + for _, modify := range b.modifiers { + modify(dst) + } +} + +// setters + +func (b *Builder_append) SetRequest(r *http.Request) { + b.append(func(runner *benchttp.Runner) { + runner.Request = r + }) +} + +func (b *Builder_append) SetRequestMethod(v string) { + b.append(func(runner *benchttp.Runner) { + if runner.Request == nil { + runner.Request = &http.Request{} + } + runner.Request.Method = v + }) +} + +func (b *Builder_append) SetRequestURL(v *url.URL) { + b.append(func(runner *benchttp.Runner) { + if runner.Request == nil { + runner.Request = &http.Request{} + } + runner.Request.URL = v + }) +} + +func (b *Builder_append) SetRequestHeader(v http.Header) { + b.SetRequestHeaderFunc(func(_ http.Header) http.Header { + return v + }) +} + +func (b *Builder_append) SetRequestHeaderFunc(f func(prev http.Header) http.Header) { + b.append(func(runner *benchttp.Runner) { + if runner.Request == nil { + runner.Request = &http.Request{} + } + runner.Request.Header = f(runner.Request.Header) + }) +} + +func (b *Builder_append) SetRequestBody(v io.ReadCloser) { + b.append(func(runner *benchttp.Runner) { + if runner.Request == nil { + runner.Request = &http.Request{} + } + runner.Request.Body = v + }) +} + +func (b *Builder_append) SetRequests(v int) { + b.append(func(runner *benchttp.Runner) { + runner.Requests = v + }) +} + +func (b *Builder_append) SetConcurrency(v int) { + b.append(func(runner *benchttp.Runner) { + runner.Concurrency = v + }) +} + +func (b *Builder_append) SetInterval(v time.Duration) { + b.append(func(runner *benchttp.Runner) { + runner.Interval = v + }) +} + +func (b *Builder_append) SetRequestTimeout(v time.Duration) { + b.append(func(runner *benchttp.Runner) { + runner.RequestTimeout = v + }) +} + +func (b *Builder_append) SetGlobalTimeout(v time.Duration) { + b.append(func(runner *benchttp.Runner) { + runner.GlobalTimeout = v + }) +} + +func (b *Builder_append) SetTests(v []benchttp.TestCase) { + b.append(func(runner *benchttp.Runner) { + runner.Tests = v + }) +} + +func (b *Builder_append) AddTests(v ...benchttp.TestCase) { + b.append(func(runner *benchttp.Runner) { + runner.Tests = append(runner.Tests, v...) + }) +} + +func (b *Builder_append) append(modifier func(runner *benchttp.Runner)) { + if modifier == nil { + panicInternal("Builder.append", "call with nil modifier") + } + b.modifiers = append(b.modifiers, modifier) +} diff --git a/configio/builder_append_test.go b/configio/builder_append_test.go new file mode 100644 index 0000000..0a6d2ab --- /dev/null +++ b/configio/builder_append_test.go @@ -0,0 +1,131 @@ +package configio_test + +import ( + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/benchttp/sdk/benchttp" + "github.com/benchttp/sdk/benchttptest" + "github.com/benchttp/sdk/configio" +) + +func TestBuilder_append_WriteJSON(t *testing.T) { + in := []byte(`{"runner":{"requests": 5}}`) + dest := benchttp.Runner{Requests: 0, Concurrency: 2} + want := benchttp.Runner{Requests: 5, Concurrency: 2} + + b := configio.Builder_append{} + if err := b.WriteJSON(in); err != nil { + t.Fatal(err) + } + b.Into(&dest) + + benchttptest.AssertEqualRunners(t, want, dest) +} + +func TestBuilder_append_WriteYAML(t *testing.T) { + in := []byte(`runner: { requests: 5 }`) + dest := benchttp.Runner{Requests: 0, Concurrency: 2} + want := benchttp.Runner{Requests: 5, Concurrency: 2} + + b := configio.Builder_append{} + if err := b.WriteYAML(in); err != nil { + t.Fatal(err) + } + b.Into(&dest) + + benchttptest.AssertEqualRunners(t, want, dest) +} + +func TestBuilder_append_Set(t *testing.T) { + t.Run("basic fields", func(t *testing.T) { + want := benchttp.Runner{ + Requests: 5, + Concurrency: 2, + Interval: 10 * time.Millisecond, + RequestTimeout: 1 * time.Second, + GlobalTimeout: 10 * time.Second, + } + + b := configio.Builder_append{} + b.SetRequests(want.Requests) + b.SetConcurrency(-1) + b.SetConcurrency(want.Concurrency) + b.SetInterval(want.Interval) + b.SetRequestTimeout(want.RequestTimeout) + b.SetGlobalTimeout(want.GlobalTimeout) + + benchttptest.AssertEqualRunners(t, want, b.Runner()) + }) + + t.Run("request", func(t *testing.T) { + want := benchttp.Runner{ + Request: httptest.NewRequest("GET", "https://example.com", nil), + } + + b := configio.Builder_append{} + b.SetRequest(want.Request) + + benchttptest.AssertEqualRunners(t, want, b.Runner()) + }) + + t.Run("request fields", func(t *testing.T) { + want := benchttp.Runner{ + Request: &http.Request{ + Method: "PUT", + URL: mustParseRequestURI("https://example.com"), + Header: http.Header{ + "API_KEY": []string{"abc"}, + "Accept": []string{"text/html", "application/json"}, + }, + Body: readcloser("hello"), + }, + } + + b := configio.Builder_append{} + b.SetRequestMethod(want.Request.Method) + b.SetRequestURL(want.Request.URL) + b.SetRequestHeader(http.Header{"API_KEY": []string{"abc"}}) + b.SetRequestHeaderFunc(func(prev http.Header) http.Header { + prev.Add("Accept", "text/html") + prev.Add("Accept", "application/json") + return prev + }) + b.SetRequestBody(readcloser("hello")) + + benchttptest.AssertEqualRunners(t, want, b.Runner()) + }) + + t.Run("test cases", func(t *testing.T) { + want := benchttp.Runner{ + Tests: []benchttp.TestCase{ + { + Name: "maximum response time", + Field: "ResponseTimes.Max", + Predicate: "LT", + Target: 100 * time.Millisecond, + }, + { + Name: "similar response times", + Field: "ResponseTimes.StdDev", + Predicate: "LTE", + Target: 20 * time.Millisecond, + }, + { + Name: "100% availability", + Field: "RequestFailureCount", + Predicate: "EQ", + Target: 0, + }, + }, + } + + b := configio.Builder_append{} + b.SetTests([]benchttp.TestCase{want.Tests[0]}) + b.AddTests(want.Tests[1:]...) + + benchttptest.AssertEqualRunners(t, want, b.Runner()) + }) +} diff --git a/configio/builder_benchmark_test.go b/configio/builder_benchmark_test.go new file mode 100644 index 0000000..f4431c9 --- /dev/null +++ b/configio/builder_benchmark_test.go @@ -0,0 +1,75 @@ +package configio_test + +import ( + "strconv" + "testing" + + "github.com/benchttp/sdk/configio" +) + +/* +Output: + +Running tool: /Users/greg/sdk/go1.17.6/bin/go test -benchmem -run=^$ -bench ^(BenchmarkBuilder)$ github.com/benchttp/sdk/configio + +goos: darwin +goarch: amd64 +pkg: github.com/benchttp/sdk/configio +cpu: Intel(R) Core(TM) i5-8279U CPU @ 2.40GHz +BenchmarkBuilder/setters/Builder_pipe-8 12966073 97.47 ns/op 40 B/op 2 allocs/op +BenchmarkBuilder/setters/Builder_append-8 21533343 81.65 ns/op 64 B/op 1 allocs/op +BenchmarkBuilder/build/Builder_pipe_100-8 3044132 396.5 ns/op 96 B/op 1 allocs/op +BenchmarkBuilder/build/Builder_append_100-8 5413728 222.9 ns/op 96 B/op 1 allocs/op +BenchmarkBuilder/build/Builder_pipe_10000-8 25195 41284 ns/op 96 B/op 1 allocs/op +BenchmarkBuilder/build/Builder_append_10000-8 71463 16838 ns/op 96 B/op 1 allocs/op +BenchmarkBuilder/build/Builder_pipe_1000000-8 156 6993338 ns/op 96 B/op 1 allocs/op +BenchmarkBuilder/build/Builder_append_1000000-8 451 2730270 ns/op 96 B/op 1 allocs/op +*/ +func BenchmarkBuilder(b *testing.B) { + b.Run("setters", func(b *testing.B) { + b.Run("Builder_pipe", func(b *testing.B) { + builder := configio.Builder{} + for i := 0; i < b.N; i++ { + builder.SetConcurrency(100) + } + }) + b.Run("Builder_append", func(b *testing.B) { + builder := configio.Builder_append{} + for i := 0; i < b.N; i++ { + builder.SetConcurrency(100) + } + }) + }) + + b.Run("build", func(b *testing.B) { + for _, iter := range []int{100, 10_000, 1_000_000} { + b.Run("Builder_pipe_"+strconv.Itoa(iter), func(b *testing.B) { + builder := configio.Builder{} + setupBuilder(b, &builder, iter) + for i := 0; i < b.N; i++ { + _ = builder.Runner() + } + }) + b.Run("Builder_append_"+strconv.Itoa(iter), func(b *testing.B) { + builder := configio.Builder_append{} + setupBuilder(b, &builder, iter) + for i := 0; i < b.N; i++ { + _ = builder.Runner() + } + }) + } + }) +} + +func setupBuilder( + b *testing.B, + builder interface{ SetConcurrency(int) }, + iter int, +) { + b.Helper() + values := []int{-100, 100} + for i := 0; i < iter; i++ { + builder.SetConcurrency(values[i%2]) + } + b.ResetTimer() +} From ccd510080074cd9947b85f70950f14be43985c88 Mon Sep 17 00:00:00 2001 From: Gregory Albouy Date: Sun, 6 Nov 2022 19:09:29 +0100 Subject: [PATCH 41/53] Revert "tmp: benchmark pipe vs append implementations for configio.Builder" This reverts commit 731547e2925a323b1c568c3c440e0aab637c58fd. --- configio/builder_append.go | 151 ----------------------------- configio/builder_append_test.go | 131 ------------------------- configio/builder_benchmark_test.go | 75 -------------- 3 files changed, 357 deletions(-) delete mode 100644 configio/builder_append.go delete mode 100644 configio/builder_append_test.go delete mode 100644 configio/builder_benchmark_test.go diff --git a/configio/builder_append.go b/configio/builder_append.go deleted file mode 100644 index d067cd1..0000000 --- a/configio/builder_append.go +++ /dev/null @@ -1,151 +0,0 @@ -package configio - -import ( - "io" - "net/http" - "net/url" - "time" - - "github.com/benchttp/sdk/benchttp" -) - -type Builder_append struct { // nolint:revive - modifiers []func(*benchttp.Runner) -} - -func (b *Builder_append) WriteJSON(in []byte) error { - return b.decodeAndWrite(in, FormatJSON) -} - -func (b *Builder_append) WriteYAML(in []byte) error { - return b.decodeAndWrite(in, FormatYAML) -} - -func (b *Builder_append) decodeAndWrite(in []byte, format Format) error { - repr := Representation{} - if err := DecoderOf(format, in).Decode(&repr); err != nil { - return err - } - if err := repr.validate(); err != nil { - return err - } - b.append(func(dst *benchttp.Runner) { - _ = repr.Into(dst) - }) - return nil -} - -func (b *Builder_append) Runner() benchttp.Runner { - runner := benchttp.Runner{} - b.into(&runner) - return runner -} - -func (b *Builder_append) Into(dst *benchttp.Runner) { - b.into(dst) -} - -func (b *Builder_append) into(dst *benchttp.Runner) { - for _, modify := range b.modifiers { - modify(dst) - } -} - -// setters - -func (b *Builder_append) SetRequest(r *http.Request) { - b.append(func(runner *benchttp.Runner) { - runner.Request = r - }) -} - -func (b *Builder_append) SetRequestMethod(v string) { - b.append(func(runner *benchttp.Runner) { - if runner.Request == nil { - runner.Request = &http.Request{} - } - runner.Request.Method = v - }) -} - -func (b *Builder_append) SetRequestURL(v *url.URL) { - b.append(func(runner *benchttp.Runner) { - if runner.Request == nil { - runner.Request = &http.Request{} - } - runner.Request.URL = v - }) -} - -func (b *Builder_append) SetRequestHeader(v http.Header) { - b.SetRequestHeaderFunc(func(_ http.Header) http.Header { - return v - }) -} - -func (b *Builder_append) SetRequestHeaderFunc(f func(prev http.Header) http.Header) { - b.append(func(runner *benchttp.Runner) { - if runner.Request == nil { - runner.Request = &http.Request{} - } - runner.Request.Header = f(runner.Request.Header) - }) -} - -func (b *Builder_append) SetRequestBody(v io.ReadCloser) { - b.append(func(runner *benchttp.Runner) { - if runner.Request == nil { - runner.Request = &http.Request{} - } - runner.Request.Body = v - }) -} - -func (b *Builder_append) SetRequests(v int) { - b.append(func(runner *benchttp.Runner) { - runner.Requests = v - }) -} - -func (b *Builder_append) SetConcurrency(v int) { - b.append(func(runner *benchttp.Runner) { - runner.Concurrency = v - }) -} - -func (b *Builder_append) SetInterval(v time.Duration) { - b.append(func(runner *benchttp.Runner) { - runner.Interval = v - }) -} - -func (b *Builder_append) SetRequestTimeout(v time.Duration) { - b.append(func(runner *benchttp.Runner) { - runner.RequestTimeout = v - }) -} - -func (b *Builder_append) SetGlobalTimeout(v time.Duration) { - b.append(func(runner *benchttp.Runner) { - runner.GlobalTimeout = v - }) -} - -func (b *Builder_append) SetTests(v []benchttp.TestCase) { - b.append(func(runner *benchttp.Runner) { - runner.Tests = v - }) -} - -func (b *Builder_append) AddTests(v ...benchttp.TestCase) { - b.append(func(runner *benchttp.Runner) { - runner.Tests = append(runner.Tests, v...) - }) -} - -func (b *Builder_append) append(modifier func(runner *benchttp.Runner)) { - if modifier == nil { - panicInternal("Builder.append", "call with nil modifier") - } - b.modifiers = append(b.modifiers, modifier) -} diff --git a/configio/builder_append_test.go b/configio/builder_append_test.go deleted file mode 100644 index 0a6d2ab..0000000 --- a/configio/builder_append_test.go +++ /dev/null @@ -1,131 +0,0 @@ -package configio_test - -import ( - "net/http" - "net/http/httptest" - "testing" - "time" - - "github.com/benchttp/sdk/benchttp" - "github.com/benchttp/sdk/benchttptest" - "github.com/benchttp/sdk/configio" -) - -func TestBuilder_append_WriteJSON(t *testing.T) { - in := []byte(`{"runner":{"requests": 5}}`) - dest := benchttp.Runner{Requests: 0, Concurrency: 2} - want := benchttp.Runner{Requests: 5, Concurrency: 2} - - b := configio.Builder_append{} - if err := b.WriteJSON(in); err != nil { - t.Fatal(err) - } - b.Into(&dest) - - benchttptest.AssertEqualRunners(t, want, dest) -} - -func TestBuilder_append_WriteYAML(t *testing.T) { - in := []byte(`runner: { requests: 5 }`) - dest := benchttp.Runner{Requests: 0, Concurrency: 2} - want := benchttp.Runner{Requests: 5, Concurrency: 2} - - b := configio.Builder_append{} - if err := b.WriteYAML(in); err != nil { - t.Fatal(err) - } - b.Into(&dest) - - benchttptest.AssertEqualRunners(t, want, dest) -} - -func TestBuilder_append_Set(t *testing.T) { - t.Run("basic fields", func(t *testing.T) { - want := benchttp.Runner{ - Requests: 5, - Concurrency: 2, - Interval: 10 * time.Millisecond, - RequestTimeout: 1 * time.Second, - GlobalTimeout: 10 * time.Second, - } - - b := configio.Builder_append{} - b.SetRequests(want.Requests) - b.SetConcurrency(-1) - b.SetConcurrency(want.Concurrency) - b.SetInterval(want.Interval) - b.SetRequestTimeout(want.RequestTimeout) - b.SetGlobalTimeout(want.GlobalTimeout) - - benchttptest.AssertEqualRunners(t, want, b.Runner()) - }) - - t.Run("request", func(t *testing.T) { - want := benchttp.Runner{ - Request: httptest.NewRequest("GET", "https://example.com", nil), - } - - b := configio.Builder_append{} - b.SetRequest(want.Request) - - benchttptest.AssertEqualRunners(t, want, b.Runner()) - }) - - t.Run("request fields", func(t *testing.T) { - want := benchttp.Runner{ - Request: &http.Request{ - Method: "PUT", - URL: mustParseRequestURI("https://example.com"), - Header: http.Header{ - "API_KEY": []string{"abc"}, - "Accept": []string{"text/html", "application/json"}, - }, - Body: readcloser("hello"), - }, - } - - b := configio.Builder_append{} - b.SetRequestMethod(want.Request.Method) - b.SetRequestURL(want.Request.URL) - b.SetRequestHeader(http.Header{"API_KEY": []string{"abc"}}) - b.SetRequestHeaderFunc(func(prev http.Header) http.Header { - prev.Add("Accept", "text/html") - prev.Add("Accept", "application/json") - return prev - }) - b.SetRequestBody(readcloser("hello")) - - benchttptest.AssertEqualRunners(t, want, b.Runner()) - }) - - t.Run("test cases", func(t *testing.T) { - want := benchttp.Runner{ - Tests: []benchttp.TestCase{ - { - Name: "maximum response time", - Field: "ResponseTimes.Max", - Predicate: "LT", - Target: 100 * time.Millisecond, - }, - { - Name: "similar response times", - Field: "ResponseTimes.StdDev", - Predicate: "LTE", - Target: 20 * time.Millisecond, - }, - { - Name: "100% availability", - Field: "RequestFailureCount", - Predicate: "EQ", - Target: 0, - }, - }, - } - - b := configio.Builder_append{} - b.SetTests([]benchttp.TestCase{want.Tests[0]}) - b.AddTests(want.Tests[1:]...) - - benchttptest.AssertEqualRunners(t, want, b.Runner()) - }) -} diff --git a/configio/builder_benchmark_test.go b/configio/builder_benchmark_test.go deleted file mode 100644 index f4431c9..0000000 --- a/configio/builder_benchmark_test.go +++ /dev/null @@ -1,75 +0,0 @@ -package configio_test - -import ( - "strconv" - "testing" - - "github.com/benchttp/sdk/configio" -) - -/* -Output: - -Running tool: /Users/greg/sdk/go1.17.6/bin/go test -benchmem -run=^$ -bench ^(BenchmarkBuilder)$ github.com/benchttp/sdk/configio - -goos: darwin -goarch: amd64 -pkg: github.com/benchttp/sdk/configio -cpu: Intel(R) Core(TM) i5-8279U CPU @ 2.40GHz -BenchmarkBuilder/setters/Builder_pipe-8 12966073 97.47 ns/op 40 B/op 2 allocs/op -BenchmarkBuilder/setters/Builder_append-8 21533343 81.65 ns/op 64 B/op 1 allocs/op -BenchmarkBuilder/build/Builder_pipe_100-8 3044132 396.5 ns/op 96 B/op 1 allocs/op -BenchmarkBuilder/build/Builder_append_100-8 5413728 222.9 ns/op 96 B/op 1 allocs/op -BenchmarkBuilder/build/Builder_pipe_10000-8 25195 41284 ns/op 96 B/op 1 allocs/op -BenchmarkBuilder/build/Builder_append_10000-8 71463 16838 ns/op 96 B/op 1 allocs/op -BenchmarkBuilder/build/Builder_pipe_1000000-8 156 6993338 ns/op 96 B/op 1 allocs/op -BenchmarkBuilder/build/Builder_append_1000000-8 451 2730270 ns/op 96 B/op 1 allocs/op -*/ -func BenchmarkBuilder(b *testing.B) { - b.Run("setters", func(b *testing.B) { - b.Run("Builder_pipe", func(b *testing.B) { - builder := configio.Builder{} - for i := 0; i < b.N; i++ { - builder.SetConcurrency(100) - } - }) - b.Run("Builder_append", func(b *testing.B) { - builder := configio.Builder_append{} - for i := 0; i < b.N; i++ { - builder.SetConcurrency(100) - } - }) - }) - - b.Run("build", func(b *testing.B) { - for _, iter := range []int{100, 10_000, 1_000_000} { - b.Run("Builder_pipe_"+strconv.Itoa(iter), func(b *testing.B) { - builder := configio.Builder{} - setupBuilder(b, &builder, iter) - for i := 0; i < b.N; i++ { - _ = builder.Runner() - } - }) - b.Run("Builder_append_"+strconv.Itoa(iter), func(b *testing.B) { - builder := configio.Builder_append{} - setupBuilder(b, &builder, iter) - for i := 0; i < b.N; i++ { - _ = builder.Runner() - } - }) - } - }) -} - -func setupBuilder( - b *testing.B, - builder interface{ SetConcurrency(int) }, - iter int, -) { - b.Helper() - values := []int{-100, 100} - for i := 0; i < iter; i++ { - builder.SetConcurrency(values[i%2]) - } - b.ResetTimer() -} From 07aa5d2351f928a270540c1d201e9fa21e3c610a Mon Sep 17 00:00:00 2001 From: Gregory Albouy Date: Sun, 6 Nov 2022 19:42:44 +0100 Subject: [PATCH 42/53] perf(configio): use an array of modifiers for Builder Previous benchmarks showed 2.5x better performance over piped modifier. --- configio/builder.go | 48 ++++++++++++++++++--------------------------- 1 file changed, 19 insertions(+), 29 deletions(-) diff --git a/configio/builder.go b/configio/builder.go index ad15021..b306f4f 100644 --- a/configio/builder.go +++ b/configio/builder.go @@ -10,8 +10,7 @@ import ( ) type Builder struct { - // TODO: benchmark this vs []func(*benchttp.Runner) - modifier func(*benchttp.Runner) + modifiers []func(*benchttp.Runner) } func (b *Builder) WriteJSON(in []byte) error { @@ -30,7 +29,7 @@ func (b *Builder) decodeAndWrite(in []byte, format Format) error { if err := repr.validate(); err != nil { return err } - b.pipe(func(dst *benchttp.Runner) { + b.append(func(dst *benchttp.Runner) { _ = repr.Into(dst) }) return nil @@ -47,22 +46,21 @@ func (b *Builder) Into(dst *benchttp.Runner) { } func (b *Builder) into(dst *benchttp.Runner) { - if b.modifier == nil { - return + for _, modify := range b.modifiers { + modify(dst) } - b.modifier(dst) } // setters func (b *Builder) SetRequest(r *http.Request) { - b.pipe(func(runner *benchttp.Runner) { + b.append(func(runner *benchttp.Runner) { runner.Request = r }) } func (b *Builder) SetRequestMethod(v string) { - b.pipe(func(runner *benchttp.Runner) { + b.append(func(runner *benchttp.Runner) { if runner.Request == nil { runner.Request = &http.Request{} } @@ -71,7 +69,7 @@ func (b *Builder) SetRequestMethod(v string) { } func (b *Builder) SetRequestURL(v *url.URL) { - b.pipe(func(runner *benchttp.Runner) { + b.append(func(runner *benchttp.Runner) { if runner.Request == nil { runner.Request = &http.Request{} } @@ -86,7 +84,7 @@ func (b *Builder) SetRequestHeader(v http.Header) { } func (b *Builder) SetRequestHeaderFunc(f func(prev http.Header) http.Header) { - b.pipe(func(runner *benchttp.Runner) { + b.append(func(runner *benchttp.Runner) { if runner.Request == nil { runner.Request = &http.Request{} } @@ -95,7 +93,7 @@ func (b *Builder) SetRequestHeaderFunc(f func(prev http.Header) http.Header) { } func (b *Builder) SetRequestBody(v io.ReadCloser) { - b.pipe(func(runner *benchttp.Runner) { + b.append(func(runner *benchttp.Runner) { if runner.Request == nil { runner.Request = &http.Request{} } @@ -104,58 +102,50 @@ func (b *Builder) SetRequestBody(v io.ReadCloser) { } func (b *Builder) SetRequests(v int) { - b.pipe(func(runner *benchttp.Runner) { + b.append(func(runner *benchttp.Runner) { runner.Requests = v }) } func (b *Builder) SetConcurrency(v int) { - b.pipe(func(runner *benchttp.Runner) { + b.append(func(runner *benchttp.Runner) { runner.Concurrency = v }) } func (b *Builder) SetInterval(v time.Duration) { - b.pipe(func(runner *benchttp.Runner) { + b.append(func(runner *benchttp.Runner) { runner.Interval = v }) } func (b *Builder) SetRequestTimeout(v time.Duration) { - b.pipe(func(runner *benchttp.Runner) { + b.append(func(runner *benchttp.Runner) { runner.RequestTimeout = v }) } func (b *Builder) SetGlobalTimeout(v time.Duration) { - b.pipe(func(runner *benchttp.Runner) { + b.append(func(runner *benchttp.Runner) { runner.GlobalTimeout = v }) } func (b *Builder) SetTests(v []benchttp.TestCase) { - b.pipe(func(runner *benchttp.Runner) { + b.append(func(runner *benchttp.Runner) { runner.Tests = v }) } func (b *Builder) AddTests(v ...benchttp.TestCase) { - b.pipe(func(runner *benchttp.Runner) { + b.append(func(runner *benchttp.Runner) { runner.Tests = append(runner.Tests, v...) }) } -func (b *Builder) pipe(modifier func(runner *benchttp.Runner)) { +func (b *Builder) append(modifier func(runner *benchttp.Runner)) { if modifier == nil { - panicInternal("Builder.pipe", "call with nil modifier") - } - if b.modifier == nil { - b.modifier = modifier - return - } - applyPreviousModifier := b.modifier - b.modifier = func(dst *benchttp.Runner) { - applyPreviousModifier(dst) - modifier(dst) + panicInternal("Builder.append", "call with nil modifier") } + b.modifiers = append(b.modifiers, modifier) } From e6e80622c1003fae27a113337ba966bd1cebe1e0 Mon Sep 17 00:00:00 2001 From: Gregory Albouy Date: Sun, 6 Nov 2022 20:12:54 +0100 Subject: [PATCH 43/53] refactor(configio): Builder renamings - Builder.Into -> Builder.Mutate - Builder.modifiers -> Builder.mutations --- configio/builder.go | 16 ++++++---------- configio/builder_test.go | 4 ++-- configio/example_test.go | 2 +- 3 files changed, 9 insertions(+), 13 deletions(-) diff --git a/configio/builder.go b/configio/builder.go index b306f4f..a541896 100644 --- a/configio/builder.go +++ b/configio/builder.go @@ -10,7 +10,7 @@ import ( ) type Builder struct { - modifiers []func(*benchttp.Runner) + mutations []func(*benchttp.Runner) } func (b *Builder) WriteJSON(in []byte) error { @@ -37,17 +37,13 @@ func (b *Builder) decodeAndWrite(in []byte, format Format) error { func (b *Builder) Runner() benchttp.Runner { runner := benchttp.Runner{} - b.into(&runner) + b.Mutate(&runner) return runner } -func (b *Builder) Into(dst *benchttp.Runner) { - b.into(dst) -} - -func (b *Builder) into(dst *benchttp.Runner) { - for _, modify := range b.modifiers { - modify(dst) +func (b *Builder) Mutate(dst *benchttp.Runner) { + for _, mutate := range b.mutations { + mutate(dst) } } @@ -147,5 +143,5 @@ func (b *Builder) append(modifier func(runner *benchttp.Runner)) { if modifier == nil { panicInternal("Builder.append", "call with nil modifier") } - b.modifiers = append(b.modifiers, modifier) + b.mutations = append(b.mutations, modifier) } diff --git a/configio/builder_test.go b/configio/builder_test.go index 6dc95ee..4123882 100644 --- a/configio/builder_test.go +++ b/configio/builder_test.go @@ -23,7 +23,7 @@ func TestBuilder_WriteJSON(t *testing.T) { if err := b.WriteJSON(in); err != nil { t.Fatal(err) } - b.Into(&dest) + b.Mutate(&dest) benchttptest.AssertEqualRunners(t, want, dest) } @@ -37,7 +37,7 @@ func TestBuilder_WriteYAML(t *testing.T) { if err := b.WriteYAML(in); err != nil { t.Fatal(err) } - b.Into(&dest) + b.Mutate(&dest) benchttptest.AssertEqualRunners(t, want, dest) } diff --git a/configio/example_test.go b/configio/example_test.go index 4efe5b2..b2f86dc 100644 --- a/configio/example_test.go +++ b/configio/example_test.go @@ -24,7 +24,7 @@ func ExampleBuilder() { _ = b.WriteYAML(yamlConfig) b.SetInterval(100 * time.Millisecond) - b.Into(&runner) + b.Mutate(&runner) // Output: // PUT From 61ff33c6d9d9ff1bb58d5a8a0e096ac782453a30 Mon Sep 17 00:00:00 2001 From: Gregory Albouy Date: Sun, 6 Nov 2022 20:44:56 +0100 Subject: [PATCH 44/53] doc(configio): add documenting comments for Builder --- configio/builder.go | 42 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/configio/builder.go b/configio/builder.go index a541896..5ed43ac 100644 --- a/configio/builder.go +++ b/configio/builder.go @@ -9,14 +9,25 @@ import ( "github.com/benchttp/sdk/benchttp" ) +// A Builder is used to incrementally build a benchttp.Runner +// using Set and Write methods. +// The zero value is ready to use. type Builder struct { mutations []func(*benchttp.Runner) } +// WriteJSON decodes the input bytes as a JSON benchttp configuration +// and appends the resulting modifier to the builder. +// It returns any encountered during the decoding process or if the +// decoded configuration is invalid. func (b *Builder) WriteJSON(in []byte) error { return b.decodeAndWrite(in, FormatJSON) } +// WriteYAML decodes the input bytes as a YAML benchttp configuration +// and appends the resulting modifier to the builder. +// It returns any encountered during the decoding process or if the +// decoded configuration is invalid. func (b *Builder) WriteYAML(in []byte) error { return b.decodeAndWrite(in, FormatYAML) } @@ -26,21 +37,29 @@ func (b *Builder) decodeAndWrite(in []byte, format Format) error { if err := DecoderOf(format, in).Decode(&repr); err != nil { return err } + // early check for invalid configuration if err := repr.validate(); err != nil { return err } b.append(func(dst *benchttp.Runner) { - _ = repr.Into(dst) + // err is already checked via repr.validate(), so nil is expected. + if err := repr.Into(dst); err != nil { + panicInternal("Builder.decodeAndWrite", "unexpected error: "+err.Error()) + } }) return nil } +// Runner successively applies Builder's mutations to a zero benchttp.Runner +// and returns it. func (b *Builder) Runner() benchttp.Runner { runner := benchttp.Runner{} b.Mutate(&runner) return runner } +// Mutate successively applies Builder's mutations to the benchttp.Runner +// value pointed to by dst. func (b *Builder) Mutate(dst *benchttp.Runner) { for _, mutate := range b.mutations { mutate(dst) @@ -49,12 +68,14 @@ func (b *Builder) Mutate(dst *benchttp.Runner) { // setters +// SetRequest adds a mutation that sets a runner's request to r. func (b *Builder) SetRequest(r *http.Request) { b.append(func(runner *benchttp.Runner) { runner.Request = r }) } +// SetRequestMethod adds a mutation that sets a runner's request method to v. func (b *Builder) SetRequestMethod(v string) { b.append(func(runner *benchttp.Runner) { if runner.Request == nil { @@ -64,6 +85,7 @@ func (b *Builder) SetRequestMethod(v string) { }) } +// SetRequestURL adds a mutation that sets a runner's request URL to v. func (b *Builder) SetRequestURL(v *url.URL) { b.append(func(runner *benchttp.Runner) { if runner.Request == nil { @@ -73,12 +95,15 @@ func (b *Builder) SetRequestURL(v *url.URL) { }) } +// SetRequestHeader adds a mutation that sets a runner's request header to v. func (b *Builder) SetRequestHeader(v http.Header) { b.SetRequestHeaderFunc(func(_ http.Header) http.Header { return v }) } +// SetRequestHeaderFunc adds a mutation that sets a runner's request header +// to the result of calling f with its current request header. func (b *Builder) SetRequestHeaderFunc(f func(prev http.Header) http.Header) { b.append(func(runner *benchttp.Runner) { if runner.Request == nil { @@ -88,6 +113,7 @@ func (b *Builder) SetRequestHeaderFunc(f func(prev http.Header) http.Header) { }) } +// SetRequestBody adds a mutation that sets a runner's request body to v. func (b *Builder) SetRequestBody(v io.ReadCloser) { b.append(func(runner *benchttp.Runner) { if runner.Request == nil { @@ -97,42 +123,56 @@ func (b *Builder) SetRequestBody(v io.ReadCloser) { }) } +// SetRequests adds a mutation that sets a runner's +// Requests field to v. func (b *Builder) SetRequests(v int) { b.append(func(runner *benchttp.Runner) { runner.Requests = v }) } +// SetConcurrency adds a mutation that sets a runner's +// Concurrency field to v. func (b *Builder) SetConcurrency(v int) { b.append(func(runner *benchttp.Runner) { runner.Concurrency = v }) } +// SetInterval adds a mutation that sets a runner's +// Interval field to v. func (b *Builder) SetInterval(v time.Duration) { b.append(func(runner *benchttp.Runner) { runner.Interval = v }) } +// SetRequestTimeout adds a mutation that sets a runner's +// RequestTimeout field to v. func (b *Builder) SetRequestTimeout(v time.Duration) { b.append(func(runner *benchttp.Runner) { runner.RequestTimeout = v }) } +// SetGlobalTimeout adds a mutation that sets a runner's +// GlobalTimeout field to v. func (b *Builder) SetGlobalTimeout(v time.Duration) { b.append(func(runner *benchttp.Runner) { runner.GlobalTimeout = v }) } +// SetTests adds a mutation that sets a runner's +// Tests field to v. func (b *Builder) SetTests(v []benchttp.TestCase) { b.append(func(runner *benchttp.Runner) { runner.Tests = v }) } +// SetTests adds a mutation that appends the given benchttp.TestCases +// to a runner's Tests field. func (b *Builder) AddTests(v ...benchttp.TestCase) { b.append(func(runner *benchttp.Runner) { runner.Tests = append(runner.Tests, v...) From c89ecb39787edad0a9ac9e37c0ea1863730ee9a7 Mon Sep 17 00:00:00 2001 From: Gregory Albouy Date: Sun, 6 Nov 2022 22:01:41 +0100 Subject: [PATCH 45/53] fixup! doc(configio): add documenting comments for Builder --- configio/builder.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/configio/builder.go b/configio/builder.go index 5ed43ac..92efe91 100644 --- a/configio/builder.go +++ b/configio/builder.go @@ -50,16 +50,16 @@ func (b *Builder) decodeAndWrite(in []byte, format Format) error { return nil } -// Runner successively applies Builder's mutations to a zero benchttp.Runner -// and returns it. +// Runner successively applies the Builder's mutations +// to a zero benchttp.Runner and returns it. func (b *Builder) Runner() benchttp.Runner { runner := benchttp.Runner{} b.Mutate(&runner) return runner } -// Mutate successively applies Builder's mutations to the benchttp.Runner -// value pointed to by dst. +// Mutate successively applies the Builder's mutations +// to the benchttp.Runner value pointed to by dst. func (b *Builder) Mutate(dst *benchttp.Runner) { for _, mutate := range b.mutations { mutate(dst) From 9e8cc8619a0ceb83516255da2c153a4783a96e6f Mon Sep 17 00:00:00 2001 From: Gregory Albouy Date: Sun, 6 Nov 2022 22:06:30 +0100 Subject: [PATCH 46/53] feat(configio): make Representation private - configio.Representation -> configio.representation - make all representation methods private --- configio/builder.go | 4 ++-- configio/decoder.go | 2 +- configio/file.go | 4 ++-- configio/json.go | 8 ++++---- configio/representation.go | 22 +++++++++++----------- configio/yaml.go | 8 ++++---- 6 files changed, 24 insertions(+), 24 deletions(-) diff --git a/configio/builder.go b/configio/builder.go index 92efe91..ffd3a15 100644 --- a/configio/builder.go +++ b/configio/builder.go @@ -33,7 +33,7 @@ func (b *Builder) WriteYAML(in []byte) error { } func (b *Builder) decodeAndWrite(in []byte, format Format) error { - repr := Representation{} + repr := representation{} if err := DecoderOf(format, in).Decode(&repr); err != nil { return err } @@ -43,7 +43,7 @@ func (b *Builder) decodeAndWrite(in []byte, format Format) error { } b.append(func(dst *benchttp.Runner) { // err is already checked via repr.validate(), so nil is expected. - if err := repr.Into(dst); err != nil { + if err := repr.parseAndMutate(dst); err != nil { panicInternal("Builder.decodeAndWrite", "unexpected error: "+err.Error()) } }) diff --git a/configio/decoder.go b/configio/decoder.go index 0014f70..4045ec5 100644 --- a/configio/decoder.go +++ b/configio/decoder.go @@ -8,7 +8,7 @@ import ( ) type Decoder interface { - Decode(dst *Representation) error + Decode(dst *representation) error DecodeRunner(dst *benchttp.Runner) error } diff --git a/configio/file.go b/configio/file.go index f0aff7d..d2fb01c 100644 --- a/configio/file.go +++ b/configio/file.go @@ -49,7 +49,7 @@ func UnmarshalFile(filename string, dst *benchttp.Runner) error { type file struct { prev *file path string - repr Representation + repr representation } // decodeAll reads f.path as a file and decodes it into f.repr. @@ -122,7 +122,7 @@ func (f file) seen(p string) bool { // reprs returns a slice of Representation, starting with the receiver // and ending with the last child. func (f file) reprs() representations { - reprs := []Representation{f.repr} + reprs := []representation{f.repr} if f.prev != nil { reprs = append(reprs, f.prev.reprs()...) } diff --git a/configio/json.go b/configio/json.go index debd94e..de31c1e 100644 --- a/configio/json.go +++ b/configio/json.go @@ -13,7 +13,7 @@ import ( // UnmarshalJSON parses the JSON-encoded data and stores the result // in the Representation pointed to by dst. -func UnmarshalJSON(in []byte, dst *Representation) error { +func UnmarshalJSON(in []byte, dst *representation) error { dec := NewJSONDecoder(bytes.NewReader(in)) return dec.Decode(dst) } @@ -34,7 +34,7 @@ func NewJSONDecoder(r io.Reader) JSONDecoder { // Decode reads the next JSON-encoded value from its input // and stores it in the Representation pointed to by dst. -func (d JSONDecoder) Decode(dst *Representation) error { +func (d JSONDecoder) Decode(dst *representation) error { decoder := json.NewDecoder(d.r) decoder.DisallowUnknownFields() return d.handleError(decoder.Decode(dst)) @@ -43,11 +43,11 @@ func (d JSONDecoder) Decode(dst *Representation) error { // Decode reads the next JSON-encoded value from its input // and stores it in the benchttp.Runner pointed to by dst. func (d JSONDecoder) DecodeRunner(dst *benchttp.Runner) error { - repr := Representation{} + repr := representation{} if err := d.Decode(&repr); err != nil { return err } - return repr.Into(dst) + return repr.parseAndMutate(dst) } // handleError handles an error from package json, diff --git a/configio/representation.go b/configio/representation.go index e13f2db..6097283 100644 --- a/configio/representation.go +++ b/configio/representation.go @@ -14,12 +14,12 @@ import ( "github.com/benchttp/sdk/internal/errorutil" ) -// Representation is a raw data model for formatted runner config (json, yaml). +// representation is a raw data model for formatted runner config (json, yaml). // It serves as a receiver for unmarshaling processes and for that reason // its types are kept simple (certain types are incompatible with certain // unmarshalers). // It exposes a method Unmarshal to convert its values into a runner.Config. -type Representation struct { +type representation struct { Extends *string `yaml:"extends" json:"extends"` Request struct { @@ -49,14 +49,14 @@ type Representation struct { } `yaml:"tests" json:"tests"` } -func (repr Representation) validate() error { - return repr.Into(&benchttp.Runner{}) +func (repr representation) validate() error { + return repr.parseAndMutate(&benchttp.Runner{}) } -// Into parses the Representation receiver as a benchttp.Runner +// parseAndMutate parses the Representation receiver as a benchttp.Runner // and stores any non-nil field value into the corresponding field // of dst. -func (repr Representation) Into(dst *benchttp.Runner) error { +func (repr representation) parseAndMutate(dst *benchttp.Runner) error { if err := repr.parseRequestInto(dst); err != nil { return err } @@ -66,7 +66,7 @@ func (repr Representation) Into(dst *benchttp.Runner) error { return repr.parseTestsInto(dst) } -func (repr Representation) parseRequestInto(dst *benchttp.Runner) error { +func (repr representation) parseRequestInto(dst *benchttp.Runner) error { if dst.Request == nil { dst.Request = &http.Request{} } @@ -103,7 +103,7 @@ func (repr Representation) parseRequestInto(dst *benchttp.Runner) error { return nil } -func (repr Representation) parseRunnerInto(dst *benchttp.Runner) error { +func (repr representation) parseRunnerInto(dst *benchttp.Runner) error { if requests := repr.Runner.Requests; requests != nil { dst.Requests = *requests } @@ -139,7 +139,7 @@ func (repr Representation) parseRunnerInto(dst *benchttp.Runner) error { return nil } -func (repr Representation) parseTestsInto(dst *benchttp.Runner) error { +func (repr representation) parseTestsInto(dst *benchttp.Runner) error { testSuite := repr.Tests if len(testSuite) == 0 { return nil @@ -254,7 +254,7 @@ func requireConfigFields(fields map[string]interface{}) error { return nil } -type representations []Representation +type representations []representation // mergeInto successively parses the given representations into dst. // @@ -265,7 +265,7 @@ func (reprs representations) mergeInto(dst *benchttp.Runner) error { } for _, repr := range reprs { - if err := repr.Into(dst); err != nil { + if err := repr.parseAndMutate(dst); err != nil { return errorutil.WithDetails(ErrFileParse, err) } } diff --git a/configio/yaml.go b/configio/yaml.go index d1130c3..7b13b90 100644 --- a/configio/yaml.go +++ b/configio/yaml.go @@ -14,7 +14,7 @@ import ( // UnmarshalYAML parses the YAML-encoded data and stores the result // in the Representation pointed to by dst. -func UnmarshalYAML(in []byte, dst *Representation) error { +func UnmarshalYAML(in []byte, dst *representation) error { dec := NewYAMLDecoder(bytes.NewReader(in)) return dec.Decode(dst) } @@ -35,7 +35,7 @@ func NewYAMLDecoder(r io.Reader) YAMLDecoder { // Decode reads the next YAML-encoded value from its input // and stores it in the Representation pointed to by dst. -func (d YAMLDecoder) Decode(dst *Representation) error { +func (d YAMLDecoder) Decode(dst *representation) error { decoder := yaml.NewDecoder(d.r) decoder.KnownFields(true) return d.handleError(decoder.Decode(dst)) @@ -44,11 +44,11 @@ func (d YAMLDecoder) Decode(dst *Representation) error { // Decode reads the next YAML-encoded value from its input // and stores it in the benchttp.Runner pointed to by dst. func (d YAMLDecoder) DecodeRunner(dst *benchttp.Runner) error { - repr := Representation{} + repr := representation{} if err := d.Decode(&repr); err != nil { return err } - return repr.Into(dst) + return repr.parseAndMutate(dst) } // handleError handles a raw yaml decoder.Decode error, filters it, From e865353b58f1664c9a1e0f762abbb2985a15c619 Mon Sep 17 00:00:00 2001 From: Gregory Albouy Date: Sun, 6 Nov 2022 22:25:49 +0100 Subject: [PATCH 47/53] feat(configio): remove public references to representation - remove Decode methods from interface Decoder and its implementations - rename DecodeRunner methods to Decode - remove UnmarshalXXXX functions - rename UnmarshalXXXXRunner functions to UnmarshalXXXX --- configio/builder.go | 2 +- configio/decoder.go | 18 +++++++++++++----- configio/file.go | 2 +- configio/json.go | 39 +++++++++++++++++---------------------- configio/json_test.go | 4 ++-- configio/yaml.go | 39 +++++++++++++++++---------------------- configio/yaml_test.go | 2 +- 7 files changed, 52 insertions(+), 54 deletions(-) diff --git a/configio/builder.go b/configio/builder.go index ffd3a15..bb7d08e 100644 --- a/configio/builder.go +++ b/configio/builder.go @@ -34,7 +34,7 @@ func (b *Builder) WriteYAML(in []byte) error { func (b *Builder) decodeAndWrite(in []byte, format Format) error { repr := representation{} - if err := DecoderOf(format, in).Decode(&repr); err != nil { + if err := decoderOf(format, in).decodeRepr(&repr); err != nil { return err } // early check for invalid configuration diff --git a/configio/decoder.go b/configio/decoder.go index 4045ec5..41ca421 100644 --- a/configio/decoder.go +++ b/configio/decoder.go @@ -7,11 +7,6 @@ import ( "github.com/benchttp/sdk/benchttp" ) -type Decoder interface { - Decode(dst *representation) error - DecodeRunner(dst *benchttp.Runner) error -} - type Format string const ( @@ -19,9 +14,22 @@ const ( FormatYAML Format = "yaml" ) +type Decoder interface { + Decode(dst *benchttp.Runner) error +} + // DecoderOf returns the appropriate Decoder for the given Format. // It panics if the format is not a Format declared in configio. func DecoderOf(format Format, in []byte) Decoder { + return decoderOf(format, in) +} + +type decoder interface { + Decoder + decodeRepr(dst *representation) error +} + +func decoderOf(format Format, in []byte) decoder { r := bytes.NewReader(in) switch format { case FormatYAML: diff --git a/configio/file.go b/configio/file.go index d2fb01c..8a73d1e 100644 --- a/configio/file.go +++ b/configio/file.go @@ -88,7 +88,7 @@ func (f *file) decode() (err error) { return err } - if err := DecoderOf(ext, b).Decode(&f.repr); err != nil { + if err := decoderOf(ext, b).decodeRepr(&f.repr); err != nil { return errorutil.WithDetails(ErrFileParse, f.path, err) } diff --git a/configio/json.go b/configio/json.go index de31c1e..9d83a11 100644 --- a/configio/json.go +++ b/configio/json.go @@ -11,45 +11,40 @@ import ( "github.com/benchttp/sdk/benchttp" ) -// UnmarshalJSON parses the JSON-encoded data and stores the result -// in the Representation pointed to by dst. -func UnmarshalJSON(in []byte, dst *representation) error { - dec := NewJSONDecoder(bytes.NewReader(in)) - return dec.Decode(dst) -} +// JSONDecoder implements Decoder +type JSONDecoder struct{ r io.Reader } + +var _ decoder = (*JSONDecoder)(nil) -// UnmarshalJSONRunner parses the JSON-encoded data and stores the result +// UnmarshalJSON parses the JSON-encoded data and stores the result // in the benchttp.Runner pointed to by dst. -func UnmarshalJSONRunner(in []byte, dst *benchttp.Runner) error { +func UnmarshalJSON(in []byte, dst *benchttp.Runner) error { dec := NewJSONDecoder(bytes.NewReader(in)) - return dec.DecodeRunner(dst) + return dec.Decode(dst) } -// JSONDecoder implements Decoder -type JSONDecoder struct{ r io.Reader } - func NewJSONDecoder(r io.Reader) JSONDecoder { return JSONDecoder{r: r} } -// Decode reads the next JSON-encoded value from its input -// and stores it in the Representation pointed to by dst. -func (d JSONDecoder) Decode(dst *representation) error { - decoder := json.NewDecoder(d.r) - decoder.DisallowUnknownFields() - return d.handleError(decoder.Decode(dst)) -} - // Decode reads the next JSON-encoded value from its input // and stores it in the benchttp.Runner pointed to by dst. -func (d JSONDecoder) DecodeRunner(dst *benchttp.Runner) error { +func (d JSONDecoder) Decode(dst *benchttp.Runner) error { repr := representation{} - if err := d.Decode(&repr); err != nil { + if err := d.decodeRepr(&repr); err != nil { return err } return repr.parseAndMutate(dst) } +// decodeRepr reads the next JSON-encoded value from its input +// and stores it in the Representation pointed to by dst. +func (d JSONDecoder) decodeRepr(dst *representation) error { + decoder := json.NewDecoder(d.r) + decoder.DisallowUnknownFields() + return d.handleError(decoder.Decode(dst)) +} + // handleError handles an error from package json, // transforms it into a user-friendly standardized format // and returns the resulting error. diff --git a/configio/json_test.go b/configio/json_test.go index c04bba2..d21a98b 100644 --- a/configio/json_test.go +++ b/configio/json_test.go @@ -57,7 +57,7 @@ func TestMarshalJSON(t *testing.T) { for _, tc := range testcases { t.Run(tc.name, func(t *testing.T) { gotRunner := benchttp.DefaultRunner() - gotError := configio.UnmarshalJSONRunner(tc.input, &gotRunner) + gotError := configio.UnmarshalJSON(tc.input, &gotRunner) if !tc.isValidRunner(benchttp.DefaultRunner(), gotRunner) { t.Errorf("unexpected runner:\n%+v", gotRunner) @@ -103,7 +103,7 @@ func TestJSONDecoder(t *testing.T) { runner := benchttp.Runner{} decoder := configio.NewJSONDecoder(bytes.NewReader(tc.in)) - gotErr := decoder.DecodeRunner(&runner) + gotErr := decoder.Decode(&runner) if tc.exp == "" { if gotErr != nil { diff --git a/configio/yaml.go b/configio/yaml.go index 7b13b90..9bc604b 100644 --- a/configio/yaml.go +++ b/configio/yaml.go @@ -12,45 +12,40 @@ import ( "github.com/benchttp/sdk/benchttp" ) -// UnmarshalYAML parses the YAML-encoded data and stores the result -// in the Representation pointed to by dst. -func UnmarshalYAML(in []byte, dst *representation) error { - dec := NewYAMLDecoder(bytes.NewReader(in)) - return dec.Decode(dst) -} +// YAMLDecoder implements Decoder +type YAMLDecoder struct{ r io.Reader } + +var _ decoder = (*YAMLDecoder)(nil) -// UnmarshalYAMLRunner parses the YAML-encoded data and stores the result +// UnmarshalYAML parses the YAML-encoded data and stores the result // in the benchttp.Runner pointed to by dst. -func UnmarshalYAMLRunner(in []byte, dst *benchttp.Runner) error { +func UnmarshalYAML(in []byte, dst *benchttp.Runner) error { dec := NewYAMLDecoder(bytes.NewReader(in)) - return dec.DecodeRunner(dst) + return dec.Decode(dst) } -// YAMLDecoder implements Decoder -type YAMLDecoder struct{ r io.Reader } - func NewYAMLDecoder(r io.Reader) YAMLDecoder { return YAMLDecoder{r: r} } -// Decode reads the next YAML-encoded value from its input -// and stores it in the Representation pointed to by dst. -func (d YAMLDecoder) Decode(dst *representation) error { - decoder := yaml.NewDecoder(d.r) - decoder.KnownFields(true) - return d.handleError(decoder.Decode(dst)) -} - // Decode reads the next YAML-encoded value from its input // and stores it in the benchttp.Runner pointed to by dst. -func (d YAMLDecoder) DecodeRunner(dst *benchttp.Runner) error { +func (d YAMLDecoder) Decode(dst *benchttp.Runner) error { repr := representation{} - if err := d.Decode(&repr); err != nil { + if err := d.decodeRepr(&repr); err != nil { return err } return repr.parseAndMutate(dst) } +// decodeRepr reads the next YAML-encoded value from its input +// and stores it in the Representation pointed to by dst. +func (d YAMLDecoder) decodeRepr(dst *representation) error { + decoder := yaml.NewDecoder(d.r) + decoder.KnownFields(true) + return d.handleError(decoder.Decode(dst)) +} + // handleError handles a raw yaml decoder.Decode error, filters it, // and return the resulting error. func (d YAMLDecoder) handleError(err error) error { diff --git a/configio/yaml_test.go b/configio/yaml_test.go index 2f191cf..4bff966 100644 --- a/configio/yaml_test.go +++ b/configio/yaml_test.go @@ -69,7 +69,7 @@ func TestYAMLDecoder(t *testing.T) { runner := benchttp.Runner{} decoder := configio.NewYAMLDecoder(bytes.NewReader(tc.in)) - gotErr := decoder.DecodeRunner(&runner) + gotErr := decoder.Decode(&runner) if tc.expErr == nil { if gotErr != nil { From 7c3655f3d6f839faedadd2a0dc421a4a9d81beb4 Mon Sep 17 00:00:00 2001 From: Gregory Albouy Date: Mon, 7 Nov 2022 00:55:05 +0100 Subject: [PATCH 48/53] refactor(configio): move `representation` to internal/conversion - configio.representation -> configio/internal/Repr - expose necessary methods --- configio/builder.go | 7 ++-- configio/decoder.go | 3 +- configio/file.go | 9 ++-- .../conversion}/representation.go | 42 ++++++++++++------- configio/json.go | 7 ++-- configio/yaml.go | 7 ++-- 6 files changed, 45 insertions(+), 30 deletions(-) rename configio/{ => internal/conversion}/representation.go (85%) diff --git a/configio/builder.go b/configio/builder.go index bb7d08e..9ce4356 100644 --- a/configio/builder.go +++ b/configio/builder.go @@ -7,6 +7,7 @@ import ( "time" "github.com/benchttp/sdk/benchttp" + "github.com/benchttp/sdk/configio/internal/conversion" ) // A Builder is used to incrementally build a benchttp.Runner @@ -33,17 +34,17 @@ func (b *Builder) WriteYAML(in []byte) error { } func (b *Builder) decodeAndWrite(in []byte, format Format) error { - repr := representation{} + repr := conversion.Repr{} if err := decoderOf(format, in).decodeRepr(&repr); err != nil { return err } // early check for invalid configuration - if err := repr.validate(); err != nil { + if err := repr.Validate(); err != nil { return err } b.append(func(dst *benchttp.Runner) { // err is already checked via repr.validate(), so nil is expected. - if err := repr.parseAndMutate(dst); err != nil { + if err := repr.ParseAndMutate(dst); err != nil { panicInternal("Builder.decodeAndWrite", "unexpected error: "+err.Error()) } }) diff --git a/configio/decoder.go b/configio/decoder.go index 41ca421..448e95b 100644 --- a/configio/decoder.go +++ b/configio/decoder.go @@ -5,6 +5,7 @@ import ( "fmt" "github.com/benchttp/sdk/benchttp" + "github.com/benchttp/sdk/configio/internal/conversion" ) type Format string @@ -26,7 +27,7 @@ func DecoderOf(format Format, in []byte) Decoder { type decoder interface { Decoder - decodeRepr(dst *representation) error + decodeRepr(dst *conversion.Repr) error } func decoderOf(format Format, in []byte) decoder { diff --git a/configio/file.go b/configio/file.go index 8a73d1e..0386102 100644 --- a/configio/file.go +++ b/configio/file.go @@ -6,6 +6,7 @@ import ( "path/filepath" "github.com/benchttp/sdk/benchttp" + "github.com/benchttp/sdk/configio/internal/conversion" "github.com/benchttp/sdk/internal/errorutil" ) @@ -42,14 +43,14 @@ func UnmarshalFile(filename string, dst *benchttp.Runner) error { if err != nil { return err } - return f.reprs().mergeInto(dst) + return f.reprs().MergeInto(dst) } // file represents a config file type file struct { prev *file path string - repr representation + repr conversion.Repr } // decodeAll reads f.path as a file and decodes it into f.repr. @@ -121,8 +122,8 @@ func (f file) seen(p string) bool { // reprs returns a slice of Representation, starting with the receiver // and ending with the last child. -func (f file) reprs() representations { - reprs := []representation{f.repr} +func (f file) reprs() conversion.Reprs { + reprs := []conversion.Repr{f.repr} if f.prev != nil { reprs = append(reprs, f.prev.reprs()...) } diff --git a/configio/representation.go b/configio/internal/conversion/representation.go similarity index 85% rename from configio/representation.go rename to configio/internal/conversion/representation.go index 6097283..c4e1602 100644 --- a/configio/representation.go +++ b/configio/internal/conversion/representation.go @@ -1,4 +1,4 @@ -package configio +package conversion import ( "bytes" @@ -11,15 +11,14 @@ import ( "time" "github.com/benchttp/sdk/benchttp" - "github.com/benchttp/sdk/internal/errorutil" ) -// representation is a raw data model for formatted runner config (json, yaml). +// Repr is a raw data model for formatted runner config (json, yaml). // It serves as a receiver for unmarshaling processes and for that reason // its types are kept simple (certain types are incompatible with certain // unmarshalers). // It exposes a method Unmarshal to convert its values into a runner.Config. -type representation struct { +type Repr struct { Extends *string `yaml:"extends" json:"extends"` Request struct { @@ -49,14 +48,14 @@ type representation struct { } `yaml:"tests" json:"tests"` } -func (repr representation) validate() error { - return repr.parseAndMutate(&benchttp.Runner{}) +func (repr Repr) Validate() error { + return repr.ParseAndMutate(&benchttp.Runner{}) } -// parseAndMutate parses the Representation receiver as a benchttp.Runner +// ParseAndMutate parses the Representation receiver as a benchttp.Runner // and stores any non-nil field value into the corresponding field // of dst. -func (repr representation) parseAndMutate(dst *benchttp.Runner) error { +func (repr Repr) ParseAndMutate(dst *benchttp.Runner) error { if err := repr.parseRequestInto(dst); err != nil { return err } @@ -66,7 +65,7 @@ func (repr representation) parseAndMutate(dst *benchttp.Runner) error { return repr.parseTestsInto(dst) } -func (repr representation) parseRequestInto(dst *benchttp.Runner) error { +func (repr Repr) parseRequestInto(dst *benchttp.Runner) error { if dst.Request == nil { dst.Request = &http.Request{} } @@ -103,7 +102,7 @@ func (repr representation) parseRequestInto(dst *benchttp.Runner) error { return nil } -func (repr representation) parseRunnerInto(dst *benchttp.Runner) error { +func (repr Repr) parseRunnerInto(dst *benchttp.Runner) error { if requests := repr.Runner.Requests; requests != nil { dst.Requests = *requests } @@ -139,7 +138,7 @@ func (repr representation) parseRunnerInto(dst *benchttp.Runner) error { return nil } -func (repr representation) parseTestsInto(dst *benchttp.Runner) error { +func (repr Repr) parseTestsInto(dst *benchttp.Runner) error { testSuite := repr.Tests if len(testSuite) == 0 { return nil @@ -254,20 +253,31 @@ func requireConfigFields(fields map[string]interface{}) error { return nil } -type representations []representation +type Reprs []Repr -// mergeInto successively parses the given representations into dst. +// MergeInto successively parses the given representations into dst. // // The input Representation slice must never be nil or empty, otherwise it panics. -func (reprs representations) mergeInto(dst *benchttp.Runner) error { +func (reprs Reprs) MergeInto(dst *benchttp.Runner) error { if len(reprs) == 0 { // supposedly catched upstream, should not occur panicInternal("parseAndMergeReprs", "nil or empty []Representation") } for _, repr := range reprs { - if err := repr.parseAndMutate(dst); err != nil { - return errorutil.WithDetails(ErrFileParse, err) + if err := repr.ParseAndMutate(dst); err != nil { + return err + // TODO: uncomment once wrapped from configio/file.go + // return errorutil.WithDetails(ErrFileParse, err) } } return nil } + +func panicInternal(funcname, detail string) { + const reportURL = "https://github.com/benchttp/sdk/issues/new" + source := fmt.Sprintf("configio.%s", funcname) + panic(fmt.Sprintf( + "%s: unexpected internal error: %s, please file an issue at %s", + source, detail, reportURL, + )) +} diff --git a/configio/json.go b/configio/json.go index 9d83a11..1dec656 100644 --- a/configio/json.go +++ b/configio/json.go @@ -9,6 +9,7 @@ import ( "regexp" "github.com/benchttp/sdk/benchttp" + "github.com/benchttp/sdk/configio/internal/conversion" ) // JSONDecoder implements Decoder @@ -30,16 +31,16 @@ func NewJSONDecoder(r io.Reader) JSONDecoder { // Decode reads the next JSON-encoded value from its input // and stores it in the benchttp.Runner pointed to by dst. func (d JSONDecoder) Decode(dst *benchttp.Runner) error { - repr := representation{} + repr := conversion.Repr{} if err := d.decodeRepr(&repr); err != nil { return err } - return repr.parseAndMutate(dst) + return repr.ParseAndMutate(dst) } // decodeRepr reads the next JSON-encoded value from its input // and stores it in the Representation pointed to by dst. -func (d JSONDecoder) decodeRepr(dst *representation) error { +func (d JSONDecoder) decodeRepr(dst *conversion.Repr) error { decoder := json.NewDecoder(d.r) decoder.DisallowUnknownFields() return d.handleError(decoder.Decode(dst)) diff --git a/configio/yaml.go b/configio/yaml.go index 9bc604b..3058344 100644 --- a/configio/yaml.go +++ b/configio/yaml.go @@ -10,6 +10,7 @@ import ( "gopkg.in/yaml.v3" "github.com/benchttp/sdk/benchttp" + "github.com/benchttp/sdk/configio/internal/conversion" ) // YAMLDecoder implements Decoder @@ -31,16 +32,16 @@ func NewYAMLDecoder(r io.Reader) YAMLDecoder { // Decode reads the next YAML-encoded value from its input // and stores it in the benchttp.Runner pointed to by dst. func (d YAMLDecoder) Decode(dst *benchttp.Runner) error { - repr := representation{} + repr := conversion.Repr{} if err := d.decodeRepr(&repr); err != nil { return err } - return repr.parseAndMutate(dst) + return repr.ParseAndMutate(dst) } // decodeRepr reads the next YAML-encoded value from its input // and stores it in the Representation pointed to by dst. -func (d YAMLDecoder) decodeRepr(dst *representation) error { +func (d YAMLDecoder) decodeRepr(dst *conversion.Repr) error { decoder := yaml.NewDecoder(d.r) decoder.KnownFields(true) return d.handleError(decoder.Decode(dst)) From fe8218f4c3115aaddd5f4c5bbf85960697e06182 Mon Sep 17 00:00:00 2001 From: Gregory Albouy Date: Mon, 7 Nov 2022 00:58:25 +0100 Subject: [PATCH 49/53] refactor(conversion): rename Repr.ParseAndMutate -> Repr.Decode --- configio/builder.go | 2 +- .../internal/conversion/representation.go | 20 +++++++++---------- configio/json.go | 2 +- configio/yaml.go | 2 +- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/configio/builder.go b/configio/builder.go index 9ce4356..58f7fe4 100644 --- a/configio/builder.go +++ b/configio/builder.go @@ -44,7 +44,7 @@ func (b *Builder) decodeAndWrite(in []byte, format Format) error { } b.append(func(dst *benchttp.Runner) { // err is already checked via repr.validate(), so nil is expected. - if err := repr.ParseAndMutate(dst); err != nil { + if err := repr.Decode(dst); err != nil { panicInternal("Builder.decodeAndWrite", "unexpected error: "+err.Error()) } }) diff --git a/configio/internal/conversion/representation.go b/configio/internal/conversion/representation.go index c4e1602..bc89527 100644 --- a/configio/internal/conversion/representation.go +++ b/configio/internal/conversion/representation.go @@ -49,23 +49,23 @@ type Repr struct { } func (repr Repr) Validate() error { - return repr.ParseAndMutate(&benchttp.Runner{}) + return repr.Decode(&benchttp.Runner{}) } -// ParseAndMutate parses the Representation receiver as a benchttp.Runner +// Decode parses the Representation receiver as a benchttp.Runner // and stores any non-nil field value into the corresponding field // of dst. -func (repr Repr) ParseAndMutate(dst *benchttp.Runner) error { - if err := repr.parseRequestInto(dst); err != nil { +func (repr Repr) Decode(dst *benchttp.Runner) error { + if err := repr.decodeRequestField(dst); err != nil { return err } - if err := repr.parseRunnerInto(dst); err != nil { + if err := repr.decodeRunnerFields(dst); err != nil { return err } - return repr.parseTestsInto(dst) + return repr.decodeTestsField(dst) } -func (repr Repr) parseRequestInto(dst *benchttp.Runner) error { +func (repr Repr) decodeRequestField(dst *benchttp.Runner) error { if dst.Request == nil { dst.Request = &http.Request{} } @@ -102,7 +102,7 @@ func (repr Repr) parseRequestInto(dst *benchttp.Runner) error { return nil } -func (repr Repr) parseRunnerInto(dst *benchttp.Runner) error { +func (repr Repr) decodeRunnerFields(dst *benchttp.Runner) error { if requests := repr.Runner.Requests; requests != nil { dst.Requests = *requests } @@ -138,7 +138,7 @@ func (repr Repr) parseRunnerInto(dst *benchttp.Runner) error { return nil } -func (repr Repr) parseTestsInto(dst *benchttp.Runner) error { +func (repr Repr) decodeTestsField(dst *benchttp.Runner) error { testSuite := repr.Tests if len(testSuite) == 0 { return nil @@ -264,7 +264,7 @@ func (reprs Reprs) MergeInto(dst *benchttp.Runner) error { } for _, repr := range reprs { - if err := repr.ParseAndMutate(dst); err != nil { + if err := repr.Decode(dst); err != nil { return err // TODO: uncomment once wrapped from configio/file.go // return errorutil.WithDetails(ErrFileParse, err) diff --git a/configio/json.go b/configio/json.go index 1dec656..0058e59 100644 --- a/configio/json.go +++ b/configio/json.go @@ -35,7 +35,7 @@ func (d JSONDecoder) Decode(dst *benchttp.Runner) error { if err := d.decodeRepr(&repr); err != nil { return err } - return repr.ParseAndMutate(dst) + return repr.Decode(dst) } // decodeRepr reads the next JSON-encoded value from its input diff --git a/configio/yaml.go b/configio/yaml.go index 3058344..4ef91e3 100644 --- a/configio/yaml.go +++ b/configio/yaml.go @@ -36,7 +36,7 @@ func (d YAMLDecoder) Decode(dst *benchttp.Runner) error { if err := d.decodeRepr(&repr); err != nil { return err } - return repr.ParseAndMutate(dst) + return repr.Decode(dst) } // decodeRepr reads the next YAML-encoded value from its input From 14dcbaf449d50b5b3e75dc0c8c3f1440fea60076 Mon Sep 17 00:00:00 2001 From: Gregory Albouy Date: Mon, 7 Nov 2022 02:33:08 +0100 Subject: [PATCH 50/53] refactor(conversion): use declarative converters --- configio/internal/conversion/converters.go | 247 ++++++++++++++++++ configio/internal/conversion/parsing.go | 75 ++++++ .../internal/conversion/representation.go | 200 +------------- 3 files changed, 324 insertions(+), 198 deletions(-) create mode 100644 configio/internal/conversion/converters.go create mode 100644 configio/internal/conversion/parsing.go diff --git a/configio/internal/conversion/converters.go b/configio/internal/conversion/converters.go new file mode 100644 index 0000000..0aee743 --- /dev/null +++ b/configio/internal/conversion/converters.go @@ -0,0 +1,247 @@ +package conversion + +import ( + "bytes" + "errors" + "fmt" + "io" + "net/http" + "time" + + "github.com/benchttp/sdk/benchttp" +) + +type converter struct { + // TODO: + // encode func(src benchttp.Runner, dst *Repr) + decode func(src Repr, dst *benchttp.Runner) error +} + +type requestConverter struct { + // TODO: + // encode func(src *http.Request, dst *Repr) + decode func(src Repr, dst *http.Request) error +} + +var converters = []converter{ + fieldRequest, + fieldRunner, + fieldTests, +} + +var fieldRequest = converter{ + decode: func(src Repr, dst *benchttp.Runner) error { + if dst.Request == nil { + dst.Request = &http.Request{} + } + for _, c := range requestConverters { + if err := c.decode(src, dst.Request); err != nil { + return err + } + } + return nil + }, +} + +var fieldRunner = converter{ + decode: func(src Repr, dst *benchttp.Runner) error { + for _, c := range runnerConverters { + if err := c.decode(src, dst); err != nil { + return err + } + } + return nil + }, +} + +var fieldTests = converter{ + decode: func(src Repr, dst *benchttp.Runner) error { + testSuite := src.Tests + if len(testSuite) == 0 { + return nil + } + + cases := make([]benchttp.TestCase, len(testSuite)) + for i, t := range testSuite { + fieldPath := func(caseField string) string { + return fmt.Sprintf("tests[%d].%s", i, caseField) + } + + if err := requireConfigFields(map[string]interface{}{ + fieldPath("name"): t.Name, + fieldPath("field"): t.Field, + fieldPath("predicate"): t.Predicate, + fieldPath("target"): t.Target, + }); err != nil { + return err + } + + field := benchttp.MetricsField(*t.Field) + if err := field.Validate(); err != nil { + return fmt.Errorf("%s: %s", fieldPath("field"), err) + } + + predicate := benchttp.TestPredicate(*t.Predicate) + if err := predicate.Validate(); err != nil { + return fmt.Errorf("%s: %s", fieldPath("predicate"), err) + } + + target, err := parseMetricValue(field, fmt.Sprint(t.Target)) + if err != nil { + return fmt.Errorf("%s: %s", fieldPath("target"), err) + } + + cases[i] = benchttp.TestCase{ + Name: *t.Name, + Field: field, + Predicate: predicate, + Target: target, + } + } + + dst.Tests = cases + return nil + }, +} + +var requestConverters = []requestConverter{ + fieldRequestMethod, + fieldRequestURL, + fieldRequestHeader, + fieldRequestBody, +} + +var runnerConverters = []converter{ + fieldRunnerRequests, + fieldRunnerConcurrency, + fieldRunnerInterval, + fieldRunnerRequestTimeout, + fieldRunnerGlobalTimeout, +} + +// TODO: +// var testsConverters = []converter{ +// fieldTests, +// } + +var fieldRequestMethod = requestConverter{ + decode: func(src Repr, dst *http.Request) error { + if m := src.Request.Method; m != nil { + dst.Method = *m + } + return nil + }, +} + +var fieldRequestURL = requestConverter{ + decode: func(src Repr, dst *http.Request) error { + if rawURL := src.Request.URL; rawURL != nil { + parsedURL, err := parseAndBuildURL(*rawURL, src.Request.QueryParams) + if err != nil { + return fmt.Errorf(`configio: invalid url: %q`, *rawURL) + } + dst.URL = parsedURL + } + return nil + }, +} + +var fieldRequestHeader = requestConverter{ + decode: func(src Repr, dst *http.Request) error { + if header := src.Request.Header; len(header) != 0 { + httpHeader := http.Header{} + for key, val := range header { + httpHeader[key] = val + } + dst.Header = httpHeader + } + return nil + }, +} + +var fieldRequestBody = requestConverter{ + decode: func(src Repr, dst *http.Request) error { + if body := src.Request.Body; body != nil { + switch body.Type { + case "raw": + dst.Body = io.NopCloser(bytes.NewReader([]byte(body.Content))) + default: + return errors.New(`configio: request.body.type: only "raw" accepted`) + } + } + return nil + }, +} + +var fieldRunnerRequests = intField( + func(src *Repr, dst *benchttp.Runner) (*int, *int) { + return src.Runner.Requests, &dst.Requests + }, +) + +var fieldRunnerConcurrency = intField( + func(src *Repr, dst *benchttp.Runner) (*int, *int) { + return src.Runner.Concurrency, &dst.Concurrency + }, +) + +var fieldRunnerInterval = durationField( + func(src *Repr, dst *benchttp.Runner) (*string, *time.Duration) { + return src.Runner.Interval, &dst.Interval + }, +) + +var fieldRunnerRequestTimeout = durationField( + func(src *Repr, dst *benchttp.Runner) (*string, *time.Duration) { + return src.Runner.RequestTimeout, &dst.RequestTimeout + }, +) + +var fieldRunnerGlobalTimeout = durationField( + func(src *Repr, dst *benchttp.Runner) (*string, *time.Duration) { + return src.Runner.GlobalTimeout, &dst.GlobalTimeout + }, +) + +func durationField( + bind func(*Repr, *benchttp.Runner) (*string, *time.Duration), +) converter { + return converter{ + decode: func(src Repr, dst *benchttp.Runner) error { + if vsrc, vdst := bind(&src, dst); vsrc != nil { + parsed, err := parseOptionalDuration(*vsrc) + if err != nil { + return err + } + *vdst = parsed + } + return nil + }, + } +} + +func intField( + bind func(*Repr, *benchttp.Runner) (*int, *int), +) converter { + return converter{ + decode: func(src Repr, dst *benchttp.Runner) error { + if vsrc, vdst := bind(&src, dst); vsrc != nil { + *vdst = *vsrc + } + return nil + }, + } +} + +func stringField( + bind func(*Repr, *benchttp.Runner) (*string, *string), +) converter { + return converter{ + decode: func(src Repr, dst *benchttp.Runner) error { + if vsrc, vdst := bind(&src, dst); vsrc != nil { + *vdst = *vsrc + } + return nil + }, + } +} diff --git a/configio/internal/conversion/parsing.go b/configio/internal/conversion/parsing.go new file mode 100644 index 0000000..5e7d3c9 --- /dev/null +++ b/configio/internal/conversion/parsing.go @@ -0,0 +1,75 @@ +package conversion + +import ( + "fmt" + "net/url" + "strconv" + "time" + + "github.com/benchttp/sdk/benchttp" +) + +// parseAndBuildURL parses a raw string as a *url.URL and adds any extra +// query parameters. It returns the first non-nil error occurring in the +// process. +func parseAndBuildURL(raw string, qp map[string]string) (*url.URL, error) { + u, err := url.ParseRequestURI(raw) + if err != nil { + return nil, err + } + + // retrieve url query, add extra params, re-attach to url + if qp != nil { + q := u.Query() + for k, v := range qp { + q.Add(k, v) + } + u.RawQuery = q.Encode() + } + + return u, nil +} + +// parseOptionalDuration parses the raw string as a time.Duration +// and returns the parsed value or a non-nil error. +// Contrary to time.ParseDuration, it does not return an error +// if raw == "". +func parseOptionalDuration(raw string) (time.Duration, error) { + if raw == "" { + return 0, nil + } + return time.ParseDuration(raw) +} + +func parseMetricValue( + field benchttp.MetricsField, + inputValue string, +) (benchttp.MetricsValue, error) { + fieldType := field.Type() + handleError := func(v interface{}, err error) (benchttp.MetricsValue, error) { + if err != nil { + return nil, fmt.Errorf( + "value %q is incompatible with field %s (want %s)", + inputValue, field, fieldType, + ) + } + return v, nil + } + switch fieldType { + case "int": + return handleError(strconv.Atoi(inputValue)) + case "time.Duration": + return handleError(time.ParseDuration(inputValue)) + default: + return nil, fmt.Errorf("unknown field: %s", field) + } +} + +func requireConfigFields(fields map[string]interface{}) error { + for name, value := range fields { + if value == nil { + return fmt.Errorf("%s: missing field", name) + } + } + return nil +} diff --git a/configio/internal/conversion/representation.go b/configio/internal/conversion/representation.go index bc89527..cab1570 100644 --- a/configio/internal/conversion/representation.go +++ b/configio/internal/conversion/representation.go @@ -1,14 +1,7 @@ package conversion import ( - "bytes" - "errors" "fmt" - "io" - "net/http" - "net/url" - "strconv" - "time" "github.com/benchttp/sdk/benchttp" ) @@ -56,199 +49,10 @@ func (repr Repr) Validate() error { // and stores any non-nil field value into the corresponding field // of dst. func (repr Repr) Decode(dst *benchttp.Runner) error { - if err := repr.decodeRequestField(dst); err != nil { - return err - } - if err := repr.decodeRunnerFields(dst); err != nil { - return err - } - return repr.decodeTestsField(dst) -} - -func (repr Repr) decodeRequestField(dst *benchttp.Runner) error { - if dst.Request == nil { - dst.Request = &http.Request{} - } - - if method := repr.Request.Method; method != nil { - dst.Request.Method = *method - } - - if rawURL := repr.Request.URL; rawURL != nil { - parsedURL, err := parseAndBuildURL(*rawURL, repr.Request.QueryParams) - if err != nil { - return fmt.Errorf(`configio: invalid url: %q`, *rawURL) - } - dst.Request.URL = parsedURL - } - - if header := repr.Request.Header; len(header) != 0 { - httpHeader := http.Header{} - for key, val := range header { - httpHeader[key] = val - } - dst.Request.Header = httpHeader - } - - if body := repr.Request.Body; body != nil { - switch body.Type { - case "raw": - dst.Request.Body = io.NopCloser(bytes.NewReader([]byte(body.Content))) - default: - return errors.New(`configio: request.body.type: only "raw" accepted`) - } - } - - return nil -} - -func (repr Repr) decodeRunnerFields(dst *benchttp.Runner) error { - if requests := repr.Runner.Requests; requests != nil { - dst.Requests = *requests - } - - if concurrency := repr.Runner.Concurrency; concurrency != nil { - dst.Concurrency = *concurrency - } - - if interval := repr.Runner.Interval; interval != nil { - parsedInterval, err := parseOptionalDuration(*interval) - if err != nil { - return err - } - dst.Interval = parsedInterval - } - - if requestTimeout := repr.Runner.RequestTimeout; requestTimeout != nil { - parsedTimeout, err := parseOptionalDuration(*requestTimeout) - if err != nil { + for _, decoder := range converters { + if err := decoder.decode(repr, dst); err != nil { return err } - dst.RequestTimeout = parsedTimeout - } - - if globalTimeout := repr.Runner.GlobalTimeout; globalTimeout != nil { - parsedGlobalTimeout, err := parseOptionalDuration(*globalTimeout) - if err != nil { - return err - } - dst.GlobalTimeout = parsedGlobalTimeout - } - - return nil -} - -func (repr Repr) decodeTestsField(dst *benchttp.Runner) error { - testSuite := repr.Tests - if len(testSuite) == 0 { - return nil - } - - cases := make([]benchttp.TestCase, len(testSuite)) - for i, t := range testSuite { - fieldPath := func(caseField string) string { - return fmt.Sprintf("tests[%d].%s", i, caseField) - } - - if err := requireConfigFields(map[string]interface{}{ - fieldPath("name"): t.Name, - fieldPath("field"): t.Field, - fieldPath("predicate"): t.Predicate, - fieldPath("target"): t.Target, - }); err != nil { - return err - } - - field := benchttp.MetricsField(*t.Field) - if err := field.Validate(); err != nil { - return fmt.Errorf("%s: %s", fieldPath("field"), err) - } - - predicate := benchttp.TestPredicate(*t.Predicate) - if err := predicate.Validate(); err != nil { - return fmt.Errorf("%s: %s", fieldPath("predicate"), err) - } - - target, err := parseMetricValue(field, fmt.Sprint(t.Target)) - if err != nil { - return fmt.Errorf("%s: %s", fieldPath("target"), err) - } - - cases[i] = benchttp.TestCase{ - Name: *t.Name, - Field: field, - Predicate: predicate, - Target: target, - } - } - - dst.Tests = cases - return nil -} - -// helpers - -// parseAndBuildURL parses a raw string as a *url.URL and adds any extra -// query parameters. It returns the first non-nil error occurring in the -// process. -func parseAndBuildURL(raw string, qp map[string]string) (*url.URL, error) { - u, err := url.ParseRequestURI(raw) - if err != nil { - return nil, err - } - - // retrieve url query, add extra params, re-attach to url - if qp != nil { - q := u.Query() - for k, v := range qp { - q.Add(k, v) - } - u.RawQuery = q.Encode() - } - - return u, nil -} - -// parseOptionalDuration parses the raw string as a time.Duration -// and returns the parsed value or a non-nil error. -// Contrary to time.ParseDuration, it does not return an error -// if raw == "". -func parseOptionalDuration(raw string) (time.Duration, error) { - if raw == "" { - return 0, nil - } - return time.ParseDuration(raw) -} - -func parseMetricValue( - field benchttp.MetricsField, - inputValue string, -) (benchttp.MetricsValue, error) { - fieldType := field.Type() - handleError := func(v interface{}, err error) (benchttp.MetricsValue, error) { - if err != nil { - return nil, fmt.Errorf( - "value %q is incompatible with field %s (want %s)", - inputValue, field, fieldType, - ) - } - return v, nil - } - switch fieldType { - case "int": - return handleError(strconv.Atoi(inputValue)) - case "time.Duration": - return handleError(time.ParseDuration(inputValue)) - default: - return nil, fmt.Errorf("unknown field: %s", field) - } -} - -func requireConfigFields(fields map[string]interface{}) error { - for name, value := range fields { - if value == nil { - return fmt.Errorf("%s: missing field", name) - } } return nil } From fe16752adb1c8255d004b15a941e980367ad91fd Mon Sep 17 00:00:00 2001 From: Gregory Albouy Date: Mon, 7 Nov 2022 03:19:05 +0100 Subject: [PATCH 51/53] refactor(conversion): extract parsing functions from converter --- configio/internal/conversion/converters.go | 59 ++----------------- configio/internal/conversion/parsing.go | 51 +++++++++++++++- .../internal/conversion/representation.go | 51 ++++++++-------- 3 files changed, 82 insertions(+), 79 deletions(-) diff --git a/configio/internal/conversion/converters.go b/configio/internal/conversion/converters.go index 0aee743..2ed3be3 100644 --- a/configio/internal/conversion/converters.go +++ b/configio/internal/conversion/converters.go @@ -56,50 +56,14 @@ var fieldRunner = converter{ var fieldTests = converter{ decode: func(src Repr, dst *benchttp.Runner) error { - testSuite := src.Tests - if len(testSuite) == 0 { - return nil - } - - cases := make([]benchttp.TestCase, len(testSuite)) - for i, t := range testSuite { - fieldPath := func(caseField string) string { - return fmt.Sprintf("tests[%d].%s", i, caseField) - } - - if err := requireConfigFields(map[string]interface{}{ - fieldPath("name"): t.Name, - fieldPath("field"): t.Field, - fieldPath("predicate"): t.Predicate, - fieldPath("target"): t.Target, - }); err != nil { - return err - } - - field := benchttp.MetricsField(*t.Field) - if err := field.Validate(); err != nil { - return fmt.Errorf("%s: %s", fieldPath("field"), err) - } - - predicate := benchttp.TestPredicate(*t.Predicate) - if err := predicate.Validate(); err != nil { - return fmt.Errorf("%s: %s", fieldPath("predicate"), err) - } - - target, err := parseMetricValue(field, fmt.Sprint(t.Target)) + if tests := src.Tests; tests != nil { + cases, err := parseTests(tests) if err != nil { - return fmt.Errorf("%s: %s", fieldPath("target"), err) - } - - cases[i] = benchttp.TestCase{ - Name: *t.Name, - Field: field, - Predicate: predicate, - Target: target, + return err } + dst.Tests = cases + return nil } - - dst.Tests = cases return nil }, } @@ -232,16 +196,3 @@ func intField( }, } } - -func stringField( - bind func(*Repr, *benchttp.Runner) (*string, *string), -) converter { - return converter{ - decode: func(src Repr, dst *benchttp.Runner) error { - if vsrc, vdst := bind(&src, dst); vsrc != nil { - *vdst = *vsrc - } - return nil - }, - } -} diff --git a/configio/internal/conversion/parsing.go b/configio/internal/conversion/parsing.go index 5e7d3c9..7961495 100644 --- a/configio/internal/conversion/parsing.go +++ b/configio/internal/conversion/parsing.go @@ -41,6 +41,55 @@ func parseOptionalDuration(raw string) (time.Duration, error) { return time.ParseDuration(raw) } +func parseTests(tests []testcaseRepr) ([]benchttp.TestCase, error) { + cases := make([]benchttp.TestCase, len(tests)) + for i, in := range tests { + c, err := parseTestcase(in, i) + if err != nil { + return nil, err + } + cases[i] = c + } + return cases, nil +} + +func parseTestcase(in testcaseRepr, idx int) (benchttp.TestCase, error) { + fieldDesc := func(caseField string) string { + return fmt.Sprintf("tests[%d].%s", idx, caseField) + } + + if err := assertDefinedFields(map[string]interface{}{ + fieldDesc("name"): in.Name, + fieldDesc("field"): in.Field, + fieldDesc("predicate"): in.Predicate, + fieldDesc("target"): in.Target, + }); err != nil { + return benchttp.TestCase{}, err + } + + field := benchttp.MetricsField(*in.Field) + if err := field.Validate(); err != nil { + return benchttp.TestCase{}, fmt.Errorf("%s: %s", fieldDesc("field"), err) + } + + predicate := benchttp.TestPredicate(*in.Predicate) + if err := predicate.Validate(); err != nil { + return benchttp.TestCase{}, fmt.Errorf("%s: %s", fieldDesc("predicate"), err) + } + + target, err := parseMetricValue(field, fmt.Sprint(in.Target)) + if err != nil { + return benchttp.TestCase{}, fmt.Errorf("%s: %s", fieldDesc("target"), err) + } + + return benchttp.TestCase{ + Name: *in.Name, + Field: field, + Predicate: predicate, + Target: target, + }, nil +} + func parseMetricValue( field benchttp.MetricsField, inputValue string, @@ -65,7 +114,7 @@ func parseMetricValue( } } -func requireConfigFields(fields map[string]interface{}) error { +func assertDefinedFields(fields map[string]interface{}) error { for name, value := range fields { if value == nil { return fmt.Errorf("%s: missing field", name) diff --git a/configio/internal/conversion/representation.go b/configio/internal/conversion/representation.go index cab1570..bd3f532 100644 --- a/configio/internal/conversion/representation.go +++ b/configio/internal/conversion/representation.go @@ -12,33 +12,36 @@ import ( // unmarshalers). // It exposes a method Unmarshal to convert its values into a runner.Config. type Repr struct { - Extends *string `yaml:"extends" json:"extends"` + Extends *string `yaml:"extends" json:"extends"` + Request requestRepr `yaml:"request" json:"request"` + Runner runnerRepr `yaml:"runner" json:"runner"` + Tests []testcaseRepr `yaml:"tests" json:"tests"` +} - Request struct { - Method *string `yaml:"method" json:"method"` - URL *string `yaml:"url" json:"url"` - QueryParams map[string]string `yaml:"queryParams" json:"queryParams"` - Header map[string][]string `yaml:"header" json:"header"` - Body *struct { - Type string `yaml:"type" json:"type"` - Content string `yaml:"content" json:"content"` - } `yaml:"body" json:"body"` - } `yaml:"request" json:"request"` +type requestRepr struct { + Method *string `yaml:"method" json:"method"` + URL *string `yaml:"url" json:"url"` + QueryParams map[string]string `yaml:"queryParams" json:"queryParams"` + Header map[string][]string `yaml:"header" json:"header"` + Body *struct { + Type string `yaml:"type" json:"type"` + Content string `yaml:"content" json:"content"` + } `yaml:"body" json:"body"` +} - Runner struct { - Requests *int `yaml:"requests" json:"requests"` - Concurrency *int `yaml:"concurrency" json:"concurrency"` - Interval *string `yaml:"interval" json:"interval"` - RequestTimeout *string `yaml:"requestTimeout" json:"requestTimeout"` - GlobalTimeout *string `yaml:"globalTimeout" json:"globalTimeout"` - } `yaml:"runner" json:"runner"` +type runnerRepr struct { + Requests *int `yaml:"requests" json:"requests"` + Concurrency *int `yaml:"concurrency" json:"concurrency"` + Interval *string `yaml:"interval" json:"interval"` + RequestTimeout *string `yaml:"requestTimeout" json:"requestTimeout"` + GlobalTimeout *string `yaml:"globalTimeout" json:"globalTimeout"` +} - Tests []struct { - Name *string `yaml:"name" json:"name"` - Field *string `yaml:"field" json:"field"` - Predicate *string `yaml:"predicate" json:"predicate"` - Target interface{} `yaml:"target" json:"target"` - } `yaml:"tests" json:"tests"` +type testcaseRepr struct { + Name *string `yaml:"name" json:"name"` + Field *string `yaml:"field" json:"field"` + Predicate *string `yaml:"predicate" json:"predicate"` + Target interface{} `yaml:"target" json:"target"` } func (repr Repr) Validate() error { From 1235680131043a96070cc863aa928640448f3fc0 Mon Sep 17 00:00:00 2001 From: Gregory Albouy Date: Wed, 9 Nov 2022 00:29:13 +0100 Subject: [PATCH 52/53] refactor(conversion): naming, error handling --- configio/internal/conversion/converters.go | 19 +++++------- configio/internal/conversion/parsing.go | 34 +++++++++++++--------- 2 files changed, 27 insertions(+), 26 deletions(-) diff --git a/configio/internal/conversion/converters.go b/configio/internal/conversion/converters.go index 2ed3be3..1e58141 100644 --- a/configio/internal/conversion/converters.go +++ b/configio/internal/conversion/converters.go @@ -83,11 +83,6 @@ var runnerConverters = []converter{ fieldRunnerGlobalTimeout, } -// TODO: -// var testsConverters = []converter{ -// fieldTests, -// } - var fieldRequestMethod = requestConverter{ decode: func(src Repr, dst *http.Request) error { if m := src.Request.Method; m != nil { @@ -137,37 +132,37 @@ var fieldRequestBody = requestConverter{ }, } -var fieldRunnerRequests = intField( +var fieldRunnerRequests = bindInt( func(src *Repr, dst *benchttp.Runner) (*int, *int) { return src.Runner.Requests, &dst.Requests }, ) -var fieldRunnerConcurrency = intField( +var fieldRunnerConcurrency = bindInt( func(src *Repr, dst *benchttp.Runner) (*int, *int) { return src.Runner.Concurrency, &dst.Concurrency }, ) -var fieldRunnerInterval = durationField( +var fieldRunnerInterval = bindDuration( func(src *Repr, dst *benchttp.Runner) (*string, *time.Duration) { return src.Runner.Interval, &dst.Interval }, ) -var fieldRunnerRequestTimeout = durationField( +var fieldRunnerRequestTimeout = bindDuration( func(src *Repr, dst *benchttp.Runner) (*string, *time.Duration) { return src.Runner.RequestTimeout, &dst.RequestTimeout }, ) -var fieldRunnerGlobalTimeout = durationField( +var fieldRunnerGlobalTimeout = bindDuration( func(src *Repr, dst *benchttp.Runner) (*string, *time.Duration) { return src.Runner.GlobalTimeout, &dst.GlobalTimeout }, ) -func durationField( +func bindDuration( bind func(*Repr, *benchttp.Runner) (*string, *time.Duration), ) converter { return converter{ @@ -184,7 +179,7 @@ func durationField( } } -func intField( +func bindInt( bind func(*Repr, *benchttp.Runner) (*int, *int), ) converter { return converter{ diff --git a/configio/internal/conversion/parsing.go b/configio/internal/conversion/parsing.go index 7961495..26125f9 100644 --- a/configio/internal/conversion/parsing.go +++ b/configio/internal/conversion/parsing.go @@ -54,32 +54,30 @@ func parseTests(tests []testcaseRepr) ([]benchttp.TestCase, error) { } func parseTestcase(in testcaseRepr, idx int) (benchttp.TestCase, error) { - fieldDesc := func(caseField string) string { - return fmt.Sprintf("tests[%d].%s", idx, caseField) - } - - if err := assertDefinedFields(map[string]interface{}{ - fieldDesc("name"): in.Name, - fieldDesc("field"): in.Field, - fieldDesc("predicate"): in.Predicate, - fieldDesc("target"): in.Target, - }); err != nil { - return benchttp.TestCase{}, err + for fieldName, fieldValue := range map[string]interface{}{ + "name": in.Name, + "field": in.Field, + "predicate": in.Predicate, + "target": in.Target, + } { + if fieldValue == nil { + return benchttp.TestCase{}, errMissingTestField(fieldName, idx) + } } field := benchttp.MetricsField(*in.Field) if err := field.Validate(); err != nil { - return benchttp.TestCase{}, fmt.Errorf("%s: %s", fieldDesc("field"), err) + return benchttp.TestCase{}, errInvalidTestField("field", idx, err) } predicate := benchttp.TestPredicate(*in.Predicate) if err := predicate.Validate(); err != nil { - return benchttp.TestCase{}, fmt.Errorf("%s: %s", fieldDesc("predicate"), err) + return benchttp.TestCase{}, errInvalidTestField("predicate", idx, err) } target, err := parseMetricValue(field, fmt.Sprint(in.Target)) if err != nil { - return benchttp.TestCase{}, fmt.Errorf("%s: %s", fieldDesc("target"), err) + return benchttp.TestCase{}, errInvalidTestField("target", idx, err) } return benchttp.TestCase{ @@ -122,3 +120,11 @@ func assertDefinedFields(fields map[string]interface{}) error { } return nil } + +func errMissingTestField(fieldName string, idx int) error { + return fmt.Errorf("tests[%d]: missing field %s", idx, fieldName) +} + +func errInvalidTestField(fieldName string, idx int, err error) error { + return fmt.Errorf("tests[%d].%s: invalid value: %w", idx, fieldName, err) +} From 9847cb82f268208216ab00a1f4e65581f72841ee Mon Sep 17 00:00:00 2001 From: Gregory Albouy Date: Wed, 9 Nov 2022 02:30:57 +0100 Subject: [PATCH 53/53] wip: encoding todo: - configio: decoder API - conversion: decode: fix nil pointer derefence - conversion: separate encode/decode (use dedicated pointerless structure for encode?) - body: to encode or not to encode (alters request) - yaml encode - various refactoring --- configio/internal/conversion/converters.go | 62 +++++++++++++++++-- .../internal/conversion/representation.go | 6 ++ configio/json.go | 6 ++ configio/json_test.go | 23 ++++++- 4 files changed, 92 insertions(+), 5 deletions(-) diff --git a/configio/internal/conversion/converters.go b/configio/internal/conversion/converters.go index 1e58141..e9437bf 100644 --- a/configio/internal/conversion/converters.go +++ b/configio/internal/conversion/converters.go @@ -12,14 +12,12 @@ import ( ) type converter struct { - // TODO: - // encode func(src benchttp.Runner, dst *Repr) + encode func(src benchttp.Runner, dst *Repr) decode func(src Repr, dst *benchttp.Runner) error } type requestConverter struct { - // TODO: - // encode func(src *http.Request, dst *Repr) + encode func(src *http.Request, dst *Repr) decode func(src Repr, dst *http.Request) error } @@ -41,6 +39,13 @@ var fieldRequest = converter{ } return nil }, + encode: func(src benchttp.Runner, dst *Repr) { + if src.Request != nil { + for _, c := range requestConverters { + c.encode(src.Request, dst) + } + } + }, } var fieldRunner = converter{ @@ -52,6 +57,11 @@ var fieldRunner = converter{ } return nil }, + encode: func(src benchttp.Runner, dst *Repr) { + for _, c := range runnerConverters { + c.encode(src, dst) + } + }, } var fieldTests = converter{ @@ -66,6 +76,25 @@ var fieldTests = converter{ } return nil }, + encode: func(src benchttp.Runner, dst *Repr) { + for _, c := range src.Tests { + // /!\ loop ref hazard + name := c.Name + field := string(c.Field) + predicate := string(c.Predicate) + target := c.Target + switch t := target.(type) { + case time.Duration: + target = t.String() + } + dst.Tests = append(dst.Tests, testcaseRepr{ + Name: &name, + Field: &field, + Predicate: &predicate, + Target: &target, + }) + } + }, } var requestConverters = []requestConverter{ @@ -90,6 +119,9 @@ var fieldRequestMethod = requestConverter{ } return nil }, + encode: func(src *http.Request, dst *Repr) { + dst.Request.Method = &src.Method + }, } var fieldRequestURL = requestConverter{ @@ -103,6 +135,10 @@ var fieldRequestURL = requestConverter{ } return nil }, + encode: func(src *http.Request, dst *Repr) { + s := src.URL.String() + dst.Request.URL = &s + }, } var fieldRequestHeader = requestConverter{ @@ -116,6 +152,9 @@ var fieldRequestHeader = requestConverter{ } return nil }, + encode: func(src *http.Request, dst *Repr) { + dst.Request.Header = src.Header + }, } var fieldRequestBody = requestConverter{ @@ -130,6 +169,9 @@ var fieldRequestBody = requestConverter{ } return nil }, + encode: func(src *http.Request, dst *Repr) { + // TODO + }, } var fieldRunnerRequests = bindInt( @@ -176,6 +218,12 @@ func bindDuration( } return nil }, + encode: func(src benchttp.Runner, dst *Repr) { + if vdst, vsrc := bind(dst, &src); vsrc != nil { + // FIXME: nil pointer deref + *vdst = vsrc.String() + } + }, } } @@ -189,5 +237,11 @@ func bindInt( } return nil }, + encode: func(src benchttp.Runner, dst *Repr) { + if vdst, vsrc := bind(dst, &src); vsrc != nil { + // FIXME: nil pointer deref + *vdst = *vsrc + } + }, } } diff --git a/configio/internal/conversion/representation.go b/configio/internal/conversion/representation.go index bd3f532..91de501 100644 --- a/configio/internal/conversion/representation.go +++ b/configio/internal/conversion/representation.go @@ -60,6 +60,12 @@ func (repr Repr) Decode(dst *benchttp.Runner) error { return nil } +func (repr *Repr) Encode(src benchttp.Runner) { + for _, c := range converters { + c.encode(src, repr) + } +} + type Reprs []Repr // MergeInto successively parses the given representations into dst. diff --git a/configio/json.go b/configio/json.go index 0058e59..1bfc4b8 100644 --- a/configio/json.go +++ b/configio/json.go @@ -17,6 +17,12 @@ type JSONDecoder struct{ r io.Reader } var _ decoder = (*JSONDecoder)(nil) +func MarshalJSON(runner benchttp.Runner) ([]byte, error) { + repr := conversion.Repr{} + repr.Encode(runner) + return json.Marshal(repr) +} + // UnmarshalJSON parses the JSON-encoded data and stores the result // in the benchttp.Runner pointed to by dst. func UnmarshalJSON(in []byte, dst *benchttp.Runner) error { diff --git a/configio/json_test.go b/configio/json_test.go index d21a98b..7e982e4 100644 --- a/configio/json_test.go +++ b/configio/json_test.go @@ -7,10 +7,12 @@ import ( "testing" "github.com/benchttp/sdk/benchttp" + "github.com/benchttp/sdk/benchttptest" "github.com/benchttp/sdk/configio" + "github.com/benchttp/sdk/configio/internal/testdata" ) -func TestMarshalJSON(t *testing.T) { +func TestUnmarshalJSON(t *testing.T) { const testURL = "https://example.com" baseInput := object{ "request": object{ @@ -122,6 +124,25 @@ func TestJSONDecoder(t *testing.T) { }) } +func TestMarshalJSON(t *testing.T) { + src := testdata.ValidFullJSON().Runner + + b, err := configio.MarshalJSON(src) + if err != nil { + t.Log(string(b)) + t.Fatal(err) + } + + got := benchttp.Runner{} + if err := configio.UnmarshalJSON(b, &got); err != nil { + t.Log(string(b)) + t.Fatal(err) + } + + benchttptest.AssertEqualRunners(t, src, got) + t.Log(string(b)) +} + // helpers type object map[string]interface{}