stomp 1.2.16 → 1.3.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 +8 -8
- data/CHANGELOG.rdoc +10 -0
- data/README.rdoc +2 -0
- data/lib/client/utils.rb +49 -41
- data/lib/connection/heartbeats.rb +21 -33
- data/lib/connection/netio.rb +15 -22
- data/lib/connection/utils.rb +11 -17
- data/lib/stomp.rb +1 -0
- data/lib/stomp/client.rb +34 -10
- data/lib/stomp/connection.rb +15 -38
- data/lib/stomp/errors.rb +51 -0
- data/lib/stomp/null_logger.rb +28 -0
- data/{examples → lib/stomp}/slogger.rb +14 -2
- data/lib/stomp/version.rb +2 -2
- data/spec/client_spec.rb +39 -1
- data/spec/connection_spec.rb +3 -0
- data/stomp.gemspec +15 -12
- data/test/test_anonymous.rb +523 -0
- data/test/test_helper.rb +15 -0
- metadata +9 -5
@@ -0,0 +1,523 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
if Kernel.respond_to?(:require_relative)
|
4
|
+
require_relative("test_helper")
|
5
|
+
else
|
6
|
+
$:.unshift(File.dirname(__FILE__))
|
7
|
+
require 'test_helper'
|
8
|
+
end
|
9
|
+
|
10
|
+
=begin
|
11
|
+
|
12
|
+
Main class for testing Stomp::Connection instances.
|
13
|
+
|
14
|
+
=end
|
15
|
+
class TestConnection < Test::Unit::TestCase
|
16
|
+
include TestBase
|
17
|
+
|
18
|
+
def setup
|
19
|
+
@conn = get_anonymous_connection()
|
20
|
+
# Data for multi_thread tests
|
21
|
+
@max_threads = 20
|
22
|
+
@max_msgs = 100
|
23
|
+
end
|
24
|
+
|
25
|
+
def teardown
|
26
|
+
@conn.disconnect if @conn.open? # allow tests to disconnect
|
27
|
+
end
|
28
|
+
|
29
|
+
# Test basic connection creation.
|
30
|
+
def test_connection_exists
|
31
|
+
assert_not_nil @conn
|
32
|
+
end
|
33
|
+
|
34
|
+
# Test asynchronous polling.
|
35
|
+
def test_poll_async
|
36
|
+
@conn.subscribe("/queue/do.not.put.messages.on.this.queue", :id => "a.no.messages.queue")
|
37
|
+
# If the test 'hangs' here, Connection#poll is broken.
|
38
|
+
m = @conn.poll
|
39
|
+
assert m.nil?
|
40
|
+
end
|
41
|
+
|
42
|
+
# Test suppression of content length header.
|
43
|
+
def test_no_length
|
44
|
+
conn_subscribe make_destination
|
45
|
+
#
|
46
|
+
@conn.publish make_destination, "test_stomp#test_no_length",
|
47
|
+
{ :suppress_content_length => true }
|
48
|
+
msg = @conn.receive
|
49
|
+
assert_equal "test_stomp#test_no_length", msg.body
|
50
|
+
#
|
51
|
+
@conn.publish make_destination, "test_stomp#test_\000_length",
|
52
|
+
{ :suppress_content_length => true }
|
53
|
+
msg2 = @conn.receive
|
54
|
+
assert_equal "test_stomp#test_", msg2.body
|
55
|
+
checkEmsg(@conn)
|
56
|
+
end unless ENV['STOMP_RABBIT']
|
57
|
+
|
58
|
+
# Test direct / explicit receive.
|
59
|
+
def test_explicit_receive
|
60
|
+
conn_subscribe make_destination
|
61
|
+
@conn.publish make_destination, "test_stomp#test_explicit_receive"
|
62
|
+
msg = @conn.receive
|
63
|
+
assert_equal "test_stomp#test_explicit_receive", msg.body
|
64
|
+
end
|
65
|
+
|
66
|
+
# Test asking for a receipt.
|
67
|
+
def test_receipt
|
68
|
+
conn_subscribe make_destination, :receipt => "abc"
|
69
|
+
msg = @conn.receive
|
70
|
+
assert_equal "abc", msg.headers['receipt-id']
|
71
|
+
checkEmsg(@conn)
|
72
|
+
end
|
73
|
+
|
74
|
+
# Test asking for a receipt on disconnect.
|
75
|
+
def test_disconnect_receipt
|
76
|
+
@conn.disconnect :receipt => "abc123"
|
77
|
+
assert_nothing_raised {
|
78
|
+
assert_not_nil(@conn.disconnect_receipt, "should have a receipt")
|
79
|
+
assert_equal(@conn.disconnect_receipt.headers['receipt-id'],
|
80
|
+
"abc123", "receipt sent and received should match")
|
81
|
+
}
|
82
|
+
end
|
83
|
+
|
84
|
+
# Test ACKs for Stomp 1.0
|
85
|
+
def test_client_ack_with_symbol_10
|
86
|
+
if @conn.protocol != Stomp::SPL_10
|
87
|
+
assert true
|
88
|
+
return
|
89
|
+
end
|
90
|
+
queue = make_destination()
|
91
|
+
@conn.subscribe queue, :ack => :client
|
92
|
+
@conn.publish queue, "test_stomp#test_client_ack_with_symbol_10"
|
93
|
+
msg = @conn.receive
|
94
|
+
assert_nothing_raised {
|
95
|
+
# ACK has one required header, message-id, which must contain a value
|
96
|
+
# matching the message-id for the MESSAGE being acknowledged.
|
97
|
+
@conn.ack msg.headers['message-id']
|
98
|
+
}
|
99
|
+
checkEmsg(@conn)
|
100
|
+
end
|
101
|
+
|
102
|
+
# Test ACKs for Stomp 1.1
|
103
|
+
def test_client_ack_with_symbol_11
|
104
|
+
if @conn.protocol != Stomp::SPL_11
|
105
|
+
assert true
|
106
|
+
return
|
107
|
+
end
|
108
|
+
sid = @conn.uuid()
|
109
|
+
queue = make_destination()
|
110
|
+
@conn.subscribe queue, :ack => :client, :id => sid
|
111
|
+
@conn.publish queue, "test_stomp#test_client_ack_with_symbol_11"
|
112
|
+
msg = @conn.receive
|
113
|
+
assert_nothing_raised {
|
114
|
+
# ACK has two REQUIRED headers: message-id, which MUST contain a value
|
115
|
+
# matching the message-id for the MESSAGE being acknowledged and
|
116
|
+
# subscription, which MUST be set to match the value of the subscription's
|
117
|
+
# id header.
|
118
|
+
@conn.ack msg.headers['message-id'], :subscription => msg.headers['subscription']
|
119
|
+
}
|
120
|
+
checkEmsg(@conn)
|
121
|
+
end
|
122
|
+
|
123
|
+
# Test ACKs for Stomp 1.2
|
124
|
+
def test_client_ack_with_symbol_12
|
125
|
+
if @conn.protocol != Stomp::SPL_12
|
126
|
+
assert true
|
127
|
+
return
|
128
|
+
end
|
129
|
+
sid = @conn.uuid()
|
130
|
+
queue = make_destination()
|
131
|
+
@conn.subscribe queue, :ack => :client, :id => sid
|
132
|
+
@conn.publish queue, "test_stomp#test_client_ack_with_symbol_11"
|
133
|
+
msg = @conn.receive
|
134
|
+
assert_nothing_raised {
|
135
|
+
# The ACK frame MUST include an id header matching the ack header
|
136
|
+
# of the MESSAGE being acknowledged.
|
137
|
+
@conn.ack msg.headers['ack']
|
138
|
+
}
|
139
|
+
checkEmsg(@conn)
|
140
|
+
end
|
141
|
+
|
142
|
+
# Test a message with 0x00 embedded in the body.
|
143
|
+
def test_embedded_null
|
144
|
+
conn_subscribe make_destination
|
145
|
+
@conn.publish make_destination, "a\0"
|
146
|
+
msg = @conn.receive
|
147
|
+
assert_equal "a\0" , msg.body
|
148
|
+
checkEmsg(@conn)
|
149
|
+
end
|
150
|
+
|
151
|
+
# Test connection open checking.
|
152
|
+
def test_connection_open?
|
153
|
+
assert_equal true , @conn.open?
|
154
|
+
@conn.disconnect
|
155
|
+
assert_equal false, @conn.open?
|
156
|
+
end
|
157
|
+
|
158
|
+
# Test connection closed checking.
|
159
|
+
def test_connection_closed?
|
160
|
+
assert_equal false, @conn.closed?
|
161
|
+
@conn.disconnect
|
162
|
+
assert_equal true, @conn.closed?
|
163
|
+
end
|
164
|
+
|
165
|
+
# Test that methods detect a closed connection.
|
166
|
+
def test_closed_checks_conn
|
167
|
+
@conn.disconnect
|
168
|
+
#
|
169
|
+
assert_raise Stomp::Error::NoCurrentConnection do
|
170
|
+
@conn.ack("dummy_data")
|
171
|
+
end
|
172
|
+
#
|
173
|
+
assert_raise Stomp::Error::NoCurrentConnection do
|
174
|
+
@conn.begin("dummy_data")
|
175
|
+
end
|
176
|
+
#
|
177
|
+
assert_raise Stomp::Error::NoCurrentConnection do
|
178
|
+
@conn.commit("dummy_data")
|
179
|
+
end
|
180
|
+
#
|
181
|
+
assert_raise Stomp::Error::NoCurrentConnection do
|
182
|
+
@conn.abort("dummy_data")
|
183
|
+
end
|
184
|
+
#
|
185
|
+
assert_raise Stomp::Error::NoCurrentConnection do
|
186
|
+
conn_subscribe("dummy_data")
|
187
|
+
end
|
188
|
+
#
|
189
|
+
assert_raise Stomp::Error::NoCurrentConnection do
|
190
|
+
@conn.unsubscribe("dummy_data")
|
191
|
+
end
|
192
|
+
#
|
193
|
+
assert_raise Stomp::Error::NoCurrentConnection do
|
194
|
+
@conn.publish("dummy_data","dummy_data")
|
195
|
+
end
|
196
|
+
#
|
197
|
+
assert_raise Stomp::Error::NoCurrentConnection do
|
198
|
+
@conn.unreceive("dummy_data")
|
199
|
+
end
|
200
|
+
#
|
201
|
+
assert_raise Stomp::Error::NoCurrentConnection do
|
202
|
+
@conn.disconnect("dummy_data")
|
203
|
+
end
|
204
|
+
#
|
205
|
+
assert_raise Stomp::Error::NoCurrentConnection do
|
206
|
+
m = @conn.receive
|
207
|
+
end
|
208
|
+
#
|
209
|
+
assert_raise Stomp::Error::NoCurrentConnection do
|
210
|
+
m = @conn.poll
|
211
|
+
end
|
212
|
+
end
|
213
|
+
|
214
|
+
# Test that we receive a Stomp::Message.
|
215
|
+
def test_response_is_instance_of_message_class
|
216
|
+
conn_subscribe make_destination
|
217
|
+
@conn.publish make_destination, "a\0"
|
218
|
+
msg = @conn.receive
|
219
|
+
assert_instance_of Stomp::Message , msg
|
220
|
+
checkEmsg(@conn)
|
221
|
+
end
|
222
|
+
|
223
|
+
# Test converting a Message to a string.
|
224
|
+
def test_message_to_s
|
225
|
+
conn_subscribe make_destination
|
226
|
+
@conn.publish make_destination, "a\0"
|
227
|
+
msg = @conn.receive
|
228
|
+
assert_match /^<Stomp::Message headers=/ , msg.to_s
|
229
|
+
checkEmsg(@conn)
|
230
|
+
end
|
231
|
+
|
232
|
+
# Test that a connection frame is present.
|
233
|
+
def test_connection_frame
|
234
|
+
assert_not_nil @conn.connection_frame
|
235
|
+
end
|
236
|
+
|
237
|
+
# Test messages with multiple line ends.
|
238
|
+
def test_messages_with_multipleLine_ends
|
239
|
+
conn_subscribe make_destination
|
240
|
+
@conn.publish make_destination, "a\n\n"
|
241
|
+
@conn.publish make_destination, "b\n\na\n\n"
|
242
|
+
|
243
|
+
msg_a = @conn.receive
|
244
|
+
msg_b = @conn.receive
|
245
|
+
|
246
|
+
assert_equal "a\n\n", msg_a.body
|
247
|
+
assert_equal "b\n\na\n\n", msg_b.body
|
248
|
+
checkEmsg(@conn)
|
249
|
+
end
|
250
|
+
|
251
|
+
# Test publishing multiple messages.
|
252
|
+
def test_publish_two_messages
|
253
|
+
conn_subscribe make_destination
|
254
|
+
@conn.publish make_destination, "a\0"
|
255
|
+
@conn.publish make_destination, "b\0"
|
256
|
+
msg_a = @conn.receive
|
257
|
+
msg_b = @conn.receive
|
258
|
+
|
259
|
+
assert_equal "a\0", msg_a.body
|
260
|
+
assert_equal "b\0", msg_b.body
|
261
|
+
checkEmsg(@conn)
|
262
|
+
end
|
263
|
+
|
264
|
+
def test_thread_hang_one
|
265
|
+
received = nil
|
266
|
+
Thread.new(@conn) do |amq|
|
267
|
+
while true
|
268
|
+
received = amq.receive
|
269
|
+
end
|
270
|
+
end
|
271
|
+
#
|
272
|
+
conn_subscribe( make_destination )
|
273
|
+
message = Time.now.to_s
|
274
|
+
@conn.publish(make_destination, message)
|
275
|
+
sleep 1
|
276
|
+
assert_not_nil received
|
277
|
+
assert_equal message, received.body
|
278
|
+
checkEmsg(@conn)
|
279
|
+
end
|
280
|
+
|
281
|
+
# Test polling with a single thread.
|
282
|
+
def test_thread_poll_one
|
283
|
+
received = nil
|
284
|
+
max_sleep = (RUBY_VERSION =~ /1\.8/) ? 10 : 1
|
285
|
+
Thread.new(@conn) do |amq|
|
286
|
+
while true
|
287
|
+
received = amq.poll
|
288
|
+
# One message is needed
|
289
|
+
Thread.exit if received
|
290
|
+
sleep max_sleep
|
291
|
+
end
|
292
|
+
end
|
293
|
+
#
|
294
|
+
conn_subscribe( make_destination )
|
295
|
+
message = Time.now.to_s
|
296
|
+
@conn.publish(make_destination, message)
|
297
|
+
sleep max_sleep+1
|
298
|
+
assert_not_nil received
|
299
|
+
assert_equal message, received.body
|
300
|
+
checkEmsg(@conn)
|
301
|
+
end
|
302
|
+
|
303
|
+
# Test receiving with multiple threads.
|
304
|
+
def test_multi_thread_receive
|
305
|
+
lock = Mutex.new
|
306
|
+
msg_ctr = 0
|
307
|
+
dest = make_destination
|
308
|
+
#
|
309
|
+
1.upto(@max_threads) do |tnum|
|
310
|
+
Thread.new(@conn) do |amq|
|
311
|
+
while true
|
312
|
+
received = amq.receive
|
313
|
+
lock.synchronize do
|
314
|
+
msg_ctr += 1
|
315
|
+
end
|
316
|
+
# Simulate message processing
|
317
|
+
sleep 0.05
|
318
|
+
end
|
319
|
+
end
|
320
|
+
end
|
321
|
+
#
|
322
|
+
conn_subscribe( dest )
|
323
|
+
1.upto(@max_msgs) do |mnum|
|
324
|
+
msg = Time.now.to_s + " #{mnum}"
|
325
|
+
@conn.publish(dest, msg)
|
326
|
+
end
|
327
|
+
#
|
328
|
+
max_sleep = (RUBY_VERSION =~ /1\.8/) ? 30 : 5
|
329
|
+
max_sleep = 30 if RUBY_ENGINE =~ /mingw/
|
330
|
+
sleep_incr = 0.10
|
331
|
+
total_slept = 0
|
332
|
+
while true
|
333
|
+
break if @max_msgs == msg_ctr
|
334
|
+
total_slept += sleep_incr
|
335
|
+
break if total_slept > max_sleep
|
336
|
+
sleep sleep_incr
|
337
|
+
end
|
338
|
+
assert_equal @max_msgs, msg_ctr
|
339
|
+
checkEmsg(@conn)
|
340
|
+
end unless RUBY_ENGINE =~ /jruby/
|
341
|
+
|
342
|
+
# Test polling with multiple threads.
|
343
|
+
def test_multi_thread_poll
|
344
|
+
#
|
345
|
+
lock = Mutex.new
|
346
|
+
msg_ctr = 0
|
347
|
+
dest = make_destination
|
348
|
+
#
|
349
|
+
1.upto(@max_threads) do |tnum|
|
350
|
+
Thread.new(@conn) do |amq|
|
351
|
+
while true
|
352
|
+
received = amq.poll
|
353
|
+
if received
|
354
|
+
lock.synchronize do
|
355
|
+
msg_ctr += 1
|
356
|
+
end
|
357
|
+
# Simulate message processing
|
358
|
+
sleep 0.05
|
359
|
+
else
|
360
|
+
# Wait a bit for more work
|
361
|
+
sleep 0.05
|
362
|
+
end
|
363
|
+
end
|
364
|
+
end
|
365
|
+
end
|
366
|
+
#
|
367
|
+
conn_subscribe( dest )
|
368
|
+
1.upto(@max_msgs) do |mnum|
|
369
|
+
msg = Time.now.to_s + " #{mnum}"
|
370
|
+
@conn.publish(dest, msg)
|
371
|
+
end
|
372
|
+
#
|
373
|
+
max_sleep = (RUBY_VERSION =~ /1\.8\.6/) ? 30 : 5
|
374
|
+
max_sleep = 30 if RUBY_ENGINE =~ /mingw/
|
375
|
+
sleep_incr = 0.10
|
376
|
+
total_slept = 0
|
377
|
+
while true
|
378
|
+
break if @max_msgs == msg_ctr
|
379
|
+
total_slept += sleep_incr
|
380
|
+
break if total_slept > max_sleep
|
381
|
+
sleep sleep_incr
|
382
|
+
end
|
383
|
+
assert_equal @max_msgs, msg_ctr
|
384
|
+
checkEmsg(@conn)
|
385
|
+
end unless RUBY_ENGINE =~ /jruby/
|
386
|
+
|
387
|
+
# Test using a nil body.
|
388
|
+
def test_nil_body
|
389
|
+
dest = make_destination
|
390
|
+
assert_nothing_raised {
|
391
|
+
@conn.publish dest, nil
|
392
|
+
}
|
393
|
+
conn_subscribe dest
|
394
|
+
msg = @conn.receive
|
395
|
+
assert_equal "", msg.body
|
396
|
+
checkEmsg(@conn)
|
397
|
+
end
|
398
|
+
|
399
|
+
# Test transaction message sequencing.
|
400
|
+
def test_transaction
|
401
|
+
conn_subscribe make_destination
|
402
|
+
|
403
|
+
@conn.begin "txA"
|
404
|
+
@conn.publish make_destination, "txn message", 'transaction' => "txA"
|
405
|
+
|
406
|
+
@conn.publish make_destination, "first message"
|
407
|
+
|
408
|
+
msg = @conn.receive
|
409
|
+
assert_equal "first message", msg.body
|
410
|
+
|
411
|
+
@conn.commit "txA"
|
412
|
+
msg = @conn.receive
|
413
|
+
assert_equal "txn message", msg.body
|
414
|
+
checkEmsg(@conn)
|
415
|
+
end
|
416
|
+
|
417
|
+
# Test duplicate subscriptions.
|
418
|
+
def test_duplicate_subscription
|
419
|
+
@conn.disconnect # not reliable
|
420
|
+
@conn = Stomp::Connection.open(nil, nil, host, port, true, nil, nil) # reliable
|
421
|
+
dest = make_destination
|
422
|
+
conn_subscribe dest
|
423
|
+
#
|
424
|
+
assert_raise Stomp::Error::DuplicateSubscription do
|
425
|
+
conn_subscribe dest
|
426
|
+
end
|
427
|
+
checkEmsg(@conn)
|
428
|
+
end
|
429
|
+
|
430
|
+
# Test nil 1.1 connection parameters.
|
431
|
+
def test_nil_connparms
|
432
|
+
@conn.disconnect
|
433
|
+
#
|
434
|
+
assert_nothing_raised do
|
435
|
+
@conn = Stomp::Connection.open(nil, nil, host, port, false, 5, nil)
|
436
|
+
end
|
437
|
+
checkEmsg(@conn)
|
438
|
+
end
|
439
|
+
|
440
|
+
# Basic NAK test.
|
441
|
+
def test_nack11p_0010
|
442
|
+
if @conn.protocol == Stomp::SPL_10
|
443
|
+
assert_raise Stomp::Error::UnsupportedProtocolError do
|
444
|
+
@conn.nack "dummy msg-id"
|
445
|
+
end
|
446
|
+
else
|
447
|
+
dest = make_destination
|
448
|
+
smsg = "test_stomp#test_nack01: #{Time.now.to_f}"
|
449
|
+
@conn.publish dest, smsg
|
450
|
+
#
|
451
|
+
sid = @conn.uuid()
|
452
|
+
@conn.subscribe dest, :ack => :client, :id => sid
|
453
|
+
msg = @conn.receive
|
454
|
+
assert_equal smsg, msg.body
|
455
|
+
case @conn.protocol
|
456
|
+
when Stomp::SPL_12
|
457
|
+
assert_nothing_raised {
|
458
|
+
@conn.nack msg.headers["ack"]
|
459
|
+
sleep 0.05 # Give racy brokers a chance to handle the last nack before unsubscribe
|
460
|
+
@conn.unsubscribe dest, :id => sid
|
461
|
+
}
|
462
|
+
else # Stomp::SPL_11
|
463
|
+
assert_nothing_raised {
|
464
|
+
@conn.nack msg.headers["message-id"], :subscription => sid
|
465
|
+
sleep 0.05 # Give racy brokers a chance to handle the last nack before unsubscribe
|
466
|
+
@conn.unsubscribe dest, :id => sid
|
467
|
+
}
|
468
|
+
end
|
469
|
+
|
470
|
+
# phase 2
|
471
|
+
teardown()
|
472
|
+
setup()
|
473
|
+
sid = @conn.uuid()
|
474
|
+
@conn.subscribe dest, :ack => :auto, :id => sid
|
475
|
+
msg2 = @conn.receive
|
476
|
+
assert_equal smsg, msg2.body
|
477
|
+
checkEmsg(@conn)
|
478
|
+
end
|
479
|
+
end unless ENV['STOMP_AMQ11'] # AMQ sends NACK'd messages to a DLQ
|
480
|
+
|
481
|
+
# Test to illustrate Issue #44. Prior to a fix for #44, these tests would
|
482
|
+
# fail only when connecting to a pure STOMP 1.0 server that does not
|
483
|
+
# return a 'version' header at all.
|
484
|
+
def test_conn10_simple
|
485
|
+
@conn.disconnect
|
486
|
+
#
|
487
|
+
vhost = ENV['STOMP_RABBIT'] ? "/" : host
|
488
|
+
hash = { :hosts => [
|
489
|
+
{:host => host, :port => port, :ssl => false},
|
490
|
+
],
|
491
|
+
:connect_headers => {"accept-version" => "1.0", "host" => vhost},
|
492
|
+
:reliable => false,
|
493
|
+
}
|
494
|
+
c = nil
|
495
|
+
assert_nothing_raised {
|
496
|
+
c = Stomp::Connection.new(hash)
|
497
|
+
}
|
498
|
+
c.disconnect if c
|
499
|
+
#
|
500
|
+
hash = { :hosts => [
|
501
|
+
{:host => host, :port => port, :ssl => false},
|
502
|
+
],
|
503
|
+
:connect_headers => {"accept-version" => "3.14159,1.0,12.0", "host" => vhost},
|
504
|
+
:reliable => false,
|
505
|
+
}
|
506
|
+
c = nil
|
507
|
+
assert_nothing_raised {
|
508
|
+
c = Stomp::Connection.new(hash)
|
509
|
+
}
|
510
|
+
c.disconnect if c
|
511
|
+
end
|
512
|
+
|
513
|
+
# test JRuby detection
|
514
|
+
def test_jruby_presence
|
515
|
+
if defined?(RUBY_ENGINE) && RUBY_ENGINE =~ /jruby/
|
516
|
+
assert @conn.jruby
|
517
|
+
else
|
518
|
+
assert !@conn.jruby
|
519
|
+
end
|
520
|
+
end
|
521
|
+
|
522
|
+
end
|
523
|
+
|