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(&param, "param", "", "String parameter.")
   295  
   296  	beta := FlagOption{Name: "beta", Description: "The new option, which is still beta."}
   297  	beta.Flags().StringVar(&param, "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  }