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

Oleg Babin olegrok at tarantool.org
Fri Feb 11 19:38:40 MSK 2022


Thanks for your changes. LGTM.

On 11.02.2022 01:32, Vladislav Shpilevoy wrote:
> 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 tarantoolhttps://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.

Ops, I missed this sentence.


>>> 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
> +    iffile: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 =


More information about the Tarantool-patches mailing list