tina4ruby 3.11.13 → 3.11.15
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 +80 -80
- data/LICENSE.txt +21 -21
- data/README.md +137 -137
- data/exe/tina4ruby +5 -5
- data/lib/tina4/ai.rb +696 -696
- data/lib/tina4/api.rb +189 -189
- data/lib/tina4/auth.rb +305 -305
- data/lib/tina4/auto_crud.rb +244 -244
- data/lib/tina4/cache.rb +154 -154
- data/lib/tina4/cli.rb +1449 -1449
- data/lib/tina4/constants.rb +46 -46
- data/lib/tina4/container.rb +74 -74
- data/lib/tina4/cors.rb +74 -74
- data/lib/tina4/crud.rb +692 -692
- data/lib/tina4/database/sqlite3_adapter.rb +165 -165
- data/lib/tina4/database.rb +625 -625
- data/lib/tina4/database_result.rb +208 -208
- data/lib/tina4/debug.rb +8 -8
- data/lib/tina4/dev.rb +14 -14
- data/lib/tina4/dev_admin.rb +935 -935
- data/lib/tina4/dev_mailbox.rb +191 -191
- data/lib/tina4/drivers/firebird_driver.rb +124 -110
- data/lib/tina4/drivers/mongodb_driver.rb +561 -561
- data/lib/tina4/drivers/mssql_driver.rb +112 -112
- data/lib/tina4/drivers/mysql_driver.rb +90 -90
- data/lib/tina4/drivers/odbc_driver.rb +191 -191
- data/lib/tina4/drivers/postgres_driver.rb +116 -106
- data/lib/tina4/drivers/sqlite_driver.rb +122 -122
- data/lib/tina4/env.rb +95 -95
- data/lib/tina4/error_overlay.rb +252 -252
- data/lib/tina4/events.rb +109 -109
- data/lib/tina4/field_types.rb +154 -154
- data/lib/tina4/frond.rb +2025 -2025
- data/lib/tina4/gallery/auth/meta.json +1 -1
- data/lib/tina4/gallery/auth/src/routes/api/gallery_auth.rb +114 -114
- data/lib/tina4/gallery/database/meta.json +1 -1
- data/lib/tina4/gallery/database/src/routes/api/gallery_db.rb +43 -43
- data/lib/tina4/gallery/error-overlay/meta.json +1 -1
- data/lib/tina4/gallery/error-overlay/src/routes/api/gallery_crash.rb +17 -17
- data/lib/tina4/gallery/orm/meta.json +1 -1
- data/lib/tina4/gallery/orm/src/routes/api/gallery_products.rb +16 -16
- data/lib/tina4/gallery/queue/meta.json +1 -1
- data/lib/tina4/gallery/queue/src/routes/api/gallery_queue.rb +325 -325
- data/lib/tina4/gallery/rest-api/meta.json +1 -1
- data/lib/tina4/gallery/rest-api/src/routes/api/gallery_hello.rb +14 -14
- data/lib/tina4/gallery/templates/meta.json +1 -1
- data/lib/tina4/gallery/templates/src/routes/gallery_page.rb +12 -12
- data/lib/tina4/gallery/templates/src/templates/gallery_page.twig +257 -257
- data/lib/tina4/graphql.rb +966 -966
- data/lib/tina4/health.rb +39 -39
- data/lib/tina4/html_element.rb +170 -170
- data/lib/tina4/job.rb +80 -80
- data/lib/tina4/localization.rb +168 -168
- data/lib/tina4/log.rb +203 -203
- data/lib/tina4/mcp.rb +696 -696
- data/lib/tina4/messenger.rb +587 -587
- data/lib/tina4/metrics.rb +793 -793
- data/lib/tina4/middleware.rb +445 -445
- data/lib/tina4/migration.rb +451 -451
- data/lib/tina4/orm.rb +790 -790
- data/lib/tina4/public/css/tina4.css +2463 -2463
- data/lib/tina4/public/css/tina4.min.css +1 -1
- data/lib/tina4/public/images/logo.svg +5 -5
- data/lib/tina4/public/js/frond.min.js +2 -2
- data/lib/tina4/public/js/tina4-dev-admin.js +565 -565
- data/lib/tina4/public/js/tina4-dev-admin.min.js +480 -480
- data/lib/tina4/public/js/tina4.min.js +92 -92
- data/lib/tina4/public/js/tina4js.min.js +48 -48
- data/lib/tina4/public/swagger/index.html +90 -90
- data/lib/tina4/public/swagger/oauth2-redirect.html +63 -63
- data/lib/tina4/query_builder.rb +380 -380
- data/lib/tina4/queue.rb +366 -366
- data/lib/tina4/queue_backends/kafka_backend.rb +80 -80
- data/lib/tina4/queue_backends/lite_backend.rb +298 -298
- data/lib/tina4/queue_backends/mongo_backend.rb +126 -126
- data/lib/tina4/queue_backends/rabbitmq_backend.rb +73 -73
- data/lib/tina4/rack_app.rb +817 -817
- data/lib/tina4/rate_limiter.rb +130 -130
- data/lib/tina4/request.rb +268 -255
- data/lib/tina4/response.rb +346 -346
- data/lib/tina4/response_cache.rb +551 -551
- data/lib/tina4/router.rb +406 -406
- data/lib/tina4/scss/tina4css/_alerts.scss +34 -34
- data/lib/tina4/scss/tina4css/_badges.scss +22 -22
- data/lib/tina4/scss/tina4css/_buttons.scss +69 -69
- data/lib/tina4/scss/tina4css/_cards.scss +49 -49
- data/lib/tina4/scss/tina4css/_forms.scss +156 -156
- data/lib/tina4/scss/tina4css/_grid.scss +81 -81
- data/lib/tina4/scss/tina4css/_modals.scss +84 -84
- data/lib/tina4/scss/tina4css/_nav.scss +149 -149
- data/lib/tina4/scss/tina4css/_reset.scss +94 -94
- data/lib/tina4/scss/tina4css/_tables.scss +54 -54
- data/lib/tina4/scss/tina4css/_typography.scss +55 -55
- data/lib/tina4/scss/tina4css/_utilities.scss +197 -197
- data/lib/tina4/scss/tina4css/_variables.scss +117 -117
- data/lib/tina4/scss/tina4css/base.scss +1 -1
- data/lib/tina4/scss/tina4css/colors.scss +48 -48
- data/lib/tina4/scss/tina4css/tina4.scss +17 -17
- data/lib/tina4/scss_compiler.rb +178 -178
- data/lib/tina4/seeder.rb +567 -567
- data/lib/tina4/service_runner.rb +303 -303
- data/lib/tina4/session.rb +297 -297
- data/lib/tina4/session_handlers/database_handler.rb +72 -72
- data/lib/tina4/session_handlers/file_handler.rb +67 -67
- data/lib/tina4/session_handlers/mongo_handler.rb +49 -49
- data/lib/tina4/session_handlers/redis_handler.rb +43 -43
- data/lib/tina4/session_handlers/valkey_handler.rb +43 -43
- data/lib/tina4/shutdown.rb +84 -84
- data/lib/tina4/sql_translation.rb +158 -158
- data/lib/tina4/swagger.rb +124 -124
- data/lib/tina4/template.rb +894 -894
- data/lib/tina4/templates/base.twig +26 -26
- data/lib/tina4/templates/errors/302.twig +14 -14
- data/lib/tina4/templates/errors/401.twig +9 -9
- data/lib/tina4/templates/errors/403.twig +29 -29
- data/lib/tina4/templates/errors/404.twig +29 -29
- data/lib/tina4/templates/errors/500.twig +38 -38
- data/lib/tina4/templates/errors/502.twig +9 -9
- data/lib/tina4/templates/errors/503.twig +12 -12
- data/lib/tina4/templates/errors/base.twig +37 -37
- data/lib/tina4/test_client.rb +159 -159
- data/lib/tina4/testing.rb +340 -340
- data/lib/tina4/validator.rb +174 -174
- data/lib/tina4/version.rb +1 -1
- data/lib/tina4/webserver.rb +312 -312
- data/lib/tina4/websocket.rb +343 -343
- data/lib/tina4/websocket_backplane.rb +190 -190
- data/lib/tina4/wsdl.rb +564 -564
- data/lib/tina4.rb +458 -458
- data/lib/tina4ruby.rb +4 -4
- metadata +3 -3
data/lib/tina4/testing.rb
CHANGED
|
@@ -1,340 +1,340 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
require "json"
|
|
3
|
-
require "stringio"
|
|
4
|
-
|
|
5
|
-
module Tina4
|
|
6
|
-
module Testing
|
|
7
|
-
class << self
|
|
8
|
-
def suites
|
|
9
|
-
@suites ||= []
|
|
10
|
-
end
|
|
11
|
-
|
|
12
|
-
def results
|
|
13
|
-
@results ||= { passed: 0, failed: 0, errors: 0, tests: [] }
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
def reset!
|
|
17
|
-
@suites = []
|
|
18
|
-
@inline_registry = []
|
|
19
|
-
@results = { passed: 0, failed: 0, errors: 0, tests: [] }
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
def describe(name, &block)
|
|
23
|
-
suite = TestSuite.new(name)
|
|
24
|
-
suite.instance_eval(&block)
|
|
25
|
-
suites << suite
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
def run_all(quiet: false, failfast: false)
|
|
29
|
-
reset_results
|
|
30
|
-
suites.each do |suite|
|
|
31
|
-
run_suite(suite, quiet: quiet, failfast: failfast)
|
|
32
|
-
break if failfast && results[:failed] > 0
|
|
33
|
-
end
|
|
34
|
-
# Run inline-registered tests
|
|
35
|
-
inline_registry.each do |entry|
|
|
36
|
-
run_inline_entry(entry, quiet: quiet)
|
|
37
|
-
break if failfast && results[:failed] > 0
|
|
38
|
-
end
|
|
39
|
-
print_results unless quiet
|
|
40
|
-
results
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
# ── Inline testing (parity with Python/PHP/Node decorator pattern) ──
|
|
44
|
-
|
|
45
|
-
# Assertion builder: assert_equal(args, expected)
|
|
46
|
-
def assert_equal(args, expected)
|
|
47
|
-
{ type: :equal, args: args, expected: expected }
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
# Assertion builder: assert_raises(exception_class, args)
|
|
51
|
-
def assert_raises(exception_class, args)
|
|
52
|
-
{ type: :raises, exception: exception_class, args: args }
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
# Assertion builder: assert_true(args)
|
|
56
|
-
def assert_true(args)
|
|
57
|
-
{ type: :true, args: args }
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
# Assertion builder: assert_false(args)
|
|
61
|
-
def assert_false(args)
|
|
62
|
-
{ type: :false, args: args }
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
# Register a callable with inline assertions (mirrors Python's @tests decorator).
|
|
66
|
-
#
|
|
67
|
-
# Tina4::Testing.tests(
|
|
68
|
-
# Tina4::Testing.assert_equal([5, 3], 8),
|
|
69
|
-
# Tina4::Testing.assert_raises(ArgumentError, [nil]),
|
|
70
|
-
# ) { |a, b| raise ArgumentError, "b required" if b.nil?; a + b }
|
|
71
|
-
#
|
|
72
|
-
def tests(*assertions, name: nil, &block)
|
|
73
|
-
raise ArgumentError, "tests requires a block" unless block_given?
|
|
74
|
-
inline_registry << {
|
|
75
|
-
fn: block,
|
|
76
|
-
name: name || "anonymous",
|
|
77
|
-
assertions: assertions
|
|
78
|
-
}
|
|
79
|
-
block
|
|
80
|
-
end
|
|
81
|
-
|
|
82
|
-
def inline_registry
|
|
83
|
-
@inline_registry ||= []
|
|
84
|
-
end
|
|
85
|
-
|
|
86
|
-
private
|
|
87
|
-
|
|
88
|
-
def run_inline_entry(entry, quiet: false)
|
|
89
|
-
fn = entry[:fn]
|
|
90
|
-
name = entry[:name]
|
|
91
|
-
puts "\n #{name}" unless entry[:assertions].empty? || quiet
|
|
92
|
-
|
|
93
|
-
entry[:assertions].each do |assertion|
|
|
94
|
-
args = assertion[:args]
|
|
95
|
-
case assertion[:type]
|
|
96
|
-
when :equal
|
|
97
|
-
begin
|
|
98
|
-
result = fn.call(*args)
|
|
99
|
-
if result == assertion[:expected]
|
|
100
|
-
results[:passed] += 1
|
|
101
|
-
puts " \e[32m✓\e[0m #{name}(#{args.inspect}) == #{assertion[:expected].inspect}" unless quiet
|
|
102
|
-
else
|
|
103
|
-
results[:failed] += 1
|
|
104
|
-
puts " \e[31m✗\e[0m #{name}(#{args.inspect}) expected #{assertion[:expected].inspect}, got #{result.inspect}" unless quiet
|
|
105
|
-
end
|
|
106
|
-
rescue => e
|
|
107
|
-
results[:errors] += 1
|
|
108
|
-
puts " \e[33m!\e[0m #{name}(#{args.inspect}) raised #{e.class}: #{e.message}" unless quiet
|
|
109
|
-
end
|
|
110
|
-
when :raises
|
|
111
|
-
begin
|
|
112
|
-
fn.call(*args)
|
|
113
|
-
results[:failed] += 1
|
|
114
|
-
puts " \e[31m✗\e[0m #{name}(#{args.inspect}) expected #{assertion[:exception]} but none raised" unless quiet
|
|
115
|
-
rescue assertion[:exception]
|
|
116
|
-
results[:passed] += 1
|
|
117
|
-
puts " \e[32m✓\e[0m #{name}(#{args.inspect}) raises #{assertion[:exception]}" unless quiet
|
|
118
|
-
rescue => e
|
|
119
|
-
results[:failed] += 1
|
|
120
|
-
puts " \e[31m✗\e[0m #{name}(#{args.inspect}) expected #{assertion[:exception]}, got #{e.class}" unless quiet
|
|
121
|
-
end
|
|
122
|
-
when :true
|
|
123
|
-
begin
|
|
124
|
-
result = fn.call(*args)
|
|
125
|
-
if result
|
|
126
|
-
results[:passed] += 1
|
|
127
|
-
puts " \e[32m✓\e[0m #{name}(#{args.inspect}) is truthy" unless quiet
|
|
128
|
-
else
|
|
129
|
-
results[:failed] += 1
|
|
130
|
-
puts " \e[31m✗\e[0m #{name}(#{args.inspect}) expected truthy, got #{result.inspect}" unless quiet
|
|
131
|
-
end
|
|
132
|
-
rescue => e
|
|
133
|
-
results[:errors] += 1
|
|
134
|
-
puts " \e[33m!\e[0m #{name}(#{args.inspect}) raised #{e.class}: #{e.message}" unless quiet
|
|
135
|
-
end
|
|
136
|
-
when :false
|
|
137
|
-
begin
|
|
138
|
-
result = fn.call(*args)
|
|
139
|
-
if !result
|
|
140
|
-
results[:passed] += 1
|
|
141
|
-
puts " \e[32m✓\e[0m #{name}(#{args.inspect}) is falsy" unless quiet
|
|
142
|
-
else
|
|
143
|
-
results[:failed] += 1
|
|
144
|
-
puts " \e[31m✗\e[0m #{name}(#{args.inspect}) expected falsy, got #{result.inspect}" unless quiet
|
|
145
|
-
end
|
|
146
|
-
rescue => e
|
|
147
|
-
results[:errors] += 1
|
|
148
|
-
puts " \e[33m!\e[0m #{name}(#{args.inspect}) raised #{e.class}: #{e.message}" unless quiet
|
|
149
|
-
end
|
|
150
|
-
end
|
|
151
|
-
end
|
|
152
|
-
end
|
|
153
|
-
|
|
154
|
-
def reset_results
|
|
155
|
-
@results = { passed: 0, failed: 0, errors: 0, tests: [] }
|
|
156
|
-
end
|
|
157
|
-
|
|
158
|
-
def run_suite(suite, quiet: false, failfast: false)
|
|
159
|
-
puts "\n #{suite.name}" unless quiet
|
|
160
|
-
suite.tests.each do |test|
|
|
161
|
-
run_test(suite, test, quiet: quiet)
|
|
162
|
-
break if failfast && results[:failed] > 0
|
|
163
|
-
end
|
|
164
|
-
end
|
|
165
|
-
|
|
166
|
-
def run_test(suite, test, quiet: false)
|
|
167
|
-
suite.run_before_each
|
|
168
|
-
context = TestContext.new
|
|
169
|
-
context.instance_eval(&test[:block])
|
|
170
|
-
results[:passed] += 1
|
|
171
|
-
results[:tests] << { name: test[:name], status: :passed, suite: suite.name }
|
|
172
|
-
puts " \e[32m✓\e[0m #{test[:name]}" unless quiet
|
|
173
|
-
rescue TestFailure => e
|
|
174
|
-
results[:failed] += 1
|
|
175
|
-
results[:tests] << { name: test[:name], status: :failed, suite: suite.name, message: e.message }
|
|
176
|
-
puts " \e[31m✗\e[0m #{test[:name]}: #{e.message}" unless quiet
|
|
177
|
-
rescue => e
|
|
178
|
-
results[:errors] += 1
|
|
179
|
-
results[:tests] << { name: test[:name], status: :error, suite: suite.name, message: e.message }
|
|
180
|
-
puts " \e[33m!\e[0m #{test[:name]}: #{e.message}" unless quiet
|
|
181
|
-
ensure
|
|
182
|
-
suite.run_after_each
|
|
183
|
-
end
|
|
184
|
-
|
|
185
|
-
def print_results
|
|
186
|
-
total = results[:passed] + results[:failed] + results[:errors]
|
|
187
|
-
puts "\n #{total} tests: \e[32m#{results[:passed]} passed\e[0m, " \
|
|
188
|
-
"\e[31m#{results[:failed]} failed\e[0m, " \
|
|
189
|
-
"\e[33m#{results[:errors]} errors\e[0m\n"
|
|
190
|
-
end
|
|
191
|
-
end
|
|
192
|
-
|
|
193
|
-
class TestFailure < StandardError; end
|
|
194
|
-
|
|
195
|
-
class TestSuite
|
|
196
|
-
attr_reader :name, :tests
|
|
197
|
-
|
|
198
|
-
def initialize(name)
|
|
199
|
-
@name = name
|
|
200
|
-
@tests = []
|
|
201
|
-
@before_each = nil
|
|
202
|
-
@after_each = nil
|
|
203
|
-
end
|
|
204
|
-
|
|
205
|
-
def it(description, &block)
|
|
206
|
-
@tests << { name: description, block: block }
|
|
207
|
-
end
|
|
208
|
-
|
|
209
|
-
def before_each(&block)
|
|
210
|
-
@before_each = block
|
|
211
|
-
end
|
|
212
|
-
|
|
213
|
-
def after_each(&block)
|
|
214
|
-
@after_each = block
|
|
215
|
-
end
|
|
216
|
-
|
|
217
|
-
def run_before_each
|
|
218
|
-
@before_each&.call
|
|
219
|
-
end
|
|
220
|
-
|
|
221
|
-
def run_after_each
|
|
222
|
-
@after_each&.call
|
|
223
|
-
end
|
|
224
|
-
end
|
|
225
|
-
|
|
226
|
-
class TestContext
|
|
227
|
-
def assert(condition, message = "Assertion failed")
|
|
228
|
-
raise TestFailure, message unless condition
|
|
229
|
-
end
|
|
230
|
-
|
|
231
|
-
def assert_equal(expected, actual, message = nil)
|
|
232
|
-
msg = message || "Expected #{expected.inspect}, got #{actual.inspect}"
|
|
233
|
-
raise TestFailure, msg unless expected == actual
|
|
234
|
-
end
|
|
235
|
-
|
|
236
|
-
def assert_not_equal(expected, actual, message = nil)
|
|
237
|
-
msg = message || "Expected #{actual.inspect} to not equal #{expected.inspect}"
|
|
238
|
-
raise TestFailure, msg if expected == actual
|
|
239
|
-
end
|
|
240
|
-
|
|
241
|
-
def assert_nil(value, message = nil)
|
|
242
|
-
msg = message || "Expected nil, got #{value.inspect}"
|
|
243
|
-
raise TestFailure, msg unless value.nil?
|
|
244
|
-
end
|
|
245
|
-
|
|
246
|
-
def assert_not_nil(value, message = nil)
|
|
247
|
-
msg = message || "Expected non-nil value"
|
|
248
|
-
raise TestFailure, msg if value.nil?
|
|
249
|
-
end
|
|
250
|
-
|
|
251
|
-
def assert_includes(collection, item, message = nil)
|
|
252
|
-
msg = message || "Expected #{collection.inspect} to include #{item.inspect}"
|
|
253
|
-
raise TestFailure, msg unless collection.include?(item)
|
|
254
|
-
end
|
|
255
|
-
|
|
256
|
-
def assert_raises(exception_class, message = nil)
|
|
257
|
-
yield
|
|
258
|
-
raise TestFailure, message || "Expected #{exception_class} to be raised"
|
|
259
|
-
rescue exception_class
|
|
260
|
-
true
|
|
261
|
-
end
|
|
262
|
-
|
|
263
|
-
def assert_true(value, message = nil)
|
|
264
|
-
msg = message || "Expected truthy, got #{value.inspect}"
|
|
265
|
-
raise TestFailure, msg unless value
|
|
266
|
-
end
|
|
267
|
-
|
|
268
|
-
def assert_false(value, message = nil)
|
|
269
|
-
msg = message || "Expected falsy, got #{value.inspect}"
|
|
270
|
-
raise TestFailure, msg if value
|
|
271
|
-
end
|
|
272
|
-
|
|
273
|
-
def assert_match(pattern, string, message = nil)
|
|
274
|
-
msg = message || "Expected #{string.inspect} to match #{pattern.inspect}"
|
|
275
|
-
raise TestFailure, msg unless pattern.match?(string)
|
|
276
|
-
end
|
|
277
|
-
|
|
278
|
-
def assert_json(response_body)
|
|
279
|
-
JSON.parse(response_body)
|
|
280
|
-
rescue JSON::ParserError => e
|
|
281
|
-
raise TestFailure, "Invalid JSON: #{e.message}"
|
|
282
|
-
end
|
|
283
|
-
|
|
284
|
-
def assert_status(response, expected_status)
|
|
285
|
-
actual = response.is_a?(Array) ? response[0] : response.status
|
|
286
|
-
assert_equal(expected_status, actual, "Expected status #{expected_status}, got #{actual}")
|
|
287
|
-
end
|
|
288
|
-
|
|
289
|
-
# HTTP test helpers
|
|
290
|
-
def simulate_request(method, path, body: nil, headers: {}, params: {})
|
|
291
|
-
env = build_test_env(method, path, body: body, headers: headers, params: params)
|
|
292
|
-
app = Tina4::RackApp.new
|
|
293
|
-
app.call(env)
|
|
294
|
-
end
|
|
295
|
-
|
|
296
|
-
def get(path, headers: {}, params: {})
|
|
297
|
-
simulate_request("GET", path, headers: headers, params: params)
|
|
298
|
-
end
|
|
299
|
-
|
|
300
|
-
def post(path, body: nil, headers: {})
|
|
301
|
-
simulate_request("POST", path, body: body, headers: headers)
|
|
302
|
-
end
|
|
303
|
-
|
|
304
|
-
def put(path, body: nil, headers: {})
|
|
305
|
-
simulate_request("PUT", path, body: body, headers: headers)
|
|
306
|
-
end
|
|
307
|
-
|
|
308
|
-
def delete(path, headers: {})
|
|
309
|
-
simulate_request("DELETE", path, headers: headers)
|
|
310
|
-
end
|
|
311
|
-
|
|
312
|
-
private
|
|
313
|
-
|
|
314
|
-
def build_test_env(method, path, body: nil, headers: {}, params: {})
|
|
315
|
-
query_string = params.empty? ? "" : URI.encode_www_form(params)
|
|
316
|
-
body_str = body.is_a?(Hash) ? JSON.generate(body) : (body || "")
|
|
317
|
-
input = StringIO.new(body_str)
|
|
318
|
-
|
|
319
|
-
env = {
|
|
320
|
-
"REQUEST_METHOD" => method.upcase,
|
|
321
|
-
"PATH_INFO" => path,
|
|
322
|
-
"QUERY_STRING" => query_string,
|
|
323
|
-
"CONTENT_TYPE" => body.is_a?(Hash) ? "application/json" : "text/plain",
|
|
324
|
-
"CONTENT_LENGTH" => body_str.length.to_s,
|
|
325
|
-
"REMOTE_ADDR" => "127.0.0.1",
|
|
326
|
-
"rack.input" => input,
|
|
327
|
-
"rack.errors" => StringIO.new,
|
|
328
|
-
"rack.url_scheme" => "http"
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
headers.each do |key, value|
|
|
332
|
-
env_key = "HTTP_#{key.upcase.gsub('-', '_')}"
|
|
333
|
-
env[env_key] = value
|
|
334
|
-
end
|
|
335
|
-
|
|
336
|
-
env
|
|
337
|
-
end
|
|
338
|
-
end
|
|
339
|
-
end
|
|
340
|
-
end
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
require "json"
|
|
3
|
+
require "stringio"
|
|
4
|
+
|
|
5
|
+
module Tina4
|
|
6
|
+
module Testing
|
|
7
|
+
class << self
|
|
8
|
+
def suites
|
|
9
|
+
@suites ||= []
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def results
|
|
13
|
+
@results ||= { passed: 0, failed: 0, errors: 0, tests: [] }
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def reset!
|
|
17
|
+
@suites = []
|
|
18
|
+
@inline_registry = []
|
|
19
|
+
@results = { passed: 0, failed: 0, errors: 0, tests: [] }
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def describe(name, &block)
|
|
23
|
+
suite = TestSuite.new(name)
|
|
24
|
+
suite.instance_eval(&block)
|
|
25
|
+
suites << suite
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def run_all(quiet: false, failfast: false)
|
|
29
|
+
reset_results
|
|
30
|
+
suites.each do |suite|
|
|
31
|
+
run_suite(suite, quiet: quiet, failfast: failfast)
|
|
32
|
+
break if failfast && results[:failed] > 0
|
|
33
|
+
end
|
|
34
|
+
# Run inline-registered tests
|
|
35
|
+
inline_registry.each do |entry|
|
|
36
|
+
run_inline_entry(entry, quiet: quiet)
|
|
37
|
+
break if failfast && results[:failed] > 0
|
|
38
|
+
end
|
|
39
|
+
print_results unless quiet
|
|
40
|
+
results
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# ── Inline testing (parity with Python/PHP/Node decorator pattern) ──
|
|
44
|
+
|
|
45
|
+
# Assertion builder: assert_equal(args, expected)
|
|
46
|
+
def assert_equal(args, expected)
|
|
47
|
+
{ type: :equal, args: args, expected: expected }
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Assertion builder: assert_raises(exception_class, args)
|
|
51
|
+
def assert_raises(exception_class, args)
|
|
52
|
+
{ type: :raises, exception: exception_class, args: args }
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Assertion builder: assert_true(args)
|
|
56
|
+
def assert_true(args)
|
|
57
|
+
{ type: :true, args: args }
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Assertion builder: assert_false(args)
|
|
61
|
+
def assert_false(args)
|
|
62
|
+
{ type: :false, args: args }
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Register a callable with inline assertions (mirrors Python's @tests decorator).
|
|
66
|
+
#
|
|
67
|
+
# Tina4::Testing.tests(
|
|
68
|
+
# Tina4::Testing.assert_equal([5, 3], 8),
|
|
69
|
+
# Tina4::Testing.assert_raises(ArgumentError, [nil]),
|
|
70
|
+
# ) { |a, b| raise ArgumentError, "b required" if b.nil?; a + b }
|
|
71
|
+
#
|
|
72
|
+
def tests(*assertions, name: nil, &block)
|
|
73
|
+
raise ArgumentError, "tests requires a block" unless block_given?
|
|
74
|
+
inline_registry << {
|
|
75
|
+
fn: block,
|
|
76
|
+
name: name || "anonymous",
|
|
77
|
+
assertions: assertions
|
|
78
|
+
}
|
|
79
|
+
block
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def inline_registry
|
|
83
|
+
@inline_registry ||= []
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
private
|
|
87
|
+
|
|
88
|
+
def run_inline_entry(entry, quiet: false)
|
|
89
|
+
fn = entry[:fn]
|
|
90
|
+
name = entry[:name]
|
|
91
|
+
puts "\n #{name}" unless entry[:assertions].empty? || quiet
|
|
92
|
+
|
|
93
|
+
entry[:assertions].each do |assertion|
|
|
94
|
+
args = assertion[:args]
|
|
95
|
+
case assertion[:type]
|
|
96
|
+
when :equal
|
|
97
|
+
begin
|
|
98
|
+
result = fn.call(*args)
|
|
99
|
+
if result == assertion[:expected]
|
|
100
|
+
results[:passed] += 1
|
|
101
|
+
puts " \e[32m✓\e[0m #{name}(#{args.inspect}) == #{assertion[:expected].inspect}" unless quiet
|
|
102
|
+
else
|
|
103
|
+
results[:failed] += 1
|
|
104
|
+
puts " \e[31m✗\e[0m #{name}(#{args.inspect}) expected #{assertion[:expected].inspect}, got #{result.inspect}" unless quiet
|
|
105
|
+
end
|
|
106
|
+
rescue => e
|
|
107
|
+
results[:errors] += 1
|
|
108
|
+
puts " \e[33m!\e[0m #{name}(#{args.inspect}) raised #{e.class}: #{e.message}" unless quiet
|
|
109
|
+
end
|
|
110
|
+
when :raises
|
|
111
|
+
begin
|
|
112
|
+
fn.call(*args)
|
|
113
|
+
results[:failed] += 1
|
|
114
|
+
puts " \e[31m✗\e[0m #{name}(#{args.inspect}) expected #{assertion[:exception]} but none raised" unless quiet
|
|
115
|
+
rescue assertion[:exception]
|
|
116
|
+
results[:passed] += 1
|
|
117
|
+
puts " \e[32m✓\e[0m #{name}(#{args.inspect}) raises #{assertion[:exception]}" unless quiet
|
|
118
|
+
rescue => e
|
|
119
|
+
results[:failed] += 1
|
|
120
|
+
puts " \e[31m✗\e[0m #{name}(#{args.inspect}) expected #{assertion[:exception]}, got #{e.class}" unless quiet
|
|
121
|
+
end
|
|
122
|
+
when :true
|
|
123
|
+
begin
|
|
124
|
+
result = fn.call(*args)
|
|
125
|
+
if result
|
|
126
|
+
results[:passed] += 1
|
|
127
|
+
puts " \e[32m✓\e[0m #{name}(#{args.inspect}) is truthy" unless quiet
|
|
128
|
+
else
|
|
129
|
+
results[:failed] += 1
|
|
130
|
+
puts " \e[31m✗\e[0m #{name}(#{args.inspect}) expected truthy, got #{result.inspect}" unless quiet
|
|
131
|
+
end
|
|
132
|
+
rescue => e
|
|
133
|
+
results[:errors] += 1
|
|
134
|
+
puts " \e[33m!\e[0m #{name}(#{args.inspect}) raised #{e.class}: #{e.message}" unless quiet
|
|
135
|
+
end
|
|
136
|
+
when :false
|
|
137
|
+
begin
|
|
138
|
+
result = fn.call(*args)
|
|
139
|
+
if !result
|
|
140
|
+
results[:passed] += 1
|
|
141
|
+
puts " \e[32m✓\e[0m #{name}(#{args.inspect}) is falsy" unless quiet
|
|
142
|
+
else
|
|
143
|
+
results[:failed] += 1
|
|
144
|
+
puts " \e[31m✗\e[0m #{name}(#{args.inspect}) expected falsy, got #{result.inspect}" unless quiet
|
|
145
|
+
end
|
|
146
|
+
rescue => e
|
|
147
|
+
results[:errors] += 1
|
|
148
|
+
puts " \e[33m!\e[0m #{name}(#{args.inspect}) raised #{e.class}: #{e.message}" unless quiet
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def reset_results
|
|
155
|
+
@results = { passed: 0, failed: 0, errors: 0, tests: [] }
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def run_suite(suite, quiet: false, failfast: false)
|
|
159
|
+
puts "\n #{suite.name}" unless quiet
|
|
160
|
+
suite.tests.each do |test|
|
|
161
|
+
run_test(suite, test, quiet: quiet)
|
|
162
|
+
break if failfast && results[:failed] > 0
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def run_test(suite, test, quiet: false)
|
|
167
|
+
suite.run_before_each
|
|
168
|
+
context = TestContext.new
|
|
169
|
+
context.instance_eval(&test[:block])
|
|
170
|
+
results[:passed] += 1
|
|
171
|
+
results[:tests] << { name: test[:name], status: :passed, suite: suite.name }
|
|
172
|
+
puts " \e[32m✓\e[0m #{test[:name]}" unless quiet
|
|
173
|
+
rescue TestFailure => e
|
|
174
|
+
results[:failed] += 1
|
|
175
|
+
results[:tests] << { name: test[:name], status: :failed, suite: suite.name, message: e.message }
|
|
176
|
+
puts " \e[31m✗\e[0m #{test[:name]}: #{e.message}" unless quiet
|
|
177
|
+
rescue => e
|
|
178
|
+
results[:errors] += 1
|
|
179
|
+
results[:tests] << { name: test[:name], status: :error, suite: suite.name, message: e.message }
|
|
180
|
+
puts " \e[33m!\e[0m #{test[:name]}: #{e.message}" unless quiet
|
|
181
|
+
ensure
|
|
182
|
+
suite.run_after_each
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def print_results
|
|
186
|
+
total = results[:passed] + results[:failed] + results[:errors]
|
|
187
|
+
puts "\n #{total} tests: \e[32m#{results[:passed]} passed\e[0m, " \
|
|
188
|
+
"\e[31m#{results[:failed]} failed\e[0m, " \
|
|
189
|
+
"\e[33m#{results[:errors]} errors\e[0m\n"
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
class TestFailure < StandardError; end
|
|
194
|
+
|
|
195
|
+
class TestSuite
|
|
196
|
+
attr_reader :name, :tests
|
|
197
|
+
|
|
198
|
+
def initialize(name)
|
|
199
|
+
@name = name
|
|
200
|
+
@tests = []
|
|
201
|
+
@before_each = nil
|
|
202
|
+
@after_each = nil
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def it(description, &block)
|
|
206
|
+
@tests << { name: description, block: block }
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def before_each(&block)
|
|
210
|
+
@before_each = block
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def after_each(&block)
|
|
214
|
+
@after_each = block
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def run_before_each
|
|
218
|
+
@before_each&.call
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def run_after_each
|
|
222
|
+
@after_each&.call
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
class TestContext
|
|
227
|
+
def assert(condition, message = "Assertion failed")
|
|
228
|
+
raise TestFailure, message unless condition
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def assert_equal(expected, actual, message = nil)
|
|
232
|
+
msg = message || "Expected #{expected.inspect}, got #{actual.inspect}"
|
|
233
|
+
raise TestFailure, msg unless expected == actual
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def assert_not_equal(expected, actual, message = nil)
|
|
237
|
+
msg = message || "Expected #{actual.inspect} to not equal #{expected.inspect}"
|
|
238
|
+
raise TestFailure, msg if expected == actual
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
def assert_nil(value, message = nil)
|
|
242
|
+
msg = message || "Expected nil, got #{value.inspect}"
|
|
243
|
+
raise TestFailure, msg unless value.nil?
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def assert_not_nil(value, message = nil)
|
|
247
|
+
msg = message || "Expected non-nil value"
|
|
248
|
+
raise TestFailure, msg if value.nil?
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def assert_includes(collection, item, message = nil)
|
|
252
|
+
msg = message || "Expected #{collection.inspect} to include #{item.inspect}"
|
|
253
|
+
raise TestFailure, msg unless collection.include?(item)
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
def assert_raises(exception_class, message = nil)
|
|
257
|
+
yield
|
|
258
|
+
raise TestFailure, message || "Expected #{exception_class} to be raised"
|
|
259
|
+
rescue exception_class
|
|
260
|
+
true
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
def assert_true(value, message = nil)
|
|
264
|
+
msg = message || "Expected truthy, got #{value.inspect}"
|
|
265
|
+
raise TestFailure, msg unless value
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
def assert_false(value, message = nil)
|
|
269
|
+
msg = message || "Expected falsy, got #{value.inspect}"
|
|
270
|
+
raise TestFailure, msg if value
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
def assert_match(pattern, string, message = nil)
|
|
274
|
+
msg = message || "Expected #{string.inspect} to match #{pattern.inspect}"
|
|
275
|
+
raise TestFailure, msg unless pattern.match?(string)
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
def assert_json(response_body)
|
|
279
|
+
JSON.parse(response_body)
|
|
280
|
+
rescue JSON::ParserError => e
|
|
281
|
+
raise TestFailure, "Invalid JSON: #{e.message}"
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
def assert_status(response, expected_status)
|
|
285
|
+
actual = response.is_a?(Array) ? response[0] : response.status
|
|
286
|
+
assert_equal(expected_status, actual, "Expected status #{expected_status}, got #{actual}")
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
# HTTP test helpers
|
|
290
|
+
def simulate_request(method, path, body: nil, headers: {}, params: {})
|
|
291
|
+
env = build_test_env(method, path, body: body, headers: headers, params: params)
|
|
292
|
+
app = Tina4::RackApp.new
|
|
293
|
+
app.call(env)
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
def get(path, headers: {}, params: {})
|
|
297
|
+
simulate_request("GET", path, headers: headers, params: params)
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
def post(path, body: nil, headers: {})
|
|
301
|
+
simulate_request("POST", path, body: body, headers: headers)
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
def put(path, body: nil, headers: {})
|
|
305
|
+
simulate_request("PUT", path, body: body, headers: headers)
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
def delete(path, headers: {})
|
|
309
|
+
simulate_request("DELETE", path, headers: headers)
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
private
|
|
313
|
+
|
|
314
|
+
def build_test_env(method, path, body: nil, headers: {}, params: {})
|
|
315
|
+
query_string = params.empty? ? "" : URI.encode_www_form(params)
|
|
316
|
+
body_str = body.is_a?(Hash) ? JSON.generate(body) : (body || "")
|
|
317
|
+
input = StringIO.new(body_str)
|
|
318
|
+
|
|
319
|
+
env = {
|
|
320
|
+
"REQUEST_METHOD" => method.upcase,
|
|
321
|
+
"PATH_INFO" => path,
|
|
322
|
+
"QUERY_STRING" => query_string,
|
|
323
|
+
"CONTENT_TYPE" => body.is_a?(Hash) ? "application/json" : "text/plain",
|
|
324
|
+
"CONTENT_LENGTH" => body_str.length.to_s,
|
|
325
|
+
"REMOTE_ADDR" => "127.0.0.1",
|
|
326
|
+
"rack.input" => input,
|
|
327
|
+
"rack.errors" => StringIO.new,
|
|
328
|
+
"rack.url_scheme" => "http"
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
headers.each do |key, value|
|
|
332
|
+
env_key = "HTTP_#{key.upcase.gsub('-', '_')}"
|
|
333
|
+
env[env_key] = value
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
env
|
|
337
|
+
end
|
|
338
|
+
end
|
|
339
|
+
end
|
|
340
|
+
end
|