oktest 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/Rakefile.rb ADDED
@@ -0,0 +1,61 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ ###
4
+ ### $Release: 1.0.0 $
5
+ ### $Copyright: copyright(c) 2011-2021 kuwata-lab.com all rights reserved $
6
+ ### $License: MIT License $
7
+ ###
8
+
9
+ PROJECT = "oktest"
10
+ RELEASE = ENV['RELEASE'] || "0.0.0"
11
+ COPYRIGHT = "copyright(c) 2011-2021 kuwata-lab.com all rights reserved"
12
+ LICENSE = "MIT License"
13
+
14
+ $ruby_versions ||= %w[2.4 2.5 2.6 2.7 3.0]
15
+
16
+ desc "show release guide"
17
+ task :guide do
18
+ RELEASE != '0.0.0' or abort "rake help: required 'RELEASE=X.X.X'"
19
+ rel, proj = RELEASE, PROJECT
20
+ rel =~ /(\d+\.\d+)/
21
+ branch = "ruby-#{$1}"
22
+ puts <<END
23
+ How to release:
24
+
25
+ $ which ruby
26
+ $ rake test
27
+ $ rake test:all
28
+ $ rake readme:execute # optional
29
+ $ rake readme:toc # optional
30
+ $ git diff .
31
+ $ git status .
32
+ $ git checkout -b #{branch}
33
+ $ rake edit RELEASE=#{rel}
34
+ $ git add -u
35
+ $ git commit -m "ruby: preparation for release #{rel}"
36
+ $ vim CHANGES.md
37
+ $ git add -p CHANGES.md
38
+ $ git commit -m "ruby: update 'CHANGES.md'"
39
+ $ git log -1
40
+ $ rake package RELEASE=#{rel}
41
+ $ rake package:extract # confirm files in gem file
42
+ $ pushd #{proj}-#{rel}/data; find . -type f; popd
43
+ $ gem install #{proj}-#{rel}.gem # confirm gem package
44
+ $ gem uninstall #{proj}
45
+ $ gem push #{proj}-#{rel}.gem # publish gem to rubygems.org
46
+ $ git tag rb-#{rel}
47
+ $ git push -u origin #{branch}
48
+ $ git push --tags
49
+ $ rake clean
50
+ $ git checkout ruby
51
+ $ git cherry-pick xxxxxxxxx # cherry-pick update of CHANGES.md
52
+ END
53
+ end
54
+
55
+ README_EXTRACT = /^(test\/.*_test\.rb):/
56
+
57
+ Dir.glob("./task/*.rb").each {|x| require_relative x }
58
+
59
+ def readme_extract_callback(filename, str)
60
+ return str
61
+ end
@@ -0,0 +1,230 @@
1
+ # coding: utf-8
2
+
3
+
4
+ $nfiles = (ENV['N'] || 100).to_i
5
+ $ntopics = (ENV['T'] || 100).to_i
6
+ $nspecs = (ENV['S'] || 10).to_i
7
+ $tail = ENV['TAIL'] != 'off'
8
+ ENV['N'] = nil if ENV['N'] # to avoid warning of minitest
9
+
10
+
11
+ require 'erb'
12
+
13
+
14
+ class Renderer
15
+
16
+ TEMPLATE_OKTEST = <<END
17
+ # coding: utf-8
18
+
19
+ require 'oktest'
20
+
21
+ Oktest.scope do
22
+ <% for i in 1..(ntopics/10) %>
23
+
24
+ topic "Example #<%= i %>" do
25
+ <% for j in 1..10 %>
26
+
27
+ topic "Example #<%= i %>-<%= j %>" do
28
+
29
+ <% for k in 1..nspecs %>
30
+ spec "#<%= k %>: 1+1 should be 2" do
31
+ ok {1+1} == 2
32
+ end
33
+ <% end %>
34
+
35
+ end
36
+ <% end %>
37
+
38
+ end
39
+
40
+ <% end %>
41
+ end
42
+ END
43
+
44
+ TEMPLATE_RSPEC = <<'END'
45
+ # coding: utf-8
46
+
47
+ <% for i in 1..(ntopics/10) %>
48
+
49
+ RSpec.describe "Example #<%= i %>" do
50
+ <% for j in 1..10 %>
51
+
52
+ describe "Example #<%= i %>-<%= j %>" do
53
+
54
+ <% for k in 1..nspecs %>
55
+ it "#<%= k %>: 1+1 should be 2" do
56
+ expect(1+1).to eq 2
57
+ end
58
+ <% end %>
59
+
60
+ end
61
+ <% end %>
62
+
63
+ end
64
+ <% end %>
65
+ END
66
+
67
+ TEMPLATE_MINITEST = <<'END'
68
+ # coding: utf-8
69
+
70
+ require 'minitest/spec'
71
+ require 'minitest/autorun'
72
+
73
+ <% for i in 1..(ntopics/10) %>
74
+
75
+ describe "Example #<%= i %>" do
76
+ <% for j in 1..10 %>
77
+
78
+ describe "Example #<% i %>-<%= j %>" do
79
+
80
+ <% for k in 1..nspecs %>
81
+ it "#<%= k %>: 1+1 should be 2" do
82
+ assert_equal 2, 1+1
83
+ end
84
+ <% end %>
85
+
86
+ end
87
+ <% end %>
88
+
89
+ end
90
+ <% end %>
91
+ END
92
+
93
+ TEMPLATE_TESTUNIT = <<'END'
94
+ # coding: utf-8
95
+
96
+ require 'test/unit'
97
+
98
+ <% n = nfile %>
99
+ <% for i in 1..(ntopics/10) %>
100
+
101
+ class Example_<%= n %>_<%= i %>_TC < Test::Unit::TestCase
102
+ <% for j in 1..10 %>
103
+
104
+ class Example_<%= n %>_<%= i %>_<%= j %>_TC < self
105
+
106
+ <% for k in 1..nspecs %>
107
+ def test_<%= n %>_<%= i %>_<%= j %>_<%= k %>()
108
+ #assert 1+1 == 2
109
+ assert_equal 2, 1+1
110
+ end
111
+ <% end %>
112
+
113
+ end
114
+ <% end %>
115
+
116
+ end
117
+ <% end %>
118
+ END
119
+
120
+ def render(template_string, binding_)
121
+ ERB.new(template_string, nil, '<>').result(binding_)
122
+ end
123
+
124
+ def render_oktest(nfile, ntopics, nspecs)
125
+ render(TEMPLATE_OKTEST, binding())
126
+ end
127
+
128
+ def render_rspec(nfile, ntopics, nspecs)
129
+ render(TEMPLATE_RSPEC, binding())
130
+ end
131
+
132
+ def render_minitest(nfile, ntopics, nspecs)
133
+ render(TEMPLATE_MINITEST, binding())
134
+ end
135
+
136
+ def render_testunit(nfile, ntopics, nspecs)
137
+ render(TEMPLATE_TESTUNIT, binding())
138
+ end
139
+
140
+ end
141
+
142
+
143
+ task :default do
144
+ system "rake -T"
145
+ end
146
+
147
+
148
+ namespace :benchmark do
149
+
150
+ desc "remove 'example*_tst.rb' files"
151
+ task :clean do
152
+ FileUtils.rm_f Dir["example*_test.rb"]
153
+ end
154
+
155
+ def generate_files(&b)
156
+ nfiles, ntopics, nspecs = $nfiles, $ntopics, $nspecs
157
+ for n in 1..nfiles do
158
+ content = yield n, ntopics, nspecs
159
+ filename = "example%04d_test.rb" % n
160
+ File.write(filename, content)
161
+ end
162
+ end
163
+
164
+ def time(format=nil, &b)
165
+ pt1 = Process.times()
166
+ t1 = Time.now()
167
+ yield
168
+ t2 = Time.now()
169
+ pt2 = Process.times()
170
+ user = pt2.cutime - pt1.cutime
171
+ sys = pt2.cstime - pt1.cstime
172
+ real = t2 - t1
173
+ format ||= "\n %.3f real %.3f user %.3f sys\n"
174
+ $stderr.print format % [real, user, sys]
175
+ end
176
+
177
+ desc "start oktest benchmark"
178
+ task :oktest => :clean do
179
+ r = Renderer.new
180
+ generate_files {|*args| r.render_oktest(*args) }
181
+ #time { sh "oktest -sp run_all.rb" }
182
+ time { sh "oktest -sq run_all.rb" }
183
+ end
184
+
185
+ desc "start oktest benchmark (with '--faster' option)"
186
+ task :'oktest:faster' => :clean do
187
+ r = Renderer.new
188
+ generate_files {|*args| r.render_oktest(*args) }
189
+ #time { sh "oktest -sp --faster run_all.rb" }
190
+ time { sh "oktest -sq --faster run_all.rb" }
191
+ end
192
+
193
+ desc "start rspec benchmark"
194
+ task :rspec do
195
+ r = Renderer.new
196
+ generate_files {|*args| r.render_rspec(*args) }
197
+ time { sh "rspec run_all.rb | tail -4" } if $tail
198
+ time { sh "rspec run_all.rb" } unless $tail
199
+ end
200
+
201
+ desc "start minitest benchmark"
202
+ task :minitest do
203
+ r = Renderer.new
204
+ generate_files {|*args| r.render_minitest(*args) }
205
+ time { sh "ruby run_all.rb | tail -4" } if $tail
206
+ time { sh "ruby run_all.rb" } unless $tail
207
+ end
208
+
209
+ desc "start testunit benchmark"
210
+ task :testunit do
211
+ r = Renderer.new
212
+ generate_files {|*args| r.render_testunit(*args) }
213
+ time { sh "ruby run_all.rb | tail -5" } if $tail
214
+ time { sh "ruby run_all.rb" } unless $tail
215
+ end
216
+
217
+ desc "start all benchmarks"
218
+ task :all do
219
+ #$tail = true
220
+ interval = 0
221
+ [:oktest, :'oktest:faster', :rspec, :minitest, :testunit].each do |sym|
222
+ puts ""
223
+ sleep interval; interval = 1
224
+ puts "==================== #{sym} ===================="
225
+ #sh "rake benchmark:#{sym}"
226
+ Rake.application["benchmark:#{sym}"].invoke()
227
+ end
228
+ end
229
+
230
+ end
@@ -0,0 +1,6 @@
1
+ # coding: utf-8
2
+
3
+ Dir.glob(File.join(File.dirname(__FILE__), '*_test.rb')).each do |x|
4
+ #load x
5
+ require_relative x
6
+ end
data/bin/oktest ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env ruby
2
+ require 'oktest'
3
+ Oktest.main()
data/lib/oktest.rb ADDED
@@ -0,0 +1,2359 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ ###
4
+ ### $Release: 1.0.0 $
5
+ ### $Copyright: copyright(c) 2011-2021 kuwata-lab.com all rights reserved $
6
+ ### $License: MIT License $
7
+ ###
8
+
9
+
10
+ module Oktest
11
+
12
+
13
+ VERSION = '$Release: 1.0.0 $'.split()[1]
14
+
15
+
16
+ class OktestError < StandardError
17
+ end
18
+
19
+
20
+ class AssertionFailed < StandardError
21
+ end
22
+
23
+ class SkipException < StandardError
24
+ end
25
+
26
+ class TodoException < StandardError
27
+ end
28
+
29
+ #FAIL_EXCEPTION = (defined?(MiniTest) ? MiniTest::Assertion :
30
+ # defined?(Test::Unit) ? Test::Unit::AssertionFailedError : AssertionFailed)
31
+ FAIL_EXCEPTION = AssertionFailed
32
+ SKIP_EXCEPTION = SkipException
33
+ TODO_EXCEPTION = TodoException
34
+
35
+
36
+ class AssertionObject
37
+
38
+ self.instance_methods.grep(/\?\z/).each do |k|
39
+ undef_method k unless k.to_s == 'equal?' || k.to_s =~ /^assert/
40
+ end
41
+
42
+ NOT_YET = {} # `ok()` registers AssertionObject into this. `__done()` removes from this.
43
+
44
+ def initialize(actual, bool, location)
45
+ @actual = actual
46
+ @bool = bool
47
+ @location = location
48
+ end
49
+
50
+ attr_reader :actual, :bool, :location
51
+
52
+ def __done()
53
+ NOT_YET.delete(self.__id__)
54
+ end
55
+ private :__done
56
+
57
+ def self.report_not_yet()
58
+ #; [!3nksf] reports if 'ok{}' called but assertion not performed.
59
+ return if NOT_YET.empty?
60
+ NOT_YET.each_value do |ass|
61
+ s = ass.location ? " (at #{ass.location})" : nil
62
+ $stderr.write "** warning: ok() is called but not tested yet#{s}.\n"
63
+ end
64
+ #; [!f92q4] clears remained objects.
65
+ NOT_YET.clear()
66
+ end
67
+
68
+ def __assert(result)
69
+ raise FAIL_EXCEPTION, yield unless result
70
+ end
71
+
72
+ def NOT()
73
+ #; [!63dde] toggles internal boolean.
74
+ @bool = ! @bool
75
+ #; [!g775v] returns self.
76
+ self
77
+ end
78
+
79
+ def ==(expected)
80
+ __done()
81
+ #; [!1iun4] raises assertion error when failed.
82
+ #; [!eyslp] is avaialbe with NOT.
83
+ __assert(@bool == (@actual == expected)) {
84
+ if @bool && ! (@actual == expected) \
85
+ && @actual.is_a?(String) && expected.is_a?(String) \
86
+ && (@actual =~ /\n/ || expected =~ /\n/)
87
+ #; [!3xnqv] shows context diff when both actual and expected are text.
88
+ diff = Util.unified_diff(expected, @actual, "--- $<expected>\n+++ $<actual>\n")
89
+ "$<actual> == $<expected>: failed.\n#{diff}"
90
+ else
91
+ op = @bool ? '==' : '!='
92
+ "$<actual> #{op} $<expected>: failed.\n"\
93
+ " $<actual>: #{@actual.inspect}\n"\
94
+ " $<expected>: #{expected.inspect}"
95
+ end
96
+ }
97
+ #; [!c6p0e] returns self when passed.
98
+ self
99
+ end
100
+
101
+ def !=(expected) # Ruby >= 1.9
102
+ __done()
103
+ #; [!90tfb] raises assertion error when failed.
104
+ #; [!l6afg] is avaialbe with NOT.
105
+ __assert(@bool == (@actual != expected)) {
106
+ op = @bool ? '!=' : '=='
107
+ "$<actual> #{op} $<expected>: failed.\n"\
108
+ " $<actual>: #{@actual.inspect}\n"\
109
+ " $<expected>: #{expected.inspect}"
110
+ }
111
+ #; [!iakbb] returns self when passed.
112
+ self
113
+ end
114
+
115
+ def ===(expected)
116
+ __done()
117
+ #; [!42f6a] raises assertion error when failed.
118
+ #; [!vhvyu] is avaialbe with NOT.
119
+ __assert(@bool == (@actual === expected)) {
120
+ s = "$<actual> === $<expected>"
121
+ s = "!(#{s})" unless @bool
122
+ "#{s}: failed.\n"\
123
+ " $<actual>: #{@actual.inspect}\n"\
124
+ " $<expected>: #{expected.inspect}"
125
+ }
126
+ #; [!uh8bm] returns self when passed.
127
+ self
128
+ end
129
+
130
+ def __assert_op(bool, op1, op2, expected)
131
+ __assert(@bool == bool) {
132
+ "#{@actual.inspect} #{@bool ? op1 : op2} #{expected.inspect}: failed."
133
+ }
134
+ end
135
+ private :__assert_op
136
+
137
+ def >(expected)
138
+ __done()
139
+ #; [!vjjuq] raises assertion error when failed.
140
+ #; [!73a0t] is avaialbe with NOT.
141
+ __assert_op(@actual > expected, '>', '<=', expected)
142
+ #; [!3j7ty] returns self when passed.
143
+ self
144
+ end
145
+
146
+ def >=(expected)
147
+ __done()
148
+ #; [!isdfc] raises assertion error when failed.
149
+ #; [!3dgmh] is avaialbe with NOT.
150
+ __assert_op(@actual >= expected, '>=', '<', expected)
151
+ #; [!75iqw] returns self when passed.
152
+ self
153
+ end
154
+
155
+ def <(expected)
156
+ __done()
157
+ #; [!ukqa0] raises assertion error when failed.
158
+ #; [!gwvdl] is avaialbe with NOT.
159
+ __assert_op(@actual < expected, '<', '>=', expected)
160
+ #; [!vkwcc] returns self when passed.
161
+ self
162
+ end
163
+
164
+ def <=(expected)
165
+ __done()
166
+ #; [!ordwe] raises assertion error when failed.
167
+ #; [!mcb9w] is avaialbe with NOT.
168
+ __assert_op(@actual <= expected, '<=', '>', expected)
169
+ #; [!yk7t2] returns self when passed.
170
+ self
171
+ end
172
+
173
+ def __assert_match(result, op1, op2, expected)
174
+ __assert(@bool == !!result) {
175
+ msg = "$<actual> #{@bool ? op1 : op2} $<expected>: failed.\n"\
176
+ " $<expected>: #{expected.inspect}\n"
177
+ if @actual =~ /\n\z/
178
+ msg + " $<actual>: <<'END'\n#{@actual}END\n"
179
+ else
180
+ msg + " $<actual>: #{@actual.inspect}\n"
181
+ end
182
+ }
183
+ end
184
+ private :__assert_match
185
+
186
+ def =~(expected)
187
+ __done()
188
+ #; [!xkldu] raises assertion error when failed.
189
+ #; [!2aa6f] is avaialbe with NOT.
190
+ __assert_match(@actual =~ expected, '=~', '!~', expected)
191
+ #; [!acypf] returns self when passed.
192
+ self
193
+ end
194
+
195
+ def !~(expected) # Ruby >= 1.9
196
+ __done()
197
+ #; [!58udu] raises assertion error when failed.
198
+ #; [!iuf5j] is avaialbe with NOT.
199
+ __assert_match(@actual !~ expected, '!~', '=~', expected)
200
+ #; [!xywdr] returns self when passed.
201
+ self
202
+ end
203
+
204
+ def in_delta?(expected, delta)
205
+ __done()
206
+ #; [!f3zui] raises assertion error when failed.
207
+ #; [!t7liw] is avaialbe with NOT.
208
+ __assert(@bool == !!((@actual - expected).abs < delta)) {
209
+ eq = @bool ? '' : ' == false'
210
+ "($<actual> - $<expected>).abs < #{delta}#{eq}: failed.\n"\
211
+ " $<actual>: #{@actual.inspect}\n"\
212
+ " $<expected>: #{expected.inspect}\n"\
213
+ " ($<actual> - $<expected>).abs: #{(@actual - expected).abs.inspect}"
214
+ }
215
+ #; [!m0791] returns self when passed.
216
+ self
217
+ end
218
+
219
+ def same?(expected)
220
+ __done()
221
+ #; [!ozbf4] raises assertion error when failed.
222
+ #; [!dwtig] is avaialbe with NOT.
223
+ __assert(@bool == !! @actual.equal?(expected)) {
224
+ eq = @bool ? '' : ' == false'
225
+ "$<actual>.equal?($<expected>)#{eq}: failed.\n"\
226
+ " $<actual>: #{@actual.inspect}\n"\
227
+ " $<expected>: #{expected.inspect}\n"
228
+ }
229
+ #; [!yk7zo] returns self when passed.
230
+ self
231
+ end
232
+
233
+ def method_missing(method_name, *args)
234
+ __done()
235
+ #; [!yjnxb] enables to handle boolean methods.
236
+ #; [!ttow6] raises NoMethodError when not a boolean method.
237
+ method_name.to_s =~ /\?\z/ or
238
+ super
239
+ begin
240
+ ret = @actual.__send__(method_name, *args)
241
+ rescue NoMethodError, TypeError => exc
242
+ #; [!f0ekh] skip top of backtrace when NoMethodError raised.
243
+ while !exc.backtrace.empty? && exc.backtrace[0].start_with?(__FILE__)
244
+ exc.backtrace.shift()
245
+ end
246
+ raise
247
+ end
248
+ #; [!cun59] fails when boolean method failed returned false.
249
+ #; [!4objh] is available with NOT.
250
+ if ret == true || ret == false
251
+ __assert(@bool == ret) {
252
+ args = args.empty? ? '' : "(#{args.collect {|x| x.inspect }.join(', ')})"
253
+ eq = @bool ? '' : ' == false'
254
+ "$<actual>.#{method_name}#{args}#{eq}: failed.\n"\
255
+ " $<actual>: #{@actual.inspect}"
256
+ }
257
+ #; [!sljta] raises TypeError when boolean method returned non-boolean value.
258
+ else
259
+ raise TypeError, "ok(): #{@actual.class}##{method_name}() expected to return true or false, but got #{ret.inspect}."
260
+ end
261
+ #; [!7bbrv] returns self when passed.
262
+ self
263
+ end
264
+
265
+ def raise!(errcls=nil, errmsg=nil, &b)
266
+ #; [!8k6ee] compares error class by '.is_a?' instead of '=='.
267
+ return raise?(errcls, errmsg, subclass: true, &b)
268
+ end
269
+
270
+ def raise?(errcls=nil, errmsg=nil, subclass: false, &b)
271
+ __done()
272
+ #; [!2rnni] 1st argument can be error message string or rexp.
273
+ if errmsg.nil? && ! errcls.nil? && ! (errcls.is_a?(Class) && errcls <= Exception)
274
+ errmsg = errcls
275
+ errcls = RuntimeError
276
+ end
277
+ #
278
+ proc_obj = @actual
279
+ exc = nil
280
+ #; [!dpv5g] when `ok{}` called...
281
+ if @bool
282
+ begin
283
+ proc_obj.call
284
+ rescue Exception => exc
285
+ #; [!4c6x3] not check exception class when nil specified as errcls.
286
+ if errcls.nil?
287
+ nil
288
+ #; [!yps62] assertion passes when expected exception raised.
289
+ #; [!lq6jv] compares error class with '==' operator, not '.is_a?'.
290
+ elsif exc.class == errcls # not `exc.is_a?(errcls)`
291
+ nil
292
+ #; [!hwg0z] compares error class with '.is_a?' if 'subclass: true' specified.
293
+ elsif subclass && exc.class < errcls
294
+ nil
295
+ #; [!4n3ed] reraises if exception is not matched to specified error class.
296
+ else
297
+ #__assert(false) { "Expected #{errcls} to be raised but got #{exc.class}." }
298
+ raise
299
+ end
300
+ else
301
+ #; [!wbwdo] raises assertion error when nothing raised.
302
+ __assert(false) { "Expected #{errcls} to be raised but nothing raised." }
303
+ end
304
+ #; [!tpxlv] accepts string or regexp as error message.
305
+ if errmsg
306
+ __assert(errmsg === exc.message) {
307
+ op = errmsg.is_a?(Regexp) ? '=~' : '=='
308
+ "$<error_message> #{op} #{errmsg.inspect}: failed.\n"\
309
+ " $<error_message>: #{exc.message.inspect}"
310
+ }
311
+ end
312
+ #; [!dq97o] if block given, call it with exception object.
313
+ yield exc if block_given?()
314
+ #; [!qkr3h] when `ok{}.NOT` called...
315
+ else
316
+ #; [!cownv] not support error message.
317
+ ! errmsg or
318
+ raise ArgumentError, "#{errmsg.inspect}: NOT.raise?() can't take errmsg."
319
+ #; [!spzy2] is available with NOT.
320
+ begin
321
+ proc_obj.call
322
+ rescue Exception => exc
323
+ #; [!36032] re-raises exception when errcls is nil.
324
+ if errcls.nil?
325
+ #__assert(false) { "Nothing should be raised but got #{exc.inspect}." }
326
+ raise
327
+ #; [!61vtv] assertion fails when specified exception raised.
328
+ #; [!smprc] compares error class with '==' operator, not '.is_a?'.
329
+ elsif exc.class == errcls # not `exc.is_a?(errcls)`
330
+ __assert(false) { "#{errcls.inspect} should not be raised but got #{exc.inspect}." }
331
+ #; [!34nd8] compares error class with '.is_a?' if 'subclass: true' specified.
332
+ elsif subclass && exc.class < errcls
333
+ __assert(false) { "#{errcls.inspect} should not be raised but got #{exc.inspect}." }
334
+ #; [!shxne] reraises exception if different from specified error class.
335
+ else
336
+ raise
337
+ end
338
+ else
339
+ #; [!a1a40] assertion passes when nothing raised.
340
+ nil
341
+ end
342
+ end
343
+ #; [!vnc6b] sets exception object into '#exc' attribute.
344
+ (class << proc_obj; self; end).class_eval { attr_accessor :exc }
345
+ proc_obj.exc = exc
346
+ #; [!y1b28] returns self when passed.
347
+ self
348
+ end
349
+
350
+ def throw?(expected)
351
+ __done()
352
+ proc_obj = @actual
353
+ if @bool
354
+ #; [!w7935] raises ArgumentError when arg of 'thrown?()' is nil.
355
+ expected != nil or
356
+ raise ArgumentError, "throw?(#{expected.inspect}): expected tag required."
357
+ #
358
+ begin
359
+ proc_obj.call
360
+ rescue UncaughtThrowError => exc
361
+ #; [!lglzr] assertion passes when expected symbol thrown.
362
+ if exc.tag.equal?(expected)
363
+ nil
364
+ #; [!gf9nx] assertion fails when thrown tag is equal to but not same as expected.
365
+ elsif exc.tag == expected
366
+ __assert(false) {
367
+ "Thrown tag #{exc.tag.inspect} is equal to but not same as expected.\n"\
368
+ " (`#{exc.tag.inspect}.equal?(#{expected.inspect})` should be true but not.)"
369
+ }
370
+ #; [!flgwy] assertion fails when thrown tag is different from expectd.
371
+ else
372
+ __assert(false) {
373
+ "#{expected.inspect} should be thrown but actually #{exc.tag.inspect} thrown."
374
+ }
375
+ end
376
+ else
377
+ #; [!9ik3x] assertion fails when nothing thrown.
378
+ __assert(false) { "#{expected.inspect} should be thrown but nothing thrown." }
379
+ end
380
+ else
381
+ #; [!m03vq] raises ArgumentError when non-nil arg passed to 'NOT.thrown?()'.
382
+ expected == nil or
383
+ raise ArgumentError, "NOT.throw?(#{expected.inspect}): argument should be nil."
384
+ #; [!kxizg] assertion fails when something thrown in 'NOT.throw?()'.
385
+ begin
386
+ proc_obj.call
387
+ rescue UncaughtThrowError => exc
388
+ __assert(false) {
389
+ "Nothing should be thrown but #{exc.tag.inspect} thrown."
390
+ }
391
+ end
392
+ end
393
+ #; [!zq9h6] returns self when passed.
394
+ self
395
+ end
396
+
397
+ def in?(expected)
398
+ __done()
399
+ #; [!9rm8g] raises assertion error when failed.
400
+ #; [!singl] is available with NOT.
401
+ __assert(@bool == !! expected.include?(@actual)) {
402
+ eq = @bool ? '' : ' == false'
403
+ "$<expected>.include?($<actual>)#{eq}: failed.\n"\
404
+ " $<actual>: #{@actual.inspect}\n"\
405
+ " $<expected>: #{expected.inspect}"
406
+ }
407
+ #; [!jzoxg] returns self when passed.
408
+ self
409
+ end
410
+
411
+ def attr(name, expected)
412
+ __done()
413
+ #; [!79tgn] raises assertion error when failed.
414
+ #; [!cqnu3] is available with NOT.
415
+ val = @actual.__send__(name)
416
+ __assert(@bool == (expected == val)) {
417
+ op = @bool ? '==' : '!='
418
+ "$<actual>.#{name} #{op} $<expected>: failed.\n"\
419
+ " $<actual>.#{name}: #{val.inspect}\n"\
420
+ " $<expected>: #{expected.inspect}"\
421
+ }
422
+ #; [!lz3lb] returns self when passed.
423
+ self
424
+ end
425
+
426
+ def attrs(**keyvals)
427
+ __done()
428
+ #; [!7ta0s] raises assertion error when failed.
429
+ #; [!s0pnk] is available with NOT.
430
+ keyvals.each {|name, expected| attr(name, expected) }
431
+ #; [!rtq9f] returns self when passed.
432
+ self
433
+ end
434
+
435
+ def keyval(key, expected)
436
+ __done()
437
+ #; [!vtrlz] raises assertion error when failed.
438
+ #; [!mmpwz] is available with NOT.
439
+ val = @actual[key]
440
+ __assert(@bool == (expected == val)) {
441
+ op = @bool ? '==' : '!='
442
+ "$<actual>[#{key.inspect}] #{op} $<expected>: failed.\n"\
443
+ " $<actual>[#{key.inspect}]: #{val.inspect}\n"\
444
+ " $<expected>: #{expected.inspect}"\
445
+ }
446
+ #; [!byebv] returns self when passed.
447
+ self
448
+ end
449
+ alias item keyval # for compatibility with minitest-ok
450
+
451
+ def keyvals(keyvals={}) # never use keyword args!
452
+ __done()
453
+ #; [!fyvmn] raises assertion error when failed.
454
+ #; [!js2j2] is available with NOT.
455
+ keyvals.each {|name, expected| keyval(name, expected) }
456
+ #; [!vtw22] returns self when passed.
457
+ self
458
+ end
459
+ alias items keyvals # for compatibility with minitest-ok
460
+
461
+ def length(n)
462
+ __done()
463
+ #; [!1y787] raises assertion error when failed.
464
+ #; [!kryx2] is available with NOT.
465
+ __assert(@bool == (@actual.length == n)) {
466
+ op = @bool ? '==' : '!='
467
+ "$<actual>.length #{op} #{n}: failed.\n"\
468
+ " $<actual>.length: #{@actual.length}\n"\
469
+ " $<actual>: #{actual.inspect}"
470
+ }
471
+ #; [!l9vnv] returns self when passed.
472
+ self
473
+ end
474
+
475
+ def truthy?
476
+ __done()
477
+ #; [!3d94h] raises assertion error when failed.
478
+ #; [!8rmgp] is available with NOT.
479
+ __assert(@bool == (!!@actual == true)) {
480
+ op = @bool ? '==' : '!='
481
+ "!!$<actual> #{op} true: failed.\n"\
482
+ " $<actual>: #{@actual.inspect}"
483
+ }
484
+ #; [!nhmuk] returns self when passed.
485
+ self
486
+ end
487
+
488
+ def falsy?
489
+ __done()
490
+ #; [!7o48g] raises assertion error when failed.
491
+ #; [!i44q6] is available with NOT.
492
+ __assert(@bool == (!!@actual == false)) {
493
+ op = @bool ? '==' : '!='
494
+ "!!$<actual> #{op} false: failed.\n"\
495
+ " $<actual>: #{@actual.inspect}"
496
+ }
497
+ #; [!w1vm6] returns self when passed.
498
+ self
499
+ end
500
+
501
+ def __assert_fs(bool, s)
502
+ __assert(@bool == bool) {
503
+ "#{s}#{@bool ? '' : ' == false'}: failed.\n"\
504
+ " $<actual>: #{@actual.inspect}"
505
+ }
506
+ end
507
+ private :__assert_fs
508
+
509
+ def file_exist?
510
+ __done()
511
+ #; [!69bs0] raises assertion error when failed.
512
+ #; [!r1mze] is available with NOT.
513
+ __assert_fs(File.file?(@actual) , "File.file?($<actual>)")
514
+ #; [!6bcpp] returns self when passed.
515
+ self
516
+ end
517
+
518
+ def dir_exist?
519
+ __done()
520
+ #; [!vfh7a] raises assertion error when failed.
521
+ #; [!qtllp] is available with NOT.
522
+ __assert_fs(File.directory?(@actual), "File.directory?($<actual>)")
523
+ #; [!8qe7u] returns self when passed.
524
+ self
525
+ end
526
+
527
+ def symlink_exist?
528
+ __done()
529
+ #; [!qwngl] raises assertion error when failed.
530
+ #; [!cgpbt] is available with NOT.
531
+ __assert_fs(File.symlink?(@actual), "File.symlink?($<actual>)")
532
+ #; [!ugfi3] returns self when passed.
533
+ self
534
+ end
535
+
536
+ def not_exist?
537
+ __done()
538
+ #; [!ja84s] raises assertion error when failed.
539
+ #; [!to5z3] is available with NOT.
540
+ __assert(@bool == ! File.exist?(@actual)) {
541
+ "File.exist?($<actual>)#{@bool ? ' == false' : ''}: failed.\n"\
542
+ " $<actual>: #{@actual.inspect}"
543
+ }
544
+ #; [!1ujag] returns self when passed.
545
+ self
546
+ end
547
+
548
+ end
549
+
550
+
551
+ class Context
552
+ ## * Context class is separated from ScopeNode, TopicNode, and SpecLeaf.
553
+ ## * `topic()` and `spec()` creates subclass of Context class,
554
+ ## and run blocks in these subclasses.
555
+ ## * `scope()` instanciates those subclasses, and run spec blocks
556
+ ## in that instance objects.
557
+
558
+ class << self
559
+ attr_accessor :__node
560
+ end
561
+
562
+ def self.topic(target, tag: nil, &block)
563
+ #; [!0gfvq] creates new topic node.
564
+ node = @__node
565
+ topic = TopicNode.new(node, target, tag: tag)
566
+ topic.run_block_in_context_class(&block)
567
+ return topic
568
+ end
569
+
570
+ def self.case_when(desc, tag: nil, &block)
571
+ #; [!g3cvh] returns topic object.
572
+ #; [!ofw1i] target is a description starting with 'When '.
573
+ return __case_when("When #{desc}", tag, &block)
574
+ end
575
+
576
+ def self.case_else(desc=nil, tag: nil, &block)
577
+ #; [!hs1to] 1st parameter is optional.
578
+ desc = desc ? "Else #{desc}" : "Else"
579
+ #; [!oww4b] returns topic object.
580
+ #; [!j5gnp] target is a description which is 'Else'.
581
+ return __case_when(desc, tag, &block)
582
+ end
583
+
584
+ def self.__case_when(desc, tag, &block) #:nodoc:
585
+ to = topic(desc, tag: tag, &block)
586
+ to._prefix = '-'
587
+ return to
588
+ end
589
+
590
+ def self.spec(desc, tag: nil, &block)
591
+ node = @__node
592
+ node.is_a?(Node) or raise "internal error: node=#{node.inspect}" # for debug
593
+ #; [!ala78] provides raising TodoException block if block not given.
594
+ block ||= proc { raise TodoException, "not implemented yet" }
595
+ #; [!x48db] keeps called location only when block has parameters.
596
+ if block.parameters.empty?
597
+ location = nil
598
+ else
599
+ location = caller(1).first # caller() makes performance slower, but necessary.
600
+ end
601
+ #; [!c8c8o] creates new spec object.
602
+ spec = SpecLeaf.new(node, desc, tag: tag, location: location, &block)
603
+ return spec
604
+ end
605
+
606
+ def self.fixture(name, &block)
607
+ #; [!8wfrq] registers fixture factory block.
608
+ #; [!y3ks3] retrieves block parameter names.
609
+ location = caller(1).first # caller() makes performance slower, but necessary.
610
+ @__node.register_fixture_block(name, location, &block)
611
+ self
612
+ end
613
+
614
+ def self.before(&block)
615
+ #; [!275zr] registers 'before' hook block.
616
+ @__node.register_hook_block(:before, &block)
617
+ self
618
+ end
619
+
620
+ def self.after(&block)
621
+ #; [!ngkvz] registers 'after' hook block.
622
+ @__node.register_hook_block(:after, &block)
623
+ self
624
+ end
625
+
626
+ def self.before_all(&block)
627
+ #; [!8v1y4] registers 'before_all' hook block.
628
+ @__node.register_hook_block(:before_all, &block)
629
+ self
630
+ end
631
+
632
+ def self.after_all(&block)
633
+ #; [!0w5ik] registers 'after_all' hook block.
634
+ @__node.register_hook_block(:after_all, &block)
635
+ self
636
+ end
637
+
638
+ end
639
+
640
+
641
+ class Item
642
+
643
+ def accept_visitor(visitor, *args)
644
+ #; [!b0e20] raises NotImplementedError.
645
+ raise NotImplementedError, "#{self.class.name}#accept_visitor(): not implemented yet."
646
+ end
647
+
648
+ def unlink_parent()
649
+ #; [!5a0i9] raises NotImplementedError.
650
+ raise NotImplementedError.new("#{self.class.name}#unlink_parent(): not implemented yet.")
651
+ end
652
+
653
+ def _repr(depth=0, buf="") #:nodoc:
654
+ #; [!qi1af] raises NotImplementedError.
655
+ raise NotImplementedError, "#{self.class.name}#_repr(): not implemented yet."
656
+ end
657
+
658
+ end
659
+
660
+
661
+ class Node < Item
662
+
663
+ def initialize(parent, tag: nil)
664
+ @parent = parent
665
+ @tag = tag
666
+ @children = []
667
+ parent.add_child(self) if parent
668
+ @context_class = Class.new(parent ? parent.context_class : Oktest::Context)
669
+ @context_class.__node = self
670
+ @fixtures = {} # {name: [[param], block]}
671
+ @hooks = {} # {name: block}
672
+ end
673
+
674
+ attr_reader :parent, :tag, :context_class, :fixtures, :hooks
675
+
676
+ def topic?; false; end
677
+
678
+ def add_child(child)
679
+ #; [!1fyk9] keeps children.
680
+ @children << child
681
+ #; [!w5r6l] returns self.
682
+ self
683
+ end
684
+
685
+ def has_child?
686
+ #; [!xb30d] return true when no children, else false.
687
+ return !@children.empty?
688
+ end
689
+
690
+ def each_child(&b)
691
+ #; [!osoep] returns enumerator if block not given.
692
+ return @children.each unless block_given?()
693
+ #; [!pve8m] yields block for each child.
694
+ @children.each(&b)
695
+ #; [!8z6un] returns nil.
696
+ nil
697
+ end
698
+
699
+ def remove_child_at(index)
700
+ #; [!hsomo] removes child at index.
701
+ child = @children.delete_at(index)
702
+ #; [!7fhx1] unlinks reference between parent and child.
703
+ child.unlink_parent() if child
704
+ #; [!hiz1b] returns removed child.
705
+ return child
706
+ end
707
+
708
+ def clear_children()
709
+ #; [!o8xfb] removes all children.
710
+ @children.clear()
711
+ #; [!cvaq1] return self.
712
+ self
713
+ end
714
+
715
+ def unlink_parent()
716
+ #; [!59m52] clears '@parent' instance variable.
717
+ #; [!qksxv] returns parent object.
718
+ parent = @parent
719
+ @parent = nil
720
+ return parent
721
+ end
722
+
723
+ def run_block_in_context_class(&block)
724
+ #; [!j9qdh] run block in context class.
725
+ @context_class.class_eval(&block)
726
+ end
727
+
728
+ def new_context_object()
729
+ #; [!p271z] creates new context object.
730
+ context = @context_class.new()
731
+ #; [!9hbxn] context object has 'ok()' method.
732
+ context.extend SpecHelper
733
+ return context
734
+ end
735
+
736
+ def register_fixture_block(name, location, &block)
737
+ #; [!5ctsn] registers fixture name, block, and location.
738
+ params = Util.required_param_names_of_block(block) # [Symbol]
739
+ params = nil if params.empty?
740
+ @fixtures[name] = [block, params, location]
741
+ #; [!hfcvo] returns self.
742
+ self
743
+ end
744
+
745
+ def get_fixture_block(name)
746
+ #; [!f0105] returns fixture info.
747
+ return @fixtures[name]
748
+ end
749
+
750
+ def register_hook_block(key, &block)
751
+ #; [!zb66o] registers block with key.
752
+ @hooks[key] = block
753
+ self
754
+ end
755
+
756
+ def get_hook_block(key)
757
+ #; [!u3fc6] returns block corresponding to key.
758
+ return @hooks[key]
759
+ end
760
+
761
+ def _repr(depth=0, buf="")
762
+ #; [!bt5j8] builds debug string.
763
+ if depth < 0
764
+ id_str = "%x" % self.object_id
765
+ return "#<#{self.class.name}:0x#{id_str}>"
766
+ end
767
+ indent = " " * depth
768
+ id_str = "%x" % self.object_id
769
+ buf << "#{indent}- #<#{self.class.name}:0x#{id_str}>\n"
770
+ instance_variables().sort.each do |name|
771
+ next if name.to_s == "@children"
772
+ val = instance_variable_get(name)
773
+ next if val.nil?
774
+ next if val.respond_to?(:empty?) && val.empty?
775
+ if val.respond_to?(:_repr)
776
+ buf << "#{indent} #{name}: #{val._repr(-1)}\n"
777
+ else
778
+ buf << "#{indent} #{name}: #{val.inspect}\n"
779
+ end
780
+ end
781
+ @children.each {|child| child._repr(depth+1, buf) }
782
+ return buf
783
+ end
784
+
785
+ end
786
+
787
+
788
+ class ScopeNode < Node
789
+
790
+ def initialize(parent, filename, tag: nil)
791
+ super(parent, tag: tag)
792
+ @filename = filename
793
+ end
794
+
795
+ attr_reader :filename
796
+
797
+ def accept_visitor(visitor, *args)
798
+ #; [!vr6ko] invokes 'visit_spec()' method of visitor and returns result of it.
799
+ return visitor.visit_scope(self, *args)
800
+ end
801
+
802
+ end
803
+
804
+
805
+ class TopicNode < Node
806
+
807
+ def initialize(parent, target, tag: nil)
808
+ super(parent, tag: tag)
809
+ @target = target
810
+ end
811
+
812
+ attr_reader :target
813
+ attr_accessor :_prefix
814
+
815
+ def _prefix
816
+ @_prefix || '*'
817
+ end
818
+
819
+ def topic?; true; end
820
+
821
+ def accept_visitor(visitor, *args)
822
+ #; [!c1b33] invokes 'visit_topic()' method of visitor and returns result of it.
823
+ return visitor.visit_topic(self, *args)
824
+ end
825
+
826
+ def +@
827
+ #; [!tzorv] returns self.
828
+ self
829
+ end
830
+
831
+ end
832
+
833
+
834
+ class SpecLeaf < Item
835
+
836
+ def initialize(parent, desc, tag: nil, location: nil, &block)
837
+ #@parent = parent # not keep parent node to avoid recursive reference
838
+ @desc = desc
839
+ @tag = tag
840
+ @location = location # necessary when raising fixture not found error
841
+ @block = block
842
+ parent.add_child(self) if parent
843
+ end
844
+
845
+ attr_reader :desc, :tag, :location, :block
846
+
847
+ def _prefix
848
+ '-'
849
+ end
850
+
851
+ def run_block_in_context_object(context, *args)
852
+ #; [!tssim] run spec block in text object.
853
+ context.instance_exec(*args, &@block)
854
+ end
855
+
856
+ def accept_visitor(visitor, *args)
857
+ #; [!ya32z] invokes 'visit_spec()' method of visitor and returns result of it.
858
+ return visitor.visit_spec(self, *args)
859
+ end
860
+
861
+ def unlink_parent()
862
+ #; [!e9sv9] do nothing.
863
+ nil
864
+ end
865
+
866
+ def _repr(depth=0, buf="") #:nodoc:
867
+ #; [!6nsgy] builds debug string.
868
+ buf << " " * depth << "- #{@desc}"
869
+ buf << " (tag: #{@tag.inspect})" if @tag
870
+ buf << "\n"
871
+ return buf
872
+ end
873
+
874
+ def -@
875
+ #; [!bua80] returns self.
876
+ self
877
+ end
878
+
879
+ end
880
+
881
+
882
+ THE_GLOBAL_SCOPE = ScopeNode.new(nil, __FILE__)
883
+
884
+
885
+ def self.global_scope(&block)
886
+ #; [!flnpc] run block in the THE_GLOBAL_SCOPE object.
887
+ #; [!pe0g2] raises error when nested called.
888
+ self.__scope(THE_GLOBAL_SCOPE, &block)
889
+ #; [!fcmt2] not create new scope object.
890
+ return THE_GLOBAL_SCOPE
891
+ end
892
+
893
+ def self.scope(tag: nil, &block)
894
+ #; [!vxoy1] creates new scope object.
895
+ #; [!rsimc] adds scope object as child of THE_GLOBAL_SCOPE.
896
+ location = caller(1).first # caller() makes performance slower, but necessary.
897
+ filename = location =~ /:\d+/ ? $` : nil
898
+ filename = filename.sub(/\A\.\//, '')
899
+ scope = ScopeNode.new(THE_GLOBAL_SCOPE, filename, tag: tag)
900
+ #; [!jmc4q] raises error when nested called.
901
+ self.__scope(scope, &block)
902
+ return scope
903
+ end
904
+
905
+ def self.__scope(scope, &block) #:nodoc:
906
+ ! @_in_scope or
907
+ raise OktestError, "scope() and global_scope() are not nestable."
908
+ @_in_scope = true
909
+ scope.run_block_in_context_class(&block)
910
+ ensure
911
+ @_in_scope = false
912
+ end
913
+
914
+
915
+ module SpecHelper
916
+
917
+ attr_accessor :__TODO, :__at_end_blocks
918
+
919
+ def ok()
920
+ #; [!bc3l2] records invoked location.
921
+ #; [!mqtdy] not record invoked location when `Config.ok_location == false`.
922
+ if Config.ok_location
923
+ location = caller(1).first # caller() makes performance slower, but necessary.
924
+ else
925
+ location = nil
926
+ end
927
+ #; [!3jhg6] creates new assertion object.
928
+ actual = yield
929
+ ass = Oktest::AssertionObject.new(actual, true, location)
930
+ Oktest::AssertionObject::NOT_YET[ass.__id__] = ass
931
+ return ass
932
+ end
933
+
934
+ def not_ok()
935
+ #; [!agmx8] records invoked location.
936
+ #; [!a9508] not record invoked location when `Config.ok_location == false`.
937
+ if Config.ok_location
938
+ location = caller(1).first # caller() makes performance slower, but necessary.
939
+ else
940
+ location = nil
941
+ end
942
+ #; [!d332o] creates new assertion object for negative condition.
943
+ actual = yield
944
+ ass = Oktest::AssertionObject.new(actual, false, location)
945
+ Oktest::AssertionObject::NOT_YET[ass.__id__] = ass
946
+ return ass
947
+ end
948
+
949
+ def skip_when(condition, reason)
950
+ #; [!3xqf4] raises SkipException if condition is truthy.
951
+ #; [!r7cxx] not raise nothing if condition is falsy.
952
+ raise SkipException, reason if condition
953
+ end
954
+
955
+ def fixture(name, *args)
956
+ #; [!zgfg9] finds fixture block in current or parent node.
957
+ node = self.class.__node
958
+ while node && (tuple = node.get_fixture_block(name)) == nil
959
+ node = node.parent
960
+ end
961
+ #; [!wxcsp] raises error when fixture not found.
962
+ unless tuple
963
+ exc = FixtureNotFoundError.new("`#{name.inspect}`: fixture not found.")
964
+ exc.set_backtrace([caller(1).first])
965
+ raise exc
966
+ end
967
+ #; [!m4ava] calls fixture block and returns result of it.
968
+ #; [!l2mcx] accepts block arguments.
969
+ block, _, _ = tuple
970
+ return block.call(*args)
971
+ end
972
+
973
+ def TODO()
974
+ location = caller(1).first # ex: "foo_test.rb:123:in ...."
975
+ @__TODO = location
976
+ end
977
+
978
+ def at_end(&block)
979
+ #; [!x58eo] records clean-up block.
980
+ (@__at_end_blocks ||= []) << block
981
+ end
982
+
983
+ def capture_sio(input="", tty: false, &b)
984
+ require 'stringio' unless defined?(StringIO)
985
+ bkup = [$stdin, $stdout, $stderr]
986
+ #; [!53mai] takes $stdin data.
987
+ $stdin = sin = StringIO.new(input)
988
+ #; [!1kbnj] captures $stdio and $stderr.
989
+ $stdout = sout = StringIO.new
990
+ $stderr = serr = StringIO.new
991
+ #; [!6ik8b] can simulate tty.
992
+ if tty
993
+ def sin.tty?; true; end
994
+ def sout.tty?; true; end
995
+ def serr.tty?; true; end
996
+ end
997
+ #; [!4j494] returns outpouts of stdout and stderr.
998
+ yield sout, serr
999
+ return sout.string, serr.string
1000
+ ensure
1001
+ #; [!wq8a9] recovers stdio even when exception raised.
1002
+ $stdin, $stdout, $stderr = bkup
1003
+ end
1004
+
1005
+ def __do_dummy(val, recover, &b)
1006
+ if block_given?()
1007
+ begin
1008
+ return yield val
1009
+ ensure
1010
+ recover.call
1011
+ end
1012
+ else
1013
+ at_end(&recover)
1014
+ return val
1015
+ end
1016
+ end
1017
+ private :__do_dummy
1018
+
1019
+ def dummy_file(filename=nil, content=nil, encoding: 'utf-8', &b)
1020
+ #; [!3mg26] generates temporary filename if 1st arg is nil.
1021
+ filename ||= "_tmpfile_#{rand().to_s[2...8]}"
1022
+ #; [!yvfxq] raises error when dummy file already exists.
1023
+ ! File.exist?(filename) or
1024
+ raise ArgumentError, "dummy_file('#{filename}'): temporary file already exists."
1025
+ #; [!7e0bo] creates dummy file.
1026
+ File.write(filename, content, encoding: encoding)
1027
+ recover = proc { File.unlink(filename) if File.exist?(filename) }
1028
+ #; [!nvlkq] returns filename.
1029
+ #; [!ky7nh] can take block argument.
1030
+ return __do_dummy(filename, recover, &b)
1031
+ end
1032
+
1033
+ def dummy_dir(dirname=nil, &b)
1034
+ #; [!r14uy] generates temporary directory name if 1st arg is nil.
1035
+ dirname ||= "_tmpdir_#{rand().to_s[2...8]}"
1036
+ #; [!zypj6] raises error when dummy dir already exists.
1037
+ ! File.exist?(dirname) or
1038
+ raise ArgumentError, "dummy_dir('#{dirname}'): temporary directory already exists."
1039
+ #; [!l34d5] creates dummy directory.
1040
+ require 'fileutils' unless defined?(FileUtils)
1041
+ FileUtils.mkdir_p(dirname)
1042
+ #; [!01gt7] removes dummy directory even if it contains other files.
1043
+ recover = proc { FileUtils.rm_rf(dirname) if File.exist?(dirname) }
1044
+ #; [!jxh30] returns directory name.
1045
+ #; [!tfsqo] can take block argument.
1046
+ return __do_dummy(dirname, recover, &b)
1047
+ end
1048
+
1049
+ def dummy_values(hashobj, keyvals={}, &b) # never use keyword args!
1050
+ #; [!hgwg2] changes hash value temporarily.
1051
+ prev_values = {}
1052
+ key_not_exists = {}
1053
+ keyvals.each do |k, v|
1054
+ if hashobj.key?(k)
1055
+ prev_values[k] = hashobj[k]
1056
+ else
1057
+ key_not_exists[k] = true
1058
+ end
1059
+ hashobj[k] = v
1060
+ end
1061
+ #; [!jw2kx] recovers hash values.
1062
+ recover = proc do
1063
+ key_not_exists.each {|k, _| hashobj.delete(k) }
1064
+ prev_values.each {|k, v| hashobj[k] = v }
1065
+ end
1066
+ #; [!w3r0p] returns keyvals.
1067
+ #; [!pwq6v] can take block argument.
1068
+ return __do_dummy(keyvals, recover, &b)
1069
+ end
1070
+
1071
+ def dummy_attrs(object, **keyvals, &b)
1072
+ #; [!4vd73] changes object attributes temporarily.
1073
+ prev_values = {}
1074
+ keyvals.each do |k, v|
1075
+ prev_values[k] = object.__send__(k)
1076
+ object.__send__("#{k}=", v)
1077
+ end
1078
+ #; [!fi0t3] recovers attribute values.
1079
+ recover = proc do
1080
+ prev_values.each {|k, v| object.__send__("#{k}=", v) }
1081
+ end
1082
+ #; [!27yeh] returns keyvals.
1083
+ #; [!j7tvp] can take block argument.
1084
+ return __do_dummy(keyvals, recover, &b)
1085
+ end
1086
+
1087
+ def dummy_ivars(object, **keyvals, &b)
1088
+ #; [!rnqiv] changes instance variables temporarily.
1089
+ prev_values = {}
1090
+ keyvals.each do |k, v|
1091
+ prev_values[k] = object.instance_variable_get("@#{k}")
1092
+ object.instance_variable_set("@#{k}", v)
1093
+ end
1094
+ #; [!8oirn] recovers instance variables.
1095
+ recover = proc do
1096
+ prev_values.each {|k, v| object.instance_variable_set("@#{k}", v) }
1097
+ end
1098
+ #; [!01dc8] returns keyvals.
1099
+ #; [!myzk4] can take block argument.
1100
+ return __do_dummy(keyvals, recover, &b)
1101
+ end
1102
+
1103
+ def recorder()
1104
+ #; [!qwrr8] loads 'benry/recorder' automatically.
1105
+ require 'benry/recorder' unless defined?(Benry::Recorder)
1106
+ #; [!glfvx] creates Benry::Recorder object.
1107
+ return Benry::Recorder.new
1108
+ end
1109
+
1110
+ end
1111
+
1112
+
1113
+ class Visitor
1114
+
1115
+ def start()
1116
+ #; [!8h8qf] start visiting tree.
1117
+ #visit_scope(THE_GLOBAL_SCOPE, -1, nil)
1118
+ THE_GLOBAL_SCOPE.each_child {|c| c.accept_visitor(self, 0, nil) }
1119
+ end
1120
+
1121
+ def visit_scope(scope, depth, parent)
1122
+ #; [!hebhz] visits each child scope.
1123
+ scope.each_child {|c| c.accept_visitor(self, depth+1, scope) }
1124
+ end
1125
+
1126
+ def visit_topic(topic, depth, parent)
1127
+ #; [!mu3fn] visits each child of topic.
1128
+ topic.each_child {|c| c.accept_visitor(self, depth+1, topic) }
1129
+ end
1130
+
1131
+ def visit_spec(spec, depth, parent)
1132
+ #; [!9f7i9] do something on spec.
1133
+ end
1134
+
1135
+ end
1136
+
1137
+
1138
+ class Traverser < Visitor
1139
+
1140
+ def start()
1141
+ #; [!5zonp] visits topics and specs and calls callbacks.
1142
+ #; [!gkopz] doesn't change Oktest::THE_GLOBAL_SCOPE.
1143
+ #visit_scope(THE_GLOBAL_SCOPE, -1, nil)
1144
+ THE_GLOBAL_SCOPE.each_child {|c| c.accept_visitor(self, 0, nil) }
1145
+ end
1146
+
1147
+ def visit_scope(scope, depth, parent) #:nodoc:
1148
+ #; [!ledj3] calls on_scope() callback on scope.
1149
+ on_scope(scope.filename, scope.tag, depth) do
1150
+ scope.each_child {|c| c.accept_visitor(self, depth+1, scope) }
1151
+ end
1152
+ end
1153
+
1154
+ def visit_topic(topic, depth, parent) #:nodoc:
1155
+ #; [!x8r9w] calls on_topic() callback on topic.
1156
+ if topic._prefix == '*'
1157
+ on_topic(topic.target, topic.tag, depth) do
1158
+ topic.each_child {|c| c.accept_visitor(self, depth+1, topic) }
1159
+ end
1160
+ #; [!qh0q3] calls on_case() callback on case_when or case_else.
1161
+ else
1162
+ on_case(topic.target, topic.tag, depth) do
1163
+ topic.each_child {|c| c.accept_visitor(self, depth+1, topic) }
1164
+ end
1165
+ end
1166
+ end
1167
+
1168
+ def visit_spec(spec, depth, parent) #:nodoc:
1169
+ #; [!41uyj] calls on_spec() callback.
1170
+ on_spec(spec.desc, spec.tag, depth)
1171
+ end
1172
+
1173
+ def on_scope(scope_filename, tag, depth)
1174
+ yield
1175
+ end
1176
+
1177
+ def on_topic(topic_target, tag, depth)
1178
+ yield
1179
+ end
1180
+
1181
+ def on_case(case_cond, tag, depth)
1182
+ yield
1183
+ end
1184
+
1185
+ def on_spec(spec_desc, tag, depth)
1186
+ end
1187
+
1188
+ end
1189
+
1190
+
1191
+ STATUSES = [:PASS, :FAIL, :ERROR, :SKIP, :TODO]
1192
+
1193
+
1194
+ class Runner < Visitor
1195
+
1196
+ def initialize(reporter)
1197
+ @reporter = reporter
1198
+ end
1199
+
1200
+ def start()
1201
+ #; [!xrisl] runs topics and specs.
1202
+ #; [!dth2c] clears toplvel scope list.
1203
+ @reporter.enter_all(self)
1204
+ visit_scope(THE_GLOBAL_SCOPE, -1, nil)
1205
+ THE_GLOBAL_SCOPE.clear_children()
1206
+ @reporter.exit_all(self)
1207
+ end
1208
+
1209
+ def visit_scope(scope, depth, parent)
1210
+ @reporter.enter_scope(scope) unless scope.equal?(THE_GLOBAL_SCOPE)
1211
+ #; [!5anr7] calls before_all and after_all blocks.
1212
+ call_before_all_block(scope)
1213
+ scope.each_child {|c| c.accept_visitor(self, depth+1, scope) }
1214
+ call_after_all_block(scope)
1215
+ @reporter.exit_scope(scope) unless scope.equal?(THE_GLOBAL_SCOPE)
1216
+ end
1217
+
1218
+ def visit_topic(topic, depth, parent)
1219
+ @reporter.enter_topic(topic, depth)
1220
+ #; [!i3yfv] calls 'before_all' and 'after_all' blocks.
1221
+ call_before_all_block(topic)
1222
+ topic.each_child {|c| c.accept_visitor(self, depth+1, topic) }
1223
+ call_after_all_block(topic)
1224
+ @reporter.exit_topic(topic, depth)
1225
+ end
1226
+
1227
+ def visit_spec(spec, depth, parent)
1228
+ @reporter.enter_spec(spec, depth)
1229
+ #; [!u45di] runs spec block with context object which allows to call methods defined in topics.
1230
+ node = parent
1231
+ context = node.new_context_object()
1232
+ #; [!yagka] calls 'before' and 'after' blocks with context object as self.
1233
+ call_before_blocks(node, context)
1234
+ status = :PASS
1235
+ exc = nil
1236
+ #; [!yd24o] runs spec body, catching assertions or exceptions.
1237
+ begin
1238
+ params = Util.required_param_names_of_block(spec.block)
1239
+ values = params.nil? || params.empty? ? [] \
1240
+ : get_fixture_values(params, node, spec, context)
1241
+ spec.run_block_in_context_object(context, *values)
1242
+ rescue NoMemoryError => exc; raise exc
1243
+ rescue SignalException => exc; raise exc
1244
+ rescue FAIL_EXCEPTION => exc; status = :FAIL
1245
+ rescue SKIP_EXCEPTION => exc; status = :SKIP
1246
+ rescue TODO_EXCEPTION => exc; status = :TODO
1247
+ rescue Exception => exc; status = :ERROR
1248
+ end
1249
+ #; [!68cnr] if TODO() called in spec...
1250
+ if context.__TODO
1251
+ #; [!6ol3p] changes PASS status to FAIL because test passed unexpectedly.
1252
+ if status == :PASS
1253
+ status = :FAIL
1254
+ exc = FAIL_EXCEPTION.new("spec should be failed (because not implemented yet), but passed unexpectedly.")
1255
+ #; [!6syw4] changes FAIL status to TODO because test failed expectedly.
1256
+ elsif status == :FAIL
1257
+ status = :TODO
1258
+ exc = TODO_EXCEPTION.new("not implemented yet")
1259
+ #; [!4aecm] changes also ERROR status to TODO because test failed expectedly.
1260
+ elsif status == :ERROR
1261
+ status = :TODO
1262
+ exc = TODO_EXCEPTION.new("#{exc.class} raised because not implemented yet")
1263
+ end
1264
+ location = context.__TODO
1265
+ exc.set_backtrace([location])
1266
+ end
1267
+ #; [!dihkr] calls 'at_end' blocks, even when exception raised.
1268
+ begin
1269
+ call_at_end_blocks(context)
1270
+ #; [!76g7q] calls 'after' blocks even when exception raised.
1271
+ ensure
1272
+ call_after_blocks(node, context)
1273
+ end
1274
+ @reporter.exit_spec(spec, depth, status, exc, parent)
1275
+ end
1276
+
1277
+ private
1278
+
1279
+ def get_fixture_values(names, node, spec, context)
1280
+ return THE_FIXTURE_MANAGER.get_fixture_values(names, node, spec, context)
1281
+ end
1282
+
1283
+ def _call_blocks_parent_first(node, name, context)
1284
+ blocks = []
1285
+ while node
1286
+ block = node.get_hook_block(name)
1287
+ blocks << block if block
1288
+ node = node.parent
1289
+ end
1290
+ blocks.reverse.each {|blk| context.instance_eval(&blk) }
1291
+ blocks.clear
1292
+ end
1293
+
1294
+ def _call_blocks_child_first(node, name, context)
1295
+ while node
1296
+ block = node.get_hook_block(name)
1297
+ context.instance_eval(&block) if block
1298
+ node = node.parent
1299
+ end
1300
+ end
1301
+
1302
+ def call_before_blocks(node, context)
1303
+ _call_blocks_parent_first(node, :before, context)
1304
+ end
1305
+
1306
+ def call_after_blocks(node, context)
1307
+ _call_blocks_child_first(node, :after, context)
1308
+ end
1309
+
1310
+ def call_before_all_block(node)
1311
+ block = node.get_hook_block(:before_all)
1312
+ node.instance_eval(&block) if block
1313
+ end
1314
+
1315
+ def call_after_all_block(node)
1316
+ block = node.get_hook_block(:after_all)
1317
+ node.instance_eval(&block) if block
1318
+ end
1319
+
1320
+ def call_at_end_blocks(context)
1321
+ blocks = context.__at_end_blocks
1322
+ if blocks
1323
+ blocks.reverse_each {|block| context.instance_eval(&block) }
1324
+ blocks.clear
1325
+ end
1326
+ end
1327
+
1328
+ end
1329
+
1330
+
1331
+ RUNNER_CLASS = Runner
1332
+
1333
+
1334
+ class FixtureManager
1335
+
1336
+ def get_fixture_values(names, node, spec, context, location=nil, _resolved={}, _resolving=[])
1337
+ #; [!w6ffs] resolves 'this_topic' fixture name as target objec of current topic.
1338
+ _resolved[:this_topic] = node.target if !_resolved.key?(:this_topic) && node.topic?
1339
+ #; [!ja2ew] resolves 'this_spec' fixture name as description of current spec.
1340
+ _resolved[:this_spec] = spec.desc if !_resolved.key?(:this_spec)
1341
+ #; [!v587k] resolves fixtures.
1342
+ location ||= spec.location
1343
+ return names.collect {|name|
1344
+ #; [!np4p9] raises error when loop exists in dependency.
1345
+ ! _resolving.include?(name) or
1346
+ raise _looped_dependency_error(name, _resolving, location)
1347
+ get_fixture_value(name, node, spec, context, location, _resolved, _resolving)
1348
+ }
1349
+ end
1350
+
1351
+ def get_fixture_value(name, node, spec, context, location=nil, _resolved={}, _resolving=[])
1352
+ return _resolved[name] if _resolved.key?(name)
1353
+ location ||= spec.location
1354
+ tuple = node.get_fixture_block(name)
1355
+ if tuple
1356
+ block, param_names, location = tuple
1357
+ #; [!2esaf] resolves fixture dependencies.
1358
+ if param_names
1359
+ _resolving << name
1360
+ args = get_fixture_values(param_names, node, spec, context, location, _resolved, _resolving)
1361
+ (popped = _resolving.pop) == name or
1362
+ raise "** assertion failed: name=#{name.inspect}, resolvng[-1]=#{popped.inspect}"
1363
+ #; [!4xghy] calls fixture block with context object as self.
1364
+ val = context.instance_exec(*args, &block)
1365
+ else
1366
+ val = context.instance_eval(&block)
1367
+ end
1368
+ #; [!8t3ul] caches fixture value to call fixture block only once per spec.
1369
+ _resolved[name] = val
1370
+ return val
1371
+ elsif node.parent
1372
+ #; [!4chb9] traverses parent topics if fixture not found in current topic.
1373
+ return get_fixture_value(name, node.parent, spec, context, location, _resolved, _resolving)
1374
+ elsif ! node.equal?(THE_GLOBAL_SCOPE)
1375
+ #; [!wt3qk] suports global scope.
1376
+ return get_fixture_value(name, THE_GLOBAL_SCOPE, spec, context, location, _resolved, _resolving)
1377
+ else
1378
+ #; [!nr79z] raises error when fixture not found.
1379
+ exc = FixtureNotFoundError.new("#{name}: fixture not found. (spec: #{spec.desc})")
1380
+ exc.set_backtrace([location]) if location
1381
+ raise exc
1382
+ end
1383
+ end
1384
+
1385
+ private
1386
+
1387
+ def _looped_dependency_error(name, resolving, location)
1388
+ resolving << name
1389
+ i = resolving.index(name)
1390
+ s1 = resolving[0...i].join('->')
1391
+ s2 = resolving[i..-1].join('=>')
1392
+ loop = s1.empty? ? s2 : "#{s1}->#{s2}"
1393
+ #location = $1 if location =~ /(.*:\d+)/
1394
+ exc = LoopedDependencyError.new("fixture dependency is looped: #{loop}")
1395
+ exc.set_backtrace([location])
1396
+ return exc
1397
+ end
1398
+
1399
+ end
1400
+
1401
+ THE_FIXTURE_MANAGER = FixtureManager.new()
1402
+
1403
+
1404
+ class FixtureNotFoundError < StandardError
1405
+ end
1406
+
1407
+ class LoopedDependencyError < StandardError
1408
+ end
1409
+
1410
+
1411
+ class Reporter
1412
+
1413
+ def enter_all(runner); end
1414
+ def exit_all(runner); end
1415
+ def enter_scope(scope); end
1416
+ def exit_scope(scope); end
1417
+ def enter_topic(topic, depth); end
1418
+ def exit_topic(topic, depth); end
1419
+ def enter_spec(spec, depth); end
1420
+ def exit_spec(spec, depth, status, error, parent); end
1421
+ #
1422
+ def counts; {}; end
1423
+
1424
+ end
1425
+
1426
+
1427
+ class BaseReporter < Reporter
1428
+
1429
+ LABELS = { :PASS=>'pass', :FAIL=>'Fail', :ERROR=>'ERROR', :SKIP=>'Skip', :TODO=>'TODO' }
1430
+ CHARS = { :PASS=>'.', :FAIL=>'f', :ERROR=>'E', :SKIP=>'s', :TODO=>'t' }
1431
+
1432
+
1433
+ def initialize
1434
+ @exceptions = []
1435
+ @counts = {}
1436
+ end
1437
+
1438
+ attr_reader :counts
1439
+
1440
+ def enter_all(runner)
1441
+ #; [!pq3ia] initalizes counter by zero.
1442
+ reset_counts()
1443
+ @start_at = Time.now
1444
+ end
1445
+
1446
+ def exit_all(runner)
1447
+ #; [!wjp7u] prints footer with elapsed time.
1448
+ elapsed = Time.now - @start_at
1449
+ puts footer(elapsed)
1450
+ end
1451
+
1452
+ def enter_scope(scope)
1453
+ end
1454
+
1455
+ def exit_scope(scope)
1456
+ end
1457
+
1458
+ def enter_topic(topic, depth)
1459
+ end
1460
+
1461
+ def exit_topic(topic, depth)
1462
+ end
1463
+
1464
+ def enter_spec(spec, depth)
1465
+ end
1466
+
1467
+ def exit_spec(spec, depth, status, exc, parent)
1468
+ #; [!r6yge] increments counter according to status.
1469
+ @counts[status] += 1
1470
+ #; [!nupb4] keeps exception info when status is FAIL or ERROR.
1471
+ @exceptions << [spec, status, exc, parent] if status == :FAIL || status == :ERROR
1472
+ end
1473
+
1474
+ protected
1475
+
1476
+ def reset_counts()
1477
+ #; [!oc29s] clears counters to zero.
1478
+ STATUSES.each {|sym| @counts[sym] = 0 }
1479
+ end
1480
+
1481
+ def print_exceptions()
1482
+ #; [!fbr16] prints assertion failures and excerptions with separator.
1483
+ sep = '-' * 70
1484
+ @exceptions.each do |tuple|
1485
+ puts sep
1486
+ print_exc(*tuple)
1487
+ tuple.clear
1488
+ end
1489
+ #; [!2s9r2] prints nothing when no fails nor errors.
1490
+ puts sep if ! @exceptions.empty?
1491
+ #; [!ueeih] clears exceptions.
1492
+ @exceptions.clear
1493
+ end
1494
+
1495
+ def print_exc(spec, status, exc, topic)
1496
+ #; [!5ara3] prints exception info of assertion failure.
1497
+ #; [!pcpy4] prints exception info of error.
1498
+ label = Color.status(status, LABELS[status])
1499
+ path = Color.topic(spec_path(spec, topic))
1500
+ puts "[#{label}] #{path}"
1501
+ print_exc_backtrace(exc, status)
1502
+ print_exc_message(exc, status)
1503
+ end
1504
+
1505
+ def print_exc_backtrace(exc, status)
1506
+ #; [!ocxy6] prints backtrace info and lines in file.
1507
+ rexp = FILENAME_FILTER
1508
+ prev_file = prev_line = nil
1509
+ exc.backtrace.each_with_index do |str, i|
1510
+ #; [!jbped] skips backtrace of oktest.rb when assertion failure.
1511
+ #; [!cfkzg] don't skip first backtrace entry when error.
1512
+ next if str =~ rexp && ! (i == 0 && status == :ERROR)
1513
+ linestr = nil
1514
+ if str =~ /:(\d+)/
1515
+ file = $` # file path
1516
+ line = $1.to_i # line number
1517
+ next if file == prev_file && line == prev_line
1518
+ linestr = Util.file_line(file, line) if str && File.exist?(file)
1519
+ prev_file, prev_line = file, line
1520
+ end
1521
+ puts " #{str}"
1522
+ puts " #{linestr.strip}" if linestr
1523
+ end
1524
+ end
1525
+
1526
+ FILENAME_FILTER = %r`/(?:oktest|minitest/unit|test/unit(?:/assertions|/testcase)?)(?:\.rbc?)?:` #:nodoc:
1527
+
1528
+ def print_exc_message(exc, status)
1529
+ #; [!hr7jn] prints detail of assertion failed.
1530
+ #; [!pd41p] prints detail of exception.
1531
+ if status == :FAIL
1532
+ msg = "#{exc}"
1533
+ else
1534
+ msg = "#{exc.class.name}: #{exc}"
1535
+ end
1536
+ lines = []
1537
+ msg.each_line {|line| lines << line }
1538
+ puts lines.shift.chomp
1539
+ puts lines.join.chomp unless lines.empty?
1540
+ puts exc.diff if exc.respond_to?(:diff) && exc.diff # for oktest.rb
1541
+ end
1542
+
1543
+ def footer(elapsed)
1544
+ #; [!iy4uo] calculates total count of specs.
1545
+ total = 0; @counts.each {|_, v| total += v }
1546
+ #; [!2nnma] includes count of each status.
1547
+ arr = STATUSES.collect {|st|
1548
+ s = "#{st.to_s.downcase}:#{@counts[st]}"
1549
+ @counts[st] == 0 ? s : Color.status(st, s)
1550
+ }
1551
+ #; [!fp57l] includes elapsed time.
1552
+ #; [!r5y02] elapsed time format is adjusted along to time length.
1553
+ hhmmss = Util.hhmmss(elapsed)
1554
+ #; [!gx0n2] builds footer line.
1555
+ return "## total:#{total} (#{arr.join(', ')}) in #{hhmmss}s"
1556
+ end
1557
+
1558
+ def spec_path(spec, topic)
1559
+ #; [!dv6fu] returns path string from top topic to current spec.
1560
+ arr = [spec.desc]
1561
+ while topic && topic.topic?
1562
+ arr << topic.target.to_s if topic.target
1563
+ topic = topic.parent
1564
+ end
1565
+ return arr.reverse.join(" > ")
1566
+ end
1567
+
1568
+ end
1569
+
1570
+
1571
+ class VerboseReporter < BaseReporter
1572
+ #; [!6o9nw] reports topic name and spec desc.
1573
+
1574
+ LABELS = { :PASS=>'pass', :FAIL=>'Fail', :ERROR=>'ERROR', :SKIP=>'Skip', :TODO=>'TODO' }
1575
+
1576
+ def enter_topic(topic, depth)
1577
+ super
1578
+ puts "#{' ' * (depth - 1)}#{topic._prefix} #{Color.topic(topic.target)}"
1579
+ end
1580
+
1581
+ def exit_topic(topic, depth)
1582
+ print_exceptions()
1583
+ end
1584
+
1585
+ def enter_spec(spec, depth)
1586
+ if $stdout.tty?
1587
+ str = "#{' ' * (depth - 1)}#{spec._prefix} [ ] #{spec.desc}"
1588
+ print Util.strfold(str, 79)
1589
+ $stdout.flush
1590
+ end
1591
+ end
1592
+
1593
+ def exit_spec(spec, depth, status, error, parent)
1594
+ super
1595
+ if $stdout.tty?
1596
+ print "\r" # clear line
1597
+ $stdout.flush
1598
+ end
1599
+ label = Color.status(status, LABELS[status] || '???')
1600
+ msg = "#{' ' * (depth - 1)}- [#{label}] #{spec.desc}"
1601
+ msg << " " << Color.reason("(reason: #{error.message})") if status == :SKIP
1602
+ puts msg
1603
+ end
1604
+
1605
+ end
1606
+
1607
+
1608
+ class SimpleReporter < BaseReporter
1609
+ #; [!xfd5o] reports filename.
1610
+
1611
+ def enter_scope(scope)
1612
+ print "#{scope.filename}: "
1613
+ end
1614
+
1615
+ def exit_scope(scope)
1616
+ puts()
1617
+ print_exceptions()
1618
+ end
1619
+
1620
+ def exit_spec(spec, depth, status, error, parent)
1621
+ super
1622
+ print Color.status(status, CHARS[status] || '?')
1623
+ $stdout.flush
1624
+ end
1625
+
1626
+ end
1627
+
1628
+
1629
+ class PlainReporter < BaseReporter
1630
+ #; [!w842j] reports progress.
1631
+
1632
+ def exit_all(runner)
1633
+ elapsed = Time.now - @start_at
1634
+ puts()
1635
+ print_exceptions()
1636
+ puts footer(elapsed)
1637
+ end
1638
+
1639
+ def exit_spec(spec, depth, status, error, parent)
1640
+ super
1641
+ print Color.status(status, CHARS[status] || '?')
1642
+ $stdout.flush
1643
+ end
1644
+
1645
+ end
1646
+
1647
+
1648
+ class QuietReporter < BaseReporter
1649
+ #; [!0z4im] reports all statuses except PASS status.
1650
+
1651
+ def exit_all(runner)
1652
+ elapsed = Time.now - @start_at
1653
+ puts()
1654
+ print_exceptions()
1655
+ puts footer(elapsed)
1656
+ end
1657
+
1658
+ def exit_spec(spec, depth, status, error, parent)
1659
+ super
1660
+ if status != :PASS
1661
+ print Color.status(status, CHARS[status] || '?')
1662
+ $stdout.flush
1663
+ end
1664
+ end
1665
+
1666
+ end
1667
+
1668
+
1669
+ REPORTER_CLASS = VerboseReporter
1670
+
1671
+
1672
+ REPORTER_CLASSES = {
1673
+ 'verbose' => VerboseReporter, 'v' => VerboseReporter,
1674
+ 'simple' => SimpleReporter, 's' => SimpleReporter,
1675
+ 'plain' => PlainReporter, 'p' => PlainReporter,
1676
+ 'quiet' => QuietReporter, 'q' => QuietReporter,
1677
+ }
1678
+
1679
+
1680
+ def self.run(reporter: nil, style: nil)
1681
+ #; [!kfi8b] do nothing when 'Oktest.scope()' not called.
1682
+ return unless THE_GLOBAL_SCOPE.has_child?
1683
+ #; [!6xn3t] creates reporter object according to 'style:' keyword arg.
1684
+ klass = (style ? REPORTER_CLASSES[style] : REPORTER_CLASS) or
1685
+ raise ArgumentError, "#{style.inspect}: unknown style."
1686
+ reporter ||= klass.new
1687
+ #; [!mn451] run test cases.
1688
+ runner = RUNNER_CLASS.new(reporter)
1689
+ runner.start()
1690
+ ! THE_GLOBAL_SCOPE.has_child? or "** internal error"
1691
+ #; [!p52se] returns total number of failures and errors.
1692
+ counts = reporter.counts
1693
+ return counts[:FAIL] + counts[:ERROR]
1694
+ end
1695
+
1696
+
1697
+ module Util
1698
+
1699
+ module_function
1700
+
1701
+ def file_line(filename, linenum)
1702
+ #; [!4z65g] returns nil if file not exist or not a file.
1703
+ return nil unless File.file?(filename)
1704
+ #; [!4a2ji] caches recent file content for performance reason.
1705
+ @__cache ||= [nil, []]
1706
+ if @__cache[0] != filename
1707
+ #; [!wtrl5] recreates cache data if other file requested.
1708
+ @__cache[0] = filename
1709
+ @__cache[1].clear
1710
+ @__cache[1] = lines = File.open(filename, 'rb') {|f| f.to_a }
1711
+ else
1712
+ lines = @__cache[1]
1713
+ end
1714
+ #; [!162e1] returns line string.
1715
+ return lines[linenum-1]
1716
+ end
1717
+
1718
+ def required_param_names_of_block(block)
1719
+ #; [!a9n46] returns nil if argument is nil.
1720
+ return nil unless block
1721
+ #; [!7m81p] returns empty array if block has no parameters.
1722
+ n = block.arity
1723
+ n = - n - 1 if n < 0
1724
+ return [] if n == 0
1725
+ #; [!n3g63] returns parameter names of block.
1726
+ #; [!d5kym] collects only normal parameter names.
1727
+ param_names = block.parameters[0...n].collect {|pair| pair[1] }
1728
+ return param_names
1729
+ end
1730
+
1731
+ def strfold(str, width=80, mark='...')
1732
+ #; [!wb7m8] returns string as it is if string is not long.
1733
+ return str if str.bytesize <= width
1734
+ #; [!a2igb] shorten string if it is enough long.
1735
+ return str[0, width - mark.length] + mark if str.ascii_only?
1736
+ #; [!0gjye] supports non-ascii characters.
1737
+ limit = width - mark.length
1738
+ w = len = 0
1739
+ str.each_char do |ch|
1740
+ w += ch.bytesize == 1 ? 1 : 2
1741
+ break if w >= limit
1742
+ len += 1
1743
+ end
1744
+ str = str[0, len] + mark if w >= limit
1745
+ return str
1746
+ end
1747
+
1748
+ def hhmmss(n)
1749
+ h, n = n.divmod(60*60)
1750
+ m, s = n.divmod(60)
1751
+ #; [!shyl1] converts 400953.444 into '111:22:33.4'.
1752
+ #; [!vyi2v] converts 5025.678 into '1:23:45.7'.
1753
+ return "%d:%02d:%04.1f" % [h, m, s] if h > 0
1754
+ #; [!pm4xf] converts 754.888 into '12:34.9'.
1755
+ #; [!lwewr] converts 83.444 into '1:23.4'.
1756
+ return "%d:%04.1f" % [m, s] if m > 0
1757
+ #; [!ijx52] converts 56.8888 into '56.9'.
1758
+ return "%.1f" % s if s >= 10
1759
+ #; [!2kra2] converts 9.777 into '9.78'.
1760
+ return "%.2f" % s if s >= 1
1761
+ #; [!4aomb] converts 0.7777 into '0.778'.
1762
+ return "%.3f" % s
1763
+ end
1764
+
1765
+ def _text2lines(text, no_newline_msg=nil)
1766
+ lines = []
1767
+ text.each_line {|line| line.chomp!; lines << line }
1768
+ lines[-1] << no_newline_msg if no_newline_msg && text[-1] && text[-1] != ?\n
1769
+ return lines
1770
+ end
1771
+ private :_text2lines
1772
+
1773
+ ## platform independent, but requires 'diff-lcs' gem
1774
+ def unified_diff(text_old, text_new, label="--- old\n+++ new\n", context=3)
1775
+ #; [!rnx4f] checks whether text string ends with newline char.
1776
+ msg = "\\ No newline at end of string"
1777
+ lines_old = _text2lines(text_old, msg)
1778
+ lines_new = _text2lines(text_new, msg)
1779
+ #; [!wf4ns] calculates unified diff from two text strings.
1780
+ buf = [label]
1781
+ len = 0
1782
+ prevhunk = hunk = nil
1783
+ diffs = Diff::LCS.diff(lines_old, lines_new)
1784
+ diffs.each do |diff|
1785
+ hunk = Diff::LCS::Hunk.new(lines_old, lines_new, diff, context, len)
1786
+ if hunk.overlaps?(prevhunk)
1787
+ hunk.unshift(prevhunk)
1788
+ else
1789
+ buf << prevhunk.diff(:unified) << "\n"
1790
+ end if prevhunk
1791
+ len = hunk.file_length_difference
1792
+ prevhunk = hunk
1793
+ end
1794
+ buf << prevhunk.diff(:unified) << "\n" if prevhunk
1795
+ return buf.join()
1796
+ end
1797
+
1798
+ ## platform depend, but not require extra library
1799
+ def diff_unified(text_old, text_new, label="--- old\n+++ new\n", context=3)
1800
+ #; [!ulyq5] returns unified diff string of two text strings.
1801
+ #; [!6tgum] detects whether char at end of file is newline or not.
1802
+ tmp_old = "_tmp.old.#{rand()}"
1803
+ tmp_new = "_tmp.new.#{rand()}"
1804
+ File.open(tmp_old, 'w') {|f| f.write(text_old) }
1805
+ File.open(tmp_new, 'w') {|f| f.write(text_new) }
1806
+ begin
1807
+ #diff = `diff -u #{tmp_old} #{tmp_new}`
1808
+ diff = `diff --unified=#{context} #{tmp_old} #{tmp_new}`
1809
+ ensure
1810
+ File.unlink(tmp_old)
1811
+ File.unlink(tmp_new)
1812
+ end
1813
+ diff.sub!(/\A\-\-\-.*\n\+\+\+.*\n/, label.to_s)
1814
+ return diff
1815
+ end
1816
+
1817
+ ## when diff-lcs is not installed then use diff command
1818
+ begin
1819
+ require 'diff/lcs'
1820
+ #require 'diff/lcs/string'
1821
+ require 'diff/lcs/hunk'
1822
+ rescue LoadError
1823
+ alias _unified_diff unified_diff
1824
+ alias unified_diff diff_unified
1825
+ class << self
1826
+ alias _unified_diff unified_diff
1827
+ alias unified_diff diff_unified
1828
+ end
1829
+ end
1830
+
1831
+ end
1832
+
1833
+
1834
+ class Config
1835
+
1836
+ @os_windows = RUBY_PLATFORM =~ /mswin|mingw/i
1837
+ @auto_run = true
1838
+ @ok_location = true # false will make 'ok()' faster
1839
+ @color_available = ! @os_windows || ENV['COLORTERM'] =~ /color|24bit/i
1840
+ @color_enabled = @color_available && $stdout.tty?
1841
+ @diff_command = @os_windows ? "diff.exe -u" : "diff -u"
1842
+
1843
+ class << self
1844
+ attr_accessor :auto_run, :ok_location, :color_available, :color_enabled
1845
+ end
1846
+
1847
+ end
1848
+
1849
+
1850
+ class Filter < Visitor
1851
+
1852
+ def initialize(topic_pattern, spec_pattern, tag_pattern, negative: false)
1853
+ @topic_pattern = topic_pattern
1854
+ @spec_pattern = spec_pattern
1855
+ @tag_pattern = tag_pattern
1856
+ @negative = negative
1857
+ end
1858
+
1859
+ attr_reader :topic_pattern, :spec_pattern, :tag_pattern, :negative
1860
+
1861
+ def self.create_from(pattern) # ex: 'topic=name', 'spec="*pat*"'
1862
+ #; [!gtpt1] parses 'sid=...' as filter pattern for spec.
1863
+ pattern = "spec#{$1}\\[!#{$2}\\]*" if pattern =~ /\Asid(=|!=)(.*)/ # filter by spec id
1864
+ #; [!xt364] parses 'topic=...' as filter pattern for topic.
1865
+ #; [!53ega] parses 'spec=...' as filter pattern for spec.
1866
+ #; [!go6us] parses 'tag=...' as filter pattern for tag.
1867
+ #; [!cmp6e] raises ArgumentError when invalid argument.
1868
+ pat = {'topic'=>nil, 'spec'=>nil, 'tag'=>nil}
1869
+ pattern =~ /\A(\w+)(=|!=)/ && pat.key?($1) or
1870
+ raise ArgumentError, "#{pattern.inspect}: unexpected pattern string."
1871
+ pat[$1] = $'
1872
+ #; [!5hl7z] parses 'xxx!=...' as negative filter pattern.
1873
+ negative = ($2 == '!=')
1874
+ #; [!9dzmg] returns filter object.
1875
+ return self.new(pat['topic'], pat['spec'], pat['tag'], negative: negative)
1876
+ end
1877
+
1878
+ def scope_match?(scope)
1879
+ #; [!zkq6r] returns true only if tag name matched to pattern.
1880
+ return true if @tag_pattern && _match_tag?(scope.tag, @tag_pattern)
1881
+ return false
1882
+ end
1883
+ alias visit_scope scope_match?
1884
+
1885
+ def topic_match?(topic)
1886
+ #; [!jpycj] returns true if topic target name matched to pattern.
1887
+ #; [!6lfp1] returns true if tag name matched to pattern.
1888
+ return true if @topic_pattern && _match?(topic.target.to_s, @topic_pattern)
1889
+ return true if @tag_pattern && _match_tag?(topic.tag, @tag_pattern)
1890
+ return false
1891
+ end
1892
+ alias visit_topic topic_match?
1893
+
1894
+ def spec_match?(spec)
1895
+ #; [!k45p3] returns true if spec description matched to pattern.
1896
+ #; [!li3pd] returns true if tag name matched to pattern.
1897
+ return true if @spec_pattern && _match?(spec.desc, @spec_pattern)
1898
+ return true if @tag_pattern && _match_tag?(spec.tag, @tag_pattern)
1899
+ return false
1900
+ end
1901
+ alias visit_spec spec_match?
1902
+
1903
+ private
1904
+
1905
+ def _match?(str, pattern)
1906
+ #; [!h90x3] returns true if str matched to pattern.
1907
+ return File.fnmatch(pattern, str.to_s, File::FNM_EXTGLOB)
1908
+ end
1909
+
1910
+ def _match_tag?(tag, pattern)
1911
+ #; [!lyo18] returns false if tag is nil.
1912
+ #; [!8lxin] returns true if tag matched to pattern.
1913
+ #; [!7wxmh] supports multiple tag names.
1914
+ return false if tag.nil?
1915
+ return [tag].flatten.any? {|tag_| _match?(tag_, pattern) }
1916
+ end
1917
+
1918
+ public
1919
+
1920
+ def filter_children!(node)
1921
+ _filter_children!(node)
1922
+ end
1923
+
1924
+ private
1925
+
1926
+ def _filter_children!(node) #:nodoc:
1927
+ #; [!r6g6a] supports negative filter by topic.
1928
+ #; [!doozg] supports negative filter by spec.
1929
+ #; [!ntv44] supports negative filter by tag name.
1930
+ positive = ! @negative
1931
+ #
1932
+ i = -1
1933
+ removes = []
1934
+ node.each_child do |item|
1935
+ i += 1
1936
+ #; [!osoq2] can filter topics by full name.
1937
+ #; [!wzcco] can filter topics by pattern.
1938
+ #; [!eirmu] can filter topics by tag name.
1939
+ #; [!0kw9c] can filter specs by full name.
1940
+ #; [!fd8wt] can filter specs by pattern.
1941
+ #; [!6sq7g] can filter specs by tag name.
1942
+ #; [!6to6n] can filter by multiple tag name.
1943
+ if item.accept_visitor(self)
1944
+ removes << i unless positive
1945
+ #; [!mz6id] can filter nested topics.
1946
+ elsif item.is_a?(Node)
1947
+ removes << i unless _filter_children!(item)
1948
+ #; [!1jphf] can filter specs from nested topics.
1949
+ elsif item.is_a?(SpecLeaf)
1950
+ removes << i if positive
1951
+ else
1952
+ raise "** internal error: item=#{item.inspect}"
1953
+ end
1954
+ end
1955
+ removes.reverse.each {|j| node.remove_child_at(j) }
1956
+ return node.has_child?
1957
+ end
1958
+
1959
+ end
1960
+
1961
+ FILTER_CLASS = Filter
1962
+
1963
+ def self.filter(filter_obj)
1964
+ filter_obj.filter_children!(THE_GLOBAL_SCOPE)
1965
+ end
1966
+
1967
+
1968
+ module Color
1969
+
1970
+ module_function
1971
+
1972
+ def normal s; return s; end
1973
+ def bold s; return "\e[0;1m#{s}\e[22m"; end
1974
+ def black s; return "\e[1;30m#{s}\e[0m"; end
1975
+ def red s; return "\e[1;31m#{s}\e[0m"; end
1976
+ def green s; return "\e[1;32m#{s}\e[0m"; end
1977
+ def yellow s; return "\e[1;33m#{s}\e[0m"; end
1978
+ def blue s; return "\e[1;34m#{s}\e[0m"; end
1979
+ def magenta s; return "\e[1;35m#{s}\e[0m"; end
1980
+ def cyan s; return "\e[1;36m#{s}\e[0m"; end
1981
+ def white s; return "\e[1;37m#{s}\e[0m"; end
1982
+
1983
+ def topic s; Config.color_enabled ? bold(s) : s; end
1984
+ def spec s; Config.color_enabled ? normal(s) : s; end
1985
+ def pass s; Config.color_enabled ? blue(s) : s; end
1986
+ def fail s; Config.color_enabled ? red(s) : s; end
1987
+ def error s; Config.color_enabled ? red(s) : s; end
1988
+ def skip s; Config.color_enabled ? yellow(s) : s; end
1989
+ def todo s; Config.color_enabled ? yellow(s) : s; end
1990
+ def reason s; Config.color_enabled ? yellow(s) : s; end
1991
+
1992
+ def status(status, s)
1993
+ #; [!yev5y] returns string containing color escape sequence.
1994
+ return __send__(status.to_s.downcase, s)
1995
+ end
1996
+
1997
+ end
1998
+
1999
+
2000
+ class TestGenerator
2001
+
2002
+ def initialize(styleoption=nil)
2003
+ @styleoption = styleoption
2004
+ end
2005
+ attr_reader :styleoption
2006
+
2007
+ def parse(io)
2008
+ #; [!5mzd3] parses ruby code.
2009
+ tree = _parse(io, [], nil)
2010
+ return tree
2011
+ end
2012
+
2013
+ def _parse(io, tree, end_indent)
2014
+ while (line = io.gets())
2015
+ case line
2016
+ when /^([ \t]*)end\b/
2017
+ return tree if $1 == end_indent
2018
+ when /^([ \t]*)(module|class|def) +(\w+[.:\w]*)/
2019
+ indent, keyword, topic = $1, $2, $3
2020
+ next if line =~ /\bend$/
2021
+ if keyword == 'def'
2022
+ topic = topic =~ /^self\./ ? ".#{$'}" : "\##{topic}"
2023
+ end
2024
+ newtree = []
2025
+ _parse(io, newtree, indent)
2026
+ tree << [indent, keyword, topic, newtree]
2027
+ when /^([ \t]*)\#[:;] (.*)/
2028
+ indent, keyword, spec = $1, 'spec', $2
2029
+ tree << [indent, keyword, spec]
2030
+ end
2031
+ end
2032
+ end_indent == nil or
2033
+ raise "parse error: end_indent=#{end_indent.inspect}"
2034
+ return tree
2035
+ end
2036
+ private :_parse
2037
+
2038
+ def transform(tree, depth=1)
2039
+ #; [!te7zw] converts tree into test code.
2040
+ buf = []
2041
+ tree.each do |tuple|
2042
+ _transform(tuple, depth, buf)
2043
+ end
2044
+ buf.pop() if buf[-1] == "\n"
2045
+ return buf.join()
2046
+ end
2047
+
2048
+ def _transform(tuple, depth, buf)
2049
+ #; [!q5duk] supports 'unaryop' style option.
2050
+ unaryop = @styleoption == 'unaryop'
2051
+ indent = ' ' * (depth - 1)
2052
+ keyword = tuple[1]
2053
+ if keyword == 'spec'
2054
+ _, _, spec = tuple
2055
+ escaped = spec.gsub(/"/, '\\\"')
2056
+ buf << "\n"
2057
+ buf << "#{indent}- spec(\"#{escaped}\")\n" if unaryop
2058
+ buf << "#{indent} spec \"#{escaped}\"\n" unless unaryop
2059
+ else
2060
+ _, _, topic, children = tuple
2061
+ topic += '()' if keyword == 'def'
2062
+ topic_ = keyword == 'def' ? "'#{topic}'" : topic
2063
+ buf << "\n"
2064
+ buf << "#{indent}+ topic(#{topic_}) do\n" if unaryop
2065
+ buf << "#{indent} topic #{topic_} do\n" unless unaryop
2066
+ buf << "\n" unless keyword == 'def'
2067
+ children.each do |child_tuple|
2068
+ _transform(child_tuple, depth+1, buf)
2069
+ end
2070
+ buf << "\n"
2071
+ buf << "#{indent} end # #{topic}\n"
2072
+ buf << "\n"
2073
+ end
2074
+ end
2075
+ private :_transform
2076
+
2077
+ def generate(io)
2078
+ #; [!5hdw4] generates test code.
2079
+ tree = parse(io)
2080
+ return <<END
2081
+ # coding: utf-8
2082
+
2083
+ require 'oktest'
2084
+
2085
+ Oktest.scope do
2086
+
2087
+ #{transform(tree, 1)}
2088
+
2089
+ end
2090
+ END
2091
+ end
2092
+
2093
+ end
2094
+
2095
+
2096
+ class MainApp
2097
+
2098
+ def self.main(argv=nil)
2099
+ #; [!tb6sx] returns 0 when no errors raised.
2100
+ #; [!d5mql] returns 1 when a certain error raised.
2101
+ argv ||= ARGV
2102
+ begin
2103
+ status = self.new.run(*argv) or raise "** internal error"
2104
+ return status
2105
+ #; [!jr49p] reports error when unknown option specified.
2106
+ #; [!uqomj] reports error when required argument is missing.
2107
+ #; [!8i755] reports error when argument is invalid.
2108
+ rescue OptionParser::ParseError => exc
2109
+ case exc
2110
+ when OptionParser::InvalidOption ; s = "unknown option."
2111
+ when OptionParser::InvalidArgument ; s = "invalid argument."
2112
+ when OptionParser::MissingArgument ; s = "argument required."
2113
+ else ; s = nil
2114
+ end
2115
+ msg = s ? "#{exc.args.join(' ')}: #{s}" : exc.message
2116
+ $stderr.puts("#{File.basename($0)}: #{msg}")
2117
+ return 1
2118
+ end
2119
+ end
2120
+
2121
+ def run(*args)
2122
+ color_enabled = nil
2123
+ opts = Options.new
2124
+ parser = option_parser(opts)
2125
+ filenames = parser.parse(args)
2126
+ #; [!9973n] '-h' or '--help' option prints help message.
2127
+ if opts.help
2128
+ puts help_message()
2129
+ return 0
2130
+ end
2131
+ #; [!qqizl] '--version' option prints version number.
2132
+ if opts.version
2133
+ puts VERSION
2134
+ return 0
2135
+ end
2136
+ #; [!dk8eg] '-C' or '--create' option prints test code skeleton.
2137
+ if opts.create
2138
+ print SKELETON
2139
+ return 0
2140
+ end
2141
+ #; [!uxh5e] '-G' or '--generate' option prints test code.
2142
+ #; [!wmxu5] '--generate=unaryop' option prints test code with unary op.
2143
+ if opts.generate
2144
+ print generate(filenames, opts.generate)
2145
+ return 0
2146
+ end
2147
+ #; [!65vdx] prints help message if no arguments specified.
2148
+ if filenames.empty?
2149
+ puts help_message()
2150
+ return 0
2151
+ end
2152
+ #; [!6ro7j] '--color=on' option enables output coloring forcedly.
2153
+ #; [!vmw0q] '--color=off' option disables output coloring forcedly.
2154
+ if opts.color
2155
+ color_enabled = Config.color_enabled
2156
+ Config.color_enabled = (opts.color == 'on')
2157
+ end
2158
+ #; [!qs8ab] '--faster' chanages 'Config.ok_location' to false.
2159
+ if opts.faster
2160
+ Config.ok_location = false # will make 'ok{}' faster
2161
+ end
2162
+ #
2163
+ $LOADED_FEATURES << __FILE__ unless $LOADED_FEATURES.include?(__FILE__) # avoid loading twice
2164
+ #; [!hiu5b] finds test scripts in directory and runs them.
2165
+ load_files(filenames)
2166
+ #; [!yz7g5] '-F topic=...' option filters topics.
2167
+ #; [!ww2mp] '-F spec=...' option filters specs.
2168
+ #; [!8uvib] '-F tag=...' option filters by tag name.
2169
+ #; [!m0iwm] '-F sid=...' option filters by spec id.
2170
+ #; [!noi8i] '-F' option supports negative filter.
2171
+ if opts.filter
2172
+ filter_obj = FILTER_CLASS.create_from(opts.filter)
2173
+ Oktest.filter(filter_obj)
2174
+ end
2175
+ #; [!bim36] changes auto-running to off.
2176
+ Config.auto_run = false
2177
+ #; [!18qpe] runs test scripts.
2178
+ #; [!0qd92] '-s verbose' or '-sv' option prints test results in verbose mode.
2179
+ #; [!ef5v7] '-s simple' or '-ss' option prints test results in simple mode.
2180
+ #; [!244te] '-s plain' or '-sp' option prints test results in plain mode.
2181
+ #; [!ai61w] '-s quiet' or '-sq' option prints test results in quiet mode.
2182
+ n_errors = Oktest.run(:style=>opts.style)
2183
+ #; [!dsrae] reports if 'ok()' called but assertion not performed.
2184
+ AssertionObject.report_not_yet()
2185
+ #; [!bzgiw] returns total number of failures and errors.
2186
+ return n_errors
2187
+ ensure
2188
+ #; [!937kw] recovers 'Config.color_enabled' value.
2189
+ Config.color_enabled = color_enabled if color_enabled != nil
2190
+ end
2191
+
2192
+ private
2193
+
2194
+ class Options #:nodoc:
2195
+ attr_accessor :help, :version, :style, :filter, :color, :create, :generate, :faster
2196
+ end
2197
+
2198
+ def option_parser(opts)
2199
+ require 'optparse' unless defined?(OptionParser)
2200
+ parser = OptionParser.new
2201
+ parser.on('-h', '--help') { opts.help = true }
2202
+ parser.on( '--version') { opts.version = true }
2203
+ parser.on('-s STYLE') {|val|
2204
+ REPORTER_CLASSES.key?(val) or
2205
+ raise OptionParser::InvalidArgument, val
2206
+ opts.style = val
2207
+ }
2208
+ parser.on('-F PATTERN') {|val|
2209
+ #; [!71h2x] '-F ...' option will be error.
2210
+ val =~ /\A(topic|spec|tag|sid)(=|!=)/ or
2211
+ raise OptionParser::InvalidArgument, val
2212
+ opts.filter = val
2213
+ }
2214
+ parser.on( '--color[={on|off}]') {|val|
2215
+ #; [!9nr94] '--color=true' option raises error.
2216
+ val.nil? || val == 'on' || val == 'off' or
2217
+ raise OptionParser::InvalidArgument, val
2218
+ #; [!dptgn] '--color' is same as '--color=on'.
2219
+ opts.color = val || 'on'
2220
+ }
2221
+ parser.on('-C', '--create') { opts.create = true }
2222
+ parser.on('-G', '--generate[=styleoption]') {|val|
2223
+ val.nil? || val == 'unaryop' or
2224
+ raise OptionParser::InvalidArgument, val
2225
+ opts.generate = val || true
2226
+ }
2227
+ parser.on( '--faster') { opts.faster = true }
2228
+ return parser
2229
+ end
2230
+
2231
+ def help_message(command=nil)
2232
+ command ||= File.basename($0)
2233
+ return HELP_MESSAGE % {command: command}
2234
+ end
2235
+
2236
+ HELP_MESSAGE = <<'END'
2237
+ Usage: %{command} [<options>] [<file-or-directory>...]
2238
+ -h, --help : show help
2239
+ --version : print version
2240
+ -s <STYLE> : report style (verbose/simple/plain/quiet, or v/s/p/q)
2241
+ -F <PATTERN> : filter topic or spec with pattern (see below)
2242
+ --color[={on|off}] : enable/disable output coloring forcedly
2243
+ -C, --create : print test code skeleton
2244
+ -G, --generate : generate test code skeleton from ruby file
2245
+ --faster : make 'ok{}' faster (for very large project)
2246
+
2247
+ Filter examples:
2248
+ $ oktest -F topic=Hello # filter by topic
2249
+ $ oktest -F spec='*hello*' # filter by spec
2250
+ $ oktest -F tag=name # filter by tag name
2251
+ $ oktest -F tag!=name # negative filter by tag name
2252
+ $ oktest -F tag='{name1,name2}' # filter by multiple tag names
2253
+
2254
+ See https://github.com/kwatch/oktest/blob/ruby/ruby/README.md for details.
2255
+ END
2256
+
2257
+ def load_files(filenames)
2258
+ filenames.each do |fname|
2259
+ File.exist?(fname) or
2260
+ raise OptionParser::InvalidOption, "#{fname}: not found."
2261
+ end
2262
+ filenames.each do |fname|
2263
+ File.directory?(fname) ? load_dir(fname) : load(fname)
2264
+ end
2265
+ end
2266
+
2267
+ def load_dir(dir, pattern=/^(test_.*|.*_test)\.rb$/)
2268
+ Dir.glob("#{dir}/**/*").sort.each do |path|
2269
+ next unless File.file?(path)
2270
+ load(path) if File.basename(path) =~ pattern
2271
+ end
2272
+ end
2273
+
2274
+ def generate(filenames, styleoption)
2275
+ buf = []
2276
+ filenames.each do |fname|
2277
+ generator = TestGenerator.new(styleoption)
2278
+ File.open(fname) do |f|
2279
+ buf << generator.generate(f)
2280
+ end
2281
+ end
2282
+ return buf.join()
2283
+ end
2284
+
2285
+ SKELETON = <<'END'
2286
+ # coding: utf-8
2287
+
2288
+ ## see https://github.com/kwatch/oktest/blob/ruby/ruby/README.md for details.
2289
+ require 'oktest'
2290
+
2291
+ Oktest.scope do
2292
+
2293
+ fixture :alice do
2294
+ {name: "Alice"}
2295
+ end
2296
+ fixture :bob do
2297
+ {name: "Bob"}
2298
+ end
2299
+
2300
+ topic Class do
2301
+
2302
+ before do nil end
2303
+ after do nil end
2304
+ before_all do nil end
2305
+ after_all do nil end
2306
+
2307
+ topic '#method_name()' do
2308
+
2309
+ spec "1+1 should be 2." do
2310
+ ok {1+1} == 2
2311
+ end
2312
+
2313
+ spec "fixture injection examle." do
2314
+ |alice, bob|
2315
+ ok {alice[:name]} == "Alice"
2316
+ ok {bob[:name]} == "Bob"
2317
+ end
2318
+
2319
+ end
2320
+
2321
+ end
2322
+
2323
+ end
2324
+ END
2325
+
2326
+ end
2327
+
2328
+
2329
+ def self.main(argv=nil)
2330
+ status = MainApp.main(argv)
2331
+ exit(status)
2332
+ end
2333
+
2334
+ def self.on_exit() # :nodoc:
2335
+ Oktest.main() if self.auto_run?()
2336
+ end
2337
+
2338
+ def self.auto_run?() # :nodoc:
2339
+ #; [!7vm4d] returns false if error raised when loading test scripts.
2340
+ #; [!oae85] returns true if exit() called.
2341
+ exc = $!
2342
+ return false if exc && !exc.is_a?(SystemExit)
2343
+ #; [!rg5aw] returns false if Oktest.scope() never been called.
2344
+ return false unless THE_GLOBAL_SCOPE.has_child?
2345
+ #; [!0j3ek] returns true if Config.auto_run is enabled.
2346
+ return Config.auto_run
2347
+ end
2348
+
2349
+
2350
+ end
2351
+
2352
+
2353
+ at_exit { Oktest.on_exit() }
2354
+
2355
+
2356
+ if __FILE__ == $0
2357
+ $LOADED_FEATURES << File.expand_path(__FILE__) # avoid loading oktest.rb twice
2358
+ Oktest.main() # run test scripts
2359
+ end