syntropy 0.33.0 → 0.34.1
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 +4 -4
- data/CHANGELOG.md +11 -0
- data/cmd/console.rb +18 -7
- data/cmd/serve.rb +26 -18
- data/cmd/test.rb +37 -24
- data/examples/blog/.gitignore +1 -0
- data/examples/blog/app/_layout/default.rb +3 -0
- data/examples/blog/app/_lib/database.rb +13 -0
- data/examples/blog/app/_lib/{post_store.rb → posts.rb} +3 -1
- data/examples/blog/app/assets/style.css +20 -0
- data/examples/blog/app/index.rb +12 -2
- data/examples/blog/app/posts/[id]/edit.rb +2 -2
- data/examples/blog/app/posts/[id]/index.rb +4 -4
- data/examples/blog/app/posts/index.rb +4 -4
- data/examples/blog/app/posts/new.rb +1 -1
- data/examples/blog/app/test.rb +7 -0
- data/examples/blog/config/development.rb +5 -0
- data/examples/blog/config/production.rb +4 -0
- data/examples/blog/config/test.rb +5 -0
- data/examples/blog/test/test_posts.rb +65 -0
- data/examples/mcp-oauth/app/oauth/token.rb +1 -1
- data/examples/template/.gitignore +2 -0
- data/examples/template/Gemfile +3 -0
- data/examples/template/app/_layout/default.rb +14 -0
- data/examples/template/app/_lib/database.rb +13 -0
- data/examples/template/app/_schema/2026-01-01-initial.rb +9 -0
- data/examples/template/app/assets/style.css +25 -0
- data/examples/template/app/index.rb +27 -0
- data/examples/template/app/test.rb +7 -0
- data/examples/template/config/development.rb +5 -0
- data/examples/template/config/production.rb +4 -0
- data/examples/template/config/test.rb +5 -0
- data/examples/template/test/test_app.rb +14 -0
- data/lib/syntropy/app.rb +48 -40
- data/lib/syntropy/applets/builtin/auto_refresh/watch.sse.rb +1 -1
- data/lib/syntropy/applets/builtin/default_error_handler/style.css +4 -8
- data/lib/syntropy/applets/builtin/default_error_handler.rb +18 -9
- data/lib/syntropy/db/schema.rb +1 -1
- data/lib/syntropy/db/store.rb +2 -0
- data/lib/syntropy/errors.rb +6 -2
- data/lib/syntropy/http/client.rb +1 -0
- data/lib/syntropy/http/server_connection.rb +0 -4
- data/lib/syntropy/json_api.rb +27 -1
- data/lib/syntropy/logger.rb +81 -27
- data/lib/syntropy/markdown.rb +61 -32
- data/lib/syntropy/mime_types.rb +9 -5
- data/lib/syntropy/module_loader.rb +31 -9
- data/lib/syntropy/papercraft_extensions.rb +2 -2
- data/lib/syntropy/request/mock_adapter.rb +10 -8
- data/lib/syntropy/request/request_info.rb +91 -0
- data/lib/syntropy/request/response.rb +1 -12
- data/lib/syntropy/request/validation.rb +1 -0
- data/lib/syntropy/request.rb +51 -19
- data/lib/syntropy/routing_tree.rb +27 -28
- data/lib/syntropy/session.rb +198 -0
- data/lib/syntropy/side_run.rb +25 -2
- data/lib/syntropy/test.rb +105 -10
- data/lib/syntropy/utils.rb +53 -18
- data/lib/syntropy/version.rb +1 -1
- data/lib/syntropy.rb +44 -10
- data/test/bm_router_proc.rb +4 -4
- data/test/fixtures/app/class_instance.rb +5 -0
- data/test/fixtures/app/http.rb +5 -0
- data/test/fixtures/app/post_ct.rb +5 -0
- data/test/fixtures/app/singleton.rb +3 -0
- data/test/test_app.rb +13 -52
- data/test/test_caching.rb +2 -2
- data/test/test_db_schema.rb +1 -1
- data/test/test_http_server_connection.rb +3 -3
- data/test/test_module_loader.rb +5 -2
- data/test/test_response.rb +0 -19
- data/test/test_routing_tree.rb +69 -69
- data/test/test_server.rb +5 -9
- data/test/test_test.rb +70 -0
- metadata +66 -42
- data/examples/blog/app/_setup.rb +0 -4
- data/lib/syntropy/request/session.rb +0 -113
- /data/test/{app → fixtures/app}/.well-known/foo.rb +0 -0
- /data/test/{app → fixtures/app}/_hook.rb +0 -0
- /data/test/{app → fixtures/app}/_layout/default.rb +0 -0
- /data/test/{app → fixtures/app}/_lib/callable.rb +0 -0
- /data/test/{app → fixtures/app}/_lib/dep.rb +0 -0
- /data/test/{app → fixtures/app}/_lib/env.rb +0 -0
- /data/test/{app → fixtures/app}/_lib/klass.rb +0 -0
- /data/test/{app → fixtures/app}/_lib/missing-export.rb +0 -0
- /data/test/{app → fixtures/app}/_lib/self.rb +0 -0
- /data/test/{app → fixtures/app}/about/_error.rb +0 -0
- /data/test/{app → fixtures/app}/about/foo.md +0 -0
- /data/test/{app → fixtures/app}/about/index.rb +0 -0
- /data/test/{app → fixtures/app}/about/raise.rb +0 -0
- /data/test/{app → fixtures/app}/api+.rb +0 -0
- /data/test/{app → fixtures/app}/assets/style.css +0 -0
- /data/test/{app → fixtures/app}/bad_mod.rb +0 -0
- /data/test/{app → fixtures/app}/bar.rb +0 -0
- /data/test/{app → fixtures/app}/baz.rb +0 -0
- /data/test/{app → fixtures/app}/by_method.rb +0 -0
- /data/test/{app → fixtures/app}/deps.rb +0 -0
- /data/test/{app → fixtures/app}/index.html +0 -0
- /data/test/{app → fixtures/app}/mod/bar/index+.rb +0 -0
- /data/test/{app → fixtures/app}/mod/foo/index.rb +0 -0
- /data/test/{app → fixtures/app}/mod/path/a.rb +0 -0
- /data/test/{app → fixtures/app}/mod/path/b.rb +0 -0
- /data/test/{app → fixtures/app}/params/[foo].rb +0 -0
- /data/test/{app → fixtures/app}/rss.rb +0 -0
- /data/test/{app → fixtures/app}/tmp.rb +0 -0
- /data/test/{app_custom → fixtures/app_custom}/_site.rb +0 -0
- /data/test/{app_multi_site → fixtures/app_multi_site}/_site.rb +0 -0
- /data/test/{app_multi_site → fixtures/app_multi_site}/bar.baz/index.html +0 -0
- /data/test/{app_multi_site → fixtures/app_multi_site}/foo.bar/index.html +0 -0
- /data/test/{app_setup → fixtures/app_setup}/_setup.rb +0 -0
- /data/test/{app_setup → fixtures/app_setup}/index.rb +0 -0
- /data/test/{app_with_schema → fixtures/app_with_schema}/_schema/2026-01-02-foo.rb +0 -0
- /data/test/{app_with_schema → fixtures/app_with_schema}/_schema/2026-05-30-bar.rb +0 -0
- /data/test/{schema → fixtures/schema}/2026-01-02-foo.rb +0 -0
- /data/test/{schema → fixtures/schema}/2026-05-30-bar.rb +0 -0
data/lib/syntropy/test.rb
CHANGED
|
@@ -7,27 +7,49 @@ require 'json'
|
|
|
7
7
|
require 'uri'
|
|
8
8
|
|
|
9
9
|
module Syntropy
|
|
10
|
+
# Test provides a class for testing a Syntropy app, based on Minitest.
|
|
10
11
|
class Test < Minitest::Test
|
|
11
12
|
HTTP = Syntropy::HTTP
|
|
12
13
|
|
|
14
|
+
# Sets the app environment for all Syntropy tests.
|
|
15
|
+
#
|
|
16
|
+
# @param env [Hash] app environment hash
|
|
17
|
+
# @return [void]
|
|
13
18
|
def self.env=(env)
|
|
14
19
|
@@env = env
|
|
15
20
|
end
|
|
16
21
|
|
|
17
22
|
attr_reader :machine, :app
|
|
18
23
|
|
|
24
|
+
# Returns the test environment.
|
|
25
|
+
#
|
|
26
|
+
# @return [Hash] test app environment
|
|
19
27
|
def env
|
|
20
28
|
@@env
|
|
21
29
|
end
|
|
22
30
|
|
|
23
|
-
|
|
24
|
-
|
|
31
|
+
# Loads and returns a module with the given reference.
|
|
32
|
+
#
|
|
33
|
+
# @param ref [String] module reference
|
|
34
|
+
# @return [any] module
|
|
35
|
+
def load_module(ref, raise_on_missing: true)
|
|
36
|
+
app.module_loader.load(ref, raise_on_missing:)
|
|
25
37
|
end
|
|
26
38
|
|
|
39
|
+
# Makes an HTTP request to the test app.
|
|
40
|
+
#
|
|
41
|
+
# @param headers [Hash] request headers
|
|
42
|
+
# @param body [String, nil] request body
|
|
43
|
+
# @return [Syntropy::Request]
|
|
27
44
|
def http_request(headers, body = nil)
|
|
28
45
|
@test_harness.request(headers, body)
|
|
29
46
|
end
|
|
30
47
|
|
|
48
|
+
# Makes an HTTP GET request to the test app.
|
|
49
|
+
#
|
|
50
|
+
# @param path [String] request path
|
|
51
|
+
# @param headers [Hash] request headers
|
|
52
|
+
# @return [Syntropy::Request]
|
|
31
53
|
def get(path, **headers)
|
|
32
54
|
http_request(
|
|
33
55
|
headers.merge(
|
|
@@ -37,6 +59,13 @@ module Syntropy
|
|
|
37
59
|
)
|
|
38
60
|
end
|
|
39
61
|
|
|
62
|
+
# Makes an HTTP POST request to the test app.
|
|
63
|
+
#
|
|
64
|
+
# @param path [String] request path
|
|
65
|
+
# @param content_type [String, nil] content MIME type
|
|
66
|
+
# @param body [String] request body
|
|
67
|
+
# @param headers [Hash] request headers
|
|
68
|
+
# @return [Syntropy::Request]
|
|
40
69
|
def post(path, content_type, body, **headers)
|
|
41
70
|
headers = headers.merge('content-type' => content_type) if content_type
|
|
42
71
|
http_request(
|
|
@@ -50,26 +79,52 @@ module Syntropy
|
|
|
50
79
|
)
|
|
51
80
|
end
|
|
52
81
|
|
|
53
|
-
|
|
54
|
-
|
|
82
|
+
# Makes an HTTP POST request to the test app with a "application/json"
|
|
83
|
+
# content type. The given object is converted to JSON and sent as the
|
|
84
|
+
# request body.
|
|
85
|
+
#
|
|
86
|
+
# @param path [String] request path
|
|
87
|
+
# @param data [any] data
|
|
88
|
+
# @return [Syntropy::Request]
|
|
89
|
+
def post_json(path, data, **)
|
|
90
|
+
post(path, 'application/json', JSON.dump(data), **)
|
|
55
91
|
end
|
|
56
92
|
|
|
57
|
-
|
|
58
|
-
|
|
93
|
+
# Makes an HTTP POST request to the test app with a
|
|
94
|
+
# "application/x-www-form-urlencoded" content type. The given data is
|
|
95
|
+
# converted to URL Encoded form format and sent as the request body.
|
|
96
|
+
#
|
|
97
|
+
# @param path [String] request path
|
|
98
|
+
# @param data [Hash] form data
|
|
99
|
+
# @return [Syntropy::Request]
|
|
100
|
+
def post_form(path, data, **)
|
|
101
|
+
post(path, 'application/x-www-form-urlencoded', URI.encode_www_form(data), **)
|
|
59
102
|
end
|
|
60
103
|
|
|
104
|
+
# Sets up a test instance.
|
|
105
|
+
#
|
|
106
|
+
# @return [void]
|
|
61
107
|
def setup
|
|
62
108
|
raise 'Environment not set' if !@@env
|
|
63
109
|
|
|
110
|
+
Syntropy.load_config(@@env)
|
|
111
|
+
|
|
64
112
|
@machine = UM.new
|
|
65
113
|
@app = Syntropy::App.new(
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
114
|
+
**@@env.merge(
|
|
115
|
+
machine: @machine,
|
|
116
|
+
test_mode: true
|
|
117
|
+
)
|
|
69
118
|
)
|
|
70
119
|
@test_harness = Syntropy::TestHarness.new(@app)
|
|
120
|
+
|
|
121
|
+
@db = load_module('/_lib/database', raise_on_missing: false)
|
|
122
|
+
@db&.migrate!
|
|
71
123
|
end
|
|
72
124
|
|
|
125
|
+
# Cleans up a test instance.
|
|
126
|
+
#
|
|
127
|
+
# @return [void]
|
|
73
128
|
def teardown
|
|
74
129
|
@machine = nil
|
|
75
130
|
@app = nil
|
|
@@ -77,18 +132,31 @@ module Syntropy
|
|
|
77
132
|
end
|
|
78
133
|
end
|
|
79
134
|
|
|
135
|
+
# TestHarness provides glue code for performing HTTP requests against a
|
|
136
|
+
# Syntropy app.
|
|
80
137
|
class TestHarness
|
|
138
|
+
# Initializes the test harness with the given app.
|
|
139
|
+
#
|
|
140
|
+
# @param app [Syntropy::App]
|
|
141
|
+
# @return [void]
|
|
81
142
|
def initialize(app)
|
|
82
143
|
@app = app
|
|
83
144
|
@app.raise_internal_server_error = true if @app.respond_to?(:raise_internal_server_error=)
|
|
84
145
|
end
|
|
85
146
|
|
|
147
|
+
# Perfrms a request against the associated app.
|
|
148
|
+
#
|
|
149
|
+
# @param headers [Hash] request headers
|
|
150
|
+
# @param body [String, nil] request body
|
|
151
|
+
# @return [Syntropy::Request]
|
|
86
152
|
def request(headers, body = nil)
|
|
87
153
|
req = mock_req(headers, body)
|
|
88
154
|
@app.call(req)
|
|
89
155
|
req
|
|
90
156
|
end
|
|
91
157
|
|
|
158
|
+
# Temporarily disables raising an exception in case of an internal server
|
|
159
|
+
# error while running the given block.
|
|
92
160
|
def no_raise_internal_server_error
|
|
93
161
|
return yield if !@app.respond_to?(:raise_internal_server_error=)
|
|
94
162
|
|
|
@@ -102,29 +170,50 @@ module Syntropy
|
|
|
102
170
|
|
|
103
171
|
private
|
|
104
172
|
|
|
173
|
+
# Creates a Syntropy request running on a mock adapter.
|
|
174
|
+
#
|
|
175
|
+
# @param headers [Hash] request headers
|
|
176
|
+
# @param body [String, nil] request body
|
|
177
|
+
# @return [Syntropy::Request]
|
|
105
178
|
def mock_req(headers, body = nil)
|
|
106
179
|
Syntropy::MockAdapter.mock(headers, body)
|
|
107
180
|
end
|
|
108
181
|
end
|
|
109
182
|
|
|
110
|
-
|
|
183
|
+
# Extensions to Syntropy::Request for testing.
|
|
184
|
+
module TestRequestExtensions
|
|
185
|
+
# Returns the response headers.
|
|
186
|
+
#
|
|
187
|
+
# @return [Hash]
|
|
111
188
|
def response_headers
|
|
112
189
|
adapter.response_headers
|
|
113
190
|
end
|
|
114
191
|
|
|
192
|
+
# Returns the response status
|
|
193
|
+
#
|
|
194
|
+
# @return [Integer]
|
|
115
195
|
def response_status
|
|
116
196
|
adapter.status
|
|
117
197
|
end
|
|
118
198
|
|
|
199
|
+
# Returns the response body
|
|
200
|
+
#
|
|
201
|
+
# @return [String, nil]
|
|
119
202
|
def response_body
|
|
120
203
|
adapter.response_body
|
|
121
204
|
end
|
|
122
205
|
|
|
206
|
+
# Parses the response body from JSON.
|
|
207
|
+
#
|
|
208
|
+
# @return [any] parsed JSON object
|
|
123
209
|
def response_json
|
|
124
210
|
raise if response_content_type != 'application/json'
|
|
125
211
|
JSON.parse(response_body)
|
|
126
212
|
end
|
|
127
213
|
|
|
214
|
+
# Returns the response content MIME type.
|
|
215
|
+
#
|
|
216
|
+
# @return [String, nil]
|
|
128
217
|
def response_content_type
|
|
129
218
|
ct = response_headers['Content-Type']
|
|
130
219
|
return nil if !ct
|
|
@@ -135,6 +224,10 @@ module Syntropy
|
|
|
135
224
|
m[1]
|
|
136
225
|
end
|
|
137
226
|
|
|
227
|
+
# Returns the cookie value for the given cookie name from the response.
|
|
228
|
+
#
|
|
229
|
+
# @param name [String, Symbol] cookie name
|
|
230
|
+
# @return [String, nil] cookie value
|
|
138
231
|
def response_cookie(name)
|
|
139
232
|
sc = response_headers['Set-Cookie']
|
|
140
233
|
return nil if !sc
|
|
@@ -145,4 +238,6 @@ module Syntropy
|
|
|
145
238
|
m[1]
|
|
146
239
|
end
|
|
147
240
|
end
|
|
241
|
+
|
|
242
|
+
Request.include TestRequestExtensions
|
|
148
243
|
end
|
data/lib/syntropy/utils.rb
CHANGED
|
@@ -1,52 +1,87 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'securerandom'
|
|
4
|
+
|
|
3
5
|
module Syntropy
|
|
4
6
|
# Utilities for use in modules
|
|
5
7
|
module Utilities
|
|
6
|
-
|
|
8
|
+
def tmp_path(prefix = 'syntropy')
|
|
9
|
+
"/tmp/#{prefix}-#{SecureRandom.hex(16)}"
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# Returns a request handler that routes request according to the host
|
|
13
|
+
# header. Looks for site directories (named by host name) in the app's root
|
|
14
|
+
# directory. A map may be given in order to provide additional hostnames to
|
|
15
|
+
# site directories.
|
|
16
|
+
#
|
|
17
|
+
# @param env [Hash] app environment hash
|
|
18
|
+
# @param map [Hash, nil] additional hostname map
|
|
19
|
+
# @return [Proc] router proc
|
|
7
20
|
def route_by_host(env, map = nil)
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
.select { File.directory?(it) }
|
|
12
|
-
.each_with_object({}) { |fn, h|
|
|
13
|
-
name = File.basename(fn)
|
|
14
|
-
h[name] = Syntropy::App.new(**env.merge(root_dir: fn))
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
# copy over map refs
|
|
21
|
+
sites = find_hostname_sites(env)
|
|
22
|
+
|
|
23
|
+
# add map refs
|
|
18
24
|
map&.each { |k, v| sites[k] = sites[v] }
|
|
19
25
|
|
|
20
|
-
#
|
|
21
26
|
lambda { |req|
|
|
22
27
|
site = sites[req.host]
|
|
23
28
|
site ? site.call(req) : req.respond(nil, ':status' => HTTP::BAD_REQUEST)
|
|
24
29
|
}
|
|
25
30
|
end
|
|
26
31
|
|
|
32
|
+
# Returns a list of parsed markdown pages at the given path.
|
|
33
|
+
#
|
|
34
|
+
# @param env [Hash] app environment hash
|
|
35
|
+
# @param ref [String] directory path
|
|
36
|
+
# @return [Array<Hash>] array of page entries
|
|
27
37
|
def page_list(env, ref)
|
|
28
|
-
full_path = File.join(env[:
|
|
38
|
+
full_path = File.join(env[:app_root], ref)
|
|
29
39
|
raise 'Not a directory' if !File.directory?(full_path)
|
|
30
40
|
|
|
31
41
|
Dir[File.join(full_path, '*.md')].sort.map {
|
|
32
|
-
atts, markdown = Syntropy.
|
|
42
|
+
atts, markdown = Syntropy::Markdown.parse(it, env)
|
|
33
43
|
{ atts:, markdown: }
|
|
34
44
|
}
|
|
35
45
|
end
|
|
36
46
|
|
|
37
|
-
|
|
38
|
-
|
|
47
|
+
# Instantiates a Syntropy app for the given environment hash.
|
|
48
|
+
#
|
|
49
|
+
# @return [Syntropy::App]
|
|
50
|
+
def app(**)
|
|
51
|
+
Syntropy::App.new(**)
|
|
39
52
|
end
|
|
40
53
|
|
|
41
|
-
|
|
54
|
+
BUILTIN_APPLET_app_root = File.expand_path(File.join(__dir__, 'applets/builtin'))
|
|
55
|
+
|
|
56
|
+
# Creates a builtin applet with the given environment hash. By default the
|
|
57
|
+
# builtin applet is mounted at /.syntropy.
|
|
58
|
+
#
|
|
59
|
+
# @param env [Hash] app environment
|
|
60
|
+
# @param mount_path [String] mount path for the builtin applet
|
|
61
|
+
# @return [Syntropy::App] applet
|
|
42
62
|
def builtin_applet(env, mount_path: '/.syntropy')
|
|
43
63
|
app(
|
|
44
64
|
machine: env[:machine],
|
|
45
|
-
|
|
65
|
+
app_root: BUILTIN_APPLET_app_root,
|
|
46
66
|
mount_path: mount_path,
|
|
47
67
|
builtin_applet_path: nil,
|
|
48
68
|
watch_files: nil
|
|
49
69
|
)
|
|
50
70
|
end
|
|
71
|
+
|
|
72
|
+
private
|
|
73
|
+
|
|
74
|
+
# Finds sites in the root directory for the given environment hash.
|
|
75
|
+
#
|
|
76
|
+
# @param env [Hash] app environment hash
|
|
77
|
+
# @return [Hash] hash mapping hostname to app
|
|
78
|
+
def find_hostname_sites(env)
|
|
79
|
+
Dir[File.join(env[:app_root], '*')]
|
|
80
|
+
.select { File.directory?(it) && File.basename(it) !~ /^_/ }
|
|
81
|
+
.each_with_object({}) { |fn, h|
|
|
82
|
+
name = File.basename(fn)
|
|
83
|
+
h[name] = Syntropy::App.new(**env.merge(app_root: fn))
|
|
84
|
+
}
|
|
85
|
+
end
|
|
51
86
|
end
|
|
52
87
|
end
|
data/lib/syntropy/version.rb
CHANGED
data/lib/syntropy.rb
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require 'uringmachine'
|
|
4
4
|
require 'papercraft'
|
|
5
|
+
require 'yaml'
|
|
5
6
|
|
|
6
7
|
require 'syntropy/request'
|
|
7
8
|
require 'syntropy/logger'
|
|
@@ -21,20 +22,31 @@ require 'syntropy/side_run'
|
|
|
21
22
|
require 'syntropy/utils'
|
|
22
23
|
require 'syntropy/version'
|
|
23
24
|
|
|
25
|
+
# Syntropy is a web framework for building web apps in Ruby. Syntropy uses
|
|
26
|
+
# UringMachine for I/O and concurrency, and provides a comprehensive and
|
|
27
|
+
# flexible solution for writing web apps with minimal boilerplate.
|
|
24
28
|
module Syntropy
|
|
25
29
|
extend Utilities
|
|
26
30
|
|
|
27
31
|
class << self
|
|
28
|
-
attr_accessor :machine
|
|
32
|
+
attr_accessor :machine, :dev_mode
|
|
29
33
|
|
|
34
|
+
# Runs the given block on a separate thread. Use this method for running
|
|
35
|
+
# code that is not fiber-aware (i.e. does not use UringMachine).
|
|
36
|
+
#
|
|
37
|
+
# @return [any] operation return value
|
|
30
38
|
def side_run(&block)
|
|
31
39
|
raise 'Syntropy.machine not set' if !@machine
|
|
32
40
|
|
|
33
41
|
SideRun.call(@machine, &block)
|
|
34
42
|
end
|
|
35
|
-
end
|
|
36
43
|
|
|
37
|
-
|
|
44
|
+
# Runs a web app with the given environment hash. The given block is either
|
|
45
|
+
# an instance of Syntropy::App, or a Proc/callable that takes a request as
|
|
46
|
+
# argument.
|
|
47
|
+
#
|
|
48
|
+
# @param env [Hash] environment
|
|
49
|
+
# @return [void]
|
|
38
50
|
def run(env = {}, &app)
|
|
39
51
|
if @in_run
|
|
40
52
|
@env = env
|
|
@@ -47,7 +59,14 @@ module Syntropy
|
|
|
47
59
|
@in_run = true
|
|
48
60
|
machine = env[:machine] || UM.new
|
|
49
61
|
|
|
50
|
-
env[:logger]
|
|
62
|
+
if (logger = env[:logger])
|
|
63
|
+
logger.info(
|
|
64
|
+
message: "Syntropy #{Syntropy::VERSION}, UringMachine #{UM::VERSION}, Ruby #{RUBY_VERSION}"
|
|
65
|
+
)
|
|
66
|
+
logger.info(
|
|
67
|
+
message: "Running in #{env[:mode]} mode"
|
|
68
|
+
)
|
|
69
|
+
end
|
|
51
70
|
|
|
52
71
|
server = HTTP::Server.new(machine, env, &app)
|
|
53
72
|
|
|
@@ -58,23 +77,38 @@ module Syntropy
|
|
|
58
77
|
end
|
|
59
78
|
end
|
|
60
79
|
|
|
61
|
-
def
|
|
62
|
-
return
|
|
80
|
+
def load_config(env)
|
|
81
|
+
return if !env[:config_root]
|
|
82
|
+
|
|
83
|
+
loader_env = env.merge(
|
|
84
|
+
app_root: env[:config_root],
|
|
85
|
+
logger: nil
|
|
86
|
+
)
|
|
87
|
+
loader = ModuleLoader.new(loader_env)
|
|
88
|
+
config = loader.load(env[:mode])
|
|
63
89
|
|
|
64
|
-
|
|
65
|
-
@env[:app] = app if app
|
|
90
|
+
env[:config] = config
|
|
66
91
|
end
|
|
67
92
|
|
|
68
93
|
private
|
|
69
94
|
|
|
95
|
+
# Sets up asynchronous SIGINT handling.
|
|
96
|
+
#
|
|
97
|
+
# @param machine [UringMachine] machine instance
|
|
98
|
+
# @param fiber [Fiber] fiber to terminate on SIGINT
|
|
99
|
+
# @return [void]
|
|
70
100
|
def setup_signal_handling(machine, fiber)
|
|
71
101
|
queue = UM::Queue.new
|
|
72
102
|
trap('SIGINT') { machine.push(queue, :SIGINT) }
|
|
73
103
|
machine.spin { watch_for_int_signal(machine, queue, fiber) }
|
|
74
104
|
end
|
|
75
105
|
|
|
76
|
-
#
|
|
77
|
-
#
|
|
106
|
+
# Waits for signal from queue, then terminates the given fiber.
|
|
107
|
+
#
|
|
108
|
+
# @param machine [UringMachine] machine instance
|
|
109
|
+
# @param queue [UringMachine::Queue] queue to wait on
|
|
110
|
+
# @param fiber [Fiber] fiber to terminate
|
|
111
|
+
# @return [void]
|
|
78
112
|
def watch_for_int_signal(machine, queue, fiber)
|
|
79
113
|
machine.shift(queue)
|
|
80
114
|
machine.schedule(fiber, UM::Terminate.new)
|
data/test/bm_router_proc.rb
CHANGED
|
@@ -138,8 +138,8 @@ def make_tmp_file_tree(dir, spec)
|
|
|
138
138
|
dir
|
|
139
139
|
end
|
|
140
140
|
|
|
141
|
-
|
|
142
|
-
make_tmp_file_tree(
|
|
141
|
+
app_root = "/tmp/#{__FILE__.gsub('/', '-')}-#{SecureRandom.hex}"
|
|
142
|
+
make_tmp_file_tree(app_root, {
|
|
143
143
|
'index.rb': "export ->(req) { req.redirect('/hello') }",
|
|
144
144
|
'hello': {
|
|
145
145
|
'index.rb': "export ->(req) { req.respond('Hello!', 'Content-Type' => 'text/html') }",
|
|
@@ -149,7 +149,7 @@ make_tmp_file_tree(ROOT_DIR, {
|
|
|
149
149
|
|
|
150
150
|
machine = UM.new
|
|
151
151
|
syntropy_app = Syntropy::App.new(
|
|
152
|
-
|
|
152
|
+
app_root: app_root,
|
|
153
153
|
mount_path: '/',
|
|
154
154
|
machine: machine
|
|
155
155
|
)
|
|
@@ -185,7 +185,7 @@ BM.run do
|
|
|
185
185
|
def setup
|
|
186
186
|
machine = UM.new
|
|
187
187
|
syntropy_app = Syntropy::App.new(
|
|
188
|
-
|
|
188
|
+
app_root: app_root,
|
|
189
189
|
mount_path: '/',
|
|
190
190
|
# watch_files: 0.05,
|
|
191
191
|
machine: machine
|
data/test/test_app.rb
CHANGED
|
@@ -5,7 +5,7 @@ require_relative 'helper'
|
|
|
5
5
|
class AppTest < Minitest::Test
|
|
6
6
|
HTTP = Syntropy::HTTP
|
|
7
7
|
|
|
8
|
-
APP_ROOT = File.join(__dir__, 'app')
|
|
8
|
+
APP_ROOT = File.join(__dir__, 'fixtures/app')
|
|
9
9
|
|
|
10
10
|
def setup
|
|
11
11
|
@machine = UM.new
|
|
@@ -14,7 +14,7 @@ class AppTest < Minitest::Test
|
|
|
14
14
|
@tmp_fn = File.join(APP_ROOT, 'tmp.rb')
|
|
15
15
|
|
|
16
16
|
@app = Syntropy::App.new(
|
|
17
|
-
|
|
17
|
+
app_root: APP_ROOT,
|
|
18
18
|
mount_path: '/test',
|
|
19
19
|
watch_files: 0.05,
|
|
20
20
|
machine: @machine
|
|
@@ -133,6 +133,9 @@ class AppTest < Minitest::Test
|
|
|
133
133
|
|
|
134
134
|
req = @test_harness.request(':method' => 'DELETE', ':path' => '/test/by_method')
|
|
135
135
|
assert_equal HTTP::METHOD_NOT_ALLOWED, req.response_status
|
|
136
|
+
|
|
137
|
+
req = @test_harness.request(':method' => 'GET', ':path' => '/test/http')
|
|
138
|
+
assert_equal HTTP::TEAPOT, req.response_status
|
|
136
139
|
end
|
|
137
140
|
|
|
138
141
|
def test_automatic_redirect_on_trailing_slash
|
|
@@ -183,13 +186,13 @@ end
|
|
|
183
186
|
class CustomAppTest < Minitest::Test
|
|
184
187
|
HTTP = Syntropy::HTTP
|
|
185
188
|
|
|
186
|
-
APP_ROOT = File.join(__dir__, 'app_custom')
|
|
189
|
+
APP_ROOT = File.join(__dir__, 'fixtures/app_custom')
|
|
187
190
|
|
|
188
191
|
def setup
|
|
189
192
|
@machine = UM.new
|
|
190
193
|
@app = Syntropy::App.load(
|
|
191
194
|
machine: @machine,
|
|
192
|
-
|
|
195
|
+
app_root: APP_ROOT,
|
|
193
196
|
mount_path: '/'
|
|
194
197
|
)
|
|
195
198
|
@test_harness = Syntropy::TestHarness.new(@app)
|
|
@@ -205,13 +208,13 @@ end
|
|
|
205
208
|
class MultiSiteAppTest < Minitest::Test
|
|
206
209
|
HTTP = Syntropy::HTTP
|
|
207
210
|
|
|
208
|
-
APP_ROOT = File.join(__dir__, 'app_multi_site')
|
|
211
|
+
APP_ROOT = File.join(__dir__, 'fixtures/app_multi_site')
|
|
209
212
|
|
|
210
213
|
def setup
|
|
211
214
|
@machine = UM.new
|
|
212
215
|
@app = Syntropy::App.load(
|
|
213
216
|
machine: @machine,
|
|
214
|
-
|
|
217
|
+
app_root: APP_ROOT,
|
|
215
218
|
mount_path: '/'
|
|
216
219
|
)
|
|
217
220
|
@test_harness = Syntropy::TestHarness.new(@app)
|
|
@@ -233,7 +236,7 @@ end
|
|
|
233
236
|
class AppAPITest < Minitest::Test
|
|
234
237
|
HTTP = Syntropy::HTTP
|
|
235
238
|
|
|
236
|
-
APP_ROOT = File.join(__dir__, 'app')
|
|
239
|
+
APP_ROOT = File.join(__dir__, 'fixtures/app')
|
|
237
240
|
|
|
238
241
|
def setup
|
|
239
242
|
@machine = UM.new
|
|
@@ -242,7 +245,7 @@ class AppAPITest < Minitest::Test
|
|
|
242
245
|
@tmp_fn = File.join(APP_ROOT, 'tmp.rb')
|
|
243
246
|
|
|
244
247
|
@app = Syntropy::App.new(
|
|
245
|
-
|
|
248
|
+
app_root: APP_ROOT,
|
|
246
249
|
mount_path: '/test',
|
|
247
250
|
watch_files: 0.05,
|
|
248
251
|
machine: @machine
|
|
@@ -297,7 +300,7 @@ end
|
|
|
297
300
|
class AppDependenciesTest < Minitest::Test
|
|
298
301
|
HTTP = Syntropy::HTTP
|
|
299
302
|
|
|
300
|
-
APP_ROOT = File.join(__dir__, 'app')
|
|
303
|
+
APP_ROOT = File.join(__dir__, 'fixtures/app')
|
|
301
304
|
|
|
302
305
|
def test_app_dependencies
|
|
303
306
|
foo = { foo: 'foo' }
|
|
@@ -309,7 +312,7 @@ class AppDependenciesTest < Minitest::Test
|
|
|
309
312
|
@tmp_fn = File.join(APP_ROOT, 'tmp.rb')
|
|
310
313
|
|
|
311
314
|
@app = Syntropy::App.new(
|
|
312
|
-
|
|
315
|
+
app_root: APP_ROOT,
|
|
313
316
|
mount_path: '/test',
|
|
314
317
|
machine: @machine,
|
|
315
318
|
foo: foo,
|
|
@@ -322,45 +325,3 @@ class AppDependenciesTest < Minitest::Test
|
|
|
322
325
|
assert_equal HTTP::OK, req.response_status
|
|
323
326
|
end
|
|
324
327
|
end
|
|
325
|
-
|
|
326
|
-
class AppDBSetupDBTest < Minitest::Test
|
|
327
|
-
HTTP = Syntropy::HTTP
|
|
328
|
-
|
|
329
|
-
APP_ROOT = File.join(__dir__, 'app_with_schema')
|
|
330
|
-
|
|
331
|
-
def test_app_setup_db
|
|
332
|
-
machine = UM.new
|
|
333
|
-
|
|
334
|
-
app = Syntropy::App.new(
|
|
335
|
-
root_dir: APP_ROOT,
|
|
336
|
-
mount_path: '/test',
|
|
337
|
-
machine: machine
|
|
338
|
-
)
|
|
339
|
-
|
|
340
|
-
assert_equal false, app.respond_to?(:connection_pool)
|
|
341
|
-
assert_equal false, app.respond_to?(:schema)
|
|
342
|
-
|
|
343
|
-
fn = "/tmp/#{rand(100000)}.db"
|
|
344
|
-
|
|
345
|
-
app.setup_db(
|
|
346
|
-
db_path: fn,
|
|
347
|
-
schema_root: '_schema'
|
|
348
|
-
)
|
|
349
|
-
|
|
350
|
-
assert_equal true, app.respond_to?(:connection_pool)
|
|
351
|
-
assert_equal fn, app.connection_pool.with_db { it.filename }
|
|
352
|
-
|
|
353
|
-
assert_equal true, app.respond_to?(:schema)
|
|
354
|
-
app.schema.apply(app.connection_pool)
|
|
355
|
-
assert_equal '2026-05-30-bar', app.schema.current_version(app.connection_pool)
|
|
356
|
-
|
|
357
|
-
assert_equal [
|
|
358
|
-
{
|
|
359
|
-
id: 1,
|
|
360
|
-
title: 'foo',
|
|
361
|
-
body: 'baz'
|
|
362
|
-
}
|
|
363
|
-
], app.connection_pool.query('select id, title, body from posts')
|
|
364
|
-
|
|
365
|
-
end
|
|
366
|
-
end
|
data/test/test_caching.rb
CHANGED
|
@@ -6,7 +6,7 @@ require 'digest/sha1'
|
|
|
6
6
|
class CachingTest < Minitest::Test
|
|
7
7
|
HTTP = Syntropy::HTTP
|
|
8
8
|
|
|
9
|
-
APP_ROOT = File.join(__dir__, 'app')
|
|
9
|
+
APP_ROOT = File.join(__dir__, 'fixtures/app')
|
|
10
10
|
|
|
11
11
|
def make_socket_pair
|
|
12
12
|
port = SecureRandom.random_number(10000..40000)
|
|
@@ -32,7 +32,7 @@ class CachingTest < Minitest::Test
|
|
|
32
32
|
|
|
33
33
|
@env = {
|
|
34
34
|
machine: @machine,
|
|
35
|
-
|
|
35
|
+
app_root: APP_ROOT,
|
|
36
36
|
mount_path: '/test',
|
|
37
37
|
watch_files: 0.05
|
|
38
38
|
}
|
data/test/test_db_schema.rb
CHANGED
|
@@ -77,7 +77,7 @@ class DBSchemaTest < Minitest::Test
|
|
|
77
77
|
|
|
78
78
|
def test_schema_from_module_files
|
|
79
79
|
module_loader = Syntropy::ModuleLoader.new({
|
|
80
|
-
|
|
80
|
+
app_root: File.join(__dir__, 'fixtures/schema')
|
|
81
81
|
})
|
|
82
82
|
schema = Syntropy::DB::Schema.new(module_loader:, schema_root: '/')
|
|
83
83
|
|
|
@@ -454,7 +454,7 @@ class HTTPServerConnectionTest < Minitest::Test
|
|
|
454
454
|
write_client_side("GET / HTTP/1.1\r\n\r\n")
|
|
455
455
|
@machine.spin do
|
|
456
456
|
@connection.serve_request
|
|
457
|
-
rescue => e
|
|
457
|
+
rescue StandardError => e
|
|
458
458
|
p e
|
|
459
459
|
p e.backtrace
|
|
460
460
|
end
|
|
@@ -479,7 +479,7 @@ class HTTPServerConnectionTest < Minitest::Test
|
|
|
479
479
|
|
|
480
480
|
@hook = ->(req) do
|
|
481
481
|
req.respond_with_static_file(fn, nil, nil, nil)
|
|
482
|
-
rescue => e
|
|
482
|
+
rescue StandardError => e
|
|
483
483
|
p e
|
|
484
484
|
p e.backtrace
|
|
485
485
|
end
|
|
@@ -490,7 +490,7 @@ class HTTPServerConnectionTest < Minitest::Test
|
|
|
490
490
|
write_client_side("GET / HTTP/1.1\r\n\r\n")
|
|
491
491
|
@machine.spin do
|
|
492
492
|
@connection.serve_request
|
|
493
|
-
rescue => e
|
|
493
|
+
rescue StandardError => e
|
|
494
494
|
p e
|
|
495
495
|
p e.backtrace
|
|
496
496
|
end
|