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 }