difects 3.0.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/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