Skip to content

resetting string-to-string flag to default state is not possible #455

@brandon1024

Description

@brandon1024

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)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions