[Tarantool-patches] [PATCH vshard 1/4] test: support luatest

Vladislav Shpilevoy v.shpilevoy at tarantool.org
Fri Feb 11 01:32:35 MSK 2022


Hi! Thanks for the review!

On 09.02.2022 18:53, Oleg Babin wrote:
> Thanks for your patch.
> 
> Am I right that it's partially imported from tarantool https://github.com/tarantool/tarantool/tree/master/test/luatest_helpers ?

It is not just partially imported. It is a complete 100% copy-paste
of everything except vtest.lua. I wrote it under `---` below.

>> diff --git a/test/luatest_helpers.lua b/test/luatest_helpers.lua
>> new file mode 100644
>> index 0000000..283906c
>> --- /dev/null
>> +++ b/test/luatest_helpers.lua
>> @@ -0,0 +1,72 @@
>> +local fun = require('fun')
>> +local json = require('json')
>> +local fio = require('fio')
>> +local log = require('log')
>> +local yaml = require('yaml')
>> +local fiber = require('fiber')
>> +
>> +local luatest_helpers = {
>> +    SOCKET_DIR = fio.abspath(os.getenv('VARDIR') or 'test/var')
>> +}
> 
> Is fio.abspath really needed here? AFAIK the max length of unix socket is 108 symbols. Relative paths give a more chances that we don't face any issues.

VARDIR is already absolute path set by luatest, it won't help much.
As for why fio.abspath is used then - I don't know. I would rather treat
this file as a part of abomination called luatest (which it should have
been from the beginning instead of being copy-pasted across projects) and
try not to change anything here. Unless something breaks.

>> diff --git a/test/luatest_helpers/asserts.lua b/test/luatest_helpers/asserts.lua
>> new file mode 100644
>> index 0000000..77385d8
>> --- /dev/null
>> +++ b/test/luatest_helpers/asserts.lua
>> @@ -0,0 +1,43 @@

<...>

