syntropy 0.28.2 → 0.29.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,336 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative './helper'
4
+
5
+ class ServerTest < Minitest::Test
6
+ def make_socket_pair
7
+ port = 10000 + rand(30000)
8
+ server_fd = @machine.socket(UM::AF_INET, UM::SOCK_STREAM, 0, 0)
9
+ @machine.setsockopt(server_fd, UM::SOL_SOCKET, UM::SO_REUSEADDR, true)
10
+ @machine.bind(server_fd, '127.0.0.1', port)
11
+ @machine.listen(server_fd, UM::SOMAXCONN)
12
+
13
+ client_conn_fd = @machine.socket(UM::AF_INET, UM::SOCK_STREAM, 0, 0)
14
+ @machine.connect(client_conn_fd, '127.0.0.1', port)
15
+
16
+ server_conn_fd = @machine.accept(server_fd)
17
+
18
+ @machine.close(server_fd)
19
+ [client_conn_fd, server_conn_fd]
20
+ end
21
+
22
+ class STOP < StandardError
23
+ end
24
+
25
+ def setup(opts = {})
26
+ @machine = UM.new
27
+ @port = 10000 + rand(30000)
28
+ @env = { bind: "127.0.0.1:#{@port}" }.merge(opts)
29
+ @server = Syntropy::Server.new(@machine, @env) { @app&.call(it) }
30
+ @f_server = @machine.spin { run_server }
31
+
32
+ # let server spin and listen to incoming connections
33
+ @machine.sleep(0.01)
34
+
35
+ @client_fd = @machine.socket(UM::AF_INET, UM::SOCK_STREAM, 0, 0)
36
+ @machine.connect(@client_fd, '127.0.0.1', @port)
37
+ end
38
+
39
+ def run_server
40
+ @server.run
41
+ ensure
42
+ @server_done = true
43
+ end
44
+
45
+ def teardown
46
+ @machine.close(@client_fd) rescue nil
47
+ @server.stop!
48
+ @machine.snooze until @server_done
49
+ end
50
+
51
+ def write_http_request(msg)
52
+ @machine.send(@client_fd, msg, msg.bytesize, UM::MSG_WAITALL)
53
+ end
54
+
55
+ def write_client_side(msg)
56
+ @machine.send(@client_fd, msg, msg.bytesize, UM::MSG_WAITALL)
57
+ end
58
+
59
+ def read_client_side(len = 65536)
60
+ buf = +''
61
+ res = @machine.recv(@client_fd, buf, len, 0)
62
+ res == 0 ? nil : buf
63
+ end
64
+
65
+ def test_http_1_0_response
66
+ @app = ->(req) {
67
+ req.respond('Hello, world!', {})
68
+ }
69
+
70
+ write_http_request "GET / HTTP/1.0\r\n\r\n"
71
+ response = read_client_side
72
+ expected = "HTTP/1.1 505\r\nTransfer-Encoding: chunked\r\n\r\n1a\r\nHTTP version not supported\r\n0\r\n\r\n"
73
+ assert_equal(expected, response)
74
+ end
75
+
76
+ def test_basic_app_response
77
+ @app = ->(req) {
78
+ req.respond('Hello, world!', {})
79
+ }
80
+
81
+ write_http_request "GET / HTTP/1.1\r\n\r\n"
82
+ response = read_client_side
83
+ expected = "HTTP/1.1 200\r\nTransfer-Encoding: chunked\r\n\r\nd\r\nHello, world!\r\n0\r\n\r\n"
84
+ assert_equal(expected, response)
85
+ end
86
+
87
+ def test_pipelined_requests
88
+ @app = ->(req) {
89
+ req.respond("method: #{req.method}")
90
+ }
91
+
92
+ write_http_request "GET /foo HTTP/1.1\r\nServer: foo.com\r\n\r\nSCHmet /bar HTTP/1.1\r\n\r\n"
93
+
94
+ @machine.sleep(0.1)
95
+ response = read_client_side
96
+ expected = "HTTP/1.1 200\r\nTransfer-Encoding: chunked\r\n\r\nb\r\nmethod: get\r\n0\r\n\r\n" +
97
+ "HTTP/1.1 200\r\nTransfer-Encoding: chunked\r\n\r\ne\r\nmethod: schmet\r\n0\r\n\r\n"
98
+ assert_equal(expected, response)
99
+ end
100
+
101
+ def test_graceful_shutdown
102
+ @app = ->(req) do
103
+ @machine.sleep(1)
104
+ req.respond('Hello, world!', {})
105
+ rescue UM::Terminate
106
+ req.respond('Terminated!', {})
107
+ raise
108
+ end
109
+
110
+ write_http_request "GET /foo HTTP/1.1\r\nServer: foo.com\r\n\r\nSCHMET /bar HTTP/1.1\r\n\r\n"
111
+
112
+ @machine.sleep(0.01)
113
+ @server.stop!
114
+ @machine.snooze
115
+
116
+ response = read_client_side
117
+ expected = "HTTP/1.1 200\r\nTransfer-Encoding: chunked\r\n\r\nb\r\nTerminated!\r\n0\r\n\r\n"
118
+ assert_equal(expected, response)
119
+ end
120
+
121
+ def test_pipelined_requests_with_body
122
+ @bodies = []
123
+ @reqs = []
124
+ @app = ->(req) {
125
+ @reqs << req
126
+ @bodies << req.read
127
+ req.respond("method: #{req.method}")
128
+ }
129
+
130
+ msg = <<~HTTP.crlf_lines
131
+ POST /foo HTTP/1.1
132
+ Server: foo.com
133
+ Content-Length: 3
134
+
135
+ abcSCHMOST /bar HTTP/1.1
136
+ Server: bar.com
137
+ Content-Length: 6
138
+
139
+ defghi
140
+ HTTP
141
+ write_http_request msg
142
+
143
+ read_client_side
144
+ assert_equal 2, @bodies.size
145
+
146
+ req0 = @reqs.shift
147
+ headers = req0.headers
148
+ assert_equal({
149
+ ':method' => 'post',
150
+ ':path' => '/foo',
151
+ ':protocol' => 'http/1.1',
152
+ 'server' => 'foo.com',
153
+ 'content-length' => '3',
154
+ ':body-done-reading' => true,
155
+ ':tx' => 56,
156
+ }, headers)
157
+ body = @bodies.shift
158
+ assert_equal 'abc', body
159
+
160
+ req1 = @reqs.shift
161
+ headers = req1.headers
162
+ assert_equal({
163
+ ':method' => 'schmost',
164
+ ':path' => '/bar',
165
+ ':protocol' => 'http/1.1',
166
+ 'server' => 'bar.com',
167
+ 'content-length' => '6',
168
+ ':body-done-reading' => true,
169
+ ':tx' => 59,
170
+ }, headers)
171
+ body = @bodies.shift
172
+ assert_equal 'defghi', body
173
+ end
174
+
175
+ def test_pipelined_requests_with_body_chunked
176
+ @bodies = []
177
+ @reqs = []
178
+ @app = ->(req) do
179
+ @reqs << req
180
+ @bodies << (b = req.read)
181
+ req.respond("method: #{req.method}")
182
+ rescue => e
183
+ p e
184
+ p e.backtrace
185
+ exit!
186
+ end
187
+
188
+ msg = <<~HTTP.crlf_lines
189
+ POST /foo HTTP/1.1
190
+ Server: foo.com
191
+ Transfer-Encoding: chunked
192
+
193
+ 3
194
+ abc
195
+ 2
196
+ de
197
+ 0
198
+
199
+ SCHMOST /bar HTTP/1.1
200
+ Server: bar.com
201
+ Transfer-Encoding: chunked
202
+
203
+ 1f
204
+ 123456789abcdefghijklmnopqrstuv
205
+ 0
206
+
207
+
208
+
209
+ HTTP
210
+
211
+ write_http_request msg
212
+ read_client_side
213
+ read_client_side
214
+ assert_equal 2, @bodies.size
215
+
216
+ req0 = @reqs.shift
217
+ headers = req0.headers
218
+ assert_equal({
219
+ ':method' => 'post',
220
+ ':path' => '/foo',
221
+ ':protocol' => 'http/1.1',
222
+ 'server' => 'foo.com',
223
+ 'transfer-encoding' => 'chunked',
224
+ ':body-done-reading' => true,
225
+ ':tx' => 56
226
+ }, headers)
227
+ body = @bodies.shift
228
+ assert_equal 'abcde', body
229
+
230
+ req1 = @reqs.shift
231
+ headers = req1.headers
232
+ assert_equal({
233
+ ':method' => 'schmost',
234
+ ':path' => '/bar',
235
+ ':protocol' => 'http/1.1',
236
+ 'server' => 'bar.com',
237
+ 'transfer-encoding' => 'chunked',
238
+ ':body-done-reading' => true,
239
+ ':tx' => 59
240
+ }, headers)
241
+ body = @bodies.shift
242
+ assert_equal '123456789abcdefghijklmnopqrstuv', body
243
+ end
244
+
245
+ class TestLogger
246
+ attr_reader :entries
247
+
248
+ def initialize
249
+ @entries = []
250
+ end
251
+
252
+ def info(o)
253
+ @entries << o.merge(level: :INFO)
254
+ end
255
+
256
+ def error(o)
257
+ @entries << o.merge(level: :ERROR)
258
+ end
259
+ end
260
+
261
+ def test_logging
262
+ skip
263
+ reqs = []
264
+ @env[:logger] = TestLogger.new
265
+ @app = ->(req) { reqs << req; req.respond('Hello, world!', {}) }
266
+
267
+ write_http_request "GET / HTTP/1.1\r\n\r\n"
268
+ response = read_client_side
269
+ expected = "HTTP/1.1 200\r\nTransfer-Encoding: chunked\r\n\r\nd\r\nHello, world!\r\n0\r\n\r\n"
270
+ assert_equal(expected, response)
271
+
272
+ entries = @env[:logger].entries
273
+ assert_equal 1, entries.size
274
+ assert_equal 1, reqs.size
275
+
276
+ assert_equal reqs.first, entries.first[:request]
277
+ end
278
+
279
+ def test_server_headers
280
+ @env[:server_headers] = "Server: Tipi\r\n"
281
+
282
+ @app = ->(req) {
283
+ req.respond('Hello, world!', {})
284
+ }
285
+
286
+ write_http_request "GET / HTTP/1.1\r\n\r\n"
287
+ response = read_client_side
288
+ expected = "HTTP/1.1 200\r\nTransfer-Encoding: chunked\r\nServer: Tipi\r\n\r\nd\r\nHello, world!\r\n0\r\n\r\n"
289
+ assert_equal(expected, response)
290
+ end
291
+
292
+ def test_server_headers_date
293
+ setup({ server_extensions: { date: true } })
294
+ @machine.sleep(0.1)
295
+ assert_kind_of Time, @env[:server_date]
296
+
297
+ @app = ->(req) {
298
+ req.respond('foo', {})
299
+ }
300
+
301
+ write_http_request "GET / HTTP/1.1\r\n\r\n"
302
+ response = read_client_side
303
+ expected = "HTTP/1.1 200\r\nTransfer-Encoding: chunked\r\nDate: #{@env[:server_date].httpdate}\r\n\r\n3\r\nfoo\r\n0\r\n\r\n"
304
+ assert_equal(expected, response)
305
+ end
306
+
307
+ def test_server_headers_date_and_server_name
308
+ setup({ server_extensions: { date: true, name: 'Foo' } })
309
+ @machine.sleep(0.1)
310
+ assert_kind_of Time, @env[:server_date]
311
+
312
+ @app = ->(req) {
313
+ req.respond('foo', {})
314
+ }
315
+
316
+ write_http_request "GET / HTTP/1.1\r\n\r\n"
317
+ response = read_client_side
318
+ expected = "HTTP/1.1 200\r\nTransfer-Encoding: chunked\r\nServer: Foo\r\nDate: #{@env[:server_date].httpdate}\r\n\r\n3\r\nfoo\r\n0\r\n\r\n"
319
+ assert_equal(expected, response)
320
+ end
321
+
322
+ def test_server_headers_server_name
323
+ setup({ server_extensions: { name: 'Bar' } })
324
+ @machine.sleep(0.1)
325
+ assert_nil @env[:server_date]
326
+
327
+ @app = ->(req) {
328
+ req.respond('foo', {})
329
+ }
330
+
331
+ write_http_request "GET / HTTP/1.1\r\n\r\n"
332
+ response = read_client_side
333
+ expected = "HTTP/1.1 200\r\nTransfer-Encoding: chunked\r\nServer: Bar\r\n\r\n3\r\nfoo\r\n0\r\n\r\n"
334
+ assert_equal(expected, response)
335
+ end
336
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: syntropy
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.28.2
4
+ version: 0.29.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sharon Rosner
@@ -51,34 +51,20 @@ dependencies:
51
51
  - - "~>"
