angelo 0.1.2 → 0.1.3

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 667a3b95c269095d00f41d03cadfe1e3cc40f404
4
- data.tar.gz: 9143b9fee78b9b06591516f147a29ce147169447
3
+ metadata.gz: eb21b9dcec8959a4845e5e6063ae0051b71589c4
4
+ data.tar.gz: 88e2ca4bd79df106deaa35c7509d22b237e5918a
5
5
  SHA512:
6
- metadata.gz: 85107d56bb0446a5611a53eec5fd91b737f5e27362345bd9d87370d7183639da3254aa3101709297f9c7fcd90d15563e597de418255fcf14090a5bbb7b91c369
7
- data.tar.gz: 8e248165128d31c5f4e891cf74d7508d982fc9798def4fea2fa022e6bf24fbcf77ce87b5d6b12c7e3e1eeef071bf24f123064386433ebbf03d1199b5c285fa3e
6
+ metadata.gz: a468e2ad3a12a7b3a7fab94816d13141f25675651d456651470c287b1773ef9e766b3a3eba7e45cb6310159caeef32b222026cc2853bded2c7b78ce6d6ccd4ad
7
+ data.tar.gz: f844a3b8410c35392490058ee699202b7a29f4b9df923635ec8500485789832db69f470db62f39e4001842cde0581b178207ab9d4bd80bff6ca5175bf85c3014
data/CHANGELOG.md CHANGED
@@ -1,6 +1,12 @@
1
1
  changelog
2
2
  =========
3
3
 
4
+ ### 0.1.3 24 mar 2014
5
+
6
+ * better testing of websockets
7
+ * slightly better handling of socket errors
8
+ * static file content type fix
9
+
4
10
  ### 0.1.2 5 mar 2014 burnout, denial
5
11
 
6
12
  * basic static file support (i.e. /public files)
data/Gemfile CHANGED
@@ -3,6 +3,7 @@ source 'https://rubygems.org'
3
3
  gem 'reel'
4
4
  gem 'tilt'
5
5
  gem 'mime-types'
6
+ gem 'websocket-driver'
6
7
 
7
8
  platform :rbx do
8
9
  gem 'rubysl-cgi'
@@ -25,15 +25,23 @@ module Angelo
25
25
  end
26
26
  rescue IOError => ioe
27
27
  warn "#{ioe.class} - #{ioe.message}"
28
- @websocket.close
29
- @base.websockets.delete @websocket
28
+ close_websocket
30
29
  rescue => e
31
30
  error e.message
32
31
  ::STDERR.puts e.backtrace
33
- @connection.close
32
+ begin
33
+ @connection.close
34
+ rescue Reel::Connection::StateError => rcse
35
+ close_websocket
36
+ end
34
37
  end
35
38
  end
36
39
 
40
+ def close_websocket
41
+ @websocket.close
42
+ @base.websockets.delete @websocket
43
+ end
44
+
37
45
  end
38
46
 
39
47
  end
@@ -1,3 +1,5 @@
1
+ require 'websocket/driver'
2
+
1
3
  module Angelo
2
4
  module RSpec
3
5
 
@@ -15,6 +17,7 @@ module Angelo
15
17
  app.class_eval { content_type :html } # reset
16
18
  app.class_eval &block
17
19
  @server = Angelo::Server.new app
20
+ $reactor = Reactor.new unless $reactor.alive?
18
21
  end
19
22
 
20
23
  after do
@@ -42,31 +45,19 @@ module Angelo
42
45
  end
43
46
  end
44
47
 
