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.
Files changed (193) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/Makefile +134 -0
  4. data/README.md +247 -0
  5. data/SINATRA_COMPAT.md +376 -0
  6. data/bin/tep +2156 -0
  7. data/examples/agentic_chat/README.md +103 -0
  8. data/examples/agentic_chat/app.rb +310 -0
  9. data/examples/api_gateway/README.md +49 -0
  10. data/examples/api_gateway/app.rb +66 -0
  11. data/examples/blog/app.rb +367 -0
  12. data/examples/blog/views/index.erb +36 -0
  13. data/examples/blog/views/login.erb +28 -0
  14. data/examples/blog/views/new_post.erb +25 -0
  15. data/examples/blog/views/show.erb +16 -0
  16. data/examples/chat/app.rb +278 -0
  17. data/examples/chat/assets/logo.svg +13 -0
  18. data/examples/chat/assets/style.css +209 -0
  19. data/examples/chat/views/index.erb +142 -0
  20. data/examples/chatbot/README.md +111 -0
  21. data/examples/chatbot/app.rb +1024 -0
  22. data/examples/chatbot/assets/chat.js +249 -0
  23. data/examples/chatbot/assets/compare.js +93 -0
  24. data/examples/chatbot/assets/markdown.js +84 -0
  25. data/examples/chatbot/assets/style.css +215 -0
  26. data/examples/chatbot/schema.sql +25 -0
  27. data/examples/chatbot/views/compare.erb +43 -0
  28. data/examples/chatbot/views/index.erb +42 -0
  29. data/examples/chatbot/views/login.erb +22 -0
  30. data/examples/chatbot/views/setup.erb +23 -0
  31. data/examples/counter/README.md +68 -0
  32. data/examples/counter/app.rb +85 -0
  33. data/examples/experiments/AGENTS.md +91 -0
  34. data/examples/experiments/README.md +99 -0
  35. data/examples/experiments/app.rb +225 -0
  36. data/examples/geohash/Gemfile +11 -0
  37. data/examples/geohash/Gemfile.lock +17 -0
  38. data/examples/geohash/README.md +58 -0
  39. data/examples/geohash/app.rb +33 -0
  40. data/examples/hello.rb +120 -0
  41. data/examples/llm_gateway/README.md +73 -0
  42. data/examples/llm_gateway/app.rb +91 -0
  43. data/examples/maidenhead/Gemfile +7 -0
  44. data/examples/maidenhead/Gemfile.lock +17 -0
  45. data/examples/maidenhead/README.md +47 -0
  46. data/examples/maidenhead/app.rb +46 -0
  47. data/examples/pg_hello.rb +76 -0
  48. data/examples/qdrant/Gemfile +11 -0
  49. data/examples/qdrant/Gemfile.lock +29 -0
  50. data/examples/qdrant/README.md +54 -0
  51. data/examples/sinatra_style.rb +32 -0
  52. data/examples/websocket_echo.rb +37 -0
  53. data/lib/tep/agent_delegation.rb +35 -0
  54. data/lib/tep/app.rb +291 -0
  55. data/lib/tep/assets.rb +52 -0
  56. data/lib/tep/auth.rb +78 -0
  57. data/lib/tep/auth_bearer_token.rb +126 -0
  58. data/lib/tep/auth_oauth2.rb +189 -0
  59. data/lib/tep/auth_oauth2_client.rb +29 -0
  60. data/lib/tep/auth_oauth2_code.rb +40 -0
  61. data/lib/tep/auth_session_cookie.rb +132 -0
  62. data/lib/tep/broadcast.rb +265 -0
  63. data/lib/tep/broadcast_subscription.rb +42 -0
  64. data/lib/tep/cache.rb +49 -0
  65. data/lib/tep/events.rb +257 -0
  66. data/lib/tep/filter.rb +21 -0
  67. data/lib/tep/handler.rb +35 -0
  68. data/lib/tep/http.rb +599 -0
  69. data/lib/tep/identity.rb +67 -0
  70. data/lib/tep/job.rb +186 -0
  71. data/lib/tep/json.rb +572 -0
  72. data/lib/tep/jwt.rb +126 -0
  73. data/lib/tep/live_view.rb +219 -0
  74. data/lib/tep/llm.rb +505 -0
  75. data/lib/tep/logger.rb +85 -0
  76. data/lib/tep/mcp.rb +203 -0
  77. data/lib/tep/multipart.rb +98 -0
  78. data/lib/tep/net.rb +155 -0
  79. data/lib/tep/openai_server.rb +725 -0
  80. data/lib/tep/parallel.rb +168 -0
  81. data/lib/tep/parser.rb +81 -0
  82. data/lib/tep/password.rb +102 -0
  83. data/lib/tep/pg.rb +1128 -0
  84. data/lib/tep/presence.rb +589 -0
  85. data/lib/tep/presence_entry.rb +52 -0
  86. data/lib/tep/proxy.rb +801 -0
  87. data/lib/tep/request.rb +194 -0
  88. data/lib/tep/response.rb +134 -0
  89. data/lib/tep/router.rb +137 -0
  90. data/lib/tep/scheduler.rb +342 -0
  91. data/lib/tep/security.rb +140 -0
  92. data/lib/tep/server.rb +276 -0
  93. data/lib/tep/server_scheduled.rb +375 -0
  94. data/lib/tep/session.rb +98 -0
  95. data/lib/tep/shell.rb +62 -0
  96. data/lib/tep/sphttp.c +858 -0
  97. data/lib/tep/sqlite.rb +215 -0
  98. data/lib/tep/streamer.rb +31 -0
  99. data/lib/tep/tep_pg.c +769 -0
  100. data/lib/tep/tep_sqlite.c +320 -0
  101. data/lib/tep/url.rb +161 -0
  102. data/lib/tep/version.rb +3 -0
  103. data/lib/tep/websocket/connection.rb +171 -0
  104. data/lib/tep/websocket/driver.rb +169 -0
  105. data/lib/tep/websocket/frame.rb +238 -0
  106. data/lib/tep/websocket/handshake.rb +159 -0
  107. data/lib/tep/websocket.rb +68 -0
  108. data/lib/tep.rb +981 -0
  109. data/public/hello.txt +1 -0
  110. data/public/style.css +4 -0
  111. data/spinel-ext.json +33 -0
  112. data/test/helper.rb +248 -0
  113. data/test/real_world/01_simple.rb +5 -0
  114. data/test/real_world/02_lifecycle.rb +20 -0
  115. data/test/real_world/03_chat.rb +75 -0
  116. data/test/real_world/04_health_api.rb +25 -0
  117. data/test/real_world/05_todo_api.rb +57 -0
  118. data/test/real_world/06_basic_auth.rb +25 -0
  119. data/test/real_world/07_bbc_rest_api.rb +228 -0
  120. data/test/real_world/07_sklise_things.rb +109 -0
  121. data/test/real_world/08_jwd83_helloworld.rb +56 -0
  122. data/test/run_all.rb +7 -0
  123. data/test/run_parallel.rb +89 -0
  124. data/test/spinel_scheduled_burst_segv_repro.rb +33 -0
  125. data/test/test_api_gateway.rb +76 -0
  126. data/test/test_auth.rb +223 -0
  127. data/test/test_auth_oauth2.rb +208 -0
  128. data/test/test_auth_session_cookie.rb +198 -0
  129. data/test/test_broadcast.rb +197 -0
  130. data/test/test_broadcast_pg.rb +135 -0
  131. data/test/test_cache.rb +98 -0
  132. data/test/test_cache_static.rb +48 -0
  133. data/test/test_cookies.rb +52 -0
  134. data/test/test_erb.rb +53 -0
  135. data/test/test_erb_ivars.rb +58 -0
  136. data/test/test_events.rb +114 -0
  137. data/test/test_filters.rb +41 -0
  138. data/test/test_geohash_example.rb +89 -0
  139. data/test/test_http.rb +137 -0
  140. data/test/test_http_pool.rb +122 -0
  141. data/test/test_http_pool_send.rb +57 -0
  142. data/test/test_identity.rb +165 -0
  143. data/test/test_inbound_tls.rb +101 -0
  144. data/test/test_inbound_tls_scheduled.rb +101 -0
  145. data/test/test_job.rb +108 -0
  146. data/test/test_json.rb +168 -0
  147. data/test/test_jwt.rb +143 -0
  148. data/test/test_live_view.rb +324 -0
  149. data/test/test_llm.rb +250 -0
  150. data/test/test_llm_gateway.rb +95 -0
  151. data/test/test_logger.rb +101 -0
  152. data/test/test_maidenhead_example.rb +86 -0
  153. data/test/test_mcp.rb +264 -0
  154. data/test/test_misc_v02.rb +54 -0
  155. data/test/test_modular.rb +43 -0
  156. data/test/test_multi_filters.rb +40 -0
  157. data/test/test_mustache.rb +57 -0
  158. data/test/test_openai_server.rb +598 -0
  159. data/test/test_optional_segments.rb +45 -0
  160. data/test/test_parallel.rb +102 -0
  161. data/test/test_params.rb +99 -0
  162. data/test/test_pass.rb +42 -0
  163. data/test/test_password.rb +101 -0
  164. data/test/test_pg.rb +673 -0
  165. data/test/test_presence.rb +374 -0
  166. data/test/test_presence_pg.rb +309 -0
  167. data/test/test_proxy.rb +556 -0
  168. data/test/test_proxy_dsl.rb +119 -0
  169. data/test/test_proxy_streaming.rb +146 -0
  170. data/test/test_real_world.rb +397 -0
  171. data/test/test_regex_routes.rb +52 -0
  172. data/test/test_request_methods.rb +102 -0
  173. data/test/test_response.rb +123 -0
  174. data/test/test_routing.rb +109 -0
  175. data/test/test_scheduler.rb +153 -0
  176. data/test/test_security.rb +72 -0
  177. data/test/test_server_scheduled.rb +56 -0
  178. data/test/test_sessions.rb +59 -0
  179. data/test/test_shell.rb +54 -0
  180. data/test/test_sqlite.rb +148 -0
  181. data/test/test_sqlite_cached.rb +171 -0
  182. data/test/test_static.rb +57 -0
  183. data/test/test_streaming.rb +96 -0
  184. data/test/test_unsupported.rb +32 -0
  185. data/test/test_websocket.rb +152 -0
  186. data/test/test_websocket_echo.rb +138 -0
  187. data/test/views/greet.erb +5 -0
  188. data/test/views/hello.erb +5 -0
  189. data/test/views/list.erb +5 -0
  190. data/test/views/m_ivars.mustache +3 -0
  191. data/test/views/m_simple.mustache +4 -0
  192. data/test/views/mixed.erb +3 -0
  193. 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,7 @@
1
+ # Drive-all: run every test_*.rb under test/ in one process. Use
2
+ # `make test` or `ruby test/run_all.rb`.
3
+ require_relative "helper"
4
+
5
+ Dir[File.join(__dir__, "test_*.rb")].sort.each do |f|
6
+ require_relative File.basename(f)
7
+ end
@@ -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