...

Source file src/edge-infra.dev/pkg/lib/cli/rags/rag_set_test.go

Documentation: edge-infra.dev/pkg/lib/cli/rags

     1  package rags
     2  
     3  import (
     4  	"bytes"
     5  	"flag"
     6  	"fmt"
     7  	"strconv"
     8  	"strings"
     9  	"testing"
    10  
    11  	"github.com/stretchr/testify/assert"
    12  )
    13  
    14  type tType struct {
    15  	person string
    16  	age    int
    17  }
    18  
    19  func (t *tType) Set(s string) error {
    20  	v := strings.Split(s, ":")
    21  	age, err := strconv.Atoi(v[1])
    22  	if err != nil {
    23  		return err
    24  	}
    25  	*t = tType{
    26  		person: v[0],
    27  		age:    age,
    28  	}
    29  	return nil
    30  }
    31  
    32  func (t *tType) String() string {
    33  	if t.person == "" || t.age == 0 {
    34  		return ""
    35  	}
    36  	return fmt.Sprintf("%s is %d years old", t.person, t.age)
    37  }
    38  func (t *tType) Get() any { return *t }
    39  
    40  // tTypedValue is a test implementation of the TypedValue interface for the test type tType
    41  type tTypedValue struct{ *tType }
    42  
    43  func (t *tTypedValue) Get() any     { return *t.tType }
    44  func (t *tTypedValue) Type() string { return "tvalue" }
    45  
    46  // Verify that Rags get applied to underlying FlagSet correctly
    47  func TestRagSet_FlagSet_Binding(t *testing.T) {
    48  	t.Parallel()
    49  
    50  	flags := []*Rag{
    51  		{
    52  			Name:  "help",
    53  			Short: "h",
    54  			Usage: "display help information for a command",
    55  			Value: &Bool{},
    56  		},
    57  		{
    58  			Name:  "log-level",
    59  			Short: "v",
    60  			Usage: "control verbosity. a higher number means chattier logs",
    61  			Value: &Bool{},
    62  		},
    63  		{
    64  			Name:  "log-json",
    65  			Usage: "emit json logs",
    66  			Value: &Bool{},
    67  		},
    68  		{
    69  			Name:  "foo",
    70  			Usage: "i foo the bar. see --bar",
    71  			Value: &String{},
    72  		},
    73  		{
    74  			Name:  "bar",
    75  			Usage: "dont see me",
    76  			Value: &Int{},
    77  		},
    78  		{
    79  			Name:  "def-value",
    80  			Short: "d",
    81  			Value: NewValueDefault(new(string), "foo"),
    82  		},
    83  		{
    84  			Name:  "def-value-ptr",
    85  			Value: NewValueDefault(new(int), 100),
    86  		},
    87  		{
    88  			Name:  "custom-value",
    89  			Value: &tType{},
    90  		},
    91  	}
    92  
    93  	testFlagSetBinding := func(t *testing.T, rs *RagSet, flags ...*Rag) {
    94  		t.Helper()
    95  
    96  		fs := rs.FlagSet()
    97  
    98  		var expGoFlags = []string{}
    99  
   100  		for _, f := range flags {
   101  			for _, n := range f.Names() {
   102  				expGoFlags = append(expGoFlags, n)
   103  
   104  				gof := fs.Lookup(n)
   105  				assert.NotNil(t, gof)
   106  
   107  				fval := f.Value
   108  				assert.Same(t, fval, gof.Value)
   109  				assert.Implements(t, (*flag.Getter)(nil), gof.Value)
   110  				getter, _ := gof.Value.(flag.Getter)
   111  				assert.Equal(t, fval.Get(), getter.Get())
   112  			}
   113  		}
   114  
   115  		actualCount := 0
   116  		fs.VisitAll(func(f *flag.Flag) {
   117  			actualCount++
   118  			found := false
   119  			for _, name := range expGoFlags {
   120  				if name == f.Name {
   121  					found = true
   122  					break
   123  				}
   124  			}
   125  			assert.True(t, found, "%s wasnt in expected flags %s", f.Name, expGoFlags)
   126  		})
   127  		assert.Equal(t, len(expGoFlags), actualCount)
   128  	}
   129  
   130  	t.Run("New_Add_Flags", func(t *testing.T) {
   131  		rs := New("test-add-on-create", flag.ContinueOnError, flags...)
   132  		assert.Len(t, rs.rags, 8)
   133  		testFlagSetBinding(t, rs, flags...)
   134  	})
   135  
   136  	t.Run("Add", func(t *testing.T) {
   137  		rs := New("test-add", flag.ContinueOnError)
   138  		rs.Add(flags...)
   139  		assert.Len(t, rs.rags, 8)
   140  		testFlagSetBinding(t, rs, flags...)
   141  	})
   142  }
   143  
   144  func TestRagUsageLine(t *testing.T) {
   145  	var (
   146  		valDst      = &tType{person: "tom", age: 100}
   147  		typedValDst = &tTypedValue{&tType{person: "betty", age: 24}}
   148  	)
   149  
   150  	tcs := map[string]struct {
   151  		flag *Rag
   152  		exp  string
   153  	}{
   154  		"categorized bool": {
   155  			&Rag{
   156  				Name:     "flag-name",
   157  				Category: "special bools",
   158  				Usage:    "this is my special bool flag, note the category",
   159  				Value:    &Bool{},
   160  			},
   161  			"\t   \t--flag-name\tthis is my special bool flag, note the category\t",
   162  		},
   163  		"categorized required bool": {
   164  			&Rag{
   165  				Name:     "flag-name",
   166  				Category: "special bools",
   167  				Required: true,
   168  				Usage:    "this is my special bool flag, note the category",
   169  				Value:    &Bool{},
   170  			},
   171  			"\t   \t--flag-name\t[required] this is my special bool flag, note the category\t",
   172  		},
   173  		"categorized required bool with shorthand": {
   174  			&Rag{
   175  				Name:     "flag-name",
   176  				Category: "special bools",
   177  				Required: true,
   178  				Short:    "f",
   179  				Usage:    "this is my special bool flag, note the category",
   180  				Value:    &Bool{},
   181  			},
   182  			"\t-f,\t--flag-name\t[required] this is my special bool flag, note the category\t",
   183  		},
   184  		"default true bool": {
   185  			&Rag{
   186  				Name:     "flag-name",
   187  				Category: "special bools",
   188  				Required: true,
   189  				Usage:    "this is my special bool flag, note the category",
   190  				Value:    &Bool{Default: true},
   191  			},
   192  			"\t   \t--flag-name\t[required] this is my special bool flag, note the category [default: true]\t",
   193  		},
   194  		"categorized required var": {
   195  			// Imitate what RagSet.Var does
   196  			&Rag{
   197  				Name:     "flag-name",
   198  				Category: "special vars",
   199  				Required: true,
   200  				Usage:    "special var flag with category",
   201  				Value:    valDst,
   202  			},
   203  			"\t   \t--flag-name value\t[required] special var flag with category [default: tom is 100 years old]\t",
   204  		},
   205  		"categorized typed var with shorthand": {
   206  			// Imitate what RagSet.Var does
   207  			&Rag{
   208  				Name:     "flag-name",
   209  				Category: "special vars",
   210  				Usage:    "special var flag with category",
   211  				Value:    typedValDst,
   212  				Short:    "f",
   213  			},
   214  			"\t-f,\t--flag-name tvalue\tspecial var flag with category [default: betty is 24 years old]\t",
   215  		},
   216  
   217  		// TODO: categorized w shorthand, required w shorthand
   218  		// TODO: string variations, int, duration, var what other kinds? depends on data type mappings
   219  	}
   220  
   221  	for name, tc := range tcs {
   222  		tc := tc
   223  		name := name
   224  		t.Run(name, func(t *testing.T) {
   225  			t.Parallel()
   226  			rs := New(name, flag.ContinueOnError, tc.flag)
   227  			actual := usageLine(tc.flag, rs.FlagSet())
   228  			assert.Equal(t, tc.exp, actual)
   229  		})
   230  	}
   231  }
   232  
   233  func TestHasZeroDefault(t *testing.T) {
   234  	var (
   235  		emptyVar  = &tTypedValue{new(tType)}
   236  		customVar = &tTypedValue{&tType{person: "tom", age: 100}}
   237  	)
   238  
   239  	tcs := map[string]struct {
   240  		v   flag.Getter
   241  		exp bool
   242  	}{
   243  		"empty string": {&String{}, true},
   244  		"string":       {&String{Default: "foo"}, false},
   245  		"uint 0":       {&Uint{}, true},
   246  		"uint":         {&Uint{Default: 10}, false},
   247  		"uint8 0":      {&Uint8{}, true},
   248  		"uint8":        {&Uint8{Default: 10}, false},
   249  		"uint16 0":     {&Uint16{}, true},
   250  		"uint16":       {&Uint16{Default: 10}, false},
   251  		"uint32 0":     {&Uint32{}, true},
   252  		"uint32":       {&Uint32{Default: 10}, false},
   253  		"uint64 0":     {&Uint64{}, true},
   254  		"uint64":       {&Uint64{Default: 10}, false},
   255  		"int 0":        {&Int{}, true},
   256  		"int":          {&Int{Default: 10}, false},
   257  		"int8 0":       {&Int8{}, true},
   258  		"int8":         {&Int8{Default: 10}, false},
   259  		"int16 0":      {&Int16{}, true},
   260  		"int16":        {&Int16{Default: 10}, false},
   261  		"int32 0":      {&Int32{}, true},
   262  		"int32":        {&Int32{Default: 10}, false},
   263  		"int64 0":      {&Int64{}, true},
   264  		"int64":        {&Int64{Default: 10}, false},
   265  		"float32 0":    {&Float32{}, true},
   266  		"float32":      {&Float32{Default: 10}, false},
   267  		"float64 0":    {&Float64{}, true},
   268  		"float64":      {&Float64{Default: 10}, false},
   269  		"complex64 0":  {&Complex64{}, true},
   270  		"complex64":    {&Complex64{Default: 10}, false},
   271  		"complex128 0": {&Complex128{}, true},
   272  		"complex128":   {&Complex128{Default: complex(3, -5)}, false},
   273  		"var empty":    {emptyVar, true},
   274  		"var":          {customVar, false},
   275  	}
   276  
   277  	for name, tc := range tcs {
   278  		t.Run(name, func(t *testing.T) {
   279  			rs := New(name, flag.ContinueOnError)
   280  			r := &Rag{Name: strings.ReplaceAll(name, " ", "-"), Value: tc.v}
   281  			assert.NotPanics(t, func() { rs.Add(r) })
   282  			f := rs.FlagSet().Lookup(r.Name)
   283  			assert.Equal(t, tc.exp, hasZeroValueDefault(f))
   284  		})
   285  	}
   286  }
   287  
   288  func TestPanicsWithBigShort(t *testing.T) {
   289  	var (
   290  		bigShort string
   291  	)
   292  	lsRag := &Rag{
   293  		Name:     "big-short",
   294  		Value:    &String{Var: &bigShort},
   295  		Short:    "bs",
   296  		Required: true,
   297  	}
   298  
   299  	assert.Panics(t, func() { New("bigShort Set", flag.ContinueOnError, lsRag) })
   300  }
   301  
   302  func TestUnquoteUsage(t *testing.T) {
   303  	var (
   304  		strDst     string
   305  		int64Dst   int64
   306  		intDst     int
   307  		float64Dst float64
   308  		boolDst    bool
   309  		untypedDst = &tType{}
   310  		typedDst   = &tTypedValue{new(tType)}
   311  	)
   312  
   313  	tcs := map[string]struct {
   314  		usage    string
   315  		val      flag.Getter
   316  		expName  string
   317  		expUsage string
   318  	}{
   319  		"backticked name": {
   320  			"load configuration from `file`",
   321  			&String{Default: "~/.config/file", Var: &strDst},
   322  			"file", "load configuration from file",
   323  		},
   324  		"int64 is int": {
   325  			"weird verbosity level",
   326  			&Int64{Default: 100, Var: &int64Dst},
   327  			"int", "weird verbosity level",
   328  		},
   329  		"int is int": {
   330  			"weird verbosity level",
   331  			&Int{Default: 100, Var: &intDst},
   332  			"int", "weird verbosity level",
   333  		},
   334  		"float64 is float": {
   335  			"weird verbosity level",
   336  			&Float64{Default: 100, Var: &float64Dst},
   337  			"float", "weird verbosity level",
   338  		},
   339  		"bool is empty": {
   340  			"weird verbosity toggle",
   341  			&Bool{Var: &boolDst},
   342  			"", "weird verbosity toggle",
   343  		},
   344  		"backtick takes precedence": {
   345  			"weird verbosity `level`",
   346  			&Int64{Default: 100, Var: &int64Dst},
   347  			"level", "weird verbosity level",
   348  		},
   349  		"implements TypedValue": {
   350  			"weird verbosity level",
   351  			typedDst,
   352  			"tvalue", "weird verbosity level",
   353  		},
   354  		"fallback to value": {
   355  			"weird verbosity level",
   356  			untypedDst,
   357  			"value", "weird verbosity level",
   358  		},
   359  		"single backtick is skipped": {
   360  			"weird verbosity `level",
   361  			&Int64{Default: 100, Var: &int64Dst},
   362  			"int", "weird verbosity `level",
   363  		},
   364  	}
   365  
   366  	for name, tc := range tcs {
   367  		t.Run(name, func(t *testing.T) {
   368  			f := &flag.Flag{Usage: tc.usage, Value: tc.val}
   369  			varname, usage := UnquoteUsage(f)
   370  			assert.Equal(t, tc.expName, varname)
   371  			assert.Equal(t, tc.expUsage, usage)
   372  		})
   373  	}
   374  }
   375  
   376  func TestUsage(t *testing.T) {
   377  	tcs := map[string]struct {
   378  		rs  func() *RagSet
   379  		exp string
   380  	}{
   381  		"simple": {
   382  			func() *RagSet {
   383  				rs := New("simple", flag.ContinueOnError)
   384  				rs.Bool("force", false, "force operation in event of conflict")
   385  				rs.String("target", "", "action target")
   386  				rs.String("config", "", "config `file` to load", WithShort("c"))
   387  				return rs
   388  			},
   389  			`Flags:
   390        --force         force operation in event of conflict 
   391        --target string action target                        
   392    -c, --config file   config file to load                  
   393  `,
   394  		},
   395  		"categories": {
   396  			func() *RagSet {
   397  				rs := New("simple", flag.ContinueOnError)
   398  				rs.Bool("force", false, "force operation in event of conflict")
   399  				rs.String("target", "", "action target")
   400  				rs.String("config", "", "config `file` to load", WithShort("c"))
   401  				rs.Int("channel", 0, "slack channel ID to notify", WithCategory("slack"))
   402  				rs.Bool("notify", false, "send a slack notification",
   403  					WithShort("n"), WithCategory("slack"))
   404  				return rs
   405  			},
   406  			`Flags:
   407        --force         force operation in event of conflict 
   408        --target string action target                        
   409    -c, --config file   config file to load                  
   410  
   411  Slack Flags:
   412        --channel int slack channel ID to notify 
   413    -n, --notify      send a slack notification  
   414  `,
   415  		},
   416  		"only categories": {
   417  			func() *RagSet {
   418  				rs := New("simple", flag.ContinueOnError)
   419  				rs.Bool("force", false, "force operation in event of conflict", WithCategory("action"))
   420  				rs.String("target", "", "action target", WithCategory("action"))
   421  				rs.String("config", "", "config `file` to load", WithShort("c"), WithCategory("action"))
   422  				rs.Int("channel", 0, "slack channel ID to notify", WithCategory("slack"))
   423  				rs.Bool("notify", false, "send a slack notification",
   424  					WithShort("n"), WithCategory("slack"))
   425  				return rs
   426  			},
   427  			`Action Flags:
   428        --force         force operation in event of conflict 
   429        --target string action target                        
   430    -c, --config file   config file to load                  
   431  
   432  Slack Flags:
   433        --channel int slack channel ID to notify 
   434    -n, --notify      send a slack notification  
   435  `,
   436  		},
   437  		"categories without shorts": {
   438  			func() *RagSet {
   439  				rs := New("simple", flag.ContinueOnError)
   440  				rs.Bool("force", false, "force operation in event of conflict")
   441  				rs.String("target", "", "action target")
   442  				rs.String("config", "", "config `file` to load", WithShort("c"))
   443  				rs.Int("channel", 0, "slack channel ID to notify", WithCategory("slack"))
   444  				rs.Bool("notify", false, "send a slack notification", WithCategory("slack"))
   445  				return rs
   446  			},
   447  			`Flags:
   448        --force         force operation in event of conflict 
   449        --target string action target                        
   450    -c, --config file   config file to load                  
   451  
   452  Slack Flags:
   453        --channel int slack channel ID to notify 
   454        --notify      send a slack notification  
   455  `,
   456  		},
   457  	}
   458  
   459  	for name, tc := range tcs {
   460  		name := name
   461  		tc := tc
   462  		t.Run(name, func(t *testing.T) {
   463  			t.Parallel()
   464  			// Run each test case multiple times to ensure output does not
   465  			// shift.
   466  			for i := 0; i < 5; i++ {
   467  				b := new(bytes.Buffer)
   468  				rs := tc.rs()
   469  				rs.SetOutput(b)
   470  				rs.Usage()
   471  				assert.Equal(t, tc.exp, b.String())
   472  			}
   473  		})
   474  	}
   475  }
   476  

View as plain text