ably 0.7.5 → 0.7.6
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 +5 -13
- data/.gitignore +1 -0
- data/.gitmodules +3 -0
- data/README.md +46 -22
- data/SPEC.md +345 -240
- data/ably.gemspec +4 -2
- data/lib/ably/auth.rb +18 -14
- data/lib/ably/models/message.rb +1 -1
- data/lib/ably/models/paginated_resource.rb +31 -44
- data/lib/ably/models/presence_message.rb +1 -1
- data/lib/ably/models/stat.rb +67 -24
- data/lib/ably/models/stats_types.rb +131 -0
- data/lib/ably/modules/async_wrapper.rb +3 -2
- data/lib/ably/modules/message_emitter.rb +2 -2
- data/lib/ably/realtime.rb +1 -1
- data/lib/ably/realtime/channel.rb +24 -3
- data/lib/ably/realtime/channel/channel_manager.rb +1 -0
- data/lib/ably/realtime/client.rb +2 -2
- data/lib/ably/realtime/connection.rb +1 -1
- data/lib/ably/realtime/presence.rb +12 -1
- data/lib/ably/rest.rb +1 -1
- data/lib/ably/rest/channel.rb +4 -5
- data/lib/ably/rest/client.rb +5 -5
- data/lib/ably/rest/presence.rb +2 -2
- data/lib/ably/version.rb +1 -1
- data/spec/acceptance/realtime/channel_history_spec.rb +74 -23
- data/spec/acceptance/realtime/channel_spec.rb +3 -3
- data/spec/acceptance/realtime/client_spec.rb +3 -3
- data/spec/acceptance/realtime/connection_failures_spec.rb +2 -2
- data/spec/acceptance/realtime/connection_spec.rb +4 -4
- data/spec/acceptance/realtime/message_spec.rb +5 -5
- data/spec/acceptance/realtime/presence_history_spec.rb +56 -13
- data/spec/acceptance/realtime/presence_spec.rb +8 -8
- data/spec/acceptance/realtime/stats_spec.rb +1 -1
- data/spec/acceptance/realtime/time_spec.rb +1 -1
- data/spec/acceptance/rest/auth_spec.rb +31 -4
- data/spec/acceptance/rest/base_spec.rb +3 -3
- data/spec/acceptance/rest/channel_spec.rb +19 -19
- data/spec/acceptance/rest/channels_spec.rb +1 -1
- data/spec/acceptance/rest/client_spec.rb +9 -6
- data/spec/acceptance/rest/encoders_spec.rb +1 -1
- data/spec/acceptance/rest/message_spec.rb +10 -10
- data/spec/acceptance/rest/presence_spec.rb +81 -51
- data/spec/acceptance/rest/stats_spec.rb +46 -41
- data/spec/acceptance/rest/time_spec.rb +1 -1
- data/spec/shared/client_initializer_behaviour.rb +30 -19
- data/spec/spec_helper.rb +3 -0
- data/spec/support/markdown_spec_formatter.rb +1 -1
- data/spec/support/test_app.rb +11 -24
- data/spec/unit/auth_spec.rb +1 -1
- data/spec/unit/models/paginated_resource_spec.rb +81 -72
- data/spec/unit/models/stats_spec.rb +289 -0
- data/spec/unit/modules/async_wrapper_spec.rb +1 -1
- data/spec/unit/realtime/client_spec.rb +1 -1
- data/spec/unit/realtime/realtime_spec.rb +1 -1
- data/spec/unit/rest/channel_spec.rb +1 -1
- data/spec/unit/rest/client_spec.rb +8 -8
- data/spec/unit/rest/rest_spec.rb +1 -1
- data/spec/unit/util/crypto_spec.rb +1 -1
- metadata +55 -43
- data/spec/resources/crypto-data-128.json +0 -56
- data/spec/resources/crypto-data-256.json +0 -56
- data/spec/unit/models/stat_spec.rb +0 -113
@@ -3,7 +3,7 @@ require 'spec_helper'
|
|
3
3
|
describe Ably::Rest::Client, '#time' do
|
4
4
|
vary_by_protocol do
|
5
5
|
let(:client) do
|
6
|
-
Ably::Rest::Client.new(
|
6
|
+
Ably::Rest::Client.new(key: api_key, environment: environment, protocol: protocol)
|
7
7
|
end
|
8
8
|
|
9
9
|
describe 'fetching the service time' do
|
@@ -26,7 +26,7 @@ shared_examples 'a client initializer' do
|
|
26
26
|
let(:client_options) { Hash.new }
|
27
27
|
|
28
28
|
it 'raises an exception' do
|
29
|
-
expect { subject }.to raise_error(ArgumentError, /
|
29
|
+
expect { subject }.to raise_error(ArgumentError, /key is missing/)
|
30
30
|
end
|
31
31
|
end
|
32
32
|
|
@@ -38,35 +38,35 @@ shared_examples 'a client initializer' do
|
|
38
38
|
end
|
39
39
|
end
|
40
40
|
|
41
|
-
context '
|
42
|
-
let(:client_options) { {
|
41
|
+
context 'key: "invalid"' do
|
42
|
+
let(:client_options) { { key: 'invalid' } }
|
43
43
|
|
44
44
|
it 'raises an exception' do
|
45
|
-
expect { subject }.to raise_error(ArgumentError, /
|
45
|
+
expect { subject }.to raise_error(ArgumentError, /key is invalid/)
|
46
46
|
end
|
47
47
|
end
|
48
48
|
|
49
|
-
context '
|
50
|
-
let(:client_options) { {
|
49
|
+
context 'key: "invalid:asdad"' do
|
50
|
+
let(:client_options) { { key: 'invalid:asdad' } }
|
51
51
|
|
52
52
|
it 'raises an exception' do
|
53
|
-
expect { subject }.to raise_error(ArgumentError, /
|
53
|
+
expect { subject }.to raise_error(ArgumentError, /key is invalid/)
|
54
54
|
end
|
55
55
|
end
|
56
56
|
|
57
|
-
context '
|
58
|
-
let(:client_options) { {
|
57
|
+
context 'key and key_id' do
|
58
|
+
let(:client_options) { { key: 'appid.keyuid:keysecret', key_id: 'invalid' } }
|
59
59
|
|
60
60
|
it 'raises an exception' do
|
61
|
-
expect { subject }.to raise_error(ArgumentError, /
|
61
|
+
expect { subject }.to raise_error(ArgumentError, /key and key_id or key_secret are mutually exclusive/)
|
62
62
|
end
|
63
63
|
end
|
64
64
|
|
65
|
-
context '
|
66
|
-
let(:client_options) { {
|
65
|
+
context 'key and key_secret' do
|
66
|
+
let(:client_options) { { key: 'appid.keyuid:keysecret', key_secret: 'invalid' } }
|
67
67
|
|
68
68
|
it 'raises an exception' do
|
69
|
-
expect { subject }.to raise_error(ArgumentError, /
|
69
|
+
expect { subject }.to raise_error(ArgumentError, /key and key_id or key_secret are mutually exclusive/)
|
70
70
|
end
|
71
71
|
end
|
72
72
|
|
@@ -80,28 +80,39 @@ shared_examples 'a client initializer' do
|
|
80
80
|
end
|
81
81
|
|
82
82
|
context 'with valid arguments' do
|
83
|
-
let(:default_options) { {
|
83
|
+
let(:default_options) { { key: 'appid.keyuid:keysecret' } }
|
84
84
|
let(:client_options) { default_options }
|
85
85
|
|
86
|
-
context '
|
86
|
+
context 'key only' do
|
87
87
|
it 'connects to the Ably service' do
|
88
88
|
expect { subject }.to_not raise_error
|
89
89
|
end
|
90
90
|
end
|
91
91
|
|
92
|
+
context 'with legacy :api_key only' do
|
93
|
+
let(:default_options) { { api_key: 'api_key_id.keyuid:keysecret' } }
|
94
|
+
it 'connects to the Ably service' do
|
95
|
+
expect { subject }.to_not raise_error
|
96
|
+
end
|
97
|
+
|
98
|
+
it 'sets the Auth#key' do
|
99
|
+
expect(subject.auth.key).to eql('api_key_id.keyuid:keysecret')
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
92
103
|
context 'key_id and key_secret' do
|
93
104
|
let(:client_options) { { key_id: 'id', key_secret: 'secret' } }
|
94
105
|
|
95
|
-
it 'constructs an
|
96
|
-
expect(subject.auth.
|
106
|
+
it 'constructs an key' do
|
107
|
+
expect(subject.auth.key).to eql('id:secret')
|
97
108
|
end
|
98
109
|
end
|
99
110
|
|
100
111
|
context 'with a string key instead of options hash' do
|
101
112
|
let(:client_options) { 'app.key:secret' }
|
102
113
|
|
103
|
-
it 'sets the
|
104
|
-
expect(subject.auth.
|
114
|
+
it 'sets the key' do
|
115
|
+
expect(subject.auth.key).to eql(client_options)
|
105
116
|
end
|
106
117
|
|
107
118
|
it 'sets the key_id' do
|
data/spec/spec_helper.rb
CHANGED
data/spec/support/test_app.rb
CHANGED
@@ -1,28 +1,15 @@
|
|
1
1
|
require 'singleton'
|
2
2
|
|
3
3
|
class TestApp
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
],
|
14
|
-
'channels' => [
|
15
|
-
{
|
16
|
-
'name' => 'persisted:presence_fixtures',
|
17
|
-
'presence' => [
|
18
|
-
{ 'clientId' => 'client_bool', 'data' => 'true' },
|
19
|
-
{ 'clientId' => 'client_int', 'data' => '24' },
|
20
|
-
{ 'clientId' => 'client_string', 'data' => 'This is a string clientData payload' },
|
21
|
-
{ 'clientId' => 'client_json', 'data' => '{ "test" => \'This is a JSONObject clientData payload\'}' }
|
22
|
-
]
|
23
|
-
}
|
24
|
-
]
|
25
|
-
}
|
4
|
+
TEST_RESOURCES_PATH = File.expand_path('../../../lib/submodules/ably-common/test-resources', __FILE__)
|
5
|
+
|
6
|
+
# App configuration for test app
|
7
|
+
# See https://github.com/ably/ably-common/blob/master/test-resources/test-app-setup.json
|
8
|
+
APP_SPEC = JSON.parse(File.read(File.join(TEST_RESOURCES_PATH, 'test-app-setup.json')))['post_apps']
|
9
|
+
|
10
|
+
# Cipher details used for client_encoded presence data in test app
|
11
|
+
# See https://github.com/ably/ably-common/blob/master/test-resources/test-app-setup.json
|
12
|
+
APP_SPEC_CIPHER = JSON.parse(File.read(File.join(TEST_RESOURCES_PATH, 'test-app-setup.json')))['cipher']
|
26
13
|
|
27
14
|
# If an app has already been created and we need a new app, create a new test app
|
28
15
|
# This is sometimes needed when a test needs to be isolated from any other tests
|
@@ -107,13 +94,13 @@ class TestApp
|
|
107
94
|
end
|
108
95
|
|
109
96
|
def create_test_stats(stats)
|
110
|
-
client = Ably::Rest::Client.new(
|
97
|
+
client = Ably::Rest::Client.new(key: api_key, environment: environment)
|
111
98
|
response = client.post('/stats', stats)
|
112
99
|
raise "Could not create stats fixtures. Ably responded with status #{response.status}\n#{response.body}" unless (200..299).include?(response.status)
|
113
100
|
end
|
114
101
|
|
115
102
|
private
|
116
103
|
def sandbox_client
|
117
|
-
@sandbox_client ||= Ably::Rest::Client.new(
|
104
|
+
@sandbox_client ||= Ably::Rest::Client.new(key: 'app.key:secret', tls: true, environment: environment)
|
118
105
|
end
|
119
106
|
end
|
data/spec/unit/auth_spec.rb
CHANGED
@@ -4,7 +4,7 @@ require 'shared/protocol_msgbus_behaviour'
|
|
4
4
|
describe Ably::Auth do
|
5
5
|
let(:client) { double('client').as_null_object }
|
6
6
|
let(:client_id) { nil }
|
7
|
-
let(:options) { {
|
7
|
+
let(:options) { { key: 'appid.keyuid:keysecret', client_id: client_id } }
|
8
8
|
|
9
9
|
subject do
|
10
10
|
Ably::Auth.new(client, options)
|
@@ -5,7 +5,7 @@ describe Ably::Models::PaginatedResource do
|
|
5
5
|
let(:paginated_resource_class) { Ably::Models::PaginatedResource }
|
6
6
|
let(:headers) { Hash.new }
|
7
7
|
let(:client) do
|
8
|
-
instance_double('Ably::Rest::Client', logger:
|
8
|
+
instance_double('Ably::Rest::Client', logger: Ably::Models::NilLogger.new).tap do |client|
|
9
9
|
allow(client).to receive(:get).and_return(http_response)
|
10
10
|
end
|
11
11
|
end
|
@@ -27,57 +27,54 @@ describe Ably::Models::PaginatedResource do
|
|
27
27
|
let(:first_paged_request) { paginated_resource_class.new(http_response, full_url, client, paginated_resource_options) }
|
28
28
|
subject { first_paged_request }
|
29
29
|
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
it 'supports alias methods for length' do
|
35
|
-
expect(subject.count).to eql(subject.length)
|
36
|
-
expect(subject.size).to eql(subject.length)
|
37
|
-
end
|
38
|
-
|
39
|
-
it 'is Enumerable' do
|
40
|
-
expect(subject).to be_kind_of(Enumerable)
|
41
|
-
end
|
30
|
+
context '#items' do
|
31
|
+
it 'returns correct length from body' do
|
32
|
+
expect(subject.items.length).to eql(body.length)
|
33
|
+
end
|
42
34
|
|
43
|
-
|
44
|
-
|
45
|
-
|
35
|
+
it 'is Enumerable' do
|
36
|
+
expect(subject.items).to be_kind_of(Enumerable)
|
37
|
+
end
|
46
38
|
|
47
|
-
|
48
|
-
|
49
|
-
expect(subject.each).to be_a(Enumerator)
|
39
|
+
it 'is iterable' do
|
40
|
+
expect(subject.items.map { |d| d }).to eql(body)
|
50
41
|
end
|
51
42
|
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
43
|
+
context '#each' do
|
44
|
+
it 'returns an enumerator' do
|
45
|
+
expect(subject.items.each).to be_a(Enumerator)
|
46
|
+
end
|
47
|
+
|
48
|
+
it 'yields each item' do
|
49
|
+
items = []
|
50
|
+
subject.items.each do |item|
|
51
|
+
items << item
|
52
|
+
end
|
53
|
+
expect(items).to eq(body)
|
56
54
|
end
|
57
|
-
expect(items).to eq(body)
|
58
55
|
end
|
59
|
-
end
|
60
56
|
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
57
|
+
it 'provides [] accessor method' do
|
58
|
+
expect(subject.items[0][:id]).to eql(body[0][:id])
|
59
|
+
expect(subject.items[1][:id]).to eql(body[1][:id])
|
60
|
+
expect(subject.items[2]).to be_nil
|
61
|
+
end
|
66
62
|
|
67
|
-
|
68
|
-
|
69
|
-
|
63
|
+
specify '#first gets the first item in page' do
|
64
|
+
expect(subject.items.first[:id]).to eql(body[0][:id])
|
65
|
+
end
|
70
66
|
|
71
|
-
|
72
|
-
|
73
|
-
|
67
|
+
specify '#last gets the last item in page' do
|
68
|
+
expect(subject.items.last[:id]).to eql(body[1][:id])
|
69
|
+
end
|
74
70
|
|
75
|
-
|
76
|
-
|
71
|
+
context 'with coercion', :api_private do
|
72
|
+
let(:paginated_resource_options) { { coerce_into: 'OpenStruct' } }
|
77
73
|
|
78
|
-
|
79
|
-
|
80
|
-
|
74
|
+
it 'returns coerced objects' do
|
75
|
+
expect(subject.items.first).to be_a(OpenStruct)
|
76
|
+
expect(subject.items.first.id).to eql(body.first[:id])
|
77
|
+
end
|
81
78
|
end
|
82
79
|
end
|
83
80
|
|
@@ -90,7 +87,7 @@ describe Ably::Models::PaginatedResource do
|
|
90
87
|
}
|
91
88
|
end
|
92
89
|
let(:paged_client) do
|
93
|
-
instance_double('Ably::Rest::Client', logger:
|
90
|
+
instance_double('Ably::Rest::Client', logger: Ably::Models::NilLogger.new).tap do |client|
|
94
91
|
allow(client).to receive(:get).and_return(http_response_page2)
|
95
92
|
end
|
96
93
|
end
|
@@ -116,15 +113,15 @@ describe Ably::Models::PaginatedResource do
|
|
116
113
|
end
|
117
114
|
|
118
115
|
it 'calls the block for each resource after retrieving the resources' do
|
119
|
-
expect(subject[0][:added_attribute_from_block]).to eql("id:#{body[0][:id]}")
|
116
|
+
expect(subject.items[0][:added_attribute_from_block]).to eql("id:#{body[0][:id]}")
|
120
117
|
end
|
121
118
|
|
122
119
|
it 'calls the block for each resource on second page after retrieving the resources' do
|
123
|
-
page_1_first_id = subject[0][:id]
|
124
|
-
next_page = subject.
|
120
|
+
page_1_first_id = subject.items[0][:id]
|
121
|
+
next_page = subject.next
|
125
122
|
|
126
|
-
expect(next_page[0][:added_attribute_from_block]).to eql("id:#{body_page2[0][:id]}")
|
127
|
-
expect(next_page[0][:id]).to_not eql(page_1_first_id)
|
123
|
+
expect(next_page.items[0][:added_attribute_from_block]).to eql("id:#{body_page2[0][:id]}")
|
124
|
+
expect(next_page.items[0][:id]).to_not eql(page_1_first_id)
|
128
125
|
end
|
129
126
|
end
|
130
127
|
|
@@ -136,17 +133,17 @@ describe Ably::Models::PaginatedResource do
|
|
136
133
|
paginated_resource_class.new(http_response, full_url, paged_client, async_blocking_operations: true)
|
137
134
|
end
|
138
135
|
|
139
|
-
context '#
|
136
|
+
context '#next' do
|
140
137
|
it 'returns a SafeDeferrable that catches exceptions in callbacks and logs them' do
|
141
138
|
run_reactor do
|
142
|
-
expect(subject.
|
139
|
+
expect(subject.next).to be_a(Ably::Util::SafeDeferrable)
|
143
140
|
stop_reactor
|
144
141
|
end
|
145
142
|
end
|
146
143
|
|
147
144
|
it 'allows a success callback block to be added' do
|
148
145
|
run_reactor do
|
149
|
-
subject.
|
146
|
+
subject.next do |paginated_resource|
|
150
147
|
expect(paginated_resource).to be_a(Ably::Models::PaginatedResource)
|
151
148
|
stop_reactor
|
152
149
|
end
|
@@ -154,11 +151,11 @@ describe Ably::Models::PaginatedResource do
|
|
154
151
|
end
|
155
152
|
end
|
156
153
|
|
157
|
-
context '#
|
154
|
+
context '#first' do
|
158
155
|
it 'calls the errback callback when first page headers are missing' do
|
159
156
|
run_reactor do
|
160
|
-
subject.
|
161
|
-
deferrable = subject.
|
157
|
+
subject.next do |paginated_resource|
|
158
|
+
deferrable = subject.first
|
162
159
|
deferrable.errback do |error|
|
163
160
|
expect(error).to be_a(Ably::Exceptions::InvalidPageError)
|
164
161
|
stop_reactor
|
@@ -173,23 +170,27 @@ describe Ably::Models::PaginatedResource do
|
|
173
170
|
|
174
171
|
context 'with non paged http response' do
|
175
172
|
it 'is the first page' do
|
176
|
-
expect(subject).to
|
173
|
+
expect(subject).to be_first
|
177
174
|
end
|
178
175
|
|
179
176
|
it 'is the last page' do
|
180
|
-
expect(subject).to
|
177
|
+
expect(subject).to be_last
|
178
|
+
end
|
179
|
+
|
180
|
+
it 'does not have next page' do
|
181
|
+
expect(subject).to_not have_next
|
181
182
|
end
|
182
183
|
|
183
184
|
it 'does not support pagination' do
|
184
185
|
expect(subject.supports_pagination?).to_not eql(true)
|
185
186
|
end
|
186
187
|
|
187
|
-
it '
|
188
|
-
expect
|
188
|
+
it 'returns nil when accessing next page' do
|
189
|
+
expect(subject.next).to be_nil
|
189
190
|
end
|
190
191
|
|
191
|
-
it '
|
192
|
-
expect
|
192
|
+
it 'returns nil when accessing first page' do
|
193
|
+
expect(subject.first).to be_nil
|
193
194
|
end
|
194
195
|
end
|
195
196
|
|
@@ -207,11 +208,15 @@ describe Ably::Models::PaginatedResource do
|
|
207
208
|
end
|
208
209
|
|
209
210
|
it 'is the first page' do
|
210
|
-
expect(subject).to
|
211
|
+
expect(subject).to be_first
|
212
|
+
end
|
213
|
+
|
214
|
+
it 'has next page' do
|
215
|
+
expect(subject).to have_next
|
211
216
|
end
|
212
217
|
|
213
218
|
it 'is not the last page' do
|
214
|
-
expect(subject).to_not
|
219
|
+
expect(subject).to_not be_last
|
215
220
|
end
|
216
221
|
|
217
222
|
it 'supports pagination' do
|
@@ -236,7 +241,7 @@ describe Ably::Models::PaginatedResource do
|
|
236
241
|
headers: next_headers
|
237
242
|
})
|
238
243
|
end
|
239
|
-
let(:subject) { first_paged_request.
|
244
|
+
let(:subject) { first_paged_request.next }
|
240
245
|
|
241
246
|
before do
|
242
247
|
expect(client).to receive(:get).with("#{base_url}/history?index=1").and_return(next_http_response).once
|
@@ -247,39 +252,43 @@ describe Ably::Models::PaginatedResource do
|
|
247
252
|
end
|
248
253
|
|
249
254
|
it 'retrieves the next page of results' do
|
250
|
-
expect(subject.length).to eql(next_body.length)
|
251
|
-
expect(subject[0][:id]).to eql(next_body[0][:id])
|
255
|
+
expect(subject.items.length).to eql(next_body.length)
|
256
|
+
expect(subject.items[0][:id]).to eql(next_body[0][:id])
|
252
257
|
end
|
253
258
|
|
254
259
|
it 'is not the first page' do
|
255
|
-
expect(subject).to_not
|
260
|
+
expect(subject).to_not be_first
|
261
|
+
end
|
262
|
+
|
263
|
+
it 'does not have a next page' do
|
264
|
+
expect(subject).to_not have_next
|
256
265
|
end
|
257
266
|
|
258
267
|
it 'is the last page' do
|
259
|
-
expect(subject).to
|
268
|
+
expect(subject).to be_last
|
260
269
|
end
|
261
270
|
|
262
|
-
it '
|
263
|
-
expect(subject).to
|
264
|
-
expect
|
271
|
+
it 'returns nil when trying to access the last page when it is the last page' do
|
272
|
+
expect(subject).to be_last
|
273
|
+
expect(subject.next).to be_nil
|
265
274
|
end
|
266
275
|
|
267
276
|
context 'and then first page' do
|
268
277
|
before do
|
269
278
|
expect(client).to receive(:get).with("#{base_url}/history?index=0").and_return(http_response).once
|
270
279
|
end
|
271
|
-
subject { first_paged_request.
|
280
|
+
subject { first_paged_request.next.first }
|
272
281
|
|
273
282
|
it 'returns a PaginatedResource' do
|
274
283
|
expect(subject).to be_a(paginated_resource_class)
|
275
284
|
end
|
276
285
|
|
277
286
|
it 'retrieves the first page of results' do
|
278
|
-
expect(subject.length).to eql(body.length)
|
287
|
+
expect(subject.items.length).to eql(body.length)
|
279
288
|
end
|
280
289
|
|
281
290
|
it 'is the first page' do
|
282
|
-
expect(subject).to
|
291
|
+
expect(subject).to be_first
|
283
292
|
end
|
284
293
|
end
|
285
294
|
end
|