option_test.go 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470
  1. package cli
  2. import (
  3. "flag"
  4. "os"
  5. "reflect"
  6. "testing"
  7. "time"
  8. "github.com/stretchr/testify/require"
  9. )
  10. type nestedStruct struct {
  11. Float float64
  12. }
  13. func TestOptionParserParse(t *testing.T) {
  14. tests := []struct {
  15. scenario string
  16. args []string
  17. env map[string]string
  18. options interface{}
  19. expectedOptions interface{}
  20. err bool
  21. parseFlagsErr bool
  22. }{
  23. {
  24. scenario: "parsing a non-pointer option returns an error",
  25. options: struct{}{},
  26. err: true,
  27. },
  28. {
  29. scenario: "parsing a non-struct pointer option returns an error",
  30. options: new(int),
  31. err: true,
  32. },
  33. {
  34. scenario: "parsing empty options succeed",
  35. options: &struct{}{},
  36. expectedOptions: struct{}{},
  37. },
  38. {
  39. scenario: "parsing options succeed",
  40. options: &struct {
  41. Int int
  42. String string
  43. }{
  44. Int: 42,
  45. String: "foo",
  46. },
  47. expectedOptions: struct {
  48. Int int
  49. String string
  50. }{
  51. Int: 42,
  52. String: "foo",
  53. },
  54. },
  55. {
  56. scenario: "parsing options from env succeed",
  57. env: map[string]string{
  58. "INT": "21",
  59. "STRING": "bar",
  60. },
  61. options: &struct {
  62. Int int
  63. String string
  64. }{
  65. Int: 42,
  66. String: "foo",
  67. },
  68. expectedOptions: struct {
  69. Int int
  70. String string
  71. }{
  72. Int: 21,
  73. String: "bar",
  74. },
  75. },
  76. {
  77. scenario: "parsing options from env with tagged name succeed",
  78. env: map[string]string{
  79. "INT": "21",
  80. "TEST_STRING": "bar",
  81. },
  82. options: &struct {
  83. Int int
  84. String string `env:"TEST_STRING"`
  85. }{
  86. Int: 42,
  87. String: "foo",
  88. },
  89. expectedOptions: struct {
  90. Int int
  91. String string `env:"TEST_STRING"`
  92. }{
  93. Int: 21,
  94. String: "bar",
  95. },
  96. },
  97. {
  98. scenario: "ignore env",
  99. env: map[string]string{
  100. "-": "21",
  101. "STRING": "bar",
  102. },
  103. options: &struct {
  104. Int int `env:"-"`
  105. String string
  106. }{
  107. Int: 42,
  108. String: "foo",
  109. },
  110. expectedOptions: struct {
  111. Int int `env:"-"`
  112. String string
  113. }{
  114. Int: 42,
  115. String: "bar",
  116. },
  117. },
  118. {
  119. scenario: "parsing options from args succeed",
  120. args: []string{
  121. "-int", "84",
  122. "-string", "boo",
  123. },
  124. options: &struct {
  125. Int int
  126. String string
  127. }{
  128. Int: 42,
  129. String: "foo",
  130. },
  131. expectedOptions: struct {
  132. Int int
  133. String string
  134. }{
  135. Int: 84,
  136. String: "boo",
  137. },
  138. },
  139. {
  140. scenario: "parsing options from args with tagged name succeed",
  141. args: []string{
  142. "-i", "21",
  143. "-string", "boo",
  144. },
  145. options: &struct {
  146. Int int `cli:"i"`
  147. String string
  148. }{
  149. Int: 42,
  150. String: "foo",
  151. },
  152. expectedOptions: struct {
  153. Int int `cli:"i"`
  154. String string
  155. }{
  156. Int: 21,
  157. String: "boo",
  158. },
  159. },
  160. {
  161. scenario: "nonexported args are ignored",
  162. args: []string{
  163. "-int", "84",
  164. },
  165. options: &struct {
  166. Int int
  167. str string
  168. }{
  169. Int: 42,
  170. },
  171. expectedOptions: struct {
  172. Int int
  173. str string
  174. }{
  175. Int: 84,
  176. },
  177. },
  178. {
  179. scenario: "args take priority over env variables",
  180. args: []string{
  181. "-int", "84",
  182. "-string", "boo",
  183. },
  184. options: &struct {
  185. Int int
  186. String string
  187. }{
  188. Int: 42,
  189. String: "foo",
  190. },
  191. expectedOptions: struct {
  192. Int int
  193. String string
  194. }{
  195. Int: 84,
  196. String: "boo",
  197. },
  198. },
  199. {
  200. scenario: "parsing options with bool values succeed",
  201. args: []string{
  202. "-f",
  203. },
  204. options: &struct {
  205. Force bool `cli:"f"`
  206. Verbose bool `cli:"v"`
  207. }{},
  208. expectedOptions: struct {
  209. Force bool `cli:"f"`
  210. Verbose bool `cli:"v"`
  211. }{
  212. Force: true,
  213. },
  214. },
  215. {
  216. scenario: "parsing options with nested struct succeed",
  217. args: []string{
  218. "-int", "21",
  219. "-struct", `{"Float":49.3}`,
  220. },
  221. options: &struct {
  222. Int int
  223. Struct nestedStruct
  224. }{},
  225. expectedOptions: struct {
  226. Int int
  227. Struct nestedStruct
  228. }{
  229. Int: 21,
  230. Struct: nestedStruct{
  231. Float: 49.3,
  232. },
  233. },
  234. },
  235. {
  236. scenario: "parsing options with defined nested struct fields succeed",
  237. args: []string{
  238. "-int", "21",
  239. "-struct.float", "49.3",
  240. },
  241. options: &struct {
  242. Int int
  243. Struct nestedStruct
  244. }{},
  245. expectedOptions: struct {
  246. Int int
  247. Struct nestedStruct
  248. }{
  249. Int: 21,
  250. Struct: nestedStruct{
  251. Float: 49.3,
  252. },
  253. },
  254. },
  255. // {
  256. // scenario: "parsing options with invalid nested json returns an error",
  257. // args: []string{
  258. // "-int", "21",
  259. // "-struct.float", "{}",
  260. // },
  261. // options: &struct {
  262. // Int int
  263. // Struct nestedStruct
  264. // }{},
  265. // parseFlagsErr: true,
  266. // },
  267. {
  268. scenario: "parsing options with a slice field succeed",
  269. args: []string{
  270. "-slice", `["foo","bar"]`,
  271. },
  272. options: &struct {
  273. Slice []string
  274. }{},
  275. expectedOptions: struct {
  276. Slice []string
  277. }{
  278. Slice: []string{"foo", "bar"},
  279. },
  280. },
  281. {
  282. scenario: "parsing options with duration fields succeed",
  283. args: []string{
  284. "-duration", "42",
  285. },
  286. options: &struct {
  287. Duration time.Duration
  288. }{},
  289. expectedOptions: struct {
  290. Duration time.Duration
  291. }{
  292. Duration: 42,
  293. },
  294. },
  295. {
  296. scenario: "parsing options with duration fields and litteral format succeed",
  297. args: []string{
  298. "-duration", "42s",
  299. },
  300. options: &struct {
  301. Duration time.Duration
  302. }{},
  303. expectedOptions: struct {
  304. Duration time.Duration
  305. }{
  306. Duration: 42000000000,
  307. },
  308. },
  309. {
  310. scenario: "parsing options with invalid duration fields return an error",
  311. args: []string{
  312. "-duration", "4klm41238rub+_8u498qurfvn",
  313. },
  314. options: &struct {
  315. Duration time.Duration
  316. }{},
  317. parseFlagsErr: true,
  318. },
  319. {
  320. scenario: "parsing options with time fields succeed",
  321. args: []string{
  322. "-time", "1986-02-14T00:00:00Z",
  323. },
  324. options: &struct {
  325. Time time.Time
  326. }{},
  327. expectedOptions: struct {
  328. Time time.Time
  329. }{
  330. Time: time.Date(1986, 2, 14, 0, 0, 0, 0, time.UTC),
  331. },
  332. },
  333. }
  334. for _, test := range tests {
  335. t.Run(test.scenario, func(t *testing.T) {
  336. flags := flag.NewFlagSet("test", flag.ContinueOnError)
  337. flags.SetOutput(writerNoop{})
  338. p := optionParser{
  339. flags: flags,
  340. }
  341. for k, v := range test.env {
  342. os.Setenv(k, v)
  343. defer os.Unsetenv(k)
  344. }
  345. _, err := p.parse(test.options)
  346. if test.err {
  347. require.Error(t, err)
  348. t.Log("error:", err)
  349. return
  350. }
  351. require.NoError(t, err)
  352. if test.parseFlagsErr {
  353. require.Panics(t, func() {
  354. err = p.flags.Parse(test.args)
  355. })
  356. t.Log("error:", err)
  357. return
  358. }
  359. err = p.flags.Parse(test.args)
  360. require.NoError(t, err)
  361. options := reflect.ValueOf(test.options).Elem().Interface()
  362. require.Equal(t, test.expectedOptions, options)
  363. })
  364. }
  365. }
  366. func TestNormalizeOptionName(t *testing.T) {
  367. tests := []struct {
  368. scenario string
  369. baseName string
  370. expectedName string
  371. }{
  372. {
  373. scenario: "name with camel case",
  374. baseName: "HelloWorld",
  375. expectedName: "hello-world",
  376. },
  377. {
  378. scenario: "name with number",
  379. baseName: "HelloWorld42",
  380. expectedName: "hello-world42",
  381. },
  382. {
  383. scenario: "name with dash",
  384. baseName: "hello-world",
  385. expectedName: "hello-world",
  386. },
  387. {
  388. scenario: "name with dash and upper case",
  389. baseName: "hello-World",
  390. expectedName: "hello-world",
  391. },
  392. {
  393. scenario: "name with dash at the end",
  394. baseName: "hello-World-",
  395. expectedName: "hello-world",
  396. },
  397. {
  398. scenario: "name with dash at the start",
  399. baseName: "-hello-World",
  400. expectedName: "hello-world",
  401. },
  402. {
  403. scenario: "name with underscore",
  404. baseName: "hello_World",
  405. expectedName: "hello-world",
  406. },
  407. {
  408. scenario: "name with space",
  409. baseName: "hello World",
  410. expectedName: "hello-world",
  411. },
  412. {
  413. scenario: "name with tab",
  414. baseName: "hello\tWorld",
  415. expectedName: "hello-world",
  416. },
  417. {
  418. scenario: "name with dot",
  419. baseName: "hello.World",
  420. expectedName: "hello-world",
  421. },
  422. {
  423. scenario: "name consecutive upper case letter",
  424. baseName: "helloWORLD",
  425. expectedName: "hello-world",
  426. },
  427. {
  428. scenario: "name consecutive upper case letter and dash",
  429. baseName: "helloW-ORLD",
  430. expectedName: "hello-w-orld",
  431. },
  432. }
  433. for _, test := range tests {
  434. t.Run(test.scenario, func(t *testing.T) {
  435. name := normalizeOptionName(test.baseName, "-")
  436. require.Equal(t, test.expectedName, name)
  437. })
  438. }
  439. }
  440. func TestNormalizeCLIOptionName(t *testing.T) {
  441. name := normalizeCLIOptionName("hello world- i great_lolYeah")
  442. require.Equal(t, "hello-world-i-great-lol-yeah", name)
  443. }
  444. func TestNormalizeEnvOptionName(t *testing.T) {
  445. name := normalizeEnvOptionName("hello world- i great_lolYeah")
  446. require.Equal(t, "HELLO_WORLD_I_GREAT_LOL_YEAH", name)
  447. }
  448. func BenchmarkNormalizeOptionName(b *testing.B) {
  449. s := "hello world- i great_lolYeahPORIGON\tbwa"
  450. for n := 0; n < b.N; n++ {
  451. normalizeOptionName(s, "-")
  452. }
  453. }