sinatra 1.3.0.e → 1.3.0.f
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of sinatra might be problematic. Click here for more details.
- data/.travis.yml +16 -0
- data/CHANGES +77 -18
- data/Gemfile +43 -26
- data/README.de.rdoc +264 -88
- data/README.es.rdoc +241 -80
- data/README.fr.rdoc +206 -81
- data/README.hu.rdoc +2 -2
- data/README.jp.rdoc +2 -2
- data/README.pt-br.rdoc +2 -2
- data/README.pt-pt.rdoc +2 -2
- data/README.rdoc +169 -23
- data/README.ru.rdoc +373 -433
- data/README.zh.rdoc +5 -5
- data/Rakefile +8 -2
- data/lib/sinatra/base.rb +191 -46
- data/lib/sinatra/main.rb +1 -1
- data/lib/sinatra/showexceptions.rb +2 -2
- data/lib/sinatra/version.rb +1 -1
- data/sinatra.gemspec +7 -5
- data/test/contest.rb +62 -28
- data/test/filter_test.rb +2 -2
- data/test/helpers_test.rb +185 -12
- data/test/mapped_error_test.rb +25 -6
- data/test/nokogiri_test.rb +5 -6
- data/test/response_test.rb +10 -1
- data/test/result_test.rb +2 -2
- data/test/routing_test.rb +13 -0
- data/test/server_test.rb +3 -2
- data/test/settings_test.rb +98 -11
- data/test/slim_test.rb +15 -25
- data/test/static_test.rb +3 -3
- data/test/streaming_test.rb +100 -0
- metadata +82 -35
data/test/mapped_error_test.rb
CHANGED
@@ -6,6 +6,10 @@ end
|
|
6
6
|
class FooNotFound < Sinatra::NotFound
|
7
7
|
end
|
8
8
|
|
9
|
+
class FooSpecialError < RuntimeError
|
10
|
+
def code; 501 end
|
11
|
+
end
|
12
|
+
|
9
13
|
class MappedErrorTest < Test::Unit::TestCase
|
10
14
|
def test_default
|
11
15
|
assert true
|
@@ -25,6 +29,15 @@ class MappedErrorTest < Test::Unit::TestCase
|
|
25
29
|
assert_equal 'Foo!', body
|
26
30
|
end
|
27
31
|
|
32
|
+
it 'passes the exception object to the error handler' do
|
33
|
+
mock_app do
|
34
|
+
set :raise_errors, false
|
35
|
+
error(FooError) { |e| assert_equal(FooError, e.class) }
|
36
|
+
get('/') { raise FooError }
|
37
|
+
end
|
38
|
+
get('/')
|
39
|
+
end
|
40
|
+
|
28
41
|
it 'uses the Exception handler if no matching handler found' do
|
29
42
|
mock_app {
|
30
43
|
set :raise_errors, false
|
@@ -108,12 +121,7 @@ class MappedErrorTest < Test::Unit::TestCase
|
|
108
121
|
end
|
109
122
|
|
110
123
|
it "never raises Sinatra::NotFound beyond the application" do
|
111
|
-
mock_app {
|
112
|
-
set :raise_errors, true
|
113
|
-
get '/' do
|
114
|
-
raise Sinatra::NotFound
|
115
|
-
end
|
116
|
-
}
|
124
|
+
mock_app(Sinatra::Application) { get('/') { raise Sinatra::NotFound }}
|
117
125
|
assert_nothing_raised { get '/' }
|
118
126
|
assert_equal 404, status
|
119
127
|
end
|
@@ -173,6 +181,17 @@ class MappedErrorTest < Test::Unit::TestCase
|
|
173
181
|
get '/'
|
174
182
|
assert_equal 'subclass', body
|
175
183
|
end
|
184
|
+
|
185
|
+
it 'honors Exception#code if present' do
|
186
|
+
mock_app do
|
187
|
+
set :raise_errors, false
|
188
|
+
error(501) { 'Foo!' }
|
189
|
+
get('/') { raise FooSpecialError }
|
190
|
+
end
|
191
|
+
get '/'
|
192
|
+
assert_equal 501, status
|
193
|
+
assert_equal 'Foo!', body
|
194
|
+
end
|
176
195
|
end
|
177
196
|
|
178
197
|
describe 'Custom Error Pages' do
|
data/test/nokogiri_test.rb
CHANGED
@@ -15,7 +15,7 @@ class NokogiriTest < Test::Unit::TestCase
|
|
15
15
|
it 'renders inline Nokogiri strings' do
|
16
16
|
nokogiri_app { nokogiri 'xml' }
|
17
17
|
assert ok?
|
18
|
-
assert_body %
|
18
|
+
assert_body %(<?xml version="1.0"?>\n)
|
19
19
|
end
|
20
20
|
|
21
21
|
it 'renders inline blocks' do
|
@@ -26,7 +26,7 @@ class NokogiriTest < Test::Unit::TestCase
|
|
26
26
|
end
|
27
27
|
end
|
28
28
|
assert ok?
|
29
|
-
assert_body
|
29
|
+
assert_body %(<?xml version="1.0"?>\n<couple>Frank & Mary</couple>\n)
|
30
30
|
end
|
31
31
|
|
32
32
|
it 'renders .nokogiri files in views path' do
|
@@ -35,7 +35,7 @@ class NokogiriTest < Test::Unit::TestCase
|
|
35
35
|
nokogiri :hello
|
36
36
|
end
|
37
37
|
assert ok?
|
38
|
-
assert_body
|
38
|
+
assert_body "<?xml version=\"1.0\"?>\n<exclaim>You're my boy, Blue!</exclaim>\n"
|
39
39
|
end
|
40
40
|
|
41
41
|
it "renders with inline layouts" do
|
@@ -46,17 +46,16 @@ class NokogiriTest < Test::Unit::TestCase
|
|
46
46
|
end
|
47
47
|
get '/'
|
48
48
|
assert ok?
|
49
|
-
assert_body
|
49
|
+
assert_body %(<?xml version="1.0"?>\n<layout>\n <em>Hello World</em>\n</layout>\n)
|
50
50
|
end
|
51
51
|
|
52
52
|
it "renders with file layouts" do
|
53
53
|
next if Tilt::VERSION <= "1.1"
|
54
54
|
nokogiri_app do
|
55
|
-
@name = "Blue"
|
56
55
|
nokogiri %(xml.em 'Hello World'), :layout => :layout2
|
57
56
|
end
|
58
57
|
assert ok?
|
59
|
-
assert_body
|
58
|
+
assert_body %(<?xml version="1.0"?>\n<layout>\n <em>Hello World</em>\n</layout>\n)
|
60
59
|
end
|
61
60
|
|
62
61
|
it "raises error if template not found" do
|
data/test/response_test.rb
CHANGED
@@ -37,7 +37,16 @@ class ResponseTest < Test::Unit::TestCase
|
|
37
37
|
@response.body = ['Hello', 'World!', '✈']
|
38
38
|
status, headers, body = @response.finish
|
39
39
|
assert_equal '14', headers['Content-Length']
|
40
|
-
assert_equal @response.body, body
|
40
|
+
assert_equal @response.body, body
|
41
|
+
end
|
42
|
+
|
43
|
+
it 'does not call #to_ary or #inject on the body' do
|
44
|
+
object = Object.new
|
45
|
+
def object.inject(*) fail 'called' end
|
46
|
+
def object.to_ary(*) fail 'called' end
|
47
|
+
def object.each(*) end
|
48
|
+
@response.body = object
|
49
|
+
assert @response.finish
|
41
50
|
end
|
42
51
|
|
43
52
|
it 'does not nest a Sinatra::Response' do
|
data/test/result_test.rb
CHANGED
@@ -54,12 +54,12 @@ class ResultTest < Test::Unit::TestCase
|
|
54
54
|
it "sets status, headers, and body when result is a Rack response tuple" do
|
55
55
|
mock_app {
|
56
56
|
get '/' do
|
57
|
-
[
|
57
|
+
[203, {'Content-Type' => 'foo/bar'}, 'Hello World']
|
58
58
|
end
|
59
59
|
}
|
60
60
|
|
61
61
|
get '/'
|
62
|
-
assert_equal
|
62
|
+
assert_equal 203, status
|
63
63
|
assert_equal 'foo/bar', response['Content-Type']
|
64
64
|
assert_equal 'Hello World', body
|
65
65
|
end
|
data/test/routing_test.rb
CHANGED
@@ -1080,4 +1080,17 @@ class RoutingTest < Test::Unit::TestCase
|
|
1080
1080
|
assert ok?
|
1081
1081
|
assert_body 'hello'
|
1082
1082
|
end
|
1083
|
+
|
1084
|
+
it 'returns the route signature' do
|
1085
|
+
signature = list = nil
|
1086
|
+
|
1087
|
+
mock_app do
|
1088
|
+
signature = post('/') { }
|
1089
|
+
list = routes['POST']
|
1090
|
+
end
|
1091
|
+
|
1092
|
+
assert_equal Array, signature.class
|
1093
|
+
assert_equal 4, signature.length
|
1094
|
+
assert list.include?(signature)
|
1095
|
+
end
|
1083
1096
|
end
|
data/test/server_test.rb
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
require File.expand_path('../helper', __FILE__)
|
2
|
+
require 'stringio'
|
2
3
|
|
3
4
|
module Rack::Handler
|
4
5
|
class Mock
|
@@ -25,11 +26,11 @@ class ServerTest < Test::Unit::TestCase
|
|
25
26
|
set :bind, 'foo.local'
|
26
27
|
set :port, 9001
|
27
28
|
}
|
28
|
-
$
|
29
|
+
$stderr = StringIO.new
|
29
30
|
end
|
30
31
|
|
31
32
|
def teardown
|
32
|
-
$
|
33
|
+
$stderr = STDERR
|
33
34
|
end
|
34
35
|
|
35
36
|
it "locates the appropriate Rack handler and calls ::run" do
|
data/test/settings_test.rb
CHANGED
@@ -306,6 +306,29 @@ class SettingsTest < Test::Unit::TestCase
|
|
306
306
|
get '/'
|
307
307
|
assert body.include?("RuntimeError") && body.include?("settings_test.rb")
|
308
308
|
end
|
309
|
+
|
310
|
+
it 'does not dump 404 errors' do
|
311
|
+
klass = Sinatra.new(Sinatra::Application)
|
312
|
+
|
313
|
+
mock_app(klass) {
|
314
|
+
enable :dump_errors
|
315
|
+
disable :raise_errors
|
316
|
+
|
317
|
+
error do
|
318
|
+
error = @env['rack.errors'].instance_variable_get(:@error)
|
319
|
+
error.rewind
|
320
|
+
|
321
|
+
error.read
|
322
|
+
end
|
323
|
+
|
324
|
+
get '/' do
|
325
|
+
raise Sinatra::NotFound
|
326
|
+
end
|
327
|
+
}
|
328
|
+
|
329
|
+
get '/'
|
330
|
+
assert !body.include?("NotFound") && !body.include?("settings_test.rb")
|
331
|
+
end
|
309
332
|
end
|
310
333
|
|
311
334
|
describe 'sessions' do
|
@@ -336,13 +359,13 @@ class SettingsTest < Test::Unit::TestCase
|
|
336
359
|
assert ! @base.static?
|
337
360
|
end
|
338
361
|
|
339
|
-
it 'is enabled on Base when
|
362
|
+
it 'is enabled on Base when public_folder is set and exists' do
|
340
363
|
@base.set :environment, :development
|
341
|
-
@base.set :
|
364
|
+
@base.set :public_folder, File.dirname(__FILE__)
|
342
365
|
assert @base.static?
|
343
366
|
end
|
344
367
|
|
345
|
-
it 'is enabled on Base when root is set and root/
|
368
|
+
it 'is enabled on Base when root is set and root/public_folder exists' do
|
346
369
|
@base.set :environment, :development
|
347
370
|
@base.set :root, File.dirname(__FILE__)
|
348
371
|
assert @base.static?
|
@@ -352,17 +375,36 @@ class SettingsTest < Test::Unit::TestCase
|
|
352
375
|
assert ! @application.static?
|
353
376
|
end
|
354
377
|
|
355
|
-
it 'is enabled on Application when
|
378
|
+
it 'is enabled on Application when public_folder is set and exists' do
|
356
379
|
@application.set :environment, :development
|
357
|
-
@application.set :
|
380
|
+
@application.set :public_folder, File.dirname(__FILE__)
|
358
381
|
assert @application.static?
|
359
382
|
end
|
360
383
|
|
361
|
-
it 'is enabled on Application when root is set and root/
|
384
|
+
it 'is enabled on Application when root is set and root/public_folder exists' do
|
362
385
|
@application.set :environment, :development
|
363
386
|
@application.set :root, File.dirname(__FILE__)
|
364
387
|
assert @application.static?
|
365
388
|
end
|
389
|
+
|
390
|
+
it 'is possible to use Module#public' do
|
391
|
+
@base.send(:define_method, :foo) { }
|
392
|
+
@base.send(:private, :foo)
|
393
|
+
assert !@base.public_method_defined?(:foo)
|
394
|
+
@base.send(:public, :foo)
|
395
|
+
assert @base.public_method_defined?(:foo)
|
396
|
+
end
|
397
|
+
|
398
|
+
it 'is possible to use the keyword public in a sinatra app' do
|
399
|
+
app = Sinatra.new do
|
400
|
+
private
|
401
|
+
def priv; end
|
402
|
+
public
|
403
|
+
def pub; end
|
404
|
+
end
|
405
|
+
assert !app.public_method_defined?(:priv)
|
406
|
+
assert app.public_method_defined?(:pub)
|
407
|
+
end
|
366
408
|
end
|
367
409
|
|
368
410
|
describe 'bind' do
|
@@ -427,18 +469,18 @@ class SettingsTest < Test::Unit::TestCase
|
|
427
469
|
end
|
428
470
|
end
|
429
471
|
|
430
|
-
describe '
|
472
|
+
describe 'public_folder' do
|
431
473
|
it 'is nil if root is not set' do
|
432
|
-
assert @base.
|
433
|
-
assert @application.
|
474
|
+
assert @base.public_folder.nil?
|
475
|
+
assert @application.public_folder.nil?
|
434
476
|
end
|
435
477
|
|
436
478
|
it 'is set to root joined with public/' do
|
437
479
|
@base.root = File.dirname(__FILE__)
|
438
|
-
assert_equal File.dirname(__FILE__) + "/public", @base.
|
480
|
+
assert_equal File.dirname(__FILE__) + "/public", @base.public_folder
|
439
481
|
|
440
482
|
@application.root = File.dirname(__FILE__)
|
441
|
-
assert_equal File.dirname(__FILE__) + "/public", @application.
|
483
|
+
assert_equal File.dirname(__FILE__) + "/public", @application.public_folder
|
442
484
|
end
|
443
485
|
end
|
444
486
|
|
@@ -448,4 +490,49 @@ class SettingsTest < Test::Unit::TestCase
|
|
448
490
|
assert ! @application.lock?
|
449
491
|
end
|
450
492
|
end
|
493
|
+
|
494
|
+
describe 'protection' do
|
495
|
+
class MiddlewareTracker < Rack::Builder
|
496
|
+
def self.track
|
497
|
+
Rack.send :remove_const, :Builder
|
498
|
+
Rack.const_set :Builder, MiddlewareTracker
|
499
|
+
MiddlewareTracker.used.clear
|
500
|
+
yield
|
501
|
+
ensure
|
502
|
+
Rack.send :remove_const, :Builder
|
503
|
+
Rack.const_set :Builder, MiddlewareTracker.superclass
|
504
|
+
end
|
505
|
+
|
506
|
+
def self.used
|
507
|
+
@used ||= []
|
508
|
+
end
|
509
|
+
|
510
|
+
def use(middleware, *)
|
511
|
+
MiddlewareTracker.used << middleware
|
512
|
+
super
|
513
|
+
end
|
514
|
+
end
|
515
|
+
|
516
|
+
it 'sets up Rack::Protection' do
|
517
|
+
MiddlewareTracker.track do
|
518
|
+
Sinatra::Base.new
|
519
|
+
assert_include MiddlewareTracker.used, Rack::Protection
|
520
|
+
end
|
521
|
+
end
|
522
|
+
|
523
|
+
it 'sets up Rack::Protection::PathTraversal' do
|
524
|
+
MiddlewareTracker.track do
|
525
|
+
Sinatra::Base.new
|
526
|
+
assert_include MiddlewareTracker.used, Rack::Protection::PathTraversal
|
527
|
+
end
|
528
|
+
end
|
529
|
+
|
530
|
+
it 'does not set up Rack::Protection::PathTraversal when disabling it' do
|
531
|
+
MiddlewareTracker.track do
|
532
|
+
Sinatra.new { set :protection, :except => :path_traversal }.new
|
533
|
+
assert_include MiddlewareTracker.used, Rack::Protection
|
534
|
+
assert !MiddlewareTracker.used.include?(Rack::Protection::PathTraversal)
|
535
|
+
end
|
536
|
+
end
|
537
|
+
end
|
451
538
|
end
|
data/test/slim_test.rb
CHANGED
@@ -52,44 +52,34 @@ class SlimTest < Test::Unit::TestCase
|
|
52
52
|
HTML4_DOCTYPE = "<!DOCTYPE html PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\" \"http://www.w3.org/TR/html4/loose.dtd\">"
|
53
53
|
|
54
54
|
it "passes slim options to the slim engine" do
|
55
|
-
mock_app {
|
56
|
-
get '/' do
|
57
|
-
slim "! doctype html\nh1 Hello World", :format => :html4
|
58
|
-
end
|
59
|
-
}
|
55
|
+
mock_app { get('/') { slim "x foo='bar'", :attr_wrapper => "'" }}
|
60
56
|
get '/'
|
61
57
|
assert ok?
|
62
|
-
|
58
|
+
assert_body "<x foo='bar'></x>"
|
63
59
|
end
|
64
60
|
|
65
61
|
it "passes default slim options to the slim engine" do
|
66
|
-
mock_app
|
67
|
-
set :slim,
|
68
|
-
get
|
69
|
-
|
70
|
-
end
|
71
|
-
}
|
62
|
+
mock_app do
|
63
|
+
set :slim, :attr_wrapper => "'"
|
64
|
+
get('/') { slim "x foo='bar'" }
|
65
|
+
end
|
72
66
|
get '/'
|
73
67
|
assert ok?
|
74
|
-
|
68
|
+
assert_body "<x foo='bar'></x>"
|
75
69
|
end
|
76
70
|
|
77
71
|
it "merges the default slim options with the overrides and passes them to the slim engine" do
|
78
|
-
mock_app
|
79
|
-
set :slim,
|
80
|
-
get
|
81
|
-
|
82
|
-
|
83
|
-
get '/html5' do
|
84
|
-
slim "! doctype html\nh1.header Hello World", :format => :html5
|
85
|
-
end
|
86
|
-
}
|
72
|
+
mock_app do
|
73
|
+
set :slim, :attr_wrapper => "'"
|
74
|
+
get('/') { slim "x foo='bar'" }
|
75
|
+
get('/other') { slim "x foo='bar'", :attr_wrapper => '"' }
|
76
|
+
end
|
87
77
|
get '/'
|
88
78
|
assert ok?
|
89
|
-
|
90
|
-
get '/
|
79
|
+
assert_body "<x foo='bar'></x>"
|
80
|
+
get '/other'
|
91
81
|
assert ok?
|
92
|
-
|
82
|
+
assert_body '<x foo="bar"></x>'
|
93
83
|
end
|
94
84
|
end
|
95
85
|
|
data/test/static_test.rb
CHANGED
@@ -4,7 +4,7 @@ class StaticTest < Test::Unit::TestCase
|
|
4
4
|
setup do
|
5
5
|
mock_app {
|
6
6
|
set :static, true
|
7
|
-
set :
|
7
|
+
set :public_folder, File.dirname(__FILE__)
|
8
8
|
}
|
9
9
|
end
|
10
10
|
|
@@ -66,7 +66,7 @@ class StaticTest < Test::Unit::TestCase
|
|
66
66
|
end
|
67
67
|
|
68
68
|
it 'passes to the next handler when the public option is nil' do
|
69
|
-
@app.set :
|
69
|
+
@app.set :public_folder, nil
|
70
70
|
get "/#{File.basename(__FILE__)}"
|
71
71
|
assert not_found?
|
72
72
|
end
|
@@ -85,7 +85,7 @@ class StaticTest < Test::Unit::TestCase
|
|
85
85
|
it '404s when .. path traverses outside of public directory' do
|
86
86
|
mock_app {
|
87
87
|
set :static, true
|
88
|
-
set :
|
88
|
+
set :public_folder, File.dirname(__FILE__) + '/data'
|
89
89
|
}
|
90
90
|
get "/../#{File.basename(__FILE__)}"
|
91
91
|
assert not_found?
|
@@ -0,0 +1,100 @@
|
|
1
|
+
require File.expand_path('../helper', __FILE__)
|
2
|
+
|
3
|
+
class StreamingTest < Test::Unit::TestCase
|
4
|
+
Stream = Sinatra::Helpers::Stream
|
5
|
+
|
6
|
+
it 'returns the concatinated body' do
|
7
|
+
mock_app do
|
8
|
+
get '/' do
|
9
|
+
stream do |out|
|
10
|
+
out << "Hello" << " "
|
11
|
+
out << "World!"
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
get('/')
|
17
|
+
assert_body "Hello World!"
|
18
|
+
end
|
19
|
+
|
20
|
+
it 'always yields strings' do
|
21
|
+
stream = Stream.new { |out| out << :foo }
|
22
|
+
stream.each { |str| assert_equal 'foo', str }
|
23
|
+
end
|
24
|
+
|
25
|
+
it 'postpones body generation' do
|
26
|
+
step = 0
|
27
|
+
|
28
|
+
stream = Stream.new do |out|
|
29
|
+
10.times do
|
30
|
+
out << step
|
31
|
+
step += 1
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
stream.each do |s|
|
36
|
+
assert_equal s, step.to_s
|
37
|
+
step += 1
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
it 'calls the callback after it is done' do
|
42
|
+
step = 0
|
43
|
+
final = 0
|
44
|
+
stream = Stream.new { |o| 10.times { step += 1 }}
|
45
|
+
stream.callback { final = step }
|
46
|
+
stream.each { |str| }
|
47
|
+
assert_equal 10, final
|
48
|
+
end
|
49
|
+
|
50
|
+
it 'does not trigger the callback if close is set to :keep_open' do
|
51
|
+
step = 0
|
52
|
+
final = 0
|
53
|
+
stream = Stream.new(Stream, :keep_open) { |o| 10.times { step += 1 } }
|
54
|
+
stream.callback { final = step }
|
55
|
+
stream.each { |str| }
|
56
|
+
assert_equal 0, final
|
57
|
+
end
|
58
|
+
|
59
|
+
class MockScheduler
|
60
|
+
def initialize(*) @schedule, @defer = [], [] end
|
61
|
+
def schedule(&block) @schedule << block end
|
62
|
+
def defer(&block) @defer << block end
|
63
|
+
def schedule!(*) @schedule.pop.call until @schedule.empty? end
|
64
|
+
def defer!(*) @defer.pop.call until @defer.empty? end
|
65
|
+
end
|
66
|
+
|
67
|
+
it 'allows dropping in another scheduler' do
|
68
|
+
scheduler = MockScheduler.new
|
69
|
+
processing = sending = done = false
|
70
|
+
|
71
|
+
stream = Stream.new(scheduler) do |out|
|
72
|
+
processing = true
|
73
|
+
out << :foo
|
74
|
+
end
|
75
|
+
|
76
|
+
stream.each { sending = true}
|
77
|
+
stream.callback { done = true }
|
78
|
+
|
79
|
+
scheduler.schedule!
|
80
|
+
assert !processing
|
81
|
+
assert !sending
|
82
|
+
assert !done
|
83
|
+
|
84
|
+
scheduler.defer!
|
85
|
+
assert processing
|
86
|
+
assert !sending
|
87
|
+
assert !done
|
88
|
+
|
89
|
+
scheduler.schedule!
|
90
|
+
assert sending
|
91
|
+
assert done
|
92
|
+
end
|
93
|
+
|
94
|
+
it 'schedules exceptions to be raised on the main thread/event loop/...' do
|
95
|
+
scheduler = MockScheduler.new
|
96
|
+
Stream.new(scheduler) { fail 'should be caught' }.each { }
|
97
|
+
scheduler.defer!
|
98
|
+
assert_raise(RuntimeError) { scheduler.schedule! }
|
99
|
+
end
|
100
|
+
end
|