command_mapper-gen 0.1.0.pre1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/ruby.yml +27 -0
  3. data/.gitignore +10 -0
  4. data/.rspec +1 -0
  5. data/.yardopts +1 -0
  6. data/ChangeLog.md +20 -0
  7. data/Gemfile +17 -0
  8. data/LICENSE.txt +20 -0
  9. data/README.md +145 -0
  10. data/Rakefile +15 -0
  11. data/bin/command_mapper-gen +7 -0
  12. data/commnad_mapper-gen.gemspec +61 -0
  13. data/examples/grep.rb +63 -0
  14. data/gemspec.yml +26 -0
  15. data/lib/command_mapper/gen/arg.rb +43 -0
  16. data/lib/command_mapper/gen/argument.rb +53 -0
  17. data/lib/command_mapper/gen/cli.rb +233 -0
  18. data/lib/command_mapper/gen/command.rb +202 -0
  19. data/lib/command_mapper/gen/exceptions.rb +9 -0
  20. data/lib/command_mapper/gen/option.rb +66 -0
  21. data/lib/command_mapper/gen/option_value.rb +23 -0
  22. data/lib/command_mapper/gen/parsers/common.rb +49 -0
  23. data/lib/command_mapper/gen/parsers/help.rb +351 -0
  24. data/lib/command_mapper/gen/parsers/man.rb +80 -0
  25. data/lib/command_mapper/gen/parsers/options.rb +127 -0
  26. data/lib/command_mapper/gen/parsers/usage.rb +141 -0
  27. data/lib/command_mapper/gen/parsers.rb +2 -0
  28. data/lib/command_mapper/gen/task.rb +90 -0
  29. data/lib/command_mapper/gen/types/enum.rb +30 -0
  30. data/lib/command_mapper/gen/types/key_value.rb +34 -0
  31. data/lib/command_mapper/gen/types/list.rb +34 -0
  32. data/lib/command_mapper/gen/types/map.rb +36 -0
  33. data/lib/command_mapper/gen/types/num.rb +18 -0
  34. data/lib/command_mapper/gen/types/str.rb +48 -0
  35. data/lib/command_mapper/gen/types.rb +6 -0
  36. data/lib/command_mapper/gen/version.rb +6 -0
  37. data/lib/command_mapper/gen.rb +2 -0
  38. data/spec/argument_spec.rb +92 -0
  39. data/spec/cli_spec.rb +269 -0
  40. data/spec/command_spec.rb +316 -0
  41. data/spec/option_spec.rb +85 -0
  42. data/spec/option_value_spec.rb +20 -0
  43. data/spec/parsers/common_spec.rb +616 -0
  44. data/spec/parsers/help_spec.rb +612 -0
  45. data/spec/parsers/man_spec.rb +158 -0
  46. data/spec/parsers/options_spec.rb +802 -0
  47. data/spec/parsers/usage_spec.rb +1175 -0
  48. data/spec/spec_helper.rb +6 -0
  49. data/spec/task_spec.rb +69 -0
  50. data/spec/types/enum_spec.rb +45 -0
  51. data/spec/types/key_value_spec.rb +36 -0
  52. data/spec/types/list_spec.rb +36 -0
  53. data/spec/types/map_spec.rb +48 -0
  54. data/spec/types/str_spec.rb +70 -0
  55. metadata +133 -0