52
52
  - !ruby/object:Gem::Version
53
53
  version: '0.24'
54
- - !ruby/object:Gem::Dependency
55
- name: tp2
56
- requirement: !ruby/object:Gem::Requirement
57
- requirements:
58
- - - "~>"
59
- - !ruby/object:Gem::Version
60
- version: 0.20.2
61
- type: :runtime
62
- prerelease: false
63
- version_requirements: !ruby/object:Gem::Requirement
64
- requirements:
65
- - - "~>"
66
- - !ruby/object:Gem::Version
67
- version: 0.20.2
68
54
  - !ruby/object:Gem::Dependency
69
55
  name: uringmachine
70
56
  requirement: !ruby/object:Gem::Requirement
71
57
  requirements:
72
58
  - - "~>"
73
59
  - !ruby/object:Gem::Version
74
- version: 0.24.0
60
+ version: 1.0.0
75
61
  type: :runtime
76
62
  prerelease: false
77
63
  version_requirements: !ruby/object:Gem::Requirement
78
64
  requirements:
79
65
  - - "~>"
80
66
  - !ruby/object:Gem::Version
81
- version: 0.24.0
67
+ version: 1.0.0
82
68
  - !ruby/object:Gem::Dependency
83
69
  name: listen
84
70
  requirement: !ruby/object:Gem::Requirement
