async_sinatra 0.2.1 → 0.2.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,4 +1,4 @@
1
- = async_sinatra (for Thin)
1
+ = async_sinatra
2
2
  by James Tucker
3
3
  http://ra66i.org
4
4
  http://github.com/raggi/async_sinatra
@@ -73,7 +73,10 @@ module Sinatra #:nodoc:
73
73
  # request.
74
74
  def body(*args)
75
75
  if @async_running
76
- block_given? ? async_handle_exception { super yield } : super
76
+ block_given? ? async_handle_exception { super(yield) } : super
77
+ if response.body.respond_to?(:call)
78
+ response.body = Array(async_handle_exception {response.body.call})
79
+ end
77
80
  request.env['async.callback'][
78
81
  [response.status, response.headers, response.body]
79
82
  ]
@@ -133,6 +136,14 @@ module Sinatra #:nodoc:
133
136
  invoke { error_block! response.status }
134
137
  body response.body
135
138
  end
139
+
140
+ # The given block will be executed if the user closes the connection
141
+ # prematurely (before we've sent a response). This is good for
142
+ # deregistering callbacks that would otherwise send the body (for
143
+ # example channel subscriptions).
144
+ def on_close(&blk)
145
+ env['async.close'].callback(&blk)
146
+ end
136
147
  end
137
148
 
138
149
  def self.registered(app) #:nodoc:
@@ -8,10 +8,31 @@ class Rack::MockResponse
8
8
  end
9
9
 
10
10
  class Sinatra::Async::Test
11
+
11
12
  class AsyncSession < Rack::MockSession
13
+ class AsyncCloser
14
+ def initialize
15
+ @callbacks, @errbacks = [], []
16
+ end
17
+ def callback(&b)
18
+ @callbacks << b
19
+ end
20
+ def errback(&b)
21
+ @errbacks << b
22
+ end
23
+ def fail
24
+ @errbacks.each { |cb| cb.call }
25
+ @errbacks.clear
26
+ end
27
+ def succeed
28
+ @callbacks.each { |cb| cb.call }
29
+ @callbacks.clear
30
+ end
31
+ end
32
+
12
33
  def request(uri, env)
13
34
  env['async.callback'] = lambda { |r| s,h,b = *r; handle_last_response(uri, env, s,h,b) }
14
- env['async.close'] = lambda { raise 'close connection' } # XXX deal with this
35
+ env['async.close'] = AsyncCloser.new
15
36
  catch(:async) { super }
16
37
  @last_response ||= Rack::MockResponse.new(-1, {}, [], env["rack.errors"].flush)
17
38
  end
@@ -29,14 +50,14 @@ class Sinatra::Async::Test
29
50
 
30
51
  module Methods
31
52
  include Rack::Test::Methods
32
-
53
+
33
54
  %w(get put post delete head).each do |m|
34
55
  eval <<-RUBY, binding, __FILE__, __LINE__ + 1
35
- def a#{m}(*args)
36
- #{m}(*args)
37
- assert_async
38
- async_continue
39
- end
56
+ def a#{m}(*args)
57
+ #{m}(*args)
58
+ assert_async
59
+ async_continue
60
+ end
40
61
  RUBY
41
62
  end
42
63
 
@@ -48,20 +69,40 @@ class Sinatra::Async::Test
48
69
  assert last_response.async?
49
70
  end
50
71
 
72
+ # Simulate a user closing the connection before a response is sent.
73
+ def async_close
74
+ raise ArgumentError, 'please make a request first' unless last_request
75
+ current_session.last_request.env['async.close'].succeed
76
+ end
77
+
78
+ # Executes the pending asynchronous blocks, required for the
79
+ # aget/apost/etc blocks to run.
51
80
  def async_continue
52
81
  while b = app.options.async_schedules.shift
53
82
  b.call
54
83
  end
55
84
  end
56
85
 
86
+ # Crank the eventmachine loop until a response is made, or timeout after a
87
+ # particular period, by default 10s. If the timeout is nil, no timeout
88
+ # will occur.
57
89
  def em_async_continue(timeout = 10)
58
90
  timed = false
59
91
  EM.run do
60
92
  async_continue
61
- EM.tick_loop { EM.stop unless last_response.async? }
62
- EM.add_timer(timeout) { timed = true; EM.stop }
93
+ em_hard_loop { EM.stop unless last_response.async? }
94
+ EM.add_timer(timeout) { timed = true; EM.stop } if timeout
63
95
  end
