go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/common/flag/multiflag/multiflag_test.go (about) 1 // Copyright 2016 The LUCI Authors. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package multiflag 16 17 import ( 18 "bytes" 19 "errors" 20 "flag" 21 "fmt" 22 "os" 23 "sort" 24 "testing" 25 26 . "github.com/smartystreets/goconvey/convey" 27 ) 28 29 type testMultiFlag struct { 30 MultiFlag 31 32 S string 33 I int 34 B bool 35 } 36 37 func (of *testMultiFlag) newOption(name, description string) Option { 38 o := &FlagOption{Name: name, Description: description} 39 40 flags := o.Flags() 41 flags.StringVar(&of.S, "string-var", "", "A string variable.") 42 flags.IntVar(&of.I, "int-var", 123, "An integer variable.") 43 flags.BoolVar(&of.B, "bool-var", false, "A boolean variable.") 44 45 // Set our option name. 46 return o 47 } 48 49 // A Writer implementation that controllably fails. 50 type failWriter struct { 51 Max int // When failing, never "write" more than this many bytes. 52 Error error // The error to return when used to write. 53 } 54 55 // Implements io.Writer. 56 func (w *failWriter) Write(data []byte) (int, error) { 57 size := len(data) 58 if w.Error != nil && size > w.Max { 59 size = w.Max 60 } 61 return size, w.Error 62 } 63 64 func TestParsing(t *testing.T) { 65 Convey("Given empty MultiFlag", t, func() { 66 of := &testMultiFlag{} 67 68 Convey("Its option list should be empty", func() { 69 So(of.OptionNames(), ShouldBeEmpty) 70 }) 71 72 Convey(`Parsing an option spec with an empty option value should fail.`, func() { 73 So(of.Parse(`,params`), ShouldNotBeNil) 74 }) 75 }) 76 77 Convey("Given Flags with two option values", t, func() { 78 of := &testMultiFlag{} 79 of.Options = []Option{ 80 of.newOption("foo", "Test option 'foo'."), 81 of.newOption("bar", "Test option 'bar'."), 82 } 83 84 Convey("Its option list should be: ['foo', 'bar'].", func() { 85 So(of.OptionNames(), ShouldResemble, []string{"foo", "bar"}) 86 }) 87 88 Convey(`When parsing 'foo,string-var="hello world"'`, func() { 89 err := of.Parse(`foo,string-var="hello world",bool-var=true`) 90 So(err, ShouldBeNil) 91 92 Convey("The option, 'foo', should be selected.", func() { 93 So(of.Selected, ShouldNotBeNil) 94 So(of.Selected.Descriptor().Name, ShouldEqual, "foo") 95 }) 96 97 Convey(`The value of 'string-var' should be, "hello world".`, func() { 98 So(of.S, ShouldEqual, "hello world") 99 }) 100 101 Convey(`The value of 'int-var' should be default (123)".`, func() { 102 So(of.I, ShouldEqual, 123) 103 }) 104 105 Convey(`The value of 'bool-var' should be "true".`, func() { 106 So(of.B, ShouldEqual, true) 107 }) 108 }) 109 }) 110 } 111 112 func TestHelp(t *testing.T) { 113 Convey(`A 'MultiFlag' instance`, t, func() { 114 of := &MultiFlag{} 115 116 Convey(`Uses os.Stderr for output.`, func() { 117 So(of.GetOutput(), ShouldEqual, os.Stderr) 118 }) 119 120 Convey(`Configured with a simple FlagOption with flags`, func() { 121 opt := &FlagOption{ 122 Name: "foo", 123 Description: "An option, 'foo'.", 124 } 125 opt.Flags().String("bar", "", "An option, 'bar'.") 126 of.Options = []Option{opt} 127 128 Convey(`Should successfully parse "foo" with no flags.`, func() { 129 So(of.Parse(`foo`), ShouldBeNil) 130 }) 131 132 Convey(`Should successfully parse "foo" with a "bar" flag.`, func() { 133 So(of.Parse(`foo,bar="Hello!"`), ShouldBeNil) 134 }) 135 136 Convey(`Should fail to parse a non-existent flag.`, func() { 137 So(of.Parse(`foo,baz`), ShouldNotBeNil) 138 }) 139 }) 140 }) 141 142 Convey(`Given a testMultiFlag configured with 'nil' output.`, t, func() { 143 of := &testMultiFlag{} 144 145 Convey(`Should default to os.Stderr`, func() { 146 So(of.GetOutput(), ShouldEqual, os.Stderr) 147 }) 148 }) 149 150 Convey("Given Flags with two options, one of which is 'help'", t, func() { 151 var buf bytes.Buffer 152 of := &testMultiFlag{} 153 of.Output = &buf 154 of.Options = []Option{ 155 HelpOption(&of.MultiFlag), 156 of.newOption("foo", "Test option 'foo'."), 157 } 158 159 correctHelpString := ` 160 help Displays this help message. Can be run as "help,<option>" to display help for an option. 161 foo Test option 'foo'. 162 ` 163 164 Convey("Should print a correct help string", func() { 165 err := of.PrintHelp() 166 So(err, ShouldBeNil) 167 168 So(buf.String(), ShouldEqual, correctHelpString) 169 }) 170 171 Convey(`Should fail to print a help string when the writer fails.`, func() { 172 w := &failWriter{} 173 w.Error = errors.New("fail") 174 of.Output = w 175 So(of.PrintHelp(), ShouldNotBeNil) 176 }) 177 178 Convey(`Should parse a request for a specific option's help string.`, func() { 179 err := of.Parse(`help,foo`) 180 So(err, ShouldBeNil) 181 182 Convey(`And should print help for that option.`, func() { 183 correctOptionHelpString := `Help for 'foo': Test option 'foo'. 184 -bool-var 185 A boolean variable. 186 -int-var int 187 An integer variable. (default 123) 188 -string-var string 189 A string variable. 190 ` 191 So(buf.String(), ShouldEqual, correctOptionHelpString) 192 }) 193 }) 194 195 Convey("Should parse the 'help' option", func() { 196 err := of.Parse(`help`) 197 So(err, ShouldBeNil) 198 199 Convey("Should print the correct help string in response.", func() { 200 So(buf.String(), ShouldEqual, correctHelpString) 201 }) 202 }) 203 204 Convey("Should parse 'help,junk=1'", func() { 205 err := of.Parse(`help,junk=1`) 206 So(err, ShouldBeNil) 207 208 Convey(`Should notify the user that "junk=1" is not an option.`, func() { 209 So(buf.String(), ShouldEqual, "Unknown option 'junk=1'\n") 210 }) 211 }) 212 }) 213 } 214 215 func TestHelpItemSlice(t *testing.T) { 216 Convey(`Given a slice of testOption instances`, t, func() { 217 options := optionDescriptorSlice{ 218 &OptionDescriptor{ 219 Name: "a", 220 Description: "An unpinned help item", 221 Pinned: false, 222 }, 223 &OptionDescriptor{ 224 Name: "b", 225 Description: "Another unpinned help item", 226 Pinned: false, 227 }, 228 &OptionDescriptor{ 229 Name: "c", 230 Description: "A pinned help item", 231 Pinned: true, 232 }, 233 &OptionDescriptor{ 234 Name: "d", 235 Description: "Another pinned help item", 236 Pinned: true, 237 }, 238 } 239 sort.Sort(options) 240 241 Convey(`The options should be sorted: c, b, a, b`, func() { 242 var names []string 243 for _, opt := range options { 244 names = append(names, opt.Name) 245 } 246 So(names, ShouldResemble, []string{"c", "d", "a", "b"}) 247 }) 248 }) 249 } 250 251 func TestFlagParse(t *testing.T) { 252 Convey("Given a MultiFlag with one option, 'foo'", t, func() { 253 of := &testMultiFlag{} 254 of.Options = []Option{ 255 of.newOption("foo", "Test option 'foo'."), 256 } 257 258 Convey("When configured as an output to a Go flag.MultiFlag", func() { 259 gof := &flag.FlagSet{} 260 gof.Var(of, "option", "Single line option") 261 262 Convey(`Should parse '-option foo,string-var="hello world",int-var=1337'`, func() { 263 err := gof.Parse([]string{"-option", `foo,string-var="hello world",int-var=1337"`}) 264 So(err, ShouldBeNil) 265 266 Convey("Should parse out 'foo' as the option", func() { 267 So(of.Selected.Descriptor().Name, ShouldEqual, "foo") 268 }) 269 270 Convey(`Should parse out 'string-var' as "hello world".`, func() { 271 So(of.S, ShouldEqual, "hello world") 272 }) 273 274 Convey(`Should parse out 'int-var' as '1337'.`, func() { 275 So(of.I, ShouldEqual, 1337) 276 }) 277 }) 278 279 Convey(`When parsing 'bar'`, func() { 280 err := gof.Parse([]string{"-option", "bar"}) 281 So(err, ShouldNotBeNil) 282 }) 283 }) 284 }) 285 } 286 287 func ExampleMultiFlag() { 288 // Setup multiflag. 289 param := "" 290 deprecated := FlagOption{ 291 Name: "deprecated", 292 Description: "The deprecated option.", 293 } 294 deprecated.Flags().StringVar(¶m, "param", "", "String parameter.") 295 296 beta := FlagOption{Name: "beta", Description: "The new option, which is still beta."} 297 beta.Flags().StringVar(¶m, "param", "", "Beta string parameter.") 298 299 mf := MultiFlag{ 300 Description: "My test MultiFlag.", 301 Output: os.Stdout, 302 } 303 mf.Options = []Option{ 304 HelpOption(&mf), 305 &deprecated, 306 &beta, 307 } 308 309 // Install the multiflag as a flag in "flags". 310 fs := flag.NewFlagSet("test", flag.ContinueOnError) 311 fs.Var(&mf, "multiflag", "Multiflag option.") 312 313 // Parse flags (help). 314 cmd := []string{"-multiflag", "help"} 315 fmt.Println("Selecting help option:", cmd) 316 if err := fs.Parse(cmd); err != nil { 317 panic(err) 318 } 319 320 // Parse flags (param). 321 cmd = []string{"-multiflag", "beta,param=Sup"} 322 fmt.Println("Selecting beta option:", cmd) 323 if err := fs.Parse(cmd); err != nil { 324 panic(err) 325 } 326 fmt.Printf("Option [%s], parameter: [%s].\n", mf.Selected.Descriptor().Name, param) 327 328 // Output: 329 // Selecting help option: [-multiflag help] 330 // My test MultiFlag. 331 // help Displays this help message. Can be run as "help,<option>" to display help for an option. 332 // beta The new option, which is still beta. 333 // deprecated The deprecated option. 334 // Selecting beta option: [-multiflag beta,param=Sup] 335 // Option [beta], parameter: [Sup]. 336 }