-
Notifications
You must be signed in to change notification settings - Fork 0
fix: zero-values cli flags do not override config file #28
Conversation
- implement Config.Override
Codecov Report
@@ Coverage Diff @@
## main #28 +/- ##
==========================================
+ Coverage 59.71% 62.67% +2.95%
==========================================
Files 9 10 +1
Lines 278 292 +14
==========================================
+ Hits 166 183 +17
+ Misses 101 98 -3
Partials 11 11
Flags with carried forward coverage won't be shown. Click here to find out more.
Continue to review full report at Codecov.
|
cmd/runner/main.go
Outdated
| // corresponding CLI flag set. It must be called after flag.Parse. | ||
| func overrideConfigWithCLIFlags(cfg *config.Config) { | ||
| // flagNames returns a slice of all flags set. | ||
| func flagNames() []string { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If find the use of this flagNames() method more elegant than the switch! 👍
config/config.go
Outdated
| func (cfg Config) Override(c Config, options ...string) Config { | ||
| for _, option := range options { | ||
| switch option { | ||
| case "url": | ||
| cfg.Request.URL = c.Request.URL | ||
| case "timeout": | ||
| cfg.Request.Timeout = c.Request.Timeout | ||
| case "requests": | ||
| cfg.RunnerOptions.Requests = c.RunnerOptions.Requests | ||
| case "concurrency": | ||
| cfg.RunnerOptions.Concurrency = c.RunnerOptions.Concurrency | ||
| case "globalTimeout": | ||
| cfg.RunnerOptions.GlobalTimeout = c.RunnerOptions.GlobalTimeout | ||
| } | ||
| } | ||
| return cfg | ||
| } | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it makes sense that it is a method of Config and not in main. But I have no strong opinion about this, so the first implementation works for me too 🤷♀️
|
I have added some comments, but both versions could work as they are for me! |
|
Solves the problem but the api becomes a bit confused:
Suggestion: Or this could not work, but anyway I think the current API is not really what we want. |
|
Some clarification about the PR (my bad for not pointing that up earlier):
I disagree with that point: there's factually no parsing involved in
That's interesting, but indeed not doable in a way that makes sense: all flags are already parsed before we get to call |
- do not override unspecified fields - override specified fields
| type unmarshaledConfig struct { | ||
| Request struct { | ||
| Method *string `yaml:"method" json:"method"` | ||
| URL *string `yaml:"url" json:"url"` | ||
| QueryParams map[string]string `yaml:"queryParams" json:"queryParams"` | ||
| Timeout *string `yaml:"timeout" json:"timeout"` | ||
| } `yaml:"request" json:"request"` | ||
|
|
||
| RunnerOptions struct { | ||
| Requests *int `yaml:"requests" json:"requests"` | ||
| Concurrency *int `yaml:"concurrency" json:"concurrency"` | ||
| GlobalTimeout *string `yaml:"globalTimeout" json:"globalTimeout"` | ||
| } `yaml:"runnerOptions" json:"runnerOptions"` | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see by the commit message that somehow using pointers to type fixes zero-value override.
As I did not see the bug happening, do you care to explain a bit ? :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure! In short, it's only a necessary step to make parseRawConfig, where lies the fix, possible:
Lines 35 to 86 in 22cb9d0
| // parseRawConfig parses an input raw config as a config.Config and returns it | |
| // or the first non-nil error occurring in the process. | |
| func parseRawConfig(raw unmarshaledConfig) (config.Config, error) { //nolint:gocognit // acceptable complexity for a parsing func | |
| cfg := config.Config{} | |
| fields := make([]config.Field, 0, 6) | |
| if method := raw.Request.Method; method != nil { | |
| cfg.Request.Method = *method | |
| fields = append(fields, config.FieldMethod) | |
| } | |
| if rawURL := raw.Request.URL; rawURL != nil { | |
| parsedURL, err := parseAndBuildURL(*raw.Request.URL, raw.Request.QueryParams) | |
| if err != nil { | |
| return config.Config{}, err | |
| } | |
| cfg.Request.URL = parsedURL | |
| fields = append(fields, config.FieldURL) | |
| } | |
| if timeout := raw.Request.Timeout; timeout != nil { | |
| parsedTimeout, err := parseOptionalDuration(*timeout) | |
| if err != nil { | |
| return config.Config{}, err | |
| } | |
| cfg.Request.Timeout = parsedTimeout | |
| fields = append(fields, config.FieldTimeout) | |
| } | |
| if requests := raw.RunnerOptions.Requests; requests != nil { | |
| cfg.RunnerOptions.Requests = *requests | |
| fields = append(fields, config.FieldRequests) | |
| } | |
| if concurrency := raw.RunnerOptions.Concurrency; concurrency != nil { | |
| cfg.RunnerOptions.Concurrency = *concurrency | |
| fields = append(fields, config.FieldConcurrency) | |
| } | |
| if globalTimeout := raw.RunnerOptions.GlobalTimeout; globalTimeout != nil { | |
| parsedGlobalTimeout, err := parseOptionalDuration(*globalTimeout) | |
| if err != nil { | |
| return config.Config{}, err | |
| } | |
| cfg.RunnerOptions.GlobalTimeout = parsedGlobalTimeout | |
| fields = append(fields, config.FieldGlobalTimeout) | |
| } | |
| fmt.Println(fields) | |
| return config.Default().Override(cfg, fields...), nil | |
| } |
Just like for CLI flags with main.configFlags, we need to know which options are set regardless of their value. But this is much more complex to achieve for parsed json/yaml, so here we use pointers instead to determine whether a field is set. It's handy because their zero value is nil: without them it would be "" or 0 and we wouldn't be able to tell if it's defaulted or an actual value from the config file.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah obviously ! relying on nil is a great way of doing this !
c1-ra
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice! It looks good to me!
|
Maybe you could just re-add the comment that Thomas suggested concerning |
- bad switch implemention prevented correct override - write unit tests for IsField
config/default.go
Outdated
| RunnerOptions: RunnerOptions{ | ||
| Concurrency: 1, | ||
| Requests: -1, | ||
| Requests: -1, // Use GlobalTimeout as exit condition if omitted. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nitpick-but-not-a-nitpick: explicit -1 results is the use of global timeout as exit condition. Omitting this option results in defaulting to -1 thus the global timeout.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Isn't it already implied given this comment is on defaultConfig?
On a side note we really need to rethink our defaults, I don't think infinite requests by default is a great idea, especially with a default timeout of 30s 🤔 100 would be more appropriate
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree that a limited default value would actually be safer.
ab0c5fd to
0f3438e
Compare
Description
Due to the behaviour of
config.Mergethat relies on zero values to decide whether an option should be overridden, it is currently not possible to override a config file option with a CLI option that is a zero value.For now, it's not that much of an issue since any zero value is an invalid value. However this is not guaranteed to last: in the future we can implement options accepting
0as a valid value (such asinterval) or bools, for which obviouslyfalseshould be acceptable.So this PR provides a new overriding logic via
Config.Overridebased on which values are actually set rather than zero values.Changes
config.Merge,config.MergeDefaultwithConfig.Overridethat allows to replace fields selectivelyflag.CommandLine.Visitconfig/file.marshaledConfigNotes