jls-clamp 0.3.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,766 @@
1
+ require 'spec_helper'
2
+
3
+ describe Clamp::Command do
4
+
5
+ extend CommandFactory
6
+ include OutputCapture
7
+
8
+ given_command("cmd") do
9
+
10
+ def execute
11
+ puts "Hello, world"
12
+ end
13
+
14
+ end
15
+
16
+ describe "#help" do
17
+
18
+ it "describes usage" do
19
+ @command.help.should =~ /^Usage:\n cmd.*\n/
20
+ end
21
+
22
+ end
23
+
24
+ describe "#run" do
25
+
26
+ before do
27
+ @command.run([])
28
+ end
29
+
30
+ it "executes the #execute method" do
31
+ stdout.should_not be_empty
32
+ end
33
+
34
+ end
35
+
36
+ describe ".option" do
37
+
38
+ it "declares option argument accessors" do
39
+ @command.class.option "--flavour", "FLAVOUR", "Flavour of the month"
40
+ @command.flavour.should == nil
41
+ @command.flavour = "chocolate"
42
+ @command.flavour.should == "chocolate"
43
+ end
44
+
45
+ describe "with type :flag" do
46
+
47
+ before do
48
+ @command.class.option "--verbose", :flag, "Be heartier"
49
+ end
50
+
51
+ it "declares a predicate-style reader" do
52
+ @command.should respond_to(:verbose?)
53
+ @command.should_not respond_to(:verbose)
54
+ end
55
+
56
+ end
57
+
58
+ describe "with explicit :attribute_name" do
59
+
60
+ before do
61
+ @command.class.option "--foo", "FOO", "A foo", :attribute_name => :bar
62
+ end
63
+
64
+ it "uses the specified attribute_name name to name accessors" do
65
+ @command.bar = "chocolate"
66
+ @command.bar.should == "chocolate"
67
+ end
68
+
69
+ it "does not attempt to create the default accessors" do
70
+ @command.should_not respond_to(:foo)
71
+ @command.should_not respond_to(:foo=)
72
+ end
73
+
74
+ end
75
+
76
+ describe "with default method" do
77
+
78
+ before do
79
+ @command.class.option "--port", "PORT", "port"
80
+ @command.class.class_eval do
81
+ def default_port
82
+ 4321
83
+ end
84
+ end
85
+ end
86
+
87
+ it "sets the specified default value" do
88
+ @command.port.should == 4321
89
+ end
90
+
91
+ end
92
+
93
+ describe "with :default value" do
94
+
95
+ before do
96
+ @command.class.option "--port", "PORT", "port to listen on", :default => 4321
97
+ end
98
+
99
+ it "declares default method" do
100
+ @command.default_port.should == 4321
101
+ end
102
+
103
+ describe "#help" do
104
+
105
+ it "describes the default value" do
106
+ @command.help.should include("port to listen on (default: 4321)")
107
+ end
108
+
109
+ end
110
+
111
+ end
112
+
113
+ describe "with :env value" do
114
+
115
+ before do
116
+ @command.class.option "--port", "PORT", "port to listen on", :default => 4321, :env => "PORT" do |value|
117
+ value.to_i
118
+ end
119
+ end
120
+
121
+ it "should use the default if neither flag nor env var are present" do
122
+ @command.parse([])
123
+ @command.port.should == 4321
124
+ end
125
+
126
+ it "should use the env value if present (instead of default)" do
127
+ ENV["PORT"] = rand(10000).to_s
128
+ @command.parse([])
129
+ @command.port.should == ENV["PORT"].to_i
130
+ end
131
+
132
+ it "should use the the flag value if present (instead of env)" do
133
+ ENV["PORT"] = "12345"
134
+ @command.parse(%w(--port 1500))
135
+ @command.port.should == 1500
136
+ end
137
+
138
+ describe "#help" do
139
+
140
+ it "describes the default value and env usage" do
141
+ @command.help.should include("port to listen on (default: 4321) (env: \"PORT\")")
142
+ end
143
+
144
+ end
145
+
146
+ end
147
+
148
+ describe "with :env value on a :flag option" do
149
+
150
+ before do
151
+ @command.class.option "--[no-]enable", :flag, "enable?", :default => false, :env => "ENABLE"
152
+ end
153
+
154
+ it "should use the default if neither flag nor env var are present" do
155
+ @command.parse([])
156
+ @command.enable?.should == false
157
+ end
158
+
159
+ it "should use an env value of '1' to mean truth" do
160
+ ENV["ENABLE"] = "1"
161
+ @command.parse([])
162
+ @command.enable?.should == true
163
+ end
164
+
165
+ it "should use an env value other than '1' to mean false" do
166
+ ENV["ENABLE"] = "0"
167
+ @command.parse([])
168
+ @command.enable?.should == false
169
+ end
170
+
171
+ it "should use the the flag value if present (instead of env)" do
172
+ ENV["ENABLE"] = "1"
173
+ @command.parse(%w(--no-enable))
174
+ @command.enable?.should == false
175
+ end
176
+
177
+ describe "#help" do
178
+
179
+ it "describes the default value and env usage" do
180
+ @command.help.should include("enable? (default: false) (env: \"ENABLE\")")
181
+ end
182
+
183
+ end
184
+
185
+ end
186
+
187
+ describe "with a block" do
188
+
189
+ before do
190
+ @command.class.option "--port", "PORT", "Port to listen on" do |port|
191
+ Integer(port)
192
+ end
193
+ end
194
+
195
+ it "uses the block to validate and convert the option argument" do
196
+ lambda do
197
+ @command.port = "blah"
198
+ end.should raise_error(ArgumentError)
199
+ @command.port = "1234"
200
+ @command.port.should == 1234
201
+ end
202
+
203
+ end
204
+
205
+ end
206
+
207
+ describe "with options declared" do
208
+
209
+ before do
210
+ @command.class.option ["-f", "--flavour"], "FLAVOUR", "Flavour of the month"
211
+ @command.class.option ["-c", "--color"], "COLOR", "Preferred hue"
212
+ @command.class.option ["-n", "--[no-]nuts"], :flag, "Nuts (or not)\nMay include nuts"
213
+ @command.class.parameter "[ARG] ...", "extra arguments", :attribute_name => :arguments
214
+ end
215
+
216
+ describe "#parse" do
217
+
218
+ describe "with an unrecognised option" do
219
+
220
+ it "raises a UsageError" do
221
+ lambda do
222
+ @command.parse(%w(--foo bar))
223
+ end.should raise_error(Clamp::UsageError)
224
+ end
225
+
226
+ end
227
+
228
+ describe "with options" do
229
+
230
+ before do
231
+ @command.parse(%w(--flavour strawberry --nuts --color blue))
232
+ end
233
+
234
+ it "maps the option values onto the command object" do
235
+ @command.flavour.should == "strawberry"
236
+ @command.color.should == "blue"
237
+ @command.nuts?.should == true
238
+ end
239
+
240
+ end
241
+
242
+ describe "with short options" do
243
+
244
+ before do
245
+ @command.parse(%w(-f strawberry -c blue))
246
+ end
247
+
248
+ it "recognises short options as aliases" do
249
+ @command.flavour.should == "strawberry"
250
+ @command.color.should == "blue"
251
+ end
252
+
253
+ end
254
+
255
+ describe "with combined short options" do
256
+
257
+ before do
258
+ @command.parse(%w(-nf strawberry))
259
+ end
260
+
261
+ it "works as though the options were separate" do
262
+ @command.flavour.should == "strawberry"
263
+ @command.nuts?.should == true
264
+ end
265
+
266
+ end
267
+
268
+ describe "with option arguments attached using equals sign" do
269
+
270
+ before do
271
+ @command.parse(%w(--flavour=strawberry --color=blue))
272
+ end
273
+
274
+ it "works as though the option arguments were separate" do
275
+ @command.flavour.should == "strawberry"
276
+ @command.color.should == "blue"
277
+ end
278
+
279
+ end
280
+
281
+ describe "with option-like things beyond the arguments" do
282
+
283
+ it "treats them as positional arguments" do
284
+ @command.parse(%w(a b c --flavour strawberry))
285
+ @command.arguments.should == %w(a b c --flavour strawberry)
286
+ end
287
+
288
+ end
289
+
290
+ describe "with an option terminator" do
291
+
292
+ it "considers everything after the terminator to be an argument" do
293
+ @command.parse(%w(--color blue -- --flavour strawberry))
294
+ @command.arguments.should == %w(--flavour strawberry)
295
+ end
296
+
297
+ end
298
+
299
+ describe "with --flag" do
300
+
301
+ before do
302
+ @command.parse(%w(--nuts))
303
+ end
304
+
305
+ it "sets the flag" do
306
+ @command.nuts?.should be_true
307
+ end
308
+
309
+ end
310
+
311
+ describe "with --no-flag" do
312
+
313
+ before do
314
+ @command.nuts = true
315
+ @command.parse(%w(--no-nuts))
316
+ end
317
+
318
+ it "clears the flag" do
319
+ @command.nuts?.should be_false
320
+ end
321
+
322
+ end
323
+
324
+ describe "with --help" do
325
+
326
+ it "requests help" do
327
+ lambda do
328
+ @command.parse(%w(--help))
329
+ end.should raise_error(Clamp::HelpWanted)
330
+ end
331
+
332
+ end
333
+
334
+ describe "with -h" do
335
+
336
+ it "requests help" do
337
+ lambda do
338
+ @command.parse(%w(-h))
339
+ end.should raise_error(Clamp::HelpWanted)
340
+ end
341
+
342
+ end
343
+
344
+ describe "when option-writer raises an ArgumentError" do
345
+
346
+ before do
347
+ @command.class.class_eval do
348
+
349
+ def color=(c)
350
+ unless c == "black"
351
+ raise ArgumentError, "sorry, we're out of #{c}"
352
+ end
353
+ end
354
+
355
+ end
356
+ end
357
+
358
+ it "re-raises it as a UsageError" do
359
+ lambda do
360
+ @command.parse(%w(--color red))
361
+ end.should raise_error(Clamp::UsageError, /^option '--color': sorry, we're out of red/)
362
+ end
363
+
364
+ end
365
+
366
+ end
367
+
368
+ describe "#help" do
369
+
370
+ it "indicates that there are options" do
371
+ @command.help.should include("cmd [OPTIONS]")
372
+ end
373
+
374
+ it "includes option details" do
375
+ @command.help.should =~ %r(--flavour FLAVOUR +Flavour of the month)
376
+ @command.help.should =~ %r(--color COLOR +Preferred hue)
377
+ end
378
+
379
+ it "handles new lines in option descriptions" do
380
+ @command.help.should =~ %r(--\[no-\]nuts +Nuts \(or not\)\n +May include nuts)
381
+ end
382
+
383
+ end
384
+
385
+ end
386
+
387
+ describe "with an explicit --help option declared" do
388
+
389
+ before do
390
+ @command.class.option ["--help"], :flag, "help wanted"
391
+ end
392
+
393
+ it "does not generate implicit help option" do
394
+ lambda do
395
+ @command.parse(%w(--help))
396
+ end.should_not raise_error
397
+ @command.help.should be_true
398
+ end
399
+
400
+ it "does not recognise -h" do
401
+ lambda do
402
+ @command.parse(%w(-h))
403
+ end.should raise_error(Clamp::UsageError)
404
+ end
405
+
406
+ end
407
+
408
+ describe "with an explicit -h option declared" do
409
+
410
+ before do
411
+ @command.class.option ["-h", "--humidity"], "PERCENT", "relative humidity" do |n|
412
+ Integer(n)
413
+ end
414
+ end
415
+
416
+ it "does not map -h to help" do
417
+ @command.help.should_not =~ %r( -h[, ].*help)
418
+ end
419
+
420
+ it "still recognises --help" do
421
+ lambda do
422
+ @command.parse(%w(--help))
423
+ end.should raise_error(Clamp::HelpWanted)
424
+ end
425
+
426
+ end
427
+
428
+ describe ".parameter" do
429
+
430
+ it "declares option argument accessors" do
431
+ @command.class.parameter "FLAVOUR", "flavour of the month"
432
+ @command.flavour.should == nil
433
+ @command.flavour = "chocolate"
434
+ @command.flavour.should == "chocolate"
435
+ end
436
+
437
+ describe "with explicit :attribute_name" do
438
+
439
+ before do
440
+ @command.class.parameter "FOO", "a foo", :attribute_name => :bar
441
+ end
442
+
443
+ it "uses the specified attribute_name name to name accessors" do
444
+ @command.bar = "chocolate"
445
+ @command.bar.should == "chocolate"
446
+ end
447
+
448
+ end
449
+
450
+ describe "with :default value" do
451
+
452
+ before do
453
+ @command.class.parameter "[ORIENTATION]", "direction", :default => "west"
454
+ end
455
+
456
+ it "sets the specified default value" do
457
+ @command.orientation.should == "west"
458
+ end
459
+
460
+ describe "#help" do
461
+
462
+ it "describes the default value" do
463
+ @command.help.should include("direction (default: \"west\")")
464
+ end
465
+
466
+ end
467
+
468
+ end
469
+
470
+ describe "with a block" do
471
+
472
+ before do
473
+ @command.class.parameter "PORT", "port to listen on" do |port|
474
+ Integer(port)
475
+ end
476
+ end
477
+
478
+ it "uses the block to validate and convert the argument" do
479
+ lambda do
480
+ @command.port = "blah"
481
+ end.should raise_error(ArgumentError)
482
+ @command.port = "1234"
483
+ @command.port.should == 1234
484
+ end
485
+
486
+ end
487
+
488
+ describe "with ellipsis" do
489
+
490
+ before do
491
+ @command.class.parameter "FILE ...", "files"
492
+ end
493
+
494
+ it "accepts multiple arguments" do
495
+ @command.parse(%w(X Y Z))
496
+ @command.file_list.should == %w(X Y Z)
497
+ end
498
+
499
+ end
500
+
501
+ describe "optional, with ellipsis" do
502
+
503
+ before do
504
+ @command.class.parameter "[FILE] ...", "files"
505
+ end
506
+
507
+ it "default to an empty list" do
508
+ @command.parse([])
509
+ @command.default_file_list.should == []
510
+ @command.file_list.should == []
511
+ end
512
+
513
+ end
514
+
515
+ end
516
+
517
+ describe "with no parameters declared" do
518
+
519
+ describe "#parse" do
520
+
521
+ describe "with arguments" do
522
+
523
+ it "raises a UsageError" do
524
+ lambda do
525
+ @command.parse(["crash"])
526
+ end.should raise_error(Clamp::UsageError, "too many arguments")
527
+ end
528
+
529
+ end
530
+
531
+ end
532
+
533
+ end
534
+
535
+ describe "with parameters declared" do
536
+
537
+ before do
538
+ @command.class.parameter "X", "x\nxx"
539
+ @command.class.parameter "Y", "y"
540
+ @command.class.parameter "[Z]", "z", :default => "ZZZ"
541
+ end
542
+
543
+ describe "#parse" do
544
+
545
+ describe "with arguments for all parameters" do
546
+
547
+ before do
548
+ @command.parse(["crash", "bang", "wallop"])
549
+ end
550
+
551
+ it "maps arguments onto the command object" do
552
+ @command.x.should == "crash"
553
+ @command.y.should == "bang"
554
+ @command.z.should == "wallop"
555
+ end
556
+
557
+ end
558
+
559
+ describe "with insufficient arguments" do
560
+
561
+ it "raises a UsageError" do
562
+ lambda do
563
+ @command.parse(["crash"])
564
+ end.should raise_error(Clamp::UsageError, "parameter 'Y': no value provided")
565
+ end
566
+
567
+ end
568
+
569
+ describe "with optional argument omitted" do
570
+
571
+ it "defaults the optional argument" do
572
+ @command.parse(["crash", "bang"])
573
+ @command.x.should == "crash"
574
+ @command.y.should == "bang"
575
+ @command.z.should == "ZZZ"
576
+ end
577
+
578
+ end
579
+
580
+ describe "with too many arguments" do
581
+
582
+ it "raises a UsageError" do
583
+ lambda do
584
+ @command.parse(["crash", "bang", "wallop", "kapow"])
585
+ end.should raise_error(Clamp::UsageError, "too many arguments")
586
+ end
587
+
588
+ end
589
+
590
+ end
591
+
592
+ describe "#help" do
593
+
594
+ it "indicates that there are parameters" do
595
+ @command.help.should include("cmd [OPTIONS] X Y [Z]")
596
+ end
597
+
598
+ it "includes parameter details" do
599
+ @command.help.should =~ %r(X +x)
600
+ @command.help.should =~ %r(Y +y)
601
+ @command.help.should =~ %r(\[Z\] +z \(default: "ZZZ"\))
602
+ end
603
+
604
+ it "handles new lines in option descriptions" do
605
+ @command.help.should =~ %r(X +x\n +xx)
606
+ end
607
+
608
+ end
609
+
610
+
611
+ end
612
+
613
+ describe "with explicit usage" do
614
+
615
+ given_command("blah") do
616
+
617
+ usage "FOO BAR ..."
618
+
619
+ end
620
+
621
+ describe "#help" do
622
+
623
+ it "includes the explicit usage" do
624
+ @command.help.should include("blah FOO BAR ...\n")
625
+ end
626
+
627
+ end
628
+
629
+ end
630
+
631
+ describe "with multiple usages" do
632
+
633
+ given_command("put") do
634
+
635
+ usage "THIS HERE"
636
+ usage "THAT THERE"
637
+
638
+ end
639
+
640
+ describe "#help" do
641
+
642
+ it "includes both potential usages" do
643
+ @command.help.should include("put THIS HERE\n")
644
+ @command.help.should include("put THAT THERE\n")
645
+ end
646
+
647
+ end
648
+
649
+ end
650
+
651
+ describe "with a description" do
652
+
653
+ given_command("punt") do
654
+
655
+ self.description = <<-EOF
656
+ Punt is an example command. It doesn't do much, really.
657
+
658
+ The prefix at the beginning of this description should be normalised
659
+ to two spaces.
660
+ EOF
661
+
662
+ end
663
+
664
+ describe "#help" do
665
+
666
+ it "includes the description" do
667
+ @command.help.should =~ /^ Punt is an example command/
668
+ @command.help.should =~ /^ The prefix/
669
+ end
670
+
671
+ end
672
+
673
+ end
674
+
675
+ describe ".run" do
676
+
677
+ it "creates a new Command instance and runs it" do
678
+ @command.class.class_eval do
679
+ parameter "WORD ...", "words"
680
+ def execute
681
+ print word_list.inspect
682
+ end
683
+ end
684
+ @xyz = %w(x y z)
685
+ @command.class.run("cmd", @xyz)
686
+ stdout.should == @xyz.inspect
687
+ end
688
+
689
+ describe "invoked with a context hash" do
690
+
691
+ it "makes the context available within the command" do
692
+ @command.class.class_eval do
693
+ def execute
694
+ print context[:foo]
695
+ end
696
+ end
697
+ @command.class.run("xyz", [], :foo => "bar")
698
+ stdout.should == "bar"
699
+ end
700
+
701
+ end
702
+
703
+ describe "when there's a UsageError" do
704
+
705
+ before do
706
+
707
+ @command.class.class_eval do
708
+ def execute
709
+ signal_usage_error "bad dog!"
710
+ end
711
+ end
712
+
713
+ begin
714
+ @command.class.run("cmd", [])
715
+ rescue SystemExit => e
716
+ @system_exit = e
717
+ end
718
+
719
+ end
720
+
721
+ it "outputs the error message" do
722
+ stderr.should include "ERROR: bad dog!"
723
+ end
724
+
725
+ it "outputs help" do
726
+ stderr.should include "See: 'cmd --help'"
727
+ end
728
+
729
+ it "exits with a non-zero status" do
730
+ @system_exit.should_not be_nil
731
+ @system_exit.status.should == 1
732
+ end
733
+
734
+ end
735
+
736
+ describe "when help is requested" do
737
+
738
+ it "outputs help" do
739
+ @command.class.run("cmd", ["--help"])
740
+ stdout.should include "Usage:"
741
+ end
742
+
743
+ end
744
+
745
+ end
746
+
747
+ describe "subclass" do
748
+
749
+ before do
750
+ @parent_command_class = Class.new(Clamp::Command) do
751
+ option "--verbose", :flag, "be louder"
752
+ end
753
+ @derived_command_class = Class.new(@parent_command_class) do
754
+ option "--iterations", "N", "number of times to go around"
755
+ end
756
+ @command = @derived_command_class.new("cmd")
757
+ end
758
+
759
+ it "inherits options from it's superclass" do
760
+ @command.parse(["--verbose"])
761
+ @command.should be_verbose
762
+ end
763
+
764
+ end
765
+
766
+ end