uma 0.1.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.
- checksums.yaml +7 -0
- data/.gitignore +56 -0
- data/CHANGELOG.md +3 -0
- data/Gemfile +13 -0
- data/LICENSE +21 -0
- data/README.md +18 -0
- data/Rakefile +22 -0
- data/TODO.md +43 -0
- data/bin/uma +10 -0
- data/lib/uma/app.rb +39 -0
- data/lib/uma/cli/error.rb +13 -0
- data/lib/uma/cli.rb +202 -0
- data/lib/uma/error.rb +7 -0
- data/lib/uma/http.rb +308 -0
- data/lib/uma/server.rb +158 -0
- data/lib/uma/version.rb +5 -0
- data/lib/uma.rb +3 -0
- data/test/apps/bad_syntax.ru +3 -0
- data/test/apps/config.ru +3 -0
- data/test/apps/roda1.ru +41 -0
- data/test/apps/simple.ru +3 -0
- data/test/helper.rb +107 -0
- data/test/run.rb +5 -0
- data/test/test_app.rb +26 -0
- data/test/test_cli.rb +242 -0
- data/test/test_http.rb +336 -0
- data/test/test_server.rb +344 -0
- data/uma.gemspec +28 -0
- metadata +116 -0
data/lib/uma/http.rb
ADDED
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Uma
|
|
4
|
+
module HTTP
|
|
5
|
+
class Error < StandardError; end
|
|
6
|
+
class ParseError < Error; end
|
|
7
|
+
class ResponseError < Error; end
|
|
8
|
+
|
|
9
|
+
class RackIO
|
|
10
|
+
def initialize(env, stream)
|
|
11
|
+
@env = env
|
|
12
|
+
@stream = stream
|
|
13
|
+
@machine = stream.machine
|
|
14
|
+
@fd = stream.fd
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def gets
|
|
18
|
+
@stream.get_line(nil, 0)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def read(len, buf = +'')
|
|
22
|
+
@stream.get_string(buf, -30)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def each(&)
|
|
26
|
+
if @env['HTTP_TRANSFER_ENCODING'] == 'chunked'
|
|
27
|
+
read_chunks(&)
|
|
28
|
+
elsif (v = @env['CONTENT_LENGTH'])
|
|
29
|
+
len = v.to_i
|
|
30
|
+
yield @stream.get_string(+'', len)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def read_chunks
|
|
35
|
+
buf = +''
|
|
36
|
+
while true
|
|
37
|
+
line = @stream.get_line(buf, 8)
|
|
38
|
+
len = line.to_i(16)
|
|
39
|
+
break if len == 0
|
|
40
|
+
|
|
41
|
+
chunk = @stream.get_string(nil, len)
|
|
42
|
+
yield chunk
|
|
43
|
+
@stream.skip(2)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def close
|
|
48
|
+
@machine.close(@fd)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def puts(str)
|
|
52
|
+
write("#{str}\n")
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def write(*bufs)
|
|
56
|
+
@machine.writev(@fd, *bufs)
|
|
57
|
+
end
|
|
58
|
+
alias_method :<<, :write
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
class ChunkedIO
|
|
62
|
+
def initialize(machine, fd, stream)
|
|
63
|
+
@machine = machine
|
|
64
|
+
@fd = fd
|
|
65
|
+
@stream = stream
|
|
66
|
+
@first_write = true
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def read(len, buf = +'')
|
|
70
|
+
stream.get_string(buf, len)
|
|
71
|
+
buf
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def write(*chunks)
|
|
75
|
+
bufs = []
|
|
76
|
+
chunks.each do
|
|
77
|
+
bufs << (@first_write ? "#{it.bytesize.to_s(16)}\r\n" : "\r\n#{it.bytesize.to_s(16)}\r\n")
|
|
78
|
+
bufs << it
|
|
79
|
+
@first_write = false
|
|
80
|
+
end
|
|
81
|
+
@machine.sendv(@fd, *bufs)
|
|
82
|
+
end
|
|
83
|
+
alias_method :<<, :write
|
|
84
|
+
|
|
85
|
+
def flush
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def close_read
|
|
89
|
+
@machine.shutdown(@fd, UM::SHUT_RD)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def close_write
|
|
93
|
+
@machine.shutdown(@fd, UM::SHUT_WR)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def close
|
|
97
|
+
return if @closed
|
|
98
|
+
|
|
99
|
+
@closed = true
|
|
100
|
+
chunked_ending = @first_write ? "0\r\n\r\n" : "\r\n0\r\n\r\n"
|
|
101
|
+
@machine.sendv(@fd, chunked_ending)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def closed?
|
|
105
|
+
@closed
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
extend self
|
|
110
|
+
|
|
111
|
+
def http_connection(machine, config, fd)
|
|
112
|
+
stream = UM::Stream.new(machine, fd)
|
|
113
|
+
while true
|
|
114
|
+
break if !process_request(machine, config, fd, stream)
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def process_request(machine, config, fd, stream)
|
|
119
|
+
env = get_request_env(config, stream)
|
|
120
|
+
return false if !env
|
|
121
|
+
|
|
122
|
+
rack_response = config[:app].(env)
|
|
123
|
+
if !env['uma.hijacked?']
|
|
124
|
+
send_rack_response(machine, env, fd, rack_response)
|
|
125
|
+
should_process_next_request?(env)
|
|
126
|
+
else
|
|
127
|
+
false
|
|
128
|
+
end
|
|
129
|
+
rescue => e
|
|
130
|
+
if (h = config[:error_handler])
|
|
131
|
+
h.(e)
|
|
132
|
+
else
|
|
133
|
+
send_error_response(machine, fd, e)
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def get_request_env(config, stream)
|
|
138
|
+
env = {
|
|
139
|
+
'rack.url_scheme' => 'http',
|
|
140
|
+
'SCRIPT_NAME' => '',
|
|
141
|
+
'SERVER_NAME' => 'localhost',
|
|
142
|
+
'rack.hijack?' => true,
|
|
143
|
+
'rack.hijack' => -> {
|
|
144
|
+
env['uma.hijacked?'] = true
|
|
145
|
+
env['rack.hijack'] = env['rack.input'] || RackIO.new(env, stream)
|
|
146
|
+
},
|
|
147
|
+
'rack.errors' => config[:error_stream]
|
|
148
|
+
}
|
|
149
|
+
buf = +''
|
|
150
|
+
ret = stream.get_line(buf, 4096)
|
|
151
|
+
return if !ret
|
|
152
|
+
|
|
153
|
+
parse_request_line(env, buf)
|
|
154
|
+
while true
|
|
155
|
+
ret = stream.get_line(buf, 4096)
|
|
156
|
+
raise ParseError, "Unexpected EOF" if !ret
|
|
157
|
+
break if buf.empty?
|
|
158
|
+
|
|
159
|
+
parse_header(env, buf)
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
if env['CONTENT_LENGTH'] || env['HTTP_TRANSFER_ENCODING'] == 'chunked'
|
|
163
|
+
env['rack.input'] = RackIO.new(env, stream)
|
|
164
|
+
end
|
|
165
|
+
env
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
RE_REQUEST_LINE = /^([a-z]+)\s+([^\s\?]+)(?:\?([^\s]+))?\s+(http\/[019\.]{1,3})/i
|
|
169
|
+
|
|
170
|
+
def parse_request_line(env, line)
|
|
171
|
+
m = line.match(RE_REQUEST_LINE)
|
|
172
|
+
raise ParseError, 'Invalid request line' if !m
|
|
173
|
+
|
|
174
|
+
env['REQUEST_METHOD'] = m[1].upcase
|
|
175
|
+
env['PATH_INFO'] = m[2]
|
|
176
|
+
env['QUERY_STRING'] = m[3] || ''
|
|
177
|
+
env['SERVER_PROTOCOL'] = m[4]
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
RE_HEADER_LINE = /^([a-z0-9-]+):\s+(.+)/i
|
|
181
|
+
|
|
182
|
+
def parse_header(env, line)
|
|
183
|
+
m = line.match(RE_HEADER_LINE)
|
|
184
|
+
raise ParseError, 'Invalid header' if !m
|
|
185
|
+
|
|
186
|
+
key = "HTTP_#{m[1].upcase.tr('-', '_')}"
|
|
187
|
+
value = m[2]
|
|
188
|
+
case key
|
|
189
|
+
when 'HTTP_CONTENT_TYPE'
|
|
190
|
+
env['CONTENT_TYPE'] = value
|
|
191
|
+
when 'HTTP_CONTENT_LENGTH'
|
|
192
|
+
env['CONTENT_LENGTH'] = value
|
|
193
|
+
when 'HTTP_HOST'
|
|
194
|
+
env['SERVER_NAME'] = value
|
|
195
|
+
env[key] = value
|
|
196
|
+
else
|
|
197
|
+
env[key] = value
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
CHUNK_END = "\r\n0\r\n\r\n"
|
|
202
|
+
|
|
203
|
+
def send_rack_response(machine, env, fd, response)
|
|
204
|
+
status, headers, body = response
|
|
205
|
+
hijack = headers['rack.hijack']
|
|
206
|
+
|
|
207
|
+
empty_body = body.nil? || body == '' || (body.is_a?(Array) && body.empty?)
|
|
208
|
+
|
|
209
|
+
if !hijack
|
|
210
|
+
chunked = nil
|
|
211
|
+
headers['content-length'] = '0' if empty_body
|
|
212
|
+
chunked = !headers['content-length']
|
|
213
|
+
headers['transfer-encoding'] = 'chunked' if chunked
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
buf_status = "HTTP/1.1 #{status}\r\n"
|
|
217
|
+
buf_headers = format_headers(headers)
|
|
218
|
+
|
|
219
|
+
if hijack
|
|
220
|
+
machine.sendv(fd, buf_status, buf_headers)
|
|
221
|
+
hijack.(env['rack.hijack'].())
|
|
222
|
+
elsif body && !empty_body
|
|
223
|
+
if chunked
|
|
224
|
+
send_body_chunked(machine, env, fd, buf_status, buf_headers, body)
|
|
225
|
+
else
|
|
226
|
+
if body.is_a?(Array)
|
|
227
|
+
machine.sendv(fd, buf_status, buf_headers, *body)
|
|
228
|
+
else
|
|
229
|
+
machine.sendv(fd, buf_status, buf_headers, body)
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
else
|
|
233
|
+
machine.sendv(fd, buf_status, buf_headers)
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def send_body_chunked(machine, env, fd, buf_status, buf_headers, body)
|
|
238
|
+
case body
|
|
239
|
+
when String
|
|
240
|
+
buf_chunk_header = "#{body.bytesize.to_s(16)}\r\n"
|
|
241
|
+
machine.sendv(
|
|
242
|
+
fd,
|
|
243
|
+
buf_status,
|
|
244
|
+
buf_headers,
|
|
245
|
+
buf_chunk_header,
|
|
246
|
+
body,
|
|
247
|
+
CHUNK_END
|
|
248
|
+
)
|
|
249
|
+
when Array
|
|
250
|
+
bufs = [buf_status, buf_headers]
|
|
251
|
+
first = true
|
|
252
|
+
body.each {
|
|
253
|
+
bufs << (first ? "#{it.bytesize.to_s(16)}\r\n" : "\r\n#{it.bytesize.to_s(16)}\r\n")
|
|
254
|
+
first = false
|
|
255
|
+
bufs << it
|
|
256
|
+
}
|
|
257
|
+
bufs << CHUNK_END
|
|
258
|
+
machine.sendv(fd, *bufs)
|
|
259
|
+
else
|
|
260
|
+
if body.respond_to?(:each)
|
|
261
|
+
first = true
|
|
262
|
+
body.each do
|
|
263
|
+
chunk_header = first ? "#{it.bytesize.to_s(16)}\r\n" : "\r\n#{it.bytesize.to_s(16)}\r\n"
|
|
264
|
+
if first
|
|
265
|
+
machine.sendv(fd, buf_status, buf_headers, chunk_header, it)
|
|
266
|
+
first = false
|
|
267
|
+
else
|
|
268
|
+
machine.sendv(fd, chunk_header, it)
|
|
269
|
+
end
|
|
270
|
+
end
|
|
271
|
+
machine.sendv(fd, first ? "0\r\n\r\n" : "\r\n0\r\n\r\n")
|
|
272
|
+
elsif body.respond_to?(:call)
|
|
273
|
+
machine.sendv(fd, buf_status, buf_headers)
|
|
274
|
+
chunked_stream = ChunkedIO.new(machine, fd, env['uma.stream'])
|
|
275
|
+
body.call(chunked_stream)
|
|
276
|
+
else
|
|
277
|
+
raise ResponseError, "Invalid response body: #{body.inspect}"
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
chunked_stream.close if chunked_stream
|
|
281
|
+
body.close if body.respond_to?(:close)
|
|
282
|
+
end
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
def format_headers(headers)
|
|
286
|
+
buf = +''
|
|
287
|
+
headers.each do |k, v|
|
|
288
|
+
next if k =~ /^rack\./
|
|
289
|
+
|
|
290
|
+
case v
|
|
291
|
+
when String
|
|
292
|
+
buf << "#{k}: #{v}\r\n"
|
|
293
|
+
when Array
|
|
294
|
+
v.each { buf << "#{k}: #{it}\r\n" }
|
|
295
|
+
else
|
|
296
|
+
raise ResponseError, "Invalid header value #{v.inspect}"
|
|
297
|
+
end
|
|
298
|
+
end
|
|
299
|
+
buf << "\r\n"
|
|
300
|
+
buf
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
def should_process_next_request?(env)
|
|
304
|
+
# TODO: look at env
|
|
305
|
+
true
|
|
306
|
+
end
|
|
307
|
+
end
|
|
308
|
+
end
|
data/lib/uma/server.rb
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'uringmachine/fiber_scheduler'
|
|
4
|
+
|
|
5
|
+
module Uma
|
|
6
|
+
|
|
7
|
+
class Server
|
|
8
|
+
def initialize(env)
|
|
9
|
+
@env = env
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def start
|
|
13
|
+
@machine = UM.new
|
|
14
|
+
@stop_queue = UM::Queue.new
|
|
15
|
+
|
|
16
|
+
@config = ServerControl.server_config(@env)
|
|
17
|
+
@threads = @config[:thread_count].times.map {
|
|
18
|
+
ServerControl.start_worker_thread(@config, @stop_queue)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
ServerControl.await_process_termination(@machine)
|
|
22
|
+
|
|
23
|
+
stop
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def stop
|
|
27
|
+
return if !@threads
|
|
28
|
+
|
|
29
|
+
@config[:thread_count].times { @machine.push(@stop_queue, :stop) }
|
|
30
|
+
@threads.each(&:join)
|
|
31
|
+
@threads = nil
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
module ServerControl
|
|
36
|
+
extend self
|
|
37
|
+
|
|
38
|
+
def server_config(env)
|
|
39
|
+
{
|
|
40
|
+
thread_count: 2,
|
|
41
|
+
bind_entries: env[:bind] ? bind_entries(env[:bind]) : [],
|
|
42
|
+
connection_proc: env[:connection_proc],
|
|
43
|
+
error_stream: env[:error_stream]
|
|
44
|
+
}
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def bind_entries(bind_value)
|
|
48
|
+
case bind_value
|
|
49
|
+
when Array
|
|
50
|
+
bind_value.map { parse_bind_spec(it) }
|
|
51
|
+
when String
|
|
52
|
+
[parse_bind_spec(bind_value)]
|
|
53
|
+
else
|
|
54
|
+
raise ArgumentError, "invalid bind value"
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
BIND_SPEC_RE = /^(.+)\:(\d+)$/.freeze
|
|
59
|
+
|
|
60
|
+
def parse_bind_spec(spec)
|
|
61
|
+
if (m = spec.match(BIND_SPEC_RE))
|
|
62
|
+
[m[1], m[2].to_i]
|
|
63
|
+
else
|
|
64
|
+
raise ArgumentError, "Invalid bind spec"
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def start_worker_thread(config, stop_queue)
|
|
69
|
+
Thread.new do
|
|
70
|
+
machine = UM.new
|
|
71
|
+
scheduler = UM::FiberScheduler.new(machine)
|
|
72
|
+
Fiber.set_scheduler(scheduler)
|
|
73
|
+
|
|
74
|
+
worker_thread(machine, config, stop_queue)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def worker_thread(machine, config, stop_queue)
|
|
79
|
+
connection_fibers = Set.new
|
|
80
|
+
accept_fibers = start_acceptors(machine, config, connection_fibers)
|
|
81
|
+
|
|
82
|
+
machine.shift(stop_queue)
|
|
83
|
+
|
|
84
|
+
worker_thread_graceful_stop(machine, accept_fibers, connection_fibers)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# @return [Set<Fiber>] a set of accept fibers
|
|
88
|
+
def start_acceptors(machine, config, connection_fibers)
|
|
89
|
+
set = Set.new
|
|
90
|
+
return set if !config[:bind_entries] || config[:bind_entries].empty?
|
|
91
|
+
|
|
92
|
+
config[:bind_entries].each do
|
|
93
|
+
host, port = it
|
|
94
|
+
set << machine.spin do
|
|
95
|
+
fd = prepare_listening_socket(machine, host, port)
|
|
96
|
+
machine.accept_each(fd) { |fd|
|
|
97
|
+
start_connection(machine, config, connection_fibers, fd)
|
|
98
|
+
}
|
|
99
|
+
rescue UM::Terminate
|
|
100
|
+
ensure
|
|
101
|
+
machine.close(fd)
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
set
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def prepare_listening_socket(machine, host, port)
|
|
108
|
+
fd = machine.socket(UM::AF_INET, UM::SOCK_STREAM, 0, 0)
|
|
109
|
+
machine.setsockopt(fd, UM::SOL_SOCKET, UM::SO_REUSEPORT, true)
|
|
110
|
+
machine.bind(fd, host, port)
|
|
111
|
+
machine.listen(fd, UM::SOMAXCONN)
|
|
112
|
+
fd
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# @return [Fiber]
|
|
116
|
+
def start_connection(machine, config, connection_fibers, fd)
|
|
117
|
+
f = machine.spin do
|
|
118
|
+
connection_fibers << f
|
|
119
|
+
config[:connection_proc]&.(machine, fd)
|
|
120
|
+
ensure
|
|
121
|
+
machine.close(fd) rescue nil
|
|
122
|
+
connection_fibers.delete(f)
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def worker_thread_graceful_stop(machine, accept_fibers, connection_fibers)
|
|
127
|
+
# stop accepting connections
|
|
128
|
+
machine.terminate(accept_fibers)
|
|
129
|
+
machine.await_fibers(accept_fibers)
|
|
130
|
+
|
|
131
|
+
machine.terminate(connection_fibers)
|
|
132
|
+
|
|
133
|
+
# graceful stop with a timeout of 10 seconds
|
|
134
|
+
machine.timeout(10, UM::Terminate) do
|
|
135
|
+
machine.await_fibers(connection_fibers)
|
|
136
|
+
rescue UM::Terminate
|
|
137
|
+
alive = connection_fibers.reject(&:done?)
|
|
138
|
+
machine.terminate(alive)
|
|
139
|
+
machine.await_fibers(alive)
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def await_process_termination(machine)
|
|
144
|
+
sig_queue = UM::Queue.new
|
|
145
|
+
|
|
146
|
+
old_term_handler = trap('SIGTERM') {
|
|
147
|
+
machine.push(sig_queue, :term)
|
|
148
|
+
trap('SIGTERM', old_term_handler)
|
|
149
|
+
}
|
|
150
|
+
old_int_handler = trap('SIGINT') {
|
|
151
|
+
machine.push(sig_queue, :int)
|
|
152
|
+
trap('SIGINT', old_int_handler)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
machine.shift(sig_queue)
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
data/lib/uma/version.rb
ADDED
data/lib/uma.rb
ADDED
data/test/apps/config.ru
ADDED
data/test/apps/roda1.ru
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "roda"
|
|
4
|
+
require "irb"
|
|
5
|
+
|
|
6
|
+
# Demo app copied from Roda website:
|
|
7
|
+
# https://roda.jeremyevans.net/
|
|
8
|
+
class App < Roda
|
|
9
|
+
route do |r|
|
|
10
|
+
# GET / request
|
|
11
|
+
r.root do
|
|
12
|
+
r.redirect "/hello"
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# /hello branch
|
|
16
|
+
r.on "hello" do
|
|
17
|
+
# Set variable for all routes in /hello branch
|
|
18
|
+
@greeting = 'Hello'
|
|
19
|
+
|
|
20
|
+
# GET /hello/world request
|
|
21
|
+
r.get "world" do
|
|
22
|
+
"#{@greeting} world!"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# /hello request
|
|
26
|
+
r.is do
|
|
27
|
+
# GET /hello request
|
|
28
|
+
r.get do
|
|
29
|
+
"#{@greeting}!"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# POST /hello request
|
|
33
|
+
r.post do
|
|
34
|
+
r.redirect
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
run App.freeze.app
|
data/test/apps/simple.ru
ADDED
data/test/helper.rb
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'bundler/setup'
|
|
4
|
+
require_relative './coverage' if ENV['COVERAGE']
|
|
5
|
+
require 'uma'
|
|
6
|
+
require 'minitest/autorun'
|
|
7
|
+
require 'securerandom'
|
|
8
|
+
|
|
9
|
+
STDOUT.sync = true
|
|
10
|
+
STDERR.sync = true
|
|
11
|
+
|
|
12
|
+
module ::Kernel
|
|
13
|
+
def debug(**h)
|
|
14
|
+
k, v = h.first
|
|
15
|
+
h.delete(k)
|
|
16
|
+
|
|
17
|
+
rest = h.inject(+'') { |s, (k, v)| s << " #{k}: #{v.inspect}\n" }
|
|
18
|
+
STDOUT.orig_write("#{k}=>#{v} #{caller[0]}\n#{rest}")
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def trace(*args)
|
|
22
|
+
STDOUT.orig_write(format_trace(args))
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def format_trace(args)
|
|
26
|
+
if args.first.is_a?(String)
|
|
27
|
+
if args.size > 1
|
|
28
|
+
format("%s: %p\n", args.shift, args)
|
|
29
|
+
else
|
|
30
|
+
format("%s\n", args.first)
|
|
31
|
+
end
|
|
32
|
+
else
|
|
33
|
+
format("%p\n", args.size == 1 ? args.first : args)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def monotonic_clock
|
|
38
|
+
::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
module Minitest::Assertions
|
|
43
|
+
# def setup
|
|
44
|
+
# sleep 0.0001
|
|
45
|
+
# end
|
|
46
|
+
|
|
47
|
+
def assert_in_range exp_range, act
|
|
48
|
+
msg = message(msg) { "Expected #{mu_pp(act)} to be in range #{mu_pp(exp_range)}" }
|
|
49
|
+
assert exp_range.include?(act), msg
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
class UMBaseTest < Minitest::Test
|
|
54
|
+
# pull in UM constants
|
|
55
|
+
UM.constants.each do |c|
|
|
56
|
+
v = UM.const_get(c)
|
|
57
|
+
const_set(c, v) if v.is_a?(Integer)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
attr_accessor :machine
|
|
61
|
+
|
|
62
|
+
def setup
|
|
63
|
+
@machine = UM.new
|
|
64
|
+
@machine.test_mode = true
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def teardown
|
|
68
|
+
return if !@machine
|
|
69
|
+
|
|
70
|
+
if ENV['UM_METRICS']
|
|
71
|
+
pp machine.metrics
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
pending_fibers = @machine.pending_fibers
|
|
75
|
+
raise "leaked fibers: #{pending_fibers}" if pending_fibers.size > 0
|
|
76
|
+
|
|
77
|
+
GC.start
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def scheduler_calls_tally
|
|
81
|
+
@scheduler.calls.map { it[:sym] }.tally
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
@@port ||= 10001 + SecureRandom.rand(50000)
|
|
85
|
+
|
|
86
|
+
def random_port
|
|
87
|
+
@@port_assign_mutex ||= Mutex.new
|
|
88
|
+
@@port_assign_mutex.synchronize do
|
|
89
|
+
@@port += 1
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def make_tmp_file_tree(dir, spec)
|
|
94
|
+
FileUtils.mkdir(dir) rescue nil
|
|
95
|
+
spec.each do |k, v|
|
|
96
|
+
fn = File.join(dir, k.to_s)
|
|
97
|
+
case v
|
|
98
|
+
when String
|
|
99
|
+
IO.write(fn, v)
|
|
100
|
+
when Hash
|
|
101
|
+
FileUtils.mkdir(fn) rescue nil
|
|
102
|
+
make_tmp_file_tree(fn, v)
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
dir
|
|
106
|
+
end
|
|
107
|
+
end
|
data/test/run.rb
ADDED
data/test/test_app.rb
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'helper'
|
|
4
|
+
require 'uma/app'
|
|
5
|
+
|
|
6
|
+
class AppTest < UMBaseTest
|
|
7
|
+
App = Uma::App
|
|
8
|
+
|
|
9
|
+
def test_app
|
|
10
|
+
fn = File.join(__dir__, 'apps/simple.ru')
|
|
11
|
+
app = App.new(fn)
|
|
12
|
+
|
|
13
|
+
assert_kind_of App, app
|
|
14
|
+
assert_equal [200, {}, 'simple'], app.to_proc.({})
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def test_app_bad_filename
|
|
18
|
+
fn = File.join(__dir__, 'apps/simple2.ru')
|
|
19
|
+
assert_raises(Uma::Error) { App.new(fn) }
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def test_app_invalid_syntax
|
|
23
|
+
fn = File.join(__dir__, 'apps/bad_syntax.ru')
|
|
24
|
+
assert_raises(Uma::Error) { App.new(fn) }
|
|
25
|
+
end
|
|
26
|
+
end
|