src.elv.sh@v0.21.0-dev.0.20240515223629-06979efb9a2a/pkg/getopt/getopt_test.go (about)

     1  package getopt
     2  
     3  import (
     4  	"errors"
     5  	"reflect"
     6  	"testing"
     7  
     8  	"src.elv.sh/pkg/errutil"
     9  )
    10  
    11  var (
    12  	vSpec = &OptionSpec{'v', "verbose", NoArgument}
    13  	nSpec = &OptionSpec{'n', "dry-run", NoArgument}
    14  	fSpec = &OptionSpec{'f', "file", RequiredArgument}
    15  	iSpec = &OptionSpec{'i', "in-place", OptionalArgument}
    16  	specs = []*OptionSpec{vSpec, nSpec, fSpec, iSpec}
    17  )
    18  
    19  var parseTests = []struct {
    20  	name     string
    21  	cfg      Config
    22  	args     []string
    23  	wantOpts []*Option
    24  	wantArgs []string
    25  	wantErr  error
    26  }{
    27  	{
    28  		name:     "short option",
    29  		args:     []string{"-v"},
    30  		wantOpts: []*Option{{Spec: vSpec}},
    31  	},
    32  	{
    33  		name:     "short option with required argument",
    34  		args:     []string{"-fname"},
    35  		wantOpts: []*Option{{Spec: fSpec, Argument: "name"}},
    36  	},
    37  	{
    38  		name:     "short option with required argument in separate argument",
    39  		args:     []string{"-f", "name"},
    40  		wantOpts: []*Option{{Spec: fSpec, Argument: "name"}},
    41  	},
    42  	{
    43  		name:     "short option with optional argument",
    44  		args:     []string{"-i.bak"},
    45  		wantOpts: []*Option{{Spec: iSpec, Argument: ".bak"}},
    46  	},
    47  	{
    48  		name:     "short option with optional argument omitted",
    49  		args:     []string{"-i", ".bak"},
    50  		wantOpts: []*Option{{Spec: iSpec}},
    51  		wantArgs: []string{".bak"},
    52  	},
    53  	{
    54  		name:     "short option chaining",
    55  		args:     []string{"-vn"},
    56  		wantOpts: []*Option{{Spec: vSpec}, {Spec: nSpec}},
    57  	},
    58  	{
    59  		name:     "short option chaining with argument",
    60  		args:     []string{"-vfname"},
    61  		wantOpts: []*Option{{Spec: vSpec}, {Spec: fSpec, Argument: "name"}},
    62  	},
    63  	{
    64  		name:     "short option chaining with argument in separate argument",
    65  		args:     []string{"-vf", "name"},
    66  		wantOpts: []*Option{{Spec: vSpec}, {Spec: fSpec, Argument: "name"}},
    67  	},
    68  
    69  	{
    70  		name:     "long option",
    71  		args:     []string{"--verbose"},
    72  		wantOpts: []*Option{{Spec: vSpec, Long: true}},
    73  	},
    74  	{
    75  		name:     "long option with required argument",
    76  		args:     []string{"--file=name"},
    77  		wantOpts: []*Option{{Spec: fSpec, Long: true, Argument: "name"}},
    78  	},
    79  	{
    80  		name:     "long option with required argument in separate argument",
    81  		args:     []string{"--file", "name"},
    82  		wantOpts: []*Option{{Spec: fSpec, Long: true, Argument: "name"}},
    83  	},
    84  	{
    85  		name:     "long option with optional argument",
    86  		args:     []string{"--in-place=.bak"},
    87  		wantOpts: []*Option{{Spec: iSpec, Long: true, Argument: ".bak"}},
    88  	},
    89  	{
    90  		name:     "long option with optional argument omitted",
    91  		args:     []string{"--in-place", ".bak"},
    92  		wantOpts: []*Option{{Spec: iSpec, Long: true}},
    93  		wantArgs: []string{".bak"},
    94  	},
    95  
    96  	{
    97  		name:     "long option, LongOnly mode",
    98  		args:     []string{"-verbose"},
    99  		cfg:      LongOnly,
   100  		wantOpts: []*Option{{Spec: vSpec, Long: true}},
   101  	},
   102  	{
   103  		name:     "long option with required argument, LongOnly mode",
   104  		args:     []string{"-file", "name"},
   105  		cfg:      LongOnly,
   106  		wantOpts: []*Option{{Spec: fSpec, Long: true, Argument: "name"}},
   107  	},
   108  
   109  	{
   110  		name:     "StopAfterDoubleDash off",
   111  		args:     []string{"-v", "--", "-n"},
   112  		wantOpts: []*Option{{Spec: vSpec}, {Spec: nSpec}},
   113  		wantArgs: []string{"--"},
   114  	},
   115  	{
   116  		name:     "StopAfterDoubleDash on",
   117  		args:     []string{"-v", "--", "-n"},
   118  		cfg:      StopAfterDoubleDash,
   119  		wantOpts: []*Option{{Spec: vSpec}},
   120  		wantArgs: []string{"-n"},
   121  	},
   122  
   123  	{
   124  		name:     "StopBeforeFirstNonOption off",
   125  		args:     []string{"-v", "foo", "-n"},
   126  		wantOpts: []*Option{{Spec: vSpec}, {Spec: nSpec}},
   127  		wantArgs: []string{"foo"},
   128  	},
   129  	{
   130  		name:     "StopBeforeFirstNonOption on",
   131  		args:     []string{"-v", "foo", "-n"},
   132  		cfg:      StopBeforeFirstNonOption,
   133  		wantOpts: []*Option{{Spec: vSpec}},
   134  		wantArgs: []string{"foo", "-n"},
   135  	},
   136  
   137  	{
   138  		name:     "single dash is not an option",
   139  		args:     []string{"-"},
   140  		wantArgs: []string{"-"},
   141  	},
   142  	{
   143  		name:     "single dash is not an option, LongOnly mode",
   144  		args:     []string{"-"},
   145  		cfg:      LongOnly,
   146  		wantArgs: []string{"-"},
   147  	},
   148  
   149  	{
   150  		name:    "short option with required argument missing",
   151  		args:    []string{"-f"},
   152  		wantErr: errors.New("missing argument for -f"),
   153  	},
   154  	{
   155  		name:    "long option with required argument missing",
   156  		args:    []string{"--file"},
   157  		wantErr: errors.New("missing argument for --file"),
   158  	},
   159  	{
   160  		name: "unknown short option",
   161  		args: []string{"-b"},
   162  		wantOpts: []*Option{
   163  			{Spec: &OptionSpec{Short: 'b', Arity: OptionalArgument}, Unknown: true}},
   164  		wantErr: errors.New("unknown option -b"),
   165  	},
   166  	{
   167  		name: "unknown short option with argument",
   168  		args: []string{"-bfoo"},
   169  		wantOpts: []*Option{
   170  			{Spec: &OptionSpec{Short: 'b', Arity: OptionalArgument}, Unknown: true, Argument: "foo"}},
   171  		wantErr: errors.New("unknown option -b"),
   172  	},
   173  	{
   174  		name: "unknown long option",
   175  		args: []string{"--bad"},
   176  		wantOpts: []*Option{
   177  			{Spec: &OptionSpec{Long: "bad", Arity: OptionalArgument}, Long: true, Unknown: true}},
   178  		wantErr: errors.New("unknown option --bad"),
   179  	},
   180  	{
   181  		name: "unknown long option with argument",
   182  		args: []string{"--bad=foo"},
   183  		wantOpts: []*Option{
   184  			{Spec: &OptionSpec{Long: "bad", Arity: OptionalArgument}, Long: true, Unknown: true, Argument: "foo"}},
   185  		wantErr: errors.New("unknown option --bad"),
   186  	},
   187  	{
   188  		name: "multiple errors",
   189  		args: []string{"-b", "-f"},
   190  		wantOpts: []*Option{
   191  			{Spec: &OptionSpec{Short: 'b', Arity: OptionalArgument}, Unknown: true}},
   192  		wantErr: errutil.Multi(
   193  			errors.New("missing argument for -f"), errors.New("unknown option -b")),
   194  	},
   195  }
   196  
   197  func TestParse(t *testing.T) {
   198  	for _, tc := range parseTests {
   199  		t.Run(tc.name, func(t *testing.T) {
   200  			opts, args, err := Parse(tc.args, specs, tc.cfg)
   201  			check := func(name string, got, want any) {
   202  				if !reflect.DeepEqual(got, want) {
   203  					t.Errorf("Parse(%#v) (config = %v)\ngot %s = %v, want %v",
   204  						tc.args, tc.cfg, name, got, want)
   205  				}
   206  			}
   207  			check("opts", opts, tc.wantOpts)
   208  			check("args", args, tc.wantArgs)
   209  			check("err", err, tc.wantErr)
   210  		})
   211  	}
   212  }
   213  
   214  var completeTests = []struct {
   215  	name     string
   216  	cfg      Config
   217  	args     []string
   218  	wantOpts []*Option
   219  	wantArgs []string
   220  	wantCtx  Context
   221  }{
   222  	{
   223  		name:    "NewOptionOrArgument",
   224  		args:    []string{""},
   225  		wantCtx: Context{Type: OptionOrArgument},
   226  	},
   227  	{
   228  		name:    "NewOption",
   229  		args:    []string{"-"},
   230  		wantCtx: Context{Type: AnyOption},
   231  	},
   232  	{
   233  		name:    "LongOption",
   234  		args:    []string{"--f"},
   235  		wantCtx: Context{Type: LongOption, Text: "f"},
   236  	},
   237  	{
   238  		name:    "LongOption with LongOnly",
   239  		args:    []string{"-f"},
   240  		cfg:     LongOnly,
   241  		wantCtx: Context{Type: LongOption, Text: "f"},
   242  	},
   243  	{
   244  		name:     "ChainShortOption",
   245  		args:     []string{"-v"},
   246  		wantOpts: []*Option{{Spec: vSpec}},
   247  		wantCtx:  Context{Type: ChainShortOption},
   248  	},
   249  	{
   250  		name: "OptionArgument of short option, separate argument",
   251  		args: []string{"-f", "foo"},
   252  		wantCtx: Context{
   253  			Type:   OptionArgument,
   254  			Option: &Option{Spec: fSpec, Argument: "foo"}},
   255  	},
   256  	{
   257  		name: "OptionArgument of short option, same argument",
   258  		args: []string{"-ffoo"},
   259  		wantCtx: Context{
   260  			Type:   OptionArgument,
   261  			Option: &Option{Spec: fSpec, Argument: "foo"}},
   262  	},
   263  	{
   264  		name: "OptionArgument of long option, separate argument",
   265  		args: []string{"--file", "foo"},
   266  		wantCtx: Context{
   267  			Type:   OptionArgument,
   268  			Option: &Option{Spec: fSpec, Long: true, Argument: "foo"}},
   269  	},
   270  	{
   271  		name: "OptionArgument of long option, same argument",
   272  		args: []string{"--file=foo"},
   273  		wantCtx: Context{
   274  			Type:   OptionArgument,
   275  			Option: &Option{Spec: fSpec, Long: true, Argument: "foo"}},
   276  	},
   277  	{
   278  		name: "OptionArgument of long option with LongOnly, same argument",
   279  		args: []string{"-file=foo"},
   280  		cfg:  LongOnly,
   281  		wantCtx: Context{
   282  			Type:   OptionArgument,
   283  			Option: &Option{Spec: fSpec, Long: true, Argument: "foo"}},
   284  	},
   285  	{
   286  		name:    "Argument",
   287  		args:    []string{"foo"},
   288  		wantCtx: Context{Type: Argument, Text: "foo"},
   289  	},
   290  	{
   291  		name:    "Argument after --",
   292  		args:    []string{"--", "foo"},
   293  		cfg:     StopAfterDoubleDash,
   294  		wantCtx: Context{Type: Argument, Text: "foo"},
   295  	},
   296  	{
   297  		name:     "Argument after first non-option argument",
   298  		args:     []string{"bar", "foo"},
   299  		cfg:      StopBeforeFirstNonOption,
   300  		wantArgs: []string{"bar"},
   301  		wantCtx:  Context{Type: Argument, Text: "foo"},
   302  	},
   303  }
   304  
   305  func TestComplete(t *testing.T) {
   306  	for _, tc := range completeTests {
   307  		t.Run(tc.name, func(t *testing.T) {
   308  			opts, args, ctx := Complete(tc.args, specs, tc.cfg)
   309  			check := func(name string, got, want any) {
   310  				if !reflect.DeepEqual(got, want) {
   311  					t.Errorf("Parse(%#v) (config = %v)\ngot %s = %v, want %v",
   312  						tc.args, tc.cfg, name, got, want)
   313  				}
   314  			}
   315  			check("opts", opts, tc.wantOpts)
   316  			check("args", args, tc.wantArgs)
   317  			check("ctx", ctx, tc.wantCtx)
   318  		})
   319  	}
   320  }