github.com/michaellihs/golab@v0.1.0-beta3.0.20180726222757-f5cdabc76dfd/cmd/mapper/flag_mapper_test.go (about) 1 // Copyright © 2017 Michael Lihs 2 // 3 // Permission is hereby granted, free of charge, to any person obtaining a copy 4 // of this software and associated documentation files (the "Software"), to deal 5 // in the Software without restriction, including without limitation the rights 6 // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 // copies of the Software, and to permit persons to whom the Software is 8 // furnished to do so, subject to the following conditions: 9 // 10 // The above copyright notice and this permission notice shall be included in 11 // all copies or substantial portions of the Software. 12 // 13 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 // THE SOFTWARE. 20 21 package mapper 22 23 import ( 24 "bytes" 25 "io" 26 "os" 27 "reflect" 28 "strings" 29 30 "time" 31 32 . "github.com/onsi/ginkgo" 33 . "github.com/onsi/gomega" 34 . "github.com/spf13/cobra" 35 "github.com/xanzy/go-gitlab" 36 ) 37 38 func mockCmd() *Command { 39 mock := &Command{ 40 Use: "mock", 41 Short: "mock", 42 Long: `mock`, 43 RunE: func(cmd *Command, args []string) error { 44 return nil 45 }, 46 } 47 return mock 48 } 49 50 var _ = Describe("FlagMapper", func() { 51 52 type testFlags struct { 53 Flag1 *bool `flag_name:"flag1" short:"f" type:"bool" required:"yes" description:"first flag"` 54 Flag2 *string `flag_name:"flag2" type:"string" required:"no" description:"second flag"` 55 Flag3 *int `flag_name:"flag3" type:"string" required:"no" description:"third flag"` 56 Flag4 *[]string `flag_name:"flag4" type:"[]string" required:"no" description:"fourth flag"` 57 Flag5 []int `flag_name:"flag5" type:"[]int" required:"no" description:"fifth flag"` 58 } 59 60 type testOpts struct { 61 Flag1 *bool 62 Flag2 *string 63 Flag3 *int 64 Flag4 *[]string 65 Flag5 []int 66 } 67 68 type testOptsNonMatching struct { 69 Flag1 *bool 70 Flag2 *string 71 Flag3 *int 72 Flag4 *string // non matching with flags 73 } 74 75 type testFlagsWithTransformation struct { 76 Flag1 *string `flag_name:"visibility" type:"bool" required:"no" description:"first flag" transform:"string2visibility"` 77 } 78 79 type optsRequireTransformation struct { 80 Flag1 *gitlab.VisibilityValue 81 } 82 83 type testFlagsWithLabelsTransformation struct { 84 Labels *string `flag_name:"labels" type:"string" required:"no" description:"labels" transform:"string2Labels"` 85 } 86 87 type optsRequireLabelsTransformation struct { 88 Labels gitlab.Labels 89 } 90 91 type testFlagsWithPropertyNotInOpts struct { 92 Id *string `flag_name:"id" short:"i" type:"string" required:"yes" description:"id"` 93 Name *string `flag_name:"name" short:"n" type:"string" required:"yes" description:"name"` 94 } 95 96 type testOptsWithMissingProperty struct { 97 Name *string `flag_name:"name" short:"n" type:"string" required:"yes" description:"name"` 98 } 99 100 It("provides a constructor that takes a cobra command as parameter", func() { 101 mockCmd := mockCmd() 102 var flagMapper = New(mockCmd) 103 Expect(reflect.TypeOf(flagMapper).String()).To(Equal("mapper.FlagMapper")) 104 }) 105 106 It("sets expected flags on command", func() { 107 flags := &testFlags{} 108 109 cmd := mockCmd() 110 var mapper = New(cmd) 111 mapper.SetFlags(flags) 112 113 Expect(cmd.Flag("flag1")).NotTo(BeNil()) 114 Expect(cmd.Flag("flag1").Name).To(Equal("flag1")) 115 Expect(cmd.Flag("flag1").Usage).To(Equal("(required) first flag")) 116 Expect(cmd.Flag("flag1").Shorthand).To(Equal("f")) 117 118 Expect(cmd.Flag("flag2")).NotTo(BeNil()) 119 Expect(cmd.Flag("flag2").Name).To(Equal("flag2")) 120 Expect(cmd.Flag("flag2").Usage).To(Equal("(optional) second flag")) 121 Expect(cmd.Flag("flag2").Shorthand).To(Equal("")) 122 123 Expect(cmd.Flag("flag3")).NotTo(BeNil()) 124 Expect(cmd.Flag("flag3").Name).To(Equal("flag3")) 125 Expect(cmd.Flag("flag3").Usage).To(Equal("(optional) third flag")) 126 Expect(cmd.Flag("flag3").Shorthand).To(Equal("")) 127 128 Expect(cmd.Flag("flag4")).NotTo(BeNil()) 129 Expect(cmd.Flag("flag4").Name).To(Equal("flag4")) 130 Expect(cmd.Flag("flag4").Usage).To(Equal("(optional) fourth flag")) 131 Expect(cmd.Flag("flag4").Shorthand).To(Equal("")) 132 133 Expect(cmd.Flag("flag5")).NotTo(BeNil()) 134 Expect(cmd.Flag("flag5").Name).To(Equal("flag5")) 135 Expect(cmd.Flag("flag5").Usage).To(Equal("(optional) fifth flag")) 136 Expect(cmd.Flag("flag5").Shorthand).To(Equal("")) 137 }) 138 139 It("sets no flags if given flags are nil", func() { 140 mockCmd := mockCmd() 141 mapper := New(mockCmd) 142 mapper.SetFlags(nil) 143 }) 144 145 It("maps flags and opts as expected with AutoMap", func() { 146 mockCmd := mockCmd() 147 var mapper = InitializedMapper(mockCmd, &testFlags{}, &testOpts{}) 148 149 executeCommand(mockCmd, "mock", "--flag1", "true", "--flag2", "string", "--flag3", "4", "--flag4", "v1, v2, v3", "--flag5", "1,2,3,4") 150 _, _, err := mapper.AutoMap() 151 flags := mapper.MappedFlags().(*testFlags) 152 opts := mapper.MappedOpts().(*testOpts) 153 154 Expect(err).To(BeNil()) 155 Expect(*flags.Flag1).To(Equal(true)) 156 Expect(*opts.Flag1).To(Equal(true)) 157 }) 158 159 It("maps valid args to given opts struct as expected", func() { 160 flags := &testFlags{} 161 opts := &testOpts{} 162 cmd := mockCmd() 163 var mapper = New(cmd) 164 mapper.SetFlags(flags) 165 166 executeCommand(cmd, "mock", "--flag1", "true", "--flag2", "string", "--flag3", "4", "--flag4", "v1, v2, v3", "--flag5", "1,2,3,4") 167 mapper.Map(flags, opts) 168 169 Expect(*opts.Flag1).To(Equal(true)) 170 Expect(*opts.Flag2).To(Equal("string")) 171 Expect(*opts.Flag3).To(Equal(4)) 172 // TODO there seems to be bug in cobra, when parsing array flags 173 Expect(*opts.Flag4).Should(ConsistOf("v1, v2, v3")) 174 Expect(opts.Flag5).Should(ConsistOf(1, 2, 3, 4)) 175 }) 176 177 It("maps args to given flags struct as expected", func() { 178 flags := &testFlags{} 179 opts := &testOpts{} 180 mockCmd := mockCmd() 181 var flagMapper = New(mockCmd) 182 flagMapper.SetFlags(flags) 183 184 executeCommand(mockCmd, "mock", "--flag1", "false", "--flag2", "string", "--flag3", "4", "--flag4", "v1, v2, v3") 185 flagMapper.Map(flags, opts) 186 187 Expect(*flags.Flag1).To(Equal(true)) 188 Expect(*flags.Flag2).To(Equal("string")) 189 Expect(*flags.Flag3).To(Equal(4)) 190 Expect(*flags.Flag4).Should(ConsistOf("v1, v2, v3")) 191 }) 192 193 It("maps nil flags as expected", func() { 194 cmd := mockCmd() 195 mapper := InitializedMapper(cmd, nil, nil) 196 opts, flags, err := mapper.AutoMap() 197 Expect(opts).To(BeNil()) 198 Expect(flags).To(BeNil()) 199 Expect(err).To(BeNil()) 200 }) 201 202 It("skips args with non-matching types as expected", func() { 203 flags := &testFlags{} 204 opts := &testOptsNonMatching{} 205 mockCmd := mockCmd() 206 var flagMapper = New(mockCmd) 207 flagMapper.SetFlags(flags) 208 209 executeCommand(mockCmd, "mock", "--flag1", "true", "--flag2", "string", "--flag3", "4", "--flag4", "v1, v2, v3") 210 flagMapper.Map(flags, opts) 211 212 Expect(opts.Flag4).To(BeNil()) 213 }) 214 215 It("calls a transform function as expected", func() { 216 flags := &testFlagsWithTransformation{} 217 opts := &optsRequireTransformation{} 218 cmd := mockCmd() 219 var flagMapper = InitializedMapper(cmd, flags, opts) 220 221 executeCommand(cmd, "mock", "--visibility", "private") 222 flagMapper.AutoMap() 223 224 Expect(*opts.Flag1).To(Equal(*gitlab.Visibility(gitlab.PrivateVisibility))) 225 }) 226 227 It("transforms string to gitlab.Labels as expected", func() { 228 flags := &testFlagsWithLabelsTransformation{} 229 opts := &optsRequireLabelsTransformation{} 230 cmd := mockCmd() 231 var mapper = InitializedMapper(cmd, flags, opts) 232 233 executeCommand(cmd, "mock", "--labels", "label1,label2,label3") 234 mapper.AutoMap() 235 236 Expect(opts.Labels).Should(ConsistOf("label1", "label2", "label3")) 237 }) 238 239 It("transforms string to time.Time value as expected", func() { 240 type str2timeValFlags struct { 241 Time *string `flag_name:"time" type:"string" required:"no" description:"time" transform:"string2TimeVal"` 242 } 243 type str2timeValOpts struct { 244 Time time.Time 245 } 246 flags := &str2timeValFlags{} 247 opts := &str2timeValOpts{} 248 cmd := mockCmd() 249 var mapper = InitializedMapper(cmd, flags, opts) 250 251 executeCommand(cmd, "mock", "--time", "2017-12-13") 252 mapper.AutoMap() 253 254 Expect(opts.Time).NotTo(BeNil()) 255 Expect(opts.Time.Day()).To(Equal(13)) 256 Expect(opts.Time.Month()).To(Equal(time.Month(12))) 257 Expect(opts.Time.Year()).To(Equal(2017)) 258 }) 259 260 It("transforms string to ISOTime as expected", func() { 261 type str2ISOTime struct { 262 Time *string `flag_name:"time" type:"string" required:"no" description:"time" transform:"string2IsoTime"` 263 } 264 type str2ISTOTime struct { 265 Time *gitlab.ISOTime 266 } 267 flags := &str2ISOTime{} 268 opts := &str2ISTOTime{} 269 cmd := mockCmd() 270 var mapper = InitializedMapper(cmd, flags, opts) 271 272 executeCommand(cmd, "mock", "--time", "2017-12-13") 273 mapper.AutoMap() 274 275 Expect(opts.Time).NotTo(BeNil()) 276 s, err := opts.Time.MarshalJSON() 277 Expect(err).To(BeNil()) 278 Expect(string(s)).To(Equal(`"2017-12-13"`)) 279 }) 280 281 It("transforms JSON to commit actions as expected", func() { 282 type json2CommitActionsFlags struct { 283 Actions *string `flag_name:"actions" transform:"json2CommitActions" type:"array" required:"yes" description:"A JSON encoded array of action hashes to commit as a batch."` 284 } 285 type json2CommitActionsOpts struct { 286 Actions []*gitlab.CommitAction 287 } 288 json := `[ 289 { 290 "action": "create", 291 "file_path": "foo/bar", 292 "content": "some content" 293 }, 294 { 295 "action": "delete", 296 "file_path": "foo/bar2" 297 }, 298 { 299 "action": "move", 300 "file_path": "foo/bar3", 301 "previous_path": "foo/bar4", 302 "content": "some content" 303 }, 304 { 305 "action": "update", 306 "file_path": "foo/bar5", 307 "content": "new content" 308 } 309 ]` 310 flags := &json2CommitActionsFlags{} 311 opts := &json2CommitActionsOpts{} 312 cmd := mockCmd() 313 var mapper = InitializedMapper(cmd, flags, opts) 314 315 executeCommand(cmd, "mock", "--actions", json) 316 mapper.AutoMap() 317 318 Expect(len(opts.Actions)).To(Equal(4)) 319 }) 320 321 It("silently ignores properties in flags that are not available in opts", func() { 322 flags := &testFlagsWithPropertyNotInOpts{} 323 opts := &testOptsWithMissingProperty{} 324 mockCmd := mockCmd() 325 var mapper = New(mockCmd) 326 mapper.SetFlags(flags) 327 328 executeCommand(mockCmd, "mock", "-i", "34", "-n", "name") 329 mapper.Map(flags, opts) 330 331 Expect(*opts.Name).To(Equal("name")) 332 }) 333 334 It("can handle nil opts", func() { 335 flags := &testFlagsWithPropertyNotInOpts{} 336 mockCmd := mockCmd() 337 var mapper = New(mockCmd) 338 mapper.SetFlags(flags) 339 340 executeCommand(mockCmd, "mock", "-i", "34", "-n", "name") 341 mapper.Map(flags, nil) 342 343 Expect(*flags.Name).To(Equal("name")) 344 }) 345 346 It("returns an error during mapping, if required flag is not set in mapping", func() { 347 flags := &testFlags{} 348 mockCmd := mockCmd() 349 var mapper = New(mockCmd) 350 mapper.SetFlags(flags) 351 352 executeCommand(mockCmd, "mock", "-n", "name") 353 err := mapper.Map(flags, nil) 354 355 Expect(err).NotTo(BeNil()) 356 Expect(err.Error()).To(Equal("required flag --flag1 was empty")) 357 }) 358 359 It("returns an error during mapping, if required flag is not set in auto-mapping", func() { 360 flags := &testFlags{} 361 mockCmd := mockCmd() 362 var mapper = InitializedMapper(mockCmd, flags, nil) 363 364 executeCommand(mockCmd, "mock", "-n", "name") 365 _, _, err := mapper.AutoMap() 366 367 Expect(err).NotTo(BeNil()) 368 Expect(err.Error()).To(Equal("required flag --flag1 was empty")) 369 }) 370 371 }) 372 373 // TODO put the following methods into a testhelper 374 375 // see https://github.com/spf13/cobra/blob/master/command_test.go for basic implementation of this method 376 func executeCommand(root *Command, args ...string) (stdout string, output string, err error) { 377 stdout, output, err = executeCommandC(root, args...) 378 return strings.TrimRight(stdout, "\n"), output, err 379 } 380 381 // see https://github.com/spf13/cobra/blob/master/command_test.go for basic implementation of this method 382 func executeCommandC(root *Command, args ...string) (stdout string, output string, err error) { 383 buf := new(bytes.Buffer) 384 root.SetOutput(buf) // this only redirects stderr, not stdout! 385 root.SetArgs(args) 386 387 // for capturing stdout, see https://stackoverflow.com/questions/10473800/in-go-how-do-i-capture-stdout-of-a-function-into-a-string 388 old := os.Stdout // keep backup of the real stdout 389 r, w, _ := os.Pipe() 390 os.Stdout = w 391 392 _, err = root.ExecuteC() 393 394 outC := make(chan string) 395 // copy the output in a separate goroutine so printing can't block indefinitely 396 go func() { 397 var buf bytes.Buffer 398 io.Copy(&buf, r) 399 outC <- buf.String() 400 }() 401 402 // back to normal state 403 w.Close() 404 os.Stdout = old // restoring the real stdout 405 stdout = <-outC 406 407 return stdout, buf.String(), err 408 }