- Implements Apache APISIX packaging for Cloudron platform. - Includes Dockerfile, CloudronManifest.json, and start.sh. - Configured to use Cloudron's etcd addon. 🤖 Generated with Gemini CLI Co-Authored-By: Gemini <noreply@google.com>
784 lines
22 KiB
Perl
784 lines
22 KiB
Perl
#
|
|
# Licensed to the Apache Software Foundation (ASF) under one or more
|
|
# contributor license agreements. See the NOTICE file distributed with
|
|
# this work for additional information regarding copyright ownership.
|
|
# The ASF licenses this file to You under the Apache License, Version 2.0
|
|
# (the "License"); you may not use this file except in compliance with
|
|
# the License. You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
#
|
|
use t::APISIX;
|
|
|
|
my $nginx_binary = $ENV{'TEST_NGINX_BINARY'} || 'nginx';
|
|
my $version = eval { `$nginx_binary -V 2>&1` };
|
|
|
|
if ($version !~ m/\/apisix-nginx-module/) {
|
|
plan(skip_all => "apisix-nginx-module not installed");
|
|
} else {
|
|
plan('no_plan');
|
|
}
|
|
|
|
$ENV{TEST_NGINX_REDIS_PORT} ||= 1985;
|
|
|
|
add_block_preprocessor(sub {
|
|
my ($block) = @_;
|
|
|
|
if (!$block->extra_yaml_config) {
|
|
my $extra_yaml_config = <<_EOC_;
|
|
xrpc:
|
|
protocols:
|
|
- name: redis
|
|
_EOC_
|
|
$block->set_value("extra_yaml_config", $extra_yaml_config);
|
|
}
|
|
|
|
if ((!defined $block->error_log) && (!defined $block->no_error_log)) {
|
|
$block->set_value("no_error_log", "[error]\nRPC is not finished");
|
|
}
|
|
|
|
if (!defined $block->request) {
|
|
$block->set_value("request", "GET /t");
|
|
}
|
|
|
|
$block;
|
|
});
|
|
|
|
worker_connections(1024);
|
|
run_tests;
|
|
|
|
__DATA__
|
|
|
|
=== TEST 1: init
|
|
--- config
|
|
location /t {
|
|
content_by_lua_block {
|
|
local t = require("lib.test_admin").test
|
|
local code, body = t('/apisix/admin/stream_routes/1',
|
|
ngx.HTTP_PUT,
|
|
{
|
|
protocol = {
|
|
name = "redis"
|
|
},
|
|
upstream = {
|
|
nodes = {
|
|
["127.0.0.1:6379"] = 1
|
|
},
|
|
type = "roundrobin"
|
|
}
|
|
}
|
|
)
|
|
if code >= 300 then
|
|
ngx.status = code
|
|
end
|
|
ngx.say(body)
|
|
}
|
|
}
|
|
--- response_body
|
|
passed
|
|
|
|
|
|
|
|
=== TEST 2: sanity
|
|
--- config
|
|
location /t {
|
|
content_by_lua_block {
|
|
local redis = require "resty.redis"
|
|
local red = redis:new()
|
|
|
|
local ok, err = red:connect("127.0.0.1", $TEST_NGINX_REDIS_PORT)
|
|
if not ok then
|
|
ngx.say("failed to connect: ", err)
|
|
return
|
|
end
|
|
|
|
local res, err = red:hmset("animals", "dog", "bark", "cat", "meow")
|
|
if not res then
|
|
ngx.say("failed to set animals: ", err)
|
|
return
|
|
end
|
|
ngx.say("hmset animals: ", res)
|
|
|
|
local res, err = red:hmget("animals", "dog", "cat")
|
|
if not res then
|
|
ngx.say("failed to get animals: ", err)
|
|
return
|
|
end
|
|
|
|
ngx.say("hmget animals: ", res)
|
|
|
|
local res, err = red:hget("animals", "dog")
|
|
if not res then
|
|
ngx.say("failed to get animals: ", err)
|
|
return
|
|
end
|
|
|
|
ngx.say("hget animals: ", res)
|
|
|
|
local res, err = red:hget("animals", "not_found")
|
|
if not res then
|
|
ngx.say("failed to get animals: ", err)
|
|
return
|
|
end
|
|
|
|
ngx.say("hget animals: ", res)
|
|
}
|
|
}
|
|
--- response_body
|
|
hmset animals: OK
|
|
hmget animals: barkmeow
|
|
hget animals: bark
|
|
hget animals: null
|
|
--- stream_conf_enable
|
|
|
|
|
|
|
|
=== TEST 3: error
|
|
--- config
|
|
location /t {
|
|
content_by_lua_block {
|
|
local redis = require "resty.redis"
|
|
local red = redis:new()
|
|
|
|
local ok, err = red:connect("127.0.0.1", $TEST_NGINX_REDIS_PORT)
|
|
if not ok then
|
|
ngx.say("failed to connect: ", err)
|
|
return
|
|
end
|
|
|
|
local res, err = red:get("animals")
|
|
if not res then
|
|
ngx.say("failed to set animals: ", err)
|
|
end
|
|
|
|
local res, err = red:hget("animals", "dog")
|
|
if not res then
|
|
ngx.say("failed to get animals: ", err)
|
|
return
|
|
end
|
|
|
|
ngx.say("hget animals: ", res)
|
|
}
|
|
}
|
|
--- response_body
|
|
failed to set animals: WRONGTYPE Operation against a key holding the wrong kind of value
|
|
hget animals: bark
|
|
--- stream_conf_enable
|
|
|
|
|
|
|
|
=== TEST 4: big value
|
|
--- config
|
|
location /t {
|
|
content_by_lua_block {
|
|
local redis = require "resty.redis"
|
|
local red = redis:new()
|
|
|
|
local ok, err = red:connect("127.0.0.1", $TEST_NGINX_REDIS_PORT)
|
|
if not ok then
|
|
ngx.say("failed to connect: ", err)
|
|
return
|
|
end
|
|
|
|
local res, err = red:set("big-key", ("\r\n"):rep(1024 * 1024 * 16))
|
|
if not res then
|
|
ngx.say("failed to set: ", err)
|
|
return
|
|
end
|
|
|
|
local res, err = red:get("big-key")
|
|
if not res then
|
|
ngx.say("failed to get: ", err)
|
|
return
|
|
end
|
|
|
|
ngx.print(res)
|
|
}
|
|
}
|
|
--- response_body eval
|
|
"\r\n" x 16777216
|
|
--- stream_conf_enable
|
|
|
|
|
|
|
|
=== TEST 5: pipeline
|
|
--- config
|
|
location /t {
|
|
content_by_lua_block {
|
|
local cjson = require("cjson")
|
|
local redis = require "resty.redis"
|
|
|
|
local t = {}
|
|
for i = 1, 180 do
|
|
local th = assert(ngx.thread.spawn(function(i)
|
|
local red = redis:new()
|
|
local ok, err = red:connect("127.0.0.1", $TEST_NGINX_REDIS_PORT)
|
|
if not ok then
|
|
ngx.say("failed to connect: ", err)
|
|
return
|
|
end
|
|
|
|
red:init_pipeline()
|
|
|
|
red:set("mark_" .. i, i)
|
|
red:get("mark_" .. i)
|
|
red:get("counter")
|
|
for j = 1, 4 do
|
|
red:incr("counter")
|
|
end
|
|
|
|
local results, err = red:commit_pipeline()
|
|
if not results then
|
|
ngx.say("failed to commit: ", err)
|
|
return
|
|
end
|
|
|
|
local begin = tonumber(results[3])
|
|
for j = 1, 4 do
|
|
local incred = results[3 + j]
|
|
if incred ~= results[2 + j] + 1 then
|
|
ngx.log(ngx.ERR, cjson.encode(results))
|
|
end
|
|
end
|
|
end, i))
|
|
table.insert(t, th)
|
|
end
|
|
for i, th in ipairs(t) do
|
|
ngx.thread.wait(th)
|
|
end
|
|
}
|
|
}
|
|
--- response_body
|
|
--- stream_conf_enable
|
|
|
|
|
|
|
|
=== TEST 6: delay
|
|
--- config
|
|
location /t {
|
|
content_by_lua_block {
|
|
local t = require("lib.test_admin").test
|
|
local code, body = t('/apisix/admin/stream_routes/1',
|
|
ngx.HTTP_PUT,
|
|
{
|
|
protocol = {
|
|
name = "redis",
|
|
conf = {
|
|
faults = {
|
|
{delay = 0.01, key = "ignored", commands = {"Ping", "time"}}
|
|
}
|
|
}
|
|
},
|
|
upstream = {
|
|
nodes = {
|
|
["127.0.0.1:6379"] = 1
|
|
},
|
|
type = "roundrobin"
|
|
}
|
|
}
|
|
)
|
|
if code >= 300 then
|
|
ngx.status = code
|
|
end
|
|
ngx.say(body)
|
|
}
|
|
}
|
|
--- response_body
|
|
passed
|
|
|
|
|
|
|
|
=== TEST 7: hit
|
|
--- config
|
|
location /t {
|
|
content_by_lua_block {
|
|
local redis = require "resty.redis"
|
|
local red = redis:new()
|
|
|
|
local ok, err = red:connect("127.0.0.1", $TEST_NGINX_REDIS_PORT)
|
|
if not ok then
|
|
ngx.say("failed to connect: ", err)
|
|
return
|
|
end
|
|
|
|
local start = ngx.now()
|
|
local res, err = red:ping()
|
|
if not res then
|
|
ngx.say(err)
|
|
return
|
|
end
|
|
local now = ngx.now()
|
|
-- use integer to bypass float point number precision problem
|
|
if math.ceil((now - start) * 1000) < 10 then
|
|
ngx.say(now, " ", start)
|
|
return
|
|
end
|
|
start = now
|
|
|
|
local res, err = red:time()
|
|
if not res then
|
|
ngx.say(err)
|
|
return
|
|
end
|
|
local now = ngx.now()
|
|
if math.ceil((now - start) * 1000) < 10 then
|
|
ngx.say(now, " ", start)
|
|
return
|
|
end
|
|
start = now
|
|
|
|
red:init_pipeline()
|
|
red:time()
|
|
red:time()
|
|
red:get("A")
|
|
|
|
local results, err = red:commit_pipeline()
|
|
if not results then
|
|
ngx.say("failed to commit: ", err)
|
|
return
|
|
end
|
|
local now = ngx.now()
|
|
if math.ceil((now - start) * 1000) < 20 or math.ceil((now - start) * 1000) > 30 then
|
|
ngx.say(now, " ", start)
|
|
return
|
|
end
|
|
|
|
ngx.say("ok")
|
|
}
|
|
}
|
|
--- response_body
|
|
ok
|
|
--- stream_conf_enable
|
|
|
|
|
|
|
|
=== TEST 8: DFS match
|
|
--- config
|
|
location /t {
|
|
content_by_lua_block {
|
|
local t = require("lib.test_admin").test
|
|
local code, body = t('/apisix/admin/stream_routes/1',
|
|
ngx.HTTP_PUT,
|
|
{
|
|
protocol = {
|
|
name = "redis",
|
|
conf = {
|
|
faults = {
|
|
{delay = 0.02, key = "a", commands = {"get"}},
|
|
{delay = 0.01, commands = {"get", "set"}},
|
|
}
|
|
}
|
|
},
|
|
upstream = {
|
|
nodes = {
|
|
["127.0.0.1:6379"] = 1
|
|
},
|
|
type = "roundrobin"
|
|
}
|
|
}
|
|
)
|
|
if code >= 300 then
|
|
ngx.status = code
|
|
end
|
|
ngx.say(body)
|
|
}
|
|
}
|
|
--- response_body
|
|
passed
|
|
|
|
|
|
|
|
=== TEST 9: hit
|
|
--- config
|
|
location /t {
|
|
content_by_lua_block {
|
|
local redis = require "resty.redis"
|
|
local red = redis:new()
|
|
|
|
local ok, err = red:connect("127.0.0.1", $TEST_NGINX_REDIS_PORT)
|
|
if not ok then
|
|
ngx.say("failed to connect: ", err)
|
|
return
|
|
end
|
|
|
|
local start = ngx.now()
|
|
local res, err = red:get("a")
|
|
if not res then
|
|
ngx.say(err)
|
|
return
|
|
end
|
|
local now = ngx.now()
|
|
if math.ceil((now - start) * 1000) < 20 then
|
|
ngx.say(now, " ", start)
|
|
return
|
|
end
|
|
start = now
|
|
|
|
local res, err = red:set("a", "a")
|
|
if not res then
|
|
ngx.say(err)
|
|
return
|
|
end
|
|
local now = ngx.now()
|
|
if math.ceil((now - start) * 1000) < 10 then
|
|
ngx.say(now, " ", start)
|
|
return
|
|
end
|
|
start = now
|
|
|
|
red:init_pipeline()
|
|
red:get("b")
|
|
red:set("A", "a")
|
|
|
|
local results, err = red:commit_pipeline()
|
|
if not results then
|
|
ngx.say("failed to commit: ", err)
|
|
return
|
|
end
|
|
local now = ngx.now()
|
|
if math.ceil((now - start) * 1000) < 20 or math.ceil((now - start) * 1000) > 30 then
|
|
ngx.say(now, " ", start)
|
|
return
|
|
end
|
|
|
|
ngx.say("ok")
|
|
}
|
|
}
|
|
--- response_body
|
|
ok
|
|
--- stream_conf_enable
|
|
|
|
|
|
|
|
=== TEST 10: multi keys
|
|
--- config
|
|
location /t {
|
|
content_by_lua_block {
|
|
local t = require("lib.test_admin").test
|
|
local code, body = t('/apisix/admin/stream_routes/1',
|
|
ngx.HTTP_PUT,
|
|
{
|
|
protocol = {
|
|
name = "redis",
|
|
conf = {
|
|
faults = {
|
|
{delay = 0.06, key = "b", commands = {"del"}},
|
|
{delay = 0.04, key = "a", commands = {"mset"}},
|
|
{delay = 0.02, key = "b", commands = {"mset"}},
|
|
}
|
|
}
|
|
},
|
|
upstream = {
|
|
nodes = {
|
|
["127.0.0.1:6379"] = 1
|
|
},
|
|
type = "roundrobin"
|
|
}
|
|
}
|
|
)
|
|
if code >= 300 then
|
|
ngx.status = code
|
|
end
|
|
ngx.say(body)
|
|
}
|
|
}
|
|
--- response_body
|
|
passed
|
|
|
|
|
|
|
|
=== TEST 11: hit
|
|
--- config
|
|
location /t {
|
|
content_by_lua_block {
|
|
local redis = require "resty.redis"
|
|
local red = redis:new()
|
|
|
|
local ok, err = red:connect("127.0.0.1", $TEST_NGINX_REDIS_PORT)
|
|
if not ok then
|
|
ngx.say("failed to connect: ", err)
|
|
return
|
|
end
|
|
|
|
local start = ngx.now()
|
|
local res, err = red:mset("c", 1, "a", 2, "b", 3)
|
|
if not res then
|
|
ngx.say(err)
|
|
return
|
|
end
|
|
local now = ngx.now()
|
|
if math.ceil((now - start) * 1000) < 40 then
|
|
ngx.say("mset a ", now, " ", start)
|
|
return
|
|
end
|
|
start = now
|
|
|
|
local res, err = red:mset("b", 2, "a", 3)
|
|
if not res then
|
|
ngx.say(err)
|
|
return
|
|
end
|
|
local now = ngx.now()
|
|
if math.ceil((now - start) * 1000) < 20 or math.ceil((now - start) * 1000) > 35 then
|
|
ngx.say("mset b ", now, " ", start)
|
|
return
|
|
end
|
|
start = now
|
|
|
|
local res, err = red:mset("c", "a")
|
|
if not res then
|
|
ngx.say(err)
|
|
return
|
|
end
|
|
local now = ngx.now()
|
|
if math.ceil((now - start) * 1000) > 20 then
|
|
ngx.say("mset mismatch ", now, " ", start)
|
|
return
|
|
end
|
|
start = now
|
|
|
|
local res, err = red:del("a", "b")
|
|
if not res then
|
|
ngx.say(err)
|
|
return
|
|
end
|
|
local now = ngx.now()
|
|
if math.ceil((now - start) * 1000) < 60 then
|
|
ngx.say("del b ", now, " ", start)
|
|
return
|
|
end
|
|
start = now
|
|
|
|
ngx.say("ok")
|
|
}
|
|
}
|
|
--- response_body
|
|
ok
|
|
--- stream_conf_enable
|
|
|
|
|
|
|
|
=== TEST 12: publish & subscribe
|
|
--- stream_extra_init_by_lua
|
|
local cjson = require "cjson"
|
|
local redis_proto = require("apisix.stream.xrpc.protocols.redis")
|
|
redis_proto.log = function(sess, ctx)
|
|
ngx.log(ngx.WARN, "log redis request ", cjson.encode(ctx.cmd_line))
|
|
end
|
|
|
|
--- config
|
|
location /t {
|
|
content_by_lua_block {
|
|
local cjson = require "cjson"
|
|
local redis = require "resty.redis"
|
|
|
|
local red = redis:new()
|
|
local red2 = redis:new()
|
|
|
|
red:set_timeout(1000) -- 1 sec
|
|
red2:set_timeout(1000) -- 1 sec
|
|
|
|
local ok, err = red:connect("127.0.0.1", $TEST_NGINX_REDIS_PORT)
|
|
if not ok then
|
|
ngx.say("1: failed to connect: ", err)
|
|
return
|
|
end
|
|
|
|
ok, err = red2:connect("127.0.0.1", $TEST_NGINX_REDIS_PORT)
|
|
if not ok then
|
|
ngx.say("2: failed to connect: ", err)
|
|
return
|
|
end
|
|
|
|
local res, err = red:subscribe("dog")
|
|
if not res then
|
|
ngx.say("1: failed to subscribe: ", err)
|
|
return
|
|
end
|
|
|
|
ngx.say("1: subscribe dog: ", cjson.encode(res))
|
|
|
|
res, err = red:subscribe("cat")
|
|
if not res then
|
|
ngx.say("1: failed to subscribe: ", err)
|
|
return
|
|
end
|
|
|
|
ngx.say("1: subscribe cat: ", cjson.encode(res))
|
|
|
|
res, err = red2:publish("dog", "Hello")
|
|
if not res then
|
|
ngx.say("2: failed to publish: ", err)
|
|
return
|
|
end
|
|
|
|
ngx.say("2: publish: ", cjson.encode(res))
|
|
|
|
res, err = red:read_reply()
|
|
if not res then
|
|
ngx.say("1: failed to read reply: ", err)
|
|
else
|
|
ngx.say("1: receive: ", cjson.encode(res))
|
|
end
|
|
|
|
red:set_timeout(10) -- 10ms
|
|
res, err = red:read_reply()
|
|
if not res then
|
|
ngx.say("1: failed to read reply: ", err)
|
|
else
|
|
ngx.say("1: receive: ", cjson.encode(res))
|
|
end
|
|
red:set_timeout(1000) -- 1s
|
|
|
|
res, err = red:unsubscribe()
|
|
if not res then
|
|
ngx.say("1: failed to unscribe: ", err)
|
|
else
|
|
ngx.say("1: unsubscribe: ", cjson.encode(res))
|
|
end
|
|
|
|
res, err = red:read_reply()
|
|
if not res then
|
|
ngx.say("1: failed to read reply: ", err)
|
|
else
|
|
ngx.say("1: receive: ", cjson.encode(res))
|
|
end
|
|
|
|
red:set_timeout(10) -- 10ms
|
|
res, err = red:read_reply()
|
|
if not res then
|
|
ngx.say("1: failed to read reply: ", err)
|
|
else
|
|
ngx.say("1: receive: ", cjson.encode(res))
|
|
end
|
|
red:set_timeout(1000) -- 1s
|
|
|
|
res, err = red:set("dog", 1)
|
|
if not res then
|
|
ngx.say("1: failed to set: ", err)
|
|
else
|
|
ngx.say("1: receive: ", cjson.encode(res))
|
|
end
|
|
|
|
red:close()
|
|
red2:close()
|
|
}
|
|
}
|
|
--- response_body_like chop
|
|
^1: subscribe dog: \["subscribe","dog",1\]
|
|
1: subscribe cat: \["subscribe","cat",2\]
|
|
2: publish: 1
|
|
1: receive: \["message","dog","Hello"\]
|
|
1: failed to read reply: timeout
|
|
1: unsubscribe: \[\["unsubscribe","(?:dog|cat)",1\],\["unsubscribe","(?:dog|cat)",0\]\]
|
|
1: failed to read reply: not subscribed
|
|
1: failed to read reply: not subscribed
|
|
1: receive: "OK"
|
|
$
|
|
--- stream_conf_enable
|
|
--- grep_error_log eval
|
|
qr/log redis request \[[^]]+\]/
|
|
--- grep_error_log_out
|
|
log redis request ["subscribe","dog"]
|
|
log redis request ["subscribe","cat"]
|
|
log redis request ["publish","dog","Hello"]
|
|
log redis request ["unsubscribe"]
|
|
log redis request ["set","dog","1"]
|
|
|
|
|
|
|
|
=== TEST 13: psubscribe & punsubscribe
|
|
--- stream_extra_init_by_lua
|
|
local cjson = require "cjson"
|
|
local redis_proto = require("apisix.stream.xrpc.protocols.redis")
|
|
redis_proto.log = function(sess, ctx)
|
|
ngx.log(ngx.WARN, "log redis request ", cjson.encode(ctx.cmd_line))
|
|
end
|
|
|
|
--- config
|
|
location /t {
|
|
content_by_lua_block {
|
|
local cjson = require "cjson"
|
|
local redis = require "resty.redis"
|
|
|
|
local red = redis:new()
|
|
local red2 = redis:new()
|
|
|
|
red:set_timeout(1000) -- 1 sec
|
|
red2:set_timeout(1000) -- 1 sec
|
|
|
|
local ok, err = red:connect("127.0.0.1", $TEST_NGINX_REDIS_PORT)
|
|
if not ok then
|
|
ngx.say("1: failed to connect: ", err)
|
|
return
|
|
end
|
|
|
|
ok, err = red2:connect("127.0.0.1", $TEST_NGINX_REDIS_PORT)
|
|
if not ok then
|
|
ngx.say("2: failed to connect: ", err)
|
|
return
|
|
end
|
|
|
|
local res, err = red:psubscribe("dog*", "cat*")
|
|
if not res then
|
|
ngx.say("1: failed to subscribe: ", err)
|
|
return
|
|
end
|
|
|
|
ngx.say("1: psubscribe: ", cjson.encode(res))
|
|
|
|
res, err = red2:publish("dog1", "Hello")
|
|
if not res then
|
|
ngx.say("2: failed to publish: ", err)
|
|
return
|
|
end
|
|
|
|
ngx.say("2: publish: ", cjson.encode(res))
|
|
|
|
res, err = red:read_reply()
|
|
if not res then
|
|
ngx.say("1: failed to read reply: ", err)
|
|
else
|
|
ngx.say("1: receive: ", cjson.encode(res))
|
|
end
|
|
|
|
res, err = red:punsubscribe("cat*", "dog*")
|
|
if not res then
|
|
ngx.say("1: failed to unscribe: ", err)
|
|
else
|
|
ngx.say("1: punsubscribe: ", cjson.encode(res))
|
|
end
|
|
|
|
res, err = red:set("dog", 1)
|
|
if not res then
|
|
ngx.say("1: failed to set: ", err)
|
|
else
|
|
ngx.say("1: receive: ", cjson.encode(res))
|
|
end
|
|
|
|
red:close()
|
|
red2:close()
|
|
}
|
|
}
|
|
--- response_body_like chop
|
|
^1: psubscribe: \[\["psubscribe","dog\*",1\],\["psubscribe","cat\*",2\]\]
|
|
2: publish: 1
|
|
1: receive: \["pmessage","dog\*","dog1","Hello"\]
|
|
1: punsubscribe: \[\["punsubscribe","cat\*",1\],\["punsubscribe","dog\*",0\]\]
|
|
1: receive: "OK"
|
|
$
|
|
--- stream_conf_enable
|
|
--- grep_error_log eval
|
|
qr/log redis request \[[^]]+\]/
|
|
--- grep_error_log_out
|
|
log redis request ["psubscribe","dog*","cat*"]
|
|
log redis request ["publish","dog1","Hello"]
|
|
log redis request ["punsubscribe","cat*","dog*"]
|
|
log redis request ["set","dog","1"]
|