-
Notifications
You must be signed in to change notification settings - Fork 362
Description
Hello 👋
One of the projects I maintain within my organization uses cobra+pflag to build an extensive command-line interface, and some of the commands use the StringToString flag type. Unfortunately due to how Set() works for this type of flag, it's impossible to reset these types of flags back to their default state, which is particularly troublesome in tests.
The way I understand it is that Set() for StringToString flags actually updates the internal map for the flag, whereas for all other flag types Set() will replace the flag value. Since there's no public mechanism for accessing the internal map value or resetting it, we need to completely destroy and recreate the FlagSet altogether.
Here's the code.
It would be great if the project offered an easy and sensible way to reset a FlagSet back to it's initial state.
Background
The project I maintain is pretty extensive, and we have a very large test suite. Our tests generally look like this:
func TestCommand(t *testing.T) {
cmd := buildCommand()
// start up some services needed for testing ...
t.Run("should do something", func(t *testing.T) {
cfg := tests.BuildTestConfig()
// IMPORTANT: Reset comand state -- including flags, contexts, etc
testutil.ResetCommand(t, cmd)
cmd.SetArgs([]string{"--version", "unknown"})
err := cmd.ExecuteContext(ctx)
if err == nil {
t.Fatalf("expected error but was nil")
}
// ...
})
// more tests
})The command is built and configured once, and then reused across multiple tests. We do this to avoid rebuilding commands with every test. For this to work, we need to reset the command back to its initial state, which means resetting all flags to their default values.
// Reset `cmd` back to its initial state, allowing `cmd` to be reused for other tests.
//
// Resets the command args and context of `cmd` and all of its children, recursively.
func ResetCommand(t *testing.T, cmd *cobra.Command) {
cmd.SetArgs([]string{})
cmd.SetContext(nil)
cmd.Flags().VisitAll(func(f *pflag.Flag) {
if !f.Changed {
return
}
switch f.Value.Type() {
case "stringToString":
// FIXME - requires special handling because Set(f.DefaultValue) will fail, and there's no way to reset to defaults
default:
if err := f.Value.Set(f.DefValue); err != nil {
t.Fatalf("unexpected error: %v", err)
}
}
f.Changed = false
})
for _, c := range cmd.Commands() {
ResetCommand(t, c)
}
}What we Tried
We tried doing something like shown below, but it doesn't work because the map m is actually constructed from the stringified flag value -- it's not a pointer to the actual internal value.
// StringToString args require special handling
m, err := cmd.Flags().GetStringToString(f.Name)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
clear(m)