migr8 0.4.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/MIT-LICENSE +20 -0
- data/README.md +356 -0
- data/Rakefile +133 -0
- data/bin/migr8.rb +14 -0
- data/lib/migr8.rb +2645 -0
- data/migr8.gemspec +57 -0
- data/setup.rb +1585 -0
- data/test/Application_test.rb +148 -0
- data/test/Migration_test.rb +261 -0
- data/test/Util_test.rb +873 -0
- data/test/helpers.rb +93 -0
- data/test/oktest.rb +1537 -0
- data/test/run_all.rb +8 -0
- metadata +71 -0
data/test/helpers.rb
ADDED
@@ -0,0 +1,93 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
|
3
|
+
|
4
|
+
class Dummy
|
5
|
+
|
6
|
+
def stdout
|
7
|
+
bkup = $stdout
|
8
|
+
$stdout = stdout = StringIO.new
|
9
|
+
begin
|
10
|
+
yield
|
11
|
+
ensure
|
12
|
+
$stdout = bkup
|
13
|
+
end
|
14
|
+
stdout.rewind
|
15
|
+
return stdout.read()
|
16
|
+
end
|
17
|
+
|
18
|
+
def stderr
|
19
|
+
bkup = $stderr
|
20
|
+
$stderr = stderr = StringIO.new
|
21
|
+
begin
|
22
|
+
yield
|
23
|
+
ensure
|
24
|
+
$stderr = bkup
|
25
|
+
end
|
26
|
+
stderr.rewind
|
27
|
+
return stderr.read()
|
28
|
+
end
|
29
|
+
|
30
|
+
def stdouterr
|
31
|
+
bkup = [$stdout, $stderr]
|
32
|
+
$stdout = stdout = StringIO.new
|
33
|
+
$stderr = stderr = StringIO.new
|
34
|
+
begin
|
35
|
+
yield
|
36
|
+
ensure
|
37
|
+
$stdout, $stderr = bkup
|
38
|
+
end
|
39
|
+
stdout.rewind
|
40
|
+
stderr.rewind
|
41
|
+
return [stdout.read(), stderr.read()]
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
45
|
+
|
46
|
+
|
47
|
+
Dummy.class_eval do
|
48
|
+
|
49
|
+
def self.partial_regexp(string, pattern=/\[==(.*)==\]/)
|
50
|
+
pat = '\A'
|
51
|
+
pos = 0
|
52
|
+
string.scan(pattern) do
|
53
|
+
m = Regexp.last_match
|
54
|
+
text = string[pos...m.begin(0)]
|
55
|
+
pos = m.end(0)
|
56
|
+
pat << Regexp.escape(text)
|
57
|
+
pat << m[1]
|
58
|
+
end
|
59
|
+
rest = pos == 0 ? string : string[pos..-1]
|
60
|
+
pat << Regexp.escape(rest)
|
61
|
+
pat << '\z'
|
62
|
+
$stderr.puts "\033[0;31m*** debug: pat=\n#{pat}\033[0m"
|
63
|
+
return Regexp.compile(pat)
|
64
|
+
end
|
65
|
+
|
66
|
+
end
|
67
|
+
|
68
|
+
|
69
|
+
if __FILE__ == $0
|
70
|
+
|
71
|
+
text = <<'END'
|
72
|
+
Usage: skeema.rb [global-options] [action [options] [...]]
|
73
|
+
-h, --help : show help
|
74
|
+
-v, --version : show version
|
75
|
+
|
76
|
+
Actions (default: [==(navi|status)==]):
|
77
|
+
navi : !!RUN THIS ACTION AT FIRST!!
|
78
|
+
help [action] : show help message of action, or list action names
|
79
|
+
init : create necessary files and a table
|
80
|
+
hist : list history of versions
|
81
|
+
new : create new migration file and open it by $SKEEMA_EDITOR
|
82
|
+
edit [version] : open migration file by $SKEEMA_EDITOR
|
83
|
+
status : show status
|
84
|
+
up : apply a next migration
|
85
|
+
down : unapply current migration
|
86
|
+
redo : do migration down, and up it again
|
87
|
+
apply version ... : apply specified migrations
|
88
|
+
unapply version ... : unapply specified migrations
|
89
|
+
END
|
90
|
+
|
91
|
+
Dummy.pattern_text(text)
|
92
|
+
|
93
|
+
end
|
data/test/oktest.rb
ADDED
@@ -0,0 +1,1537 @@
|
|
1
|
+
###
|
2
|
+
### $Release: $
|
3
|
+
### $Copyright: copyright(c) 2011 kuwata-lab.com all rights reserved $
|
4
|
+
### $License: MIT License $
|
5
|
+
###
|
6
|
+
|
7
|
+
require 'test/unit'
|
8
|
+
if defined?(Test::Unit::Runner)
|
9
|
+
Test::Unit::Runner.class_variable_set(:@@stop_auto_run, true)
|
10
|
+
end
|
11
|
+
|
12
|
+
#require 'section/tmp'
|
13
|
+
#require 'section/recorder'
|
14
|
+
|
15
|
+
|
16
|
+
module Oktest
|
17
|
+
|
18
|
+
|
19
|
+
VERSION = '$Release: 0.0.0 $'.split(/ /)[1]
|
20
|
+
|
21
|
+
|
22
|
+
class SkipException < Exception
|
23
|
+
end
|
24
|
+
|
25
|
+
class TodoException < Exception
|
26
|
+
end
|
27
|
+
|
28
|
+
FAIL_EXCEPTION = defined?(MiniTest) ? MiniTest::Assertion : Test::Unit::AssertionFailedError # :nodoc:
|
29
|
+
SKIP_EXCEPTION = SkipException
|
30
|
+
TODO_EXCEPTION = TodoException
|
31
|
+
|
32
|
+
|
33
|
+
class AssertionObject
|
34
|
+
include Test::Unit::Assertions
|
35
|
+
|
36
|
+
self.instance_methods.grep(/\?\z/).each do |k|
|
37
|
+
undef_method k unless k.to_s == 'equal?' || k.to_s =~ /^assert/
|
38
|
+
end
|
39
|
+
|
40
|
+
NOT_YET = {}
|
41
|
+
|
42
|
+
def initialize(actual, bool, location)
|
43
|
+
@actual = actual
|
44
|
+
@bool = bool
|
45
|
+
@location = location
|
46
|
+
end
|
47
|
+
|
48
|
+
attr_reader :actual, :bool, :location
|
49
|
+
|
50
|
+
def _done
|
51
|
+
AssertionObject::NOT_YET.delete(self.__id__)
|
52
|
+
end
|
53
|
+
private :_done
|
54
|
+
|
55
|
+
def self.report_not_yet
|
56
|
+
return if NOT_YET.empty?
|
57
|
+
NOT_YET.each_value do |ass|
|
58
|
+
$stderr.write "** warning: ok() is called but not tested yet (at #{ass.location})\n"
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def _not
|
63
|
+
return @bool ? '' : 'not '
|
64
|
+
end
|
65
|
+
private :_not
|
66
|
+
|
67
|
+
if defined?(MiniTest)
|
68
|
+
def __assert result
|
69
|
+
if result
|
70
|
+
assert true
|
71
|
+
else
|
72
|
+
#assert_block(yield) { false }
|
73
|
+
#flunk yield
|
74
|
+
assert false, yield
|
75
|
+
end
|
76
|
+
end
|
77
|
+
else
|
78
|
+
def __assert result
|
79
|
+
if result
|
80
|
+
assert true
|
81
|
+
else
|
82
|
+
assert_block(yield) { false }
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def NOT
|
88
|
+
@bool = ! @bool
|
89
|
+
self
|
90
|
+
end
|
91
|
+
|
92
|
+
def ==(expected)
|
93
|
+
_done()
|
94
|
+
__assert(@bool == (@actual == expected)) {
|
95
|
+
if @bool && ! (@actual == expected) \
|
96
|
+
&& @actual.is_a?(String) && expected.is_a?(String) \
|
97
|
+
&& (@actual =~ /\n/ || expected =~ /\n/)
|
98
|
+
diff = Util.unified_diff(expected, @actual, "--- $expected\n+++ $actual\n")
|
99
|
+
"$actual == $expected: failed.\n#{diff}"
|
100
|
+
else
|
101
|
+
op = @bool ? '==' : '!='
|
102
|
+
"$actual #{op} $expected: failed.\n"\
|
103
|
+
" $actual: #{@actual.inspect}\n"\
|
104
|
+
" $expected: #{expected.inspect}"
|
105
|
+
end
|
106
|
+
}
|
107
|
+
self
|
108
|
+
end
|
109
|
+
|
110
|
+
if RUBY_VERSION >= '1.9'
|
111
|
+
op = @bool ? '==' : '!='
|
112
|
+
eval <<-END, binding, __FILE__, __LINE__+1
|
113
|
+
def !=(expected)
|
114
|
+
_done()
|
115
|
+
__assert(@bool == (@actual != expected)) {
|
116
|
+
"$actual #{op} $expected: failed.\n"\
|
117
|
+
" $actual: \#{@actual.inspect}\n"\
|
118
|
+
" $expected: \#{expected.inspect}"
|
119
|
+
}
|
120
|
+
self
|
121
|
+
end
|
122
|
+
END
|
123
|
+
end
|
124
|
+
|
125
|
+
#--
|
126
|
+
#def > expected; _done(); assert_operator(@actual, @bool ? :> : :<=, expected); self end
|
127
|
+
def >= expected; _done(); assert_operator(@actual, @bool ? :>= : :<, expected); self end
|
128
|
+
def < expected; _done(); assert_operator(@actual, @bool ? :< : :>=, expected); self end
|
129
|
+
def <= expected; _done(); assert_operator(@actual, @bool ? :<= : :>, expected); self end
|
130
|
+
#++
|
131
|
+
|
132
|
+
def >(expected)
|
133
|
+
_done()
|
134
|
+
__assert(@bool == (@actual > expected)) {
|
135
|
+
"#{@actual.inspect} #{@bool ? '>' : '<='} #{expected}: failed."
|
136
|
+
}
|
137
|
+
self
|
138
|
+
end
|
139
|
+
|
140
|
+
def >=(expected)
|
141
|
+
_done()
|
142
|
+
__assert(@bool == (@actual >= expected)) {
|
143
|
+
"#{@actual.inspect} #{@bool ? '>=' : '<'} #{expected}: failed."
|
144
|
+
}
|
145
|
+
self
|
146
|
+
end
|
147
|
+
|
148
|
+
def <(expected)
|
149
|
+
_done()
|
150
|
+
__assert(@bool == (@actual < expected)) {
|
151
|
+
"#{@actual.inspect} #{@bool ? '<' : '>='} #{expected}: failed."
|
152
|
+
}
|
153
|
+
self
|
154
|
+
end
|
155
|
+
|
156
|
+
def <=(expected)
|
157
|
+
_done()
|
158
|
+
__assert(@bool == (@actual <= expected)) {
|
159
|
+
"#{@actual.inspect} #{@bool ? '<=' : '>'} #{expected}: failed."
|
160
|
+
}
|
161
|
+
self
|
162
|
+
end
|
163
|
+
|
164
|
+
def =~(expected)
|
165
|
+
_done()
|
166
|
+
#@bool ? assert_match(expected, @actual) : assert_no_match(expected, @actual)
|
167
|
+
__assert(@bool == !!(@actual =~ expected)) {
|
168
|
+
op = @bool ? '=~' : '!~'
|
169
|
+
msg = "$actual #{op} $expected: failed.\n"\
|
170
|
+
" $expected: #{expected.inspect}\n"
|
171
|
+
if @actual =~ /\n\z/
|
172
|
+
msg << " $actual: <<'END'\n#{@actual}END\n"
|
173
|
+
else
|
174
|
+
msg << " $actual: #{@actual.inspect}\n"
|
175
|
+
end
|
176
|
+
}
|
177
|
+
self
|
178
|
+
end
|
179
|
+
|
180
|
+
if RUBY_VERSION >= "1.9"
|
181
|
+
eval <<-'END', binding, __FILE__, __LINE__+1
|
182
|
+
def !~(expected)
|
183
|
+
_done()
|
184
|
+
#@bool ? assert_no_match(expected, @actual) : assert_match(expected, @actual)
|
185
|
+
__assert(@bool == !!(@actual !~ expected)) {
|
186
|
+
op = @bool ? '!~' : '=~'
|
187
|
+
msg = "$actual #{op} $expected: failed.\n"\
|
188
|
+
" $expected: #{expected.inspect}\n"
|
189
|
+
if @actual =~ /\n\z/
|
190
|
+
msg << " $actual: <<'END'\n#{@actual}END\n"
|
191
|
+
else
|
192
|
+
msg << " $actual: #{@actual.inspect}\n"
|
193
|
+
end
|
194
|
+
}
|
195
|
+
self
|
196
|
+
end
|
197
|
+
END
|
198
|
+
end
|
199
|
+
|
200
|
+
def in_delta?(expected, delta)
|
201
|
+
_done()
|
202
|
+
__assert(@bool == !!((@actual - expected).abs < delta)) {
|
203
|
+
eq = @bool ? '' : ' == false'
|
204
|
+
"($actual - $expected).abs < #{delta}#{eq}: failed.\n"\
|
205
|
+
" $actual: #{@actual.inspect}\n"\
|
206
|
+
" $expected: #{expected.inspect}\n"\
|
207
|
+
" ($actual - $expected).abs: #{(@actual - expected).abs.inspect}"
|
208
|
+
}
|
209
|
+
self
|
210
|
+
end
|
211
|
+
|
212
|
+
def same?(expected)
|
213
|
+
_done()
|
214
|
+
#@bool ? assert_same(expected, @actual) \
|
215
|
+
# : assert_not_same(expected, @actual)
|
216
|
+
__assert(@bool == !! @actual.equal?(expected)) {
|
217
|
+
eq = @bool ? '' : ' == false'
|
218
|
+
"$actual.equal?($expected)#{eq}: failed.\n"\
|
219
|
+
" $actual: #{@actual.inspect}\n"\
|
220
|
+
" $expected: #{expected.inspect}\n"
|
221
|
+
}
|
222
|
+
self
|
223
|
+
end
|
224
|
+
|
225
|
+
def method_missing(method_name, *args)
|
226
|
+
_done()
|
227
|
+
method_name.to_s =~ /\?\z/ or
|
228
|
+
super
|
229
|
+
begin
|
230
|
+
ret = @actual.__send__(method_name, *args)
|
231
|
+
rescue NoMethodError, TypeError => ex
|
232
|
+
ex.set_backtrace(caller(1))
|
233
|
+
raise ex
|
234
|
+
end
|
235
|
+
if ret == true || ret == false
|
236
|
+
__assert(@bool == ret) {
|
237
|
+
args = args.empty? ? '' : "(#{args.collect {|x| x.inspect }.join(', ')})"
|
238
|
+
eq = @bool ? '' : ' == false'
|
239
|
+
"$actual.#{method_name}#{args}#{eq}: failed.\n"\
|
240
|
+
" $actual: #{@actual.inspect}"
|
241
|
+
}
|
242
|
+
else
|
243
|
+
raise TypeError.new("ok(): #{@actual.class}##{method_name}() expected to return true or false, but got #{ret.inspect}.")
|
244
|
+
end
|
245
|
+
self
|
246
|
+
end
|
247
|
+
|
248
|
+
#--
|
249
|
+
#def same? expected ; _done(); assert_same expected, @actual; self; end
|
250
|
+
#def not_same? expected ; _done(); assert_not_same expected, @actual; self; end
|
251
|
+
#def is_a? expected ; _done(); assert_kind_of expected, @actual; self; end
|
252
|
+
#def instance_of? expected ; _done(); assert_instance_of expected, @actual; self; end
|
253
|
+
#def respond_to? expected ; _done(); assert_respond_to expected, @actual; self; end
|
254
|
+
#def nil? ; _done(); assert_nil @actual; self; end
|
255
|
+
#def not_nil? ; _done(); assert_not_nil @actual; self; end
|
256
|
+
#++
|
257
|
+
|
258
|
+
def raise?(expected=Exception, errmsg=nil)
|
259
|
+
_done()
|
260
|
+
proc_obj = @actual
|
261
|
+
if @bool
|
262
|
+
ex = nil
|
263
|
+
begin
|
264
|
+
proc_obj.call
|
265
|
+
rescue Exception => ex
|
266
|
+
ex.is_a?(expected) or
|
267
|
+
__assert(false) { "Expected #{expected.inspect} to be raised but got #{ex.class}." }
|
268
|
+
end
|
269
|
+
(class << proc_obj; self; end).class_eval { attr_accessor :exception }
|
270
|
+
proc_obj.exception = ex
|
271
|
+
__assert(! ex.nil?) { "Expected #{expected.inspect} to be raised but nothing raised." }
|
272
|
+
case errmsg
|
273
|
+
when nil; # do nothing
|
274
|
+
when Regexp
|
275
|
+
__assert(ex.message =~ errmsg) {
|
276
|
+
"$error_message =~ #{errmsg.inspect}: failed.\n"\
|
277
|
+
" $error_message: #{ex.message.inspect}"
|
278
|
+
}
|
279
|
+
else
|
280
|
+
__assert(errmsg == ex.message) {
|
281
|
+
"$error_message == #{errmsg.inspect}: failed.\n"\
|
282
|
+
" $error_message: #{ex.message.inspect}"
|
283
|
+
}
|
284
|
+
end
|
285
|
+
else
|
286
|
+
! errmsg or
|
287
|
+
raise ArgumentError.new("#{errmsg.inspect}: NOT.raise?() can't take errmsg.")
|
288
|
+
begin
|
289
|
+
proc_obj.call
|
290
|
+
rescue Exception => ex
|
291
|
+
__assert(! ex.is_a?(expected)) {
|
292
|
+
"#{expected.inspect} should not be raised but got #{ex.inspect}."
|
293
|
+
}
|
294
|
+
end
|
295
|
+
end
|
296
|
+
self
|
297
|
+
end
|
298
|
+
|
299
|
+
#--
|
300
|
+
#def not_raise?
|
301
|
+
# _done()
|
302
|
+
# ! @bool or
|
303
|
+
# StandardError.new("ok().not_raise? is not available with '.NOT'.")
|
304
|
+
# proc_obj = @actual
|
305
|
+
# assert_nothing_raised(&proc_obj)
|
306
|
+
# self
|
307
|
+
#end
|
308
|
+
#++
|
309
|
+
|
310
|
+
def in?(expected)
|
311
|
+
_done()
|
312
|
+
__assert(@bool == !! expected.include?(@actual)) {
|
313
|
+
eq = @bool ? '' : ' == false'
|
314
|
+
"$expected.include?($actual)#{eq}: failed.\n"\
|
315
|
+
" $actual: #{@actual.inspect}\n"\
|
316
|
+
" $expected: #{expected.inspect}"
|
317
|
+
}
|
318
|
+
self
|
319
|
+
end
|
320
|
+
|
321
|
+
def include?(expected)
|
322
|
+
_done()
|
323
|
+
__assert(@bool == !! @actual.include?(expected)) {
|
324
|
+
eq = @bool ? '' : ' == false'
|
325
|
+
"$actual.include?($expected)#{eq}: failed.\n"\
|
326
|
+
" $actual: #{@actual.inspect}\n"\
|
327
|
+
" $expected: #{expected.inspect}"
|
328
|
+
}
|
329
|
+
self
|
330
|
+
end
|
331
|
+
|
332
|
+
def attr(name, val=nil)
|
333
|
+
_done()
|
334
|
+
d = name.is_a?(Hash) ? name : {name=>val}
|
335
|
+
d.each_pair do |k, v|
|
336
|
+
attr_val = @actual.__send__(k)
|
337
|
+
__assert(@bool == (attr_val == v)) {
|
338
|
+
op = @bool ? '==' : '!='
|
339
|
+
"$actual.#{k} #{op} $expected: failed.\n"\
|
340
|
+
" $actual.#{k}: #{attr_val}\n"\
|
341
|
+
" $expected: #{v.inspect}"\
|
342
|
+
}
|
343
|
+
end
|
344
|
+
self
|
345
|
+
end
|
346
|
+
|
347
|
+
def length(n)
|
348
|
+
_done()
|
349
|
+
__assert(@bool == (@actual.length == n)) {
|
350
|
+
op = @bool ? '==' : '!='
|
351
|
+
"$actual.length #{op} #{n}: failed.\n"\
|
352
|
+
" $actual.length: #{@actual.length}\n"\
|
353
|
+
" $actual: #{actual.inspect}"
|
354
|
+
}
|
355
|
+
self
|
356
|
+
end
|
357
|
+
|
358
|
+
#--
|
359
|
+
#def empty?; _done(); @actual.empty? or flunk "#{@actual}.empty?: failed."; self; end
|
360
|
+
#def not_empty?; _done(); ! @actual.empty? or flunk "! #{@actual}.empty?: failed."; self; end
|
361
|
+
#def truthy?; _done(); !! @actual == true or flunk "!! #{@actual} == true: failed."; self; end
|
362
|
+
#def falsy?; _done(); !! @actual == false or flunk "!! #{@actual} == false: failed."; self; end
|
363
|
+
#++
|
364
|
+
|
365
|
+
def truthy?
|
366
|
+
_done()
|
367
|
+
__assert(@bool == (!!@actual == true)) {
|
368
|
+
op = @bool ? '==' : '!='
|
369
|
+
"!!$actual #{op} true: failed.\n"\
|
370
|
+
" $actual: #{@actual.inspect}"
|
371
|
+
}
|
372
|
+
self
|
373
|
+
end
|
374
|
+
|
375
|
+
def falsy?
|
376
|
+
_done()
|
377
|
+
__assert(@bool == (!!@actual == false)) {
|
378
|
+
op = @bool ? '==' : '!='
|
379
|
+
"!!$actual #{op} false: failed.\n"\
|
380
|
+
" $actual: #{@actual.inspect}"
|
381
|
+
}
|
382
|
+
self
|
383
|
+
end
|
384
|
+
|
385
|
+
def file_exist?
|
386
|
+
_done()
|
387
|
+
__assert(@bool == File.file?(@actual)) {
|
388
|
+
eq = @bool ? '' : ' == false'
|
389
|
+
"File.file?($actual)#{eq}: failed.\n"\
|
390
|
+
" $actual: #{@actual.inspect}"
|
391
|
+
}
|
392
|
+
self
|
393
|
+
end
|
394
|
+
|
395
|
+
def dir_exist?
|
396
|
+
_done()
|
397
|
+
__assert(@bool == File.directory?(@actual)) {
|
398
|
+
eq = @bool ? '' : ' == false'
|
399
|
+
"File.directory?($actual)#{eq}: failed.\n"\
|
400
|
+
" $actual: #{@actual.inspect}"
|
401
|
+
}
|
402
|
+
self
|
403
|
+
end
|
404
|
+
|
405
|
+
def exist?
|
406
|
+
_done()
|
407
|
+
__assert(@bool == File.exist?(@actual)) {
|
408
|
+
eq = @bool ? '' : ' == false'
|
409
|
+
"File.exist?($actual)#{eq}: failed.\n"\
|
410
|
+
" $actual: #{@actual.inspect}"
|
411
|
+
}
|
412
|
+
self
|
413
|
+
end
|
414
|
+
|
415
|
+
end
|
416
|
+
|
417
|
+
|
418
|
+
class ScopeObject
|
419
|
+
|
420
|
+
def initialize
|
421
|
+
@children = []
|
422
|
+
@fixtures = {}
|
423
|
+
end
|
424
|
+
|
425
|
+
attr_accessor :name, :parent, :children, :before, :after, :before_all, :after_all, :fixtures
|
426
|
+
attr_accessor :_klass, :_prefix #:nodoc:
|
427
|
+
|
428
|
+
def add_child(child)
|
429
|
+
if child.is_a?(ScopeObject)
|
430
|
+
child.parent.nil? or
|
431
|
+
raise ArgumentError.new("add_child(): can't add child scope which already belongs to other.")
|
432
|
+
child.parent = self
|
433
|
+
end
|
434
|
+
@children << child
|
435
|
+
end
|
436
|
+
|
437
|
+
def get_fixture_info(name)
|
438
|
+
return @fixtures[name]
|
439
|
+
#@fixtures ? @fixtures[name] : nil # or [nil, nil, nil]?
|
440
|
+
end
|
441
|
+
|
442
|
+
def new_context
|
443
|
+
return @_klass.new
|
444
|
+
end
|
445
|
+
|
446
|
+
def accept(runner, *args)
|
447
|
+
raise NotImplementedError.new("#{self.class.name}#accept(): not implemented yet.")
|
448
|
+
end
|
449
|
+
|
450
|
+
def _repr(depth=0, buf="")
|
451
|
+
indent = " " * depth
|
452
|
+
buf << "#{indent}-\n"
|
453
|
+
instance_variables().sort.each do |name|
|
454
|
+
next if name.to_s == "@children"
|
455
|
+
buf << "#{indent} #{name}: #{instance_variable_get(name).inspect}\n"
|
456
|
+
end
|
457
|
+
@children.each {|child| child._repr(depth+1, buf) }
|
458
|
+
return buf
|
459
|
+
end
|
460
|
+
|
461
|
+
def +@
|
462
|
+
self
|
463
|
+
end
|
464
|
+
|
465
|
+
end
|
466
|
+
|
467
|
+
|
468
|
+
class FileScopeObject < ScopeObject
|
469
|
+
|
470
|
+
attr_accessor :filename
|
471
|
+
|
472
|
+
def initialize(filename=nil)
|
473
|
+
super()
|
474
|
+
@filename = filename
|
475
|
+
end
|
476
|
+
|
477
|
+
def accept(runner, *args)
|
478
|
+
return runner.run_topic(self, *args)
|
479
|
+
end
|
480
|
+
|
481
|
+
end
|
482
|
+
|
483
|
+
|
484
|
+
class TopicObject < ScopeObject
|
485
|
+
|
486
|
+
def initialize(name=nil)
|
487
|
+
super()
|
488
|
+
@name = name
|
489
|
+
end
|
490
|
+
|
491
|
+
def accept(runner, *args)
|
492
|
+
return runner.run_topic(self, *args)
|
493
|
+
end
|
494
|
+
|
495
|
+
end
|
496
|
+
|
497
|
+
|
498
|
+
module ScopeClassMethods
|
499
|
+
|
500
|
+
#attr_accessor :_scope
|
501
|
+
|
502
|
+
def before(&block); @_scope.before = block; end
|
503
|
+
def after(&block); @_scope.after = block; end
|
504
|
+
def before_all(&block); @_scope.before_all = block; end
|
505
|
+
def after_all(&block); @_scope.after_all = block; end
|
506
|
+
|
507
|
+
def fixture(name, &block)
|
508
|
+
location = caller(1).first
|
509
|
+
argnames = block.arity > 0 ? Util.block_argnames(block, location) : nil
|
510
|
+
@_scope.fixtures[name] = [block, argnames, location]
|
511
|
+
end
|
512
|
+
|
513
|
+
def topic(name, &block)
|
514
|
+
topic = TopicObject.new(name)
|
515
|
+
@_scope.add_child(topic)
|
516
|
+
klass = Class.new(self)
|
517
|
+
klass.class_eval do
|
518
|
+
extend ScopeClassMethods
|
519
|
+
include Test::Unit::Assertions
|
520
|
+
include SpecHelper
|
521
|
+
@_scope = topic
|
522
|
+
end
|
523
|
+
klass.class_eval(&block)
|
524
|
+
topic._klass = klass
|
525
|
+
topic._prefix = '*'
|
526
|
+
return topic
|
527
|
+
end
|
528
|
+
|
529
|
+
def case_when(desc, &block)
|
530
|
+
return __case_when("When #{desc}", &block)
|
531
|
+
end
|
532
|
+
|
533
|
+
def case_else(&block)
|
534
|
+
return __case_when("Else", &block)
|
535
|
+
end
|
536
|
+
|
537
|
+
def __case_when(desc, &block)
|
538
|
+
obj = topic(desc, &block)
|
539
|
+
obj._prefix = '-'
|
540
|
+
return obj
|
541
|
+
end
|
542
|
+
private :__case_when
|
543
|
+
|
544
|
+
def spec(desc, &block)
|
545
|
+
location = caller(1).first
|
546
|
+
if block
|
547
|
+
argnames = Util.block_argnames(block, location)
|
548
|
+
else
|
549
|
+
block = proc { raise TodoException.new("not implemented yet") }
|
550
|
+
argnames = []
|
551
|
+
end
|
552
|
+
spec = SpecObject.new(desc, block, argnames, location)
|
553
|
+
@_scope.add_child(spec)
|
554
|
+
spec._prefix = '-'
|
555
|
+
return spec
|
556
|
+
end
|
557
|
+
|
558
|
+
end
|
559
|
+
|
560
|
+
|
561
|
+
FILESCOPES = []
|
562
|
+
|
563
|
+
|
564
|
+
def self.scope(&block)
|
565
|
+
filename = caller(1).first =~ /:\d+/ ? $` : nil
|
566
|
+
filename = filename.sub(/\A\.\//, '')
|
567
|
+
scope = FileScopeObject.new(filename)
|
568
|
+
klass = Class.new
|
569
|
+
klass.class_eval do
|
570
|
+
extend ScopeClassMethods
|
571
|
+
@_scope = scope
|
572
|
+
end
|
573
|
+
klass.class_eval(&block)
|
574
|
+
scope._klass = klass
|
575
|
+
FILESCOPES << scope
|
576
|
+
return scope
|
577
|
+
end
|
578
|
+
|
579
|
+
|
580
|
+
class SpecObject
|
581
|
+
|
582
|
+
def initialize(desc, block, argnames, location)
|
583
|
+
@desc = desc
|
584
|
+
@block = block
|
585
|
+
@argnames = argnames
|
586
|
+
@location = location # necessary when raising fixture not found error
|
587
|
+
end
|
588
|
+
|
589
|
+
attr_reader :desc, :block, :argnames, :location #:nodoc:
|
590
|
+
attr_accessor :_prefix #:nodoc:
|
591
|
+
|
592
|
+
def accept(runner, *args) #:nodoc:
|
593
|
+
runner.run_spec(self, *args)
|
594
|
+
end
|
595
|
+
|
596
|
+
def _repr(depth=0, buf="") #:nodoc:
|
597
|
+
buf << " " * depth << "- #{@desc}\n"
|
598
|
+
return buf
|
599
|
+
end
|
600
|
+
|
601
|
+
def -@
|
602
|
+
self
|
603
|
+
end
|
604
|
+
|
605
|
+
end
|
606
|
+
|
607
|
+
|
608
|
+
module SpecHelper
|
609
|
+
|
610
|
+
attr_accessor :_TODO, :_at_end_blocks
|
611
|
+
|
612
|
+
def skip_when(condition, reason)
|
613
|
+
raise SkipException.new(reason) if condition
|
614
|
+
end
|
615
|
+
|
616
|
+
def TODO
|
617
|
+
@_TODO = true
|
618
|
+
end
|
619
|
+
|
620
|
+
def at_end(&block)
|
621
|
+
(@_at_end_blocks ||= []) << block
|
622
|
+
end
|
623
|
+
|
624
|
+
def tmp
|
625
|
+
unless @_tmp
|
626
|
+
require 'section9/tmp' unless defined?(Section9::Tmp)
|
627
|
+
@_tmp = Section9::Tmp.new
|
628
|
+
at_end { @_tmp.revert }
|
629
|
+
end
|
630
|
+
return @_tmp
|
631
|
+
end
|
632
|
+
|
633
|
+
def recorder
|
634
|
+
require 'section9/recorder' unless defined?(Section9::Recorder)
|
635
|
+
return Section9::Recorder.new
|
636
|
+
end
|
637
|
+
|
638
|
+
end
|
639
|
+
|
640
|
+
|
641
|
+
STATUSES = [:PASS, :FAIL, :ERROR, :SKIP, :TODO]
|
642
|
+
|
643
|
+
|
644
|
+
class Runner
|
645
|
+
|
646
|
+
def initialize(reporter)
|
647
|
+
@reporter = reporter
|
648
|
+
end
|
649
|
+
|
650
|
+
def run_topic(topic, depth, parent)
|
651
|
+
@reporter.enter_topic(topic, depth)
|
652
|
+
call_before_all_block(topic)
|
653
|
+
topic.children.each do |child|
|
654
|
+
child.accept(self, depth+1, topic)
|
655
|
+
end
|
656
|
+
call_after_all_block(topic)
|
657
|
+
@reporter.exit_topic(topic, depth)
|
658
|
+
end
|
659
|
+
|
660
|
+
def run_spec(spec, depth, parent)
|
661
|
+
return if ENV['SPEC'] && ENV['SPEC'] != spec.desc
|
662
|
+
@reporter.enter_spec(spec, depth)
|
663
|
+
topic = parent
|
664
|
+
context = new_context(topic, spec)
|
665
|
+
call_before_blocks(topic, context)
|
666
|
+
status = :PASS
|
667
|
+
ex = nil
|
668
|
+
begin
|
669
|
+
if spec.argnames.empty?
|
670
|
+
call_spec_block(spec, context)
|
671
|
+
else
|
672
|
+
values = get_fixture_values(spec.argnames, topic, spec, context)
|
673
|
+
call_spec_block(spec, context, *values)
|
674
|
+
end
|
675
|
+
rescue NoMemoryError => ex; raise ex
|
676
|
+
rescue SignalException => ex; raise ex
|
677
|
+
rescue FAIL_EXCEPTION => ex; status = :FAIL
|
678
|
+
rescue SKIP_EXCEPTION => ex; status = :SKIP
|
679
|
+
rescue TODO_EXCEPTION => ex; status = :TODO
|
680
|
+
rescue Exception => ex; status = :ERROR
|
681
|
+
end
|
682
|
+
if context._TODO
|
683
|
+
if status == :PASS
|
684
|
+
status = :FAIL
|
685
|
+
ex = FAIL_EXCEPTION.new("spec should be failed (because not implemented yet), but passed unexpectedly.")
|
686
|
+
elsif status == :FAIL
|
687
|
+
status = :TODO
|
688
|
+
ex = TODO_EXCEPTION.new("not implemented yet")
|
689
|
+
end
|
690
|
+
ex.set_backtrace([spec.location])
|
691
|
+
end
|
692
|
+
begin
|
693
|
+
call_at_end_blocks(context)
|
694
|
+
ensure
|
695
|
+
call_after_blocks(topic, context)
|
696
|
+
end
|
697
|
+
@reporter.exit_spec(spec, depth, status, ex, parent)
|
698
|
+
end
|
699
|
+
|
700
|
+
def run_all
|
701
|
+
@reporter.enter_all(self)
|
702
|
+
while (scope = FILESCOPES.shift)
|
703
|
+
run_filescope(scope)
|
704
|
+
end
|
705
|
+
@reporter.exit_all(self)
|
706
|
+
end
|
707
|
+
|
708
|
+
def run_filescope(filescope)
|
709
|
+
@reporter.enter_file(filescope.filename)
|
710
|
+
call_before_all_block(filescope)
|
711
|
+
filescope.children.each do |child|
|
712
|
+
child.accept(self, 0, nil)
|
713
|
+
end
|
714
|
+
call_after_all_block(filescope)
|
715
|
+
@reporter.exit_file(filescope.filename)
|
716
|
+
end
|
717
|
+
|
718
|
+
private
|
719
|
+
|
720
|
+
def new_context(topic, spec)
|
721
|
+
return topic.new_context()
|
722
|
+
end
|
723
|
+
|
724
|
+
def get_fixture_values(names, topic, spec, context)
|
725
|
+
return FixtureManager.instance.get_fixture_values(names, topic, spec, context)
|
726
|
+
end
|
727
|
+
|
728
|
+
def _call_blocks_parent_first(topic, name, obj)
|
729
|
+
blocks = []
|
730
|
+
while topic
|
731
|
+
block = topic.__send__(name)
|
732
|
+
blocks << block if block
|
733
|
+
topic = topic.parent
|
734
|
+
end
|
735
|
+
blocks.reverse.each {|blk| obj.instance_eval(&blk) }
|
736
|
+
blocks.clear
|
737
|
+
end
|
738
|
+
|
739
|
+
def _call_blocks_child_first(topic, name, obj)
|
740
|
+
while topic
|
741
|
+
block = topic.__send__(name)
|
742
|
+
obj.instance_eval(&block) if block
|
743
|
+
topic = topic.parent
|
744
|
+
end
|
745
|
+
end
|
746
|
+
|
747
|
+
def call_before_blocks(topic, spec)
|
748
|
+
_call_blocks_parent_first(topic, :before, spec)
|
749
|
+
end
|
750
|
+
|
751
|
+
def call_after_blocks(topic, spec)
|
752
|
+
_call_blocks_child_first(topic, :after, spec)
|
753
|
+
end
|
754
|
+
|
755
|
+
def call_before_all_block(topic)
|
756
|
+
block = topic.before_all
|
757
|
+
topic.instance_eval(&block) if block
|
758
|
+
end
|
759
|
+
|
760
|
+
def call_after_all_block(topic)
|
761
|
+
block = topic.after_all
|
762
|
+
topic.instance_eval(&block) if block
|
763
|
+
end
|
764
|
+
|
765
|
+
def call_spec_block(spec, context, *args)
|
766
|
+
if args.empty?
|
767
|
+
context.instance_eval(&spec.block)
|
768
|
+
else
|
769
|
+
context.instance_exec(*args, &spec.block)
|
770
|
+
end
|
771
|
+
end
|
772
|
+
|
773
|
+
def call_at_end_blocks(context)
|
774
|
+
blocks = context._at_end_blocks
|
775
|
+
if blocks
|
776
|
+
blocks.reverse_each {|block| context.instance_eval(&block) }
|
777
|
+
blocks.clear
|
778
|
+
end
|
779
|
+
end
|
780
|
+
|
781
|
+
end
|
782
|
+
|
783
|
+
|
784
|
+
RUNNER = Runner
|
785
|
+
|
786
|
+
|
787
|
+
class FixtureManager
|
788
|
+
|
789
|
+
def self.instance
|
790
|
+
return @instance ||= self.new
|
791
|
+
end
|
792
|
+
|
793
|
+
def get_fixture_values(names, topic, spec, context, location=nil, resolved={}, resolving=[])
|
794
|
+
location ||= spec.location
|
795
|
+
return names.collect {|name|
|
796
|
+
! resolving.include?(name) or
|
797
|
+
raise _looped_dependency_error(name, resolving, location)
|
798
|
+
get_fixture_value(name, topic, spec, context, location, resolved, resolving)
|
799
|
+
}
|
800
|
+
end
|
801
|
+
|
802
|
+
def get_fixture_value(name, topic, spec, context, location=nil, resolved={}, resolving=[])
|
803
|
+
return resolved[name] if resolved.key?(name)
|
804
|
+
location ||= spec.location
|
805
|
+
tuple = topic.get_fixture_info(name)
|
806
|
+
if tuple
|
807
|
+
block, argnames, location = tuple
|
808
|
+
if argnames
|
809
|
+
resolving << name
|
810
|
+
args = get_fixture_values(argnames, topic, spec, context, location, resolved, resolving)
|
811
|
+
(popped = resolving.pop) == name or
|
812
|
+
raise "** assertion failed: name=#{name.inspect}, resolvng[-1]=#{popped.inspect}"
|
813
|
+
val = context.instance_exec(*args, &block)
|
814
|
+
else
|
815
|
+
val = context.instance_eval(&block)
|
816
|
+
end
|
817
|
+
resolved[name] = val
|
818
|
+
return val
|
819
|
+
elsif topic.parent
|
820
|
+
return get_fixture_value(name, topic.parent, spec, context, location, resolved, resolving)
|
821
|
+
else
|
822
|
+
ex = FixtureNotFoundError.new("#{name}: fixture not found. (spec: #{spec.desc})")
|
823
|
+
ex.set_backtrace([location])
|
824
|
+
raise ex
|
825
|
+
end
|
826
|
+
end
|
827
|
+
|
828
|
+
private
|
829
|
+
|
830
|
+
def _looped_dependency_error(name, resolving, location)
|
831
|
+
resolving << name
|
832
|
+
i = resolving.index(name)
|
833
|
+
s1 = resolving[0...i].join('->')
|
834
|
+
s2 = resolving[i..-1].join('=>')
|
835
|
+
loop = s1.empty? ? s2 : "#{s1}->#{s2}"
|
836
|
+
#location = $1 if location =~ /(.*:\d+)/
|
837
|
+
ex = LoopedDependencyError.new("fixture dependency is looped: #{loop}")
|
838
|
+
ex.set_backtrace([location])
|
839
|
+
return ex
|
840
|
+
end
|
841
|
+
|
842
|
+
end
|
843
|
+
|
844
|
+
|
845
|
+
class FixtureNotFoundError < StandardError
|
846
|
+
end
|
847
|
+
|
848
|
+
class LoopedDependencyError < StandardError
|
849
|
+
end
|
850
|
+
|
851
|
+
|
852
|
+
class Reporter
|
853
|
+
|
854
|
+
def enter_all(runner); end
|
855
|
+
def exit_all(runner); end
|
856
|
+
def enter_file(filename); end
|
857
|
+
def exit_file(filename); end
|
858
|
+
def enter_topic(topic, depth); end
|
859
|
+
def exit_topic(topic, depth); end
|
860
|
+
def enter_spec(spec, depth); end
|
861
|
+
def exit_spec(spec, depth, status, error, parent); end
|
862
|
+
#
|
863
|
+
def counts; {}; end
|
864
|
+
|
865
|
+
end
|
866
|
+
|
867
|
+
|
868
|
+
class BaseReporter < Reporter
|
869
|
+
|
870
|
+
LABELS = { :PASS=>'pass', :FAIL=>'Fail', :ERROR=>'ERROR', :SKIP=>'Skip', :TODO=>'TODO' }
|
871
|
+
CHARS = { :PASS=>'.', :FAIL=>'f', :ERROR=>'E', :SKIP=>'s', :TODO=>'t' }
|
872
|
+
|
873
|
+
|
874
|
+
def initialize
|
875
|
+
@exceptions = []
|
876
|
+
@counts = {}
|
877
|
+
end
|
878
|
+
|
879
|
+
attr_reader :counts
|
880
|
+
|
881
|
+
def enter_all(runner)
|
882
|
+
reset_counts()
|
883
|
+
@start_at = Time.now
|
884
|
+
end
|
885
|
+
|
886
|
+
def exit_all(runner)
|
887
|
+
elapsed = Time.now - @start_at
|
888
|
+
puts footer(elapsed)
|
889
|
+
end
|
890
|
+
|
891
|
+
def enter_file(filename)
|
892
|
+
end
|
893
|
+
|
894
|
+
def exit_file(filename)
|
895
|
+
end
|
896
|
+
|
897
|
+
def enter_topic(topic, depth)
|
898
|
+
end
|
899
|
+
|
900
|
+
def exit_topic(topic, depth)
|
901
|
+
end
|
902
|
+
|
903
|
+
def enter_spec(spec, depth)
|
904
|
+
end
|
905
|
+
|
906
|
+
def exit_spec(spec, depth, status, ex, parent)
|
907
|
+
@counts[status] += 1
|
908
|
+
@exceptions << [spec, status, ex, parent] if status == :FAIL || status == :ERROR
|
909
|
+
end
|
910
|
+
|
911
|
+
protected
|
912
|
+
|
913
|
+
def reset_counts
|
914
|
+
STATUSES.each {|sym| @counts[sym] = 0 }
|
915
|
+
end
|
916
|
+
|
917
|
+
def print_exceptions
|
918
|
+
sep = '-' * 70
|
919
|
+
@exceptions.each do |tuple|
|
920
|
+
puts sep
|
921
|
+
print_exc(*tuple)
|
922
|
+
tuple.clear
|
923
|
+
end
|
924
|
+
puts sep if ! @exceptions.empty?
|
925
|
+
@exceptions.clear
|
926
|
+
end
|
927
|
+
|
928
|
+
def print_exc(spec, status, ex, parent)
|
929
|
+
label = Color.status(status, LABELS[status])
|
930
|
+
topic = parent
|
931
|
+
path = Color.topic(spec_path(spec, topic))
|
932
|
+
topic = parent
|
933
|
+
puts "[#{label}] #{path}"
|
934
|
+
print_exc_backtrace(ex, status)
|
935
|
+
print_exc_message(ex, status)
|
936
|
+
end
|
937
|
+
|
938
|
+
def print_exc_backtrace(ex, status)
|
939
|
+
rexp = FILENAME_FILTER
|
940
|
+
prev_file = prev_line = nil
|
941
|
+
ex.backtrace.each_with_index do |str, i|
|
942
|
+
next if str =~ rexp && ! (i == 0 && status == :ERROR)
|
943
|
+
linestr = nil
|
944
|
+
if str =~ /:(\d+)/
|
945
|
+
file = $` # file path
|
946
|
+
line = $1.to_i # line number
|
947
|
+
next if file == prev_file && line == prev_line
|
948
|
+
linestr = Util.file_line(file, line) if str && File.exist?(file)
|
949
|
+
prev_file, prev_line = file, line
|
950
|
+
end
|
951
|
+
puts " #{str}"
|
952
|
+
puts " #{linestr.strip}" if linestr
|
953
|
+
end
|
954
|
+
end
|
955
|
+
|
956
|
+
FILENAME_FILTER = %r`/(?:oktest|minitest/unit|test/unit(?:/assertions|/testcase)?)\.rbc?:` #:nodoc:
|
957
|
+
|
958
|
+
def print_exc_message(ex, status)
|
959
|
+
#puts Color.status(status, "#{ex.class.name}: #{ex}")
|
960
|
+
if status == :FAIL
|
961
|
+
msg = "#{ex}"
|
962
|
+
else
|
963
|
+
msg = "#{ex.class.name}: #{ex}"
|
964
|
+
end
|
965
|
+
lines = []
|
966
|
+
msg.each_line {|line| lines << line }
|
967
|
+
#puts Color.status(status, lines.shift.chomp)
|
968
|
+
puts lines.shift.chomp
|
969
|
+
puts lines.join.chomp unless lines.empty?
|
970
|
+
puts ex.diff if ex.respond_to?(:diff) && ex.diff # for oktest.rb
|
971
|
+
end
|
972
|
+
|
973
|
+
def footer(elapsed)
|
974
|
+
total = 0; @counts.each {|k, v| total += v }
|
975
|
+
buf = "## total:#{total}"
|
976
|
+
STATUSES.each do |st|
|
977
|
+
s = "#{st.to_s.downcase}:#{@counts[st]}"
|
978
|
+
s = Color.status(st, s) if @counts[st] > 0
|
979
|
+
buf << ", " << s
|
980
|
+
end
|
981
|
+
buf << " (in %.3fs)" % elapsed
|
982
|
+
return buf
|
983
|
+
end
|
984
|
+
|
985
|
+
def spec_path(spec, topic)
|
986
|
+
arr = []
|
987
|
+
while topic
|
988
|
+
arr << topic.name.to_s if topic.name
|
989
|
+
topic = topic.parent
|
990
|
+
end
|
991
|
+
arr.reverse!
|
992
|
+
arr << spec.desc
|
993
|
+
return arr.join(" > ")
|
994
|
+
end
|
995
|
+
|
996
|
+
end
|
997
|
+
|
998
|
+
|
999
|
+
class VerboseReporter < BaseReporter
|
1000
|
+
|
1001
|
+
LABELS = { :PASS=>'pass', :FAIL=>'Fail', :ERROR=>'ERROR', :SKIP=>'Skip', :TODO=>'TODO' }
|
1002
|
+
|
1003
|
+
def enter_topic(topic, depth)
|
1004
|
+
super
|
1005
|
+
puts "#{' ' * depth}#{topic._prefix} #{Color.topic(topic.name)}"
|
1006
|
+
end
|
1007
|
+
|
1008
|
+
def exit_topic(topic, depth)
|
1009
|
+
print_exceptions()
|
1010
|
+
end
|
1011
|
+
|
1012
|
+
def enter_spec(spec, depth)
|
1013
|
+
if $stdout.tty?
|
1014
|
+
str = "#{' ' * depth}#{spec._prefix} [ ] #{spec.desc}"
|
1015
|
+
print Util.strfold(str, 79)
|
1016
|
+
$stdout.flush
|
1017
|
+
end
|
1018
|
+
end
|
1019
|
+
|
1020
|
+
def exit_spec(spec, depth, status, error, parent)
|
1021
|
+
super
|
1022
|
+
if $stdout.tty?
|
1023
|
+
print "\r" # clear line
|
1024
|
+
$stdout.flush
|
1025
|
+
end
|
1026
|
+
label = Color.status(status, LABELS[status] || '???')
|
1027
|
+
msg = "#{' ' * depth}- [#{label}] #{spec.desc}"
|
1028
|
+
#msg << " (reason: #{error.message})" if error.is_a?(SkipException)
|
1029
|
+
msg << " " << Color.reason("(reason: #{error.message})") if error.is_a?(SkipException)
|
1030
|
+
puts msg
|
1031
|
+
end
|
1032
|
+
|
1033
|
+
end
|
1034
|
+
|
1035
|
+
|
1036
|
+
class SimpleReporter < BaseReporter
|
1037
|
+
|
1038
|
+
def enter_file(filename)
|
1039
|
+
print "#{filename}: "
|
1040
|
+
end
|
1041
|
+
|
1042
|
+
def exit_file(filename)
|
1043
|
+
puts
|
1044
|
+
print_exceptions()
|
1045
|
+
end
|
1046
|
+
|
1047
|
+
def exit_spec(spec, depth, status, error, parent)
|
1048
|
+
super
|
1049
|
+
print Color.status(status, CHARS[status] || '?')
|
1050
|
+
$stdout.flush
|
1051
|
+
end
|
1052
|
+
|
1053
|
+
end
|
1054
|
+
|
1055
|
+
|
1056
|
+
class PlainReporter < BaseReporter
|
1057
|
+
|
1058
|
+
def exit_all(runner)
|
1059
|
+
elapsed = Time.now - @start_at
|
1060
|
+
puts
|
1061
|
+
print_exceptions()
|
1062
|
+
puts footer(elapsed)
|
1063
|
+
end
|
1064
|
+
|
1065
|
+
def exit_spec(spec, depth, status, error, parent)
|
1066
|
+
super
|
1067
|
+
print Color.status(status, CHARS[status] || '?')
|
1068
|
+
$stdout.flush
|
1069
|
+
end
|
1070
|
+
|
1071
|
+
end
|
1072
|
+
|
1073
|
+
|
1074
|
+
REPORTER = VerboseReporter
|
1075
|
+
|
1076
|
+
|
1077
|
+
REPORTER_CLASSES = {
|
1078
|
+
'verbose' => VerboseReporter, 'v' => VerboseReporter,
|
1079
|
+
'simple' => SimpleReporter, 's' => SimpleReporter,
|
1080
|
+
'plain' => PlainReporter, 'p' => PlainReporter,
|
1081
|
+
}
|
1082
|
+
|
1083
|
+
|
1084
|
+
#def self.run(*topics)
|
1085
|
+
# opts = topics[-1].is_a?(Hash) ? topics.pop() : {}
|
1086
|
+
# topics = TOPICS if topics.empty?
|
1087
|
+
# return if topics.empty?
|
1088
|
+
# klass = (opts[:style] ? REPORTER_CLASSES[opts[:style]] : REPORTER) or
|
1089
|
+
# raise ArgumentError.new("#{opts[:style].inspect}: unknown style.")
|
1090
|
+
# reporter = klass.new
|
1091
|
+
# runner = Runner.new(reporter)
|
1092
|
+
# runner.run_all(*topics)
|
1093
|
+
# counts = reporter.counts
|
1094
|
+
# return counts[:FAIL] + counts[:ERROR]
|
1095
|
+
#end
|
1096
|
+
def self.run(opts={})
|
1097
|
+
return if FILESCOPES.empty?
|
1098
|
+
klass = (opts[:style] ? REPORTER_CLASSES[opts[:style]] : REPORTER) or
|
1099
|
+
raise ArgumentError.new("#{opts[:style].inspect}: unknown style.")
|
1100
|
+
reporter = klass.new
|
1101
|
+
runner = Runner.new(reporter)
|
1102
|
+
runner.run_all()
|
1103
|
+
FILESCOPES.clear
|
1104
|
+
counts = reporter.counts
|
1105
|
+
return counts[:FAIL] + counts[:ERROR]
|
1106
|
+
end
|
1107
|
+
|
1108
|
+
|
1109
|
+
module Util
|
1110
|
+
|
1111
|
+
module_function
|
1112
|
+
|
1113
|
+
def file_line(filename, linenum)
|
1114
|
+
return nil unless File.file?(filename)
|
1115
|
+
@cache ||= [nil, []]
|
1116
|
+
if @cache[0] != filename
|
1117
|
+
@cache[0] = filename
|
1118
|
+
@cache[1].clear
|
1119
|
+
@cache[1] = lines = File.open(filename, 'rb') {|f| f.to_a }
|
1120
|
+
else
|
1121
|
+
lines = @cache[1]
|
1122
|
+
end
|
1123
|
+
return lines[linenum-1]
|
1124
|
+
end
|
1125
|
+
|
1126
|
+
def block_argnames(block, location)
|
1127
|
+
return nil unless block
|
1128
|
+
return [] if block.arity <= 0
|
1129
|
+
if block.respond_to?(:parameters)
|
1130
|
+
argnames = block.parameters.collect {|pair| pair.last }
|
1131
|
+
else
|
1132
|
+
location =~ /:(\d+)/
|
1133
|
+
filename = $`
|
1134
|
+
linenum = $1.to_i
|
1135
|
+
File.file?(filename) or
|
1136
|
+
raise ArgumentError.new("block_argnames(): #{filename.inspect}: source file not found.")
|
1137
|
+
linestr = file_line(filename, linenum) || ""
|
1138
|
+
linestr =~ /(?:\bdo|\{) *\|(.*)\|/ or
|
1139
|
+
raise ArgumentError.new("spec(): can't detect block parameters at #{filename}:#{linenum}")
|
1140
|
+
argnames = $1.split(/,/).collect {|var| var.strip.intern }
|
1141
|
+
end
|
1142
|
+
return argnames
|
1143
|
+
end
|
1144
|
+
|
1145
|
+
def strfold(str, width=80, mark='...')
|
1146
|
+
return str if str.length <= width
|
1147
|
+
return str[0, width - mark.length] + mark if str =~ /\A[\x00-\x7f]*\z/
|
1148
|
+
limit = width - mark.length
|
1149
|
+
w = len = 0
|
1150
|
+
str.split(//u).each do |ch|
|
1151
|
+
n = ch.length
|
1152
|
+
w += n == 1 ? 1 : 2
|
1153
|
+
break if w >= limit
|
1154
|
+
len += n
|
1155
|
+
end
|
1156
|
+
str = str[0, len] + mark if w >= limit
|
1157
|
+
return str
|
1158
|
+
end
|
1159
|
+
def strfold(str, width=80, mark='...')
|
1160
|
+
return str if str.bytesize <= width
|
1161
|
+
return str[0, width - mark.length] + mark if str.ascii_only?
|
1162
|
+
limit = width - mark.length
|
1163
|
+
w = len = 0
|
1164
|
+
str.each_char do |ch|
|
1165
|
+
w += ch.bytesize == 1 ? 1 : 2
|
1166
|
+
break if w >= limit
|
1167
|
+
len += 1
|
1168
|
+
end
|
1169
|
+
str = str[0, len] + mark if w >= limit
|
1170
|
+
return str
|
1171
|
+
end if RUBY_VERSION >= '1.9'
|
1172
|
+
|
1173
|
+
|
1174
|
+
def chain_errors(*errors)
|
1175
|
+
end
|
1176
|
+
|
1177
|
+
|
1178
|
+
def _text2lines(text, no_newline_msg=nil)
|
1179
|
+
lines = []
|
1180
|
+
text.each_line {|line| line.chomp!; lines << line }
|
1181
|
+
lines[-1] << no_newline_msg if no_newline_msg && text[-1] && text[-1] != ?\n
|
1182
|
+
return lines
|
1183
|
+
end
|
1184
|
+
private :_text2lines
|
1185
|
+
|
1186
|
+
## platform independent, but requires 'diff-lcs' gem
|
1187
|
+
def unified_diff(text_old, text_new, label="--- old\n+++ new\n", context=3)
|
1188
|
+
msg = "\\ No newline at end of string"
|
1189
|
+
lines_old = _text2lines(text_old, msg)
|
1190
|
+
lines_new = _text2lines(text_new, msg)
|
1191
|
+
#
|
1192
|
+
buf = "#{label}"
|
1193
|
+
len = 0
|
1194
|
+
prevhunk = hunk = nil
|
1195
|
+
diffs = Diff::LCS.diff(lines_old, lines_new)
|
1196
|
+
diffs.each do |diff|
|
1197
|
+
hunk = Diff::LCS::Hunk.new(lines_old, lines_new, diff, context, len)
|
1198
|
+
if hunk.overlaps?(prevhunk)
|
1199
|
+
hunk.unshift(prevhunk)
|
1200
|
+
else
|
1201
|
+
buf << prevhunk.diff(:unified) << "\n"
|
1202
|
+
end if prevhunk
|
1203
|
+
len = hunk.file_length_difference
|
1204
|
+
prevhunk = hunk
|
1205
|
+
end
|
1206
|
+
buf << prevhunk.diff(:unified) << "\n" if prevhunk
|
1207
|
+
return buf
|
1208
|
+
end
|
1209
|
+
|
1210
|
+
## platform depend, but not require extra library
|
1211
|
+
def diff_unified(text_old, text_new, label="--- old\n+++ new\n", context=3)
|
1212
|
+
tmp_old = "_tmp.old.#{rand()}"
|
1213
|
+
tmp_new = "_tmp.new.#{rand()}"
|
1214
|
+
File.open(tmp_old, 'w') {|f| f.write(text_old) }
|
1215
|
+
File.open(tmp_new, 'w') {|f| f.write(text_new) }
|
1216
|
+
begin
|
1217
|
+
#diff = `diff -u #{tmp_old} #{tmp_new}`
|
1218
|
+
diff = `diff --unified=#{context} #{tmp_old} #{tmp_new}`
|
1219
|
+
ensure
|
1220
|
+
File.unlink(tmp_old)
|
1221
|
+
File.unlink(tmp_new)
|
1222
|
+
end
|
1223
|
+
diff.sub!(/\A\-\-\-.*\n\+\+\+.*\n/, label.to_s)
|
1224
|
+
return diff
|
1225
|
+
end
|
1226
|
+
|
1227
|
+
## when diff-lcs is not installed then use diff command
|
1228
|
+
begin
|
1229
|
+
require 'diff/lcs'
|
1230
|
+
#require 'diff/lcs/string'
|
1231
|
+
require 'diff/lcs/hunk'
|
1232
|
+
rescue LoadError
|
1233
|
+
alias _unified_diff unified_diff
|
1234
|
+
alias unified_diff diff_unified
|
1235
|
+
class << self
|
1236
|
+
alias _unified_diff unified_diff
|
1237
|
+
alias unified_diff diff_unified
|
1238
|
+
end
|
1239
|
+
end
|
1240
|
+
|
1241
|
+
end
|
1242
|
+
|
1243
|
+
|
1244
|
+
class Config
|
1245
|
+
|
1246
|
+
@os_windows = RUBY_PLATFORM =~ /mswin|mingw/i
|
1247
|
+
@auto_run = true
|
1248
|
+
@color_available = ! @os_windows
|
1249
|
+
@color_enabled = @color_available
|
1250
|
+
@diff_command = @os_windows ? "diff.exe -u" : "diff -u"
|
1251
|
+
@system_exit = true # exit() on Oktest.main()
|
1252
|
+
|
1253
|
+
class << self
|
1254
|
+
attr_accessor :auto_run, :color_available, :color_enabled, :system_exit
|
1255
|
+
end
|
1256
|
+
|
1257
|
+
end
|
1258
|
+
|
1259
|
+
|
1260
|
+
module Color
|
1261
|
+
|
1262
|
+
module_function
|
1263
|
+
|
1264
|
+
def normal (s); return s; end
|
1265
|
+
def bold (s); return "\x1b[0;1m#{s}\x1b[22m"; end
|
1266
|
+
def black (s); return "\x1b[1;30m#{s}\x1b[0m"; end
|
1267
|
+
def red (s); return "\x1b[1;31m#{s}\x1b[0m"; end
|
1268
|
+
def green (s); return "\x1b[1;32m#{s}\x1b[0m"; end
|
1269
|
+
def yellow (s); return "\x1b[1;33m#{s}\x1b[0m"; end
|
1270
|
+
def blue (s); return "\x1b[1;34m#{s}\x1b[0m"; end
|
1271
|
+
def magenta(s); return "\x1b[1;35m#{s}\x1b[0m"; end
|
1272
|
+
def cyan (s); return "\x1b[1;36m#{s}\x1b[0m"; end
|
1273
|
+
def white (s); return "\x1b[1;37m#{s}\x1b[0m"; end
|
1274
|
+
|
1275
|
+
def topic(s); Config.color_enabled ? bold(s) : s; end
|
1276
|
+
def spec (s); Config.color_enabled ? normal(s) : s; end
|
1277
|
+
def pass (s); Config.color_enabled ? green(s) : s; end
|
1278
|
+
def fail (s); Config.color_enabled ? red(s) : s; end
|
1279
|
+
def error(s); Config.color_enabled ? red(s) : s; end
|
1280
|
+
def skip (s); Config.color_enabled ? yellow(s) : s; end
|
1281
|
+
def todo (s); Config.color_enabled ? yellow(s) : s; end
|
1282
|
+
def reason(s); Config.color_enabled ? yellow(s) : s; end
|
1283
|
+
def status(status, s); __send__(status.to_s.downcase, s); end
|
1284
|
+
|
1285
|
+
end
|
1286
|
+
|
1287
|
+
|
1288
|
+
class TestGenerator
|
1289
|
+
|
1290
|
+
def parse(io)
|
1291
|
+
tree = _parse(io, [], nil)
|
1292
|
+
return tree
|
1293
|
+
end
|
1294
|
+
|
1295
|
+
def _parse(io, tree, end_indent)
|
1296
|
+
while (line = io.gets())
|
1297
|
+
case line
|
1298
|
+
when /^([ \t]*)end\b/
|
1299
|
+
return tree if $1 == end_indent
|
1300
|
+
when /^([ \t]*)(module|class|def) +(\w+[.:\w]*)/
|
1301
|
+
indent, keyword, topic = $1, $2, $3
|
1302
|
+
next if line =~ /\bend$/
|
1303
|
+
if keyword == 'def'
|
1304
|
+
topic = topic =~ /^self\./ ? ".#{$'}" : "\##{topic}"
|
1305
|
+
end
|
1306
|
+
newtree = []
|
1307
|
+
_parse(io, newtree, indent)
|
1308
|
+
tree << [indent, keyword, topic, newtree]
|
1309
|
+
when /^([ \t]*)\#[:;] (.*)/
|
1310
|
+
indent, keyword, spec = $1, 'spec', $2
|
1311
|
+
tree << [indent, keyword, spec]
|
1312
|
+
end
|
1313
|
+
end
|
1314
|
+
end_indent == nil or
|
1315
|
+
raise "parse error: end_indent=#{end_indent.inspect}"
|
1316
|
+
return tree
|
1317
|
+
end
|
1318
|
+
private :_parse
|
1319
|
+
|
1320
|
+
def transform(tree, depth=0)
|
1321
|
+
buf = ''
|
1322
|
+
tree.each do |tuple|
|
1323
|
+
_transform(tuple, depth, buf)
|
1324
|
+
end
|
1325
|
+
return buf
|
1326
|
+
end
|
1327
|
+
|
1328
|
+
def _transform(tuple, depth, buf)
|
1329
|
+
indent = ' ' * depth
|
1330
|
+
keyword = tuple[1]
|
1331
|
+
if keyword == 'spec'
|
1332
|
+
_, _, spec = tuple
|
1333
|
+
#buf << "\n"
|
1334
|
+
#buf << "#{indent}spec \"#{spec}\" do\n"
|
1335
|
+
#buf << "#{indent}end\n"
|
1336
|
+
escaped = spec.gsub(/"/, '\\\"')
|
1337
|
+
buf << "#{indent}spec \"#{escaped}\"\n"
|
1338
|
+
#buf << "#{indent}spec \"#{spec.gsub(/"/, '\\\"')}\"\n" #
|
1339
|
+
else
|
1340
|
+
_, _, topic, children = tuple
|
1341
|
+
buf << "\n"
|
1342
|
+
buf << "#{indent}topic '#{topic}' do\n" if keyword == 'def'
|
1343
|
+
buf << "#{indent}topic #{topic} do\n" if keyword != 'def'
|
1344
|
+
buf << "\n" unless keyword == 'def'
|
1345
|
+
children.each do |child_tuple|
|
1346
|
+
_transform(child_tuple, depth+1, buf)
|
1347
|
+
end
|
1348
|
+
buf << "\n"
|
1349
|
+
buf << "#{indent}end\n" if keyword == 'def'
|
1350
|
+
buf << "#{indent}end # #{topic}\n" if keyword != 'def'
|
1351
|
+
buf << "\n"
|
1352
|
+
end
|
1353
|
+
end
|
1354
|
+
private :_transform
|
1355
|
+
|
1356
|
+
def generate(io)
|
1357
|
+
tree = parse(io)
|
1358
|
+
return <<END
|
1359
|
+
# -*- coding: utf-8 -*-
|
1360
|
+
|
1361
|
+
require 'oktest'
|
1362
|
+
|
1363
|
+
Oktest.scope do
|
1364
|
+
|
1365
|
+
#{transform(tree)}
|
1366
|
+
end
|
1367
|
+
END
|
1368
|
+
end
|
1369
|
+
|
1370
|
+
end
|
1371
|
+
|
1372
|
+
|
1373
|
+
class MainApp
|
1374
|
+
|
1375
|
+
def main(args=nil)
|
1376
|
+
require 'optparse'
|
1377
|
+
begin
|
1378
|
+
status = run(args)
|
1379
|
+
exit(status) if Config.system_exit && status
|
1380
|
+
return status || 0
|
1381
|
+
rescue OptionParser::InvalidOption => ex
|
1382
|
+
command ||= File.basename($0)
|
1383
|
+
$stderr.write("#{command}: #{ex}\n")
|
1384
|
+
exit(1) if Config.system_exit
|
1385
|
+
return 1
|
1386
|
+
end
|
1387
|
+
end
|
1388
|
+
|
1389
|
+
def run(args=nil)
|
1390
|
+
## parse command-line options
|
1391
|
+
args = ARGV.dup if args.nil?
|
1392
|
+
#opts = Options.new
|
1393
|
+
#parser = option_parser(opts)
|
1394
|
+
#filenames = parser.parse(args)
|
1395
|
+
cmdopt = option_parser()
|
1396
|
+
opts = cmdopt.parse(args)
|
1397
|
+
filenames = args
|
1398
|
+
## help, version
|
1399
|
+
if opts.help
|
1400
|
+
puts help_message()
|
1401
|
+
return 0
|
1402
|
+
end
|
1403
|
+
if opts.version
|
1404
|
+
puts VERSION
|
1405
|
+
return 0
|
1406
|
+
end
|
1407
|
+
## fix not to load this file twice.
|
1408
|
+
$" << __FILE__ unless $".include?(__FILE__)
|
1409
|
+
##
|
1410
|
+
if opts.generate
|
1411
|
+
## generate test code from source code
|
1412
|
+
generate(filenames)
|
1413
|
+
exit()
|
1414
|
+
else
|
1415
|
+
## load and run
|
1416
|
+
load_files(filenames)
|
1417
|
+
Oktest::Config.auto_run = false
|
1418
|
+
n_errors = Oktest.run(:style=>opts.style)
|
1419
|
+
AssertionObject.report_not_yet()
|
1420
|
+
return n_errors
|
1421
|
+
end
|
1422
|
+
end
|
1423
|
+
|
1424
|
+
private
|
1425
|
+
|
1426
|
+
class Options #:nodoc:
|
1427
|
+
attr_accessor :help, :version, :style
|
1428
|
+
end
|
1429
|
+
|
1430
|
+
#def option_parser(opts)
|
1431
|
+
# require 'optparse'
|
1432
|
+
# parser = OptionParser.new
|
1433
|
+
# parser.on('-h', '--help') {|val| opts.help = val }
|
1434
|
+
# parser.on('-v', '--version') {|val| opts.version = val }
|
1435
|
+
# parser.on('-s STYLE') {|val|
|
1436
|
+
# REPORTER_CLASSES.key?(val) or
|
1437
|
+
# raise OptionParser::InvalidOption.new("-s #{val}: unknown style.")
|
1438
|
+
# opts.style = val
|
1439
|
+
# }
|
1440
|
+
# return parser
|
1441
|
+
#end
|
1442
|
+
def option_parser()
|
1443
|
+
require 'section9/cmdopt'
|
1444
|
+
cmdopt = Section9::Cmdopt.new
|
1445
|
+
cmdopt.option("-h, --help", "show help")
|
1446
|
+
cmdopt.option("-v, --version", "print version")
|
1447
|
+
cmdopt.option("-s STYLE #style", "report style (verbose/simple/plain, or v/s/p)")\
|
1448
|
+
.validation {|val| "unknown style." unless REPORTER_CLASSES.key?(val) }
|
1449
|
+
cmdopt.option("-g, --generate", "genearte test code from source file")
|
1450
|
+
return cmdopt
|
1451
|
+
end
|
1452
|
+
|
1453
|
+
def help_message(command=nil)
|
1454
|
+
command ||= File.basename($0)
|
1455
|
+
buf = "Usage: #{command} [options] [file or directory...]\n"
|
1456
|
+
buf << " -h, --help : show help\n"
|
1457
|
+
buf << " -v, --version : print version\n"
|
1458
|
+
buf << " -s STYLE : report style (verbose/simple/plain, or v/s/p)\n"
|
1459
|
+
return buf
|
1460
|
+
end
|
1461
|
+
|
1462
|
+
def load_files(filenames)
|
1463
|
+
## file exists?
|
1464
|
+
filenames.each do |fname|
|
1465
|
+
File.exist?(fname) or
|
1466
|
+
raise OptionParser::InvalidOption.new("#{fname}: not found.")
|
1467
|
+
end
|
1468
|
+
## load files
|
1469
|
+
filenames.each do |fname|
|
1470
|
+
File.directory?(fname) ? load_dir(fname) : load(fname)
|
1471
|
+
end
|
1472
|
+
end
|
1473
|
+
|
1474
|
+
def load_dir(dir, pattern=/^(test_.*|.*_test)\.rb$/)
|
1475
|
+
Dir.glob("#{dir}/*").each do |path|
|
1476
|
+
if File.directory?(path)
|
1477
|
+
load_dir(path)
|
1478
|
+
elsif File.file?(path)
|
1479
|
+
load(path) if File.basename(path) =~ pattern
|
1480
|
+
else
|
1481
|
+
raise ArgumentError.new("#{path}: not a file nor directory.")
|
1482
|
+
end
|
1483
|
+
end
|
1484
|
+
end
|
1485
|
+
|
1486
|
+
def generate(filenames)
|
1487
|
+
filenames.each do |fname|
|
1488
|
+
generator = TestGenerator.new
|
1489
|
+
File.open(fname) do |f|
|
1490
|
+
print generator.generate(f)
|
1491
|
+
end
|
1492
|
+
end
|
1493
|
+
end
|
1494
|
+
|
1495
|
+
end
|
1496
|
+
|
1497
|
+
|
1498
|
+
def self.main(args=nil)
|
1499
|
+
return MainApp.new.main(args)
|
1500
|
+
end
|
1501
|
+
|
1502
|
+
|
1503
|
+
end
|
1504
|
+
|
1505
|
+
|
1506
|
+
Test::Unit::Assertions.module_eval do
|
1507
|
+
def ok
|
1508
|
+
location = caller(1).first
|
1509
|
+
actual = yield
|
1510
|
+
ass = Oktest::AssertionObject.new(actual, true, location)
|
1511
|
+
Oktest::AssertionObject::NOT_YET[ass.__id__] = ass
|
1512
|
+
return ass
|
1513
|
+
end
|
1514
|
+
end
|
1515
|
+
|
1516
|
+
|
1517
|
+
at_exit do
|
1518
|
+
if Oktest::FILESCOPES.empty?
|
1519
|
+
if defined?(Test::Unit::Runner)
|
1520
|
+
Test::Unit::Runner.class_variable_set(:@@stop_auto_run, false)
|
1521
|
+
end
|
1522
|
+
else
|
1523
|
+
ex = $!
|
1524
|
+
if (! ex || ex.is_a?(SystemExit)) && Oktest::Config.auto_run
|
1525
|
+
Oktest.main()
|
1526
|
+
raise ex if ex
|
1527
|
+
end
|
1528
|
+
end
|
1529
|
+
end
|
1530
|
+
|
1531
|
+
|
1532
|
+
if __FILE__ == $0
|
1533
|
+
## prevent to load oktest.rb twice
|
1534
|
+
$LOADED_FEATURES << File.expand_path(__FILE__)
|
1535
|
+
## run test scripts
|
1536
|
+
Oktest.main()
|
1537
|
+
end
|