45
- def socket path, params = {}, &block
46
- begin
47
- client = TCPSocket.new DEFAULT_ADDR, DEFAULT_PORT
48
-
49
- params = params.keys.reduce([]) {|a,k|
50
- a << CGI.escape(k) + '=' + CGI.escape(params[k])
51
- a
52
- }.join('&')
53
-
54
- url = WS_URL % [DEFAULT_ADDR, DEFAULT_PORT] + path + "?#{params}"
55
-
56
- handshake = WebSocket::ClientHandshake.new :get, url, {
57
- "Host" => "www.example.com",
58
- "Upgrade" => "websocket",
59
- "Connection" => "Upgrade",
60
- "Sec-WebSocket-Key" => "dGhlIHNhbXBsZSBub25jZQ==",
61
- "Origin" => "http://example.com",
62
- "Sec-WebSocket-Protocol" => "chat, superchat",
63
- "Sec-WebSocket-Version" => "13"
64
- }
65
-
66
- client << handshake.to_data
67
- yield WebsocketHelper.new client
68
- ensure
69
- client.close
48
+ def socket path, params = {}
49
+ params = params.keys.reduce([]) {|a,k|
50
+ a << CGI.escape(k) + '=' + CGI.escape(params[k])
51
+ a
52
+ }.join('&')
53
+
54
+ path += "?#{params}" unless params.empty?
55
+ wsh = WebsocketHelper.new DEFAULT_ADDR, DEFAULT_PORT, path
56
+ if block_given?
57
+ yield wsh
58
+ wsh.close
59
+ else
60
+ return wsh
70
61
  end
71
62
  end
72
63
 
@@ -85,23 +76,55 @@ module Angelo
85
76
  end
86
77
 
87
78
  class WebsocketHelper
79
+ include Celluloid::Logger
80
+
81
+ extend Forwardable
82
+ def_delegator :@socket, :write
83
+ def_delegators :@driver, :binary, :close, :text
84
+
85
+ attr_reader :driver, :socket
86
+ attr_writer :addr, :port, :path, :on_close, :on_message, :on_open
87
+
88
+ def initialize addr, port, path
89
+ @addr, @port, @path = addr, port, path
90
+ end
91
+
92
+ def init
93
+ init_socket
94
+ init_driver
95
+ end
88
96
 
89
- def initialize client
90
- @client = client
91
- @client.readpartial 4096 # ditch response handshake
97
+ def init_socket
98
+ ip = @addr
99
+ ip = Socket.getaddrinfo(@addr, 'http')[0][3] unless @addr =~ /\d+\.\d+\.\d+\.\d+/
100
+ @socket = Celluloid::IO::TCPSocket.new ip, @port
92
101
  end
93
102
 
94
- def parser
95
- @parser ||= WebSocket::Parser.new
103
+ def init_driver
104
+ @driver = WebSocket::Driver.client self
105
+
106
+ @driver.on :open do |e|
107
+ @on_open.call(e) if Proc === @on_open
108
+ end
109
+
110
+ @driver.on :message do |e|
111
+ @on_message.call(e) if Proc === @on_message
112
+ end
113
+
114
+ @driver.on :close do |e|
115
+ @on_close.call(e) if Proc === @on_close
116
+ end
96
117
  end
97
118
 
98
- def send msg
99
- @client << WebSocket::Message.new(msg).to_data
119
+ def url
120
+ WS_URL % [@addr, @port] + @path
100
121
  end
101
122
 
102
- def recv
103
- parser.append @client.readpartial(4096) until msg = parser.next_message
104
- msg
123
+ def go
124
+ @driver.start
125
+ while msg = @socket.readpartial(4096)
126
+ @driver.parse msg
127
+ end
105
128
  end
106
129
 
107
130
  end
data/lib/angelo/server.rb CHANGED
@@ -1,4 +1,5 @@
1
1
  require 'openssl'
2
+ require 'mime-types'
2
3
 
3
4
  module Angelo
4
5
 
@@ -1,3 +1,3 @@
1
1
  module Angelo
2
- VERSION = '0.1.2'
2
+ VERSION = '0.1.3'
3
3
  end
@@ -2,6 +2,7 @@ if RUBY_VERSION =~ /^2\./
2
2
 
3
3
  require_relative '../spec_helper'
4
4
  require 'angelo/mustermann'
5
+ require 'angelo/tilt/erb'
5
6
 
6
7
  describe Angelo::Mustermann do
7
8
 
@@ -54,6 +54,19 @@ describe Angelo::Server do
54
54
  expect(last_response.status).to be(304)
55
55
  end
56
56
 