@@ -159,6 +145,7 @@ files:
159
145
  - ".github/workflows/test.yml"
160
146
  - ".gitignore"
161
147
  - ".rubocop.yml"
148
+ - ".ruby-version"
162
149
  - CHANGELOG.md
163
150
  - Gemfile
164
151
  - LICENSE
@@ -209,16 +196,18 @@ files:
209
196
  - lib/syntropy/applets/builtin/json_api.js
210
197
  - lib/syntropy/applets/builtin/ping.rb
211
198
  - lib/syntropy/applets/builtin/req.rb
199
+ - lib/syntropy/connection.rb
212
200
  - lib/syntropy/connection_pool.rb
213
201
  - lib/syntropy/dev_mode.rb
214
202
  - lib/syntropy/errors.rb
215
- - lib/syntropy/file_watch.rb
216
203
  - lib/syntropy/json_api.rb
204
+ - lib/syntropy/logger.rb
217
205
  - lib/syntropy/markdown.rb
218
206
  - lib/syntropy/module.rb
219
207
  - lib/syntropy/papercraft_extensions.rb
220
208
  - lib/syntropy/request_extensions.rb
221
209
  - lib/syntropy/routing_tree.rb
210
+ - lib/syntropy/server.rb
222
211
  - lib/syntropy/side_run.rb
