github.com/mitranim/gg@v0.1.17/flag_test.go (about)

     1  package gg_test
     2  
     3  import (
     4  	r "reflect"
     5  	"testing"
     6  
     7  	"github.com/mitranim/gg"
     8  	"github.com/mitranim/gg/gtest"
     9  )
    10  
    11  type Flags struct {
    12  	Args   []string   `flag:""`
    13  	Str    string     `flag:"-s"`
    14  	Strs   []string   `flag:"-ss"`
    15  	Bool   bool       `flag:"-b"`
    16  	Bools  []bool     `flag:"-bs"`
    17  	Num    float64    `flag:"-n"`
    18  	Nums   []float64  `flag:"-ns"`
    19  	Parser StrsParser `flag:"-p"`
    20  }
    21  
    22  type FlagsWithInit struct {
    23  	Args   []string   `flag:""`
    24  	Str    string     `flag:"-s"  init:"one"`
    25  	Strs   []string   `flag:"-ss" init:"two"`
    26  	Bool   bool       `flag:"-b"  init:"true"`
    27  	Bools  []bool     `flag:"-bs" init:"true"`
    28  	Num    float64    `flag:"-n"  init:"12.34"`
    29  	Nums   []float64  `flag:"-ns" init:"56.78"`
    30  	Parser StrsParser `flag:"-p"  init:"three"`
    31  }
    32  
    33  type FlagsWithDesc struct {
    34  	Args   []string   `flag:""`
    35  	Str    string     `flag:"-s"  desc:"Str flag"`
    36  	Strs   []string   `flag:"-ss" desc:"Strs flag"`
    37  	Bool   bool       `flag:"-b"  desc:"Bool flag"`
    38  	Bools  []bool     `flag:"-bs" desc:"Bools flag"`
    39  	Num    float64    `flag:"-n"  desc:"Num flag"`
    40  	Nums   []float64  `flag:"-ns" desc:"Nums flag"`
    41  	Parser StrsParser `flag:"-p"  desc:"Parser flag"`
    42  }
    43  
    44  type FlagsPart struct {
    45  	Args   []string   `flag:""`
    46  	Str    string     `flag:"-s"  init:"one"   desc:"Str flag"   `
    47  	Strs   []string   `flag:"-ss"              desc:"Strs flag"  `
    48  	Bool   bool       `flag:"-b"  init:"true"  desc:"Bool flag"  `
    49  	Bools  []bool     `flag:"-bs"                                `
    50  	Num    float64    `flag:"-n"  init:"12.34" desc:"Num flag"   `
    51  	Nums   []float64  `flag:"-ns" init:"56.78"                   `
    52  	Parser StrsParser `flag:"-p"               desc:"Parser flag"`
    53  }
    54  
    55  type FlagsFull struct {
    56  	Args   []string   `flag:""`
    57  	Str    string     `flag:"-s"  init:"one"   desc:"Str flag"`
    58  	Strs   []string   `flag:"-ss" init:"two"   desc:"Strs flag"`
    59  	Bool   bool       `flag:"-b"  init:"true"  desc:"Bool flag"`
    60  	Bools  []bool     `flag:"-bs" init:"true"  desc:"Bools flag"`
    61  	Num    float64    `flag:"-n"  init:"12.34" desc:"Num flag"`
    62  	Nums   []float64  `flag:"-ns" init:"56.78" desc:"Nums flag"`
    63  	Parser StrsParser `flag:"-p"  init:"three" desc:"Parser flag"`
    64  }
    65  
    66  type FlagsWithoutArgs struct {
    67  	Str  string  `flag:"-s"  init:"one"   desc:"Str flag"`
    68  	Bool bool    `flag:"-b"  init:"true"  desc:"Bool flag"`
    69  	Num  float64 `flag:"-n"  init:"12.34" desc:"Num flag"`
    70  }
    71  
    72  var argsMixed = []string{
    73  	`-s=one`,
    74  	`-ss=two`, `-ss=three`, `-ss`, `four`,
    75  	`-b=false`,
    76  	`-bs=true`, `-bs=false`, `-bs=true`,
    77  	`-n=12`,
    78  	`-ns=23`, `-ns=34`, `-ns`, `45`,
    79  	`-p=five`, `-p=six`, `-p`, `seven`,
    80  	`eight`, `-nine`, `--ten`,
    81  }
    82  
    83  var flagsMixed = Flags{
    84  	Str:    `one`,
    85  	Strs:   []string{`two`, `three`, `four`},
    86  	Bool:   false,
    87  	Bools:  []bool{true, false, true},
    88  	Num:    12,
    89  	Nums:   []float64{23, 34, 45},
    90  	Parser: StrsParser{`five`, `six`, `seven`},
    91  	Args:   []string{`eight`, `-nine`, `--ten`},
    92  }
    93  
    94  func TestFlagDef(t *testing.T) {
    95  	defer gtest.Catch(t)
    96  
    97  	typ := gg.Type[FlagsFull]()
    98  	fields := gg.StructDeepPublicFieldCache.Get(typ)
    99  
   100  	gtest.Equal(
   101  		gg.FlagDefCache.Get(typ),
   102  		gg.FlagDef{
   103  			Type:  typ,
   104  			Args:  makeFlagDefField(gg.Head(fields)),
   105  			Flags: gg.Map(gg.Tail(fields), makeFlagDefField),
   106  			Index: map[string]int{
   107  				`-s`:  0,
   108  				`-ss`: 1,
   109  				`-b`:  2,
   110  				`-bs`: 3,
   111  				`-n`:  4,
   112  				`-ns`: 5,
   113  				`-p`:  6,
   114  			},
   115  		},
   116  	)
   117  }
   118  
   119  func TestFlagHelp(t *testing.T) {
   120  	defer gtest.Catch(t)
   121  
   122  	testFlagHelp[SomeModel](gg.Newline)
   123  
   124  	testFlagHelp[Flags](`
   125  flag
   126  ----
   127  -s
   128  -ss
   129  -b
   130  -bs
   131  -n
   132  -ns
   133  -p
   134  `)
   135  
   136  	testFlagHelp[FlagsWithInit](`
   137  flag    init
   138  -------------
   139  -s      one
   140  -ss     two
   141  -b      true
   142  -bs     true
   143  -n      12.34
   144  -ns     56.78
   145  -p      three
   146  `)
   147  
   148  	testFlagHelp[FlagsWithDesc](`
   149  flag    desc
   150  -------------------
   151  -s      Str flag
   152  -ss     Strs flag
   153  -b      Bool flag
   154  -bs     Bools flag
   155  -n      Num flag
   156  -ns     Nums flag
   157  -p      Parser flag
   158  `)
   159  
   160  	testFlagHelp[FlagsPart](`
   161  flag    init     desc
   162  ----------------------------
   163  -s      one      Str flag
   164  -ss              Strs flag
   165  -b      true     Bool flag
   166  -bs
   167  -n      12.34    Num flag
   168  -ns     56.78
   169  -p               Parser flag
   170  `)
   171  
   172  	t.Run(`partial_without_head`, func(t *testing.T) {
   173  		defer gtest.Catch(t)
   174  		defer gg.SnapSwap(&gg.FlagFmtDefault.Head, false).Done()
   175  
   176  		testFlagHelp[FlagsPart](`
   177  -s     one      Str flag
   178  -ss             Strs flag
   179  -b     true     Bool flag
   180  -bs
   181  -n     12.34    Num flag
   182  -ns    56.78
   183  -p              Parser flag
   184  `)
   185  	})
   186  
   187  	testFlagHelp[FlagsFull](`
   188  flag    init     desc
   189  ----------------------------
   190  -s      one      Str flag
   191  -ss     two      Strs flag
   192  -b      true     Bool flag
   193  -bs     true     Bools flag
   194  -n      12.34    Num flag
   195  -ns     56.78    Nums flag
   196  -p      three    Parser flag
   197  `)
   198  
   199  	t.Run(`full_without_head_under`, func(t *testing.T) {
   200  		defer gtest.Catch(t)
   201  		defer gg.SnapSwap(&gg.FlagFmtDefault.HeadUnder, ``).Done()
   202  
   203  		testFlagHelp[FlagsFull](`
   204  flag    init     desc
   205  -s      one      Str flag
   206  -ss     two      Strs flag
   207  -b      true     Bool flag
   208  -bs     true     Bools flag
   209  -n      12.34    Num flag
   210  -ns     56.78    Nums flag
   211  -p      three    Parser flag
   212  `)
   213  	})
   214  
   215  	t.Run(`full_without_head`, func(t *testing.T) {
   216  		defer gtest.Catch(t)
   217  		defer gg.SnapSwap(&gg.FlagFmtDefault.Head, false).Done()
   218  
   219  		testFlagHelp[FlagsFull](`
   220  -s     one      Str flag
   221  -ss    two      Strs flag
   222  -b     true     Bool flag
   223  -bs    true     Bools flag
   224  -n     12.34    Num flag
   225  -ns    56.78    Nums flag
   226  -p     three    Parser flag
   227  `)
   228  	})
   229  }
   230  
   231  func testFlagHelp[A any](exp string) {
   232  	gtest.Eq(
   233  		trimLines(gg.Newline+gg.FlagHelp[A]()),
   234  		trimLines(exp),
   235  	)
   236  }
   237  
   238  func trimLines(src string) string {
   239  	return gg.JoinLines(gg.Map(gg.SplitLines(src), gg.TrimSpaceSuffix[string])...)
   240  }
   241  
   242  func BenchmarkFlagHelp(b *testing.B) {
   243  	for ind := 0; ind < b.N; ind++ {
   244  		gg.Nop1(gg.FlagHelp[FlagsFull]())
   245  	}
   246  }
   247  
   248  func TestFlagParseTo(t *testing.T) {
   249  	defer gtest.Catch(t)
   250  
   251  	t.Run(`unknown`, func(t *testing.T) {
   252  		defer gtest.Catch(t)
   253  
   254  		testFlagUnknown(`-one`)
   255  		testFlagUnknown(`--one`)
   256  
   257  		testFlagUnknown(`-one`, ``)
   258  		testFlagUnknown(`--one`, ``)
   259  
   260  		testFlagUnknown(`-one`, `two`)
   261  		testFlagUnknown(`--one`, `two`)
   262  	})
   263  
   264  	t.Run(`defaults`, func(t *testing.T) {
   265  		defer gtest.Catch(t)
   266  
   267  		gtest.Equal(gg.FlagParseTo[FlagsFull](nil), FlagsFull{
   268  			Str:    `one`,
   269  			Strs:   []string{`two`},
   270  			Bool:   true,
   271  			Bools:  []bool{true},
   272  			Num:    12.34,
   273  			Nums:   []float64{56.78},
   274  			Parser: StrsParser{`three`},
   275  		})
   276  	})
   277  
   278  	t.Run(`just_args`, func(t *testing.T) {
   279  		defer gtest.Catch(t)
   280  
   281  		test := func(src ...string) {
   282  			gtest.Equal(gg.FlagParseTo[Flags](src), Flags{Args: src})
   283  		}
   284  
   285  		test()
   286  		test(`one`)
   287  		test(`one`, `two`)
   288  		test(`one`, `two`, `three`)
   289  		test(`one`, `two`, `three`)
   290  		test(`one`, `--two`)
   291  		test(`one`, `--two`, `three`)
   292  		test(`one`, `--two`, `three`, `--four`)
   293  	})
   294  
   295  	t.Run(`string_scalar`, func(t *testing.T) {
   296  		defer gtest.Catch(t)
   297  
   298  		test := func(src []string, exp string) {
   299  			gtest.Equal(gg.FlagParseTo[Flags](src), Flags{Str: exp})
   300  		}
   301  
   302  		test([]string{`-s`, ``}, ``)
   303  		test([]string{`-s`, `one`}, `one`)
   304  		test([]string{`-s`, `one`, `-s`, `two`}, `two`)
   305  
   306  		test([]string{`-s=`}, ``)
   307  		test([]string{`-s=one`}, `one`)
   308  		test([]string{`-s=one`, `-s=two`}, `two`)
   309  	})
   310  
   311  	t.Run(`string_slice`, func(t *testing.T) {
   312  		defer gtest.Catch(t)
   313  
   314  		type Src = [2][]string
   315  
   316  		test := func(src Src) {
   317  			gtest.Equal(gg.FlagParseTo[Flags](src[0]), Flags{Strs: src[1]})
   318  		}
   319  
   320  		test(Src{{`-ss`, ``}, {``}})
   321  		test(Src{{`-ss`, `one`}, {`one`}})
   322  		test(Src{{`-ss`, `one`, `-ss`, `two`}, {`one`, `two`}})
   323  		test(Src{{`-ss`, `one`, `-ss`, ``, `-ss`, `two`}, {`one`, ``, `two`}})
   324  
   325  		test(Src{{`-ss=`}, {``}})
   326  		test(Src{{`-ss=one`}, {`one`}})
   327  		test(Src{{`-ss=one`, `-ss=two`}, {`one`, `two`}})
   328  		test(Src{{`-ss=one`, `-ss=`, `-ss=two`}, {`one`, ``, `two`}})
   329  	})
   330  
   331  	var boolInvalid = []string{
   332  		` `, `FALSE`, `TRUE`, `0`, `1`, `123`, `false `, `true `, ` false`, ` true`,
   333  	}
   334  
   335  	t.Run(`bool_scalar`, func(t *testing.T) {
   336  		defer gtest.Catch(t)
   337  
   338  		t.Run(`invalid`, func(t *testing.T) {
   339  			defer gtest.Catch(t)
   340  			testFlagInvalids(`-b`, boolInvalid)
   341  		})
   342  
   343  		test := func(src []string, exp bool) {
   344  			gtest.Equal(gg.FlagParseTo[Flags](src).Bool, exp)
   345  		}
   346  
   347  		test([]string{}, false)
   348  		test([]string{`-b`}, true)
   349  
   350  		// Bool flags don't support `-flag value`, only `-flag=value`.
   351  		test([]string{`-b`, ``}, true)
   352  		test([]string{`-b`, `arg`}, true)
   353  		test([]string{`-b`, `true`}, true)
   354  		test([]string{`-b`, `false`}, true)
   355  
   356  		test([]string{`-b=`}, true)
   357  		test([]string{`-b=false`}, false)
   358  		test([]string{`-b=true`}, true)
   359  
   360  		test([]string{`-b`, `-b`}, true)
   361  		test([]string{`-b=false`, `-b`}, true)
   362  		test([]string{`-b=true`, `-b`}, true)
   363  
   364  		test([]string{`-b`, `-b=`}, true)
   365  		test([]string{`-b=false`, `-b=false`}, false)
   366  		test([]string{`-b=true`, `-b=true`}, true)
   367  	})
   368  
   369  	t.Run(`bool_slice`, func(t *testing.T) {
   370  		defer gtest.Catch(t)
   371  
   372  		t.Run(`invalid`, func(t *testing.T) {
   373  			defer gtest.Catch(t)
   374  			testFlagInvalids(`-bs`, boolInvalid)
   375  		})
   376  
   377  		test := func(src []string, exp []bool) {
   378  			gtest.Equal(gg.FlagParseTo[Flags](src).Bools, exp)
   379  		}
   380  
   381  		test([]string{`-bs`}, []bool{true})
   382  		test([]string{`-bs`, `-bs`}, []bool{true, true})
   383  
   384  		test([]string{`-bs=false`, `-bs`}, []bool{false, true})
   385  		test([]string{`-bs=true`, `-bs`}, []bool{true, true})
   386  
   387  		test([]string{`-bs=false`, `-bs=true`}, []bool{false, true})
   388  		test([]string{`-bs=true`, `-bs=false`}, []bool{true, false})
   389  
   390  		test([]string{`-bs`, `-bs=false`}, []bool{true, false})
   391  		test([]string{`-bs`, `-bs=true`}, []bool{true, true})
   392  
   393  		test([]string{`-bs`, ``, `-bs`}, []bool{true})
   394  		test([]string{`-bs`, `arg`, `-bs`}, []bool{true})
   395  
   396  		test([]string{`-bs=false`, ``, `-bs`}, []bool{false})
   397  		test([]string{`-bs=false`, `arg`, `-bs`}, []bool{false})
   398  	})
   399  
   400  	var numInvalid = []string{` `, `false`, `true`, `±1`, ` 1 `, `1a`, `a1`}
   401  
   402  	t.Run(`num_scalar`, func(t *testing.T) {
   403  		defer gtest.Catch(t)
   404  
   405  		t.Run(`invalid`, func(t *testing.T) {
   406  			defer gtest.Catch(t)
   407  
   408  			testFlagMissingValue(`-n`)
   409  			testFlagMissingValue(`-n`, `-0`)
   410  			testFlagMissingValue(`-n`, `-12.34`)
   411  
   412  			testFlagInvalids(`-n`, numInvalid)
   413  		})
   414  
   415  		test := func(src []string, exp float64) {
   416  			gtest.Equal(gg.FlagParseTo[Flags](src), Flags{Num: exp})
   417  		}
   418  
   419  		test([]string{}, 0)
   420  
   421  		test([]string{`-n`, `0`}, 0)
   422  		test([]string{`-n`, `+0`}, 0)
   423  
   424  		test([]string{`-n`, `12.34`}, 12.34)
   425  		test([]string{`-n`, `+12.34`}, 12.34)
   426  
   427  		test([]string{`-n=-0`}, 0)
   428  		test([]string{`-n=0`}, 0)
   429  		test([]string{`-n=+0`}, 0)
   430  
   431  		test([]string{`-n=-12.34`}, -12.34)
   432  		test([]string{`-n=12.34`}, 12.34)
   433  		test([]string{`-n=+12.34`}, 12.34)
   434  
   435  		test([]string{`-n`, `34.56`, `-n`, `12.34`}, 12.34)
   436  		test([]string{`-n`, `34.56`, `-n`, `+12.34`}, 12.34)
   437  
   438  		test([]string{`-n`, `34.56`, `-n=-12.34`}, -12.34)
   439  		test([]string{`-n`, `34.56`, `-n=12.34`}, 12.34)
   440  		test([]string{`-n`, `34.56`, `-n=+12.34`}, 12.34)
   441  	})
   442  
   443  	t.Run(`num_slice`, func(t *testing.T) {
   444  		defer gtest.Catch(t)
   445  
   446  		t.Run(`invalid`, func(t *testing.T) {
   447  			defer gtest.Catch(t)
   448  			testFlagMissingValue(`-ns`)
   449  			testFlagInvalids(`-ns`, numInvalid)
   450  		})
   451  
   452  		test := func(src []string, exp []float64) {
   453  			gtest.Equal(gg.FlagParseTo[Flags](src), Flags{Nums: exp})
   454  		}
   455  
   456  		test([]string{`-ns`, `0`}, []float64{0})
   457  		test([]string{`-ns`, `+0`}, []float64{0})
   458  
   459  		test([]string{`-ns`, `12.34`}, []float64{12.34})
   460  		test([]string{`-ns`, `+12.34`}, []float64{12.34})
   461  
   462  		test([]string{`-ns=12.34`}, []float64{12.34})
   463  		test([]string{`-ns=+12.34`}, []float64{12.34})
   464  
   465  		test([]string{`-ns`, `12.34`, `-ns`, `56.78`}, []float64{12.34, 56.78})
   466  		test([]string{`-ns=12.34`, `-ns=56.78`}, []float64{12.34, 56.78})
   467  	})
   468  
   469  	t.Run(`Parser`, func(t *testing.T) {
   470  		defer gtest.Catch(t)
   471  
   472  		type Src []string
   473  		type Out = StrsParser
   474  
   475  		type Tar struct {
   476  			Val Out `flag:"-v"`
   477  		}
   478  
   479  		test := func(src Src, out Out) {
   480  			gtest.Equal(gg.FlagParseTo[Tar](src), Tar{out})
   481  		}
   482  
   483  		test(nil, nil)
   484  
   485  		test(Src{`-v`, ``}, Out{``})
   486  		test(Src{`-v=`}, Out{``})
   487  
   488  		test(Src{`-v`, `10`}, Out{`10`})
   489  		test(Src{`-v=10`}, Out{`10`})
   490  
   491  		test(Src{`-v`, `10`, `-v`, `20`}, Out{`10`, `20`})
   492  		test(Src{`-v=10`, `-v=20`}, Out{`10`, `20`})
   493  	})
   494  
   495  	t.Run(`flag.Value`, func(t *testing.T) {
   496  		defer gtest.Catch(t)
   497  
   498  		type Src []string
   499  		type Out = IntsValue
   500  
   501  		type Tar struct {
   502  			Val Out `flag:"-v"`
   503  		}
   504  
   505  		test := func(src Src, out Out) {
   506  			gtest.Equal(gg.FlagParseTo[Tar](src), Tar{out})
   507  		}
   508  
   509  		test(nil, nil)
   510  
   511  		test(Src{`-v`, `10`}, Out{10})
   512  		test(Src{`-v=10`}, Out{10})
   513  
   514  		test(Src{`-v`, `10`, `-v`, `20`}, Out{10, 20})
   515  		test(Src{`-v=10`, `-v=20`}, Out{10, 20})
   516  	})
   517  
   518  	t.Run(`[]flag.Value`, func(t *testing.T) {
   519  		defer gtest.Catch(t)
   520  
   521  		type Src []string
   522  		type Out = []IntValue
   523  
   524  		type Tar struct {
   525  			Val Out `flag:"-v"`
   526  		}
   527  
   528  		test := func(src Src, out Out) {
   529  			gtest.Equal(gg.FlagParseTo[Tar](src), Tar{out})
   530  		}
   531  
   532  		test(nil, nil)
   533  
   534  		test(Src{`-v`, `10`}, Out{{10}})
   535  		test(Src{`-v=10`}, Out{{10}})
   536  
   537  		test(Src{`-v`, `10`, `-v`, `20`}, Out{{10}, {20}})
   538  		test(Src{`-v=10`, `-v=20`}, Out{{10}, {20}})
   539  	})
   540  
   541  	t.Run(`without_args`, func(t *testing.T) {
   542  		defer gtest.Catch(t)
   543  
   544  		type Tar = FlagsWithoutArgs
   545  
   546  		parse := func(src ...string) Tar { return gg.FlagParseTo[Tar](src) }
   547  
   548  		gtest.Equal(
   549  			parse(),
   550  			Tar{Str: `one`, Bool: true, Num: 12.34},
   551  		)
   552  
   553  		gtest.Equal(
   554  			parse(`-s=two`, `-b=false`, `-n=23.45`),
   555  			Tar{Str: `two`, Num: 23.45},
   556  		)
   557  
   558  		gtest.PanicStr(`unexpected non-flag args: ["arg"]`, func() {
   559  			parse(`arg`)
   560  		})
   561  
   562  		gtest.PanicStr(`unexpected non-flag args: ["arg0" "arg1"]`, func() {
   563  			parse(`arg0`, `arg1`)
   564  		})
   565  
   566  		gtest.PanicStr(`unexpected non-flag args: ["arg0" "arg1"]`, func() {
   567  			parse(`-s=two`, `-b=false`, `-n=23.45`, `arg0`, `arg1`)
   568  		})
   569  	})
   570  
   571  	t.Run(`mixed`, func(t *testing.T) {
   572  		defer gtest.Catch(t)
   573  
   574  		gtest.Equal(gg.FlagParseTo[Flags](argsMixed), flagsMixed)
   575  	})
   576  }
   577  
   578  func BenchmarkFlagParseTo_empty(b *testing.B) {
   579  	for ind := 0; ind < b.N; ind++ {
   580  		gg.Nop1(gg.FlagParseTo[Flags](nil))
   581  	}
   582  }
   583  
   584  // Kinda slow (microseconds) but not anyone's bottleneck.
   585  func BenchmarkFlagParseTo_full(b *testing.B) {
   586  	for ind := 0; ind < b.N; ind++ {
   587  		gg.Nop1(gg.FlagParseTo[Flags](argsMixed))
   588  	}
   589  }
   590  
   591  func testFlagInvalid(src ...string) {
   592  	gtest.PanicStr(`unable to decode`, func() {
   593  		gg.FlagParseTo[Flags](src)
   594  	})
   595  }
   596  
   597  func testFlagInvalids(flag string, src []string) {
   598  	for _, val := range src {
   599  		testFlagInvalid(flag + `=` + val)
   600  	}
   601  }
   602  
   603  func testFlagUnknown(src ...string) {
   604  	gtest.PanicStr(`unable to find flag`, func() {
   605  		gg.FlagParseTo[Flags](src)
   606  	})
   607  }
   608  
   609  func testFlagMissingValue(src ...string) {
   610  	gtest.PanicStr(`missing value for trailing flag`, func() {
   611  		gg.FlagParseTo[Flags](src)
   612  	})
   613  }
   614  
   615  func makeFlagDefField(src r.StructField) (out gg.FlagDefField) {
   616  	out.Set(src)
   617  	return
   618  }