tep 0.11.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/LICENSE +21 -0
- data/Makefile +134 -0
- data/README.md +247 -0
- data/SINATRA_COMPAT.md +376 -0
- data/bin/tep +2156 -0
- data/examples/agentic_chat/README.md +103 -0
- data/examples/agentic_chat/app.rb +310 -0
- data/examples/api_gateway/README.md +49 -0
- data/examples/api_gateway/app.rb +66 -0
- data/examples/blog/app.rb +367 -0
- data/examples/blog/views/index.erb +36 -0
- data/examples/blog/views/login.erb +28 -0
- data/examples/blog/views/new_post.erb +25 -0
- data/examples/blog/views/show.erb +16 -0
- data/examples/chat/app.rb +278 -0
- data/examples/chat/assets/logo.svg +13 -0
- data/examples/chat/assets/style.css +209 -0
- data/examples/chat/views/index.erb +142 -0
- data/examples/chatbot/README.md +111 -0
- data/examples/chatbot/app.rb +1024 -0
- data/examples/chatbot/assets/chat.js +249 -0
- data/examples/chatbot/assets/compare.js +93 -0
- data/examples/chatbot/assets/markdown.js +84 -0
- data/examples/chatbot/assets/style.css +215 -0
- data/examples/chatbot/schema.sql +25 -0
- data/examples/chatbot/views/compare.erb +43 -0
- data/examples/chatbot/views/index.erb +42 -0
- data/examples/chatbot/views/login.erb +22 -0
- data/examples/chatbot/views/setup.erb +23 -0
- data/examples/counter/README.md +68 -0
- data/examples/counter/app.rb +85 -0
- data/examples/experiments/AGENTS.md +91 -0
- data/examples/experiments/README.md +99 -0
- data/examples/experiments/app.rb +225 -0
- data/examples/geohash/Gemfile +11 -0
- data/examples/geohash/Gemfile.lock +17 -0
- data/examples/geohash/README.md +58 -0
- data/examples/geohash/app.rb +33 -0
- data/examples/hello.rb +120 -0
- data/examples/llm_gateway/README.md +73 -0
- data/examples/llm_gateway/app.rb +91 -0
- data/examples/maidenhead/Gemfile +7 -0
- data/examples/maidenhead/Gemfile.lock +17 -0
- data/examples/maidenhead/README.md +47 -0
- data/examples/maidenhead/app.rb +46 -0
- data/examples/pg_hello.rb +76 -0
- data/examples/qdrant/Gemfile +11 -0
- data/examples/qdrant/Gemfile.lock +29 -0
- data/examples/qdrant/README.md +54 -0
- data/examples/sinatra_style.rb +32 -0
- data/examples/websocket_echo.rb +37 -0
- data/lib/tep/agent_delegation.rb +35 -0
- data/lib/tep/app.rb +291 -0
- data/lib/tep/assets.rb +52 -0
- data/lib/tep/auth.rb +78 -0
- data/lib/tep/auth_bearer_token.rb +126 -0
- data/lib/tep/auth_oauth2.rb +189 -0
- data/lib/tep/auth_oauth2_client.rb +29 -0
- data/lib/tep/auth_oauth2_code.rb +40 -0
- data/lib/tep/auth_session_cookie.rb +132 -0
- data/lib/tep/broadcast.rb +265 -0
- data/lib/tep/broadcast_subscription.rb +42 -0
- data/lib/tep/cache.rb +49 -0
- data/lib/tep/events.rb +257 -0
- data/lib/tep/filter.rb +21 -0
- data/lib/tep/handler.rb +35 -0
- data/lib/tep/http.rb +599 -0
- data/lib/tep/identity.rb +67 -0
- data/lib/tep/job.rb +186 -0
- data/lib/tep/json.rb +572 -0
- data/lib/tep/jwt.rb +126 -0
- data/lib/tep/live_view.rb +219 -0
- data/lib/tep/llm.rb +505 -0
- data/lib/tep/logger.rb +85 -0
- data/lib/tep/mcp.rb +203 -0
- data/lib/tep/multipart.rb +98 -0
- data/lib/tep/net.rb +155 -0
- data/lib/tep/openai_server.rb +725 -0
- data/lib/tep/parallel.rb +168 -0
- data/lib/tep/parser.rb +81 -0
- data/lib/tep/password.rb +102 -0
- data/lib/tep/pg.rb +1128 -0
- data/lib/tep/presence.rb +589 -0
- data/lib/tep/presence_entry.rb +52 -0
- data/lib/tep/proxy.rb +801 -0
- data/lib/tep/request.rb +194 -0
- data/lib/tep/response.rb +134 -0
- data/lib/tep/router.rb +137 -0
- data/lib/tep/scheduler.rb +342 -0
- data/lib/tep/security.rb +140 -0
- data/lib/tep/server.rb +276 -0
- data/lib/tep/server_scheduled.rb +375 -0
- data/lib/tep/session.rb +98 -0
- data/lib/tep/shell.rb +62 -0
- data/lib/tep/sphttp.c +858 -0
- data/lib/tep/sqlite.rb +215 -0
- data/lib/tep/streamer.rb +31 -0
- data/lib/tep/tep_pg.c +769 -0
- data/lib/tep/tep_sqlite.c +320 -0
- data/lib/tep/url.rb +161 -0
- data/lib/tep/version.rb +3 -0
- data/lib/tep/websocket/connection.rb +171 -0
- data/lib/tep/websocket/driver.rb +169 -0
- data/lib/tep/websocket/frame.rb +238 -0
- data/lib/tep/websocket/handshake.rb +159 -0
- data/lib/tep/websocket.rb +68 -0
- data/lib/tep.rb +981 -0
- data/public/hello.txt +1 -0
- data/public/style.css +4 -0
- data/spinel-ext.json +33 -0
- data/test/helper.rb +248 -0
- data/test/real_world/01_simple.rb +5 -0
- data/test/real_world/02_lifecycle.rb +20 -0
- data/test/real_world/03_chat.rb +75 -0
- data/test/real_world/04_health_api.rb +25 -0
- data/test/real_world/05_todo_api.rb +57 -0
- data/test/real_world/06_basic_auth.rb +25 -0
- data/test/real_world/07_bbc_rest_api.rb +228 -0
- data/test/real_world/07_sklise_things.rb +109 -0
- data/test/real_world/08_jwd83_helloworld.rb +56 -0
- data/test/run_all.rb +7 -0
- data/test/run_parallel.rb +89 -0
- data/test/spinel_scheduled_burst_segv_repro.rb +33 -0
- data/test/test_api_gateway.rb +76 -0
- data/test/test_auth.rb +223 -0
- data/test/test_auth_oauth2.rb +208 -0
- data/test/test_auth_session_cookie.rb +198 -0
- data/test/test_broadcast.rb +197 -0
- data/test/test_broadcast_pg.rb +135 -0
- data/test/test_cache.rb +98 -0
- data/test/test_cache_static.rb +48 -0
- data/test/test_cookies.rb +52 -0
- data/test/test_erb.rb +53 -0
- data/test/test_erb_ivars.rb +58 -0
- data/test/test_events.rb +114 -0
- data/test/test_filters.rb +41 -0
- data/test/test_geohash_example.rb +89 -0
- data/test/test_http.rb +137 -0
- data/test/test_http_pool.rb +122 -0
- data/test/test_http_pool_send.rb +57 -0
- data/test/test_identity.rb +165 -0
- data/test/test_inbound_tls.rb +101 -0
- data/test/test_inbound_tls_scheduled.rb +101 -0
- data/test/test_job.rb +108 -0
- data/test/test_json.rb +168 -0
- data/test/test_jwt.rb +143 -0
- data/test/test_live_view.rb +324 -0
- data/test/test_llm.rb +250 -0
- data/test/test_llm_gateway.rb +95 -0
- data/test/test_logger.rb +101 -0
- data/test/test_maidenhead_example.rb +86 -0
- data/test/test_mcp.rb +264 -0
- data/test/test_misc_v02.rb +54 -0
- data/test/test_modular.rb +43 -0
- data/test/test_multi_filters.rb +40 -0
- data/test/test_mustache.rb +57 -0
- data/test/test_openai_server.rb +598 -0
- data/test/test_optional_segments.rb +45 -0
- data/test/test_parallel.rb +102 -0
- data/test/test_params.rb +99 -0
- data/test/test_pass.rb +42 -0
- data/test/test_password.rb +101 -0
- data/test/test_pg.rb +673 -0
- data/test/test_presence.rb +374 -0
- data/test/test_presence_pg.rb +309 -0
- data/test/test_proxy.rb +556 -0
- data/test/test_proxy_dsl.rb +119 -0
- data/test/test_proxy_streaming.rb +146 -0
- data/test/test_real_world.rb +397 -0
- data/test/test_regex_routes.rb +52 -0
- data/test/test_request_methods.rb +102 -0
- data/test/test_response.rb +123 -0
- data/test/test_routing.rb +109 -0
- data/test/test_scheduler.rb +153 -0
- data/test/test_security.rb +72 -0
- data/test/test_server_scheduled.rb +56 -0
- data/test/test_sessions.rb +59 -0
- data/test/test_shell.rb +54 -0
- data/test/test_sqlite.rb +148 -0
- data/test/test_sqlite_cached.rb +171 -0
- data/test/test_static.rb +57 -0
- data/test/test_streaming.rb +96 -0
- data/test/test_unsupported.rb +32 -0
- data/test/test_websocket.rb +152 -0
- data/test/test_websocket_echo.rb +138 -0
- data/test/views/greet.erb +5 -0
- data/test/views/hello.erb +5 -0
- data/test/views/list.erb +5 -0
- data/test/views/m_ivars.mustache +3 -0
- data/test/views/m_simple.mustache +4 -0
- data/test/views/mixed.erb +3 -0
- metadata +264 -0
data/test/test_sqlite.rb
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
require_relative "helper"
|
|
2
|
+
|
|
3
|
+
# Tep::SQLite -- a thin libsqlite3 binding wired through spinel's
|
|
4
|
+
# FFI DSL. Tests cover: basic CRUD, parameterised insert + select,
|
|
5
|
+
# multi-row iteration, last_rowid, and a per-test temp .db file
|
|
6
|
+
# so test order is irrelevant.
|
|
7
|
+
class TestSqlite < TepTest
|
|
8
|
+
TMP_DB = "/tmp/tep_test_#{$$}.db"
|
|
9
|
+
|
|
10
|
+
app_source <<~RB
|
|
11
|
+
require 'sinatra'
|
|
12
|
+
|
|
13
|
+
on_start do
|
|
14
|
+
db = Tep::SQLite.new
|
|
15
|
+
if db.open("#{TMP_DB}")
|
|
16
|
+
db.exec("CREATE TABLE IF NOT EXISTS notes (id INTEGER PRIMARY KEY, body TEXT)")
|
|
17
|
+
db.exec("DELETE FROM notes")
|
|
18
|
+
db.prepare("INSERT INTO notes (body) VALUES (?)")
|
|
19
|
+
db.bind_str(1, "first note")
|
|
20
|
+
db.step
|
|
21
|
+
db.reset
|
|
22
|
+
db.bind_str(1, "second note")
|
|
23
|
+
db.step
|
|
24
|
+
db.reset
|
|
25
|
+
db.bind_str(1, "third note")
|
|
26
|
+
db.step
|
|
27
|
+
db.finalize
|
|
28
|
+
db.close
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
get '/note/:id' do
|
|
33
|
+
db = Tep::SQLite.new
|
|
34
|
+
db.open("#{TMP_DB}")
|
|
35
|
+
body = db.first_str("SELECT body FROM notes WHERE id = ?", params[:id])
|
|
36
|
+
db.close
|
|
37
|
+
"id=" + params[:id] + " body=" + body
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
get '/notes' do
|
|
41
|
+
db = Tep::SQLite.new
|
|
42
|
+
db.open("#{TMP_DB}")
|
|
43
|
+
out = ""
|
|
44
|
+
db.prepare("SELECT id, body FROM notes ORDER BY id")
|
|
45
|
+
while db.step == 1
|
|
46
|
+
out = out + db.col_int(0).to_s + ":" + db.col_str(1) + "\\n"
|
|
47
|
+
end
|
|
48
|
+
db.finalize
|
|
49
|
+
db.close
|
|
50
|
+
out
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
get '/count' do
|
|
54
|
+
db = Tep::SQLite.new
|
|
55
|
+
db.open("#{TMP_DB}")
|
|
56
|
+
n = db.first_int("SELECT count(*) FROM notes", "")
|
|
57
|
+
db.close
|
|
58
|
+
"count=" + n.to_s
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
post '/notes' do
|
|
62
|
+
db = Tep::SQLite.new
|
|
63
|
+
db.open("#{TMP_DB}")
|
|
64
|
+
db.prepare("INSERT INTO notes (body) VALUES (?)")
|
|
65
|
+
db.bind_str(1, params[:body])
|
|
66
|
+
db.step
|
|
67
|
+
db.finalize
|
|
68
|
+
id = db.last_rowid
|
|
69
|
+
db.close
|
|
70
|
+
"inserted=" + id.to_s
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# 64-bit round-trip (issue #171): bind a value > 2^31 via bind_int,
|
|
74
|
+
# read it back via col_int. With the old 32-bit path this wrapped
|
|
75
|
+
# negative (3427544687 -> -867422609); now it must survive intact.
|
|
76
|
+
get '/bigint' do
|
|
77
|
+
db = Tep::SQLite.new
|
|
78
|
+
db.open("#{TMP_DB}")
|
|
79
|
+
db.exec("CREATE TABLE IF NOT EXISTS bignums (n INTEGER)")
|
|
80
|
+
db.exec("DELETE FROM bignums")
|
|
81
|
+
db.prepare("INSERT INTO bignums (n) VALUES (?)")
|
|
82
|
+
db.bind_int(1, 3427544687)
|
|
83
|
+
db.step
|
|
84
|
+
db.finalize
|
|
85
|
+
db.prepare("SELECT n FROM bignums LIMIT 1")
|
|
86
|
+
db.step
|
|
87
|
+
n = db.col_int(0)
|
|
88
|
+
db.finalize
|
|
89
|
+
db.close
|
|
90
|
+
"n=" + n.to_s
|
|
91
|
+
end
|
|
92
|
+
RB
|
|
93
|
+
|
|
94
|
+
Minitest.after_run do
|
|
95
|
+
File.unlink(TMP_DB) if File.exist?(TMP_DB)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def test_first_str_with_param
|
|
99
|
+
res = get("/note/1")
|
|
100
|
+
assert_equal "200", res.code
|
|
101
|
+
assert_equal "id=1 body=first note", res.body.strip
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def test_first_str_missing_row_returns_empty
|
|
105
|
+
res = get("/note/9999")
|
|
106
|
+
assert_equal "200", res.code
|
|
107
|
+
assert_equal "id=9999 body=", res.body.strip
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Tests that mutate are written to be order-independent: minitest
|
|
111
|
+
# randomises seed-shuffled order and the test app boots once per
|
|
112
|
+
# class (on_start runs once), so reads-then-writes-then-reads in
|
|
113
|
+
# an arbitrary order all need to make sense.
|
|
114
|
+
|
|
115
|
+
def test_iterate_all_rows_has_seeded_bodies
|
|
116
|
+
res = get("/notes")
|
|
117
|
+
assert_equal "200", res.code
|
|
118
|
+
body = res.body
|
|
119
|
+
assert_match(/:first note$/, body)
|
|
120
|
+
assert_match(/:second note$/, body)
|
|
121
|
+
assert_match(/:third note$/, body)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def test_first_int_returns_at_least_seed_count
|
|
125
|
+
res = get("/count")
|
|
126
|
+
assert_equal "200", res.code
|
|
127
|
+
n = res.body.strip.split("=").last.to_i
|
|
128
|
+
assert n >= 3, "expected count >= 3, got #{n}"
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def test_insert_and_last_rowid_round_trips
|
|
132
|
+
res = post("/notes", "body=via-test")
|
|
133
|
+
assert_equal "200", res.code
|
|
134
|
+
inserted_id = res.body.strip.split("=").last.to_i
|
|
135
|
+
assert inserted_id >= 1, "expected a positive rowid, got #{inserted_id}"
|
|
136
|
+
|
|
137
|
+
res2 = get("/note/#{inserted_id}")
|
|
138
|
+
assert_match(/body=via-test/, res2.body)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def test_bind_and_col_int_round_trip_64bit
|
|
142
|
+
# 3,427,544,687 > 2^31-1. The old 32-bit bind/col path truncated it
|
|
143
|
+
# to -867,422,609 (issue #171); 64-bit bind_int/col_int preserve it.
|
|
144
|
+
res = get("/bigint")
|
|
145
|
+
assert_equal "200", res.code
|
|
146
|
+
assert_equal "n=3427544687", res.body
|
|
147
|
+
end
|
|
148
|
+
end
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
require_relative "helper"
|
|
2
|
+
|
|
3
|
+
# Tep::SQLite#prepare_cached -- cache hit / miss / reset-on-finalize
|
|
4
|
+
# semantics. Backs the perf-leverage win documented in issue #75.
|
|
5
|
+
class TestSqliteCached < TepTest
|
|
6
|
+
TMP_DB = "/tmp/tep_test_cache_#{$$}.db"
|
|
7
|
+
|
|
8
|
+
app_source <<~RB
|
|
9
|
+
require 'sinatra'
|
|
10
|
+
|
|
11
|
+
on_start do
|
|
12
|
+
db = Tep::SQLite.new
|
|
13
|
+
if db.open("#{TMP_DB}")
|
|
14
|
+
db.exec("CREATE TABLE IF NOT EXISTS items (id INTEGER PRIMARY KEY, name TEXT)")
|
|
15
|
+
db.exec("DELETE FROM items")
|
|
16
|
+
db.prepare("INSERT INTO items (name) VALUES (?)")
|
|
17
|
+
db.bind_str(1, "alpha")
|
|
18
|
+
db.step
|
|
19
|
+
db.reset
|
|
20
|
+
db.bind_str(1, "beta")
|
|
21
|
+
db.step
|
|
22
|
+
db.reset
|
|
23
|
+
db.bind_str(1, "gamma")
|
|
24
|
+
db.step
|
|
25
|
+
db.finalize
|
|
26
|
+
db.close
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Repeated prepare_cached with the SAME sql + DIFFERENT
|
|
31
|
+
# bindings -- the cache reuse path must reset+clear_bindings
|
|
32
|
+
# so the second call's binding wins.
|
|
33
|
+
get '/cached_rebind' do
|
|
34
|
+
db = Tep::SQLite.new
|
|
35
|
+
db.open("#{TMP_DB}")
|
|
36
|
+
sql = "SELECT name FROM items WHERE id = ?"
|
|
37
|
+
out = ""
|
|
38
|
+
i = 1
|
|
39
|
+
while i <= 3
|
|
40
|
+
db.prepare_cached(sql)
|
|
41
|
+
db.bind_int(1, i)
|
|
42
|
+
if db.step == 1
|
|
43
|
+
if out.length > 0
|
|
44
|
+
out = out + ","
|
|
45
|
+
end
|
|
46
|
+
out = out + db.col_str(0)
|
|
47
|
+
end
|
|
48
|
+
db.finalize
|
|
49
|
+
i += 1
|
|
50
|
+
end
|
|
51
|
+
db.close
|
|
52
|
+
out
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Mix prepare + prepare_cached + prepare -- exercises both
|
|
56
|
+
# paths in alternation. Uncached prepare after a cached one
|
|
57
|
+
# must release the cached cursor via reset (NOT finalize),
|
|
58
|
+
# leaving the cache slot valid for the next prepare_cached.
|
|
59
|
+
get '/mixed_prepare' do
|
|
60
|
+
db = Tep::SQLite.new
|
|
61
|
+
db.open("#{TMP_DB}")
|
|
62
|
+
cached_sql = "SELECT name FROM items WHERE id = ?"
|
|
63
|
+
# First cached hit (cache miss -> populate slot)
|
|
64
|
+
db.prepare_cached(cached_sql)
|
|
65
|
+
db.bind_int(1, 1)
|
|
66
|
+
db.step
|
|
67
|
+
first = db.col_str(0)
|
|
68
|
+
db.finalize
|
|
69
|
+
# Uncached prepare in between
|
|
70
|
+
db.prepare("SELECT name FROM items WHERE id = ?")
|
|
71
|
+
db.bind_int(1, 2)
|
|
72
|
+
db.step
|
|
73
|
+
middle = db.col_str(0)
|
|
74
|
+
db.finalize
|
|
75
|
+
# Cached hit again (slot still alive)
|
|
76
|
+
db.prepare_cached(cached_sql)
|
|
77
|
+
db.bind_int(1, 3)
|
|
78
|
+
db.step
|
|
79
|
+
last = db.col_str(0)
|
|
80
|
+
db.finalize
|
|
81
|
+
db.close
|
|
82
|
+
first + "|" + middle + "|" + last
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Cache survives across a different db (per-handle scope) --
|
|
86
|
+
# the cached_sql here belongs to db1; db2 with the same SQL
|
|
87
|
+
# gets a separate cache slot.
|
|
88
|
+
get '/per_handle' do
|
|
89
|
+
sql = "SELECT name FROM items WHERE id = ?"
|
|
90
|
+
db1 = Tep::SQLite.new
|
|
91
|
+
db1.open("#{TMP_DB}")
|
|
92
|
+
db1.prepare_cached(sql)
|
|
93
|
+
db1.bind_int(1, 1)
|
|
94
|
+
db1.step
|
|
95
|
+
r1 = db1.col_str(0)
|
|
96
|
+
db1.finalize
|
|
97
|
+
db1.close # close while a cache slot still exists for this handle
|
|
98
|
+
|
|
99
|
+
db2 = Tep::SQLite.new
|
|
100
|
+
db2.open("#{TMP_DB}")
|
|
101
|
+
db2.prepare_cached(sql)
|
|
102
|
+
db2.bind_int(1, 2)
|
|
103
|
+
db2.step
|
|
104
|
+
r2 = db2.col_str(0)
|
|
105
|
+
db2.finalize
|
|
106
|
+
db2.close
|
|
107
|
+
r1 + "|" + r2
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Smoke: a tight loop of cached calls works without leaks --
|
|
111
|
+
# exercises the cache hit path repeatedly with the same SQL.
|
|
112
|
+
get '/loop_count' do
|
|
113
|
+
db = Tep::SQLite.new
|
|
114
|
+
db.open("#{TMP_DB}")
|
|
115
|
+
sql = "SELECT count(*) FROM items"
|
|
116
|
+
n = 0
|
|
117
|
+
i = 0
|
|
118
|
+
while i < 50
|
|
119
|
+
db.prepare_cached(sql)
|
|
120
|
+
if db.step == 1
|
|
121
|
+
n = db.col_int(0)
|
|
122
|
+
end
|
|
123
|
+
db.finalize
|
|
124
|
+
i += 1
|
|
125
|
+
end
|
|
126
|
+
db.close
|
|
127
|
+
"loops=" + i.to_s + " count=" + n.to_s
|
|
128
|
+
end
|
|
129
|
+
RB
|
|
130
|
+
|
|
131
|
+
Minitest.after_run do
|
|
132
|
+
File.unlink(TMP_DB) if File.exist?(TMP_DB)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def test_cache_hit_rebinds_correctly
|
|
136
|
+
# Same SQL, three different bindings, three different rows.
|
|
137
|
+
# If clear_bindings didn't fire, the int param would leak
|
|
138
|
+
# across calls and rows would repeat.
|
|
139
|
+
res = get("/cached_rebind")
|
|
140
|
+
assert_equal "200", res.code
|
|
141
|
+
assert_equal "alpha,beta,gamma", res.body
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def test_mixed_prepare_paths_coexist
|
|
145
|
+
# The uncached prepare in the middle must not invalidate the
|
|
146
|
+
# cached slot for the surrounding prepare_cached calls.
|
|
147
|
+
res = get("/mixed_prepare")
|
|
148
|
+
assert_equal "200", res.code
|
|
149
|
+
assert_equal "alpha|beta|gamma", res.body
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def test_close_finalizes_cached_stmts_for_handle
|
|
153
|
+
# /per_handle opens db1, caches a stmt, closes db1, opens db2,
|
|
154
|
+
# caches the same SQL (separate slot), closes db2. If close()
|
|
155
|
+
# didn't finalize db1's cached stmt, sqlite3_close would
|
|
156
|
+
# SQLITE_BUSY and the second open would land an invalid handle.
|
|
157
|
+
res = get("/per_handle")
|
|
158
|
+
assert_equal "200", res.code
|
|
159
|
+
assert_equal "alpha|beta", res.body
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def test_loop_of_cached_calls_no_leak
|
|
163
|
+
# 50 reuses of the same cached SQL. If the cache flag handling
|
|
164
|
+
# was wrong, sqlite would either leak or error out around
|
|
165
|
+
# iteration 16+ (SQLITE_MAX_STATEMENT_PARAMETERS / stack
|
|
166
|
+
# exhaustion).
|
|
167
|
+
res = get("/loop_count")
|
|
168
|
+
assert_equal "200", res.code
|
|
169
|
+
assert_match(/loops=50 count=3/, res.body)
|
|
170
|
+
end
|
|
171
|
+
end
|
data/test/test_static.rb
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
require_relative "helper"
|
|
2
|
+
|
|
3
|
+
# Static file serving + custom 404 handler.
|
|
4
|
+
class TestStatic < TepTest
|
|
5
|
+
app_source <<~RB
|
|
6
|
+
set :public_dir, '#{File.expand_path("../public", __dir__)}'
|
|
7
|
+
|
|
8
|
+
not_found do
|
|
9
|
+
"tep-404 " + request.path
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
get '/' do
|
|
13
|
+
"root"
|
|
14
|
+
end
|
|
15
|
+
RB
|
|
16
|
+
|
|
17
|
+
def test_static_text_file
|
|
18
|
+
res = get("/hello.txt")
|
|
19
|
+
assert_equal "200", res.code
|
|
20
|
+
assert_match(/text\/plain/, res["content-type"])
|
|
21
|
+
assert_match(/static file serving/, res.body)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def test_static_css
|
|
25
|
+
res = get("/style.css")
|
|
26
|
+
assert_equal "200", res.code
|
|
27
|
+
assert_equal "text/css", res["content-type"]
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def test_static_x_tep_marker
|
|
31
|
+
res = get("/hello.txt")
|
|
32
|
+
assert_equal "1", res["x-tep-static"]
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def test_404_for_unknown_path
|
|
36
|
+
res = get("/no-such-file.txt")
|
|
37
|
+
assert_equal "404", res.code
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def test_custom_404_body
|
|
41
|
+
res = get("/no-such-file.txt")
|
|
42
|
+
assert_match(/tep-404/, res.body)
|
|
43
|
+
assert_match(%r{/no-such-file\.txt}, res.body)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def test_path_traversal_rejected
|
|
47
|
+
res = get("/../etc/passwd")
|
|
48
|
+
assert_equal "404", res.code
|
|
49
|
+
refute_match(/root:/, res.body)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def test_get_route_still_wins_over_static
|
|
53
|
+
res = get("/")
|
|
54
|
+
assert_equal "200", res.code
|
|
55
|
+
assert_equal "root", res.body
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
require_relative "helper"
|
|
2
|
+
|
|
3
|
+
class TestStreaming < TepTest
|
|
4
|
+
app_source <<~RB
|
|
5
|
+
require 'sinatra'
|
|
6
|
+
|
|
7
|
+
class Ticks < Tep::Streamer
|
|
8
|
+
def pump(out)
|
|
9
|
+
out.write("data: 1\\n\\n")
|
|
10
|
+
out.write("data: 2\\n\\n")
|
|
11
|
+
out.write("data: 3\\n\\n")
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
get '/stream' do
|
|
16
|
+
stream Ticks.new
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
get '/normal' do
|
|
20
|
+
"regular response"
|
|
21
|
+
end
|
|
22
|
+
RB
|
|
23
|
+
|
|
24
|
+
def test_stream_uses_chunked_encoding
|
|
25
|
+
res = get("/stream")
|
|
26
|
+
assert_equal "200", res.code
|
|
27
|
+
assert_equal "chunked", res["transfer-encoding"]
|
|
28
|
+
assert_nil res["content-length"]
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def test_stream_default_content_type
|
|
32
|
+
res = get("/stream")
|
|
33
|
+
assert_match(%r{text/event-stream}, res["content-type"])
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def test_stream_emits_all_chunks
|
|
37
|
+
res = get("/stream")
|
|
38
|
+
assert_equal "data: 1\n\ndata: 2\n\ndata: 3\n\n", res.body
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def test_normal_route_still_buffered
|
|
42
|
+
res = get("/normal")
|
|
43
|
+
assert_equal "200", res.code
|
|
44
|
+
assert_nil res["transfer-encoding"]
|
|
45
|
+
assert_equal "16", res["content-length"]
|
|
46
|
+
assert_equal "regular response", res.body
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Same streaming surface under Tep::Server::Scheduled. Regression
|
|
51
|
+
# guard for #90: the scheduled server's write_response had no
|
|
52
|
+
# res.streaming branch, so streamed responses came back with
|
|
53
|
+
# Content-Length: 0 and an empty body (the Streamer never pumped).
|
|
54
|
+
class TestStreamingScheduled < TepTest
|
|
55
|
+
app_source <<~RB
|
|
56
|
+
require 'sinatra'
|
|
57
|
+
|
|
58
|
+
set :scheduler, :scheduled
|
|
59
|
+
set :workers, 1
|
|
60
|
+
|
|
61
|
+
class Ticks < Tep::Streamer
|
|
62
|
+
def pump(out)
|
|
63
|
+
out.write("data: 1\\n\\n")
|
|
64
|
+
out.write("data: 2\\n\\n")
|
|
65
|
+
out.write("data: 3\\n\\n")
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
get '/stream' do
|
|
70
|
+
stream Ticks.new
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
get '/normal' do
|
|
74
|
+
"regular response"
|
|
75
|
+
end
|
|
76
|
+
RB
|
|
77
|
+
|
|
78
|
+
def test_scheduled_stream_uses_chunked_encoding
|
|
79
|
+
res = get("/stream")
|
|
80
|
+
assert_equal "200", res.code
|
|
81
|
+
assert_equal "chunked", res["transfer-encoding"]
|
|
82
|
+
assert_nil res["content-length"]
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def test_scheduled_stream_emits_all_chunks
|
|
86
|
+
res = get("/stream")
|
|
87
|
+
assert_equal "data: 1\n\ndata: 2\n\ndata: 3\n\n", res.body
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def test_scheduled_normal_route_still_buffered
|
|
91
|
+
res = get("/normal")
|
|
92
|
+
assert_equal "200", res.code
|
|
93
|
+
assert_nil res["transfer-encoding"]
|
|
94
|
+
assert_equal "regular response", res.body
|
|
95
|
+
end
|
|
96
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
require_relative "helper"
|
|
2
|
+
|
|
3
|
+
# Features Sinatra has and tep doesn't yet. Tests that have moved
|
|
4
|
+
# into the supported column live in their own file (test_cookies.rb,
|
|
5
|
+
# test_sessions.rb, test_streaming.rb, test_regex_routes.rb,
|
|
6
|
+
# test_modular.rb, test_erb.rb, test_misc_v02.rb, test_pass.rb,
|
|
7
|
+
# test_multi_filters.rb, test_optional_segments.rb,
|
|
8
|
+
# test_request_methods.rb).
|
|
9
|
+
class TestUnsupported < TepTest
|
|
10
|
+
app_source <<~RB
|
|
11
|
+
get '/' do
|
|
12
|
+
"tep skip stub"
|
|
13
|
+
end
|
|
14
|
+
RB
|
|
15
|
+
|
|
16
|
+
def test_haml_templates
|
|
17
|
+
skip "Haml -- depends on a gem; out of scope for spinel-AOT"
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def test_helpers_block
|
|
21
|
+
skip "`helpers do ... end` -- closures not first-class in spinel; would need translator support to define methods on Tep::Handler"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def test_request_ip
|
|
25
|
+
skip "request.ip / request.remote_ip -- needs an sphttp_accept_with_peer C helper. Other Rack::Request methods (host, user_agent, scheme, ssl?, etc.) are supported -- see test_request_methods.rb."
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# `@ivar` template locals are now supported -- see test_erb_ivars.rb.
|
|
29
|
+
|
|
30
|
+
# send_file, configure, pass, multiple filters, optional segments
|
|
31
|
+
# have all moved into supported and have their own test files.
|
|
32
|
+
end
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# Tep::WebSocket frame + handshake tests via a live tep app.
|
|
2
|
+
# RFC 6455 reference vectors covered:
|
|
3
|
+
# - Frame encode for text + binary + close + ping + pong
|
|
4
|
+
# - Handshake accept-key compute (the §1.3 worked example)
|
|
5
|
+
# - Handshake header parsing (Upgrade / Connection / Version / Key)
|
|
6
|
+
# - Subprotocol negotiation parser
|
|
7
|
+
#
|
|
8
|
+
# Integration coverage (Driver + Connection over a real socket
|
|
9
|
+
# round-trip) lives separately and isn't in this commit -- it needs
|
|
10
|
+
# a Ruby-side WS client to wire up, which is its own dependency.
|
|
11
|
+
require_relative "helper"
|
|
12
|
+
|
|
13
|
+
class TestWebSocket < TepTest
|
|
14
|
+
app_source <<~'RB'
|
|
15
|
+
require "sinatra"
|
|
16
|
+
|
|
17
|
+
# Encode a small text frame and dump it as a hex string so the
|
|
18
|
+
# MRI test can check byte-by-byte. Server frames are unmasked
|
|
19
|
+
# so byte 0 = 0x81 (FIN + opcode 1), byte 1 = payload length.
|
|
20
|
+
def hex_of(s)
|
|
21
|
+
out = ""
|
|
22
|
+
i = 0
|
|
23
|
+
while i < s.length
|
|
24
|
+
b = s[i].ord & 0xff
|
|
25
|
+
out = out + ((b / 16) < 10 ? (b / 16 + 48).chr : (b / 16 + 87).chr)
|
|
26
|
+
out = out + ((b % 16) < 10 ? (b % 16 + 48).chr : (b % 16 + 87).chr)
|
|
27
|
+
i += 1
|
|
28
|
+
end
|
|
29
|
+
out
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
get '/frame/text_small' do
|
|
33
|
+
f = Tep::WebSocket::Frame.new(true, Tep::WebSocket::OPCODE_TEXT, "Hello")
|
|
34
|
+
hex_of(f.encode_unmasked)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
get '/frame/binary_short' do
|
|
38
|
+
f = Tep::WebSocket::Frame.new(true, Tep::WebSocket::OPCODE_BINARY, "abc")
|
|
39
|
+
hex_of(f.encode_unmasked)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
get '/frame/text_extended_16' do
|
|
43
|
+
payload = ""
|
|
44
|
+
i = 0
|
|
45
|
+
while i < 200
|
|
46
|
+
payload = payload + "x"
|
|
47
|
+
i += 1
|
|
48
|
+
end
|
|
49
|
+
f = Tep::WebSocket::Frame.new(true, Tep::WebSocket::OPCODE_TEXT, payload)
|
|
50
|
+
# First 4 bytes only -- enough to verify the 16-bit length
|
|
51
|
+
# encoding (0x81 0x7e 0x00 0xc8 = FIN+text, 126 marker, 200 in 16-bit).
|
|
52
|
+
hex_of(f.encode_unmasked[0, 4])
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
get '/frame/close_with_code' do
|
|
56
|
+
body = Tep::WebSocket::Driver.encode_close_payload(1000, "bye")
|
|
57
|
+
f = Tep::WebSocket::Frame.new(true, Tep::WebSocket::OPCODE_CLOSE, body)
|
|
58
|
+
hex_of(f.encode_unmasked)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Handshake accept key for the RFC 6455 §1.3 worked example.
|
|
62
|
+
get '/handshake/accept_key' do
|
|
63
|
+
Crypto.sp_crypto_websocket_accept("dGhlIHNhbXBsZSBub25jZQ==")
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
get '/handshake/build_response_with_protocol' do
|
|
67
|
+
Tep::WebSocket::Handshake.build_response("s3pPLMBiTxaQ9kYGzzhZRbK+xOo=", "chat")
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
get '/handshake/build_response_no_protocol' do
|
|
71
|
+
Tep::WebSocket::Handshake.build_response("s3pPLMBiTxaQ9kYGzzhZRbK+xOo=", "")
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
get '/handshake/split_csv' do
|
|
75
|
+
parts = Tep::WebSocket::Handshake.split_csv("a, b ,c")
|
|
76
|
+
parts.join("|")
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
get '/frame/reserved_opcode_rejects' do
|
|
80
|
+
Tep::WebSocket::Frame.reserved_opcode?(5) ? "yes" : "no"
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
get '/frame/control_opcode_classifies' do
|
|
84
|
+
a = Tep::WebSocket::Frame.control_opcode?(Tep::WebSocket::OPCODE_CLOSE) ? "y" : "n"
|
|
85
|
+
b = Tep::WebSocket::Frame.control_opcode?(Tep::WebSocket::OPCODE_TEXT) ? "y" : "n"
|
|
86
|
+
a + b
|
|
87
|
+
end
|
|
88
|
+
RB
|
|
89
|
+
|
|
90
|
+
def test_frame_text_small_encode
|
|
91
|
+
res = get("/frame/text_small")
|
|
92
|
+
# 0x81 = FIN + opcode 1 (text), 0x05 = unmasked, 5-byte payload,
|
|
93
|
+
# then "Hello" = 48 65 6c 6c 6f.
|
|
94
|
+
assert_equal "8105" + "48656c6c6f", res.body
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def test_frame_binary_short_encode
|
|
98
|
+
res = get("/frame/binary_short")
|
|
99
|
+
# 0x82 = FIN + opcode 2 (binary), 0x03 = unmasked 3-byte payload,
|
|
100
|
+
# then "abc" = 61 62 63.
|
|
101
|
+
assert_equal "8203" + "616263", res.body
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def test_frame_16bit_length_marker
|
|
105
|
+
res = get("/frame/text_extended_16")
|
|
106
|
+
# 0x81 = FIN + text, 0x7e = 126 marker (16-bit length follows),
|
|
107
|
+
# 0x00 0xc8 = 200 big-endian.
|
|
108
|
+
assert_equal "817e00c8", res.body
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def test_frame_close_with_code_and_reason
|
|
112
|
+
res = get("/frame/close_with_code")
|
|
113
|
+
# 0x88 = FIN + opcode 8 (close), 0x05 = 5-byte payload,
|
|
114
|
+
# 0x03 0xe8 = 1000 big-endian, then "bye" = 62 79 65.
|
|
115
|
+
assert_equal "8805" + "03e8" + "627965", res.body
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def test_handshake_accept_key_rfc_6455_vector
|
|
119
|
+
res = get("/handshake/accept_key")
|
|
120
|
+
# RFC 6455 §1.3 worked example.
|
|
121
|
+
assert_equal "s3pPLMBiTxaQ9kYGzzhZRbK+xOo=", res.body
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def test_handshake_response_with_protocol
|
|
125
|
+
res = get("/handshake/build_response_with_protocol")
|
|
126
|
+
assert_includes res.body, "HTTP/1.1 101 Switching Protocols"
|
|
127
|
+
assert_includes res.body, "Upgrade: websocket"
|
|
128
|
+
assert_includes res.body, "Connection: Upgrade"
|
|
129
|
+
assert_includes res.body, "Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo="
|
|
130
|
+
assert_includes res.body, "Sec-WebSocket-Protocol: chat"
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def test_handshake_response_omits_protocol_when_empty
|
|
134
|
+
res = get("/handshake/build_response_no_protocol")
|
|
135
|
+
refute_includes res.body, "Sec-WebSocket-Protocol"
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def test_handshake_split_csv_trims_whitespace
|
|
139
|
+
res = get("/handshake/split_csv")
|
|
140
|
+
assert_equal "a|b|c", res.body
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def test_frame_reserved_opcode_predicate
|
|
144
|
+
res = get("/frame/reserved_opcode_rejects")
|
|
145
|
+
assert_equal "yes", res.body
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def test_frame_control_opcode_predicate
|
|
149
|
+
res = get("/frame/control_opcode_classifies")
|
|
150
|
+
assert_equal "yn", res.body
|
|
151
|
+
end
|
|
152
|
+
end
|