riak-client 1.0.0.beta → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +7 -4
- data/Gemfile +12 -17
- data/Guardfile +1 -1
- data/LICENSE +16 -0
- data/README.markdown +178 -0
- data/RELEASE_NOTES.md +99 -0
- data/Rakefile +25 -1
- data/erl_src/riak_kv_test014_backend.beam +0 -0
- data/erl_src/riak_kv_test014_backend.erl +189 -0
- data/erl_src/riak_kv_test_backend.beam +0 -0
- data/erl_src/riak_kv_test_backend.erl +37 -19
- data/lib/riak/client.rb +322 -272
- data/lib/riak/client/beefcake_protobuffs_backend.rb +6 -10
- data/lib/riak/client/decaying.rb +28 -0
- data/lib/riak/client/excon_backend.rb +27 -11
- data/lib/riak/client/http_backend.rb +71 -2
- data/lib/riak/client/http_backend/configuration.rb +17 -3
- data/lib/riak/client/http_backend/transport_methods.rb +3 -3
- data/lib/riak/client/net_http_backend.rb +18 -14
- data/lib/riak/client/node.rb +111 -0
- data/lib/riak/client/pool.rb +180 -0
- data/lib/riak/client/protobuffs_backend.rb +15 -5
- data/lib/riak/client/search.rb +9 -3
- data/lib/riak/link.rb +5 -7
- data/lib/riak/locale/en.yml +1 -0
- data/lib/riak/node/configuration.rb +1 -0
- data/lib/riak/node/defaults.rb +19 -6
- data/lib/riak/node/generation.rb +9 -2
- data/lib/riak/node/log.rb +2 -2
- data/lib/riak/node/version.rb +22 -16
- data/lib/riak/robject.rb +19 -3
- data/lib/riak/serializers.rb +1 -1
- data/lib/riak/test_server.rb +10 -2
- data/lib/riak/version.rb +1 -1
- data/riak-client.gemspec +3 -3
- data/spec/failover/failover.rb +59 -0
- data/spec/integration/riak/http_backends_spec.rb +2 -2
- data/spec/integration/riak/node_spec.rb +16 -24
- data/spec/integration/riak/protobuffs_backends_spec.rb +1 -1
- data/spec/integration/riak/test_server_spec.rb +4 -3
- data/spec/integration/riak/threading_spec.rb +193 -0
- data/spec/riak/beefcake_protobuffs_backend/object_methods_spec.rb +23 -0
- data/spec/riak/beefcake_protobuffs_backend_spec.rb +4 -2
- data/spec/riak/bucket_spec.rb +2 -1
- data/spec/riak/client_spec.rb +80 -181
- data/spec/riak/excon_backend_spec.rb +3 -2
- data/spec/riak/http_backend/configuration_spec.rb +37 -5
- data/spec/riak/http_backend/object_methods_spec.rb +1 -1
- data/spec/riak/http_backend/transport_methods_spec.rb +2 -2
- data/spec/riak/http_backend_spec.rb +53 -3
- data/spec/riak/map_reduce_spec.rb +1 -1
- data/spec/riak/net_http_backend_spec.rb +1 -2
- data/spec/riak/node_spec.rb +173 -0
- data/spec/riak/pool_spec.rb +306 -0
- data/spec/riak/robject_spec.rb +8 -4
- data/spec/riak/search_spec.rb +66 -15
- data/spec/riak/serializers_spec.rb +12 -1
- data/spec/spec_helper.rb +9 -1
- data/spec/support/http_backend_implementation_examples.rb +6 -2
- data/spec/support/sometimes.rb +46 -0
- data/spec/support/test_server.rb +50 -19
- data/spec/support/unified_backend_examples.rb +11 -10
- data/spec/support/version_filter.rb +14 -0
- metadata +40 -29
- data/lib/active_support/cache/riak_store.rb +0 -2
- data/lib/riak/cache_store.rb +0 -84
- data/lib/riak/client/pump.rb +0 -30
- data/lib/riak/util/fiber1.8.rb +0 -48
- data/spec/integration/riak/cache_store_spec.rb +0 -129
data/spec/riak/robject_spec.rb
CHANGED
@@ -214,6 +214,10 @@ describe Riak::RObject do
|
|
214
214
|
@object.conflict?.should be_false
|
215
215
|
end
|
216
216
|
|
217
|
+
it 'should return [self] for siblings' do
|
218
|
+
@object.siblings.should == [@object]
|
219
|
+
end
|
220
|
+
|
217
221
|
describe "when there are multiple values in an object" do
|
218
222
|
before :each do
|
219
223
|
response = @sample_response.dup
|
@@ -257,7 +261,7 @@ describe Riak::RObject do
|
|
257
261
|
describe "when storing the object normally" do
|
258
262
|
before :each do
|
259
263
|
@backend = mock("Backend")
|
260
|
-
@client.stub!(:backend).
|
264
|
+
@client.stub!(:backend).and_yield(@backend)
|
261
265
|
@object = Riak::RObject.new(@bucket)
|
262
266
|
@object.content_type = "text/plain"
|
263
267
|
@object.data = "This is some text."
|
@@ -278,7 +282,7 @@ describe Riak::RObject do
|
|
278
282
|
describe "when reloading the object" do
|
279
283
|
before :each do
|
280
284
|
@backend = mock("Backend")
|
281
|
-
@client.stub!(:backend).
|
285
|
+
@client.stub!(:backend).and_yield(@backend)
|
282
286
|
@object = Riak::RObject.new(@bucket, "bar")
|
283
287
|
@object.vclock = "somereallylongstring"
|
284
288
|
end
|
@@ -318,7 +322,7 @@ describe Riak::RObject do
|
|
318
322
|
describe "walking from the object to linked objects" do
|
319
323
|
before :each do
|
320
324
|
@http = mock("HTTPBackend")
|
321
|
-
@client.stub!(:http).
|
325
|
+
@client.stub!(:http).and_yield(@http)
|
322
326
|
@client.stub!(:bucket).and_return(@bucket)
|
323
327
|
@object = Riak::RObject.new(@bucket, "bar")
|
324
328
|
end
|
@@ -332,7 +336,7 @@ describe Riak::RObject do
|
|
332
336
|
describe "when deleting" do
|
333
337
|
before :each do
|
334
338
|
@backend = mock("Backend")
|
335
|
-
@client.stub!(:backend).
|
339
|
+
@client.stub!(:backend).and_yield(@backend)
|
336
340
|
@object = Riak::RObject.new(@bucket, "bar")
|
337
341
|
end
|
338
342
|
|
data/spec/riak/search_spec.rb
CHANGED
@@ -1,11 +1,12 @@
|
|
1
1
|
require 'spec_helper'
|
2
|
+
require 'rexml/document'
|
2
3
|
|
3
4
|
describe "Search features" do
|
4
5
|
describe Riak::Client do
|
5
6
|
before :each do
|
6
7
|
@client = Riak::Client.new
|
7
8
|
@http = mock(Riak::Client::HTTPBackend)
|
8
|
-
@client.stub!(:http).
|
9
|
+
@client.stub!(:http).and_yield(@http)
|
9
10
|
end
|
10
11
|
|
11
12
|
describe "searching" do
|
@@ -28,20 +29,40 @@ describe "Search features" do
|
|
28
29
|
describe "indexing documents" do
|
29
30
|
it "should update the default index" do
|
30
31
|
doc = {'field' => "value", 'id' => 1}
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
32
|
+
expect_update_body do |body|
|
33
|
+
root = REXML::Document.new(body).root
|
34
|
+
root.name.should == 'add'
|
35
|
+
root.elements.should have(1).item
|
36
|
+
root.elements[1].each_element do |el|
|
37
|
+
case el.attributes['name']
|
38
|
+
when 'field'
|
39
|
+
el.text.should == 'value'
|
40
|
+
when 'id'
|
41
|
+
el.text.should == '1'
|
42
|
+
else
|
43
|
+
fail "Spurious element in #{root.elements[1]}! #{el}"
|
44
|
+
end
|
45
|
+
end
|
35
46
|
end
|
36
47
|
@client.index(doc)
|
37
48
|
end
|
38
49
|
|
39
50
|
it "should update the specified index" do
|
40
51
|
doc = {'field' => "value", 'id' => 1}
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
52
|
+
expect_update_body(nil, "foo") do |body|
|
53
|
+
root = REXML::Document.new(body).root
|
54
|
+
root.name.should == 'add'
|
55
|
+
root.elements.should have(1).item
|
56
|
+
root.elements[1].each_element do |el|
|
57
|
+
case el.attributes['name']
|
58
|
+
when 'field'
|
59
|
+
el.text.should == 'value'
|
60
|
+
when 'id'
|
61
|
+
el.text.should == '1'
|
62
|
+
else
|
63
|
+
fail "Spurious element in #{root.elements[1]}! #{el}"
|
64
|
+
end
|
65
|
+
end
|
45
66
|
end
|
46
67
|
@client.index("foo", doc)
|
47
68
|
end
|
@@ -54,10 +75,34 @@ describe "Search features" do
|
|
54
75
|
|
55
76
|
it "should include multiple documents in the <add> request" do
|
56
77
|
docs = {'field' => "value", 'id' => 1}, {'foo' => "bar", 'id' => 2}
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
78
|
+
expect_update_body do |body|
|
79
|
+
root = REXML::Document.new(body).root
|
80
|
+
root.name.should == 'add'
|
81
|
+
docs = root.elements
|
82
|
+
docs.each do |d|
|
83
|
+
d.name.should == 'doc'
|
84
|
+
d.elements.size.should == 2
|
85
|
+
end
|
86
|
+
docs[1].each_element do |el|
|
87
|
+
case el.attributes['name']
|
88
|
+
when 'field'
|
89
|
+
el.text.should == 'value'
|
90
|
+
when 'id'
|
91
|
+
el.text.should == '1'
|
92
|
+
else
|
93
|
+
fail "Spurious element in #{docs[0]}! #{el}"
|
94
|
+
end
|
95
|
+
end
|
96
|
+
docs[2].each_element do |el|
|
97
|
+
case el.attributes['name']
|
98
|
+
when 'foo'
|
99
|
+
el.text.should == 'bar'
|
100
|
+
when 'id'
|
101
|
+
el.text.should == '2'
|
102
|
+
else
|
103
|
+
fail "Spurious element in #{docs[1]}! #{el}"
|
104
|
+
end
|
105
|
+
end
|
61
106
|
end
|
62
107
|
@client.index(*docs)
|
63
108
|
end
|
@@ -93,8 +138,14 @@ describe "Search features" do
|
|
93
138
|
end
|
94
139
|
end
|
95
140
|
|
96
|
-
def expect_update_body(body, index=nil)
|
97
|
-
|
141
|
+
def expect_update_body(body=nil, index=nil)
|
142
|
+
if block_given?
|
143
|
+
@http.should_receive(:update_search_index).with(index, kind_of(String)) do |i, b|
|
144
|
+
yield b
|
145
|
+
end
|
146
|
+
else
|
147
|
+
@http.should_receive(:update_search_index).with(index, body)
|
148
|
+
end
|
98
149
|
end
|
99
150
|
end
|
100
151
|
|
@@ -73,7 +73,7 @@ describe Riak::Serializers do
|
|
73
73
|
end
|
74
74
|
|
75
75
|
def o.load(string)
|
76
|
-
string.sub
|
76
|
+
string.sub(/^The string is: /, '')
|
77
77
|
end
|
78
78
|
end
|
79
79
|
end
|
@@ -81,6 +81,7 @@ describe Riak::Serializers do
|
|
81
81
|
it 'can be registered' do
|
82
82
|
described_class['application/custom-type-1'] = custom_serializer
|
83
83
|
described_class['application/custom-type-1'].should be(custom_serializer)
|
84
|
+
# fail
|
84
85
|
end
|
85
86
|
|
86
87
|
it_behaves_like "a serializer", "application/custom-type-a", "foo", "The string is: foo" do
|
@@ -88,6 +89,16 @@ describe Riak::Serializers do
|
|
88
89
|
described_class['application/custom-type-a'] = custom_serializer
|
89
90
|
end
|
90
91
|
end
|
92
|
+
|
93
|
+
after(:each) do
|
94
|
+
# Make sure to clean up the registered serializer
|
95
|
+
%w{application/custom-type-1 application/custom-type-a}.each do |ctype|
|
96
|
+
described_class.send(:serializers).delete(ctype)
|
97
|
+
end
|
98
|
+
end
|
91
99
|
end
|
100
|
+
|
101
|
+
it_behaves_like "a serializer", "application/json; charset=UTF-8", { "a" => 7 }, %q|{"a":7}|
|
102
|
+
it_behaves_like "a serializer", "application/json ;charset=UTF-8", { "a" => 7 }, %q|{"a":7}|
|
92
103
|
end
|
93
104
|
|
data/spec/spec_helper.rb
CHANGED
@@ -10,6 +10,8 @@ require 'fakeweb'
|
|
10
10
|
Riak.disable_list_keys_warnings = true
|
11
11
|
|
12
12
|
%w[integration_setup
|
13
|
+
version_filter
|
14
|
+
sometimes
|
13
15
|
http_backend_implementation_examples
|
14
16
|
unified_backend_examples
|
15
17
|
mocks
|
@@ -20,7 +22,7 @@ Riak.disable_list_keys_warnings = true
|
|
20
22
|
end
|
21
23
|
|
22
24
|
RSpec.configure do |config|
|
23
|
-
config.debug = true
|
25
|
+
#config.debug = true
|
24
26
|
config.mock_with :rspec
|
25
27
|
|
26
28
|
config.before(:all, :integration => true) do
|
@@ -38,4 +40,10 @@ RSpec.configure do |config|
|
|
38
40
|
|
39
41
|
config.filter_run :focus => true
|
40
42
|
config.run_all_when_everything_filtered = true
|
43
|
+
|
44
|
+
if defined?(::Java)
|
45
|
+
config.seed = Time.now.utc
|
46
|
+
else
|
47
|
+
config.order = :random
|
48
|
+
end
|
41
49
|
end
|
@@ -185,7 +185,11 @@ shared_examples_for "HTTP backend" do
|
|
185
185
|
|
186
186
|
describe "SSL" do
|
187
187
|
it "should be supported" do
|
188
|
-
|
188
|
+
unless @client.http_backend == :NetHTTP
|
189
|
+
@client.nodes.each do |node|
|
190
|
+
node.http_port = $mock_server.port + 1
|
191
|
+
end
|
192
|
+
end
|
189
193
|
@client.ssl = true
|
190
194
|
setup_http_mock(:get, @backend.path("/riak/","ssl").to_s, :body => "Success!")
|
191
195
|
response = @backend.get(200, @backend.path("/riak/","ssl"))
|
@@ -194,7 +198,7 @@ shared_examples_for "HTTP backend" do
|
|
194
198
|
end
|
195
199
|
|
196
200
|
describe "HTTP Basic Authentication", :basic_auth => true do
|
197
|
-
|
201
|
+
sometimes "should add the http basic auth header" do
|
198
202
|
@client.basic_auth = "ripple:rocks"
|
199
203
|
if @client.http_backend == :NetHTTP
|
200
204
|
setup_http_mock(:get, "http://ripple:rocks@127.0.0.1:8098/riak/auth", :body => 'Success!')
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# Emulates the QuickCheck ?SOMETIMES macro.
|
2
|
+
|
3
|
+
module Sometimes
|
4
|
+
def run_with_retries(example_to_run, retries)
|
5
|
+
self.example.metadata[:retries] ||= retries
|
6
|
+
retries.times do |t|
|
7
|
+
self.example.metadata[:retried] = t + 1
|
8
|
+
self.example.instance_variable_set(:@exception, nil)
|
9
|
+
example_to_run.run
|
10
|
+
break unless self.example.exception
|
11
|
+
end
|
12
|
+
if e = self.example.exception
|
13
|
+
new_exception = e.exception(e.message + "[Retried #{retries} times]")
|
14
|
+
new_exception.set_backtrace e.backtrace
|
15
|
+
self.example.instance_variable_set(:@exception, new_exception)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
RSpec.configure do |config|
|
21
|
+
config.include Sometimes
|
22
|
+
config.alias_example_to :sometimes, :sometimes => true
|
23
|
+
config.add_setting :sometimes_retry_count, :default => 3
|
24
|
+
|
25
|
+
config.around(:each, :sometimes => true) do |example|
|
26
|
+
retries = example.metadata[:retries] || RSpec.configuration.sometimes_retry_count
|
27
|
+
run_with_retries(example, retries)
|
28
|
+
end
|
29
|
+
|
30
|
+
config.after(:suite) do
|
31
|
+
formatter = RSpec.configuration.formatters.first
|
32
|
+
color = lambda {|tint, msg| formatter.send(tint, msg) }
|
33
|
+
retried_examples = RSpec.world.example_groups.map do |g|
|
34
|
+
g.descendants.map do |d|
|
35
|
+
d.filtered_examples.select {|e| e.metadata[:sometimes] && e.metadata[:retried] > 1 }
|
36
|
+
end
|
37
|
+
end.flatten
|
38
|
+
formatter.message color[retried_examples.empty? ? :green : :yellow, "\n\nRetried examples: #{retried_examples.count}"]
|
39
|
+
unless retried_examples.empty?
|
40
|
+
retried_examples.each do |e|
|
41
|
+
formatter.message " #{e.full_description}"
|
42
|
+
formatter.message(color[:yellow, " [#{e.metadata[:retried]}/#{e.metadata[:retries]}] "] + formatter.class.relative_path(e.location))
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
data/spec/support/test_server.rb
CHANGED
@@ -1,30 +1,61 @@
|
|
1
1
|
require 'riak/test_server'
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
3
|
+
module TestServerSupport
|
4
|
+
def test_server
|
5
|
+
unless $test_server
|
6
|
+
begin
|
7
|
+
require 'yaml'
|
8
|
+
config = YAML.load_file(File.expand_path("../test_server.yml", __FILE__))
|
8
9
|
$test_server = Riak::TestServer.create(:root => config['root'],
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
10
|
+
:source => config['source'],
|
11
|
+
:min_port => config['min_port'] || 15000)
|
12
|
+
rescue SocketError => e
|
13
|
+
warn "Couldn't connect to Riak TestServer! #{$test_server.inspect}"
|
14
|
+
warn "Skipping remaining integration tests."
|
15
|
+
warn_crash_log
|
16
|
+
$test_server_fatal = e
|
17
|
+
rescue => e
|
18
|
+
warn "Can't run integration specs without the test server. Please create/verify spec/support/test_server.yml."
|
19
|
+
warn "Skipping remaining integration tests."
|
20
|
+
warn e.inspect
|
21
|
+
warn_crash_log
|
22
|
+
$test_server_fatal = e
|
18
23
|
end
|
19
|
-
|
20
|
-
|
21
|
-
|
24
|
+
end
|
25
|
+
$test_server
|
26
|
+
end
|
27
|
+
|
28
|
+
def test_server_fatal
|
29
|
+
$test_server_fatal
|
30
|
+
end
|
31
|
+
|
32
|
+
def warn_crash_log
|
33
|
+
if $test_server && crash_log = $test_server.log + 'crash.log'
|
34
|
+
warn crash_log.read if crash_log.exist?
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
RSpec.configure do |config|
|
40
|
+
config.include TestServerSupport, :integration => true
|
41
|
+
|
42
|
+
config.before(:each, :integration => true) do
|
43
|
+
fail "Test server not working: #{test_server_fatal}" if test_server_fatal
|
44
|
+
if example.metadata[:test_server] == false
|
45
|
+
test_server.stop
|
46
|
+
else
|
47
|
+
test_server.create unless test_server.exist?
|
48
|
+
test_server.start
|
22
49
|
end
|
23
50
|
end
|
24
51
|
|
25
52
|
config.after(:each, :integration => true) do
|
26
|
-
if
|
27
|
-
|
53
|
+
if test_server && !test_server_fatal && example.metadata[:test_server] != false
|
54
|
+
test_server.drop
|
28
55
|
end
|
29
56
|
end
|
57
|
+
|
58
|
+
config.after(:suite) do
|
59
|
+
$test_server.stop if $test_server
|
60
|
+
end
|
30
61
|
end
|
@@ -9,9 +9,9 @@ shared_examples_for "Unified backend API" do
|
|
9
9
|
context "fetching an object" do
|
10
10
|
before do
|
11
11
|
@robject = Riak::RObject.new(@client.bucket("test"), "fetch")
|
12
|
-
@robject.indexes['test_bin'] << 'pass'
|
13
12
|
@robject.content_type = "application/json"
|
14
13
|
@robject.data = { "test" => "pass" }
|
14
|
+
@robject.indexes['test_bin'] << 'pass' if test_server.version >= "1.0.0"
|
15
15
|
@backend.store_object(@robject)
|
16
16
|
end
|
17
17
|
|
@@ -38,15 +38,14 @@ shared_examples_for "Unified backend API" do
|
|
38
38
|
robj.data.should == { "test" => "pass" }
|
39
39
|
end
|
40
40
|
|
41
|
-
it "should accept a PR value of #{q.inspect} for the request" do
|
41
|
+
it "should accept a PR value of #{q.inspect} for the request", :version => "1.0.0" do
|
42
42
|
robj = @backend.fetch_object("test", "fetch", :pr => q)
|
43
43
|
robj.should be_kind_of(Riak::RObject)
|
44
44
|
robj.data.should == { "test" => "pass" }
|
45
45
|
end
|
46
46
|
end
|
47
47
|
|
48
|
-
|
49
|
-
# This really tests both storing and fetching indexes, given the setup
|
48
|
+
sometimes "should marshal indexes properly", :version => "1.0.0", :retries => 5 do
|
50
49
|
robj = @backend.fetch_object('test', 'fetch')
|
51
50
|
robj.indexes['test_bin'].should be
|
52
51
|
robj.indexes['test_bin'].should include('pass')
|
@@ -74,14 +73,16 @@ shared_examples_for "Unified backend API" do
|
|
74
73
|
@backend.reload_object(@robject, :r => q)
|
75
74
|
end
|
76
75
|
|
77
|
-
it "should accept a valid PR value of #{q.inspect} for the request" do
|
76
|
+
it "should accept a valid PR value of #{q.inspect} for the request", :version => "1.0.0" do
|
78
77
|
@backend.reload_object(@robject, :pr => q)
|
79
78
|
end
|
80
79
|
end
|
81
80
|
|
82
81
|
after do
|
83
|
-
|
84
|
-
|
82
|
+
unless example.pending?
|
83
|
+
@robject.vclock.should == @robject2.vclock
|
84
|
+
@robject.data['test'].should == "second"
|
85
|
+
end
|
85
86
|
end
|
86
87
|
end
|
87
88
|
|
@@ -112,7 +113,7 @@ shared_examples_for "Unified backend API" do
|
|
112
113
|
@backend.store_object(@robject, :returnbody => false, :w => :all, :dw => q)
|
113
114
|
end
|
114
115
|
|
115
|
-
it "should accept a PW value of #{q.inspect} for the request" do
|
116
|
+
it "should accept a PW value of #{q.inspect} for the request", :version => "1.0.0" do
|
116
117
|
@backend.store_object(@robject, :returnbody => false, :pw => q)
|
117
118
|
end
|
118
119
|
end
|
@@ -206,7 +207,7 @@ shared_examples_for "Unified backend API" do
|
|
206
207
|
@backend.list_keys("test") do |keys|
|
207
208
|
keys.each do |key|
|
208
209
|
begin
|
209
|
-
@
|
210
|
+
@client.get_object("test", key)
|
210
211
|
rescue => e
|
211
212
|
errors << e
|
212
213
|
end
|
@@ -263,7 +264,7 @@ shared_examples_for "Unified backend API" do
|
|
263
264
|
unless result.empty?
|
264
265
|
result.each do |v|
|
265
266
|
begin
|
266
|
-
@
|
267
|
+
@client.get_object("test", v['value'])
|
267
268
|
rescue => e
|
268
269
|
errors << e
|
269
270
|
end
|