detest 3.1.0

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.
data/CREDITS ADDED
@@ -0,0 +1,21 @@
1
+ %#----------------------------------------------------------------------------
2
+ ## AUTHORS
3
+ %#----------------------------------------------------------------------------
4
+
5
+ Suraj N. Kurapati
6
+
7
+ %#----------------------------------------------------------------------------
8
+ ## CREDITS
9
+ %#----------------------------------------------------------------------------
10
+
11
+ François Beausoleil,
12
+ Gavin Sinclair,
13
+ Iñaki Baz Castillo,
14
+ Sean O'Halpin
15
+
16
+ %#----------------------------------------------------------------------------
17
+ ## LICENSE
18
+ %#----------------------------------------------------------------------------
19
+
20
+ %# See the file named "LICENSE".
21
+ %< "LICENSE"
data/LICENSE ADDED
@@ -0,0 +1,15 @@
1
+ (the ISC license)
2
+
3
+ Copyright 2009 Suraj N. Kurapati <sunaku@gmail.com>
4
+
5
+ Permission to use, copy, modify, and/or distribute this software for any
6
+ purpose with or without fee is hereby granted, provided that the above
7
+ copyright notice and this permission notice appear in all copies.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
10
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
11
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
12
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
13
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
14
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
15
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
@@ -0,0 +1,25 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'detest'
4
+
5
+ if ARGV.delete('-h') or ARGV.delete('--help')
6
+ # try to display UNIX version of help manual
7
+ man_path = File.join(Detest::INSTDIR, 'man')
8
+ unless system 'man', '-M', man_path, '-a', 'detest'
9
+ # try to display HTML version of help manual
10
+ man_html = man_path + '.html'
11
+ unless %w[$BROWSER open start].any? {|b| system "#{b} #{man_html}" }
12
+ # no luck; direct user to project website
13
+ puts "See #{Detest::WEBSITE}"
14
+ end
15
+ end
16
+ exit
17
+ elsif ARGV.delete('-v') or ARGV.delete('--version')
18
+ puts Detest::VERSION
19
+ exit
20
+ end
21
+
22
+ Detest.debug = ARGV.delete('-d') || ARGV.delete('--debug')
23
+
24
+ require 'detest/auto'
25
+ ARGV.each {|glob| Dir[glob].each {|test| load test } }
@@ -0,0 +1,1211 @@
1
+ require 'detest/inochi'
2
+
3
+ require 'yaml'
4
+ #
5
+ # YAML raises this error when we try to serialize a class:
6
+ #
7
+ # TypeError: can't dump anonymous class Class
8
+ #
9
+ # Work around this by representing a class by its name.
10
+ #
11
+ class Class # @private
12
+ alias __to_yaml__ to_yaml
13
+ def to_yaml opts = {}
14
+ begin
15
+ __to_yaml__ opts
16
+ rescue TypeError
17
+ inspect.to_yaml opts
18
+ end
19
+ end
20
+ end
21
+
22
+ module Detest
23
+ class << self
24
+ ##
25
+ # Launch an interactive debugger
26
+ # during assertion failures so
27
+ # the user can investigate them?
28
+ #
29
+ # The default value is $DEBUG.
30
+ #
31
+ attr_accessor :debug
32
+
33
+ ##
34
+ # Hash of counts of major events in test execution:
35
+ #
36
+ # [:time]
37
+ # Number of seconds elapsed for test execution.
38
+ #
39
+ # [:pass]
40
+ # Number of assertions that held true.
41
+ #
42
+ # [:fail]
43
+ # Number of assertions that did not hold true.
44
+ #
45
+ # [:error]
46
+ # Number of exceptions that were not rescued.
47
+ #
48
+ attr_reader :stats
49
+
50
+ ##
51
+ # Hierarchical trace of all tests executed, where each test is
52
+ # represented by its description, is mapped to an Array of
53
+ # nested tests, and may contain zero or more assertion failures.
54
+ #
55
+ # Assertion failures are represented as a Hash:
56
+ #
57
+ # [:fail]
58
+ # Description of the assertion failure.
59
+ #
60
+ # [:call]
61
+ # Stack trace leading to the point of failure.
62
+ #
63
+ # [:code]
64
+ # Source code surrounding the point of failure.
65
+ #
66
+ # [:bind]
67
+ # Location where local variables in `:vars` were extracted.
68
+ #
69
+ # [:vars]
70
+ # Local variables visible at the point of failure.
71
+ #
72
+ attr_reader :trace
73
+
74
+ ##
75
+ # Defines a new test composed of the given
76
+ # description and the given block to execute.
77
+ #
78
+ # This test may contain nested tests.
79
+ #
80
+ # Tests at the outer-most level are automatically
81
+ # insulated from the top-level Ruby environment.
82
+ #
83
+ # @param [Object, Array<Object>] description
84
+ #
85
+ # A brief title or a series of objects
86
+ # that describe the test being defined.
87
+ #
88
+ # @example
89
+ #
90
+ # D "a new array" do
91
+ # D .< { @array = [] }
92
+ #
93
+ # D "must be empty" do
94
+ # T { @array.empty? }
95
+ # end
96
+ #
97
+ # D "when populated" do
98
+ # D .< { @array.push 55 }
99
+ #
100
+ # D "must not be empty" do
101
+ # F { @array.empty? }
102
+ # end
103
+ # end
104
+ # end
105
+ #
106
+ def D *description, &block
107
+ create_test @tests.empty?, *description, &block
108
+ end
109
+
110
+ ##
111
+ # Defines a new test that is explicitly insulated from the tests
112
+ # that contain it and also from the top-level Ruby environment.
113
+ #
114
+ # This test may contain nested tests.
115
+ #
116
+ # @param description (see Detest.D)
117
+ #
118
+ # @example
119
+ #
120
+ # D "a root-level test" do
121
+ # @outside = 1
122
+ # T { defined? @outside }
123
+ # T { @outside == 1 }
124
+ #
125
+ # D "an inner, non-insulated test" do
126
+ # T { defined? @outside }
127
+ # T { @outside == 1 }
128
+ # end
129
+ #
130
+ # D! "an inner, insulated test" do
131
+ # F { defined? @outside }
132
+ # F { @outside == 1 }
133
+ #
134
+ # @inside = 2
135
+ # T { defined? @inside }
136
+ # T { @inside == 2 }
137
+ # end
138
+ #
139
+ # F { defined? @inside }
140
+ # F { @inside == 2 }
141
+ # end
142
+ #
143
+ def D! *description, &block
144
+ create_test true, *description, &block
145
+ end
146
+
147
+ ##
148
+ # @overload def <(&block)
149
+ #
150
+ # Registers the given block to be executed
151
+ # before each nested test inside this test.
152
+ #
153
+ # @example
154
+ #
155
+ # D .< { puts "before each nested test" }
156
+ #
157
+ # D .< do
158
+ # puts "before each nested test"
159
+ # end
160
+ #
161
+ def <(klass = nil, &block)
162
+ if klass
163
+ # this method is being used as a check for inheritance
164
+ #
165
+ # NOTE: we cannot call super() here because this module
166
+ # extends itself, thereby causing an infinite loop!
167
+ #
168
+ ancestors.include? klass
169
+ else
170
+ raise ArgumentError, 'block must be given' unless block
171
+ @suite.before_each << block
172
+ end
173
+ end
174
+
175
+ ##
176
+ # Registers the given block to be executed
177
+ # after each nested test inside this test.
178
+ #
179
+ # @example
180
+ #
181
+ # D .> { puts "after each nested test" }
182
+ #
183
+ # D .> do
184
+ # puts "after each nested test"
185
+ # end
186
+ #
187
+ def > &block
188
+ raise ArgumentError, 'block must be given' unless block
189
+ @suite.after_each << block
190
+ end
191
+
192
+ ##
193
+ # Registers the given block to be executed
194
+ # before all nested tests inside this test.
195
+ #
196
+ # @example
197
+ #
198
+ # D .<< { puts "before all nested tests" }
199
+ #
200
+ # D .<< do
201
+ # puts "before all nested tests"
202
+ # end
203
+ #
204
+ def << &block
205
+ raise ArgumentError, 'block must be given' unless block
206
+ @suite.before_all << block
207
+ end
208
+
209
+ ##
210
+ # Registers the given block to be executed
211
+ # after all nested tests inside this test.
212
+ #
213
+ # @example
214
+ #
215
+ # D .>> { puts "after all nested tests" }
216
+ #
217
+ # D .>> do
218
+ # puts "after all nested tests"
219
+ # end
220
+ #
221
+ def >> &block
222
+ raise ArgumentError, 'block must be given' unless block
223
+ @suite.after_all << block
224
+ end
225
+
226
+ ##
227
+ # Asserts that the given condition or the
228
+ # result of the given block is neither
229
+ # nil nor false and returns that result.
230
+ #
231
+ # @param condition
232
+ #
233
+ # The condition to be asserted. A block
234
+ # may be given in place of this parameter.
235
+ #
236
+ # @param message
237
+ #
238
+ # Optional message to show in the test
239
+ # execution report if this assertion fails.
240
+ #
241
+ # @example no message given
242
+ #
243
+ # T { true } # passes
244
+ # T { false } # fails
245
+ # T { nil } # fails
246
+ #
247
+ # @example message is given
248
+ #
249
+ # T("computers do not doublethink") { 2 + 2 != 5 } # passes
250
+ #
251
+ def T condition = nil, message = nil, &block
252
+ assert_yield :assert, true, condition, message, &block
253
+ end
254
+
255
+ ##
256
+ # Asserts that the given condition or the
257
+ # result of the given block is either nil
258
+ # or false and returns that result.
259
+ #
260
+ # @param condition (see Detest.T)
261
+ #
262
+ # @param message (see Detest.T)
263
+ #
264
+ # @example no message given
265
+ #
266
+ # T! { true } # fails
267
+ # T! { false } # passes
268
+ # T! { nil } # passes
269
+ #
270
+ # @example message is given
271
+ #
272
+ # T!("computers do not doublethink") { 2 + 2 == 5 } # passes
273
+ #
274
+ def T! condition = nil, message = nil, &block
275
+ assert_yield :negate, true, condition, message, &block
276
+ end
277
+
278
+ ##
279
+ # Returns true if the given condition or
280
+ # the result of the given block is neither
281
+ # nil nor false. Otherwise, returns false.
282
+ #
283
+ # @param condition (see Detest.T)
284
+ #
285
+ # @param message
286
+ #
287
+ # This parameter is optional and completely ignored.
288
+ #
289
+ # @example no message given
290
+ #
291
+ # T? { true } # => true
292
+ # T? { false } # => false
293
+ # T? { nil } # => false
294
+ #
295
+ # @example message is given
296
+ #
297
+ # T?("computers do not doublethink") { 2 + 2 != 5 } # => true
298
+ #
299
+ def T? condition = nil, message = nil, &block
300
+ assert_yield :sample, true, condition, message, &block
301
+ end
302
+
303
+ alias F T!
304
+
305
+ alias F! T
306
+
307
+ ##
308
+ # Returns true if the result of the given block is
309
+ # either nil or false. Otherwise, returns false.
310
+ #
311
+ # @param message (see Detest.T?)
312
+ #
313
+ # @example no message given
314
+ #
315
+ # F? { true } # => false
316
+ # F? { false } # => true
317
+ # F? { nil } # => true
318
+ #
319
+ # @example message is given
320
+ #
321
+ # F?( "computers do not doublethink" ) { 2 + 2 == 5 } # => true
322
+ #
323
+ def F? condition = nil, message = nil, &block
324
+ assert_yield :sample, false, condition, message, &block
325
+ end
326
+
327
+ ##
328
+ # Asserts that the given condition or the
329
+ # result of the given block is nil.
330
+ #
331
+ # @param condition
332
+ #
333
+ # The condition to be asserted. A block
334
+ # may be given in place of this parameter.
335
+ #
336
+ # @param message
337
+ #
338
+ # Optional message to show in the test
339
+ # execution report if this assertion fails.
340
+ #
341
+ # @example no message given
342
+ #
343
+ # N { nil } # passes
344
+ # N { false } # fails
345
+ # N { true } # fails
346
+ #
347
+ # @example message is given
348
+ #
349
+ # T("computers do not doublethink") { 2 + 2 != 5 } # passes
350
+ #
351
+ def N condition = nil, message = nil, &block
352
+ assert_yield :assert, nil, condition, message, &block
353
+ end
354
+
355
+ def N! condition = nil, message = nil, &block
356
+ assert_yield :negate, nil, condition, message, &block
357
+ end
358
+
359
+ def N? condition = nil, message = nil, &block
360
+ assert_yield :sample, nil, condition, message, &block
361
+ end
362
+
363
+ ##
364
+ # Asserts that one of the given
365
+ # kinds of exceptions is raised
366
+ # when the given block is executed.
367
+ #
368
+ # @return
369
+ #
370
+ # If the block raises an exception,
371
+ # then that exception is returned.
372
+ #
373
+ # Otherwise, nil is returned.
374
+ #
375
+ # @param [...] kinds_then_message
376
+ #
377
+ # Exception classes that must be raised by the given
378
+ # block, optionally followed by a message to show in
379
+ # the test execution report if this assertion fails.
380
+ #
381
+ # If no exception classes are given, then
382
+ # StandardError is assumed (similar to
383
+ # how a plain 'rescue' statement without
384
+ # any arguments catches StandardError).
385
+ #
386
+ # @example no exceptions given
387
+ #
388
+ # E { } # fails
389
+ # E { raise } # passes
390
+ #
391
+ # @example single exception given
392
+ #
393
+ # E(ArgumentError) { raise ArgumentError }
394
+ # E(ArgumentError, "argument must be invalid") { raise ArgumentError }
395
+ #
396
+ # @example multiple exceptions given
397
+ #
398
+ # E(SyntaxError, NameError) { eval "..." }
399
+ # E(SyntaxError, NameError, "string must compile") { eval "..." }
400
+ #
401
+ def E *kinds_then_message, &block
402
+ assert_raise :assert, *kinds_then_message, &block
403
+ end
404
+
405
+ ##
406
+ # Asserts that one of the given kinds of exceptions
407
+ # is not raised when the given block is executed.
408
+ #
409
+ # @return (see Detest.E)
410
+ #
411
+ # @param kinds_then_message (see Detest.E)
412
+ #
413
+ # @example no exceptions given
414
+ #
415
+ # E! { } # passes
416
+ # E! { raise } # fails
417
+ #
418
+ # @example single exception given
419
+ #
420
+ # E!(ArgumentError) { raise ArgumentError } # fails
421
+ # E!(ArgumentError, "argument must be invalid") { raise ArgumentError }
422
+ #
423
+ # @example multiple exceptions given
424
+ #
425
+ # E!(SyntaxError, NameError) { eval "..." }
426
+ # E!(SyntaxError, NameError, "string must compile") { eval "..." }
427
+ #
428
+ def E! *kinds_then_message, &block
429
+ assert_raise :negate, *kinds_then_message, &block
430
+ end
431
+
432
+ ##
433
+ # Returns true if one of the given kinds of
434
+ # exceptions is raised when the given block
435
+ # is executed. Otherwise, returns false.
436
+ #
437
+ # @param [...] kinds_then_message
438
+ #
439
+ # Exception classes that must be raised by
440
+ # the given block, optionally followed by
441
+ # a message that is completely ignored.
442
+ #
443
+ # If no exception classes are given, then
444
+ # StandardError is assumed (similar to
445
+ # how a plain 'rescue' statement without
446
+ # any arguments catches StandardError).
447
+ #
448
+ # @example no exceptions given
449
+ #
450
+ # E? { } # => false
451
+ # E? { raise } # => true
452
+ #
453
+ # @example single exception given
454
+ #
455
+ # E?(ArgumentError) { raise ArgumentError } # => true
456
+ #
457
+ # @example multiple exceptions given
458
+ #
459
+ # E?(SyntaxError, NameError) { eval "..." } # => true
460
+ # E!(SyntaxError, NameError, "string must compile") { eval "..." }
461
+ #
462
+ def E? *kinds_then_message, &block
463
+ assert_raise :sample, *kinds_then_message, &block
464
+ end
465
+
466
+ ##
467
+ # Asserts that the given symbol is thrown
468
+ # when the given block is executed.
469
+ #
470
+ # @return
471
+ #
472
+ # If a value is thrown along
473
+ # with the expected symbol,
474
+ # then that value is returned.
475
+ #
476
+ # Otherwise, nil is returned.
477
+ #
478
+ # @param [Symbol] symbol
479
+ #
480
+ # Symbol that must be thrown by the given block.
481
+ #
482
+ # @param message (see Detest.T)
483
+ #
484
+ # @example no message given
485
+ #
486
+ # C(:foo) { throw :foo, 123 } # passes, => 123
487
+ # C(:foo) { throw :bar, 456 } # fails, => 456
488
+ # C(:foo) { } # fails, => nil
489
+ #
490
+ # @example message is given
491
+ #
492
+ # C(:foo, ":foo must be thrown") { throw :bar, 789 } # fails, => nil
493
+ #
494
+ def C symbol, message = nil, &block
495
+ assert_catch :assert, symbol, message, &block
496
+ end
497
+
498
+ ##
499
+ # Asserts that the given symbol is not
500
+ # thrown when the given block is executed.
501
+ #
502
+ # @return nil, always.
503
+ #
504
+ # @param [Symbol] symbol
505
+ #
506
+ # Symbol that must not be thrown by the given block.
507
+ #
508
+ # @param message (see Detest.T)
509
+ #
510
+ # @example no message given
511
+ #
512
+ # C!(:foo) { throw :foo, 123 } # fails, => nil
513
+ # C!(:foo) { throw :bar, 456 } # passes, => nil
514
+ # C!(:foo) { } # passes, => nil
515
+ #
516
+ # @example message is given
517
+ #
518
+ # C!(:foo, ":foo must be thrown") { throw :bar, 789 } # passes, => nil
519
+ #
520
+ def C! symbol, message = nil, &block
521
+ assert_catch :negate, symbol, message, &block
522
+ end
523
+
524
+ ##
525
+ # Returns true if the given symbol is thrown when the
526
+ # given block is executed. Otherwise, returns false.
527
+ #
528
+ # @param symbol (see Detest.C)
529
+ #
530
+ # @param message (see Detest.T?)
531
+ #
532
+ # @example no message given
533
+ #
534
+ # C?(:foo) { throw :foo, 123 } # => true
535
+ # C?(:foo) { throw :bar, 456 } # => false
536
+ # C?(:foo) { } # => false
537
+ #
538
+ # @example message is given
539
+ #
540
+ # C?(:foo, ":foo must be thrown") { throw :bar, 789 } # => false
541
+ #
542
+ def C? symbol, message = nil, &block
543
+ assert_catch :sample, symbol, message, &block
544
+ end
545
+
546
+ ##
547
+ # Adds the given messages to the test execution
548
+ # report beneath the currently running test.
549
+ #
550
+ # You can think of "I" as to "inform" the user.
551
+ #
552
+ # @param messages
553
+ #
554
+ # Objects to be added to the test execution report.
555
+ #
556
+ # @example single message given
557
+ #
558
+ # I "establishing connection..."
559
+ #
560
+ # @example multiple messages given
561
+ #
562
+ # I "beginning calculation...", Math::PI, [1, 2, 3, ['a', 'b', 'c']]
563
+ #
564
+ def I *messages
565
+ @trace.concat messages
566
+ end
567
+
568
+ ##
569
+ # Starts an interactive debugging session at
570
+ # the location where this method was called.
571
+ #
572
+ # You can think of "I!" as to "investigate" the program.
573
+ #
574
+ def I!
575
+ debug
576
+ end
577
+
578
+ ##
579
+ # Mechanism for sharing code between tests.
580
+ #
581
+ # If a block is given, it is shared under
582
+ # the given identifier. Otherwise, the
583
+ # code block that was previously shared
584
+ # under the given identifier is injected
585
+ # into the closest insulated Detest test
586
+ # that contains the call to this method.
587
+ #
588
+ # @param [Symbol, Object] identifier
589
+ #
590
+ # An object that identifies shared code. This must be common
591
+ # knowledge to all parties that want to partake in the sharing.
592
+ #
593
+ # @example
594
+ #
595
+ # S :knowledge do
596
+ # #...
597
+ # end
598
+ #
599
+ # D "some test" do
600
+ # S :knowledge
601
+ # end
602
+ #
603
+ # D "another test" do
604
+ # S :knowledge
605
+ # end
606
+ #
607
+ def S identifier, &block
608
+ if block_given?
609
+ if already_shared = @share[identifier]
610
+ raise ArgumentError, "A code block #{already_shared.inspect} has "\
611
+ "already been shared under the identifier #{identifier.inspect}."
612
+ end
613
+
614
+ @share[identifier] = block
615
+
616
+ elsif block = @share[identifier]
617
+ if @tests.empty?
618
+ raise "Cannot inject code block #{block.inspect} shared under "\
619
+ "identifier #{identifier.inspect} outside of a Detest test."
620
+ else
621
+ # find the closest insulated parent test; this should always
622
+ # succeed because root-level tests are insulated by default
623
+ test = @tests.reverse.find {|t| t.sandbox } or raise IndexError
624
+ test.sandbox.instance_eval(&block)
625
+ end
626
+
627
+ else
628
+ raise ArgumentError, "No code block is shared under identifier "\
629
+ "#{identifier.inspect}."
630
+ end
631
+ end
632
+
633
+ ##
634
+ # Shares the given code block under the given
635
+ # identifier and then immediately injects that
636
+ # code block into the closest insulated Detest
637
+ # test that contains the call to this method.
638
+ #
639
+ # @param identifier (see Detest.S)
640
+ #
641
+ # @example
642
+ #
643
+ # D "some test" do
644
+ # S! :knowledge do
645
+ # #...
646
+ # end
647
+ # end
648
+ #
649
+ # D "another test" do
650
+ # S :knowledge
651
+ # end
652
+ #
653
+ def S! identifier, &block
654
+ raise 'block must be given' unless block_given?
655
+ S identifier, &block
656
+ S identifier
657
+ end
658
+
659
+ ##
660
+ # Checks whether any code has been shared under the given identifier.
661
+ #
662
+ def S? identifier
663
+ @share.key? identifier
664
+ end
665
+
666
+ ##
667
+ # Executes all tests defined thus far and stores
668
+ # the results in {Detest.trace} and {Detest.stats}.
669
+ #
670
+ def start
671
+ # execute the tests
672
+ start_time = Time.now
673
+ catch :DIFECTS_STOP do
674
+ BINDINGS.track do
675
+ execute
676
+ end
677
+ end
678
+ @stats[:time] = Time.now - start_time
679
+
680
+ # print test results
681
+ unless @stats.key? :fail or @stats.key? :error
682
+ #
683
+ # show execution trace only if all tests passed.
684
+ # otherwise, we will be repeating already printed
685
+ # failure details and obstructing the developer!
686
+ #
687
+ display @trace
688
+ end
689
+
690
+ display @stats
691
+
692
+ ensure
693
+ @tests.clear
694
+ @share.clear
695
+ @files.clear
696
+ end
697
+
698
+ ##
699
+ # Stops the execution of the {Detest.start} method or raises
700
+ # an exception if that method is not currently executing.
701
+ #
702
+ def stop
703
+ throw :DIFECTS_STOP
704
+ end
705
+
706
+ ##
707
+ # Clear all test results that were recorded thus far.
708
+ #
709
+ def reset
710
+ @stats.clear
711
+ @trace.clear
712
+ end
713
+
714
+ ##
715
+ # Returns the details of the failure that
716
+ # is currently being debugged by the user.
717
+ #
718
+ def info
719
+ @trace.last
720
+ end
721
+
722
+ private
723
+
724
+ def create_test insulate, *description, &block
725
+ raise ArgumentError, 'block must be given' unless block
726
+
727
+ description = description.join(' ')
728
+ sandbox = Object.new if insulate
729
+
730
+ @suite.tests << Suite::Test.new(description, block, sandbox)
731
+ end
732
+
733
+ def assert_yield mode, expect, condition = nil, message = nil, &block
734
+ # first parameter is actually the message when block is given
735
+ message = condition if block
736
+
737
+ message ||= [
738
+ (block ? 'block must yield' : 'condition must be'),
739
+ case expect
740
+ when nil then 'nil'
741
+ when true then 'true (!nil && !false)'
742
+ when false then 'false (nil || false)'
743
+ end
744
+ ].join(' ')
745
+
746
+ passed = lambda do
747
+ @stats[:pass] += 1
748
+ end
749
+
750
+ failed = lambda do
751
+ @stats[:fail] += 1
752
+ debug message
753
+ end
754
+
755
+ result = block ? call(block) : condition
756
+
757
+ verdict =
758
+ case expect
759
+ when nil then result == nil
760
+ when true then result != nil && result != false
761
+ when false then result == nil || result == false
762
+ end
763
+
764
+ case mode
765
+ when :sample then return verdict
766
+ when :assert then verdict ? passed.call : failed.call
767
+ when :negate then verdict ? failed.call : passed.call
768
+ end
769
+
770
+ result
771
+ end
772
+
773
+ def assert_raise mode, *kinds_then_message, &block
774
+ raise ArgumentError, 'block must be given' unless block
775
+
776
+ message = kinds_then_message.pop
777
+ kinds = kinds_then_message
778
+
779
+ if message.kind_of? Class
780
+ kinds << message
781
+ message = nil
782
+ end
783
+
784
+ kinds << StandardError if kinds.empty?
785
+
786
+ message ||=
787
+ case mode
788
+ when :assert then "block must raise #{kinds.join ' or '}"
789
+ when :negate then "block must not raise #{kinds.join ' or '}"
790
+ end
791
+
792
+ passed = lambda do
793
+ @stats[:pass] += 1
794
+ end
795
+
796
+ failed = lambda do |exception|
797
+ @stats[:fail] += 1
798
+
799
+ if exception
800
+ # debug the uncaught exception...
801
+ debug_uncaught_exception exception
802
+
803
+ # ...in addition to debugging this assertion
804
+ debug [message, {'block raised' => exception}]
805
+
806
+ else
807
+ debug message
808
+ end
809
+ end
810
+
811
+ begin
812
+ block.call
813
+
814
+ rescue Exception => exception
815
+ expected = kinds.any? {|k| exception.kind_of? k }
816
+
817
+ case mode
818
+ when :sample then return expected
819
+ when :assert then expected ? passed.call : failed.call(exception)
820
+ when :negate then expected ? failed.call(exception) : passed.call
821
+ end
822
+
823
+ else # nothing was raised
824
+ case mode
825
+ when :sample then return false
826
+ when :assert then failed.call nil
827
+ when :negate then passed.call
828
+ end
829
+ end
830
+
831
+ exception
832
+ end
833
+
834
+ def assert_catch mode, symbol, message = nil, &block
835
+ raise ArgumentError, 'block must be given' unless block
836
+
837
+ symbol = symbol.to_sym
838
+ message ||= "block must throw #{symbol.inspect}"
839
+
840
+ passed = lambda do
841
+ @stats[:pass] += 1
842
+ end
843
+
844
+ failed = lambda do
845
+ @stats[:fail] += 1
846
+ debug message
847
+ end
848
+
849
+ # if nothing was thrown, the result of catch()
850
+ # is simply the result of executing the block
851
+ result = catch(symbol) do
852
+ begin
853
+ block.call
854
+ rescue Exception => e
855
+ #
856
+ # ignore error about the wrong symbol being thrown
857
+ #
858
+ # NOTE: Ruby 1.8 formats the thrown value in `quotes'
859
+ # whereas Ruby 1.9 formats it like a :symbol
860
+ #
861
+ unless e.message =~ /\Auncaught throw (`.*?'|:.*)\z/
862
+ debug_uncaught_exception e
863
+ end
864
+ end
865
+
866
+ self # unlikely that block will throw *this* object
867
+ end
868
+
869
+ caught = result != self
870
+ result = nil unless caught
871
+
872
+ case mode
873
+ when :sample then return caught
874
+ when :assert then caught ? passed.call : failed.call
875
+ when :negate then caught ? failed.call : passed.call
876
+ end
877
+
878
+ result
879
+ end
880
+
881
+ ##
882
+ # Prints the given object in YAML
883
+ # format if possible, or falls back
884
+ # to Ruby's pretty print library.
885
+ #
886
+ def display object
887
+ # stringify symbols in YAML output for better readability
888
+ puts object.to_yaml.gsub(/^([[:blank:]]*(- )?):(?=@?\w+: )/, '\1')
889
+ rescue
890
+ require 'pp'
891
+ pp object
892
+ end
893
+
894
+ ##
895
+ # Executes the current test suite recursively.
896
+ #
897
+ def execute
898
+ suite = @suite
899
+ trace = @trace
900
+
901
+ suite.before_all.each {|b| call b }
902
+
903
+ suite.tests.each do |test|
904
+ suite.before_each.each {|b| call b }
905
+
906
+ @tests.push test
907
+
908
+ begin
909
+ # create nested suite
910
+ @suite = Suite.new
911
+ @trace = []
912
+
913
+ # populate nested suite
914
+ call test.block, test.sandbox
915
+
916
+ # execute nested suite
917
+ execute
918
+
919
+ ensure
920
+ # restore outer values
921
+ @suite = suite
922
+
923
+ trace << build_exec_trace(@trace)
924
+ @trace = trace
925
+ end
926
+
927
+ @tests.pop
928
+
929
+ suite.after_each.each {|b| call b }
930
+ end
931
+
932
+ suite.after_all.each {|b| call b }
933
+ end
934
+
935
+ ##
936
+ # Invokes the given block and debugs any
937
+ # exceptions that may arise as a result.
938
+ #
939
+ def call block, sandbox = nil
940
+ if sandbox
941
+ sandbox.instance_eval(&block)
942
+ else
943
+ block.call
944
+ end
945
+
946
+ rescue Exception => e
947
+ debug_uncaught_exception e
948
+ end
949
+
950
+ INTERNALS = /^#{Regexp.escape(File.dirname(__FILE__))}/ # @private
951
+
952
+ class << BINDINGS = Hash.new {|h,k| h[k] = {} } # @private
953
+ ##
954
+ # Keeps track of bindings for all
955
+ # lines of code processed by Ruby
956
+ # for use later in {Detest.debug}.
957
+ #
958
+ def track
959
+ raise ArgumentError unless block_given?
960
+
961
+ set_trace_func lambda {|event, file, line, id, binding, klass|
962
+ unless file =~ INTERNALS
963
+ self[file][line] = binding
964
+ end
965
+ }
966
+
967
+ yield
968
+
969
+ ensure
970
+ set_trace_func nil
971
+ clear
972
+ end
973
+ end
974
+
975
+ ##
976
+ # Adds debugging information to the test execution report and
977
+ # invokes the debugger if the {Detest.debug} option is enabled.
978
+ #
979
+ # @param message
980
+ #
981
+ # Message describing the failure
982
+ # in the code being debugged.
983
+ #
984
+ # @param [Array<String>] backtrace
985
+ #
986
+ # Stack trace corresponding to point of
987
+ # failure in the code being debugged.
988
+ #
989
+ def debug message = nil, backtrace = caller
990
+ # omit internals from failure details
991
+ backtrace = backtrace.reject {|s| s =~ INTERNALS }
992
+
993
+ backtrace.first =~ /(.+?):(\d+(?=:|\z))/ or raise SyntaxError
994
+ source_file, source_line = $1, $2.to_i
995
+
996
+ binding_by_line = BINDINGS[source_file]
997
+ binding_line =
998
+ if binding_by_line.key? source_line
999
+ source_line
1000
+ else
1001
+ #
1002
+ # There is no binding for the line number given in the backtrace, so
1003
+ # try to adjust it to the nearest line that actually has a binding.
1004
+ #
1005
+ # This problem occurs because line numbers reported in backtraces
1006
+ # sometimes do not agree with those observed by set_trace_func(),
1007
+ # particularly in method calls that span multiple lines:
1008
+ # set_trace_func() will consistently observe the ending parenthesis
1009
+ # (last line of the method call) whereas the backtrace will oddly
1010
+ # report a line somewhere in the middle of the method call.
1011
+ #
1012
+ # NOTE: I chose to adjust the imprecise line to the nearest one
1013
+ # BELOW it. This might not always be correct because the nearest
1014
+ # line below could belong to a different scope, like a new class.
1015
+ #
1016
+ binding_by_line.keys.sort.find {|n| n > source_line }
1017
+ end
1018
+ binding = binding_by_line[binding_line]
1019
+
1020
+ # record failure details in the test execution report
1021
+ details = Hash[
1022
+ # user message
1023
+ :fail, message,
1024
+
1025
+ # stack trace
1026
+ :call, backtrace,
1027
+
1028
+ # code snippet
1029
+ :code, (
1030
+ if source = @files[source_file]
1031
+ radius = 5 # number of surrounding lines to show
1032
+ region = [source_line - radius, 1].max ..
1033
+ [source_line + radius, source.length].min
1034
+
1035
+ # ensure proper alignment by zero-padding line numbers
1036
+ format = "%2s %0#{region.last.to_s.length}d %s"
1037
+
1038
+ pretty = region.map do |n|
1039
+ format % [('=>' if n == source_line), n, source[n-1].chomp]
1040
+ end.unshift "[#{region.inspect}] in #{source_file}"
1041
+
1042
+ pretty.extend FailureDetails::CodeListing
1043
+ end
1044
+ )
1045
+ ]
1046
+
1047
+ if binding
1048
+ # binding location
1049
+ details[:bind] = [source_file, binding_line].join(':')
1050
+
1051
+ # variable values
1052
+ variables = eval('::Kernel.local_variables', binding, __FILE__, __LINE__)
1053
+
1054
+ details[:vars] = variables.inject(Hash.new) do |hash, variable|
1055
+ hash[variable.to_sym] = eval(variable.to_s, binding, __FILE__, __LINE__)
1056
+ hash
1057
+ end.extend(FailureDetails::VariablesListing)
1058
+ end
1059
+
1060
+ details.reject! {|key, value| (value || details[key]).nil? }
1061
+ @trace << details
1062
+
1063
+ # show all failure details to the user
1064
+ display build_fail_trace(details)
1065
+
1066
+ # allow user to investigate the failure
1067
+ if @debug and binding
1068
+ unless defined? IRB
1069
+ require 'irb'
1070
+ IRB.setup nil
1071
+ end
1072
+
1073
+ irb = IRB::Irb.new(IRB::WorkSpace.new(binding))
1074
+ IRB.conf[:MAIN_CONTEXT] = irb.context
1075
+ catch(:IRB_EXIT) { irb.eval_input }
1076
+ end
1077
+
1078
+ nil
1079
+ end
1080
+
1081
+ ##
1082
+ # Debugs the given uncaught exception inside the given context.
1083
+ #
1084
+ def debug_uncaught_exception exception
1085
+ @stats[:error] += 1
1086
+ debug exception, exception.backtrace
1087
+ end
1088
+
1089
+ ##
1090
+ # Returns a report that associates the given
1091
+ # failure details with the currently running test.
1092
+ #
1093
+ def build_exec_trace details
1094
+ if @tests.empty?
1095
+ details
1096
+ else
1097
+ { @tests.last.desc => (details unless details.empty?) }
1098
+ end
1099
+ end
1100
+
1101
+ ##
1102
+ # Returns a report that qualifies the given
1103
+ # failure details with the current test stack.
1104
+ #
1105
+ def build_fail_trace details
1106
+ @tests.reverse.inject(details) do |inner, outer|
1107
+ { outer.desc => inner }
1108
+ end
1109
+ end
1110
+
1111
+ ##
1112
+ # Logic to pretty print the details of an assertion failure.
1113
+ #
1114
+ module FailureDetails # @private
1115
+ module CodeListing
1116
+ def to_yaml options = {}
1117
+ #
1118
+ # strip because to_yaml() will render the paragraph without escaping
1119
+ # newlines ONLY IF the first and last character are non-whitespace!
1120
+ #
1121
+ join("\n").strip.to_yaml(options)
1122
+ end
1123
+
1124
+ def pretty_print printer
1125
+ margin = ' ' * printer.indent
1126
+ printer.text [
1127
+ first, self[1..-1].map {|line| margin + line }, margin
1128
+ ].join(printer.newline)
1129
+ end
1130
+ end
1131
+
1132
+ module VariablesListing
1133
+ def to_yaml options = {}
1134
+ require 'pp'
1135
+ require 'stringio'
1136
+
1137
+ inject(Hash.new) do |hash, (variable, value)|
1138
+ pretty = PP.pp(value, StringIO.new).string.chomp
1139
+ hash[variable] = "(#{value.class}) #{pretty}"
1140
+ hash
1141
+ end.to_yaml(options)
1142
+ end
1143
+ end
1144
+ end
1145
+
1146
+ class Suite # @private
1147
+ attr_reader :tests, :before_each, :after_each, :before_all, :after_all
1148
+
1149
+ def initialize
1150
+ @tests = []
1151
+ @before_each = []
1152
+ @after_each = []
1153
+ @before_all = []
1154
+ @after_all = []
1155
+ end
1156
+
1157
+ Test = Struct.new(:desc, :block, :sandbox) # @private
1158
+ end
1159
+ end
1160
+
1161
+ # provide mixin-able versions of Detest's core vocabulary
1162
+ singleton_methods(false).grep(/^[[:upper:]]?[[:punct:]]*$/).each do |meth|
1163
+ #
1164
+ # XXX: using eval() on a string because Ruby 1.8's
1165
+ # define_method() cannot take a block parameter
1166
+ #
1167
+ file, line = __FILE__, __LINE__ ; module_eval %{
1168
+ def #{meth}(*args, &block)
1169
+ ::#{name}.#{meth}(*args, &block)
1170
+ end
1171
+ }, file, line
1172
+ end
1173
+
1174
+ # allow mixin-able methods to be accessed as class methods
1175
+ extend self
1176
+
1177
+ # allow before and after hooks to be specified via the
1178
+ # following method syntax when this module is mixed-in:
1179
+ #
1180
+ # D .<< { puts "before all nested tests" }
1181
+ # D .< { puts "before each nested test" }
1182
+ # D .> { puts "after each nested test" }
1183
+ # D .>> { puts "after all nested tests" }
1184
+ #
1185
+ D = self
1186
+
1187
+ # set Detest::Hash from an ordered hash library in lesser Ruby versions
1188
+ if RUBY_VERSION < '1.9'
1189
+ begin
1190
+ #
1191
+ # NOTE: I realize that there are other libraries, such as facets and
1192
+ # activesupport, that provide an ordered hash implementation, but this
1193
+ # particular library does not interfere with pretty printing routines.
1194
+ #
1195
+ require 'orderedhash'
1196
+ Hash = OrderedHash
1197
+ rescue LoadError
1198
+ warn "#{inspect}: Install 'orderedhash' gem for better failure reports."
1199
+ end
1200
+ end
1201
+
1202
+ @debug = $DEBUG
1203
+
1204
+ @stats = Hash.new {|h,k| h[k] = 0 }
1205
+ @trace = []
1206
+
1207
+ @suite = class << self; Suite.new; end
1208
+ @share = {}
1209
+ @tests = []
1210
+ @files = Hash.new {|h,k| h[k] = File.readlines(k) rescue nil }
1211
+ end