package rags import ( "bytes" "flag" "fmt" "strconv" "strings" "testing" "github.com/stretchr/testify/assert" ) type tType struct { person string age int } func (t *tType) Set(s string) error { v := strings.Split(s, ":") age, err := strconv.Atoi(v[1]) if err != nil { return err } *t = tType{ person: v[0], age: age, } return nil } func (t *tType) String() string { if t.person == "" || t.age == 0 { return "" } return fmt.Sprintf("%s is %d years old", t.person, t.age) } func (t *tType) Get() any { return *t } // tTypedValue is a test implementation of the TypedValue interface for the test type tType type tTypedValue struct{ *tType } func (t *tTypedValue) Get() any { return *t.tType } func (t *tTypedValue) Type() string { return "tvalue" } // Verify that Rags get applied to underlying FlagSet correctly func TestRagSet_FlagSet_Binding(t *testing.T) { t.Parallel() flags := []*Rag{ { Name: "help", Short: "h", Usage: "display help information for a command", Value: &Bool{}, }, { Name: "log-level", Short: "v", Usage: "control verbosity. a higher number means chattier logs", Value: &Bool{}, }, { Name: "log-json", Usage: "emit json logs", Value: &Bool{}, }, { Name: "foo", Usage: "i foo the bar. see --bar", Value: &String{}, }, { Name: "bar", Usage: "dont see me", Value: &Int{}, }, { Name: "def-value", Short: "d", Value: NewValueDefault(new(string), "foo"), }, { Name: "def-value-ptr", Value: NewValueDefault(new(int), 100), }, { Name: "custom-value", Value: &tType{}, }, } testFlagSetBinding := func(t *testing.T, rs *RagSet, flags ...*Rag) { t.Helper() fs := rs.FlagSet() var expGoFlags = []string{} for _, f := range flags { for _, n := range f.Names() { expGoFlags = append(expGoFlags, n) gof := fs.Lookup(n) assert.NotNil(t, gof) fval := f.Value assert.Same(t, fval, gof.Value) assert.Implements(t, (*flag.Getter)(nil), gof.Value) getter, _ := gof.Value.(flag.Getter) assert.Equal(t, fval.Get(), getter.Get()) } } actualCount := 0 fs.VisitAll(func(f *flag.Flag) { actualCount++ found := false for _, name := range expGoFlags { if name == f.Name { found = true break } } assert.True(t, found, "%s wasnt in expected flags %s", f.Name, expGoFlags) }) assert.Equal(t, len(expGoFlags), actualCount) } t.Run("New_Add_Flags", func(t *testing.T) { rs := New("test-add-on-create", flag.ContinueOnError, flags...) assert.Len(t, rs.rags, 8) testFlagSetBinding(t, rs, flags...) }) t.Run("Add", func(t *testing.T) { rs := New("test-add", flag.ContinueOnError) rs.Add(flags...) assert.Len(t, rs.rags, 8) testFlagSetBinding(t, rs, flags...) }) } func TestRagUsageLine(t *testing.T) { var ( valDst = &tType{person: "tom", age: 100} typedValDst = &tTypedValue{&tType{person: "betty", age: 24}} ) tcs := map[string]struct { flag *Rag exp string }{ "categorized bool": { &Rag{ Name: "flag-name", Category: "special bools", Usage: "this is my special bool flag, note the category", Value: &Bool{}, }, "\t \t--flag-name\tthis is my special bool flag, note the category\t", }, "categorized required bool": { &Rag{ Name: "flag-name", Category: "special bools", Required: true, Usage: "this is my special bool flag, note the category", Value: &Bool{}, }, "\t \t--flag-name\t[required] this is my special bool flag, note the category\t", }, "categorized required bool with shorthand": { &Rag{ Name: "flag-name", Category: "special bools", Required: true, Short: "f", Usage: "this is my special bool flag, note the category", Value: &Bool{}, }, "\t-f,\t--flag-name\t[required] this is my special bool flag, note the category\t", }, "default true bool": { &Rag{ Name: "flag-name", Category: "special bools", Required: true, Usage: "this is my special bool flag, note the category", Value: &Bool{Default: true}, }, "\t \t--flag-name\t[required] this is my special bool flag, note the category [default: true]\t", }, "categorized required var": { // Imitate what RagSet.Var does &Rag{ Name: "flag-name", Category: "special vars", Required: true, Usage: "special var flag with category", Value: valDst, }, "\t \t--flag-name value\t[required] special var flag with category [default: tom is 100 years old]\t", }, "categorized typed var with shorthand": { // Imitate what RagSet.Var does &Rag{ Name: "flag-name", Category: "special vars", Usage: "special var flag with category", Value: typedValDst, Short: "f", }, "\t-f,\t--flag-name tvalue\tspecial var flag with category [default: betty is 24 years old]\t", }, // TODO: categorized w shorthand, required w shorthand // TODO: string variations, int, duration, var what other kinds? depends on data type mappings } for name, tc := range tcs { tc := tc name := name t.Run(name, func(t *testing.T) { t.Parallel() rs := New(name, flag.ContinueOnError, tc.flag) actual := usageLine(tc.flag, rs.FlagSet()) assert.Equal(t, tc.exp, actual) }) } } func TestHasZeroDefault(t *testing.T) { var ( emptyVar = &tTypedValue{new(tType)} customVar = &tTypedValue{&tType{person: "tom", age: 100}} ) tcs := map[string]struct { v flag.Getter exp bool }{ "empty string": {&String{}, true}, "string": {&String{Default: "foo"}, false}, "uint 0": {&Uint{}, true}, "uint": {&Uint{Default: 10}, false}, "uint8 0": {&Uint8{}, true}, "uint8": {&Uint8{Default: 10}, false}, "uint16 0": {&Uint16{}, true}, "uint16": {&Uint16{Default: 10}, false}, "uint32 0": {&Uint32{}, true}, "uint32": {&Uint32{Default: 10}, false}, "uint64 0": {&Uint64{}, true}, "uint64": {&Uint64{Default: 10}, false}, "int 0": {&Int{}, true}, "int": {&Int{Default: 10}, false}, "int8 0": {&Int8{}, true}, "int8": {&Int8{Default: 10}, false}, "int16 0": {&Int16{}, true}, "int16": {&Int16{Default: 10}, false}, "int32 0": {&Int32{}, true}, "int32": {&Int32{Default: 10}, false}, "int64 0": {&Int64{}, true}, "int64": {&Int64{Default: 10}, false}, "float32 0": {&Float32{}, true}, "float32": {&Float32{Default: 10}, false}, "float64 0": {&Float64{}, true}, "float64": {&Float64{Default: 10}, false}, "complex64 0": {&Complex64{}, true}, "complex64": {&Complex64{Default: 10}, false}, "complex128 0": {&Complex128{}, true}, "complex128": {&Complex128{Default: complex(3, -5)}, false}, "var empty": {emptyVar, true}, "var": {customVar, false}, } for name, tc := range tcs { t.Run(name, func(t *testing.T) { rs := New(name, flag.ContinueOnError) r := &Rag{Name: strings.ReplaceAll(name, " ", "-"), Value: tc.v} assert.NotPanics(t, func() { rs.Add(r) }) f := rs.FlagSet().Lookup(r.Name) assert.Equal(t, tc.exp, hasZeroValueDefault(f)) }) } } func TestPanicsWithBigShort(t *testing.T) { var ( bigShort string ) lsRag := &Rag{ Name: "big-short", Value: &String{Var: &bigShort}, Short: "bs", Required: true, } assert.Panics(t, func() { New("bigShort Set", flag.ContinueOnError, lsRag) }) } func TestUnquoteUsage(t *testing.T) { var ( strDst string int64Dst int64 intDst int float64Dst float64 boolDst bool untypedDst = &tType{} typedDst = &tTypedValue{new(tType)} ) tcs := map[string]struct { usage string val flag.Getter expName string expUsage string }{ "backticked name": { "load configuration from `file`", &String{Default: "~/.config/file", Var: &strDst}, "file", "load configuration from file", }, "int64 is int": { "weird verbosity level", &Int64{Default: 100, Var: &int64Dst}, "int", "weird verbosity level", }, "int is int": { "weird verbosity level", &Int{Default: 100, Var: &intDst}, "int", "weird verbosity level", }, "float64 is float": { "weird verbosity level", &Float64{Default: 100, Var: &float64Dst}, "float", "weird verbosity level", }, "bool is empty": { "weird verbosity toggle", &Bool{Var: &boolDst}, "", "weird verbosity toggle", }, "backtick takes precedence": { "weird verbosity `level`", &Int64{Default: 100, Var: &int64Dst}, "level", "weird verbosity level", }, "implements TypedValue": { "weird verbosity level", typedDst, "tvalue", "weird verbosity level", }, "fallback to value": { "weird verbosity level", untypedDst, "value", "weird verbosity level", }, "single backtick is skipped": { "weird verbosity `level", &Int64{Default: 100, Var: &int64Dst}, "int", "weird verbosity `level", }, } for name, tc := range tcs { t.Run(name, func(t *testing.T) { f := &flag.Flag{Usage: tc.usage, Value: tc.val} varname, usage := UnquoteUsage(f) assert.Equal(t, tc.expName, varname) assert.Equal(t, tc.expUsage, usage) }) } } func TestUsage(t *testing.T) { tcs := map[string]struct { rs func() *RagSet exp string }{ "simple": { func() *RagSet { rs := New("simple", flag.ContinueOnError) rs.Bool("force", false, "force operation in event of conflict") rs.String("target", "", "action target") rs.String("config", "", "config `file` to load", WithShort("c")) return rs }, `Flags: --force force operation in event of conflict --target string action target -c, --config file config file to load `, }, "categories": { func() *RagSet { rs := New("simple", flag.ContinueOnError) rs.Bool("force", false, "force operation in event of conflict") rs.String("target", "", "action target") rs.String("config", "", "config `file` to load", WithShort("c")) rs.Int("channel", 0, "slack channel ID to notify", WithCategory("slack")) rs.Bool("notify", false, "send a slack notification", WithShort("n"), WithCategory("slack")) return rs }, `Flags: --force force operation in event of conflict --target string action target -c, --config file config file to load Slack Flags: --channel int slack channel ID to notify -n, --notify send a slack notification `, }, "only categories": { func() *RagSet { rs := New("simple", flag.ContinueOnError) rs.Bool("force", false, "force operation in event of conflict", WithCategory("action")) rs.String("target", "", "action target", WithCategory("action")) rs.String("config", "", "config `file` to load", WithShort("c"), WithCategory("action")) rs.Int("channel", 0, "slack channel ID to notify", WithCategory("slack")) rs.Bool("notify", false, "send a slack notification", WithShort("n"), WithCategory("slack")) return rs }, `Action Flags: --force force operation in event of conflict --target string action target -c, --config file config file to load Slack Flags: --channel int slack channel ID to notify -n, --notify send a slack notification `, }, "categories without shorts": { func() *RagSet { rs := New("simple", flag.ContinueOnError) rs.Bool("force", false, "force operation in event of conflict") rs.String("target", "", "action target") rs.String("config", "", "config `file` to load", WithShort("c")) rs.Int("channel", 0, "slack channel ID to notify", WithCategory("slack")) rs.Bool("notify", false, "send a slack notification", WithCategory("slack")) return rs }, `Flags: --force force operation in event of conflict --target string action target -c, --config file config file to load Slack Flags: --channel int slack channel ID to notify --notify send a slack notification `, }, } for name, tc := range tcs { name := name tc := tc t.Run(name, func(t *testing.T) { t.Parallel() // Run each test case multiple times to ensure output does not // shift. for i := 0; i < 5; i++ { b := new(bytes.Buffer) rs := tc.rs() rs.SetOutput(b) rs.Usage() assert.Equal(t, tc.exp, b.String()) } }) } }