223
212
  - lib/syntropy/utils.rb
224
213
  - lib/syntropy/version.rb
@@ -259,13 +248,14 @@ files:
259
248
  - test/run.rb
260
249
  - test/test_app.rb
261
250
  - test/test_caching.rb
251
+ - test/test_connection.rb
262
252
  - test/test_connection_pool.rb
263
253
  - test/test_errors.rb
264
- - test/test_file_watch.rb
265
254
  - test/test_json_api.rb
266
255
  - test/test_module.rb
267
256
  - test/test_request_extensions.rb
268
257
  - test/test_routing_tree.rb
258
+ - test/test_server.rb
269
259
  - test/test_side_run.rb
270
260
  homepage: https://github.com/digital-fabric/syntropy
271
261
  licenses:
@@ -273,10 +263,10 @@ licenses:
273
263
  metadata:
274
264
  homepage_uri: https://github.com/digital-fabric/syntropy
275
265
  documentation_uri: https://www.rubydoc.info/gems/syntropy
276
- changelog_uri: https://github.com/digital-fabric/syntropy/blob/master/CHANGELOG.md
266
+ changelog_uri: https://github.com/digital-fabric/syntropy/blob/main/CHANGELOG.md
277
267
  rdoc_options:
278
268
  - "--title"
279
- - Extralite
269
+ - Syntropy
280
270
  - "--main"
