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/CREDITS +21 -0
- data/LICENSE +15 -0
- data/bin/difects +25 -0
- data/lib/difects/auto.rb +17 -0
- data/lib/difects/inochi.rb +93 -0
- data/lib/difects/long.rb +25 -0
- data/lib/difects/mini.rb +92 -0
- data/lib/difects/spec.rb +31 -0
- data/lib/difects/unit.rb +101 -0
- data/lib/difects.rb +1169 -0
- data/man/man1/difects.1.gz +0 -0
- data/man.html +1191 -0
- metadata +94 -0
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
|