async_sinatra 0.2.1 → 0.2.2

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.
@@ -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