57
+ it 'serves proper content-types' do
58
+ { 'test.js' => 'application/javascript',
59
+ 'test.html' => 'text/html',
60
+ 'test.css' => 'text/css',
61
+ 'what.png' => 'image/png' }.each do |k,v|
62
+
63
+ get "/#{k}"
64
+ expect(last_response.status).to be(200)
65
+ expect(last_response.headers['Content-Type']).to eq(v)
66
+
67
+ end
68
+ end
69
+
57
70
  end
58
71
 
59
72
  end
@@ -1,14 +1,32 @@
1
1
  require_relative '../spec_helper'
2
2
 
3
- CK = 'ANGELO_CONCURRENCY' # concurrency key
4
- DC = 5 # default concurrency
5
-
6
3
  describe Angelo::WebsocketResponder do
7
4
 
8
- let(:concurrency){ ENV.key?(CK) ? ENV[CK].to_i : DC }
5
+ def socket_wait_for path, latch, expectation, key = :swf, &block
6
+ Reactor.testers[key] = Array.new(CONCURRENCY).map do
7
+ wsh = socket path
8
+ wsh.on_message = ->(e) {
9
+ expectation[e] if Proc === expectation
10
+ latch.count_down
11
+ }
12
+ wsh.init
13
+ wsh
14
+ end
15
+ action = (key.to_s + '_go').to_sym
16
+ Reactor.define_action action do |n|
17
+ every(0.01){ terminate if Reactor.stop? }
18
+ Reactor.testers[key][n].go
19
+ end
20
+ Reactor.unstop!
21
+ CONCURRENCY.times {|n| $reactor.async.__send__(action, n)}
22
+
23
+ sleep 0.01 * CONCURRENCY
24
+ yield
9
25
 
10
- def socket_wait_for path, &block
11
- Array.new(concurrency).map {|n| Thread.new {socket path, &block}}
26
+ Reactor.testers[key].map &:close
27
+ Reactor.stop!
28
+ Reactor.testers.delete key
29
+ Reactor.remove_action action
12
30
  end
13
31
 
14
32
  describe 'basics' do
@@ -22,25 +40,66 @@ describe Angelo::WebsocketResponder do
22
40
  end
23
41
 
24
42
  it 'responds on websockets properly' do
