angelo 0.1.2 → 0.1.3

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