dfect 0.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/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