Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix overwrite by SetDefault for options that share Value #23

Merged
merged 5 commits into from
Nov 13, 2024

Conversation

mafredri
Copy link
Member

@mafredri mafredri commented Nov 11, 2024

While working on tests for coder/internal#209 I noticed we don't properly handle shared values between options.

Perhaps this isn't the intended use-case, but it's definitely something that happens in the real world. Consider this example:

	emailFrom := serpent.Option{
		Name:        "Email: From Address",
		Description: "The sender's address to use.",
		Flag:        "email-from",
		Env:         "CODER_EMAIL_FROM",
		Value:       &c.Notifications.SMTP.From,
		Group:       &deploymentGroupEmail,
		YAML:        "from",
	}
	// ...
		{
			Name:        "Notifications: Email: From Address",
			Description: "The sender's address to use.",
			Flag:        "notifications-email-from",
			Env:         "CODER_NOTIFICATIONS_EMAIL_FROM",
			Value:       &c.Notifications.SMTP.From,
			Group:       &deploymentGroupNotificationsEmail,
			YAML:        "from",
			UseInstead:  serpent.OptionSet{emailFrom},
		},

See how both options reference c.Notifications.SMTP.From? This is still OK, but if the user adds a Default to either (or both) of these, the behavior will be very unexpected.

This PR fixes this in a way that handles this use-case. A simpler approach would be to return an error if two flags reference the same value, but I feel that would be restrictive and go against common Go patterns with regard to flag parsing (i.e. sharing the same var).

Comment on lines -310 to -312
if opt.ValueSource != ValueSourceNone {
continue
}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: This was removed in this refactor because we have a different way of handling values that have been set by the user.

However, this breaks one test in coder/coder: https://github.com/coder/coder/actions/runs/11782339194/job/32817210417?pr=15476#step:7:621

I could add a if opt.Value == nil && opt.ValueSource != ValueSourceNone { continue } here and it will fix the test. The only problem is, I don't understand the logic. If the value has been provided by the user and opt.Value == nil, that's OK. But if the value hasn't been provided, then we error out. Why?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't spent time to try and figure this out. This felt vaguely familiar, so I looked up when it was added.

It was added in this commit: coder/coder@7edea99

And this PR: coder/coder#6934

