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 }