diff --git a/cliotestutils/application.go b/cliotestutils/application.go new file mode 100644 index 0000000..d69394e --- /dev/null +++ b/cliotestutils/application.go @@ -0,0 +1,73 @@ +package cliotestutils + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/spf13/cobra" + "github.com/stretchr/testify/assert" + + "github.com/anchore/clio" +) + +// NewApplication takes a testing.T, a clio setup config, and a slice of assertions, and returns +// a clio application that will, instead of setting up commands with their normal RunE, set up commands +// such that the assertions are called with the testing.T after config state is set up by reading flags, +// env vars, and config files. Useful for testing that expected configuration options are wired up. +// Note that initializers will be cleared from the clio setup config, since the initialization may happen +// more than once and affect global state. For necessary global state, a workaround is to set it in a TestingMain. +func NewApplication(t *testing.T, cfg *clio.SetupConfig, assertions ...AssertionFunc) clio.Application { + cfg.Initializers = nil + a := clio.New(*cfg) + + var asserter assertionClosure = func(cmd *cobra.Command, args []string, cfgs ...any) { + for _, assertion := range assertions { + assertion(t, cmd, args, cfgs...) + } + } + + return &testApplication{ + a, + asserter, + } +} + +type AssertionFunc func(t *testing.T, cmd *cobra.Command, args []string, cfgs ...any) + +func OptionsEquals(wantOpts any) AssertionFunc { + return func(t *testing.T, cmd *cobra.Command, args []string, cfgs ...any) { + assert.Equal(t, len(cfgs), 1) + if d := cmp.Diff(wantOpts, cfgs[0]); d != "" { + t.Errorf("mismatched options (-want +got):\n%s", d) + } + } +} + +type assertionClosure func(cmd *cobra.Command, args []string, cfgs ...any) + +type testApplication struct { + clio.Application + assertion assertionClosure +} + +func (a *testApplication) SetupCommand(cmd *cobra.Command, cfgs ...any) *cobra.Command { + cmd.RunE = func(cmd *cobra.Command, args []string) error { + a.assertion(cmd, args, cfgs...) + return nil + } + return a.Application.SetupCommand(cmd, cfgs...) +} + +func (a *testApplication) SetupRootCommand(cmd *cobra.Command, cfgs ...any) *cobra.Command { + cmd.RunE = func(cmd *cobra.Command, args []string) error { + a.assertion(cmd, args, cfgs...) + return nil + } + return a.Application.SetupRootCommand(cmd, cfgs...) +} + +/* +// TODO: WISHLIST: +1. Helper to wire up a test fixture as the only config file that will be found +2. Set env vars by passing map[string]string (currently possible by caller in test; a helper here would be nice.) +*/ diff --git a/go.mod b/go.mod index 5573334..f7c694c 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.18 require ( github.com/anchore/fangs v0.0.0-20230807173929-13c94c86f47e github.com/anchore/go-logger v0.0.0-20230725134548-c21dafa1ec5a + github.com/google/go-cmp v0.5.9 github.com/gookit/color v1.5.4 github.com/iancoleman/strcase v0.3.0 github.com/pborman/indent v1.2.1 diff --git a/go.sum b/go.sum index fb295f9..12fe4b8 100644 --- a/go.sum +++ b/go.sum @@ -109,6 +109,7 @@ github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=