dfect 0.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/lib/dfect.rb ADDED
@@ -0,0 +1,621 @@
1
+ #--
2
+ # Copyright 2009 Suraj N. Kurapati
3
+ # See the LICENSE file for details.
4
+ #++
5
+
6
+ require 'yaml'
7
+
8
+ # load interactive debugger
9
+ begin
10
+
11
+ begin
12
+ require 'rubygems'
13
+ rescue LoadError
14
+ end
15
+
16
+ require 'ruby-debug'
17
+
18
+ rescue LoadError
19
+ require 'irb'
20
+ end
21
+
22
+ module Dfect
23
+ class << self
24
+ ##
25
+ # Hash of test results, assembled by #run.
26
+ #
27
+ # [:execution]
28
+ # Hierarchical trace of all tests executed, where each test is
29
+ # represented by its description, is mapped to an Array of
30
+ # nested tests, and may contain zero or more assertion failures.
31
+ #
32
+ # Assertion failures are represented as a Hash:
33
+ #
34
+ # ["fail"]
35
+ # Description of the assertion failure.
36
+ #
37
+ # ["code"]
38
+ # Source code surrounding the point of failure.
39
+ #
40
+ # ["vars"]
41
+ # Local variables visible at the point of failure.
42
+ #
43
+ # ["call"]
44
+ # Stack trace leading to the point of failure.
45
+ #
46
+ # [:statistics]
47
+ # Hash of counts of major events in test execution:
48
+ #
49
+ # [:passed_assertions]
50
+ # Number of assertions that held true.
51
+ #
52
+ # [:failed_assertions]
53
+ # Number of assertions that did not hold true.
54
+ #
55
+ # [:uncaught_exceptions]
56
+ # Number of exceptions that were not rescued.
57
+ #
58
+ attr_reader :report
59
+
60
+ ##
61
+ # Hash of choices that affect how Dfect operates.
62
+ #
63
+ # [:debug]
64
+ # Launch an interactive debugger
65
+ # during assertion failures so
66
+ # the user can investigate them.
67
+ #
68
+ # The default value is $DEBUG.
69
+ #
70
+ # [:quiet]
71
+ # Do not print the report
72
+ # after executing all tests.
73
+ #
74
+ # The default value is false.
75
+ #
76
+ attr_accessor :options
77
+
78
+ ##
79
+ # Defines a new test, composed of the given
80
+ # description and the given block to execute.
81
+ #
82
+ # A test may contain nested tests.
83
+ #
84
+ # ==== Parameters
85
+ #
86
+ # [description]
87
+ # A short summary of the test being defined.
88
+ #
89
+ # ==== Examples
90
+ #
91
+ # D "a new array" do
92
+ # D .< { @array = [] }
93
+ #
94
+ # D "must be empty" do
95
+ # T { @array.empty? }
96
+ # end
97
+ #
98
+ # D "when populated" do
99
+ # D .< { @array.push 55 }
100
+ #
101
+ # D "must not be empty" do
102
+ # F { @array.empty? }
103
+ # end
104
+ # end
105
+ # end
106
+ #
107
+ def D description = caller.first, &block
108
+ raise ArgumentError, 'block must be given' unless block
109
+ @curr_suite.tests << Suite::Test.new(description.to_s, block)
110
+ end
111
+
112
+ ##
113
+ # Registers the given block to be executed
114
+ # before each nested test inside this test.
115
+ #
116
+ # ==== Examples
117
+ #
118
+ # D .< { puts "before each nested test" }
119
+ #
120
+ # D .< do
121
+ # puts "before each nested test"
122
+ # end
123
+ #
124
+ def < &block
125
+ raise ArgumentError, 'block must be given' unless block
126
+ @curr_suite.before_each << block
127
+ end
128
+
129
+ ##
130
+ # Registers the given block to be executed
131
+ # after each nested test inside this test.
132
+ #
133
+ # ==== Examples
134
+ #
135
+ # D .> { puts "after each nested test" }
136
+ #
137
+ # D .> do
138
+ # puts "after each nested test"
139
+ # end
140
+ #
141
+ def > &block
142
+ raise ArgumentError, 'block must be given' unless block
143
+ @curr_suite.after_each << block
144
+ end
145
+
146
+ ##
147
+ # Registers the given block to be executed
148
+ # before all nested tests inside this test.
149
+ #
150
+ # ==== Examples
151
+ #
152
+ # D .<< { puts "before all nested tests" }
153
+ #
154
+ # D .<< do
155
+ # puts "before all nested tests"
156
+ # end
157
+ #
158
+ def << &block
159
+ raise ArgumentError, 'block must be given' unless block
160
+ @curr_suite.before_all << block
161
+ end
162
+
163
+ ##
164
+ # Registers the given block to be executed
165
+ # after all nested tests inside this test.
166
+ #
167
+ # ==== Examples
168
+ #
169
+ # D .>> { puts "after all nested tests" }
170
+ #
171
+ # D .>> do
172
+ # puts "after all nested tests"
173
+ # end
174
+ #
175
+ def >> &block
176
+ raise ArgumentError, 'block must be given' unless block
177
+ @curr_suite.after_all << block
178
+ end
179
+
180
+ ##
181
+ # Asserts that the result of the given block is
182
+ # neither nil nor false and returns that result.
183
+ #
184
+ # ==== Parameters
185
+ #
186
+ # [message]
187
+ # Optional message to show in the
188
+ # report if this assertion fails.
189
+ #
190
+ # ==== Examples
191
+ #
192
+ # # no message specified:
193
+ #
194
+ # T { true } # passes
195
+ #
196
+ # T { false } # fails
197
+ # T { nil } # fails
198
+ #
199
+ # # message specified:
200
+ #
201
+ # T( "computers do not doublethink" ) { 2 + 2 != 5 } # passes
202
+ #
203
+ def T message = 'block must yield true (!nil && !false)', &block
204
+ raise ArgumentError, 'block must be given' unless block
205
+
206
+ if result = call(block)
207
+ @exec_stats[:passed_assertions] += 1
208
+ else
209
+ @exec_stats[:failed_assertions] += 1
210
+ debug block, message
211
+ end
212
+
213
+ result
214
+ end
215
+
216
+ ##
217
+ # Asserts that the result of the given block is
218
+ # either nil or false and returns that result.
219
+ #
220
+ # ==== Parameters
221
+ #
222
+ # [message]
223
+ # Optional message to show in the
224
+ # report if this assertion fails.
225
+ #
226
+ # ==== Examples
227
+ #
228
+ # # no message specified:
229
+ #
230
+ # F { true } # fails
231
+ #
232
+ # F { false } # passes
233
+ # F { nil } # passes
234
+ #
235
+ # # message specified:
236
+ #
237
+ # F( "computers do not doublethink" ) { 2 + 2 == 5 } # passes
238
+ #
239
+ def F message = 'block must yield false (nil || false)', &block
240
+ raise ArgumentError, 'block must be given' unless block
241
+
242
+ if result = call(block)
243
+ @exec_stats[:failed_assertions] += 1
244
+ debug block, message
245
+ else
246
+ @exec_stats[:passed_assertions] += 1
247
+ end
248
+
249
+ result
250
+ end
251
+
252
+ ##
253
+ # Asserts that one of the given kinds of exceptions is raised when the
254
+ # given block is executed, and returns the exception that was raised.
255
+ #
256
+ # ==== Parameters
257
+ #
258
+ # [message]
259
+ # Optional message to show in the
260
+ # report if this assertion fails.
261
+ #
262
+ # [kinds]
263
+ # Exception classes that must be raised by the given block.
264
+ #
265
+ # If none are given, then StandardError is assumed (similar to how a
266
+ # plain 'rescue' statement without any arguments catches StandardError).
267
+ #
268
+ # ==== Examples
269
+ #
270
+ # # no exceptions specified:
271
+ #
272
+ # E { } # fails
273
+ # E { raise } # passes
274
+ #
275
+ # # single exception specified:
276
+ #
277
+ # E( ArgumentError ) { raise ArgumentError }
278
+ # E( "argument must be invalid", ArgumentError ) { raise ArgumentError }
279
+ #
280
+ # # multiple exceptions specified:
281
+ #
282
+ # E( SyntaxError, NameError ) { eval "..." }
283
+ # E( "string must compile", SyntaxError, NameError ) { eval "..." }
284
+ #
285
+ def E message = nil, *kinds, &block
286
+ raise ArgumentError, 'block must be given' unless block
287
+
288
+ if message.is_a? Class
289
+ kinds.unshift message
290
+ message = nil
291
+ end
292
+
293
+ kinds << StandardError if kinds.empty?
294
+ message ||= "block must raise #{kinds.join ' or '}"
295
+
296
+ begin
297
+ block.call
298
+
299
+ rescue *kinds => raised
300
+ @exec_stats[:passed_assertions] += 1
301
+
302
+ rescue Exception => raised
303
+ @exec_stats[:failed_assertions] += 1
304
+
305
+ # debug the uncaught exception...
306
+ debug_uncaught_exception block, raised
307
+
308
+ # ...in addition to debugging this assertion
309
+ debug block, [message, {'block raised' => raised}]
310
+ end
311
+
312
+ raised
313
+ end
314
+
315
+ ##
316
+ # Asserts that the given symbol is thrown when
317
+ # the given block is executed, and returns the
318
+ # value that was thrown along with the symbol.
319
+ #
320
+ # ==== Parameters
321
+ #
322
+ # [message]
323
+ # Optional message to show in the
324
+ # report if this assertion fails.
325
+ #
326
+ # [symbol]
327
+ # Symbol that must be thrown by the given block.
328
+ #
329
+ # ==== Examples
330
+ #
331
+ # # no message specified:
332
+ #
333
+ # C(:foo) { throw :foo } # passes
334
+ #
335
+ # C(:foo) { throw :bar } # fails
336
+ # C(:foo) { } # fails
337
+ #
338
+ # # message specified:
339
+ #
340
+ # C( ":foo must be thrown", :foo ) { throw :bar } # fails
341
+ #
342
+ def C message = nil, symbol = nil, &block
343
+ raise ArgumentError, 'block must be given' unless block
344
+
345
+ if message.is_a? Symbol and not symbol
346
+ symbol = message
347
+ message = nil
348
+ end
349
+
350
+ raise ArgumentError, 'symbol must be given' unless symbol
351
+
352
+ symbol = symbol.to_sym
353
+ message ||= "block must throw #{symbol.inspect}"
354
+
355
+ # if nothing was thrown, the result of catch()
356
+ # is simply the result of executing the block
357
+ result = catch(symbol) { call block; self }
358
+
359
+ if result == self
360
+ @exec_stats[:failed_assertions] += 1
361
+ debug block, message
362
+ else
363
+ @exec_stats[:passed_assertions] += 1
364
+ result
365
+ end
366
+ end
367
+
368
+ ##
369
+ # Executes all tests defined thus far and stores the results in #report.
370
+ #
371
+ def run
372
+ # clear previous results
373
+ @exec_stats.clear
374
+ @exec_trace.clear
375
+ @test_stack.clear
376
+
377
+ # make new results
378
+ catch :stop_dfect_execution do
379
+ execute
380
+ end
381
+
382
+ # print new results
383
+ puts @report.to_yaml unless @options[:quiet]
384
+ end
385
+
386
+ ##
387
+ # Stops the execution of the #run method or raises an
388
+ # exception if that method is not currently executing.
389
+ #
390
+ def stop
391
+ throw :stop_dfect_execution
392
+ end
393
+
394
+ private
395
+
396
+ ##
397
+ # Executes the current test suite recursively.
398
+ #
399
+ def execute
400
+ suite = @curr_suite
401
+ trace = @exec_trace
402
+
403
+ suite.before_all.each {|b| call b }
404
+
405
+ suite.tests.each do |test|
406
+ suite.before_each.each {|b| call b }
407
+
408
+ @test_stack.push test
409
+
410
+ begin
411
+ # create nested suite
412
+ @curr_suite = Suite.new
413
+ @exec_trace = []
414
+
415
+ # populate nested suite
416
+ call test.block
417
+
418
+ # execute nested suite
419
+ execute
420
+
421
+ ensure
422
+ # restore outer values
423
+ @curr_suite = suite
424
+
425
+ trace << build_trace(@exec_trace)
426
+ @exec_trace = trace
427
+ end
428
+
429
+ @test_stack.pop
430
+
431
+ suite.after_each.each {|b| call b }
432
+ end
433
+
434
+ suite.after_all.each {|b| call b }
435
+ end
436
+
437
+ ##
438
+ # Invokes the given block and debugs any
439
+ # exceptions that may arise as a result.
440
+ #
441
+ def call block
442
+ begin
443
+ block.call
444
+ rescue Exception => e
445
+ debug_uncaught_exception block, e
446
+ end
447
+ end
448
+
449
+ INTERNALS = File.dirname(__FILE__) #:nodoc:
450
+
451
+ ##
452
+ # Adds debugging information to the report.
453
+ #
454
+ # ==== Parameters
455
+ #
456
+ # [context]
457
+ # Binding of code being debugged. This
458
+ # can be either a Binding or Proc object.
459
+ #
460
+ # [message]
461
+ # Message describing the failure
462
+ # in the code being debugged.
463
+ #
464
+ # [backtrace]
465
+ # Stack trace corresponding to point of
466
+ # failure in the code being debugged.
467
+ #
468
+ def debug context, message = nil, backtrace = caller
469
+ # allow a Proc to be passed instead of a binding
470
+ if context.respond_to? :binding
471
+ context = context.binding
472
+ end
473
+
474
+ # omit internals from failure details
475
+ backtrace = backtrace.reject {|s| s.include? INTERNALS }
476
+
477
+ # record failure details in the report
478
+ #
479
+ # NOTE: using string keys here instead
480
+ # of symbols because they make
481
+ # the YAML output easier to read
482
+ #
483
+ details = {
484
+ # user message
485
+ 'fail' => message,
486
+
487
+ # code snippet
488
+ 'code' => (
489
+ if frame = backtrace.first
490
+ file, line = frame.scan(/(.+?):(\d+(?=:|\z))/).first
491
+
492
+ if source = @file_cache[file]
493
+ line = line.to_i
494
+
495
+ radius = 5 # number of surrounding lines to show
496
+ region = [line - radius, 1].max ..
497
+ [line + radius, source.length].min
498
+
499
+ # ensure proper alignment by zero-padding line numbers
500
+ format = "%2s %0#{region.last.to_s.length}d %s"
501
+
502
+ pretty = region.map do |n|
503
+ format % [('=>' if n == line), n, source[n-1].chomp]
504
+ end
505
+
506
+ pretty.unshift "[#{region.inspect}] in #{file}"
507
+
508
+ # to_yaml will render the paragraph without escaping newlines
509
+ # ONLY IF the first and last character are non-whitespace
510
+ pretty.join("\n").strip
511
+ end
512
+ end
513
+ ),
514
+
515
+ # variable values
516
+ 'vars' => (
517
+ locals = eval('::Kernel.local_variables.map {|v| [v.to_s, ::Kernel.eval(v.to_s)] }', context, __FILE__, __LINE__)
518
+
519
+ Hash[*locals.flatten]
520
+ ),
521
+
522
+ # stack trace
523
+ 'call' => backtrace,
524
+ }
525
+
526
+ @exec_trace << details
527
+
528
+ # allow user to investigate the failure
529
+ if @options[:debug]
530
+ # show the failure to the user
531
+ puts build_trace(details).to_yaml
532
+
533
+ # start the investigation
534
+ if Kernel.respond_to? :debugger
535
+ eval '::Kernel.debugger', context, __FILE__, __LINE__
536
+ else
537
+ IRB.setup nil
538
+
539
+ env = IRB::WorkSpace.new(context)
540
+ irb = IRB::Irb.new(env)
541
+ IRB.conf[:MAIN_CONTEXT] = irb.context
542
+
543
+ catch :IRB_EXIT do
544
+ irb.eval_input
545
+ end
546
+ end
547
+ end
548
+
549
+ nil
550
+ end
551
+
552
+ ##
553
+ # Debugs the given uncaught exception inside the given context.
554
+ #
555
+ def debug_uncaught_exception context, exception
556
+ @exec_stats[:uncaught_exceptions] += 1
557
+ debug context, exception, exception.backtrace
558
+ end
559
+
560
+ ##
561
+ # Returns a report that associates the given
562
+ # failure details with the currently running test.
563
+ #
564
+ def build_trace details
565
+ if @test_stack.empty?
566
+ details
567
+ else
568
+ { @test_stack.last.desc => details }
569
+ end
570
+ end
571
+
572
+ #:stopdoc:
573
+
574
+ class Suite
575
+ attr_reader :tests, :before_each, :after_each, :before_all, :after_all
576
+
577
+ def initialize
578
+ @tests = []
579
+ @before_each = []
580
+ @after_each = []
581
+ @before_all = []
582
+ @after_all = []
583
+ end
584
+
585
+ Test = Struct.new :desc, :block
586
+ end
587
+
588
+ #:startdoc:
589
+ end
590
+
591
+ @options = {:debug => $DEBUG, :quiet => false}
592
+
593
+ @exec_stats = Hash.new {|h,k| h[k] = 0 }
594
+ @exec_trace = []
595
+ @report = {:execution => @exec_trace, :statistics => @exec_stats}.freeze
596
+
597
+ @curr_suite = class << self; Suite.new; end
598
+
599
+ @test_stack = []
600
+ @file_cache = Hash.new {|h,k| h[k] = File.readlines(k) rescue nil }
601
+
602
+ ##
603
+ # Allows before and after hooks to be specified via
604
+ # the D() method syntax when this module is mixed-in:
605
+ #
606
+ # D .< { puts "before each nested test" }
607
+ # D .> { puts "after each nested test" }
608
+ # D .<< { puts "before all nested tests" }
609
+ # D .>> { puts "after all nested tests" }
610
+ #
611
+ D = self
612
+
613
+ # provide mixin-able assertion methods
614
+ methods(false).grep(/^[[:upper:]]$/).each do |name|
615
+ #
616
+ # XXX: using eval() on a string because Ruby 1.8's
617
+ # define_method() cannot take a block parameter
618
+ #
619
+ eval "def #{name}(*a, &b) ::#{self}.#{name}(*a, &b) end", binding, __FILE__, __LINE__
620
+ end
621
+ end
data/rakefile ADDED
@@ -0,0 +1,19 @@
1
+ #--
2
+ # Copyright 2009 Suraj N. Kurapati
3
+ # See the LICENSE file for details.
4
+ #++
5
+
6
+ require 'inochi'
7
+
8
+ Inochi.init :Dfect,
9
+ :version => '0.0.0',
10
+ :release => '2009-04-13',
11
+ :website => 'http://snk.tuxfamily.org/lib/dfect',
12
+ :tagline => 'Assertion testing library for Ruby'
13
+
14
+ Inochi.rake :Dfect,
15
+ :test_with => :dfect,
16
+ :rubyforge_project => 'sunaku',
17
+ :upload_target => File.expand_path('~/www/lib/dfect/'),
18
+ :upload_delete => true,
19
+ :inochi_consumer => false