oktest 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/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