diff --git a/cmd/server/main.go b/cmd/server/main.go index a636c70..277091c 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -13,7 +13,7 @@ import ( "github.com/joho/godotenv" "github.com/benchttp/engine/cmd/server/response" - "github.com/benchttp/engine/internal/configparse" + "github.com/benchttp/engine/configparse" "github.com/benchttp/engine/runner" ) diff --git a/internal/configparse/json.go b/configparse/json.go similarity index 56% rename from internal/configparse/json.go rename to configparse/json.go index 736c520..ed052c9 100644 --- a/internal/configparse/json.go +++ b/configparse/json.go @@ -6,17 +6,17 @@ import ( // JSON reads input bytes as JSON and unmarshals it into a runner.ConfigGlobal. func JSON(in []byte) (runner.Config, error) { - parser := jsonParser{} + parser := JSONParser{} - var uconf UnmarshaledConfig - if err := parser.parse(in, &uconf); err != nil { + var repr Representation + if err := parser.Parse(in, &repr); err != nil { return runner.Config{}, err } - pconf, err := newParsedConfig(uconf) + cfg, err := ParseRepresentation(repr) if err != nil { return runner.Config{}, err } - return runner.DefaultConfig().Override(pconf.config, pconf.fields...), nil + return cfg.Override(runner.DefaultConfig()), nil } diff --git a/internal/configparse/json_test.go b/configparse/json_test.go similarity index 84% rename from internal/configparse/json_test.go rename to configparse/json_test.go index 54cec95..142c98d 100644 --- a/internal/configparse/json_test.go +++ b/configparse/json_test.go @@ -4,10 +4,9 @@ import ( "encoding/json" "errors" "net/url" - "reflect" "testing" - "github.com/benchttp/engine/internal/configparse" + "github.com/benchttp/engine/configparse" "github.com/benchttp/engine/runner" ) @@ -47,18 +46,16 @@ func TestJSON(t *testing.T) { input: baseInput.assign(object{ "runner": object{"concurrency": 3}, }).json(), - expConfig: runner.DefaultConfig().Override( - runner.Config{ - Request: runner.RequestConfig{ - URL: mustParseURL("https://example.com"), - }, - Runner: runner.RecorderConfig{ - Concurrency: 3, - }, + expConfig: runner.Config{ + Request: runner.RequestConfig{ + URL: mustParseURL("https://example.com"), }, - "url", - "concurrency", - ), + Runner: runner.RecorderConfig{ + Concurrency: 3, + }, + }. + WithFields("url", "concurrency"). + Override(runner.DefaultConfig()), expError: nil, }, } @@ -66,7 +63,7 @@ func TestJSON(t *testing.T) { for _, tc := range testcases { t.Run(tc.name, func(t *testing.T) { gotConfig, gotError := configparse.JSON(tc.input) - if !reflect.DeepEqual(gotConfig, tc.expConfig) { + if !gotConfig.Equal(tc.expConfig) { t.Errorf("unexpected config:\nexp %+v\ngot %+v", tc.expConfig, gotConfig) } diff --git a/internal/configparse/parse.go b/configparse/parse.go similarity index 69% rename from internal/configparse/parse.go rename to configparse/parse.go index 3c56655..66761c7 100644 --- a/internal/configparse/parse.go +++ b/configparse/parse.go @@ -10,11 +10,11 @@ import ( "github.com/benchttp/engine/runner" ) -// UnmarshaledConfig is a raw data model for runner config files. +// Representation is a raw data model for runner config files. // It serves as a receiver for unmarshaling processes and for that reason // its types are kept simple (certain types are incompatible with certain // unmarshalers). -type UnmarshaledConfig struct { +type Representation struct { Extends *string `yaml:"extends" json:"extends"` Request struct { @@ -36,10 +36,6 @@ type UnmarshaledConfig struct { GlobalTimeout *string `yaml:"globalTimeout" json:"globalTimeout"` } `yaml:"runner" json:"runner"` - Output struct { - Silent *bool `yaml:"silent" json:"silent"` - } `yaml:"output" json:"output"` - Tests []struct { Name *string `yaml:"name" json:"name"` Field *string `yaml:"field" json:"field"` @@ -48,105 +44,91 @@ type UnmarshaledConfig struct { } `yaml:"tests" json:"tests"` } -// parsedConfig embeds a parsed runner.ConfigGlobal and the list of its set fields. -type parsedConfig struct { - // TODO: do not embed, use field config - config runner.Config - fields []string -} - -// addField adds a field to the list of set fields. -func (pconf *parsedConfig) add(field string) { - pconf.fields = append(pconf.fields, field) -} +// 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{} -// newParsedConfig parses an input raw config as a runner.ConfigGlobal and returns -// a parsedConfig or the first non-nil error occurring in the process. -func newParsedConfig(uconf UnmarshaledConfig) (parsedConfig, error) { //nolint:gocognit // acceptable complexity for a parsing func - maxFields := len(runner.ConfigFieldsUsage) - pconf := parsedConfig{fields: make([]string, 0, maxFields)} - cfg := &pconf.config + addField := func(field string) { + assignedFields = append(assignedFields, field) + } - abort := func(err error) (parsedConfig, error) { - return parsedConfig{}, err + abort := func(err error) (runner.Config, error) { + return runner.Config{}, err } - if method := uconf.Request.Method; method != nil { + if method := repr.Request.Method; method != nil { cfg.Request.Method = *method - pconf.add(runner.ConfigFieldMethod) + addField(runner.ConfigFieldMethod) } - if rawURL := uconf.Request.URL; rawURL != nil { - parsedURL, err := parseAndBuildURL(*uconf.Request.URL, uconf.Request.QueryParams) + if rawURL := repr.Request.URL; rawURL != nil { + parsedURL, err := parseAndBuildURL(*repr.Request.URL, repr.Request.QueryParams) if err != nil { return abort(err) } cfg.Request.URL = parsedURL - pconf.add(runner.ConfigFieldURL) + addField(runner.ConfigFieldURL) } - if header := uconf.Request.Header; header != nil { + if header := repr.Request.Header; header != nil { httpHeader := http.Header{} for key, val := range header { httpHeader[key] = val } cfg.Request.Header = httpHeader - pconf.add(runner.ConfigFieldHeader) + addField(runner.ConfigFieldHeader) } - if body := uconf.Request.Body; body != nil { + if body := repr.Request.Body; body != nil { cfg.Request.Body = runner.RequestBody{ Type: body.Type, Content: []byte(body.Content), } - pconf.add(runner.ConfigFieldBody) + addField(runner.ConfigFieldBody) } - if requests := uconf.Runner.Requests; requests != nil { + if requests := repr.Runner.Requests; requests != nil { cfg.Runner.Requests = *requests - pconf.add(runner.ConfigFieldRequests) + addField(runner.ConfigFieldRequests) } - if concurrency := uconf.Runner.Concurrency; concurrency != nil { + if concurrency := repr.Runner.Concurrency; concurrency != nil { cfg.Runner.Concurrency = *concurrency - pconf.add(runner.ConfigFieldConcurrency) + addField(runner.ConfigFieldConcurrency) } - if interval := uconf.Runner.Interval; interval != nil { + if interval := repr.Runner.Interval; interval != nil { parsedInterval, err := parseOptionalDuration(*interval) if err != nil { return abort(err) } cfg.Runner.Interval = parsedInterval - pconf.add(runner.ConfigFieldInterval) + addField(runner.ConfigFieldInterval) } - if requestTimeout := uconf.Runner.RequestTimeout; requestTimeout != nil { + if requestTimeout := repr.Runner.RequestTimeout; requestTimeout != nil { parsedTimeout, err := parseOptionalDuration(*requestTimeout) if err != nil { return abort(err) } cfg.Runner.RequestTimeout = parsedTimeout - pconf.add(runner.ConfigFieldRequestTimeout) + addField(runner.ConfigFieldRequestTimeout) } - if globalTimeout := uconf.Runner.GlobalTimeout; globalTimeout != nil { + if globalTimeout := repr.Runner.GlobalTimeout; globalTimeout != nil { parsedGlobalTimeout, err := parseOptionalDuration(*globalTimeout) if err != nil { return abort(err) } cfg.Runner.GlobalTimeout = parsedGlobalTimeout - pconf.add(runner.ConfigFieldGlobalTimeout) - } - - if silent := uconf.Output.Silent; silent != nil { - cfg.Output.Silent = *silent - pconf.add(runner.ConfigFieldSilent) + addField(runner.ConfigFieldGlobalTimeout) } - testSuite := uconf.Tests + testSuite := repr.Tests if len(testSuite) == 0 { - return pconf, nil + return cfg.WithFields(assignedFields...), nil } cases := make([]runner.TestCase, len(testSuite)) @@ -187,9 +169,9 @@ func newParsedConfig(uconf UnmarshaledConfig) (parsedConfig, error) { //nolint:g } } cfg.Tests = cases - pconf.add(runner.ConfigFieldTests) + addField(runner.ConfigFieldTests) - return pconf, nil + return cfg.WithFields(assignedFields...), nil } // helpers diff --git a/configparse/parser_json.go b/configparse/parser_json.go new file mode 100644 index 0000000..ff7c64b --- /dev/null +++ b/configparse/parser_json.go @@ -0,0 +1,65 @@ +package configparse + +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/configparse/parser_json_test.go b/configparse/parser_json_test.go new file mode 100644 index 0000000..51a4914 --- /dev/null +++ b/configparse/parser_json_test.go @@ -0,0 +1,63 @@ +package configparse_test + +import ( + "testing" + + "github.com/benchttp/engine/configparse" +) + +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 configparse.JSONParser + rawcfg configparse.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/internal/configparse/parser.go b/configparse/parser_yaml.go similarity index 58% rename from internal/configparse/parser.go rename to configparse/parser_yaml.go index 0bae47e..160cc7b 100644 --- a/internal/configparse/parser.go +++ b/configparse/parser_yaml.go @@ -2,7 +2,6 @@ package configparse import ( "bytes" - "encoding/json" "errors" "fmt" "regexp" @@ -10,12 +9,12 @@ import ( "gopkg.in/yaml.v3" ) -// yamlParser implements configParser. -type yamlParser struct{} +// YAMLParser implements configParser. +type YAMLParser struct{} -// parse decodes a raw yaml input in strict mode (unknown fields disallowed) +// 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 *UnmarshaledConfig) error { +func (p YAMLParser) Parse(in []byte, dst *Representation) error { decoder := yaml.NewDecoder(bytes.NewReader(in)) decoder.KnownFields(true) return p.handleError(decoder.Decode(dst)) @@ -23,7 +22,7 @@ func (p yamlParser) parse(in []byte, dst *UnmarshaledConfig) error { // handleError handles a raw yaml decoder.Decode error, filters it, // and return the resulting error. -func (p yamlParser) handleError(err error) error { +func (p YAMLParser) handleError(err error) error { // yaml.TypeError errors require special handling, other errors // (nil included) can be returned as is. var typeError *yaml.TypeError @@ -54,7 +53,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 (p YAMLParser) isCustomFieldError(raw string) bool { customFieldRgx := regexp.MustCompile( // raw output example: // line 9: field x-my-alias not found in type struct { ... } @@ -66,7 +65,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 (p YAMLParser) prettyErrorMessage(raw string) string { // field not found error fieldNotFoundRgx := regexp.MustCompile( // raw output example (type unmarshaledConfig is entirely exposed): @@ -96,59 +95,3 @@ func (p yamlParser) prettyErrorMessage(raw string) string { // we may not have covered all cases, return raw output in this case return raw } - -// 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 *UnmarshaledConfig) 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/internal/configparse/parser_internal_test.go b/configparse/parser_yaml_test.go similarity index 57% rename from internal/configparse/parser_internal_test.go rename to configparse/parser_yaml_test.go index 29aeccd..e80439a 100644 --- a/internal/configparse/parser_internal_test.go +++ b/configparse/parser_yaml_test.go @@ -1,4 +1,4 @@ -package configparse +package configparse_test import ( "errors" @@ -6,6 +6,8 @@ import ( "testing" "gopkg.in/yaml.v3" + + "github.com/benchttp/engine/configparse" ) func TestYAMLParser(t *testing.T) { @@ -63,12 +65,12 @@ func TestYAMLParser(t *testing.T) { for _, tc := range testcases { t.Run(tc.label, func(t *testing.T) { var ( - parser yamlParser - rawcfg UnmarshaledConfig + parser configparse.YAMLParser + rawcfg configparse.Representation yamlErr *yaml.TypeError ) - gotErr := parser.parse(tc.in, &rawcfg) + gotErr := parser.Parse(tc.in, &rawcfg) if tc.expErr == nil { if gotErr != nil { @@ -88,59 +90,3 @@ func TestYAMLParser(t *testing.T) { } }) } - -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 jsonParser - rawcfg UnmarshaledConfig - ) - - 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/examples/config/default.yml b/examples/config/default.yml index 07e6bea..53369ae 100644 --- a/examples/config/default.yml +++ b/examples/config/default.yml @@ -8,6 +8,3 @@ runner: interval: 0ms requestTimeout: 5s globalTimeout: 30s - -output: - silent: false diff --git a/examples/config/full.yml b/examples/config/full.yml index c20fb5e..c83d434 100644 --- a/examples/config/full.yml +++ b/examples/config/full.yml @@ -17,6 +17,3 @@ runner: interval: 50ms requestTimeout: 2s globalTimeout: 60s - -output: - silent: true diff --git a/runner/internal/config/config.go b/runner/internal/config/config.go index debcdd0..837aa2e 100644 --- a/runner/internal/config/config.go +++ b/runner/internal/config/config.go @@ -7,6 +7,7 @@ import ( "fmt" "net/http" "net/url" + "reflect" "time" "github.com/benchttp/engine/runner/internal/tests" @@ -78,9 +79,12 @@ type Runner struct { GlobalTimeout time.Duration } -// Output contains options relative to the output. -type Output struct { - Silent bool +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. @@ -88,48 +92,80 @@ type Output struct { type Global struct { Request Request Runner Runner - Output Output - Tests []tests.Case + + 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 } -// String returns an indented JSON representation of Config -// for debugging purposes. +// String implements fmt.Stringer. It returns an indented JSON representation +// of Config for debugging purposes. func (cfg Global) String() string { b, _ := json.MarshalIndent(cfg, "", " ") return string(b) } -// Override returns a new Config based on cfg with overridden values from c. -// Only fields specified in options are replaced. Accepted options are limited -// to existing Fields, other values are silently ignored. -func (cfg Global) Override(c Global, fields ...string) Global { - for _, field := range fields { +// 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: - cfg.Request.Method = c.Request.Method + base.Request.Method = cfg.Request.Method case FieldURL: - cfg.Request.URL = c.Request.URL + base.Request.URL = cfg.Request.URL case FieldHeader: - cfg.overrideHeader(c.Request.Header) + base.overrideHeader(cfg.Request.Header) case FieldBody: - cfg.Request.Body = c.Request.Body + base.Request.Body = cfg.Request.Body case FieldRequests: - cfg.Runner.Requests = c.Runner.Requests + base.Runner.Requests = cfg.Runner.Requests case FieldConcurrency: - cfg.Runner.Concurrency = c.Runner.Concurrency + base.Runner.Concurrency = cfg.Runner.Concurrency case FieldInterval: - cfg.Runner.Interval = c.Runner.Interval + base.Runner.Interval = cfg.Runner.Interval case FieldRequestTimeout: - cfg.Runner.RequestTimeout = c.Runner.RequestTimeout + base.Runner.RequestTimeout = cfg.Runner.RequestTimeout case FieldGlobalTimeout: - cfg.Runner.GlobalTimeout = c.Runner.GlobalTimeout - case FieldSilent: - cfg.Output.Silent = c.Output.Silent + base.Runner.GlobalTimeout = cfg.Runner.GlobalTimeout case FieldTests: - cfg.Tests = c.Tests + base.Tests = cfg.Tests } } - return cfg + return base } // overrideHeader overrides cfg's Request.Header with the values from newHeader. diff --git a/runner/internal/config/config_test.go b/runner/internal/config/config_test.go index b4ce0ef..6d43ebe 100644 --- a/runner/internal/config/config_test.go +++ b/runner/internal/config/config_test.go @@ -73,7 +73,7 @@ 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{} - newCfg := config.Global{ + nextCfg := config.Global{ Request: config.Request{ Body: config.RequestBody{}, }.WithURL("http://a.b?p=2"), @@ -83,19 +83,26 @@ func TestGlobal_Override(t *testing.T) { RequestTimeout: 3 * time.Second, GlobalTimeout: 4 * time.Second, }, - Output: config.Output{ - Silent: true, - }, } - if gotCfg := baseCfg.Override(newCfg); !reflect.DeepEqual(gotCfg, baseCfg) { + 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{} - newCfg := config.Global{ + nextCfg := config.Global{ Request: config.Request{ Body: validBody, }.WithURL("http://a.b?p=2"), @@ -105,23 +112,10 @@ func TestGlobal_Override(t *testing.T) { RequestTimeout: 3 * time.Second, GlobalTimeout: 4 * time.Second, }, - Output: config.Output{ - Silent: true, - }, - } - fields := []string{ - config.FieldMethod, - config.FieldURL, - config.FieldRequests, - config.FieldConcurrency, - config.FieldRequestTimeout, - config.FieldGlobalTimeout, - config.FieldBody, - config.FieldSilent, - } + }.WithFields(fields...) - if gotCfg := baseCfg.Override(newCfg, fields...); !reflect.DeepEqual(gotCfg, newCfg) { - t.Errorf("did not override expected fields:\nexp %v\ngot %v", baseCfg, gotCfg) + if gotCfg := nextCfg.Override(baseCfg); !gotCfg.Equal(nextCfg) { + t.Errorf("did not override expected fields:\nexp %v\ngot %v", nextCfg, gotCfg) t.Log(fields) } }) @@ -192,19 +186,19 @@ func TestGlobal_Override(t *testing.T) { for _, tc := range testcases { t.Run(tc.label, func(t *testing.T) { - oldCfg := config.Global{ + baseCfg := config.Global{ Request: config.Request{ Header: tc.oldHeader, }, } - newCfg := config.Global{ + nextCfg := config.Global{ Request: config.Request{ Header: tc.newHeader, }, - } + }.WithFields(config.FieldHeader) - gotCfg := oldCfg.Override(newCfg, 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) @@ -303,6 +297,43 @@ 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. diff --git a/runner/internal/config/default.go b/runner/internal/config/default.go index b07e90d..b5299de 100644 --- a/runner/internal/config/default.go +++ b/runner/internal/config/default.go @@ -20,9 +20,6 @@ var defaultConfig = Global{ RequestTimeout: 5 * time.Second, GlobalTimeout: 30 * time.Second, }, - Output: Output{ - Silent: false, - }, } // Default returns a default config that is safe to use. diff --git a/runner/internal/config/field.go b/runner/internal/config/field.go index 693ae33..c23c831 100644 --- a/runner/internal/config/field.go +++ b/runner/internal/config/field.go @@ -10,7 +10,6 @@ const ( FieldInterval = "interval" FieldRequestTimeout = "requestTimeout" FieldGlobalTimeout = "globalTimeout" - FieldSilent = "silent" FieldTests = "tests" ) @@ -25,7 +24,6 @@ var FieldsUsage = map[string]string{ FieldInterval: "Minimum duration between two non concurrent requests", FieldRequestTimeout: "Timeout for each HTTP request", FieldGlobalTimeout: "Max duration of test", - FieldSilent: "Silent mode (no write to stdout)", FieldTests: "Test suite", } diff --git a/runner/internal/config/field_test.go b/runner/internal/config/field_test.go index 2ba02c9..ad53a23 100644 --- a/runner/internal/config/field_test.go +++ b/runner/internal/config/field_test.go @@ -19,7 +19,6 @@ func TestIsField(t *testing.T) { {In: config.FieldInterval, Exp: true}, {In: config.FieldRequestTimeout, Exp: true}, {In: config.FieldGlobalTimeout, Exp: true}, - {In: config.FieldSilent, Exp: true}, {In: "notafield", Exp: false}, }).Run(t) } diff --git a/runner/runner.go b/runner/runner.go index c705c78..f49067d 100644 --- a/runner/runner.go +++ b/runner/runner.go @@ -16,7 +16,6 @@ type ( RequestConfig = config.Request RequestBody = config.RequestBody RecorderConfig = config.Runner - OutputConfig = config.Output RecordingProgress = recorder.Progress RecordingStatus = recorder.Status @@ -51,7 +50,6 @@ const ( ConfigFieldInterval = config.FieldInterval ConfigFieldRequestTimeout = config.FieldRequestTimeout ConfigFieldGlobalTimeout = config.FieldGlobalTimeout - ConfigFieldSilent = config.FieldSilent ConfigFieldTests = config.FieldTests )