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.
- checksums.yaml +7 -0
- data/MIT-LICENSE +21 -0
- data/README.md +1856 -0
- data/Rakefile.rb +61 -0
- data/benchmark/Rakefile.rb +230 -0
- data/benchmark/run_all.rb +6 -0
- data/bin/oktest +3 -0
- data/lib/oktest.rb +2359 -0
- data/oktest.gemspec +45 -0
- data/test/assertion_test.rb +817 -0
- data/test/filter_test.rb +379 -0
- data/test/fixture_test.rb +205 -0
- data/test/generator_test.rb +123 -0
- data/test/helper_test.rb +542 -0
- data/test/initialize.rb +14 -0
- data/test/mainapp_test.rb +625 -0
- data/test/misc_test.rb +80 -0
- data/test/node_test.rb +669 -0
- data/test/reporter_test.rb +547 -0
- data/test/run_all.rb +13 -0
- data/test/runner_test.rb +544 -0
- data/test/tc.rb +115 -0
- data/test/util_test.rb +258 -0
- data/test/visitor_test.rb +292 -0
- metadata +105 -0
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
|
data/bin/oktest
ADDED
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
|