From 61bf298542420e6cd1496ffe35f1ec5dd84f5e04 Mon Sep 17 00:00:00 2001
From: VitaliyaIoffe <vitaioffe@yandex.ru>
Date: Thu, 21 Oct 2021 12:56:00 +0300
Subject: [PATCH] Provide helpers for luatest

The commit includes a few helpers files for luatest infrastructure.
'Server' based on luatest Server with additional functionality like
waiting Server for start, create Server's own env including box.cfg
provided by test.

Servers could be built and added in a cluster (cluster.lua file).

Asserts.lua is additional checks, which are useful for many tests.

Helpers are supportive function, could be also used for many tests.

Part of tarantool/test-run#304
---
 test/instances/default.lua       |  18 +++
 test/luatest_helpers.lua         |  72 ++++++++++
 test/luatest_helpers/asserts.lua |  43 ++++++
 test/luatest_helpers/cluster.lua | 131 ++++++++++++++++++
 test/luatest_helpers/server.lua  | 220 +++++++++++++++++++++++++++++++
 5 files changed, 484 insertions(+)
 create mode 100755 test/instances/default.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

diff --git a/test/instances/default.lua b/test/instances/default.lua
new file mode 100755
index 0000000000..ebc1177c7c
--- /dev/null
+++ b/test/instances/default.lua
@@ -0,0 +1,18 @@
+#!/usr/bin/env tarantool
+local helpers = require('test.luatest_helpers')
+
+box.cfg(helpers.box_cfg())
+box.schema.user.grant('guest', 'super', nil, nil, {if_not_exists = true})
+
+-- luatest_helpers.Server:start() unblocks only when this variable
+-- becomes true.
+--
+-- Set it when the instance is fully operable:
+--
+-- * The server listens for requests.
+-- * The database is bootstrapped.
+-- * Permissions are granted.
+--
+-- Use luatest_helpers.Server:start({wait_for_readiness = false})
+-- to don't wait for setting of this variable.
+_G.ready = true
diff --git a/test/luatest_helpers.lua b/test/luatest_helpers.lua
new file mode 100644
index 0000000000..283906c110
--- /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 0000000000..77385d8423
--- /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 0000000000..01291b43cd
--- /dev/null
+++ b/test/luatest_helpers/cluster.lua
@@ -0,0 +1,131 @@
+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.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 0000000000..b6d6f1400c
--- /dev/null
+++ b/test/luatest_helpers/server.lua
@@ -0,0 +1,220 @@
+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
+
+function Server:wait_for_readiness()
+    local alias = self.alias
+    local id = self.id
+    local pid = self.process.pid
+
+    local deadline = clock.time() + WAIT_TIMEOUT
+    while true do
+        local ok, is_ready = pcall(function()
+            self:connect_net_box()
+            return self.net_box:eval('return _G.ready') == true
+        end)
+        if ok and is_ready then
+            break
+        end
+        if clock.time() > deadline then
+            error(('Starting of server %s-%s (PID %d) was timed out'):format(
+                alias, id, pid))
+        end
+        fiber.sleep(WAIT_DELAY)
+    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
+
+-- 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
+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
-- 
GitLab