64
96
  assert !timed, "asynchronous timeout after #{timeout} seconds"
65
97
  end
98
+
99
+ # Uses EM.tick_loop or a periodic timer to check for changes
100
+ def em_hard_loop
101
+ if EM.respond_to?(:tick_loop)
102
+ EM.tick_loop { yield }
103
+ else
104
+ EM.periodic_timer(0.0001) { yield }
105
+ end
106
+ end
66
107
  end
67
108
  end
@@ -12,6 +12,12 @@ class TestSinatraAsync < Test::Unit::TestCase
12
12
  set :environment, :test
13
13
  register Sinatra::Async
14
14
 
15
+ # Hack for storing some global data accessible in tests (normally you
16
+ # shouldn't need to do this!)
17
+ def self.singletons
18
+ @singletons ||= []
19
+ end
20
+
15
21
  error 401 do
16
22
  '401'
17
23
  end
@@ -47,6 +53,22 @@ class TestSinatraAsync < Test::Unit::TestCase
47
53
  aget '/a401' do
48
54
  ahalt 401
49
55
  end
56
+
57
+ aget '/async_close' do
58
+ # don't call body here, the 'user' is going to 'disconnect' before we do
59
+ env['async.close'].callback { self.class.singletons << 'async_closed' }
60
+ end
61
+
62
+ aget '/on_close' do
63
+ # sugared version of the above
64
+ on_close do
65
+ self.class.singletons << 'async_close_cleaned_up'
66
+ end
67
+ end
68
+
69
+ aget '/redirect' do
70
+ redirect '/'
71
+ end
50
72
  end
51
73
 
52
74
  def app
@@ -113,4 +135,23 @@ class TestSinatraAsync < Test::Unit::TestCase
113
135
  assert_equal 401, last_response.status
114
136
  assert_equal '401', last_response.body
115
137
  end
138
+
139
+ def test_async_close
140
+ aget '/async_close'
141
+ async_close
142
+ assert_equal 'async_closed', TestApp.singletons.shift
143
+ end
144
+
145
+ def test_on_close
146
+ aget '/on_close'
147
+ async_close
148
+ assert_equal 'async_close_cleaned_up', TestApp.singletons.shift
149
+ end
150
+
151
+ def test_redirect
152
+ aget '/redirect'
153
+ assert last_response.redirect?
154
+ assert_equal 302, last_response.status
155
+ assert_equal '/', last_response.location
156
+ end
116
157
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: async_sinatra
3
3
  version: !ruby/object:Gem::Version
4
- hash: 21
4
+ hash: 19
5
5
  prerelease: false
6
6
  segments:
7
7
  - 0
8
8
  - 2
9
- - 1
10
- version: 0.2.1
9
+ - 2
10
+ version: 0.2.2
11
11
  platform: ruby
12
12
  authors:
13
13
  - James Tucker
@@ -15,7 +15,7 @@ autorequire:
15
15
  bindir: bin
16
16
  cert_chain: []
17
17
 
18
- date: 2010-06-20 00:00:00 -03:00
18
+ date: 2010-09-07 00:00:00 -03:00
19
19
  default_executable:
20
20
  dependencies:
21
21
  - !ruby/object:Gem::Dependency
@@ -26,12 +26,11 @@ dependencies:
26
26
  requirements:
27
27
  - - ">="
28
28
  - !ruby/object:Gem::Version
29
- hash: 57
29
+ hash: 15
30
30
  segments:
31
- - 0
32
- - 9
33
31
  - 1
34
- version: 0.9.1
32
+ - 0
33
+ version: "1.0"
35
34
  type: :runtime
36
35
  version_requirements: *id001
37
36
  - !ruby/object:Gem::Dependency
@@ -80,7 +79,6 @@ files:
80
79
  - examples/basic.ru
81
80
  - lib/sinatra/async.rb
82
81
  - lib/sinatra/async/test.rb
83
- - test/borked_test_crohr.rb
84
82
  - test/test_async.rb
85
83
  has_rdoc: true
86
84
  homepage: http://libraggi.rubyforge.org/async_sinatra