25
- socket '/' do |client|
26
- 5.times {|n|
27
- client.send "hi there #{n}"
28
- client.recv.should eq "hi there #{n}"
43
+ socket '/' do |wsh|
44
+ latch = CountDownLatch.new 500
45
+
46
+ wsh.on_message = ->(e) {
47
+ expect(e.data).to match(/hi there \d/)
48
+ latch.count_down
29
49
  }
50
+
51
+ wsh.init
52
+ Reactor.testers[:tester] = wsh
53
+ Reactor.define_action :go do
54
+ every(0.01){ terminate if Reactor.stop? }
55
+ Reactor.testers[:tester].go
56
+ end
57
+ Reactor.unstop!
58
+ $reactor.async.go
59
+
60
+ 500.times {|n| wsh.text "hi there #{n}"}
61
+ latch.wait
62
+
63
+ Reactor.stop!
64
+ Reactor.testers.delete :tester
65
+ Reactor.remove_action :go
30
66
  end
31
67
  end
32
68
 
33
69
  it 'responds on multiple websockets properly' do
34
- 5.times do
35
- Thread.new do
36
- socket '/' do |client|
37
- 5.times {|n|
38
- client.send "hi there #{n}"
39
- client.recv.should eq "hi there #{n}"
40
- }
41
- end
42
- end
70
+ latch = CountDownLatch.new CONCURRENCY * 500
71
+
72
+ Reactor.testers[:wshs] = Array.new(CONCURRENCY).map do
73
+ wsh = socket '/'
74
+ wsh.on_message = ->(e) {
75
+ expect(e.data).to match(/hi there \d/)
76
+ latch.count_down
77
+ }
78
+ wsh.init
79
+ wsh
80
+ end
81
+
82
+ Reactor.define_action :go do |n|
83
+ every(0.01){ terminate if Reactor.stop? }
84
+ Reactor.testers[:wshs][n].go
85
+ end
86
+ Reactor.unstop!
87
+ CONCURRENCY.times {|n| $reactor.async.go n}
88
+
89
+ sleep 0.01 * CONCURRENCY
90
+
91
+ ActorPool.define_action :go do |n|
92
+ 500.times {|x| Reactor.testers[:wshs][n].text "hi there #{x}"}
43
93
  end
94
+ CONCURRENCY.times {|n| $pool.async.go n}
95
+ latch.wait
96
+
97
+ Reactor.testers[:wshs].map &:close
98
+ Reactor.stop!
99
+ Reactor.testers.delete :wshs
100
+ Reactor.remove_action :go
101
+
102
+ ActorPool.remove_action :go
44
103
  end
45
104
 
46
105
  end
@@ -67,15 +126,16 @@ describe Angelo::WebsocketResponder do
67
126
 
68
127
  it 'works with http requests' do
69
128
 
70
- ts = socket_wait_for '/concur' do |client|
71
- Angelo::HTTPABLE.each do |m|
72
- client.recv.should eq "from http #{m}"
73
- end
74
- end
129
+ latch = CountDownLatch.new CONCURRENCY * Angelo::HTTPABLE.length
130
+
131
+ expectation = ->(e){
132
+ expect(e.data).to match(/from http (#{Angelo::HTTPABLE.map(&:to_s).join('|')})/)
133
+ }
75
134
 
76
- sleep 0.1
77
- Angelo::HTTPABLE.each {|m| __send__ m, '/concur', foo: 'http'}
78
- ts.each &:join
135
+ socket_wait_for '/concur', latch, expectation do
136
+ Angelo::HTTPABLE.each {|m| __send__ m, '/concur', foo: 'http'}
137
+ latch.wait
138
+ end
79
139
 
80
140
  end
81
141
 
@@ -83,7 +143,7 @@ describe Angelo::WebsocketResponder do
83
143
 
84
144
  describe 'helper contexts' do
85
145
  let(:obj){ {'foo' => 'bar'} }
86
- let(:wait_for_block){ ->(client){ JSON.parse(client.recv).should eq obj}}
146
+ let(:wait_for_block){ ->(e){ expect(JSON.parse(e.data)).to eq(obj) }}
87
147
 
88
148
  define_app do
89
149
 
@@ -126,38 +186,80 @@ describe Angelo::WebsocketResponder do
126
186
  end
127
187
 
128
188
  it 'handles single context' do
129
- ts = socket_wait_for '/', &wait_for_block
130
- sleep 0.1
131
- post '/', obj
132
- ts.each &:join
189
+ latch = CountDownLatch.new CONCURRENCY
190
+ socket_wait_for '/', latch, wait_for_block do
191
+ post '/', obj
192
+ latch.wait
193
+ end
133
194
  end
134
195
 
135
196
  it 'handles multiple contexts' do
136
- ts = socket_wait_for '/', &wait_for_block
137
- one_ts = socket_wait_for '/one', &wait_for_block
138
- other_ts = socket_wait_for '/other', &wait_for_block
139
197
 
140
- sleep 0.1
141
- post '/one', obj
198
+ latch = CountDownLatch.new CONCURRENCY
199
+
200
+ Reactor.testers[:hmc] = Array.new(CONCURRENCY).map do
201
+ wsh = socket '/'
202
+ wsh.on_message = ->(e) {
203
+ wait_for_block[e]
204
+ latch.count_down
205
+ }
206
+ wsh.init
207
+ wsh
208
+ end
142
209
 
143
- ts.each {|t| t.should be_alive}
144
- one_ts.each &:join
145
- other_ts.each {|t| t.should be_alive}
210
+ one_latch = CountDownLatch.new CONCURRENCY
146
211
 
147
- sleep 0.1
148
- post '/other', obj
212
+ Reactor.testers[:hmc_one] = Array.new(CONCURRENCY).map do
213
+ wsh = socket '/one'
214
+ wsh.on_message = ->(e) {
215
+ wait_for_block[e]
216
+ one_latch.count_down
217
+ }
218
+ wsh.init
219
+ wsh
220
+ end
149
221
 
150
- ts.each {|t| t.should be_alive}
151
- one_ts.each {|t| t.should_not be_alive}
152
- other_ts.each &:join
222
+ other_latch = CountDownLatch.new CONCURRENCY
153
223
 
154
- sleep 0.1
224
+ Reactor.testers[:hmc_other] = Array.new(CONCURRENCY).map do
225
+ wsh = socket '/other'
226
+ wsh.on_message = ->(e) {
227
+ wait_for_block[e]
228
+ other_latch.count_down
229
+ }
230
+ wsh.init
231
+ wsh
232
+ end
233
+
234
+ Reactor.define_action :go do |k, n|
235
+ Reactor.testers[k][n].go
236
+ end
237
+ Reactor.unstop!
238
+ CONCURRENCY.times do |n|
239
+ [:hmc, :hmc_one, :hmc_other].each do |k|
240
+ $reactor.async.go k, n
241
+ end
242
+ end
243
+
244
+ sleep 0.01 * CONCURRENCY
245
+
246
+ post '/one', obj
247
+ one_latch.wait
155
248
  post '/', obj
249
+ latch.wait
250
+ post '/other', obj
251
+ other_latch.wait
156
252
 
157
- ts.each &:join
158
- one_ts.each {|t| t.should_not be_alive}
159
- other_ts.each {|t| t.should_not be_alive}
253
+ [:hmc, :hmc_one, :hmc_other].each do |k|
254
+ Reactor.testers[k].map &:close
255
+ end
256
+ Reactor.stop!
257
+ [:hmc, :hmc_one, :hmc_other].each do |k|
258
+ Reactor.testers.delete k
259
+ end
260
+ Reactor.remove_action :go
160
261
  end
161
262
 
162
263
  end
264
+
163
265
  end
data/spec/spec_helper.rb CHANGED
@@ -8,3 +8,77 @@ Celluloid.logger.level = ::Logger::ERROR
8
8
  include Angelo::RSpec::Helpers
9
9
 
10
10
  TEST_APP_ROOT = File.expand_path '../test_app_root', __FILE__
11
+
12
+ CK = 'ANGELO_CONCURRENCY' # concurrency key
13
+ DC = 5 # default concurrency
14
+ CONCURRENCY = ENV.key?(CK) ? ENV[CK].to_i : DC
15
+
16
+ # https://gist.github.com/tkareine/739662
17
+ #
18
+ class CountDownLatch
19
+ attr_reader :count
20
+
21
+ def initialize(to)
22
+ @count = to.to_i
23
+ raise ArgumentError, "cannot count down from negative integer" unless @count >= 0
24
+ @lock = Mutex.new
25
+ @condition = ConditionVariable.new
26
+ end
27
+
28
+ def count_down
29
+ @lock.synchronize do
30
+ @count -= 1 if @count > 0
31
+ @condition.broadcast if @count == 0
32
+ end
33
+ end
34
+
35
+ def wait
36
+ @lock.synchronize do
37
+ @condition.wait(@lock) while @count > 0
38
+ end
39
+ end
40
+
41
+ end
42
+
43
+ module Cellper
44
+
45
+ @@stop = false
46
+ @@testers = {}
47
+
48
+ def define_action sym, &block
49
+ define_method sym, &block
50
+ end
51
+
52
+ def remove_action sym
53
+ remove_method sym
54
+ end
55
+
56
+ def unstop!
57
+ @@stop = false
58
+ end
59
+
60
+ def stop!
61
+ @@stop = true
62
+ end
63
+
64
+ def stop?
65
+ @@stop
66
+ end
67
+
68
+ def testers; @@testers; end
69
+
70
+ end
71
+
72
+ class Reactor
73
+ include Celluloid::IO
74
+ extend Cellper
75
+ end
76
+
77
+ $reactor = Reactor.new
78
+
79
+ class ActorPool
80
+ include Celluloid
81
+ extend Cellper
82
+ end
83
+
84
+ $pool = ActorPool.pool size: CONCURRENCY
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: angelo
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 0.1.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kenichi Nakamura
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-03-06 00:00:00.000000000 Z
11
+ date: 2014-03-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: reel