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
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
## Thing
|
|
2
|
+
# RESTful API example
|
|
3
|
+
# - manages single resource called Thing /thing
|
|
4
|
+
# - all results (including error messages) returned as JSON (Accept header)
|
|
5
|
+
|
|
6
|
+
## requires
|
|
7
|
+
require 'sinatra'
|
|
8
|
+
require 'json'
|
|
9
|
+
require 'time'
|
|
10
|
+
require 'pp'
|
|
11
|
+
|
|
12
|
+
### datamapper requires
|
|
13
|
+
require 'data_mapper'
|
|
14
|
+
require 'dm-types'
|
|
15
|
+
require 'dm-timestamps'
|
|
16
|
+
require 'dm-validations'
|
|
17
|
+
|
|
18
|
+
## model
|
|
19
|
+
### helper modules
|
|
20
|
+
#### StandardProperties
|
|
21
|
+
module StandardProperties
|
|
22
|
+
def self.included(other)
|
|
23
|
+
other.class_eval do
|
|
24
|
+
property :id, other::Serial
|
|
25
|
+
# property :created_at, DateTime
|
|
26
|
+
# property :updated_at, DateTime
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
#### Validations
|
|
32
|
+
module Validations
|
|
33
|
+
def valid_id?(id)
|
|
34
|
+
id && id.to_s =~ /^\d+$/
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
### Thing
|
|
39
|
+
class Thing
|
|
40
|
+
include DataMapper::Resource
|
|
41
|
+
include StandardProperties
|
|
42
|
+
extend Validations
|
|
43
|
+
|
|
44
|
+
property :name, String, :required => true
|
|
45
|
+
property :status, String
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
## set up db
|
|
49
|
+
env = ENV["RACK_ENV"]
|
|
50
|
+
puts "RACK_ENV: #{env}"
|
|
51
|
+
if env.to_s.strip == ""
|
|
52
|
+
abort "Must define RACK_ENV (used for db name)"
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
case env
|
|
56
|
+
when "test"
|
|
57
|
+
DataMapper.setup(:default, "sqlite3::memory:")
|
|
58
|
+
else
|
|
59
|
+
DataMapper.setup(:default, "sqlite3:#{ENV["RACK_ENV"]}.db")
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
## create schema if necessary
|
|
63
|
+
DataMapper.auto_upgrade!
|
|
64
|
+
|
|
65
|
+
## logger
|
|
66
|
+
def logger
|
|
67
|
+
@logger ||= Logger.new(STDOUT)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
## ThingResource application
|
|
71
|
+
class ThingResource < Sinatra::Base
|
|
72
|
+
set :methodoverride, true
|
|
73
|
+
|
|
74
|
+
## helpers
|
|
75
|
+
|
|
76
|
+
def self.put_or_post(*a, &b)
|
|
77
|
+
put *a, &b
|
|
78
|
+
post *a, &b
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
helpers do
|
|
82
|
+
def json_status(code, reason)
|
|
83
|
+
status code
|
|
84
|
+
{
|
|
85
|
+
:status => code,
|
|
86
|
+
:reason => reason
|
|
87
|
+
}.to_json
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def accept_params(params, *fields)
|
|
91
|
+
h = { }
|
|
92
|
+
fields.each do |name|
|
|
93
|
+
h[name] = params[name] if params[name]
|
|
94
|
+
end
|
|
95
|
+
h
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
## GET /thing - return all things
|
|
100
|
+
get "/thing/?", :provides => :json do
|
|
101
|
+
content_type :json
|
|
102
|
+
|
|
103
|
+
if things = Thing.all
|
|
104
|
+
things.to_json
|
|
105
|
+
else
|
|
106
|
+
json_status 404, "Not found"
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
## GET /thing/:id - return thing with specified id
|
|
111
|
+
get "/thing/:id", :provides => :json do
|
|
112
|
+
content_type :json
|
|
113
|
+
|
|
114
|
+
# check that :id param is an integer
|
|
115
|
+
if Thing.valid_id?(params[:id])
|
|
116
|
+
if thing = Thing.first(:id => params[:id].to_i)
|
|
117
|
+
thing.to_json
|
|
118
|
+
else
|
|
119
|
+
json_status 404, "Not found"
|
|
120
|
+
end
|
|
121
|
+
else
|
|
122
|
+
# TODO: find better error for this (id not an integer)
|
|
123
|
+
json_status 404, "Not found"
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
## POST /thing/ - create new thing
|
|
128
|
+
post "/thing/?", :provides => :json do
|
|
129
|
+
content_type :json
|
|
130
|
+
|
|
131
|
+
new_params = accept_params(params, :name, :status)
|
|
132
|
+
thing = Thing.new(new_params)
|
|
133
|
+
if thing.save
|
|
134
|
+
headers["Location"] = "/thing/#{thing.id}"
|
|
135
|
+
# http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.5
|
|
136
|
+
status 201 # Created
|
|
137
|
+
thing.to_json
|
|
138
|
+
else
|
|
139
|
+
json_status 400, thing.errors.to_hash
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
## PUT /thing/:id/:status - change a thing's status
|
|
144
|
+
put_or_post "/thing/:id/status/:status", :provides => :json do
|
|
145
|
+
content_type :json
|
|
146
|
+
|
|
147
|
+
if Thing.valid_id?(params[:id])
|
|
148
|
+
if thing = Thing.first(:id => params[:id].to_i)
|
|
149
|
+
thing.status = params[:status]
|
|
150
|
+
if thing.save
|
|
151
|
+
thing.to_json
|
|
152
|
+
else
|
|
153
|
+
json_status 400, thing.errors.to_hash
|
|
154
|
+
end
|
|
155
|
+
else
|
|
156
|
+
json_status 404, "Not found"
|
|
157
|
+
end
|
|
158
|
+
else
|
|
159
|
+
json_status 404, "Not found"
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
## PUT /thing/:id - change or create a thing
|
|
164
|
+
put "/thing/:id", :provides => :json do
|
|
165
|
+
content_type :json
|
|
166
|
+
|
|
167
|
+
new_params = accept_params(params, :name, :status)
|
|
168
|
+
|
|
169
|
+
if Thing.valid_id?(params[:id])
|
|
170
|
+
if thing = Thing.first_or_create(:id => params[:id].to_i)
|
|
171
|
+
thing.attributes = thing.attributes.merge(new_params)
|
|
172
|
+
if thing.save
|
|
173
|
+
thing.to_json
|
|
174
|
+
else
|
|
175
|
+
json_status 400, thing.errors.to_hash
|
|
176
|
+
end
|
|
177
|
+
else
|
|
178
|
+
json_status 404, "Not found"
|
|
179
|
+
end
|
|
180
|
+
else
|
|
181
|
+
json_status 404, "Not found"
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
## DELETE /thing/:id - delete a specific thing
|
|
186
|
+
delete "/thing/:id/?", :provides => :json do
|
|
187
|
+
content_type :json
|
|
188
|
+
|
|
189
|
+
if thing = Thing.first(:id => params[:id].to_i)
|
|
190
|
+
thing.destroy!
|
|
191
|
+
# http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.7
|
|
192
|
+
status 204 # No content
|
|
193
|
+
else
|
|
194
|
+
# http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.1.2
|
|
195
|
+
# Note: section 9.1.2 states:
|
|
196
|
+
# Methods can also have the property of "idempotence" in that
|
|
197
|
+
# (aside from error or expiration issues) the side-effects of
|
|
198
|
+
# N > 0 identical requests is the same as for a single
|
|
199
|
+
# request.
|
|
200
|
+
# i.e that the /side-effects/ are idempotent, not that the
|
|
201
|
+
# result of the /request/ is idempotent, so I think it's correct
|
|
202
|
+
# to return a 404 here.
|
|
203
|
+
json_status 404, "Not found"
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
## misc handlers: error, not_found, etc.
|
|
208
|
+
get "*" do
|
|
209
|
+
status 404
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
put_or_post "*" do
|
|
213
|
+
status 404
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
delete "*" do
|
|
217
|
+
status 404
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
not_found do
|
|
221
|
+
json_status 404, "Not found"
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
error do
|
|
225
|
+
json_status 500, env['sinatra.error'].message
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
end
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# Require the bundler gem and then call Bundler.require to load in all gems
|
|
2
|
+
# listed in Gemfile.
|
|
3
|
+
require 'bundler'
|
|
4
|
+
Bundler.require
|
|
5
|
+
|
|
6
|
+
# Setup DataMapper with a database URL. On Heroku, ENV['DATABASE_URL'] will be
|
|
7
|
+
# set, when working locally this line will fall back to using SQLite in the
|
|
8
|
+
# current directory.
|
|
9
|
+
DataMapper.setup(:default, ENV['DATABASE_URL'] || "sqlite://#{Dir.pwd}/development.sqlite")
|
|
10
|
+
|
|
11
|
+
# Define a simple DataMapper model.
|
|
12
|
+
class Thing
|
|
13
|
+
include DataMapper::Resource
|
|
14
|
+
|
|
15
|
+
property :id, Serial, :key => true
|
|
16
|
+
property :created_at, DateTime
|
|
17
|
+
property :title, String, :length => 255
|
|
18
|
+
property :description, Text
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Finalize the DataMapper models.
|
|
22
|
+
DataMapper.finalize
|
|
23
|
+
|
|
24
|
+
# Tell DataMapper to update the database according to the definitions above.
|
|
25
|
+
DataMapper.auto_upgrade!
|
|
26
|
+
|
|
27
|
+
get '/' do
|
|
28
|
+
send_file './public/index.html'
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Route to show all Things, ordered like a blog
|
|
32
|
+
get '/things' do
|
|
33
|
+
content_type :json
|
|
34
|
+
@things = Thing.all(:order => :created_at.desc)
|
|
35
|
+
|
|
36
|
+
@things.to_json
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# CREATE: Route to create a new Thing
|
|
40
|
+
post '/things' do
|
|
41
|
+
content_type :json
|
|
42
|
+
|
|
43
|
+
# These next commented lines are for if you are using Backbone.js
|
|
44
|
+
# JSON is sent in the body of the http request. We need to parse the body
|
|
45
|
+
# from a string into JSON
|
|
46
|
+
# params_json = JSON.parse(request.body.read)
|
|
47
|
+
|
|
48
|
+
# If you are using jQuery's ajax functions, the data goes through in the
|
|
49
|
+
# params.
|
|
50
|
+
@thing = Thing.new(params)
|
|
51
|
+
|
|
52
|
+
if @thing.save
|
|
53
|
+
@thing.to_json
|
|
54
|
+
else
|
|
55
|
+
halt 500
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# READ: Route to show a specific Thing based on its `id`
|
|
60
|
+
get '/things/:id' do
|
|
61
|
+
content_type :json
|
|
62
|
+
@thing = Thing.get(params[:id].to_i)
|
|
63
|
+
|
|
64
|
+
if @thing
|
|
65
|
+
@thing.to_json
|
|
66
|
+
else
|
|
67
|
+
halt 404
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# UPDATE: Route to update a Thing
|
|
72
|
+
put '/things/:id' do
|
|
73
|
+
content_type :json
|
|
74
|
+
|
|
75
|
+
# These next commented lines are for if you are using Backbone.js
|
|
76
|
+
# JSON is sent in the body of the http request. We need to parse the body
|
|
77
|
+
# from a string into JSON
|
|
78
|
+
# params_json = JSON.parse(request.body.read)
|
|
79
|
+
|
|
80
|
+
# If you are using jQuery's ajax functions, the data goes through in the
|
|
81
|
+
# params.
|
|
82
|
+
|
|
83
|
+
@thing = Thing.get(params[:id].to_i)
|
|
84
|
+
@thing.update(params)
|
|
85
|
+
|
|
86
|
+
if @thing.save
|
|
87
|
+
@thing.to_json
|
|
88
|
+
else
|
|
89
|
+
halt 500
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# DELETE: Route to delete a Thing
|
|
94
|
+
delete '/things/:id/delete' do
|
|
95
|
+
content_type :json
|
|
96
|
+
@thing = Thing.get(params[:id].to_i)
|
|
97
|
+
|
|
98
|
+
if @thing.destroy
|
|
99
|
+
{:success => "ok"}.to_json
|
|
100
|
+
else
|
|
101
|
+
halt 500
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# If there are no Things in the database, add a few.
|
|
106
|
+
if Thing.count == 0
|
|
107
|
+
Thing.create(:title => "Test Thing One", :description => "Sometimes I eat pizza.")
|
|
108
|
+
Thing.create(:title => "Test Thing Two", :description => "Other times I eat cookies.")
|
|
109
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# boilerplate includes
|
|
2
|
+
require 'rubygems'
|
|
3
|
+
require 'bundler'
|
|
4
|
+
|
|
5
|
+
# remainder of includes handled by bundler and the Gemfile
|
|
6
|
+
Bundler.require
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
# define settings
|
|
10
|
+
configure do
|
|
11
|
+
set :version, "0.0.1"
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# define helpers
|
|
15
|
+
helpers do
|
|
16
|
+
def h(text)
|
|
17
|
+
# used to safely escape html
|
|
18
|
+
Rack::Utils.escape_html(text)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# create routes
|
|
23
|
+
get '/' do
|
|
24
|
+
erb :index
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
get '/welcome/:name' do
|
|
28
|
+
@name = params[:name]
|
|
29
|
+
erb :welcomer
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
get '/goodbye/:name' do
|
|
33
|
+
@name = params[:name]
|
|
34
|
+
erb :leaver
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# below here we have templates
|
|
38
|
+
__END__
|
|
39
|
+
@@welcomer
|
|
40
|
+
<h3>Using the "welcomer" inline template</h3>
|
|
41
|
+
<h1>
|
|
42
|
+
|
|
43
|
+
Greetings <%=h @name %>
|
|
44
|
+
</h1>
|
|
45
|
+
<h5>
|
|
46
|
+
Version <%= settings.version %>
|
|
47
|
+
</h5>
|
|
48
|
+
|
|
49
|
+
@@leaver
|
|
50
|
+
<h3>Using the "leaver" inline template</h3>
|
|
51
|
+
<h1>
|
|
52
|
+
Seeya later <%=h @name %>
|
|
53
|
+
</h1>
|
|
54
|
+
<h5>
|
|
55
|
+
Version <%= settings.version %>
|
|
56
|
+
</h5>
|
data/test/run_all.rb
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# run_parallel.rb -- run each test/test_*.rb in its own process, in
|
|
3
|
+
# parallel, and aggregate the per-file summaries into one overall line.
|
|
4
|
+
#
|
|
5
|
+
# Why this exists: `make test` (test/run_all.rb) is serial and runs in
|
|
6
|
+
# one process because the per-class boot harness expects one thread
|
|
7
|
+
# touching state at a time. The wall time is dominated by ~54 test
|
|
8
|
+
# classes each doing a `bin/tep build` (~12 s of spinel codegen per
|
|
9
|
+
# class) sequentially -- around 11 min of pure compilation on the gx10.
|
|
10
|
+
# Separate processes sidestep the threading constraint cleanly: each
|
|
11
|
+
# worker runs one test file (its own minitest), with its own port base,
|
|
12
|
+
# so N classes compile + run concurrently.
|
|
13
|
+
#
|
|
14
|
+
# make test-parallel
|
|
15
|
+
# TEP_TEST_PROCS=8 make test-parallel # cap concurrency (default: Etc.nprocessors)
|
|
16
|
+
#
|
|
17
|
+
# Output: one tick (`✓` / `✗`) per test file with a per-file run/assertion
|
|
18
|
+
# tally, full output dumped for failing files, then one aggregate
|
|
19
|
+
# summary line. Exit 0 iff every file passed.
|
|
20
|
+
require "etc"
|
|
21
|
+
require "shellwords"
|
|
22
|
+
|
|
23
|
+
ROOT = File.expand_path("..", __dir__)
|
|
24
|
+
TESTS = Dir[File.join(ROOT, "test", "test_*.rb")].sort
|
|
25
|
+
PROCS = (ENV["TEP_TEST_PROCS"] || Etc.nprocessors.to_s).to_i
|
|
26
|
+
|
|
27
|
+
# Each test class boots its own app on a port from the harness's
|
|
28
|
+
# next_port counter; spacing 100 per file is comfy headroom (test
|
|
29
|
+
# files have at most a couple of classes).
|
|
30
|
+
PORT_BASE_STEP = 100
|
|
31
|
+
PORT_BASE_START = (ENV["TEP_TEST_PORT_BASE"] || "4900").to_i
|
|
32
|
+
|
|
33
|
+
queue = TESTS.each_with_index.to_a
|
|
34
|
+
mutex = Mutex.new
|
|
35
|
+
results = []
|
|
36
|
+
|
|
37
|
+
workers = PROCS.times.map do
|
|
38
|
+
Thread.new do
|
|
39
|
+
loop do
|
|
40
|
+
job = mutex.synchronize { queue.shift }
|
|
41
|
+
break unless job
|
|
42
|
+
path, idx = job
|
|
43
|
+
port_base = PORT_BASE_START + idx * PORT_BASE_STEP
|
|
44
|
+
env = { "TEP_TEST_PORT_BASE" => port_base.to_s }
|
|
45
|
+
# Honor TEP_SKIP_SPINEL_FRESH if the caller set it (Makefile
|
|
46
|
+
# handles the freshness check once; per-process would be wasteful).
|
|
47
|
+
["TEP_SKIP_SPINEL_FRESH", "SPINEL", "TEP_KEEP_TMP"].each do |k|
|
|
48
|
+
env[k] = ENV[k] if ENV[k]
|
|
49
|
+
end
|
|
50
|
+
output = nil
|
|
51
|
+
start = Time.now
|
|
52
|
+
IO.popen(env, ["ruby", path], err: [:child, :out]) { |io| output = io.read }
|
|
53
|
+
ok = $?.success?
|
|
54
|
+
elapsed = Time.now - start
|
|
55
|
+
mutex.synchronize do
|
|
56
|
+
results << { path: path, ok: ok, output: output, elapsed: elapsed }
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
workers.each(&:join)
|
|
62
|
+
|
|
63
|
+
# Sort so the printed order is deterministic (queue order), not
|
|
64
|
+
# completion order.
|
|
65
|
+
results.sort_by! { |r| TESTS.index(r[:path]) }
|
|
66
|
+
|
|
67
|
+
totals = { runs: 0, assertions: 0, failures: 0, errors: 0, skips: 0 }
|
|
68
|
+
results.each do |r|
|
|
69
|
+
m = r[:output].to_s.match(/(\d+) runs, (\d+) assertions, (\d+) failures, (\d+) errors, (\d+) skips/)
|
|
70
|
+
if m
|
|
71
|
+
totals[:runs] += m[1].to_i
|
|
72
|
+
totals[:assertions] += m[2].to_i
|
|
73
|
+
totals[:failures] += m[3].to_i
|
|
74
|
+
totals[:errors] += m[4].to_i
|
|
75
|
+
totals[:skips] += m[5].to_i
|
|
76
|
+
end
|
|
77
|
+
rel = r[:path].sub(ROOT + "/", "")
|
|
78
|
+
mark = r[:ok] ? "✓" : "✗" # ✓ / ✗
|
|
79
|
+
tally = m ? " #{m[1]}r/#{m[2]}a/#{m[3]}f/#{m[4]}e/#{m[5]}s" : ""
|
|
80
|
+
puts "%s %s%s [%.1fs]" % [mark, rel, tally, r[:elapsed]]
|
|
81
|
+
unless r[:ok]
|
|
82
|
+
puts r[:output]
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
puts ""
|
|
87
|
+
puts "%d runs, %d assertions, %d failures, %d errors, %d skips" % \
|
|
88
|
+
[totals[:runs], totals[:assertions], totals[:failures], totals[:errors], totals[:skips]]
|
|
89
|
+
exit(results.all? { |r| r[:ok] } ? 0 : 1)
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Minimal repro for the Tep::Server::Scheduled segfault under
|
|
2
|
+
# concurrent HTTP/1.1 keep-alive bursts. Stand-alone -- doesn't
|
|
3
|
+
# require PG or any other battery; the segfault reproduces with
|
|
4
|
+
# the plainest possible "hello world" route.
|
|
5
|
+
#
|
|
6
|
+
# Build + run:
|
|
7
|
+
# bin/tep build test/spinel_scheduled_burst_segv_repro.rb -o /tmp/repro
|
|
8
|
+
# /tmp/repro -p 4985 &
|
|
9
|
+
# # Drive it with two concurrent persistent-keepalive clients:
|
|
10
|
+
# ruby -e '
|
|
11
|
+
# require "net/http"; require "uri"
|
|
12
|
+
# uri = URI("http://127.0.0.1:4985/hi")
|
|
13
|
+
# 2.times.map { Thread.new {
|
|
14
|
+
# Net::HTTP.start(uri.host, uri.port) { |h|
|
|
15
|
+
# 100.times { h.request(Net::HTTP::Get.new(uri.path)) }
|
|
16
|
+
# }
|
|
17
|
+
# } }.each(&:join)
|
|
18
|
+
# '
|
|
19
|
+
#
|
|
20
|
+
# Observed (spinel master 6513d2d, 2026-05-20): server dies with
|
|
21
|
+
# SIGSEGV (SEGV_ACCERR) somewhere around request ~60-80. strace
|
|
22
|
+
# shows the segfault happens right after a normal-looking recvfrom
|
|
23
|
+
# returns the HTTP request bytes -- the crash is in the Ruby /
|
|
24
|
+
# spinel runtime between recv and the handler dispatch + write.
|
|
25
|
+
#
|
|
26
|
+
# Default Tep::Server (prefork-blocking) handles the same burst
|
|
27
|
+
# fine; the segfault is specific to Tep::Server::Scheduled.
|
|
28
|
+
require_relative "../lib/tep"
|
|
29
|
+
set :scheduler, :scheduled
|
|
30
|
+
|
|
31
|
+
get '/hi' do
|
|
32
|
+
"hello"
|
|
33
|
+
end
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
require_relative "helper"
|
|
2
|
+
|
|
3
|
+
# examples/api_gateway integration: a non-streaming Tep::Proxy that
|
|
4
|
+
# gates on a capability (before_forward short-circuit), swaps the
|
|
5
|
+
# upstream credential, and stamps observability headers in
|
|
6
|
+
# after_forward -- which must run on BOTH the forwarded and the
|
|
7
|
+
# short-circuited (denied) paths.
|
|
8
|
+
#
|
|
9
|
+
# Self-call shape (scheduled + workers=1, like test_http/test_proxy):
|
|
10
|
+
# the upstream echoes the Authorization header it received, so the
|
|
11
|
+
# test can confirm the credential swap; the gateway is the
|
|
12
|
+
# subclass-override form, built per-request with the runtime port
|
|
13
|
+
# (the example app uses the block DSL).
|
|
14
|
+
class TestApiGateway < TepTest
|
|
15
|
+
app_source <<~RB
|
|
16
|
+
require 'sinatra'
|
|
17
|
+
|
|
18
|
+
set :scheduler, :scheduled
|
|
19
|
+
set :workers, 1
|
|
20
|
+
|
|
21
|
+
# Upstream: echo the Authorization header the gateway attached.
|
|
22
|
+
get '/up/echo' do
|
|
23
|
+
"auth=" + req.req_headers["authorization"]
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Grant :call_upstream when the request carries ?auth=ok (stands in
|
|
27
|
+
# for the Auth battery populating req.identity).
|
|
28
|
+
before do
|
|
29
|
+
if params[:auth] == "ok"
|
|
30
|
+
req.identity = Tep::Identity.new("client:test", nil, [:call_upstream])
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
class ApiGw < Tep::Proxy
|
|
35
|
+
def rewrite_path(path)
|
|
36
|
+
"/up/echo"
|
|
37
|
+
end
|
|
38
|
+
def before_forward(req, res, ureq)
|
|
39
|
+
if !req.identity.may?(:call_upstream)
|
|
40
|
+
res.set_status(403)
|
|
41
|
+
res.set_body("denied")
|
|
42
|
+
true
|
|
43
|
+
else
|
|
44
|
+
ureq.set_header("Authorization", "Bearer upstream-secret")
|
|
45
|
+
false
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
def after_forward(req, ures, res)
|
|
49
|
+
res.headers["X-Proxy-Status"] = ures.status.to_s
|
|
50
|
+
0
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
get '/gw/:port' do
|
|
55
|
+
ApiGw.new("http://127.0.0.1:" + params[:port]).handle(req, res)
|
|
56
|
+
res.body
|
|
57
|
+
end
|
|
58
|
+
RB
|
|
59
|
+
|
|
60
|
+
def test_forwards_with_credential_when_capable
|
|
61
|
+
res = get("/gw/#{@port}?auth=ok")
|
|
62
|
+
assert_equal "200", res.code
|
|
63
|
+
# Upstream saw the gateway's attached credential, not the client's.
|
|
64
|
+
assert_equal "auth=Bearer upstream-secret", res.body
|
|
65
|
+
# after_forward stamped the real upstream status.
|
|
66
|
+
assert_equal "200", res["X-Proxy-Status"]
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def test_rejects_without_capability
|
|
70
|
+
res = get("/gw/#{@port}") # no ?auth=ok -> no capability
|
|
71
|
+
assert_equal "403", res.code
|
|
72
|
+
assert_equal "denied", res.body
|
|
73
|
+
# after_forward ran on the short-circuit path too (ures.status 0).
|
|
74
|
+
assert_equal "0", res["X-Proxy-Status"]
|
|
75
|
+
end
|
|
76
|
+
end
|