@@ -1,161 +0,0 @@
1
- require 'eventmachine'
2
- require "sinatra/async/test"
3
-
4
- require 'spec/autorun'
5
- require 'spec/interop/test'
6
-
7
- require 'em-http'
8
-
9
- Spec::Runner.configure { |c| }
10
-
11
- def server(base=Sinatra::Base, &block)
12
- s = Sinatra.new(base, &block).new
13
- s.options.set :environment, :test
14
- s
15
- end
16
-
17
- def app
18
- @app
19
- end
20
-
21
- include Sinatra::Async::Test::Methods
22
-
23
- describe "Asynchronous routes" do
24
- it "should still work as usual" do
25
- @app = server do
26
- register Sinatra::Async
27
- disable :raise_errors, :show_exceptions
28
-
29
- aget '/' do
30
- body "hello async"
31
- end
32
- end
33
- aget '/'
34
- last_response.status.should_equal 200
35
- last_response.body.should == "hello async"
36
- end
37
-
38
- it "should correctly deal with raised exceptions" do
39
- @app = server do
40
- register Sinatra::Async
41
- disable :raise_errors, :show_exceptions
42
- aget '/' do
43
- raise "boom"
44
- body "hello async"
45
- end
46
- error Exception do
47
- e = request.env['sinatra.error']
48
- "problem: #{e.class.name} #{e.message}"
49
- end
50
- end
51
- aget '/'
52
- last_response.status.should == 500
53
- last_response.body.should == "problem: RuntimeError boom"
54
- end
55
-
56
- it "should correctly deal with halts" do
57
- @app = server do
58
- register Sinatra::Async
59
- disable :raise_errors, :show_exceptions
60
- aget '/' do
61
- halt 406, "Format not supported"
62
- body "never called"
63
- end
64
- end
65
-
66
- aget '/'
67
- last_response.status.should == 406
68
- last_response.body.should == "Format not supported"
69
- end
70
-
71
- it "should correctly deal with halts and pass it to the defined error blocks if any" do
72
- @app = server do
73
- register Sinatra::Async
74
- disable :raise_errors, :show_exceptions
75
- aget '/' do
76
- halt 406, "Format not supported"
77
- body "never called"
78
- end
79
- error 406 do
80
- response['Content-Type'] = "text/plain"
81
- "problem: #{response.body.to_s}"
82
- end
83
- end
84
- aget '/'
85
- last_response.status.should == 406
86
- last_response.headers['Content-Type'].should == "text/plain"
87
- last_response.body.should == "problem: Format not supported"
88
- end
89
-
90
- describe "using EM libraries inside route block" do
91
- it "should still work as usual" do
92
- @app = server do
93
- register Sinatra::Async
94
- disable :raise_errors, :show_exceptions
95
- aget '/' do
96
- url = "http://ruby.activeventure.com/programmingruby/book/tut_exceptions.html"
97
- http = EM::HttpRequest.new(url).get
98
- http.callback {
99
- status http.response_header.status
100
- body "ok"
101
- }
102
- http.errback {
103
- body "nok"
104
- }
105
- end
106
- end
107
- EM.run { EM.add_timer(1) { EM.stop }; aget '/' }
108
- last_response.status.should == 200
109
- last_response.body.should == "ok"
110
- end
111
-
112
- it "should correctly deal with exceptions raised from within EM callbacks" do
113
- @app = server do
114
- register Sinatra::Async
115
- disable :raise_errors, :show_exceptions
116
- aget '/' do
117
- url = "http://doesnotexist.local/whatever"
118
- http = EM::HttpRequest.new(url).get
119
- http.callback {
120
- status http.response_header.status
121
- body "ok"
122
- }
123
- http.errback {
124
- raise "boom"
125
- }
126
- end
127
- error Exception do
128
- e = request.env['sinatra.error']
129
- "#{e.class.name}: #{e.message}"
130
- end
131
- end
132
- EM.run { EM.add_timer(1) { EM.stop }; aget '/' }
133
- last_response.status.should == 500
134
- last_response.body.should == "RuntimeError: boom"
135
- end
136
-
137
- it "should correctly deal with halts thrown from within EM callbacks" do
138
- @app = server do
139
- register Sinatra::Async
140
- disable :raise_errors, :show_exceptions
141
- aget '/' do
142
- url = "http://doesnotexist.local/whatever"
143
- http = EM::HttpRequest.new(url).get
144
- http.callback {
145
- status http.response_header.status
146
- body "ok"
147
- }
148
- http.errback {
149
- halt 503, "error: #{http.errors.inspect}"
150
- }
151
- end
152
- error 503 do
153
- "503: #{response.body.to_s}"
154
- end
155
- end
156
- EM.run { EM.add_timer(1) { EM.stop }; aget '/' }
157
- last_response.status.should == 503
158
- last_response.body.should == "503: error: \"unable to resolve server address\""
159
- end
160
- end
161
- end