281
271
  - README.md
282
272
  require_paths:
@@ -292,7 +282,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
292
282
  - !ruby/object:Gem::Version
293
283
  version: '0'
294
284
  requirements: []
295
- rubygems_version: 4.0.3
285
+ rubygems_version: 4.0.6
296
286
  specification_version: 4
297
287
  summary: Syntropic Web Framework
298
288
  test_files: []
@@ -1,28 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Syntropy
4
- def self.file_watch(machine, *roots, period: 0.1, &block)
5
- raise 'Missing root paths' if roots.empty?
6
-
7
- require 'listen'
8
-
9
- queue = Thread::Queue.new
10
- listener = Listen.to(*roots) do |modified, added, removed|
11
- modified.each { queue.push([:modified, it]) }
12
- added.each { queue.push([:added, it]) }
13
- removed.each { queue.push([:removed, it]) }
14
- end
15
- listener.start
16
-
17
- loop do
18
- machine.sleep(period) while queue.empty?
19
- event, fn = queue.shift
20
- block.call(event, fn)
21
- end
22
- rescue StandardError => e
23
- p e
24
- p e.backtrace
25
- ensure
26
- listener.stop
27
- end
28
- end
@@ -1,36 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'fileutils'
4
- require_relative 'helper'
5
-
6
- class FileWatchTest < Minitest::Test
7
- def setup
8
- @machine = UM.new
9
- @root = "/tmp/syntropy/#{rand(1000000).to_s(16)}"
10
- FileUtils.mkdir_p(@root)
11
- end
12
-
13
- def test_file_watch
14
- queue = UM::Queue.new
15
-
16
- f = @machine.spin do
17
- Syntropy.file_watch(@machine, @root, period: 0.01) { |event, fn| @machine.push(queue, [event, fn]) }
18
- end
19
- @machine.sleep(0.05)
20
- assert_equal 0, queue.count
21
-
22
- fn = File.join(@root, 'foo.bar')
23
- IO.write(fn, 'abc')
24
- assert_equal [:added, fn], @machine.shift(queue)
25
-
26
- fn = File.join(@root, 'foo.bar')
27
- IO.write(fn, 'def')
28
- assert_equal [:modified, fn], @machine.shift(queue)
29
-
30
- FileUtils.rm(fn)
31
- assert_equal [:removed, fn], @machine.shift(queue)
32
- ensure
33
- @machine.schedule(f, UM::Terminate)
34
- # @machine.join(f)
35
- end
36
- end