command_mapper 0.1.0.pre1

Sign up to get free protection for your applications and to get access to all the features.
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 +25 -0
  7. data/Gemfile +15 -0
  8. data/LICENSE.txt +20 -0
  9. data/README.md +369 -0
  10. data/Rakefile +12 -0
  11. data/commnad_mapper.gemspec +61 -0
  12. data/gemspec.yml +23 -0
  13. data/lib/command_mapper/arg.rb +75 -0
  14. data/lib/command_mapper/argument.rb +142 -0
  15. data/lib/command_mapper/command.rb +606 -0
  16. data/lib/command_mapper/exceptions.rb +19 -0
  17. data/lib/command_mapper/option.rb +282 -0
  18. data/lib/command_mapper/option_value.rb +21 -0
  19. data/lib/command_mapper/sudo.rb +73 -0
  20. data/lib/command_mapper/types/enum.rb +35 -0
  21. data/lib/command_mapper/types/hex.rb +82 -0
  22. data/lib/command_mapper/types/input_dir.rb +35 -0
  23. data/lib/command_mapper/types/input_file.rb +35 -0
  24. data/lib/command_mapper/types/input_path.rb +29 -0
  25. data/lib/command_mapper/types/key_value.rb +131 -0
  26. data/lib/command_mapper/types/key_value_list.rb +45 -0
  27. data/lib/command_mapper/types/list.rb +90 -0
  28. data/lib/command_mapper/types/map.rb +64 -0
  29. data/lib/command_mapper/types/num.rb +50 -0
  30. data/lib/command_mapper/types/str.rb +85 -0
  31. data/lib/command_mapper/types/type.rb +102 -0
  32. data/lib/command_mapper/types.rb +6 -0
  33. data/lib/command_mapper/version.rb +4 -0
  34. data/lib/command_mapper.rb +2 -0
  35. data/spec/arg_spec.rb +137 -0
  36. data/spec/argument_spec.rb +513 -0
  37. data/spec/commnad_spec.rb +1175 -0
  38. data/spec/exceptions_spec.rb +14 -0
  39. data/spec/option_spec.rb +882 -0
  40. data/spec/option_value_spec.rb +17 -0
  41. data/spec/spec_helper.rb +6 -0
  42. data/spec/sudo_spec.rb +24 -0
  43. data/spec/types/enum_spec.rb +31 -0
  44. data/spec/types/hex_spec.rb +158 -0
  45. data/spec/types/input_dir_spec.rb +30 -0
  46. data/spec/types/input_file_spec.rb +34 -0
  47. data/spec/types/input_path_spec.rb +32 -0
  48. data/spec/types/key_value_list_spec.rb +100 -0
  49. data/spec/types/key_value_spec.rb +272 -0
  50. data/spec/types/list_spec.rb +143 -0
  51. data/spec/types/map_spec.rb +62 -0
  52. data/spec/types/num_spec.rb +90 -0
  53. data/spec/types/str_spec.rb +232 -0
  54. data/spec/types/type_spec.rb +59 -0
  55. metadata +118 -0