>> +
>> +
>> +function asserts:wait_fullmesh(servers, wait_time)
>> +    wait_time = wait_time or 20
>> +    t.helpers.retrying({timeout = wait_time}, function()
>> +        for _, server in pairs(servers) do
>> +            for _, server2 in pairs(servers) do
>> +                if server ~= server2 then
>> +                    local server_id = server:eval('return box.info.id')
>> +                    local server2_id = server2:eval('return box.info.id')
>> +                    if server_id ~= server2_id then
>> +                            self:assert_server_follow_upstream(server, server2_id)
> Indention looks broken here (8 spaces instead of 4).

Thanks, fixed:

====================
@@ -32,7 +32,7 @@ function asserts:wait_fullmesh(servers, wait_time)
                     local server_id = server:eval('return box.info.id')
                     local server2_id = server2:eval('return box.info.id')
                     if server_id ~= server2_id then
-                            self:assert_server_follow_upstream(server, server2_id)
+                        self:assert_server_follow_upstream(server, server2_id)
                     end
                 end
             end
====================

I also applied this diff to make it run on 1.10:

====================
diff --git a/test/instances/router.lua b/test/instances/router.lua
index ccec6c1..587a473 100755
--- a/test/instances/router.lua
+++ b/test/instances/router.lua
@@ -7,7 +7,9 @@ _G.vshard = {
 }
 -- Somewhy shutdown hangs on new Tarantools even though the nodes do not seem to
 -- have any long requests running.
-box.ctl.set_on_shutdown_timeout(0.001)
+if box.ctl.set_on_shutdown_timeout then
+    box.ctl.set_on_shutdown_timeout(0.001)
+end
 
 box.cfg(helpers.box_cfg())
 box.schema.user.grant('guest', 'super', nil, nil, {if_not_exists = true})
diff --git a/test/instances/storage.lua b/test/instances/storage.lua
index 2d679ba..7ad2af3 100755
--- a/test/instances/storage.lua
+++ b/test/instances/storage.lua
@@ -7,7 +7,9 @@ _G.vshard = {
 }
 -- Somewhy shutdown hangs on new Tarantools even though the nodes do not seem to
 -- have any long requests running.
-box.ctl.set_on_shutdown_timeout(0.001)
+if box.ctl.set_on_shutdown_timeout then
+    box.ctl.set_on_shutdown_timeout(0.001)
+end
 
 box.cfg(helpers.box_cfg())
====================


New patch:

====================
test: support luatest

Test-run's most recent guideline is to write new tests in luatest
instead of diff console tests when possible. Luatest isn't exactly
in a perfect condition now, but it has 2 important features which
would be very useful in vshard:

- Easy cluster build. All can be done programmatically except a
  few basic things - from config creation to all replicasets
  start, router start, and buckets bootstrap. No need to hardcode
  that into files like storage_1_a.lua, router_1.lua, etc.

- Can opt-out certain tests depending on Tarantool version. For
  instance, soon coming support for netbox's return_raw option
  will need to run tests only for > 2.10.0-beta2. In diff tests it
  is also possible but would be notably complicated to achieve.

Needed for #312
---
 test-run                            |   2 +-
 test/instances/router.lua           |  17 ++
 test/instances/storage.lua          |  23 +++
 test/luatest_helpers.lua            |  72 ++++++++
 test/luatest_helpers/asserts.lua    |  43 +++++
 test/luatest_helpers/cluster.lua    | 132 ++++++++++++++
 test/luatest_helpers/server.lua     | 266 ++++++++++++++++++++++++++++
 test/luatest_helpers/vtest.lua      | 135 ++++++++++++++
 test/router-luatest/router_test.lua |  54 ++++++
 test/router-luatest/suite.ini       |   5 +
 10 files changed, 748 insertions(+), 1 deletion(-)
 create mode 100755 test/instances/router.lua
 create mode 100755 test/instances/storage.lua
 create mode 100644 test/luatest_helpers.lua
 create mode 100644 test/luatest_helpers/asserts.lua
 create mode 100644 test/luatest_helpers/cluster.lua
 create mode 100644 test/luatest_helpers/server.lua
 create mode 100644 test/luatest_helpers/vtest.lua
 create mode 100644 test/router-luatest/router_test.lua
 create mode 100644 test/router-luatest/suite.ini

diff --git a/test-run b/test-run
index c345003..2604c46 160000
--- a/test-run
+++ b/test-run
@@ -1 +1 @@
-Subproject commit c34500365efe8316e79c7936a2f2d04644602936
+Subproject commit 2604c46c7b6368dbde59489d5303ce3d1d430331
diff --git a/test/instances/router.lua b/test/instances/router.lua
new file mode 100755
index 0000000..587a473
--- /dev/null
+++ b/test/instances/router.lua
@@ -0,0 +1,17 @@
+#!/usr/bin/env tarantool
+local helpers = require('test.luatest_helpers')
+-- Do not load entire vshard into the global namespace to catch errors when code
+-- relies on that.
+_G.vshard = {
+    router = require('vshard.router'),
+}
+-- Somewhy shutdown hangs on new Tarantools even though the nodes do not seem to
+-- have any long requests running.
+if box.ctl.set_on_shutdown_timeout then
+    box.ctl.set_on_shutdown_timeout(0.001)
+end
+
+box.cfg(helpers.box_cfg())
+box.schema.user.grant('guest', 'super', nil, nil, {if_not_exists = true})
+
+_G.ready = true
diff --git a/test/instances/storage.lua b/test/instances/storage.lua
new file mode 100755
index 0000000..7ad2af3
--- /dev/null
+++ b/test/instances/storage.lua
@@ -0,0 +1,23 @@
+#!/usr/bin/env tarantool
+local helpers = require('test.luatest_helpers')
+-- Do not load entire vshard into the global namespace to catch errors when code
+-- relies on that.
+_G.vshard = {
+    storage = require('vshard.storage'),
+}
+-- Somewhy shutdown hangs on new Tarantools even though the nodes do not seem to
+-- have any long requests running.
+if box.ctl.set_on_shutdown_timeout then
+    box.ctl.set_on_shutdown_timeout(0.001)
+end
+
+box.cfg(helpers.box_cfg())
+box.schema.user.grant('guest', 'super', nil, nil, {if_not_exists = true})
+
+local function echo(...)
+    return ...
+end
+
+_G.echo = echo
+
+_G.ready = true
diff --git a/test/luatest_helpers.lua b/test/luatest_helpers.lua
new file mode 100644
index 0000000..283906c
--- /dev/null
+++ b/test/luatest_helpers.lua
@@ -0,0 +1,72 @@
+local fun = require('fun')
+local json = require('json')
+local fio = require('fio')
+local log = require('log')
+local yaml = require('yaml')
+local fiber = require('fiber')
+
+local luatest_helpers = {
+    SOCKET_DIR = fio.abspath(os.getenv('VARDIR') or 'test/var')
+}
+
+luatest_helpers.Server = require('test.luatest_helpers.server')
+
+local function default_cfg()
+    return {
+        work_dir = os.getenv('TARANTOOL_WORKDIR'),
+        listen = os.getenv('TARANTOOL_LISTEN'),
+        log = ('%s/%s.log'):format(os.getenv('TARANTOOL_WORKDIR'), os.getenv('TARANTOOL_ALIAS')),
+    }
+end
+
+local function env_cfg()
+    local src = os.getenv('TARANTOOL_BOX_CFG')
+    if src == nil then
+        return {}
+    end
+    local res = json.decode(src)
+    assert(type(res) == 'table')
+    return res
+end
+
+-- Collect box.cfg table from values passed through
+-- luatest_helpers.Server({<...>}) and from the given argument.
+--
+-- Use it from inside an instance script.
+function luatest_helpers.box_cfg(cfg)
+    return fun.chain(default_cfg(), env_cfg(), cfg or {}):tomap()
+end
+
+function luatest_helpers.instance_uri(alias, instance_id)
+    if instance_id == nil then
+        instance_id = ''
+    end
+    instance_id = tostring(instance_id)
+    return ('%s/%s%s.iproto'):format(luatest_helpers.SOCKET_DIR, alias, instance_id);
+end
+
+function luatest_helpers:get_vclock(server)
+    return server:eval('return box.info.vclock')
+end
+
+function luatest_helpers:wait_vclock(server, to_vclock)
+    while true do
+        local vclock = self:get_vclock(server)
+        local ok = true
+        for server_id, to_lsn in pairs(to_vclock) do
+            local lsn = vclock[server_id]
+            if lsn == nil or lsn < to_lsn then
+                ok = false
+                break
+            end
+        end
+        if ok then
+            return
+        end
+        log.info("wait vclock: %s to %s", yaml.encode(vclock),
+                 yaml.encode(to_vclock))
+        fiber.sleep(0.001)
+    end
+end
+
+return luatest_helpers
diff --git a/test/luatest_helpers/asserts.lua b/test/luatest_helpers/asserts.lua
new file mode 100644
index 0000000..fa015cd
--- /dev/null
+++ b/test/luatest_helpers/asserts.lua
@@ -0,0 +1,43 @@
+local t = require('luatest')
+
+local asserts = {}
+
+function asserts:new(object)
+    self:inherit(object)
+    object:initialize()
+    return object
+end
+
+function asserts:inherit(object)
+    object = object or {}
+    setmetatable(object, self)
+    self.__index = self
+    return object
+end
+
+function asserts:assert_server_follow_upstream(server, id)
+    local status = server:eval(
+        ('return box.info.replication[%d].upstream.status'):format(id))
+    t.assert_equals(status, 'follow',
+        ('%s: this server does not follow others.'):format(server.alias))
+end
+
+
+function asserts:wait_fullmesh(servers, wait_time)
+    wait_time = wait_time or 20
+    t.helpers.retrying({timeout = wait_time}, function()
+        for _, server in pairs(servers) do
+            for _, server2 in pairs(servers) do
+                if server ~= server2 then
+                    local server_id = server:eval('return box.info.id')
+                    local server2_id = server2:eval('return box.info.id')
+                    if server_id ~= server2_id then
+                        self:assert_server_follow_upstream(server, server2_id)
+                    end
+                end
+            end
+        end
+    end)
+end
+
+return asserts
diff --git a/test/luatest_helpers/cluster.lua b/test/luatest_helpers/cluster.lua
new file mode 100644
index 0000000..43e3479
--- /dev/null
+++ b/test/luatest_helpers/cluster.lua
@@ -0,0 +1,132 @@
+local fio = require('fio')
+local Server = require('test.luatest_helpers.server')
+
+local root = os.environ()['SOURCEDIR'] or '.'
+
+local Cluster = {}
+
+function Cluster:new(object)
+    self:inherit(object)
+    object:initialize()
+    self.servers = object.servers
+    self.built_servers = object.built_servers
+    return object
+end
+
+function Cluster:inherit(object)
+    object = object or {}
+    setmetatable(object, self)
+    self.__index = self
+    self.servers = {}
+    self.built_servers = {}
+    return object
+end
+
+function Cluster:initialize()
+    self.servers = {}
+end
+
+function Cluster:server(alias)
+    for _, server in ipairs(self.servers) do
+        if server.alias == alias then
+            return server
+        end
+    end
+    return nil
+end
+
+function Cluster:drop()
+    for _, server in ipairs(self.servers) do
+        if server ~= nil then
+            server:stop()
+            server:cleanup()
+        end
+    end
+end
+
+function Cluster:get_index(server)
+    local index = nil
+    for i, v in ipairs(self.servers) do
+        if (v.id == server) then
+          index = i
+        end
+    end
+    return index
+end
+
+function Cluster:delete_server(server)
+    local idx = self:get_index(server)
+    if idx == nil then
+        print("Key does not exist")
+    else
+        table.remove(self.servers, idx)
+    end
+end
+
+function Cluster:stop()
+    for _, server in ipairs(self.servers) do
+        if server ~= nil then
+            server:stop()
+        end
+    end
+end
+
+function Cluster:start(opts)
+    for _, server in ipairs(self.servers) do
+        if not server.process then
+            server:start({wait_for_readiness = false})
+        end
+    end
+
+    -- The option is true by default.
+    local wait_for_readiness = true
+    if opts ~= nil and opts.wait_for_readiness ~= nil then
+        wait_for_readiness = opts.wait_for_readiness
+    end
+
+    if wait_for_readiness then
+        for _, server in ipairs(self.servers) do
+            server:wait_for_readiness()
+        end
+    end
+end
+
+function Cluster:build_server(server_config, instance_file)
+    instance_file = instance_file or 'default.lua'
+    server_config = table.deepcopy(server_config)
+    server_config.command = fio.pathjoin(root, 'test/instances/', instance_file)
+    assert(server_config.alias, 'Either replicaset.alias or server.alias must be given')
+    local server = Server:new(server_config)
+    table.insert(self.built_servers, server)
+    return server
+end
+
+function Cluster:add_server(server)
+    if self:server(server.alias) ~= nil then
+        error('Alias is not provided')
+    end
+    table.insert(self.servers, server)
+end
+
+function Cluster:build_and_add_server(config, replicaset_config, engine)
+    local server = self:build_server(config, replicaset_config, engine)
+    self:add_server(server)
+    return server
+end
+
+
+function Cluster:get_leader()
+    for _, instance in ipairs(self.servers) do
+        if instance:eval('return box.info.ro') == false then
+            return instance
+        end
+    end
+end
+
+function Cluster:exec_on_leader(bootstrap_function)
+    local leader = self:get_leader()
+    return leader:exec(bootstrap_function)
+end
+
+
+return Cluster
diff --git a/test/luatest_helpers/server.lua b/test/luatest_helpers/server.lua
new file mode 100644
index 0000000..714c537
--- /dev/null
+++ b/test/luatest_helpers/server.lua
@@ -0,0 +1,266 @@
+local clock = require('clock')
+local digest = require('digest')
+local ffi = require('ffi')
+local fiber = require('fiber')
+local fio = require('fio')
+local fun = require('fun')
+local json = require('json')
+local errno = require('errno')
+
+local checks = require('checks')
+local luatest = require('luatest')
+
+ffi.cdef([[
+    int kill(pid_t pid, int sig);
+]])
+
+local Server = luatest.Server:inherit({})
+
+local WAIT_TIMEOUT = 60
+local WAIT_DELAY = 0.1
+
+local DEFAULT_CHECKPOINT_PATTERNS = {"*.snap", "*.xlog", "*.vylog",
+                                     "*.inprogress", "[0-9]*/"}
+
+-- Differences from luatest.Server:
+--
+-- * 'alias' is mandatory.
+-- * 'command' is optional, assumed test/instances/default.lua by
+--   default.
+-- * 'workdir' is optional, determined by 'alias'.
+-- * The new 'box_cfg' parameter.
+-- * engine - provides engine for parameterized tests
+Server.constructor_checks = fun.chain(Server.constructor_checks, {
+    alias = 'string',
+    command = '?string',
+    workdir = '?string',
+    box_cfg = '?table',
+    engine = '?string',
+}):tomap()
+
+function Server:initialize()
+    local vardir = fio.abspath(os.getenv('VARDIR') or 'test/var')
+
+    if self.id == nil then
+        local random = digest.urandom(9)
+        self.id = digest.base64_encode(random, {urlsafe = true})
+    end
+    if self.command == nil then
+        self.command = 'test/instances/default.lua'
+    end
+    if self.workdir == nil then
+        self.workdir = ('%s/%s-%s'):format(vardir, self.alias, self.id)
+        fio.rmtree(self.workdir)
+        fio.mktree(self.workdir)
+    end
+    if self.net_box_port == nil and self.net_box_uri == nil then
+        self.net_box_uri = ('%s/%s.iproto'):format(vardir, self.alias)
+        fio.mktree(vardir)
+    end
+
+    -- AFAIU, the inner getmetatable() returns our helpers.Server
+    -- class, the outer one returns luatest.Server class.
+    getmetatable(getmetatable(self)).initialize(self)
+end
+
+--- Generates environment to run process with.
+-- The result is merged into os.environ().
+-- @return map
+function Server:build_env()
+    local res = getmetatable(getmetatable(self)).build_env(self)
+    if self.box_cfg ~= nil then
+        res.TARANTOOL_BOX_CFG = json.encode(self.box_cfg)
+    end
+    res.TARANTOOL_ENGINE = self.engine
+    return res
+end
+
+local function wait_cond(cond_name, server, func, ...)
+    local alias = server.alias
+    local id = server.id
+    local pid = server.process.pid
+
+    local deadline = clock.time() + WAIT_TIMEOUT
+    while true do
+        if func(...) then
+            return
+        end
+        if clock.time() > deadline then
+            error(('Waiting for "%s" on server %s-%s (PID %d) timed out')
+                  :format(cond_name, alias, id, pid))
+        end
+        fiber.sleep(WAIT_DELAY)
+    end
+end
+
+function Server:wait_for_readiness()
+    return wait_cond('readiness', self, function()
+        local ok, is_ready = pcall(function()
+            self:connect_net_box()
+            return self.net_box:eval('return _G.ready') == true
+        end)
+        return ok and is_ready
+    end)
+end
+
+function Server:wait_election_leader()
+    -- Include read-only property too because if an instance is a leader, it
+    -- does not mean it finished the synchro queue ownership transition. It is
+    -- read-only until that happens. But in tests usually the leader is needed
+    -- as a writable node.
+    return wait_cond('election leader', self, self.exec, self, function()
+        return box.info.election.state == 'leader' and not box.info.ro
+    end)
+end
+
+function Server:wait_election_leader_found()
+    return wait_cond('election leader is found', self, self.exec, self,
+                     function() return box.info.election.leader ~= 0 end)
+end
+
+-- Unlike the original luatest.Server function it waits for
+-- starting the server.
+function Server:start(opts)
+    checks('table', {
+        wait_for_readiness = '?boolean',
+    })
+    getmetatable(getmetatable(self)).start(self)
+
+    -- The option is true by default.
+    local wait_for_readiness = true
+    if opts ~= nil and opts.wait_for_readiness ~= nil then
+        wait_for_readiness = opts.wait_for_readiness
+    end
+
+    if wait_for_readiness then
+        self:wait_for_readiness()
+    end
+end
+
+function Server:instance_id()
+    -- Cache the value when found it first time.
+    if self.instance_id_value then
+        return self.instance_id_value
+    end
+    local id = self:exec(function() return box.info.id end)
+    -- But do not cache 0 - it is an anon instance, its ID might change.
+    if id ~= 0 then
+        self.instance_id_value = id
+    end
+    return id
+end
+
+function Server:instance_uuid()
+    -- Cache the value when found it first time.
+    if self.instance_uuid_value then
+        return self.instance_uuid_value
+    end
+    local uuid = self:exec(function() return box.info.uuid end)
+    self.instance_uuid_value = uuid
+    return uuid
+end
+
+-- TODO: Add the 'wait_for_readiness' parameter for the restart()
+-- method.
+
+-- Unlike the original luatest.Server function it waits until
+-- the server will stop.
+function Server:stop()
+    local alias = self.alias
+    local id = self.id
+    if self.process then
+        local pid = self.process.pid
+        getmetatable(getmetatable(self)).stop(self)
+
+        local deadline = clock.time() + WAIT_TIMEOUT
+        while true do
+            if ffi.C.kill(pid, 0) ~= 0 then
+                break
+            end
+            if clock.time() > deadline then
+                error(('Stopping of server %s-%s (PID %d) was timed out'):format(
+                    alias, id, pid))
+            end
+            fiber.sleep(WAIT_DELAY)
+        end
+    end
+end
+
+function Server:cleanup()
+    for _, pattern in ipairs(DEFAULT_CHECKPOINT_PATTERNS) do
+        fio.rmtree(('%s/%s'):format(self.workdir, pattern))
+    end
+    self.instance_id_value = nil
+    self.instance_uuid_value = nil
+end
+
+function Server:drop()
+    self:stop()
+    self:cleanup()
+end
+
+-- A copy of test_run:grep_log.
+function Server:grep_log(what, bytes, opts)
+    local opts = opts or {}
+    local noreset = opts.noreset or false
+    -- if instance has crashed provide filename to use grep_log
+    local filename = opts.filename or self:eval('return box.cfg.log')
+    local file = fio.open(filename, {'O_RDONLY', 'O_NONBLOCK'})
+
+    local function fail(msg)
+        local err = errno.strerror()
+        if file ~= nil then
+            file:close()
+        end
+        error(string.format("%s: %s: %s", msg, filename, err))
+    end
+
+    if file == nil then
+        fail("Failed to open log file")
+    end
+    io.flush() -- attempt to flush stdout == log fd
+    local filesize = file:seek(0, 'SEEK_END')
+    if filesize == nil then
+        fail("Failed to get log file size")
+    end
+    local bytes = bytes or 65536 -- don't read whole log - it can be huge
+    bytes = bytes > filesize and filesize or bytes
+    if file:seek(-bytes, 'SEEK_END') == nil then
+        fail("Failed to seek log file")
+    end
+    local found, buf
+    repeat -- read file in chunks
+        local s = file:read(2048)
+        if s == nil then
+            fail("Failed to read log file")
+        end
+        local pos = 1
+        repeat -- split read string in lines
+            local endpos = string.find(s, '\n', pos)
+            endpos = endpos and endpos - 1 -- strip terminating \n
+            local line = string.sub(s, pos, endpos)
+            if endpos == nil and s ~= '' then
+                -- line doesn't end with \n or eof, append it to buffer
+                -- to be checked on next iteration
+                buf = buf or {}
+                table.insert(buf, line)
+            else
+                if buf ~= nil then -- prepend line with buffered data
+                    table.insert(buf, line)
+                    line = table.concat(buf)
+                    buf = nil
+                end
+                if string.match(line, "Starting instance") and not noreset then
+                    found = nil -- server was restarted, reset search
+                else
+                    found = string.match(line, what) or found
+                end
+            end
+            pos = endpos and endpos + 2 -- jump to char after \n
+        until pos == nil
+    until s == ''
+    file:close()
+    return found
+end
+
+return Server
diff --git a/test/luatest_helpers/vtest.lua b/test/luatest_helpers/vtest.lua
new file mode 100644
index 0000000..affc008
--- /dev/null
+++ b/test/luatest_helpers/vtest.lua
@@ -0,0 +1,135 @@
+local helpers = require('test.luatest_helpers')
+local cluster = require('test.luatest_helpers.cluster')
+
+local uuid_idx = 1
+
+--
+-- New UUID unique per this process. Generation is not random - for simplicity
+-- and reproducibility.
+--
+local function uuid_next()
+    local last = tostring(uuid_idx)
+    uuid_idx = uuid_idx + 1
+    assert(#last <= 12)
+    return '00000000-0000-0000-0000-'..string.rep('0', 12 - #last)..last
+end
+
+--
+-- Build a valid vshard config by a template. A template does not specify
+-- anything volatile such as URIs, UUIDs - these are installed at runtime.
+--
+local function config_new(templ)
+    local res = table.deepcopy(templ)
+    local sharding = {}
+    res.sharding = sharding
+    for _, replicaset_templ in pairs(templ.sharding) do
+        local replicaset_uuid = uuid_next()
+        local replicas = {}
+        local replicaset = table.deepcopy(replicaset_templ)
+        replicaset.replicas = replicas
+        for replica_name, replica_templ in pairs(replicaset_templ.replicas) do
+            local replica_uuid = uuid_next()
+            local replica = table.deepcopy(replica_templ)
+            replica.name = replica_name
+            replica.uri = 'storage:storage@'..helpers.instance_uri(replica_name)
+            replicas[replica_uuid] = replica
+        end
+        sharding[replicaset_uuid] = replicaset
+    end
+    return res
+end
+
+--
+-- Build new cluster by a given config.
+--
+local function storage_new(g, cfg)
+    if not g.cluster then
+        g.cluster = cluster:new({})
+    end
+    local all_servers = {}
+    local masters = {}
+    local replicas = {}
+    for replicaset_uuid, replicaset in pairs(cfg.sharding) do
+        -- Luatest depends on box.cfg being ready and listening. Need to
+        -- configure it before vshard.storage.cfg().
+        local box_repl = {}
+        for _, replica in pairs(replicaset.replicas) do
+            table.insert(box_repl, replica.uri)
+        end
+        local box_cfg = {
+            replication = box_repl,
+            -- Speed retries up.
+            replication_timeout = 0.1,
+        }
+        for replica_uuid, replica in pairs(replicaset.replicas) do
+            local name = replica.name
+            box_cfg.instance_uuid = replica_uuid
+            box_cfg.replicaset_uuid = replicaset_uuid
+            box_cfg.listen = helpers.instance_uri(replica.name)
+            -- Need to specify read-only explicitly to know how is master.
+            box_cfg.read_only = not replica.master
+            local server = g.cluster:build_server({
+                alias = name,
+                box_cfg = box_cfg,
+            }, 'storage.lua')
+            g[name] = server
+            g.cluster:add_server(server)
+
+            table.insert(all_servers, server)
+            if replica.master then
+                table.insert(masters, server)
+            else
+                table.insert(replicas, server)
+            end
+        end
+    end
+    for _, replica in pairs(all_servers) do
+        replica:start({wait_for_readiness = false})
+    end
+    for _, master in pairs(masters) do
+        master:wait_for_readiness()
+        master:exec(function(cfg)
+            -- Logged in as guest with 'super' access rights. Yet 'super' is not
+            -- enough to grant 'replication' privilege. The simplest way - login
+            -- as admin for that temporary.
+            local user = box.session.user()
+            box.session.su('admin')
+
+            vshard.storage.cfg(cfg, box.info.uuid)
+            box.schema.user.grant('storage', 'super')
+
+            box.session.su(user)
+        end, {cfg})
+    end
+    for _, replica in pairs(replicas) do
+        replica:wait_for_readiness()
+        replica:exec(function(cfg)
+            vshard.storage.cfg(cfg, box.info.uuid)
+        end, {cfg})
+    end
+end
+
+--
+-- Create a new router in the cluster.
+--
+local function router_new(g, name, cfg)
+    if not g.cluster then
+        g.cluster = cluster:new({})
+    end
+    local server = g.cluster:build_server({
+        alias = name,
+    }, 'router.lua')
+    g[name] = server
+    g.cluster:add_server(server)
+    server:start()
+    server:exec(function(cfg)
+        vshard.router.cfg(cfg)
+    end, {cfg})
+    return server
+end
+
+return {
+    config_new = config_new,
+    storage_new = storage_new,
+    router_new = router_new,
+}
diff --git a/test/router-luatest/router_test.lua b/test/router-luatest/router_test.lua
new file mode 100644
index 0000000..621794a
--- /dev/null
+++ b/test/router-luatest/router_test.lua
@@ -0,0 +1,54 @@
+local t = require('luatest')
+local vtest = require('test.luatest_helpers.vtest')
+local wait_timeout = 120
+
+local g = t.group('router')
+local cluster_cfg = vtest.config_new({
+    sharding = {
+        {
+            replicas = {
+                replica_1_a = {
+                    master = true,
+                },
+                replica_1_b = {},
+            },
+        },
+        {
+            replicas = {
+                replica_2_a = {
+                    master = true,
+                },
+                replica_2_b = {},
+            },
+        },
+    },
+    bucket_count = 100
+})
+
+g.before_all(function()
+    vtest.storage_new(g, cluster_cfg)
+
+    t.assert_equals(g.replica_1_a:exec(function()
+        return #vshard.storage.info().alerts
+    end), 0, 'no alerts after boot')
+
+    local router = vtest.router_new(g, 'router', cluster_cfg)
+    g.router = router
+    local res, err = router:exec(function(timeout)
+        return vshard.router.bootstrap({timeout = timeout})
+    end, {wait_timeout})
+    t.assert(res and not err, 'bootstrap buckets')
+end)
+
+g.after_all(function()
+    g.cluster:drop()
+end)
+
+g.test_basic = function(g)
+    local router = g.router
+    local res, err = router:exec(function(timeout)
+        return vshard.router.callrw(1, 'echo', {1}, {timeout = timeout})
+    end, {wait_timeout})
+    t.assert(not err, 'no error')
+    t.assert_equals(res, 1, 'good result')
+end
diff --git a/test/router-luatest/suite.ini b/test/router-luatest/suite.ini
new file mode 100644
index 0000000..ae79147
--- /dev/null
+++ b/test/router-luatest/suite.ini
@@ -0,0 +1,5 @@
+[default]
+core = luatest
+description = Router tests
+is_parallel = True
+release_disabled =
-- 
2.24.3 (Apple Git-128)


More information about the Tarantool-patches mailing list