websocket-rack-noodles 0.4.0

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.
@@ -0,0 +1,68 @@
1
+ module Rack
2
+ module WebSocket
3
+ module Handler
4
+ class Base
5
+
6
+ autoload :Connection, "#{ROOT_PATH}/websocket/handler/base/connection"
7
+
8
+ def on_open
9
+ set_env_instance_variable
10
+ @parent.on_open(@env)
11
+ end # Fired when a client is connected.
12
+
13
+ def on_message(msg)
14
+ set_env_instance_variable
15
+ @parent.on_message(@env, msg)
16
+ end # Fired when a message from a client is received.
17
+
18
+ def on_close
19
+ set_env_instance_variable
20
+ @parent.on_close(@env)
21
+ end # Fired when a client is disconnected.
22
+
23
+ def on_error(error)
24
+ set_env_instance_variable
25
+ @parent.on_error(@env, error)
26
+ end # Fired when error occurs.
27
+
28
+ # Set application as parent and forward options
29
+ def initialize(parent, options = {})
30
+ @parent = parent
31
+ @options = options[:backend] || {}
32
+ end
33
+
34
+ def set_env_instance_variable
35
+ @parent.instance_variable_set("@env", @env)
36
+ end
37
+
38
+ # Implemented in subclass
39
+ def call(env)
40
+ raise 'Not implemented'
41
+ end
42
+
43
+ # Implemented in subclass
44
+ def send_data(data)
45
+ raise 'Not implemented'
46
+ end
47
+
48
+ # Implemented in subclass
49
+ def close_websocket
50
+ raise 'Not implemented'
51
+ end
52
+
53
+ protected
54
+
55
+ # Standard async response
56
+ def async_response
57
+ [-1, {}, []]
58
+ end
59
+
60
+ # Standard 400 response
61
+ def failure_response
62
+ [ 400, {'Content-Type' => 'text/plain'}, [ 'Bad request' ] ]
63
+ end
64
+
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,15 @@
1
+ module Rack
2
+ module WebSocket
3
+ module Handler
4
+ class Stub < Base
5
+
6
+ # Always close socket
7
+ def call(env)
8
+ raise 'Unknown handler!'
9
+ close_websocket
10
+ end
11
+
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,58 @@
1
+ require 'thin'
2
+
3
+ module Rack
4
+ module WebSocket
5
+ module Handler
6
+ class Thin < Base
7
+
8
+ # Build request from Rack env
9
+ def call(env)
10
+ @env = env
11
+ socket = env['async.connection']
12
+ request = request_from_env(env)
13
+ @connection = Connection.new(self, socket, :debug => @options[:debug])
14
+ @connection.dispatch(request) ? async_response : failure_response
15
+ end
16
+
17
+ # Forward send_data to server
18
+ def send_data(data)
19
+ if @connection
20
+ @connection.send data
21
+ else
22
+ raise WebSocketError, "WebSocket not opened"
23
+ end
24
+ end
25
+
26
+ # Forward close_websocket to server
27
+ def close_websocket
28
+ if @connection
29
+ @connection.close_websocket
30
+ else
31
+ raise WebSocketError, "WebSocket not opened"
32
+ end
33
+ end
34
+
35
+ private
36
+
37
+ # Parse Rack env to em-websocket-compatible format
38
+ # this probably should be moved to Base in future
39
+ def request_from_env(env)
40
+ request = {}
41
+ request['path'] = env['REQUEST_URI'].to_s
42
+ request['method'] = env['REQUEST_METHOD']
43
+ request['query'] = env['QUERY_STRING'].to_s
44
+ request['Body'] = env['rack.input'].read
45
+
46
+ env.each do |key, value|
47
+ if key.match(/HTTP_(.+)/)
48
+ request[$1.downcase.gsub('_','-')] ||= value
49
+ end
50
+ end
51
+
52
+ request
53
+ end
54
+
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,21 @@
1
+ module Rack
2
+ module WebSocket
3
+ module Handler
4
+
5
+ autoload :Base, "#{ROOT_PATH}/websocket/handler/base"
6
+ autoload :Stub, "#{ROOT_PATH}/websocket/handler/stub"
7
+ autoload :Thin, "#{ROOT_PATH}/websocket/handler/thin"
8
+
9
+ # Detect current server using software Rack string
10
+ def self.detect(env)
11
+ server_software = env['SERVER_SOFTWARE']
12
+ if server_software.match(/\Athin /i)
13
+ Thin
14
+ else
15
+ Stub
16
+ end
17
+ end
18
+
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,5 @@
1
+ module Rack
2
+ module WebSocket
3
+ VERSION = "0.4.0"
4
+ end
5
+ end
@@ -0,0 +1,14 @@
1
+ require 'rack'
2
+ require 'em-websocket'
3
+
4
+ module Rack
5
+ module WebSocket
6
+ ROOT_PATH = ::File.expand_path(::File.dirname(__FILE__))
7
+
8
+ autoload :Application, "#{ROOT_PATH}/websocket/application"
9
+ autoload :Extensions, "#{ROOT_PATH}/websocket/extensions"
10
+ autoload :Handler, "#{ROOT_PATH}/websocket/handler"
11
+ end
12
+ end
13
+
14
+ Rack::WebSocket::Extensions.apply!
@@ -0,0 +1,18 @@
1
+ require 'rubygems'
2
+ require 'rspec'
3
+
4
+ require 'rack/websocket'
5
+ Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f}
6
+
7
+ RSpec.configure do |config|
8
+ config.mock_with :mocha
9
+ end
10
+
11
+ class TestApp < Rack::WebSocket::Application
12
+ end
13
+
14
+ TEST_PORT = 8081
15
+
16
+ def new_server_connection
17
+ TCPSocket.new('localhost', TEST_PORT)
18
+ end
@@ -0,0 +1,44 @@
1
+ require 'timeout'
2
+ shared_examples_for 'all drafts' do
3
+ it "should accept incoming connection" do
4
+ conn = new_server_connection
5
+ conn.write(handshake_request)
6
+ timeout(1) { conn.read(handshake_response.length).should eql(handshake_response) }
7
+ end
8
+ it "should call 'on_open' on new connection" do
9
+ TestApp.any_instance.expects(:on_open)
10
+ conn = new_server_connection
11
+ conn.write(handshake_request)
12
+ end
13
+ it "should call 'on_open' on new connection with proper env" do
14
+ TestApp.any_instance.expects(:on_open).once.with { |env| env.class == Hash && !env.keys.empty? }
15
+ conn = new_server_connection
16
+ conn.write(handshake_request)
17
+ end
18
+ it "should call 'on_close' on connection close" do
19
+ TestApp.any_instance.expects(:on_close)
20
+ conn = new_server_connection
21
+ conn.write(handshake_request)
22
+ conn.close
23
+ end
24
+ it "should call 'on_close' on connection close with proper env" do
25
+ TestApp.any_instance.expects(:on_close).once.with { |env| env.class == Hash && !env.keys.empty? }
26
+ conn = new_server_connection
27
+ conn.write(handshake_request)
28
+ conn.close
29
+ end
30
+ it "should call 'on_message' on connection sending data" do
31
+ TestApp.any_instance.expects(:on_message)
32
+ conn = new_server_connection
33
+ conn.write(handshake_request)
34
+ timeout(1) { conn.read(handshake_response.length) }
35
+ conn.write(message)
36
+ end
37
+ it "should call 'on_message' on connection sending data with proper env and message" do
38
+ TestApp.any_instance.expects(:on_message).once.with { |env, message| env.class == Hash && !env.keys.empty? && message == 'Hello' }
39
+ conn = new_server_connection
40
+ conn.write(handshake_request)
41
+ timeout(1) { conn.read(handshake_response.length) }
42
+ conn.write(message)
43
+ end
44
+ end
@@ -0,0 +1,80 @@
1
+ shared_examples_for 'all handlers' do
2
+ it "should return flash policy file" do
3
+ conn = new_server_connection
4
+ conn.write(flash_policy_request)
5
+ conn.read(flash_policy_response.length).should eql(flash_policy_response)
6
+ end
7
+
8
+ context 'for draft75' do
9
+ let(:handshake_request) { spec75_handshake_request }
10
+ let(:handshake_response) { spec75_handshake_response }
11
+ let(:message) { spec75_message }
12
+
13
+ it_should_behave_like 'all drafts'
14
+ end
15
+
16
+ # Also draft00
17
+ context 'for draft76' do
18
+ let(:handshake_request) { spec76_handshake_request }
19
+ let(:handshake_response) { spec76_handshake_response }
20
+ let(:message) { spec76_message }
21
+
22
+ it_should_behave_like 'all drafts'
23
+ end
24
+
25
+ # Drafts 01, 02 and 03 are pretty the same so one test for all
26
+ context 'for draft03' do
27
+ let(:handshake_request) { spec03_handshake_request }
28
+ let(:handshake_response) { spec03_handshake_response }
29
+ let(:message) { spec03_message }
30
+
31
+ it_should_behave_like 'all drafts'
32
+ end
33
+
34
+ context 'for draft05' do
35
+ let(:handshake_request) { spec05_handshake_request }
36
+ let(:handshake_response) { spec05_handshake_response }
37
+ let(:message) { spec05_message }
38
+
39
+ it_should_behave_like 'all drafts'
40
+ end
41
+
42
+ context 'for draft06' do
43
+ let(:handshake_request) { spec06_handshake_request }
44
+ let(:handshake_response) { spec06_handshake_response }
45
+ let(:message) { spec06_message }
46
+
47
+ it_should_behave_like 'all drafts'
48
+ end
49
+
50
+ context 'for draft07' do
51
+ let(:handshake_request) { spec07_handshake_request }
52
+ let(:handshake_response) { spec07_handshake_response }
53
+ let(:message) { spec07_unmasked_message }
54
+ let(:masked_message) { spec07_masked_message }
55
+
56
+ it_should_behave_like 'all drafts'
57
+ it_should_behave_like 'draft with masked messages'
58
+ end
59
+
60
+ context 'for draft08' do
61
+ let(:handshake_request) { spec08_handshake_request }
62
+ let(:handshake_response) { spec08_handshake_response }
63
+ let(:message) { spec08_unmasked_message }
64
+ let(:masked_message) { spec08_masked_message }
65
+
66
+ it_should_behave_like 'all drafts'
67
+ it_should_behave_like 'draft with masked messages'
68
+ end
69
+
70
+ # Drafts 09, 10, 11, 12 and 13 are pretty the same so one test for all
71
+ context 'for draft13' do
72
+ let(:handshake_request) { spec13_handshake_request }
73
+ let(:handshake_response) { spec13_handshake_response }
74
+ let(:message) { spec13_unmasked_message }
75
+ let(:masked_message) { spec13_masked_message }
76
+
77
+ it_should_behave_like 'all drafts'
78
+ it_should_behave_like 'draft with masked messages'
79
+ end
80
+ end
@@ -0,0 +1,9 @@
1
+ shared_examples_for 'draft with masked messages' do
2
+ it "should call 'on_message' on connection sending masked data with proper env and message" do
3
+ TestApp.any_instance.expects(:on_message).once.with { |env, message| env.class == Hash && !env.keys.empty? && message == 'Hello' }
4
+ conn = new_server_connection
5
+ conn.write(handshake_request)
6
+ timeout(1) { conn.read(handshake_response.length) }
7
+ conn.write(masked_message)
8
+ end
9
+ end
@@ -0,0 +1,249 @@
1
+ def flash_policy_request
2
+ "<policy-file-request/>\000"
3
+ end
4
+
5
+ def flash_policy_response
6
+ '<?xml version="1.0"?><cross-domain-policy><allow-access-from domain="*" to-ports="*"/></cross-domain-policy>'
7
+ end
8
+
9
+ def spec75_handshake_request
10
+ <<-EOF
11
+ GET /demo HTTP/1.1\r
12
+ Upgrade: WebSocket\r
13
+ Connection: Upgrade\r
14
+ Host: localhost:#{TEST_PORT}\r
15
+ Origin: http://localhost:#{TEST_PORT}\r
16
+ \r
17
+ EOF
18
+ end
19
+
20
+ def spec75_handshake_response
21
+ <<-EOF
22
+ HTTP/1.1 101 Web Socket Protocol Handshake\r
23
+ Upgrade: WebSocket\r
24
+ Connection: Upgrade\r
25
+ WebSocket-Origin: http://localhost:#{TEST_PORT}\r
26
+ WebSocket-Location: ws://localhost:#{TEST_PORT}/demo\r
27
+ \r
28
+ EOF
29
+ end
30
+
31
+ def spec75_message
32
+ "\x00Hello\xff"
33
+ end
34
+
35
+ def spec76_handshake_request
36
+ request = <<-EOF
37
+ GET /demo HTTP/1.1\r
38
+ Host: localhost:#{TEST_PORT}\r
39
+ Connection: Upgrade\r
40
+ Sec-WebSocket-Key2: 12998 5 Y3 1 .P00\r
41
+ Sec-WebSocket-Protocol: sample\r
42
+ Upgrade: WebSocket\r
43
+ Sec-WebSocket-Key1: 4 @1 46546xW%0l 1 5\r
44
+ Origin: http://localhost:#{TEST_PORT}\r
45
+ \r
46
+ ^n:ds[4U
47
+ EOF
48
+ request.rstrip
49
+ end
50
+
51
+ def spec76_handshake_response
52
+ response = <<-EOF
53
+ HTTP/1.1 101 WebSocket Protocol Handshake\r
54
+ Upgrade: WebSocket\r
55
+ Connection: Upgrade\r
56
+ Sec-WebSocket-Location: ws://localhost:#{TEST_PORT}/demo\r
57
+ Sec-WebSocket-Origin: http://localhost:#{TEST_PORT}\r
58
+ Sec-WebSocket-Protocol: sample\r
59
+ \r
60
+ 8jKS'y:G*Co,Wxa-
61
+ EOF
62
+ response.rstrip
63
+ end
64
+
65
+ def spec76_message
66
+ "\x00Hello\xff"
67
+ end
68
+
69
+ def spec03_handshake_request
70
+ request = <<-EOF
71
+ GET /demo HTTP/1.1\r
72
+ Host: localhost:#{TEST_PORT}\r
73
+ Connection: Upgrade\r
74
+ Sec-WebSocket-Key2: 12998 5 Y3 1 .P00\r
75
+ Sec-WebSocket-Protocol: sample\r
76
+ Upgrade: WebSocket\r
77
+ Sec-WebSocket-Key1: 4 @1 46546xW%0l 1 5\r
78
+ Origin: http://localhost:#{TEST_PORT}\r
79
+ Sec-WebSocket-Draft: 3\r
80
+ \r
81
+ ^n:ds[4U
82
+ EOF
83
+ request.rstrip
84
+ end
85
+
86
+ def spec03_handshake_response
87
+ response = <<-EOF
88
+ HTTP/1.1 101 WebSocket Protocol Handshake\r
89
+ Upgrade: WebSocket\r
90
+ Connection: Upgrade\r
91
+ Sec-WebSocket-Location: ws://localhost:#{TEST_PORT}/demo\r
92
+ Sec-WebSocket-Origin: http://localhost:#{TEST_PORT}\r
93
+ Sec-WebSocket-Protocol: sample\r
94
+ \r
95
+ 8jKS'y:G*Co,Wxa-
96
+ EOF
97
+ response.rstrip
98
+ end
99
+
100
+ def spec03_message
101
+ "\x04\x05Hello"
102
+ end
103
+
104
+ def spec05_handshake_request
105
+ <<-EOF
106
+ GET /chat HTTP/1.1\r
107
+ Host: localhost:#{TEST_PORT}\r
108
+ Upgrade: websocket\r
109
+ Connection: Upgrade\r
110
+ Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r
111
+ Sec-WebSocket-Origin: http://localhost:#{TEST_PORT}\r
112
+ Sec-WebSocket-Protocol: chat, superchat\r
113
+ Sec-WebSocket-Version: 5\r
114
+ \r
115
+ EOF
116
+ end
117
+
118
+ def spec05_handshake_response
119
+ <<-EOF
120
+ HTTP/1.1 101 Switching Protocols\r
121
+ Upgrade: websocket\r
122
+ Connection: Upgrade\r
123
+ Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r
124
+ EOF
125
+ end
126
+
127
+ def spec05_message
128
+ "\x00\x00\x01\x00\x84\x05Ielln"
129
+ end
130
+
131
+ def spec06_handshake_request
132
+ <<-EOF
133
+ GET /chat HTTP/1.1\r
134
+ Host: localhost:#{TEST_PORT}\r
135
+ Upgrade: websocket\r
136
+ Connection: Upgrade\r
137
+ Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r
138
+ Sec-WebSocket-Origin: http://localhost:#{TEST_PORT}\r
139
+ Sec-WebSocket-Protocol: chat, superchat\r
140
+ Sec-WebSocket-Version: 6\r
141
+ \r
142
+ EOF
143
+ end
144
+
145
+ def spec06_handshake_response
146
+ <<-EOF
147
+ HTTP/1.1 101 Switching Protocols\r
148
+ Upgrade: websocket\r
149
+ Connection: Upgrade\r
150
+ Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r
151
+ EOF
152
+ end
153
+
154
+ def spec06_message
155
+ "\x00\x00\x01\x00\x84\x05Ielln"
156
+ end
157
+
158
+ def spec07_handshake_request
159
+ <<-EOF
160
+ GET /chat HTTP/1.1\r
161
+ Host: localhost:#{TEST_PORT}\r
162
+ Upgrade: websocket\r
163
+ Connection: Upgrade\r
164
+ Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r
165
+ Sec-WebSocket-Origin: http://localhost:#{TEST_PORT}\r
166
+ Sec-WebSocket-Protocol: chat, superchat\r
167
+ Sec-WebSocket-Version: 7\r
168
+ \r
169
+ EOF
170
+ end
171
+
172
+ def spec07_handshake_response
173
+ <<-EOF
174
+ HTTP/1.1 101 Switching Protocols\r
175
+ Upgrade: websocket\r
176
+ Connection: Upgrade\r
177
+ Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r
178
+ EOF
179
+ end
180
+
181
+ def spec07_unmasked_message
182
+ "\x81\x05\x48\x65\x6c\x6c\x6f"
183
+ end
184
+
185
+ def spec07_masked_message
186
+ "\x81\x85\x37\xfa\x21\x3d\x7f\x9f\x4d\x51\x58"
187
+ end
188
+
189
+ def spec08_handshake_request
190
+ <<-EOF
191
+ GET /chat HTTP/1.1\r
192
+ Host: localhost:#{TEST_PORT}\r
193
+ Upgrade: websocket\r
194
+ Connection: Upgrade\r
195
+ Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r
196
+ Sec-WebSocket-Origin: http://localhost:#{TEST_PORT}\r
197
+ Sec-WebSocket-Protocol: chat, superchat\r
198
+ Sec-WebSocket-Version: 8\r
199
+ \r
200
+ EOF
201
+ end
202
+
203
+ def spec08_handshake_response
204
+ <<-EOF
205
+ HTTP/1.1 101 Switching Protocols\r
206
+ Upgrade: websocket\r
207
+ Connection: Upgrade\r
208
+ Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r
209
+ EOF
210
+ end
211
+
212
+ def spec08_unmasked_message
213
+ "\x81\x05\x48\x65\x6c\x6c\x6f"
214
+ end
215
+
216
+ def spec08_masked_message
217
+ "\x81\x85\x37\xfa\x21\x3d\x7f\x9f\x4d\x51\x58"
218
+ end
219
+
220
+ def spec13_handshake_request
221
+ <<-EOF
222
+ GET /chat HTTP/1.1\r
223
+ Host: localhost:#{TEST_PORT}\r
224
+ Upgrade: websocket\r
225
+ Connection: Upgrade\r
226
+ Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r
227
+ Origin: http://localhost:#{TEST_PORT}\r
228
+ Sec-WebSocket-Protocol: chat, superchat\r
229
+ Sec-WebSocket-Version: 13\r
230
+ \r
231
+ EOF
232
+ end
233
+
234
+ def spec13_handshake_response
235
+ <<-EOF
236
+ HTTP/1.1 101 Switching Protocols\r
237
+ Upgrade: websocket\r
238
+ Connection: Upgrade\r
239
+ Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r
240
+ EOF
241
+ end
242
+
243
+ def spec13_unmasked_message
244
+ "\x81\x05\x48\x65\x6c\x6c\x6f"
245
+ end
246
+
247
+ def spec13_masked_message
248
+ "\x81\x85\x37\xfa\x21\x3d\x7f\x9f\x4d\x51\x58"
249
+ end
data/spec/thin_spec.rb ADDED
@@ -0,0 +1,48 @@
1
+ require 'thin'
2
+ require 'spec_helper'
3
+
4
+ describe 'Thin handler' do
5
+ let(:app) { TestApp.new }
6
+
7
+ before(:all) { silent_thin }
8
+ before { start_thin_server(app) }
9
+ after { stop_thin_server }
10
+
11
+ it "should include extensions" do
12
+ ::Thin::Connection.include?(::Rack::WebSocket::Extensions::Common).should be_true
13
+ ::Thin::Connection.include?(::Rack::WebSocket::Extensions::Thin::Connection).should be_true
14
+ end
15
+
16
+ it_should_behave_like 'all handlers'
17
+ end
18
+
19
+ def start_thin_server(app, options = {})
20
+ @server = Thin::Server.new('0.0.0.0', TEST_PORT, options, app)
21
+ @server.ssl = options[:ssl]
22
+ # @server.threaded = options[:threaded]
23
+ # @server.timeout = 3
24
+
25
+ @thread = Thread.new { @server.start }
26
+ sleep 1 until @server.running?
27
+ end
28
+
29
+ def stop_thin_server
30
+ sleep 0.1
31
+ @server.stop!
32
+ sleep 0.1
33
+ @thread.kill
34
+ sleep 0.1
35
+ raise "Reactor still running, wtf?" if EventMachine.reactor_running?
36
+ end
37
+
38
+ def silent_thin
39
+ ::Thin::Logging.silent = true
40
+ if EM::VERSION < "1.0.0"
41
+ begin
42
+ old_verbose, $VERBOSE = $VERBOSE, nil
43
+ ::Thin::Server.const_set 'DEFAULT_TIMEOUT', 0
44
+ ensure
45
+ $VERBOSE = old_verbose
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,24 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "rack/websocket/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "websocket-rack-noodles"
7
+ s.version = Rack::WebSocket::VERSION
8
+ s.platform = Gem::Platform::RUBY
9
+ s.authors = ["Bernard Potocki"]
10
+ s.email = ["bernard.potocki@imanel.org"]
11
+ s.homepage = "http://github.com/DamirSvrtan/websocket-rack"
12
+ s.summary = %q{Rack-based WebSocket server for Noodles Web Framework}
13
+ s.description = %q{Rack-based WebSocket server for Noodles Web Framework. An fork from http://github.com/imanel/websocket-rack}
14
+
15
+ s.add_dependency 'rack'
16
+ s.add_dependency 'em-websocket', '~> 0.3.8'
17
+ s.add_dependency 'eventmachine', '~> 1.0.0'
18
+ s.add_dependency 'thin' # Temporary until we support more servers
19
+
20
+ s.files = `git ls-files`.split("\n")
21
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
22
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
23
+ s.require_paths = ["lib"]
24
+ end