So it has to relate with YAML support somehow? I was hoping the commit when it was added would have more context.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for looking and sharing, that was very helpful! It lead me to this: e053fda (#23).

@mafredri mafredri self-assigned this Nov 12, 2024

err = makeCmd("def.com", "").Invoke("--alt-url", "hup").Run()
require.NoError(t, err)
require.Equal(t, "hup", got)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we try to handle cases where both are set? Currently it defaults to the last one set. Should we at least make this deterministic, like by alphabetical order?

I have not tested if 1 is set with an env var, the other the flag.

// Passes
err = makeCmd("def.com", "").Invoke("--url", "sup", "--alt-url", "hup").Run()
require.NoError(t, err)
require.Equal(t, serpent.String("hup"), got)

// Fails
err = makeCmd("def.com", "").Invoke("--alt-url", "hup", "--url", "sup").Run()
require.NoError(t, err)
require.Equal(t, serpent.String("hup"), got)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think alphabetical order is going to be intuitive. The current behaviour of "last one wins" makes more sense to me.

Alternatively, we could say that specifying a non-default value for both is an error? That would remove any ambiguity, but would be a breaking change.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Emyrk it's typical behavior for CLI programs to use the last value of any given flag, for example:

~/Work/Code/serpent mafredri/fix-shared-value-overwrite*
❯ git log -1 --format='%h'
e053fda

~/Work/Code/serpent mafredri/fix-shared-value-overwrite*
❯ git log -1 --format='%h' --format='%H'
e053fdac0b0dd01a0e6278dfbd703bbf7486a840

So I would say the failing test-case is working as intended, just the assertion that should change.

I added some more test-cases to cover this and also the env/flag priority one in 457cbbe.

@johnstcn In this PR we already handle the case where flags specify multiple defaults, and that's OK as long as:

  • Either only one specifies a default
  • Either one or more specify the same exact default
  • If two different defaults are detected, that's an error

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This made me curious. In the case of envs, it seems to be "first wins". I tested with the following test-cases.

// Both envs are given, first wins.
inv = makeCmd("def.com", "").Invoke()
inv.Environ.Set("URL", "sup")
inv.Environ.Set("ALT_URL", "hup")
err = inv.Run()
require.NoError(t, err)
require.Equal(t, "sup", got)

// Both envs are given, first wins #2.
inv = makeCmd("def.com", "").Invoke()
inv.Environ.Set("ALT_URL", "hup")
inv.Environ.Set("URL", "sup")
err = inv.Run()
require.NoError(t, err)
require.Equal(t, "sup", got)

I'm not sure if we need to do anything about that, but just keep in mind that this behavior has not changed in this PR.

option.go Outdated Show resolved Hide resolved
Comment on lines +373 to +386
if opt.Default == "" {
continue
}
if optWithDefault != nil && optWithDefault.Default != opt.Default {
merr = multierror.Append(
merr,
xerrors.Errorf(
"parse %q: multiple defaults set for the same value: %q and %q (%q)",
opt.Name, opt.Default, optWithDefault.Default, optWithDefault.Name,
),
)
continue
}
optWithDefault = opt
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: this also looks like it could be extracted to its own function and tested independently

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see a benefit currently as this is pretty much "the meat" of this function and we don't need to call it elsewhere.


err = makeCmd("def.com", "").Invoke("--alt-url", "hup").Run()
require.NoError(t, err)
require.Equal(t, "hup", got)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think alphabetical order is going to be intuitive. The current behaviour of "last one wins" makes more sense to me.

Alternatively, we could say that specifying a non-default value for both is an error? That would remove any ambiguity, but would be a breaking change.

Copy link
Member

@Emyrk Emyrk left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I updated my go mod to this branch, and that failing you mentioned test is passing locally.

option.go Outdated
Comment on lines 337 to 346
if a.ValueSource != b.ValueSource {
for _, vs := range valueSourcePriority {
if a.ValueSource == vs {
return -1
}
if b.ValueSource == vs {
return 1
}
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is totally fine, and an optimized solution. My morning brain just took a second to understand it.

Can you just throw a comment like, // Sorts by value source then a default value being set


I like using subtraction like this for compare functions.

if a.ValueSource != b.ValueSource {
    return slices.Index(valueSourcePriority, a.ValueSource) - slices.Index(valueSourcePriority, b.ValueSource)
}

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's clean, thanks! 👍🏻

Copy link
Member

@johnstcn johnstcn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for doing this Mathias!

@ammario
Copy link
Member

ammario commented Nov 13, 2024

My instinct would be to error if the value is the same as the semantics around priority are not immediately obvious and perhaps better for clients to decide with their own logic. It seems like you have a lot of eyes here already so feel free to merge at will.

@mafredri
Copy link
Member Author

My instinct would be to error if the value is the same as the semantics around priority are not immediately obvious and perhaps better for clients to decide with their own logic. It seems like you have a lot of eyes here already so feel free to merge at will.

Yeah, I was a bit torn myself between the two. In the end settled on this approach because it makes maintenance of deprecated flags a lot easier, especially if all you're doing is renaming or reorganizing.

If we enforce the use of two different unique values, then there must always exist logic to check both or merge them at an appropriate time, and that seems like a recipe for bugs.

@Emyrk
Copy link
Member

Emyrk commented Nov 13, 2024

If we enforce the use of two different unique values, then there must always exist logic to check both or merge them at an appropriate time, and that seems like a recipe for bugs.

Agreed. I was sold when @johnstcn mentioned flag order already overrides. And it is the existing behavior.

@mafredri mafredri merged commit 09f1207 into main Nov 13, 2024
1 check passed
@mafredri mafredri deleted the mafredri/fix-shared-value-overwrite branch November 13, 2024 22:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants