difects 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/lib/difects.rb ADDED
@@ -0,0 +1,1169 @@
1
+ require 'difects/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 DIFECTS
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 DIFECTS.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, 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 DIFECTS.T)
261
+ #
262
+ # @param message (see DIFECTS.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, 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 DIFECTS.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, 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 DIFECTS.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? message = nil, &block
324
+ not T? message, &block
325
+ end
326
+
327
+ ##
328
+ # Asserts that one of the given
329
+ # kinds of exceptions is raised
330
+ # when the given block is executed.
331
+ #
332
+ # @return
333
+ #
334
+ # If the block raises an exception,
335
+ # then that exception is returned.
336
+ #
337
+ # Otherwise, nil is returned.
338
+ #
339
+ # @param [...] kinds_then_message
340
+ #
341
+ # Exception classes that must be raised by the given
342
+ # block, optionally followed by a message to show in
343
+ # the test execution report if this assertion fails.
344
+ #
345
+ # If no exception classes are given, then
346
+ # StandardError is assumed (similar to
347
+ # how a plain 'rescue' statement without
348
+ # any arguments catches StandardError).
349
+ #
350
+ # @example no exceptions given
351
+ #
352
+ # E { } # fails
353
+ # E { raise } # passes
354
+ #
355
+ # @example single exception given
356
+ #
357
+ # E(ArgumentError) { raise ArgumentError }
358
+ # E(ArgumentError, "argument must be invalid") { raise ArgumentError }
359
+ #
360
+ # @example multiple exceptions given
361
+ #
362
+ # E(SyntaxError, NameError) { eval "..." }
363
+ # E(SyntaxError, NameError, "string must compile") { eval "..." }
364
+ #
365
+ def E *kinds_then_message, &block
366
+ assert_raise :assert, *kinds_then_message, &block
367
+ end
368
+
369
+ ##
370
+ # Asserts that one of the given kinds of exceptions
371
+ # is not raised when the given block is executed.
372
+ #
373
+ # @return (see DIFECTS.E)
374
+ #
375
+ # @param kinds_then_message (see DIFECTS.E)
376
+ #
377
+ # @example no exceptions given
378
+ #
379
+ # E! { } # passes
380
+ # E! { raise } # fails
381
+ #
382
+ # @example single exception given
383
+ #
384
+ # E!(ArgumentError) { raise ArgumentError } # fails
385
+ # E!(ArgumentError, "argument must be invalid") { raise ArgumentError }
386
+ #
387
+ # @example multiple exceptions given
388
+ #
389
+ # E!(SyntaxError, NameError) { eval "..." }
390
+ # E!(SyntaxError, NameError, "string must compile") { eval "..." }
391
+ #
392
+ def E! *kinds_then_message, &block
393
+ assert_raise :negate, *kinds_then_message, &block
394
+ end
395
+
396
+ ##
397
+ # Returns true if one of the given kinds of
398
+ # exceptions is raised when the given block
399
+ # is executed. Otherwise, returns false.
400
+ #
401
+ # @param [...] kinds_then_message
402
+ #
403
+ # Exception classes that must be raised by
404
+ # the given block, optionally followed by
405
+ # a message that is completely ignored.
406
+ #
407
+ # If no exception classes are given, then
408
+ # StandardError is assumed (similar to
409
+ # how a plain 'rescue' statement without
410
+ # any arguments catches StandardError).
411
+ #
412
+ # @example no exceptions given
413
+ #
414
+ # E? { } # => false
415
+ # E? { raise } # => true
416
+ #
417
+ # @example single exception given
418
+ #
419
+ # E?(ArgumentError) { raise ArgumentError } # => true
420
+ #
421
+ # @example multiple exceptions given
422
+ #
423
+ # E?(SyntaxError, NameError) { eval "..." } # => true
424
+ # E!(SyntaxError, NameError, "string must compile") { eval "..." }
425
+ #
426
+ def E? *kinds_then_message, &block
427
+ assert_raise :sample, *kinds_then_message, &block
428
+ end
429
+
430
+ ##
431
+ # Asserts that the given symbol is thrown
432
+ # when the given block is executed.
433
+ #
434
+ # @return
435
+ #
436
+ # If a value is thrown along
437
+ # with the expected symbol,
438
+ # then that value is returned.
439
+ #
440
+ # Otherwise, nil is returned.
441
+ #
442
+ # @param [Symbol] symbol
443
+ #
444
+ # Symbol that must be thrown by the given block.
445
+ #
446
+ # @param message (see DIFECTS.T)
447
+ #
448
+ # @example no message given
449
+ #
450
+ # C(:foo) { throw :foo, 123 } # passes, => 123
451
+ # C(:foo) { throw :bar, 456 } # fails, => 456
452
+ # C(:foo) { } # fails, => nil
453
+ #
454
+ # @example message is given
455
+ #
456
+ # C(:foo, ":foo must be thrown") { throw :bar, 789 } # fails, => nil
457
+ #
458
+ def C symbol, message = nil, &block
459
+ assert_catch :assert, symbol, message, &block
460
+ end
461
+
462
+ ##
463
+ # Asserts that the given symbol is not
464
+ # thrown when the given block is executed.
465
+ #
466
+ # @return nil, always.
467
+ #
468
+ # @param [Symbol] symbol
469
+ #
470
+ # Symbol that must not be thrown by the given block.
471
+ #
472
+ # @param message (see DIFECTS.T)
473
+ #
474
+ # @example no message given
475
+ #
476
+ # C!(:foo) { throw :foo, 123 } # fails, => nil
477
+ # C!(:foo) { throw :bar, 456 } # passes, => nil
478
+ # C!(:foo) { } # passes, => nil
479
+ #
480
+ # @example message is given
481
+ #
482
+ # C!(:foo, ":foo must be thrown") { throw :bar, 789 } # passes, => nil
483
+ #
484
+ def C! symbol, message = nil, &block
485
+ assert_catch :negate, symbol, message, &block
486
+ end
487
+
488
+ ##
489
+ # Returns true if the given symbol is thrown when the
490
+ # given block is executed. Otherwise, returns false.
491
+ #
492
+ # @param symbol (see DIFECTS.C)
493
+ #
494
+ # @param message (see DIFECTS.T?)
495
+ #
496
+ # @example no message given
497
+ #
498
+ # C?(:foo) { throw :foo, 123 } # => true
499
+ # C?(:foo) { throw :bar, 456 } # => false
500
+ # C?(:foo) { } # => false
501
+ #
502
+ # @example message is given
503
+ #
504
+ # C?(:foo, ":foo must be thrown") { throw :bar, 789 } # => false
505
+ #
506
+ def C? symbol, message = nil, &block
507
+ assert_catch :sample, symbol, message, &block
508
+ end
509
+
510
+ ##
511
+ # Adds the given messages to the test execution
512
+ # report beneath the currently running test.
513
+ #
514
+ # You can think of "I" as to "inform" the user.
515
+ #
516
+ # @param messages
517
+ #
518
+ # Objects to be added to the test execution report.
519
+ #
520
+ # @example single message given
521
+ #
522
+ # I "establishing connection..."
523
+ #
524
+ # @example multiple messages given
525
+ #
526
+ # I "beginning calculation...", Math::PI, [1, 2, 3, ['a', 'b', 'c']]
527
+ #
528
+ def I *messages
529
+ @trace.concat messages
530
+ end
531
+
532
+ ##
533
+ # Starts an interactive debugging session at
534
+ # the location where this method was called.
535
+ #
536
+ # You can think of "I!" as to "investigate" the program.
537
+ #
538
+ def I!
539
+ debug
540
+ end
541
+
542
+ ##
543
+ # Mechanism for sharing code between tests.
544
+ #
545
+ # If a block is given, it is shared under
546
+ # the given identifier. Otherwise, the
547
+ # code block that was previously shared
548
+ # under the given identifier is injected
549
+ # into the closest insulated DIFECTS test
550
+ # that contains the call to this method.
551
+ #
552
+ # @param [Symbol, Object] identifier
553
+ #
554
+ # An object that identifies shared code. This must be common
555
+ # knowledge to all parties that want to partake in the sharing.
556
+ #
557
+ # @example
558
+ #
559
+ # S :knowledge do
560
+ # #...
561
+ # end
562
+ #
563
+ # D "some test" do
564
+ # S :knowledge
565
+ # end
566
+ #
567
+ # D "another test" do
568
+ # S :knowledge
569
+ # end
570
+ #
571
+ def S identifier, &block
572
+ if block_given?
573
+ if already_shared = @share[identifier]
574
+ raise ArgumentError, "A code block #{already_shared.inspect} has "\
575
+ "already been shared under the identifier #{identifier.inspect}."
576
+ end
577
+
578
+ @share[identifier] = block
579
+
580
+ elsif block = @share[identifier]
581
+ if @tests.empty?
582
+ raise "Cannot inject code block #{block.inspect} shared under "\
583
+ "identifier #{identifier.inspect} outside of a DIFECTS test."
584
+ else
585
+ # find the closest insulated parent test; this should always
586
+ # succeed because root-level tests are insulated by default
587
+ test = @tests.reverse.find {|t| t.sandbox }
588
+ test.sandbox.instance_eval(&block)
589
+ end
590
+
591
+ else
592
+ raise ArgumentError, "No code block is shared under identifier "\
593
+ "#{identifier.inspect}."
594
+ end
595
+ end
596
+
597
+ ##
598
+ # Shares the given code block under the given
599
+ # identifier and then immediately injects that
600
+ # code block into the closest insulated DIFECTS
601
+ # test that contains the call to this method.
602
+ #
603
+ # @param identifier (see DIFECTS.S)
604
+ #
605
+ # @example
606
+ #
607
+ # D "some test" do
608
+ # S! :knowledge do
609
+ # #...
610
+ # end
611
+ # end
612
+ #
613
+ # D "another test" do
614
+ # S :knowledge
615
+ # end
616
+ #
617
+ def S! identifier, &block
618
+ raise 'block must be given' unless block_given?
619
+ S identifier, &block
620
+ S identifier
621
+ end
622
+
623
+ ##
624
+ # Checks whether any code has been shared under the given identifier.
625
+ #
626
+ def S? identifier
627
+ @share.key? identifier
628
+ end
629
+
630
+ ##
631
+ # Executes all tests defined thus far and stores
632
+ # the results in {DIFECTS.trace} and {DIFECTS.stats}.
633
+ #
634
+ def start
635
+ # execute the tests
636
+ start_time = Time.now
637
+ catch :DIFECTS_STOP do
638
+ BINDINGS.track do
639
+ execute
640
+ end
641
+ end
642
+ @stats[:time] = Time.now - start_time
643
+
644
+ # print test results
645
+ unless @stats.key? :fail or @stats.key? :error
646
+ #
647
+ # show execution trace only if all tests passed.
648
+ # otherwise, we will be repeating already printed
649
+ # failure details and obstructing the developer!
650
+ #
651
+ display @trace
652
+ end
653
+
654
+ display @stats
655
+
656
+ ensure
657
+ @tests.clear
658
+ @share.clear
659
+ @files.clear
660
+ end
661
+
662
+ ##
663
+ # Stops the execution of the {DIFECTS.start} method or raises
664
+ # an exception if that method is not currently executing.
665
+ #
666
+ def stop
667
+ throw :DIFECTS_STOP
668
+ end
669
+
670
+ ##
671
+ # Clear all test results that were recorded thus far.
672
+ #
673
+ def reset
674
+ @stats.clear
675
+ @trace.clear
676
+ end
677
+
678
+ ##
679
+ # Returns the details of the failure that
680
+ # is currently being debugged by the user.
681
+ #
682
+ def info
683
+ @trace.last
684
+ end
685
+
686
+ private
687
+
688
+ def create_test insulate, *description, &block
689
+ raise ArgumentError, 'block must be given' unless block
690
+
691
+ description = description.join(' ')
692
+ sandbox = Object.new if insulate
693
+
694
+ @suite.tests << Suite::Test.new(description, block, sandbox)
695
+ end
696
+
697
+ def assert_yield mode, condition = nil, message = nil, &block
698
+ # first parameter is actually the message when block is given
699
+ message = condition if block
700
+
701
+ message ||= (
702
+ prefix = block ? 'block must yield' : 'condition must be'
703
+ case mode
704
+ when :assert then "#{prefix} true (!nil && !false)"
705
+ when :negate then "#{prefix} false (nil || false)"
706
+ end
707
+ )
708
+
709
+ passed = lambda do
710
+ @stats[:pass] += 1
711
+ end
712
+
713
+ failed = lambda do
714
+ @stats[:fail] += 1
715
+ debug message
716
+ end
717
+
718
+ result = block ? call(block) : condition
719
+
720
+ case mode
721
+ when :sample then return result ? true : false
722
+ when :assert then result ? passed.call : failed.call
723
+ when :negate then result ? failed.call : passed.call
724
+ end
725
+
726
+ result
727
+ end
728
+
729
+ def assert_raise mode, *kinds_then_message, &block
730
+ raise ArgumentError, 'block must be given' unless block
731
+
732
+ message = kinds_then_message.pop
733
+ kinds = kinds_then_message
734
+
735
+ if message.kind_of? Class
736
+ kinds << message
737
+ message = nil
738
+ end
739
+
740
+ kinds << StandardError if kinds.empty?
741
+
742
+ message ||=
743
+ case mode
744
+ when :assert then "block must raise #{kinds.join ' or '}"
745
+ when :negate then "block must not raise #{kinds.join ' or '}"
746
+ end
747
+
748
+ passed = lambda do
749
+ @stats[:pass] += 1
750
+ end
751
+
752
+ failed = lambda do |exception|
753
+ @stats[:fail] += 1
754
+
755
+ if exception
756
+ # debug the uncaught exception...
757
+ debug_uncaught_exception exception
758
+
759
+ # ...in addition to debugging this assertion
760
+ debug [message, {'block raised' => exception}]
761
+
762
+ else
763
+ debug message
764
+ end
765
+ end
766
+
767
+ begin
768
+ block.call
769
+
770
+ rescue Exception => exception
771
+ expected = kinds.any? {|k| exception.kind_of? k }
772
+
773
+ case mode
774
+ when :sample then return expected
775
+ when :assert then expected ? passed.call : failed.call(exception)
776
+ when :negate then expected ? failed.call(exception) : passed.call
777
+ end
778
+
779
+ else # nothing was raised
780
+ case mode
781
+ when :sample then return false
782
+ when :assert then failed.call nil
783
+ when :negate then passed.call
784
+ end
785
+ end
786
+
787
+ exception
788
+ end
789
+
790
+ def assert_catch mode, symbol, message = nil, &block
791
+ raise ArgumentError, 'block must be given' unless block
792
+
793
+ symbol = symbol.to_sym
794
+ message ||= "block must throw #{symbol.inspect}"
795
+
796
+ passed = lambda do
797
+ @stats[:pass] += 1
798
+ end
799
+
800
+ failed = lambda do
801
+ @stats[:fail] += 1
802
+ debug message
803
+ end
804
+
805
+ # if nothing was thrown, the result of catch()
806
+ # is simply the result of executing the block
807
+ result = catch(symbol) do
808
+ begin
809
+ block.call
810
+ rescue Exception => e
811
+ #
812
+ # ignore error about the wrong symbol being thrown
813
+ #
814
+ # NOTE: Ruby 1.8 formats the thrown value in `quotes'
815
+ # whereas Ruby 1.9 formats it like a :symbol
816
+ #
817
+ unless e.message =~ /\Auncaught throw (`.*?'|:.*)\z/
818
+ debug_uncaught_exception e
819
+ end
820
+ end
821
+
822
+ self # unlikely that block will throw *this* object
823
+ end
824
+
825
+ caught = result != self
826
+ result = nil unless caught
827
+
828
+ case mode
829
+ when :sample then return caught
830
+ when :assert then caught ? passed.call : failed.call
831
+ when :negate then caught ? failed.call : passed.call
832
+ end
833
+
834
+ result
835
+ end
836
+
837
+ ##
838
+ # Prints the given object in YAML
839
+ # format if possible, or falls back
840
+ # to Ruby's pretty print library.
841
+ #
842
+ def display object
843
+ # stringify symbols in YAML output for better readability
844
+ puts object.to_yaml.gsub(/^([[:blank:]]*(- )?):(?=@?\w+: )/, '\1')
845
+ rescue
846
+ require 'pp'
847
+ pp object
848
+ end
849
+
850
+ ##
851
+ # Executes the current test suite recursively.
852
+ #
853
+ def execute
854
+ suite = @suite
855
+ trace = @trace
856
+
857
+ suite.before_all.each {|b| call b }
858
+
859
+ suite.tests.each do |test|
860
+ suite.before_each.each {|b| call b }
861
+
862
+ @tests.push test
863
+
864
+ begin
865
+ # create nested suite
866
+ @suite = Suite.new
867
+ @trace = []
868
+
869
+ # populate nested suite
870
+ call test.block, test.sandbox
871
+
872
+ # execute nested suite
873
+ execute
874
+
875
+ ensure
876
+ # restore outer values
877
+ @suite = suite
878
+
879
+ trace << build_exec_trace(@trace)
880
+ @trace = trace
881
+ end
882
+
883
+ @tests.pop
884
+
885
+ suite.after_each.each {|b| call b }
886
+ end
887
+
888
+ suite.after_all.each {|b| call b }
889
+ end
890
+
891
+ ##
892
+ # Invokes the given block and debugs any
893
+ # exceptions that may arise as a result.
894
+ #
895
+ def call block, sandbox = nil
896
+ if sandbox
897
+ sandbox.instance_eval(&block)
898
+ else
899
+ block.call
900
+ end
901
+
902
+ rescue Exception => e
903
+ debug_uncaught_exception e
904
+ end
905
+
906
+ INTERNALS = /^#{Regexp.escape(File.dirname(__FILE__))}/ # @private
907
+
908
+ class << BINDINGS = Hash.new {|h,k| h[k] = {} } # @private
909
+ ##
910
+ # Keeps track of bindings for all
911
+ # lines of code processed by Ruby
912
+ # for use later in {DIFECTS.debug}.
913
+ #
914
+ def track
915
+ raise ArgumentError unless block_given?
916
+
917
+ set_trace_func lambda {|event, file, line, id, binding, klass|
918
+ unless file =~ INTERNALS
919
+ self[file][line] = binding
920
+ end
921
+ }
922
+
923
+ yield
924
+
925
+ ensure
926
+ set_trace_func nil
927
+ clear
928
+ end
929
+ end
930
+
931
+ ##
932
+ # Adds debugging information to the test execution report and
933
+ # invokes the debugger if the {DIFECTS.debug} option is enabled.
934
+ #
935
+ # @param message
936
+ #
937
+ # Message describing the failure
938
+ # in the code being debugged.
939
+ #
940
+ # @param [Array<String>] backtrace
941
+ #
942
+ # Stack trace corresponding to point of
943
+ # failure in the code being debugged.
944
+ #
945
+ def debug message = nil, backtrace = caller
946
+ # omit internals from failure details
947
+ backtrace = backtrace.reject {|s| s =~ INTERNALS }
948
+
949
+ backtrace.first =~ /(.+?):(\d+(?=:|\z))/ or raise SyntaxError
950
+ source_file, source_line = $1, $2.to_i
951
+
952
+ binding_by_line = BINDINGS[source_file]
953
+ binding_line =
954
+ if binding_by_line.key? source_line
955
+ source_line
956
+ else
957
+ #
958
+ # There is no binding for the line number given in the backtrace, so
959
+ # try to adjust it to the nearest line that actually has a binding.
960
+ #
961
+ # This problem occurs because line numbers reported in backtraces
962
+ # sometimes do not agree with those observed by set_trace_func(),
963
+ # particularly in method calls that span multiple lines:
964
+ # set_trace_func() will consistently observe the ending parenthesis
965
+ # (last line of the method call) whereas the backtrace will oddly
966
+ # report a line somewhere in the middle of the method call.
967
+ #
968
+ # NOTE: I chose to adjust the imprecise line to the nearest one
969
+ # BELOW it. This might not always be correct because the nearest
970
+ # line below could belong to a different scope, like a new class.
971
+ #
972
+ binding_by_line.keys.sort.find {|n| n > source_line }
973
+ end
974
+ binding = binding_by_line[binding_line]
975
+
976
+ # record failure details in the test execution report
977
+ details = Hash[
978
+ # user message
979
+ :fail, message,
980
+
981
+ # stack trace
982
+ :call, backtrace,
983
+
984
+ # code snippet
985
+ :code, (
986
+ if source = @files[source_file]
987
+ radius = 5 # number of surrounding lines to show
988
+ region = [source_line - radius, 1].max ..
989
+ [source_line + radius, source.length].min
990
+
991
+ # ensure proper alignment by zero-padding line numbers
992
+ format = "%2s %0#{region.last.to_s.length}d %s"
993
+
994
+ pretty = region.map do |n|
995
+ format % [('=>' if n == source_line), n, source[n-1].chomp]
996
+ end.unshift "[#{region.inspect}] in #{source_file}"
997
+
998
+ pretty.extend FailureDetailsCodeListing
999
+ end
1000
+ )
1001
+ ]
1002
+
1003
+ if binding
1004
+ # binding location
1005
+ details[:bind] = [source_file, binding_line].join(':')
1006
+
1007
+ # variable values
1008
+ names = eval('::Kernel.local_variables', binding, __FILE__, __LINE__)
1009
+
1010
+ pairs = names.inject([]) do |pair, name|
1011
+ value = eval(name.to_s, binding, __FILE__, __LINE__)
1012
+ pair.push name.to_sym, value
1013
+ end
1014
+
1015
+ details[:vars] = Hash[*pairs].extend(FailureDetailsVariablesListing)
1016
+ end
1017
+
1018
+ details.reject! {|k,v| v.nil? }
1019
+ @trace << details
1020
+
1021
+ # show all failure details to the user
1022
+ display build_fail_trace(details)
1023
+
1024
+ # allow user to investigate the failure
1025
+ if @debug and binding
1026
+ unless defined? IRB
1027
+ require 'irb'
1028
+ IRB.setup nil
1029
+ end
1030
+
1031
+ irb = IRB::Irb.new(IRB::WorkSpace.new(binding))
1032
+ IRB.conf[:MAIN_CONTEXT] = irb.context
1033
+ catch(:IRB_EXIT) { irb.eval_input }
1034
+ end
1035
+
1036
+ nil
1037
+ end
1038
+
1039
+ ##
1040
+ # Debugs the given uncaught exception inside the given context.
1041
+ #
1042
+ def debug_uncaught_exception exception
1043
+ @stats[:error] += 1
1044
+ debug exception, exception.backtrace
1045
+ end
1046
+
1047
+ ##
1048
+ # Returns a report that associates the given
1049
+ # failure details with the currently running test.
1050
+ #
1051
+ def build_exec_trace details
1052
+ if @tests.empty?
1053
+ details
1054
+ else
1055
+ { @tests.last.desc => (details unless details.empty?) }
1056
+ end
1057
+ end
1058
+
1059
+ ##
1060
+ # Returns a report that qualifies the given
1061
+ # failure details with the current test stack.
1062
+ #
1063
+ def build_fail_trace details
1064
+ @tests.reverse.inject(details) do |inner, outer|
1065
+ { outer.desc => inner }
1066
+ end
1067
+ end
1068
+
1069
+ ##
1070
+ # Logic to pretty print the code listing in a failure's details.
1071
+ #
1072
+ module FailureDetailsCodeListing # @private
1073
+ def to_yaml options = {}
1074
+ #
1075
+ # strip because to_yaml() will render the paragraph without escaping
1076
+ # newlines ONLY IF the first and last character are non-whitespace!
1077
+ #
1078
+ join("\n").strip.to_yaml(options)
1079
+ end
1080
+
1081
+ def pretty_print printer
1082
+ margin = ' ' * printer.indent
1083
+ printer.text [
1084
+ first, self[1..-1].map {|line| margin + line }, margin
1085
+ ].join(printer.newline)
1086
+ end
1087
+ end
1088
+
1089
+ module FailureDetailsVariablesListing # @private
1090
+ def to_yaml options = {}
1091
+ require 'pp'
1092
+ require 'stringio'
1093
+
1094
+ pairs = []
1095
+ each do |variable, value|
1096
+ pretty = PP.pp(value, StringIO.new).string.chomp
1097
+ pairs.push variable, "(#{value.class}) #{pretty}"
1098
+ end
1099
+
1100
+ Hash[*pairs].to_yaml(options)
1101
+ end
1102
+ end
1103
+
1104
+ class Suite # @private
1105
+ attr_reader :tests, :before_each, :after_each, :before_all, :after_all
1106
+
1107
+ def initialize
1108
+ @tests = []
1109
+ @before_each = []
1110
+ @after_each = []
1111
+ @before_all = []
1112
+ @after_all = []
1113
+ end
1114
+
1115
+ Test = Struct.new(:desc, :block, :sandbox) # @private
1116
+ end
1117
+ end
1118
+
1119
+ # provide mixin-able versions of DIFECTS's core vocabulary
1120
+ singleton_methods(false).grep(/^[[:upper:]]?[[:punct:]]*$/).each do |meth|
1121
+ #
1122
+ # XXX: using eval() on a string because Ruby 1.8's
1123
+ # define_method() cannot take a block parameter
1124
+ #
1125
+ file, line = __FILE__, __LINE__ ; module_eval %{
1126
+ def #{meth}(*args, &block)
1127
+ ::#{name}.#{meth}(*args, &block)
1128
+ end
1129
+ }, file, line
1130
+ end
1131
+
1132
+ # allow mixin-able methods to be accessed as class methods
1133
+ extend self
1134
+
1135
+ # allow before and after hooks to be specified via the
1136
+ # following method syntax when this module is mixed-in:
1137
+ #
1138
+ # D .<< { puts "before all nested tests" }
1139
+ # D .< { puts "before each nested test" }
1140
+ # D .> { puts "after each nested test" }
1141
+ # D .>> { puts "after all nested tests" }
1142
+ #
1143
+ D = self
1144
+
1145
+ # set DIFECTS::Hash from an ordered hash library in lesser Ruby versions
1146
+ if RUBY_VERSION < '1.9'
1147
+ begin
1148
+ #
1149
+ # NOTE: I realize that there are other libraries, such as facets and
1150
+ # activesupport, that provide an ordered hash implementation, but this
1151
+ # particular library does not interfere with pretty printing routines.
1152
+ #
1153
+ require 'orderedhash'
1154
+ Hash = OrderedHash
1155
+ rescue LoadError
1156
+ warn "#{inspect}: Install 'orderedhash' gem for better failure reports."
1157
+ end
1158
+ end
1159
+
1160
+ @debug = $DEBUG
1161
+
1162
+ @stats = Hash.new {|h,k| h[k] = 0 }
1163
+ @trace = []
1164
+
1165
+ @suite = class << self; Suite.new; end
1166
+ @share = {}
1167
+ @tests = []
1168
+ @files = Hash.new {|h,k| h[k] = File.readlines(k) rescue nil }
1169
+ end