@@ -0,0 +1,612 @@
1
+ require 'spec_helper'
2
+ require 'command_mapper/gen/parsers/help'
3
+
4
+ describe CommandMapper::Gen::Parsers::Help do
5
+ let(:command_name) { 'yes' }
6
+ let(:command) { CommandMapper::Gen::Command.new(command_name) }
7
+
8
+ subject { described_class.new(command) }
9
+
10
+ describe "#initialize" do
11
+ it "must set #command" do
12
+ expect(subject.command).to be(command)
13
+ end
14
+
15
+ context "when a block is given" do
16
+ let(:block) do
17
+ proc { |line,error| }
18
+ end
19
+
20
+ subject { described_class.new(command,&block) }
21
+
22
+ it "must set #parser_error_callback" do
23
+ expect(subject.parser_error_callback).to eq(block)
24
+ end
25
+ end
26
+ end
27
+
28
+ describe "#parse_usage" do
29
+ let(:usage) { "#{command_name} ARG1 ARG2" }
30
+
31
+ before { subject.parse_usage(usage) }
32
+
33
+ it "must parse and add the argument to the command" do
34
+ expect(command.arguments.keys).to eq([:arg1, :arg2])
35
+ expect(command.arguments[:arg1].name).to eq(:arg1)
36
+ expect(command.arguments[:arg2].name).to eq(:arg2)
37
+ end
38
+
39
+ context "when the argument is optional" do
40
+ let(:usage) { "#{command_name} ARG1 [ARG2]" }
41
+
42
+ before { subject.parse_usage(usage) }
43
+
44
+ it "must set the Argument#required to false" do
45
+ expect(command.arguments[:arg2].required).to be(false)
46
+ end
47
+ end
48
+
49
+ context "when the argument repeats" do
50
+ let(:usage) { "#{command_name} ARG1 ARG2..." }
51
+
52
+ before { subject.parse_usage(usage) }
53
+
54
+ it "must set the Argument#required to false" do
55
+ expect(command.arguments[:arg2].repeats).to be(true)
56
+ end
57
+ end
58
+
59
+ context "when an optional argument group repeats" do
60
+ let(:usage) { "#{command_name} [ARG1 ARG2]..." }
61
+
62
+ before { subject.parse_usage(usage) }
63
+
64
+ it "must set the Argument#required to false" do
65
+ expect(command.arguments[:arg1].required).to be(false)
66
+ expect(command.arguments[:arg1].repeats).to be(true)
67
+
68
+ expect(command.arguments[:arg2].required).to be(false)
69
+ expect(command.arguments[:arg2].repeats).to be(true)
70
+ end
71
+ end
72
+
73
+ context "when the argument is named OPTS" do
74
+ let(:usage) { "#{command_name} [OPTS] ARG..." }
75
+
76
+ before { subject.parse_usage(usage) }
77
+
78
+ it "must ignore it" do
79
+ expect(command.arguments.keys).to eq([:arg])
80
+ end
81
+ end
82
+
83
+ context "when the argument is named OPTION" do
84
+ let(:usage) { "#{command_name} [OPTION] ARG..." }
85
+
86
+ before { subject.parse_usage(usage) }
87
+
88
+ it "must ignore it" do
89
+ expect(command.arguments.keys).to eq([:arg])
90
+ end
91
+ end
92
+
93
+ context "when the argument is named OPTIONS" do
94
+ let(:usage) { "#{command_name} [OPTIONS] ARG..." }
95
+
96
+ before { subject.parse_usage(usage) }
97
+
98
+ it "must ignore it" do
99
+ expect(command.arguments.keys).to eq([:arg])
100
+ end
101
+ end
102
+
103
+ context "when the usage cannot be parsed" do
104
+ let(:usage) { " " }
105
+
106
+ it "must return nil" do
107
+ expect(subject.parse_usage(usage)).to be(nil)
108
+ end
109
+
110
+ context "when a parser error callback is set" do
111
+ it "must pass the text and parser error to the callback" do
112
+ yielded_line = nil
113
+ yielded_parser_error = nil
114
+
115
+ subject = described_class.new(command) do |line,parser_error|
116
+ yielded_line = line
117
+ yielded_parser_error = parser_error
118
+ end
119
+
120
+ expect(subject.parse_usage(usage)).to be(nil)
121
+
122
+ expect(yielded_line).to eq(usage)
123
+ expect(yielded_parser_error).to be_kind_of(Parslet::ParseFailed)
124
+ end
125
+ end
126
+ end
127
+ end
128
+
129
+ describe "#parse_option_line" do
130
+ let(:long_flag) { "--option" }
131
+ let(:line) { " #{long_flag} Bla bla bla" }
132
+
133
+ before { subject.parse_option_line(line) }
134
+
135
+ it "must parse and add the option to the command" do
136
+ expect(command.options.keys).to eq([long_flag])
137
+ expect(command.options[long_flag].flag).to eq(long_flag)
138
+ end
139
+
140
+ context "when the option line also includes a short flag" do
141
+ let(:short_flag) { "-o" }
142
+ let(:line) { " #{short_flag}, #{long_flag} Bla bla bla" }
143
+
144
+ before { subject.parse_option_line(line) }
145
+
146
+ it "must parse and add the option to the command" do
147
+ expect(command.options.keys).to eq([long_flag])
148
+ end
149
+ end
150
+
151
+ context "when the option line includes a value" do
152
+ let(:line) { " #{long_flag} VALUE Bla bla bla" }
153
+
154
+ before { subject.parse_option_line(line) }
155
+
156
+ it "must set the Option#value" do
157
+ expect(command.options[long_flag].value).to_not be_nil
158
+ end
159
+
160
+ it "must set the OptionValue#required to true" do
161
+ expect(command.options[long_flag].value.required).to be(true)
162
+ end
163
+
164
+ context "when the option value is optional" do
165
+ let(:line) { " #{long_flag} [VALUE] Bla bla bla" }
166
+
167
+ before { subject.parse_option_line(line) }
168
+
169
+ it "must set the OptionValue#required to false" do
170
+ expect(command.options[long_flag].value.required).to be(false)
171
+ end
172
+ end
173
+
174
+ context "when the option flag and value are joined by a '='" do
175
+ let(:line) { " #{long_flag}=VALUE Bla bla bla" }
176
+
177
+ before { subject.parse_option_line(line) }
178
+
179
+ it "must set Option#equals to true" do
180
+ expect(command.options[long_flag].equals).to be(true)
181
+ end
182
+ end
183
+
184
+ context "when the option value is named NUM" do
185
+ let(:line) { " #{long_flag} NUM Bla bla bla" }
186
+
187
+ before { subject.parse_option_line(line) }
188
+
189
+ it "must set Option#value's type to a Num" do
190
+ expect(command.options[long_flag].value.type).to be_kind_of(CommandMapper::Gen::Types::Num)
191
+ end
192
+ end
193
+
194
+ context "and when the value contains literal values" do
195
+ let(:value1) { :foo }
196
+ let(:value2) { :bar }
197
+ let(:value3) { :baz }
198
+ let(:line) { " #{long_flag}=[#{value1}|#{value2}|#{value3}] Bla bla bla" }
199
+
200
+ let(:values) { [value1, value2, value3] }
201
+
202
+ before { subject.parse_option_line(line) }
203
+
204
+ it "must set Option#value's #type to an Enum of the values" do
205
+ expect(command.options[long_flag].value.type).to be_kind_of(CommandMapper::Gen::Types::Enum)
206
+ expect(command.options[long_flag].value.type.values).to eq(values)
207
+ end
208
+
209
+ context "when the literal values are 'YES' and 'NO'" do
210
+ let(:line) { " #{long_flag}=[YES|NO] Bla bla bla" }
211
+
212
+ it "must set Option#value's #type to a Map of {true=>'YES', false=>'NO'}" do
213
+ expect(command.options[long_flag].value.type).to be_kind_of(CommandMapper::Gen::Types::Map)
214
+ expect(command.options[long_flag].value.type.map).to eq(
215
+ {true => 'YES', false => 'NO'}
216
+ )
217
+ end
218
+ end
219
+
220
+ context "when the literal values are 'Yes' and 'No'" do
221
+ let(:line) { " #{long_flag}=[Yes|No] Bla bla bla" }
222
+
223
+ it "must set Option#value's #type to a Map of {true=>'Yes', false=>'No'}" do
224
+ expect(command.options[long_flag].value.type).to be_kind_of(CommandMapper::Gen::Types::Map)
225
+ expect(command.options[long_flag].value.type.map).to eq(
226
+ {true => 'Yes', false => 'No'}
227
+ )
228
+ end
229
+ end
230
+
231
+ context "when the literal values are 'yes' and 'no'" do
232
+ let(:line) { " #{long_flag}=[yes|no] Bla bla bla" }
233
+
234
+ it "must set Option#value's #type to a Map of {true=>'yes', false=>'no'}" do
235
+ expect(command.options[long_flag].value.type).to be_kind_of(CommandMapper::Gen::Types::Map)
236
+ expect(command.options[long_flag].value.type.map).to eq(
237
+ {true => 'yes', false => 'no'}
238
+ )
239
+ end
240
+ end
241
+
242
+ context "when the literal values are 'Y' and 'N'" do
243
+ let(:line) { " #{long_flag}=[Y|N] Bla bla bla" }
244
+
245
+ it "must set Option#value's #type to a Map of {true=>'Y', false=>'N'}" do
246
+ expect(command.options[long_flag].value.type).to be_kind_of(CommandMapper::Gen::Types::Map)
247
+ expect(command.options[long_flag].value.type.map).to eq(
248
+ {true => 'Y', false => 'N'}
249
+ )
250
+ end
251
+ end
252
+
253
+ context "when the literal values are 'y' and 'n'" do
254
+ let(:line) { " #{long_flag}=[y|n] Bla bla bla" }
255
+
256
+ it "must set Option#value's #type to a Map of {true=>'y', false=>'n'}" do
257
+ expect(command.options[long_flag].value.type).to be_kind_of(CommandMapper::Gen::Types::Map)
258
+ expect(command.options[long_flag].value.type.map).to eq(
259
+ {true => 'y', false => 'n'}
260
+ )
261
+ end
262
+ end
263
+
264
+ context "when the literal values are 'ENABLED' and 'DISABLED'" do
265
+ let(:line) { " #{long_flag}=[ENABLED|DISABLED] Bla bla bla" }
266
+
267
+ it "must set Option#value's #type to a Map of {true=>'ENABLED', false=>'DISABLED'}" do
268
+ expect(command.options[long_flag].value.type).to be_kind_of(CommandMapper::Gen::Types::Map)
269
+ expect(command.options[long_flag].value.type.map).to eq(
270
+ {true => 'ENABLED', false => 'DISABLED'}
271
+ )
272
+ end
273
+ end
274
+
275
+ context "when the literal values are 'Enabled' and 'Disabled'" do
276
+ let(:line) { " #{long_flag}=[Enabled|Disabled] Bla bla bla" }
277
+
278
+ it "must set Option#value's #type to a Map of {true=>'Enabled', false=>'Disabled'}" do
279
+ expect(command.options[long_flag].value.type).to be_kind_of(CommandMapper::Gen::Types::Map)
280
+ expect(command.options[long_flag].value.type.map).to eq(
281
+ {true => 'Enabled', false => 'Disabled'}
282
+ )
283
+ end
284
+ end
285
+
286
+ context "when the literal values are 'enabled' and 'disabled'" do
287
+ let(:line) { " #{long_flag}=[enabled|disabled] Bla bla bla" }
288
+
289
+ it "must set Option#value's #type to a Map of {true=>'enabled', false=>'disabled'}" do
290
+ expect(command.options[long_flag].value.type).to be_kind_of(CommandMapper::Gen::Types::Map)
291
+ expect(command.options[long_flag].value.type.map).to eq(
292
+ {true => 'enabled', false => 'disabled'}
293
+ )
294
+ end
295
+ end
296
+ end
297
+ end
298
+
299
+ context "when the option line cannot be parsed" do
300
+ let(:line) { " FOO BAR BAZ Bla bla bla" }
301
+
302
+ it "must return nil" do
303
+ expect(subject.parse_option_line(line)).to be(nil)
304
+ end
305
+
306
+ context "when an parser error callback is given" do
307
+ it "must pass the text and parser error to the callback" do
308
+ yielded_line = nil
309
+ yielded_parser_error = nil
310
+
311
+ subject = described_class.new(command) do |line,parser_error|
312
+ yielded_line = line
313
+ yielded_parser_error = parser_error
314
+ end
315
+
316
+ expect(subject.parse_option_line(line)).to be(nil)
317
+
318
+ expect(yielded_line).to eq(line)
319
+ expect(yielded_parser_error).to be_kind_of(Parslet::ParseFailed)
320
+ end
321
+ end
322
+ end
323
+ end
324
+
325
+ describe "#parse_subcommand" do
326
+ let(:subcommand) { 'bar' }
327
+ let(:line) { " #{subcommand} bla bla bla" }
328
+
329
+ context "when the line starts with a subcommand name" do
330
+ before { subject.parse_subcommand_line(line) }
331
+
332
+ it "must parse the subcommand and add it to the command" do
333
+ expect(command.subcommands.keys).to eq([subcommand])
334
+ expect(command.subcommands[subcommand].command_name).to eq(subcommand)
335
+ end
336
+ end
337
+
338
+ context "when the line cannot be parsed" do
339
+ let(:line) { " " }
340
+
341
+ it "must return nil" do
342
+ expect(subject.parse_subcommand_line(line)).to be(nil)
343
+ end
344
+ end
345
+ end
346
+
347
+ let(:output) do
348
+ <<OUTPUT
349
+ Usage: yes [STRING]...
350
+ or: yes OPTION
351
+ Repeatedly output a line with all specified STRING(s), or 'y'.
352
+
353
+ --help display this help and exit
354
+ --version output version information and exit
355
+
356
+ GNU coreutils online help: <https://www.gnu.org/software/coreutils/>
357
+ Full documentation <https://www.gnu.org/software/coreutils/yes>
358
+ or available locally via: info '(coreutils) yes invocation'
359
+ OUTPUT
360
+ end
361
+
362
+ let(:subcommands_output) do
363
+ <<OUTPUT
364
+ NAME:
365
+ runc - Open Container Initiative runtime
366
+
367
+ runc is a command line client for running applications packaged according to
368
+ the Open Container Initiative (OCI) format and is a compliant implementation of the
369
+ Open Container Initiative specification.
370
+
371
+ runc integrates well with existing process supervisors to provide a production
372
+ container runtime environment for applications. It can be used with your
373
+ existing process monitoring tools and the container will be spawned as a
374
+ direct child of the process supervisor.
375
+
376
+ Containers are configured using bundles. A bundle for a container is a directory
377
+ that includes a specification file named "config.json" and a root filesystem.
378
+ The root filesystem contains the contents of the container.
379
+
380
+ To start a new instance of a container:
381
+
382
+ # runc run [ -b bundle ] <container-id>
383
+
384
+ Where "<container-id>" is your name for the instance of the container that you
385
+ are starting. The name you provide for the container instance must be unique on
386
+ your host. Providing the bundle directory using "-b" is optional. The default
387
+ value for "bundle" is the current directory.
388
+
389
+ USAGE:
390
+ runc [global options] command [command options] [arguments...]
391
+
392
+ VERSION:
393
+ 1.0.2
394
+ commit: e15f155-dirty
395
+ spec: 1.0.2-dev
396
+ go: go1.16.6
397
+ libseccomp: 2.5.0
398
+
399
+ COMMANDS:
400
+ checkpoint checkpoint a running container
401
+ create create a container
402
+ delete delete any resources held by the container often used with detached container
403
+ events display container events such as OOM notifications, cpu, memory, and IO usage statistics
404
+ exec execute new process inside the container
405
+ init initialize the namespaces and launch the process (do not call it outside of runc)
406
+ kill kill sends the specified signal (default: SIGTERM) to the container's init process
407
+ list lists containers started by runc with the given root
408
+ pause pause suspends all processes inside the container
409
+ ps ps displays the processes running inside a container
410
+ restore restore a container from a previous checkpoint
411
+ resume resumes all processes that have been previously paused
412
+ run create and run a container
413
+ spec create a new specification file
414
+ start executes the user defined process in a created container
415
+ state output the state of a container
416
+ update update container resource constraints
417
+ help, h Shows a list of commands or help for one command
418
+
419
+ GLOBAL OPTIONS:
420
+ --debug enable debug output for logging
421
+ --log value set the log file path where internal debug information is written
422
+ --log-format value set the format used by logs ('text' (default), or 'json') (default: "text")
423
+ --root value root directory for storage of container state (this should be located in tmpfs) (default: "/run/user/1000/runc")
424
+ --criu value path to the criu binary used for checkpoint and restore (default: "criu")
425
+ --systemd-cgroup enable systemd cgroup support, expects cgroupsPath to be of form "slice:prefix:name" for e.g. "system.slice:runc:434234"
426
+ --rootless value ignore cgroup permission errors ('true', 'false', or 'auto') (default: "auto")
427
+ --help, -h show help
428
+ --version, -v print the version
429
+ OUTPUT
430
+ end
431
+
432
+ describe "#parse" do
433
+ before { subject.parse(output) }
434
+
435
+ it "must parse and populate options" do
436
+ expect(command.options.keys).to eq(%w[--help --version])
437
+ end
438
+
439
+ it "must parse and populate arguments" do
440
+ expect(command.arguments.keys).to eq([:string])
441
+ end
442
+
443
+ context "when the `--help` output includes subcommands" do
444
+ let(:command_name) { 'runc' }
445
+ let(:output) { subcommands_output }
446
+
447
+ it "must populate the command's subcommands" do
448
+ expect(command.subcommands.keys).to eq(
449
+ %w[
450
+ checkpoint create delete events exec init kill list pause ps restore
451
+ resume run spec start state update help
452
+ ]
453
+ )
454
+ end
455
+
456
+ it "must still parse and populate options" do
457
+ expect(command.options.keys).to eq(
458
+ %w[
459
+ --debug --log --log-format --root --criu --systemd-cgroup --rootless
460
+ --help --version
461
+ ]
462
+ )
463
+ end
464
+
465
+ it "must still parse and populate arguments" do
466
+ expect(command.arguments.keys).to eq([:command, :arguments])
467
+ end
468
+ end
469
+ end
470
+
471
+ describe "#print_parser_error" do
472
+ end
473
+
474
+ describe "#parse" do
475
+ before { subject.parse(output) }
476
+
477
+ it "must populate the command's options" do
478
+ expect(command.options.keys).to eq(%w[--help --version])
479
+ expect(command.options['--help']).to be_kind_of(CommandMapper::Gen::Option)
480
+ expect(command.options['--version']).to be_kind_of(CommandMapper::Gen::Option)
481
+ end
482
+
483
+ it "must populate the command's arguments" do
484
+ expect(command.arguments.keys).to eq([:string])
485
+ expect(command.arguments[:string]).to be_kind_of(CommandMapper::Gen::Argument)
486
+ end
487
+
488
+ context "when the output contains subcommands" do
489
+ let(:command_name) { 'runc' }
490
+
491
+ let(:output) { subcommands_output }
492
+
493
+ it "must populate the command's subcommands" do
494
+ expect(command.subcommands.keys).to eq(
495
+ %w[
496
+ checkpoint create delete events exec init kill list pause ps restore
497
+ resume run spec start state update help
498
+ ]
499
+ )
500
+ end
501
+ end
502
+
503
+ context "when the usage: command is on the following line" do
504
+ let(:command) do
505
+ CommandMapper::Gen::Command.new(
506
+ 'tag', CommandMapper::Gen::Command.new(
507
+ 'image', CommandMapper::Gen::Command.new(
508
+ 'podman'
509
+ )
510
+ )
511
+ )
512
+ end
513
+
514
+ let(:output) do
515
+ <<OUTPUT
516
+ Add an additional name to a local image
517
+
518
+ Description:
519
+ Adds one or more additional names to locally-stored image.
520
+
521
+ Usage:
522
+ podman image tag IMAGE TARGET_NAME [TARGET_NAME...]
523
+
524
+ Examples:
525
+ podman image tag 0e3bbc2 fedora:latest
526
+ podman image tag imageID:latest myNewImage:newTag
527
+ podman image tag httpd myregistryhost:5000/fedora/httpd:v2
528
+
529
+ OUTPUT
530
+ end
531
+
532
+ it "must parse the line after the usage:" do
533
+ expect(command.arguments.keys).to eq(
534
+ [
535
+ :image, :target_name
536
+ ]
537
+ )
538
+ end
539
+ end
540
+ end
541
+
542
+ describe ".parse" do
543
+ subject { described_class }
544
+
545
+ before { subject.parse(output,command) }
546
+
547
+ it "must parse the output and populate the command" do
548
+ expect(command.options.keys).to eq(%w[--help --version])
549
+ expect(command.options['--help']).to be_kind_of(CommandMapper::Gen::Option)
550
+ expect(command.options['--version']).to be_kind_of(CommandMapper::Gen::Option)
551
+
552
+ expect(command.arguments.keys).to eq([:string])
553
+ expect(command.arguments[:string]).to be_kind_of(CommandMapper::Gen::Argument)
554
+ end
555
+ end
556
+
557
+ describe ".run" do
558
+ subject { described_class }
559
+
560
+ it "must parse the output, populate the commamd, and return the command" do
561
+ parsed_command = subject.run(command)
562
+
563
+ expect(parsed_command).to be(command)
564
+
565
+ expect(parsed_command.options.keys).to eq(%w[--help --version])
566
+ expect(parsed_command.options['--help']).to be_kind_of(CommandMapper::Gen::Option)
567
+ expect(parsed_command.options['--version']).to be_kind_of(CommandMapper::Gen::Option)
568
+
569
+ expect(parsed_command.arguments.keys).to eq([:string])
570
+ expect(parsed_command.arguments[:string]).to be_kind_of(CommandMapper::Gen::Argument)
571
+ end
572
+
573
+ context "when the command is not installed" do
574
+ let(:command_name) { "foo" }
575
+
576
+ before do
577
+ allow(subject).to receive(:`).and_raise(Errno::ENOENT.new("foo"))
578
+ end
579
+
580
+ it do
581
+ expect {
582
+ subject.run(command)
583
+ }.to raise_error(CommandMapper::Gen::CommandNotInstalled,"command #{command_name.inspect} is not installed")
584
+ end
585
+ end
586
+
587
+ context "when there is no output from `--help`" do
588
+ let(:command_name) { "foo" }
589
+
590
+ before do
591
+ allow(subject).to receive(:`).with("#{command_name} --help 2>&1").and_return("")
592
+ end
593
+
594
+ it "must try executing -h" do
595
+ expect(subject).to receive(:`).with("#{command_name} -h 2>&1").and_return("")
596
+
597
+ subject.run(command)
598
+ end
599
+
600
+ context "but `-h` also produces no output" do
601
+ before do
602
+ allow(subject).to receive(:`).with("#{command_name} --help 2>&1").and_return("")
603
+ allow(subject).to receive(:`).with("#{command_name} -h 2>&1").and_return("")
604
+ end
605
+
606
+ it "must return nil" do
607
+ expect(subject.run(command)).to be(nil)
608
+ end
609
+ end
610
+ end
611
+ end
612
+ end