sanford 0.17.0 → 0.18.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 +4 -4
- data/Gemfile +1 -1
- data/bench/report.rb +2 -0
- data/bench/report.txt +28 -37
- data/lib/sanford/connection_handler.rb +10 -3
- data/lib/sanford/error_handler.rb +4 -4
- data/lib/sanford/io_pipe.rb +40 -0
- data/lib/sanford/process.rb +108 -28
- data/lib/sanford/router.rb +5 -0
- data/lib/sanford/server.rb +133 -95
- data/lib/sanford/server_data.rb +27 -28
- data/lib/sanford/template_source.rb +2 -2
- data/lib/sanford/version.rb +1 -1
- data/sanford.gemspec +4 -5
- data/test/helper.rb +11 -0
- data/test/support/app_server.rb +6 -3
- data/test/support/factory.rb +7 -0
- data/test/system/server_tests.rb +21 -3
- data/test/system/service_handler_tests.rb +4 -4
- data/test/unit/cli_tests.rb +2 -2
- data/test/unit/connection_handler_tests.rb +107 -47
- data/test/unit/error_handler_tests.rb +11 -11
- data/test/unit/io_pipe_tests.rb +84 -0
- data/test/unit/process_tests.rb +266 -131
- data/test/unit/router_tests.rb +25 -7
- data/test/unit/server_data_tests.rb +68 -46
- data/test/unit/server_tests.rb +197 -240
- data/test/unit/template_engine_tests.rb +1 -1
- data/test/unit/template_source_tests.rb +6 -0
- data/test/unit/test_runner_tests.rb +0 -19
- metadata +12 -19
data/lib/sanford/server_data.rb
CHANGED
@@ -2,20 +2,17 @@ module Sanford
|
|
2
2
|
|
3
3
|
class ServerData
|
4
4
|
|
5
|
-
# The server uses this to "compile"
|
6
|
-
#
|
7
|
-
#
|
8
|
-
#
|
9
|
-
|
10
|
-
attr_reader :name
|
11
|
-
attr_reader :pid_file
|
12
|
-
attr_reader :receives_keep_alive
|
13
|
-
attr_reader :worker_class, :worker_params, :num_workers
|
14
|
-
attr_reader :debug, :logger, :dtcp_logger, :verbose_logging
|
15
|
-
attr_reader :template_source, :shutdown_timeout
|
16
|
-
attr_reader :init_procs, :error_procs
|
17
|
-
attr_reader :router, :routes
|
5
|
+
# The server uses this to "compile" the common configuration data used
|
6
|
+
# by the server instances, error handlers and routes. The goal here is
|
7
|
+
# to provide these with a simplified interface with the minimal data needed
|
8
|
+
# and to decouple the configuration from each thing that needs its data.
|
9
|
+
|
18
10
|
attr_accessor :ip, :port
|
11
|
+
attr_reader :name, :pid_file, :shutdown_timeout
|
12
|
+
attr_reader :worker_class, :worker_params, :num_workers
|
13
|
+
attr_reader :error_procs, :template_source, :logger, :router
|
14
|
+
attr_reader :receives_keep_alive, :verbose_logging
|
15
|
+
attr_reader :debug, :dtcp_logger, :routes, :process_label
|
19
16
|
|
20
17
|
def initialize(args = nil)
|
21
18
|
args ||= {}
|
@@ -24,26 +21,28 @@ module Sanford
|
|
24
21
|
@port = !(v = ENV['SANFORD_PORT'].to_s).empty? ? v.to_i : args[:port]
|
25
22
|
@pid_file = args[:pid_file]
|
26
23
|
|
27
|
-
@
|
28
|
-
|
29
|
-
@worker_class = args[:worker_class]
|
30
|
-
@worker_params = args[:worker_params] || {}
|
31
|
-
@num_workers = args[:num_workers]
|
32
|
-
|
33
|
-
@debug = !ENV['SANFORD_DEBUG'].to_s.empty?
|
34
|
-
@logger = args[:logger]
|
35
|
-
@dtcp_logger = @logger if @debug
|
36
|
-
@verbose_logging = !!args[:verbose_logging]
|
24
|
+
@shutdown_timeout = args[:shutdown_timeout]
|
37
25
|
|
26
|
+
@worker_class = args[:worker_class]
|
27
|
+
@worker_params = args[:worker_params] || {}
|
28
|
+
@num_workers = args[:num_workers]
|
29
|
+
@error_procs = args[:error_procs] || []
|
38
30
|
@template_source = args[:template_source]
|
31
|
+
@logger = args[:logger]
|
32
|
+
@router = args[:router]
|
39
33
|
|
40
|
-
@
|
34
|
+
@receives_keep_alive = !!args[:receives_keep_alive]
|
35
|
+
@verbose_logging = !!args[:verbose_logging]
|
41
36
|
|
42
|
-
@
|
43
|
-
@
|
37
|
+
@debug = !ENV['SANFORD_DEBUG'].to_s.empty?
|
38
|
+
@dtcp_logger = @logger if @debug
|
39
|
+
@routes = build_routes(args[:routes] || [])
|
44
40
|
|
45
|
-
@
|
46
|
-
|
41
|
+
@process_label = if (label = ENV['SANFORD_PROCESS_LABEL'].to_s).empty?
|
42
|
+
"#{@name}-#{@ip}-#{@port}"
|
43
|
+
else
|
44
|
+
label
|
45
|
+
end
|
47
46
|
end
|
48
47
|
|
49
48
|
def route_for(name)
|
data/lib/sanford/version.rb
CHANGED
data/sanford.gemspec
CHANGED
@@ -18,10 +18,9 @@ Gem::Specification.new do |gem|
|
|
18
18
|
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
19
19
|
gem.require_paths = ["lib"]
|
20
20
|
|
21
|
-
gem.add_development_dependency("assert", ["~> 2.
|
21
|
+
gem.add_development_dependency("assert", ["~> 2.16.1"])
|
22
22
|
|
23
|
-
gem.add_dependency("dat-tcp", ["~> 0.8.
|
24
|
-
gem.add_dependency("much-plugin", ["~> 0.
|
25
|
-
gem.add_dependency("
|
26
|
-
gem.add_dependency("sanford-protocol", ["~> 0.11.0"])
|
23
|
+
gem.add_dependency("dat-tcp", ["~> 0.8.1"])
|
24
|
+
gem.add_dependency("much-plugin", ["~> 0.2.0"])
|
25
|
+
gem.add_dependency("sanford-protocol", ["~> 0.12.0"])
|
27
26
|
end
|
data/test/helper.rb
CHANGED
@@ -12,3 +12,14 @@ require 'pathname'
|
|
12
12
|
ROOT_PATH = Pathname.new(File.expand_path('../..', __FILE__))
|
13
13
|
|
14
14
|
require 'test/support/factory'
|
15
|
+
|
16
|
+
JOIN_SECONDS = 0.1
|
17
|
+
|
18
|
+
# 1.8.7 backfills
|
19
|
+
|
20
|
+
# Array#sample
|
21
|
+
if !(a = Array.new).respond_to?(:sample) && a.respond_to?(:choice)
|
22
|
+
class Array
|
23
|
+
alias_method :sample, :choice
|
24
|
+
end
|
25
|
+
end
|
data/test/support/app_server.rb
CHANGED
@@ -10,13 +10,16 @@ LOGGER = Logger.new(ROOT_PATH.join('log/app_server.log').to_s)
|
|
10
10
|
LOGGER.datetime_format = "" # turn off the datetime in the logs
|
11
11
|
|
12
12
|
class AppERBEngine < Sanford::TemplateEngine
|
13
|
-
RenderScope = Struct.new(:view)
|
13
|
+
RenderScope = Class.new(Struct.new(:view)) do
|
14
|
+
def get_binding; binding; end
|
15
|
+
end
|
14
16
|
|
15
17
|
def render(path, service_handler, locals)
|
16
18
|
require 'erb'
|
17
19
|
full_path = ROOT_PATH.join("test/support/#{path}.erb")
|
18
|
-
|
19
|
-
|
20
|
+
|
21
|
+
b = RenderScope.new(service_handler).get_binding
|
22
|
+
ERB.new(File.read(full_path)).result(b)
|
20
23
|
end
|
21
24
|
end
|
22
25
|
|
data/test/support/factory.rb
CHANGED
data/test/system/server_tests.rb
CHANGED
@@ -308,9 +308,9 @@ module Sanford::Server
|
|
308
308
|
should "return a response generated by the error handler" do
|
309
309
|
assert_equal 200, subject.code
|
310
310
|
assert_nil subject.status.message
|
311
|
-
|
312
|
-
|
313
|
-
assert_equal
|
311
|
+
exp = "The server on #{AppServer.ip}:#{AppServer.port} " \
|
312
|
+
"threw a StandardError."
|
313
|
+
assert_equal exp, subject.data
|
314
314
|
end
|
315
315
|
|
316
316
|
end
|
@@ -346,6 +346,24 @@ module Sanford::Server
|
|
346
346
|
|
347
347
|
end
|
348
348
|
|
349
|
+
class WithEnvProcessLabelTests < SystemTests
|
350
|
+
desc "with a process label env var"
|
351
|
+
setup do
|
352
|
+
ENV['SANFORD_PROCESS_LABEL'] = Factory.string
|
353
|
+
|
354
|
+
@server = AppServer.new
|
355
|
+
end
|
356
|
+
teardown do
|
357
|
+
ENV.delete('SANFORD_PROCESS_LABEL')
|
358
|
+
end
|
359
|
+
subject{ @server }
|
360
|
+
|
361
|
+
should "set the daemons process label to the env var" do
|
362
|
+
assert_equal ENV['SANFORD_PROCESS_LABEL'], subject.process_label
|
363
|
+
end
|
364
|
+
|
365
|
+
end
|
366
|
+
|
349
367
|
class TestClient
|
350
368
|
attr_accessor :delay, :bytes
|
351
369
|
|
@@ -62,8 +62,8 @@ module Sanford::ServiceHandler
|
|
62
62
|
setup do
|
63
63
|
@params = { 'message' => Factory.text }
|
64
64
|
@runner = test_runner(AppHandlers::Template, {
|
65
|
-
:params
|
66
|
-
:template_source => AppServer.
|
65
|
+
:params => @params,
|
66
|
+
:template_source => AppServer.config.template_source
|
67
67
|
})
|
68
68
|
@handler = @runner.handler
|
69
69
|
end
|
@@ -78,8 +78,8 @@ module Sanford::ServiceHandler
|
|
78
78
|
should "return a 200 response and render the template when run" do
|
79
79
|
response = @runner.run
|
80
80
|
assert_equal 200, response.code
|
81
|
-
|
82
|
-
assert_equal
|
81
|
+
exp = "ERB Template Message: #{@params['message']}\n"
|
82
|
+
assert_equal exp, response.data
|
83
83
|
end
|
84
84
|
|
85
85
|
end
|
data/test/unit/cli_tests.rb
CHANGED
@@ -129,8 +129,8 @@ class Sanford::CLI
|
|
129
129
|
end
|
130
130
|
|
131
131
|
should "output the error with the help" do
|
132
|
-
|
133
|
-
assert_includes
|
132
|
+
exp = "#{@command.inspect} is not a valid command"
|
133
|
+
assert_includes exp, @kernel_spy.output
|
134
134
|
assert_includes "Usage: sanford", @kernel_spy.output
|
135
135
|
end
|
136
136
|
|
@@ -84,59 +84,99 @@ class Sanford::ConnectionHandler
|
|
84
84
|
|
85
85
|
end
|
86
86
|
|
87
|
-
class
|
88
|
-
desc "and run with a route that throws an exception"
|
87
|
+
class RunWithExceptionSetupTests < InitTests
|
89
88
|
setup do
|
90
|
-
|
89
|
+
@route_exception = Factory.exception
|
90
|
+
Assert.stub(@route, :run){ raise @route_exception }
|
91
|
+
Assert.stub(Sanford::ErrorHandler, :new) do |*args|
|
92
|
+
@error_handler_spy = ErrorHandlerSpy.new(*args)
|
93
|
+
end
|
94
|
+
end
|
91
95
|
|
92
|
-
|
93
|
-
:server_data => @server_data,
|
94
|
-
:request => @request
|
95
|
-
})
|
96
|
-
@expected_response = error_handler.run
|
97
|
-
@expected_exception = error_handler.exception
|
96
|
+
end
|
98
97
|
|
98
|
+
class RunWithExceptionTests < RunWithExceptionSetupTests
|
99
|
+
desc "and run with an exception"
|
100
|
+
setup do
|
99
101
|
@processed_service = @connection_handler.run
|
100
102
|
end
|
101
|
-
subject{ @processed_service }
|
102
103
|
|
103
|
-
should "
|
104
|
-
|
105
|
-
|
106
|
-
|
104
|
+
should "run an error handler" do
|
105
|
+
assert_equal @route_exception, @error_handler_spy.passed_exception
|
106
|
+
exp = {
|
107
|
+
:server_data => @server_data,
|
108
|
+
:request => @processed_service.request,
|
109
|
+
:handler_class => @processed_service.handler_class,
|
110
|
+
:response => nil
|
111
|
+
}
|
112
|
+
assert_equal exp, @error_handler_spy.context_hash
|
113
|
+
assert_true @error_handler_spy.run_called
|
107
114
|
end
|
108
115
|
|
109
|
-
should "
|
110
|
-
assert_equal @
|
116
|
+
should "store the error handler response and exception on the processed service" do
|
117
|
+
assert_equal @error_handler_spy.response, @processed_service.response
|
118
|
+
assert_equal @error_handler_spy.exception, @processed_service.exception
|
119
|
+
end
|
120
|
+
|
121
|
+
should "write the error response to the connection" do
|
122
|
+
assert_equal @error_handler_spy.response, @connection.response
|
111
123
|
assert_true @connection.write_closed
|
112
124
|
end
|
113
125
|
|
114
126
|
end
|
115
127
|
|
116
|
-
class
|
128
|
+
class RunWithShutdownErrorTests < RunWithExceptionSetupTests
|
129
|
+
desc "and run with a dat worker pool shutdown error"
|
130
|
+
setup do
|
131
|
+
@shutdown_error = DatWorkerPool::ShutdownError.new(Factory.text)
|
132
|
+
Assert.stub(@route, :run){ raise @shutdown_error }
|
133
|
+
end
|
134
|
+
|
135
|
+
should "run an error handler" do
|
136
|
+
assert_raises{ @connection_handler.run }
|
137
|
+
|
138
|
+
passed_exception = @error_handler_spy.passed_exception
|
139
|
+
assert_instance_of Sanford::ShutdownError, passed_exception
|
140
|
+
assert_equal @shutdown_error.message, passed_exception.message
|
141
|
+
assert_equal @shutdown_error.backtrace, passed_exception.backtrace
|
142
|
+
assert_true @error_handler_spy.run_called
|
143
|
+
end
|
144
|
+
|
145
|
+
should "raise the shutdown error" do
|
146
|
+
assert_raises(@shutdown_error.class){ @connection_handler.run }
|
147
|
+
end
|
148
|
+
|
149
|
+
end
|
150
|
+
|
151
|
+
class RunWithExceptionWhileWritingTests < RunWithExceptionSetupTests
|
117
152
|
desc "and run with an exception thrown while writing the response"
|
118
153
|
setup do
|
154
|
+
Assert.stub(@route, :run){ @response }
|
119
155
|
@connection.raise_on_write = true
|
120
156
|
|
121
|
-
error_handler = Sanford::ErrorHandler.new(@connection.write_exception, {
|
122
|
-
:server_data => @server_data,
|
123
|
-
:request => @request
|
124
|
-
})
|
125
|
-
@expected_response = error_handler.run
|
126
|
-
@expected_exception = error_handler.exception
|
127
|
-
|
128
157
|
@processed_service = @connection_handler.run
|
129
158
|
end
|
130
159
|
subject{ @processed_service }
|
131
160
|
|
132
|
-
should "
|
133
|
-
|
134
|
-
|
135
|
-
|
161
|
+
should "run an error handler" do
|
162
|
+
assert_equal @connection.write_exception, @error_handler_spy.passed_exception
|
163
|
+
exp = {
|
164
|
+
:server_data => @server_data,
|
165
|
+
:request => @processed_service.request,
|
166
|
+
:handler_class => @processed_service.handler_class,
|
167
|
+
:response => @response
|
168
|
+
}
|
169
|
+
assert_equal exp, @error_handler_spy.context_hash
|
170
|
+
assert_true @error_handler_spy.run_called
|
171
|
+
end
|
172
|
+
|
173
|
+
should "store the error handler response and exception on the processed service" do
|
174
|
+
assert_equal @error_handler_spy.response, @processed_service.response
|
175
|
+
assert_equal @error_handler_spy.exception, @processed_service.exception
|
136
176
|
end
|
137
177
|
|
138
|
-
should "
|
139
|
-
assert_equal @
|
178
|
+
should "write the error response to the connection" do
|
179
|
+
assert_equal @error_handler_spy.response, @connection.response
|
140
180
|
assert_true @connection.write_closed
|
141
181
|
end
|
142
182
|
|
@@ -161,19 +201,21 @@ class Sanford::ConnectionHandler
|
|
161
201
|
should "have logged the service" do
|
162
202
|
time_taken = @processed_service.time_taken
|
163
203
|
status = @processed_service.response.status.to_s
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
assert_equal
|
204
|
+
exp = "[Sanford] ===== Received request =====" \
|
205
|
+
"[Sanford] Service: #{@request.name.inspect}" \
|
206
|
+
"[Sanford] Params: #{@request.params.inspect}" \
|
207
|
+
"[Sanford] Handler: #{@route.handler_class}" \
|
208
|
+
"[Sanford] ===== Completed in #{time_taken}ms #{status} ====="
|
209
|
+
assert_equal exp, subject.info_logged.join
|
170
210
|
end
|
171
211
|
|
172
212
|
should "log an exception when one is thrown" do
|
173
213
|
err = @processed_service.exception
|
174
|
-
|
175
|
-
|
176
|
-
|
214
|
+
exp = "[Sanford] #{err.class}: #{err.message}"
|
215
|
+
assert_equal exp, subject.error_logged.first
|
216
|
+
err.backtrace.each do |l|
|
217
|
+
assert_includes "[Sanford] #{l}", subject.error_logged
|
218
|
+
end
|
177
219
|
end
|
178
220
|
|
179
221
|
end
|
@@ -198,14 +240,14 @@ class Sanford::ConnectionHandler
|
|
198
240
|
time_taken = @processed_service.time_taken
|
199
241
|
status = @processed_service.response.status.to_i
|
200
242
|
exception_msg = "#{@exception.class}: #{@exception.message}"
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
assert_equal
|
243
|
+
exp = "[Sanford] " \
|
244
|
+
"time=#{time_taken} " \
|
245
|
+
"status=#{status} " \
|
246
|
+
"handler=#{@route.handler_class} " \
|
247
|
+
"service=#{@request.name.inspect} " \
|
248
|
+
"params=#{@request.params.inspect} " \
|
249
|
+
"error=#{exception_msg.inspect}"
|
250
|
+
assert_equal exp, subject.info_logged.join
|
209
251
|
end
|
210
252
|
|
211
253
|
should "not have logged the exception" do
|
@@ -216,6 +258,24 @@ class Sanford::ConnectionHandler
|
|
216
258
|
|
217
259
|
TestHandler = Class.new
|
218
260
|
|
261
|
+
class ErrorHandlerSpy
|
262
|
+
attr_reader :passed_exception, :context_hash, :exception, :response
|
263
|
+
attr_reader :run_called
|
264
|
+
|
265
|
+
def initialize(exception, context_hash)
|
266
|
+
@passed_exception = exception
|
267
|
+
@context_hash = context_hash
|
268
|
+
@exception = Factory.exception
|
269
|
+
@response = Factory.protocol_response
|
270
|
+
@run_called = false
|
271
|
+
end
|
272
|
+
|
273
|
+
def run
|
274
|
+
@run_called = true
|
275
|
+
@response
|
276
|
+
end
|
277
|
+
end
|
278
|
+
|
219
279
|
class SpyLogger
|
220
280
|
attr_reader :info_logged, :error_logged
|
221
281
|
|
@@ -100,7 +100,7 @@ class Sanford::ErrorHandler
|
|
100
100
|
subject{ @response }
|
101
101
|
|
102
102
|
should "return a bad request response" do
|
103
|
-
exp = Sanford::Protocol::Response.new([
|
103
|
+
exp = Sanford::Protocol::Response.new([400, @exception.message])
|
104
104
|
assert_equal exp, subject
|
105
105
|
end
|
106
106
|
|
@@ -117,7 +117,7 @@ class Sanford::ErrorHandler
|
|
117
117
|
subject{ @response }
|
118
118
|
|
119
119
|
should "return a bad request response" do
|
120
|
-
exp = Sanford::Protocol::Response.new([
|
120
|
+
exp = Sanford::Protocol::Response.new([400, @exception.message])
|
121
121
|
assert_equal exp, subject
|
122
122
|
end
|
123
123
|
|
@@ -134,7 +134,7 @@ class Sanford::ErrorHandler
|
|
134
134
|
subject{ @response }
|
135
135
|
|
136
136
|
should "return a not found response" do
|
137
|
-
exp = Sanford::Protocol::Response.new(
|
137
|
+
exp = Sanford::Protocol::Response.new(404)
|
138
138
|
assert_equal exp, subject
|
139
139
|
end
|
140
140
|
|
@@ -151,7 +151,7 @@ class Sanford::ErrorHandler
|
|
151
151
|
subject{ @response }
|
152
152
|
|
153
153
|
should "return a timeout response" do
|
154
|
-
exp = Sanford::Protocol::Response.new(
|
154
|
+
exp = Sanford::Protocol::Response.new(408)
|
155
155
|
assert_equal exp, subject
|
156
156
|
end
|
157
157
|
|
@@ -168,7 +168,7 @@ class Sanford::ErrorHandler
|
|
168
168
|
subject{ @response }
|
169
169
|
|
170
170
|
should "return an error response" do
|
171
|
-
exp = Sanford::Protocol::Response.new([
|
171
|
+
exp = Sanford::Protocol::Response.new([500, "An unexpected error occurred."])
|
172
172
|
assert_equal exp, subject
|
173
173
|
end
|
174
174
|
|
@@ -198,7 +198,7 @@ class Sanford::ErrorHandler
|
|
198
198
|
end
|
199
199
|
|
200
200
|
should "return an error response" do
|
201
|
-
exp = Sanford::Protocol::Response.new([
|
201
|
+
exp = Sanford::Protocol::Response.new([500, "An unexpected error occurred."])
|
202
202
|
assert_equal exp, @response
|
203
203
|
end
|
204
204
|
|
@@ -208,7 +208,7 @@ class Sanford::ErrorHandler
|
|
208
208
|
desc "with a protocol response returned from an error proc"
|
209
209
|
setup do
|
210
210
|
@proc_response = Sanford::Protocol::Response.new(Factory.integer)
|
211
|
-
@error_proc_spies.
|
211
|
+
@error_proc_spies.sample.response = @proc_response
|
212
212
|
|
213
213
|
@response = @handler.run
|
214
214
|
end
|
@@ -224,7 +224,7 @@ class Sanford::ErrorHandler
|
|
224
224
|
desc "with a response code returned from an error proc"
|
225
225
|
setup do
|
226
226
|
@response_code = Factory.integer
|
227
|
-
@error_proc_spies.
|
227
|
+
@error_proc_spies.sample.response = @response_code
|
228
228
|
|
229
229
|
@response = @handler.run
|
230
230
|
end
|
@@ -240,15 +240,15 @@ class Sanford::ErrorHandler
|
|
240
240
|
class RunWithSymbolFromErrorProcTests < RunSetupTests
|
241
241
|
desc "with a response symbol returned from an error proc"
|
242
242
|
setup do
|
243
|
-
@
|
244
|
-
@error_proc_spies.
|
243
|
+
@response_code = [400, 404, 500].sample
|
244
|
+
@error_proc_spies.sample.response = @response_code
|
245
245
|
|
246
246
|
@response = @handler.run
|
247
247
|
end
|
248
248
|
subject{ @response }
|
249
249
|
|
250
250
|
should "use the response symbol to build a response and return it" do
|
251
|
-
exp = Sanford::Protocol::Response.new(@
|
251
|
+
exp = Sanford::Protocol::Response.new(@response_code)
|
252
252
|
assert_equal exp, subject
|
253
253
|
end
|
254
254
|
|