@@ -0,0 +1,1175 @@
1
+ require 'spec_helper'
2
+ require 'command_mapper/command'
3
+
4
+ describe CommandMapper::Command do
5
+ module TestCommand
6
+ class WithCommandName < CommandMapper::Command
7
+ command 'foo'
8
+ end
9
+
10
+ class NoCommandName < CommandMapper::Command
11
+ end
12
+ end
13
+
14
+ describe ".command_name" do
15
+ subject { command_class }
16
+
17
+ context "when @command_name has been set" do
18
+ let(:command_class) { TestCommand::WithCommandName }
19
+
20
+ it "must return the defined command name" do
21
+ expect(subject.command_name).to eq('foo')
22
+ end
23
+
24
+ it "must freeze the given command name" do
25
+ expect(subject.command_name).to be_frozen
26
+ end
27
+ end
28
+
29
+ context "when no .command has been defined" do
30
+ let(:command_class) { TestCommand::NoCommandName }
31
+
32
+ it "must raise NotImplementedError" do
33
+ expect {
34
+ subject.command_name
35
+ }.to raise_error(NotImplementedError,"#{command_class} did not call command(...)")
36
+ end
37
+ end
38
+ end
39
+
40
+ describe ".command_name" do
41
+ subject { command_class }
42
+
43
+ context "when @command_name has been set" do
44
+ let(:command_class) { TestCommand::WithCommandName }
45
+
46
+ it "must set .command_name" do
47
+ expect(subject.command_name).to eq('foo')
48
+ end
49
+ end
50
+ end
51
+
52
+ module TestCommand
53
+ class EmptyCommand < CommandMapper::Command
54
+ end
55
+ end
56
+
57
+ describe ".options" do
58
+ subject { command_class }
59
+
60
+ context "when the command has no defined options" do
61
+ let(:command_class) { TestCommand::EmptyCommand }
62
+
63
+ it { expect(subject.options).to be_empty }
64
+ end
65
+
66
+ context "and when the command inherits from another command class" do
67
+ module TestCommand
68
+ class BaseClassWithOptions < CommandMapper::Command
69
+ option "--foo"
70
+ option "--bar"
71
+ end
72
+
73
+ class InheritedOptions < BaseClassWithOptions
74
+ end
75
+ end
76
+
77
+ let(:command_class) { TestCommand::InheritedOptions }
78
+ let(:command_superclass) { TestCommand::BaseClassWithOptions }
79
+
80
+ it "must copy the options defined in the superclass" do
81
+ expect(subject.options).to eq(command_superclass.options)
82
+ end
83
+
84
+ context "and when the class defines options of it's own" do
85
+ module TestCommand
86
+ class InheritsAndDefinesOptions < BaseClassWithOptions
87
+ option "--baz"
88
+ end
89
+ end
90
+
91
+ let(:command_class) { TestCommand::InheritsAndDefinesOptions }
92
+
93
+ it "must copy the options defined in the superclass" do
94
+ expect(subject.options).to include(command_superclass.options)
95
+ end
96
+
97
+ it "must define it's own options" do
98
+ expect(command_class.options[:baz]).to be_kind_of(CommandMapper::Option)
99
+ end
100
+
101
+ it "must not modify the superclass's options" do
102
+ expect(command_superclass.options[:baz]).to be(nil)
103
+ end
104
+ end
105
+ end
106
+ end
107
+
108
+ describe ".option" do
109
+ module TestCommand
110
+ class DefinesItsOwnOptions < CommandMapper::Command
111
+ command 'test' do
112
+ option '--foo'
113
+ option '--bar'
114
+ end
115
+ end
116
+ end
117
+
118
+ let(:command_class) { TestCommand::DefinesItsOwnOptions }
119
+
120
+ subject { command_class }
121
+
122
+ it "must add options to .options" do
123
+ expect(subject.options[:foo]).to be_kind_of(CommandMapper::Option)
124
+ expect(subject.options[:foo].flag).to eq('--foo')
125
+
126
+ expect(subject.options[:bar]).to be_kind_of(CommandMapper::Option)
127
+ expect(subject.options[:bar].flag).to eq('--bar')
128
+ end
129
+
130
+ it "must define a reader method for each option" do
131
+ expect(subject.instance_methods(false)).to include(:foo)
132
+ expect(subject.instance_methods(false)).to include(:bar)
133
+ end
134
+
135
+ it "must define a writter method for each option" do
136
+ expect(subject.instance_methods(false)).to include(:foo=)
137
+ expect(subject.instance_methods(false)).to include(:bar=)
138
+ end
139
+
140
+ describe "reader method" do
141
+ subject { command_class.new }
142
+
143
+ let(:value) { "test_reading" }
144
+
145
+ before do
146
+ subject.instance_variable_set('@foo',value)
147
+ end
148
+
149
+ it "must read the options value from @options" do
150
+ expect(subject.foo).to be(value)
151
+ end
152
+ end
153
+
154
+ describe "writter method" do
155
+ subject { command_class.new }
156
+
157
+ let(:value) { "test_writing" }
158
+
159
+ before { subject.foo = value }
160
+
161
+ it "must read the options value from @options" do
162
+ expect(subject.instance_variable_get('@foo')).to be(value)
163
+ end
164
+ end
165
+
166
+ context "when given a short flag" do
167
+ context "and it's length is < 3" do
168
+ module TestCommand
169
+ class EmptyCommand < CommandMapper::Command
170
+ end
171
+ end
172
+
173
+ let(:command_class) { TestCommand::EmptyCommand }
174
+
175
+ it "must raise an ArgumentError" do
176
+ expect {
177
+ subject.option '-o'
178
+ }.to raise_error(ArgumentError,"cannot infer a name from short option flag: \"-o\"")
179
+ end
180
+ end
181
+
182
+ context "but it's length is >= 3" do
183
+ module TestCommand
184
+ class ShortOptionWithoutName < CommandMapper::Command
185
+ option "-ip"
186
+ end
187
+ end
188
+
189
+ let(:command_class) { TestCommand::ShortOptionWithoutName }
190
+
191
+ it "must register an option based on the short flag" do
192
+ expect(subject.options[:ip]).to be_kind_of(Option)
193
+ expect(subject.options[:ip].name).to eq(:ip)
194
+ expect(subject.options[:ip].flag).to eq("-ip")
195
+ end
196
+
197
+ it "must define a reader method for the option" do
198
+ expect(subject.instance_methods(false)).to include(:ip)
199
+ end
200
+
201
+ it "must define a writter method for the option" do
202
+ expect(subject.instance_methods(false)).to include(:ip=)
203
+ end
204
+ end
205
+ end
206
+
207
+ context "when the argument shares the same name as an internal method" do
208
+ let(:command_class) { Class.new(described_class) }
209
+ let(:flag) { "--flag" }
210
+ let(:name) { :command_argv }
211
+
212
+ it do
213
+ expect {
214
+ command_class.option flag, name: name
215
+ }.to raise_error(ArgumentError,"option #{flag.inspect} with name #{name.inspect} cannot override the internal method with same name: ##{name}")
216
+ end
217
+ end
218
+
219
+ context "when the argument flag maps to an existing internal method" do
220
+ let(:command_class) { Class.new(described_class) }
221
+ let(:flag) { "--command-argv" }
222
+ let(:name) { :command_argv }
223
+
224
+ it do
225
+ expect {
226
+ command_class.option(flag)
227
+ }.to raise_error(ArgumentError,"option #{flag.inspect} maps to method name ##{name} and cannot override the internal method with same name: ##{name}")
228
+ end
229
+ end
230
+ end
231
+
232
+ describe ".arguments" do
233
+ subject { command_class }
234
+
235
+ context "when the command has no defined arguments" do
236
+ let(:command_class) { TestCommand::EmptyCommand }
237
+
238
+ it { expect(subject.arguments).to be_empty }
239
+ end
240
+
241
+ context "when the comand does have defined arguments" do
242
+ module TestCommand
243
+ class BaseClassWithOptions < CommandMapper::Command
244
+ argument :foo
245
+ argument :bar
246
+ end
247
+
248
+ class InheritedOptions < BaseClassWithOptions
249
+ end
250
+ end
251
+
252
+ let(:command_class) { TestCommand::InheritedOptions }
253
+ let(:command_superclass) { TestCommand::BaseClassWithOptions }
254
+
255
+ it "must copy the arguments defined in the superclass" do
256
+ expect(subject.arguments).to eq(command_superclass.arguments)
257
+ end
258
+
259
+ context "and when the class defines arguments of it's own" do
260
+ module TestCommand
261
+ class InheritsAndDefinesOptions < BaseClassWithOptions
262
+ argument :baz
263
+ end
264
+ end
265
+
266
+ let(:command_class) { TestCommand::InheritsAndDefinesOptions }
267
+
268
+ it "must copy the arguments defined in the superclass" do
269
+ expect(subject.arguments).to include(command_superclass.arguments)
270
+ end
271
+
272
+ it "must define it's own arguments" do
273
+ expect(command_class.arguments[:baz]).to be_kind_of(Argument)
274
+ end
275
+
276
+ it "must not modify the superclass's arguments" do
277
+ expect(command_superclass.arguments[:baz]).to be(nil)
278
+ end
279
+ end
280
+ end
281
+ end
282
+
283
+ describe ".argument" do
284
+ module TestCommand
285
+ class DefinesArgument < CommandMapper::Command
286
+ command 'test' do
287
+ argument :foo
288
+ end
289
+ end
290
+ end
291
+
292
+ let(:command_class) { TestCommand::DefinesArgument }
293
+
294
+ subject { command_class }
295
+
296
+ it "must register an argument with the given name" do
297
+ expect(subject.arguments[:foo]).to be_kind_of(Argument)
298
+ expect(subject.arguments[:foo].name).to eq(:foo)
299
+ end
300
+
301
+ it "must define a reader method for the argument" do
302
+ expect(subject.instance_methods(false)).to include(:foo)
303
+ end
304
+
305
+ it "must define a writter method for the argument" do
306
+ expect(subject.instance_methods(false)).to include(:foo=)
307
+ end
308
+
309
+ describe "reader method" do
310
+ subject { command_class.new }
311
+
312
+ let(:value) { "test_reading" }
313
+
314
+ before do
315
+ subject.instance_variable_set('@foo',value)
316
+ end
317
+
318
+ it "must read the options value from @arguments" do
319
+ expect(subject.foo).to be(value)
320
+ end
321
+ end
322
+
323
+ describe "writter method" do
324
+ subject { command_class.new }
325
+
326
+ let(:value) { "test_writing" }
327
+
328
+ before { subject.foo = value }
329
+
330
+ it "must read the options value from @arguments" do
331
+ expect(subject.instance_variable_get('@foo')).to be(value)
332
+ end
333
+ end
334
+
335
+ context "when the argument shares the same name as an internal method" do
336
+ let(:command_class) { Class.new(described_class) }
337
+ let(:name) { :command_argv }
338
+
339
+ it do
340
+ expect {
341
+ command_class.argument(name)
342
+ }.to raise_error(ArgumentError,"argument #{name.inspect} cannot override internal method with same name: ##{name}")
343
+ end
344
+ end
345
+ end
346
+
347
+ describe ".subcommands" do
348
+ subject { command_class }
349
+
350
+ context "when the command has no defined subcommands" do
351
+ let(:command_class) { TestCommand::EmptyCommand }
352
+
353
+ it { expect(subject.subcommands).to be_empty }
354
+ end
355
+
356
+ context "when the comand does have defined subcommands" do
357
+ module TestCommand
358
+ class BaseClassWithOptions < CommandMapper::Command
359
+ subcommand :foo do
360
+ end
361
+
362
+ subcommand :bar do
363
+ end
364
+ end
365
+
366
+ class InheritedOptions < BaseClassWithOptions
367
+ end
368
+ end
369
+
370
+ let(:command_class) { TestCommand::InheritedOptions }
371
+ let(:command_superclass) { TestCommand::BaseClassWithOptions }
372
+
373
+ it "must copy the subcommands defined in the superclass" do
374
+ expect(subject.subcommands).to eq(command_superclass.subcommands)
375
+ end
376
+
377
+ context "and when the class defines subcommands of it's own" do
378
+ module TestCommand
379
+ class InheritsAndDefinesOptions < BaseClassWithOptions
380
+
381
+ subcommand :baz do
382
+ end
383
+
384
+ end
385
+ end
386
+
387
+ let(:command_class) { TestCommand::InheritsAndDefinesOptions }
388
+
389
+ it "must copy the subcommands defined in the superclass" do
390
+ expect(subject.subcommands).to include(command_superclass.subcommands)
391
+ end
392
+
393
+ it "must define it's own subcommands" do
394
+ expect(command_class.subcommands[:baz]).to eq(command_class::Baz)
395
+ end
396
+
397
+ it "must not modify the superclass's subcommands" do
398
+ expect(command_superclass.subcommands[:baz]).to be(nil)
399
+ end
400
+ end
401
+ end
402
+ end
403
+
404
+ describe ".subcommand" do
405
+ module TestCommand
406
+ class DefinesSubcommand < CommandMapper::Command
407
+ command 'cmd' do
408
+ subcommand "subcmd" do
409
+ option '--foo'
410
+ option '--bar'
411
+ argument :baz
412
+ end
413
+ end
414
+ end
415
+ end
416
+
417
+ let(:command_class) { TestCommand::DefinesSubcommand }
418
+
419
+ subject { command_class }
420
+
421
+ it "must add the subcommand to .subcommands using the method name" do
422
+ expect(subject.subcommands[:subcmd]).to be(command_class::Subcmd)
423
+ end
424
+
425
+ it "must define a constant for the new Subcommand class" do
426
+ expect(subject.const_get('Subcmd')).to (be < described_class)
427
+ end
428
+
429
+ it "must initialize a new Command with the given subcommand name" do
430
+ expect(subject.const_get('Subcmd').command_name).to eq("subcmd")
431
+ end
432
+
433
+ it "must define a reader method for the subcommand" do
434
+ expect(subject.instance_methods(false)).to include(:subcmd)
435
+ end
436
+
437
+ it "must define a writter method for the subcommand" do
438
+ expect(subject.instance_methods(false)).to include(:subcmd=)
439
+ end
440
+
441
+ context "when the reader method is called" do
442
+ let(:foo) { 'value1' }
443
+ let(:bar) { 'value2' }
444
+ let(:baz) { 'value3' }
445
+
446
+ subject { command_class.new }
447
+
448
+ context "when no subcommand data has been populated" do
449
+ it "must return nil" do
450
+ expect(subject.subcmd).to be(nil)
451
+ end
452
+ end
453
+
454
+ context "when the subcommand has been set" do
455
+ before do
456
+ subject.subcmd = {foo: foo, bar: bar, baz: baz}
457
+ end
458
+
459
+ it "must return an instance of the defined subcommand class" do
460
+ expect(subject.subcmd).to be_kind_of(command_class::Subcmd)
461
+ end
462
+ end
463
+
464
+ context "with a block" do
465
+ before do
466
+ subject.subcmd do |sub|
467
+ sub.foo = foo
468
+ sub.bar = bar
469
+ sub.baz = baz
470
+ end
471
+ end
472
+
473
+ it "must use the block to populate the new subcommand instance" do
474
+ expect(subject.subcmd.foo).to eq(foo)
475
+ expect(subject.subcmd.bar).to eq(bar)
476
+ expect(subject.subcmd.baz).to eq(baz)
477
+ end
478
+ end
479
+ end
480
+
481
+ context "when the writter method is called" do
482
+ let(:foo) { 'value1' }
483
+ let(:bar) { 'value2' }
484
+ let(:baz) { 'value3' }
485
+
486
+ context "with a Hash" do
487
+ subject { command_class.new }
488
+
489
+ before do
490
+ subject.subcmd = {foo: foo, bar: bar, baz: baz}
491
+ end
492
+
493
+ it "must set @command_subcommand to an instance of the defined subcommand class " do
494
+ expect(subject.instance_variable_get('@command_subcommand')).to be_kind_of(command_class::Subcmd)
495
+ end
496
+
497
+ it "must set the attributes of the subcommand" do
498
+ expect(subject.subcmd.foo).to eq(foo)
499
+ expect(subject.subcmd.bar).to eq(bar)
500
+ expect(subject.subcmd.baz).to eq(baz)
501
+ end
502
+ end
503
+
504
+ context "when nil" do
505
+ subject { command_class.new }
506
+
507
+ before do
508
+ subject.subcmd = {foo: foo, bar: bar, baz: baz}
509
+ subject.subcmd = nil
510
+ end
511
+
512
+ it "must set @commnad_subcommand to nil" do
513
+ expect(subject.instance_variable_get('@command_subcommand')).to be(nil)
514
+ end
515
+ end
516
+ end
517
+
518
+ context "when the subcommand name contains a '-'" do
519
+ module TestCommand
520
+ class DefinesSubcommand < CommandMapper::Command
521
+ subcommand 'sub-cmd' do
522
+ option '--foo'
523
+ option '--bar'
524
+ argument :baz
525
+ end
526
+ end
527
+ end
528
+
529
+ let(:command_class) { TestCommand::DefinesSubcommand }
530
+
531
+ it "must add the subcommand to .subcommands using the method name" do
532
+ expect(subject.subcommands[:sub_cmd]).to be(command_class::SubCmd)
533
+ end
534
+
535
+ it "must define a CamelCased subcommand constant" do
536
+ expect(subject.const_get('SubCmd')).to (be < described_class)
537
+ end
538
+
539
+ it "must replace any '-' characters with '_' for the reader method" do
540
+ expect(subject.instance_methods(false)).to include(:sub_cmd)
541
+ end
542
+
543
+ it "must replace any '-' characters with '_' for the writer method" do
544
+ expect(subject.instance_methods(false)).to include(:sub_cmd=)
545
+ end
546
+ end
547
+
548
+ context "when the subcommand shares the same name as an internal method" do
549
+ let(:command_class) { Class.new(described_class) }
550
+ let(:name) { "command-argv" }
551
+ let(:method_name) { 'command_argv' }
552
+
553
+ it do
554
+ expect {
555
+ command_class.subcommand(name) do
556
+ end
557
+ }.to raise_error(ArgumentError,"subcommand #{name.inspect} maps to method name ##{method_name} and cannot override the internal method with same name: ##{method_name}")
558
+ end
559
+ end
560
+ end
561
+
562
+ module TestCommand
563
+ class ExampleCommand < CommandMapper::Command
564
+ command 'test' do
565
+ option '--opt1', value: {required: true}
566
+ option '--opt2', value: {required: true}
567
+ option '--opt3', value: {required: true}
568
+
569
+ argument :arg1, required: false
570
+ argument :arg2, required: false
571
+ argument :arg3, required: false
572
+
573
+ subcommand 'subcmd' do
574
+ option '--sub-opt1', value: {required: true}
575
+
576
+ argument :sub_arg1, required: true
577
+ end
578
+ end
579
+ end
580
+ end
581
+
582
+ let(:opt1) { "foo" }
583
+ let(:opt2) { "bar" }
584
+ let(:opt3) { "baz" }
585
+ let(:arg1) { "foo" }
586
+ let(:arg2) { "bar" }
587
+ let(:arg3) { "baz" }
588
+ let(:env) { {'FOO' => 'bar'} }
589
+
590
+ let(:command_class) { TestCommand::ExampleCommand }
591
+
592
+ describe ".run" do
593
+ let(:command_instance) { double(:command_instance) }
594
+ let(:return_value) { double(:boolean) }
595
+
596
+ subject { command_class }
597
+
598
+ context "when called with a Hash of params" do
599
+ let(:params) do
600
+ {opt1: opt1, arg1: arg1}
601
+ end
602
+
603
+ it "must initialize a new command with the Hash of params and call #run" do
604
+ if RUBY_VERSION < '3.'
605
+ expect(subject).to receive(:new).with({},params).and_return(command_instance)
606
+ else
607
+ expect(subject).to receive(:new).with(params).and_return(command_instance)
608
+ end
609
+
610
+ expect(command_instance).to receive(:run_command).and_return(return_value)
611
+
612
+ expect(subject.run(params)).to be(return_value)
613
+ end
614
+ end
615
+
616
+ context "when called with keyword aguments" do
617
+ let(:kwargs) do
618
+ {opt1: opt1, arg1: arg1}
619
+ end
620
+
621
+ it "must initialize a new command with the keyword arguments and call #run" do
622
+ expect(subject).to receive(:new).with({},**kwargs).and_return(command_instance)
623
+ expect(command_instance).to receive(:run_command).and_return(return_value)
624
+
625
+ expect(subject.run(**kwargs)).to be(return_value)
626
+ end
627
+ end
628
+ end
629
+
630
+ describe ".capture" do
631
+ let(:command_instance) { double(:command_instance) }
632
+ let(:return_value) { double(:string) }
633
+
634
+ subject { command_class }
635
+
636
+ context "when called with a Hash of params" do
637
+ let(:params) do
638
+ {opt1: opt1, arg1: arg1}
639
+ end
640
+
641
+ it "must initialize a new command with the Hash of params and call #capture" do
642
+ if RUBY_VERSION < '3.'
643
+ expect(subject).to receive(:new).with({},params).and_return(command_instance)
644
+ else
645
+ expect(subject).to receive(:new).with(params).and_return(command_instance)
646
+ end
647
+
648
+ expect(command_instance).to receive(:capture_command).and_return(return_value)
649
+
650
+ expect(subject.capture(params)).to be(return_value)
651
+ end
652
+ end
653
+
654
+ context "when called with keyword aguments" do
655
+ let(:kwargs) do
656
+ {opt1: opt1, arg1: arg1}
657
+ end
658
+
659
+ it "must initialize a new command with the keyword arguments and call #capture" do
660
+ expect(subject).to receive(:new).with({},**kwargs).and_return(command_instance)
661
+ expect(command_instance).to receive(:capture_command).and_return(return_value)
662
+
663
+ expect(subject.capture(**kwargs)).to be(return_value)
664
+ end
665
+ end
666
+ end
667
+
668
+ describe ".popen" do
669
+ let(:command_instance) { double(:command_instance) }
670
+ let(:return_value) { double(:io) }
671
+
672
+ subject { command_class }
673
+
674
+ context "when called with a Hash of params" do
675
+ let(:params) do
676
+ {opt1: opt1, arg1: arg1}
677
+ end
678
+
679
+ it "must initialize a new command with the Hash of params and call #popen" do
680
+ if RUBY_VERSION < '3.'
681
+ expect(subject).to receive(:new).with({},params).and_return(command_instance)
682
+ else
683
+ expect(subject).to receive(:new).with(params).and_return(command_instance)
684
+ end
685
+
686
+ expect(command_instance).to receive(:popen_command).and_return(return_value)
687
+
688
+ expect(subject.popen(params)).to be(return_value)
689
+ end
690
+ end
691
+
692
+ context "when called with keyword aguments" do
693
+ let(:kwargs) do
694
+ {opt1: opt1, arg1: arg1}
695
+ end
696
+
697
+ it "must initialize a new command with the keyword arguments and call #popen" do
698
+ expect(subject).to receive(:new).with({},**kwargs).and_return(command_instance)
699
+ expect(command_instance).to receive(:popen_command).and_return(return_value)
700
+
701
+ expect(subject.popen(**kwargs)).to be(return_value)
702
+ end
703
+ end
704
+ end
705
+
706
+ describe ".sudo" do
707
+ let(:command_instance) { double(:command_instance) }
708
+ let(:return_value) { double(:boolean) }
709
+
710
+ subject { command_class }
711
+
712
+ context "when called with a Hash of params" do
713
+ let(:params) do
714
+ {opt1: opt1, arg1: arg1}
715
+ end
716
+
717
+ it "must initialize a new command with the Hash of params and call #sudo" do
718
+ if RUBY_VERSION < '3.'
719
+ expect(subject).to receive(:new).with({},params).and_return(command_instance)
720
+ else
721
+ expect(subject).to receive(:new).with(params).and_return(command_instance)
722
+ end
723
+
724
+ expect(command_instance).to receive(:sudo_command).and_return(return_value)
725
+
726
+ expect(subject.sudo(params)).to be(return_value)
727
+ end
728
+ end
729
+
730
+ context "when called with keyword aguments" do
731
+ let(:kwargs) do
732
+ {opt1: opt1, arg1: arg1}
733
+ end
734
+
735
+ it "must initialize a new command with the keyword arguments and call #sudo" do
736
+ expect(subject).to receive(:new).with({},**kwargs).and_return(command_instance)
737
+ expect(command_instance).to receive(:sudo_command).and_return(return_value)
738
+
739
+ expect(subject.sudo(**kwargs)).to be(return_value)
740
+ end
741
+ end
742
+ end
743
+
744
+ describe "#initialize" do
745
+ subject { command_class.new() }
746
+
747
+ it "must default #command_name to self.class.command" do
748
+ expect(subject.command_name).to eq(command_class.command_name)
749
+ end
750
+
751
+ it "must default #command_env to {}" do
752
+ expect(subject.command_env).to eq({})
753
+ end
754
+
755
+ it "must default #command_subcommand to nil" do
756
+ expect(subject.command_subcommand).to be(nil)
757
+ end
758
+
759
+ it "must default option values to nil" do
760
+ expect(subject.opt1).to be(nil)
761
+ expect(subject.opt2).to be(nil)
762
+ expect(subject.opt3).to be(nil)
763
+ end
764
+
765
+ it "must default argument values to nil" do
766
+ expect(subject.arg1).to be(nil)
767
+ expect(subject.arg2).to be(nil)
768
+ expect(subject.arg3).to be(nil)
769
+ end
770
+
771
+ context "when initialized with a Hash of options and arguments" do
772
+ let(:params) do
773
+ {opt1: opt1, opt2: opt2, opt3: opt3, arg1: arg1, arg2: arg2, arg3: arg3}
774
+ end
775
+
776
+ subject { command_class.new(params) }
777
+
778
+ it "must set option values" do
779
+ expect(subject.opt1).to be(opt1)
780
+ expect(subject.opt2).to be(opt2)
781
+ expect(subject.opt3).to be(opt3)
782
+ end
783
+
784
+ it "must set argument values" do
785
+ expect(subject.arg1).to be(arg1)
786
+ expect(subject.arg2).to be(arg2)
787
+ expect(subject.arg3).to be(arg3)
788
+ end
789
+ end
790
+
791
+ context "when initialized with additional keywords" do
792
+ let(:keywords) do
793
+ {opt1: opt1, opt2: opt2, opt3: opt3, arg1: arg1, arg2: arg2, arg3: arg3}
794
+ end
795
+
796
+ subject { command_class.new(**keywords) }
797
+
798
+ it "must set option values" do
799
+ expect(subject.opt1).to be(opt1)
800
+ expect(subject.opt2).to be(opt2)
801
+ expect(subject.opt3).to be(opt3)
802
+ end
803
+
804
+ it "must set argument values" do
805
+ expect(subject.arg1).to be(arg1)
806
+ expect(subject.arg2).to be(arg2)
807
+ expect(subject.arg3).to be(arg3)
808
+ end
809
+ end
810
+
811
+ context "when initialized with command_name: ..." do
812
+ let(:command_name) { 'foo2' }
813
+
814
+ subject { command_class.new(command_name: command_name) }
815
+
816
+ it "must set #command_name" do
817
+ expect(subject.command_name).to eq(command_name)
818
+ end
819
+ end
820
+
821
+ context "when initialized with command_path: ..." do
822
+ let(:command_path) { '/path/to/foo' }
823
+
824
+ subject { command_class.new(command_path: command_path) }
825
+
826
+ it "must set #command_path" do
827
+ expect(subject.command_path).to eq(command_path)
828
+ end
829
+ end
830
+
831
+ context "when initialized with command_env: {...}" do
832
+ subject { command_class.new(command_env: env) }
833
+
834
+ it "must populate #command_env" do
835
+ expect(subject.command_env).to eq(env)
836
+ end
837
+ end
838
+ end
839
+
840
+ describe "#[]" do
841
+ let(:name) { :opt1 }
842
+ let(:value) { 'test' }
843
+
844
+ subject { command_class.new(opt1: value) }
845
+
846
+ it "must call the method with the same given name" do
847
+ expect(subject).to receive(name).and_return(value)
848
+
849
+ expect(subject[name]).to be(value)
850
+ end
851
+
852
+ context "when there is no reader method of the same name" do
853
+ let(:name) { :fubar }
854
+
855
+ it do
856
+ expect {
857
+ subject[name]
858
+ }.to raise_error(ArgumentError,"#{command_class} does not define ##{name}")
859
+ end
860
+ end
861
+ end
862
+
863
+ describe "#[]=" do
864
+ let(:name) { :opt2 }
865
+ let(:value) { 'new_value' }
866
+
867
+ subject { command_class.new }
868
+
869
+ it "must call the writter method with the same given name" do
870
+ expect(subject).to receive(:"#{name}=").with(value).and_return(value)
871
+
872
+ subject[name] = value
873
+ end
874
+
875
+ it "must return the new value" do
876
+ expect(subject[name] = value).to be(value)
877
+ end
878
+
879
+ context "when there is no reader method of the same name" do
880
+ let(:name) { :fubar }
881
+
882
+ it do
883
+ expect {
884
+ subject[name] = value
885
+ }.to raise_error(ArgumentError,"#{command_class} does not define ##{name}=")
886
+ end
887
+ end
888
+ end
889
+
890
+ describe "#command_argv" do
891
+ context "when the command has no options or arguments set" do
892
+ subject { command_class.new }
893
+
894
+ it "must return an argv only containing the command name" do
895
+ expect(subject.command_argv).to eq([subject.class.command_name])
896
+ end
897
+
898
+ context "but the command has required arguments" do
899
+ module TestCommand
900
+ class CommandWithRequiredArguments < CommandMapper::Command
901
+ command "test" do
902
+ option '--opt1', value: {required: true}
903
+ option '--opt2', value: {required: true}
904
+ option '--opt3', value: {required: true}
905
+
906
+ argument :arg1, required: false
907
+ argument :arg2, required: true
908
+ argument :arg3, required: false
909
+ end
910
+ end
911
+ end
912
+
913
+ let(:command_class) { TestCommand::CommandWithRequiredArguments }
914
+
915
+ it do
916
+ expect {
917
+ subject.command_argv
918
+ }.to raise_error(ArgumentRequired,"argument arg2 is required")
919
+ end
920
+ end
921
+ end
922
+
923
+ context "when the command is initialized with the command_path: keyword" do
924
+ let(:command_path) { '/path/to/foo' }
925
+
926
+ subject { command_class.new(command_path: command_path) }
927
+
928
+ it "must override the command name" do
929
+ expect(subject.command_argv).to eq([subject.command_path])
930
+ end
931
+ end
932
+
933
+ context "when the command has options set" do
934
+ subject { command_class.new({opt1: opt1, opt2: opt2, opt3: opt3}) }
935
+
936
+ it "must return an argv containing the command name and option flags followed by values" do
937
+ expect(subject.command_argv).to eq(
938
+ [
939
+ subject.class.command_name,
940
+ '--opt1', opt1,
941
+ '--opt2', opt2,
942
+ '--opt3', opt3
943
+ ]
944
+ )
945
+ end
946
+ end
947
+
948
+ context "when the command has arguments set" do
949
+ subject { command_class.new({arg1: arg1, arg2: arg2, arg3: arg3}) }
950
+
951
+ it "must return an argv containing the command name and argument values" do
952
+ expect(subject.command_argv).to eq(
953
+ [subject.command_name, arg1, arg2, arg3]
954
+ )
955
+ end
956
+
957
+ context "when the arguments are initialized in a different order" do
958
+ subject { command_class.new({arg2: arg2, arg1: arg1, arg3: arg3}) }
959
+
960
+ it "must return the argument values in the order the arguments were defined" do
961
+ expect(subject.command_argv).to eq(
962
+ [subject.command_name, arg1, arg2, arg3]
963
+ )
964
+ end
965
+ end
966
+
967
+ context "and when one of the argument values starts with a '-'" do
968
+ let(:arg2) { "--bar" }
969
+
970
+ it "must separate the arguments with a '--'" do
971
+ expect(subject.command_argv).to eq(
972
+ [subject.command_name, "--", arg1, arg2, arg3]
973
+ )
974
+ end
975
+ end
976
+ end
977
+
978
+ context "when the command has both options and arguments set" do
979
+ subject do
980
+ command_class.new(
981
+ {
982
+ opt1: opt1, opt2: opt2, opt3: opt3,
983
+ arg1: arg1, arg2: arg2, arg3: arg3
984
+ }
985
+ )
986
+ end
987
+
988
+ it "must return an argv containing the command name, options flags and values, then argument values" do
989
+ expect(subject.command_argv).to eq(
990
+ [
991
+ subject.command_name,
992
+ '--opt1', opt1,
993
+ '--opt2', opt2,
994
+ '--opt3', opt3,
995
+ arg1, arg2, arg3
996
+ ]
997
+ )
998
+ end
999
+ end
1000
+
1001
+ context "when the command has a subcommand set" do
1002
+ let(:sub_opt1) { 'foo' }
1003
+ let(:sub_arg1) { 'bar' }
1004
+
1005
+ subject do
1006
+ command_class.new(
1007
+ {
1008
+ subcmd: {sub_opt1: sub_opt1, sub_arg1: sub_arg1}
1009
+ }
1010
+ )
1011
+ end
1012
+
1013
+ it "must return an argv containing the command name, sub-command name, subcommand options and arguments" do
1014
+ expect(subject.command_argv).to eq(
1015
+ [
1016
+ subject.command_name,
1017
+ 'subcmd', '--sub-opt1', sub_opt1, sub_arg1
1018
+ ]
1019
+ )
1020
+ end
1021
+
1022
+ context "and when the command also has options set" do
1023
+ subject do
1024
+ command_class.new(
1025
+ {
1026
+ opt1: opt1, opt2: opt2, opt3: opt3,
1027
+ subcmd: {sub_opt1: sub_opt1, sub_arg1: sub_arg1}
1028
+ }
1029
+ )
1030
+ end
1031
+
1032
+ it "must return an argv containing the command name, global options, sub-command name, subcommand options and arguments" do
1033
+ expect(subject.command_argv).to eq(
1034
+ [
1035
+ subject.command_name,
1036
+ '--opt1', opt1,
1037
+ '--opt2', opt2,
1038
+ '--opt3', opt3,
1039
+ 'subcmd', '--sub-opt1', sub_opt1, sub_arg1
1040
+ ]
1041
+ )
1042
+ end
1043
+ end
1044
+
1045
+ context "and when the command also has arguments set" do
1046
+ subject do
1047
+ command_class.new(
1048
+ {
1049
+ opt1: opt1, opt2: opt2, opt3: opt3,
1050
+ arg1: arg1, arg2: arg2, arg3: arg3,
1051
+ subcmd: {sub_opt1: sub_opt1, sub_arg1: sub_arg1}
1052
+ }
1053
+ )
1054
+ end
1055
+
1056
+ it "must return an argv containing the sub-command's options and arguments, instead of the command's arguments" do
1057
+ expect(subject.command_argv).to eq(
1058
+ [
1059
+ subject.command_name,
1060
+ '--opt1', opt1,
1061
+ '--opt2', opt2,
1062
+ '--opt3', opt3,
1063
+ 'subcmd', '--sub-opt1', sub_opt1, sub_arg1
1064
+ ]
1065
+ )
1066
+ end
1067
+ end
1068
+ end
1069
+ end
1070
+
1071
+ describe "#command_string" do
1072
+ let(:opt1) { "foo bar" }
1073
+ let(:arg1) { "baz qux" }
1074
+
1075
+ subject { command_class.new({opt1: opt1, arg1: arg1}) }
1076
+
1077
+ let(:escaped_command) { Shellwords.shelljoin(subject.command_argv) }
1078
+
1079
+ it "must escape the command option values and argument values" do
1080
+ expect(subject.command_string).to eq(escaped_command)
1081
+ end
1082
+
1083
+ context "when initialized with command_env: {...}" do
1084
+ let(:env) { {"FOO" => "bar baz"} }
1085
+
1086
+ let(:escaped_env) do
1087
+ env.map { |name,value|
1088
+ "#{Shellwords.shellescape(name)}=#{Shellwords.shellescape(value)}"
1089
+ }.join(' ')
1090
+ end
1091
+
1092
+ let(:escaped_command) { Shellwords.shelljoin(subject.command_argv) }
1093
+
1094
+ subject { command_class.new({opt1: opt1, arg1: arg1}, command_env: env) }
1095
+
1096
+ it "must escape both the env variables and the command" do
1097
+ expect(subject.command_string).to eq(
1098
+ "#{escaped_env} #{escaped_command}"
1099
+ )
1100
+ end
1101
+ end
1102
+ end
1103
+
1104
+ describe "#run_command" do
1105
+ subject { command_class.new({opt1: opt1, arg1: arg1}, command_env: env) }
1106
+
1107
+ it "must pass the command's env and argv to Kenrel.system" do
1108
+ expect(subject).to receive(:system).with(env,*subject.command_argv)
1109
+
1110
+ subject.run_command
1111
+ end
1112
+ end
1113
+
1114
+ describe "#capture_command" do
1115
+ subject { command_class.new({opt1: opt1, arg1: arg1}, command_env: env) }
1116
+
1117
+ it "must pass the command's env and argv to `...`" do
1118
+ expect(subject).to receive(:`).with(subject.command_string)
1119
+
1120
+ subject.capture_command
1121
+ end
1122
+ end
1123
+
1124
+ describe "#popen_command" do
1125
+ subject { command_class.new({opt1: opt1, arg1: arg1}, command_env: env) }
1126
+
1127
+ it "must pass the command's env, argv, and to IO.popen" do
1128
+ expect(IO).to receive(:popen).with(env,subject.command_argv)
1129
+
1130
+ subject.popen_command
1131
+ end
1132
+
1133
+ context "when a open mode is given" do
1134
+ let(:mode) { 'w' }
1135
+
1136
+ it "must pass the command's env, argv, and the mode to IO.popen" do
1137
+ expect(IO).to receive(:popen).with(env,subject.command_argv,mode)
1138
+
1139
+ subject.popen_command(mode)
1140
+ end
1141
+ end
1142
+ end
1143
+
1144
+ describe "#sudo!" do
1145
+ subject { command_class.new({opt1: opt1, arg1: arg1}, command_env: env) }
1146
+
1147
+ let(:expected_argv) { [command_class.command, "--opt1", opt1, arg1] }
1148
+
1149
+ it "must pass the command's env and argv, and to IO.popen" do
1150
+ expect(Sudo).to receive(:run).with({command: subject.command_argv}, command_env: env)
1151
+
1152
+ subject.sudo_command
1153
+ end
1154
+ end
1155
+
1156
+ describe "#to_s" do
1157
+ let(:opt1) { "foo bar" }
1158
+ let(:arg1) { "baz qux" }
1159
+ let(:env) { {"FOO" => "bar baz"} }
1160
+
1161
+ subject { command_class.new({opt1: opt1, arg1: arg1}, command_env: env) }
1162
+
1163
+ it "must call #command_string" do
1164
+ expect(subject.to_s).to eq(subject.command_string)
1165
+ end
1166
+ end
1167
+
1168
+ describe "#to_a" do
1169
+ subject { command_class.new({opt1: opt1, arg1: arg1}) }
1170
+
1171
+ it "must call #command_argv" do
1172
+ expect(subject.to_a).to eq(subject.command_argv)
1173
+ end
1174
+ end
1175
+ end