From 06ca83c9320d3419a8af1b1fe22de90f8657e721 Mon Sep 17 00:00:00 2001
From: Alexander Turenko <alexander.turenko@tarantool.org>
Date: Fri, 28 Jul 2023 14:08:14 +0300
Subject: [PATCH] test/config: add several application script tests

The declarative config has the `app` section with following three
parameters:

* `app.file` -- the path to the script file
* `app.module` -- the same, but the script is searched using the Lua
  search paths (and loaded using `require`)
* `app.cfg` -- a user provided configuration (arbitrary map)

This commit adds a few success/failure cases. The goal is to have simple
test examples to add more ones easier in a future.

Implemented several test helpers for typical scenarios that can be used
outside of the application script test.

Part of #8862

NO_DOC=testing improvements
NO_CHANGELOG=see NO_DOC
---
 test/config-luatest/app_test.lua   | 110 ++++++++++++++
 test/config-luatest/basic_test.lua |  18 +--
 test/config-luatest/helpers.lua    | 235 +++++++++++++++++++++++++++++
 3 files changed, 346 insertions(+), 17 deletions(-)
 create mode 100644 test/config-luatest/app_test.lua

diff --git a/test/config-luatest/app_test.lua b/test/config-luatest/app_test.lua
new file mode 100644
index 0000000000..360789c743
--- /dev/null
+++ b/test/config-luatest/app_test.lua
@@ -0,0 +1,110 @@
+local t = require('luatest')
+local helpers = require('test.config-luatest.helpers')
+
+local g = helpers.group()
+
+-- Start a script that is pointed by app.file or app.module.
+--
+-- TODO: Verify that the script has access to config:get() values.
+g.test_startup_success = function(g)
+    local script = [[
+        -- TODO: Verify that the script has access to config:get()
+        -- values.
+        -- local config = require('config')
+        -- assert(config:get('app.cfg.foo') == 42)
+
+        _G.foo = 42
+    ]]
+    local verify = function()
+        local config = require('config')
+        t.assert_equals(_G.foo, 42)
+        t.assert_equals(config:get('app.cfg.foo'), 42)
+    end
+
+    helpers.success_case(g, {
+        script = script,
+        options = {
+            ['app.file'] = 'main.lua',
+            ['app.cfg'] = {foo = 42},
+        },
+        verify = verify,
+    })
+
+    helpers.success_case(g, {
+        script = script,
+        options = {
+            ['app.module'] = 'main',
+            ['app.cfg'] = {foo = 42},
+        },
+        verify = verify,
+    })
+end
+
+-- Start a script that is pointed by app.file or app.module.
+--
+-- Verify that an error in the script leads to a startup failure.
+g.test_startup_error = function(g)
+    local err_msg = 'Application is unable to start'
+    local script = ([[
+        error('%s', 0)
+    ]]):format(err_msg)
+
+    helpers.failure_case(g, {
+        script = script,
+        options = {['app.file'] = 'main.lua'},
+        exp_err = err_msg,
+    })
+
+    helpers.failure_case(g, {
+        script = script,
+        options = {['app.module'] = 'main'},
+        exp_err = err_msg,
+    })
+end
+
+-- Start a server, write a script that raises an error, reload.
+--
+-- Verify that the error is raised by config:reload() and the same
+-- error appears in the alerts.
+--
+-- The test case uses only app.file deliberately: if the
+-- application script is set as app.module, it is not re-executed
+-- on config:reload() due to caching in package.loaded.
+g.test_reload_failure = function(g)
+    local err_msg = 'Application is unable to start'
+
+    helpers.reload_failure_case(g, {
+        script = '',
+        options = {['app.file'] = 'main.lua'},
+        verify = function() end,
+        script_2 = ([[
+            error('%s', 0)
+        ]]):format(err_msg),
+        exp_err = err_msg,
+    })
+end
+
+-- Start a server, write a script that raises an error, reload.
+--
+-- Verify that the error is NOT raised when the application script
+-- is set using app.module (due to caching in package.loaded).
+--
+-- This behavior may be changed in a future.
+g.test_reload_success = function(g)
+    local err_msg = 'Application is unable to start'
+
+    helpers.reload_success_case(g, {
+        script = '',
+        options = {['app.module'] = 'main'},
+        verify = function() end,
+        script_2 = ([[
+            error('%s', 0)
+        ]]):format(err_msg),
+        verify_2 = function()
+            local config = require('config')
+            local info = config:info()
+            t.assert_equals(info.status, 'ready')
+            t.assert_equals(#info.alerts, 0)
+        end,
+    })
+end
diff --git a/test/config-luatest/basic_test.lua b/test/config-luatest/basic_test.lua
index 595ad69d41..e0b0498297 100644
--- a/test/config-luatest/basic_test.lua
+++ b/test/config-luatest/basic_test.lua
@@ -7,23 +7,7 @@ local justrun = require('test.justrun')
 local server = require('test.luatest_helpers.server')
 local helpers = require('test.config-luatest.helpers')
 
-local g = t.group()
-
-g.before_all(function(g)
-    treegen.init(g)
-end)
-
-g.after_all(function(g)
-    treegen.clean(g)
-end)
-
-g.after_each(function(g)
-    for k, v in pairs(g) do
-        if k == 'server' or k:match('^server_%d+$') then
-            v:stop()
-        end
-    end
-end)
+local g = helpers.group()
 
 local function count_lines(s)
     return #s:split('\n')
diff --git a/test/config-luatest/helpers.lua b/test/config-luatest/helpers.lua
index b796ceec49..5b8972d1aa 100644
--- a/test/config-luatest/helpers.lua
+++ b/test/config-luatest/helpers.lua
@@ -1,7 +1,31 @@
 local fun = require('fun')
+local yaml = require('yaml')
+local fio = require('fio')
+local cluster_config = require('internal.config.cluster_config')
 local t = require('luatest')
+local treegen = require('test.treegen')
+local justrun = require('test.justrun')
 local server = require('test.luatest_helpers.server')
 
+local function group(name, params)
+    local g = t.group(name, params)
+
+    g.before_all(treegen.init)
+
+    g.after_each(function(g)
+        for k, v in pairs(table.copy(g)) do
+            if k == 'server' or k:match('^server_%d+$') then
+                v:stop()
+                g[k] = nil
+            end
+        end
+    end)
+
+    g.after_all(treegen.clean)
+
+    return g
+end
+
 local function start_example_replicaset(g, dir, config_file, opts)
     local credentials = {
         user = 'client',
@@ -40,6 +64,217 @@ local function start_example_replicaset(g, dir, config_file, opts)
     t.assert_equals(info.cluster.name, 'group-001')
 end
 
+-- A simple single instance configuration.
+local simple_config = {
+    credentials = {
+        users = {
+            guest = {
+                roles = {'super'},
+            },
+        },
+    },
+    iproto = {
+        listen = 'unix/:./{{ instance_name }}.iproto',
+    },
+    groups = {
+        ['group-001'] = {
+            replicasets = {
+                ['replicaset-001'] = {
+                    instances = {
+                        ['instance-001'] = {},
+                    },
+                },
+            },
+        },
+    },
+}
+
+local function prepare_case(g, opts)
+    local dir = opts.dir
+    local script = opts.script
+    local options = opts.options
+
+    if dir == nil then
+        dir = treegen.prepare_directory(g, {}, {})
+    end
+
+    if script ~= nil then
+        treegen.write_script(dir, 'main.lua', script)
+    end
+
+    if options ~= nil then
+        local config = table.deepcopy(simple_config)
+        for path, value in pairs(options) do
+            cluster_config:set(config, path, value)
+        end
+        treegen.write_script(dir, 'config.yaml', yaml.encode(config))
+    end
+
+    local config_file = fio.pathjoin(dir, 'config.yaml')
+    local server = {
+        config_file = config_file,
+        chdir = dir,
+        alias = 'instance-001',
+    }
+    local justrun = {
+        -- dir
+        dir,
+        -- env
+        {},
+        -- args
+        {'--name', 'instance-001', '--config', config_file},
+        -- opts
+        {nojson = true, stderr = true},
+    }
+    return {
+        dir = dir,
+        server = server,
+        justrun = justrun,
+    }
+end
+
+-- Start a server with the given script and the given
+-- configuration, run a verification function on it.
+--
+-- * opts.script
+--
+--   Code write into the main.lua file.
+--
+-- * opts.options
+--
+--   The configuration is expressed as a set of path:value pairs.
+--   It is merged into the simple config above.
+--
+-- * opts.verify
+--
+--   Function to run on the started server to verify some
+--   invariants.
+local function success_case(g, opts)
+    local verify = assert(opts.verify)
+    local prepared = prepare_case(g, opts)
+    g.server = server:new(prepared.server)
+    g.server:start()
+    g.server:exec(verify)
+    return prepared
+end
+
+-- Start tarantool process with the given script/config and check
+-- the error.
+--
+-- * opts.script
+-- * opts.options
+--
+--   Same as in success_case().
+--
+-- * opts.exp_err
+--
+--   An error that must be written into stderr by tarantool
+--   process.
+local function failure_case(g, opts)
+    local exp_err = assert(opts.exp_err)
+
+    local prepared = prepare_case(g, opts)
+    local res = justrun.tarantool(unpack(prepared.justrun))
+    t.assert_equals(res.exit_code, 1)
+    t.assert_str_contains(res.stderr, exp_err)
+end
+
+-- Start a server, write a new script/config, reload, run a
+-- verification function.
+--
+-- * opts.script
+-- * opts.options
+-- * opts.verify
+--
+--   Same as in success_case().
+--
+-- * opts.script_2
+--
+--   A new script to write into the main.lua file before
+--   config:reload().
+--
+-- * opts.verify_2
+--
+--   Verify test invariants after config:reload().
+local function reload_success_case(g, opts)
+    local script_2 = assert(opts.script_2)
+    local verify_2 = assert(opts.verify_2)
+
+    local prepared = success_case(g, opts)
+
+    prepare_case(g, {
+        dir = prepared.dir,
+        script = script_2,
+    })
+    g.server:exec(function()
+        local config = require('config')
+        config:reload()
+    end)
+    g.server:exec(verify_2)
+end
+
+-- Start a server, write a new script/config, reload, run a
+-- verification function.
+--
+-- * opts.script
+-- * opts.options
+-- * opts.verify
+--
+--   Same as in success_case().
+--
+-- * opts.script_2
+--
+--   A new script to write into the main.lua file before
+--   config:reload().
+--
+-- * opts.exp_err
+--
+--   An error that config:reload() must raise.
+local function reload_failure_case(g, opts)
+    local script_2 = assert(opts.script_2)
+    local exp_err = assert(opts.exp_err)
+
+    local prepared = success_case(g, opts)
+
+    prepare_case(g, {
+        dir = prepared.dir,
+        script = script_2,
+    })
+    t.assert_error_msg_equals(exp_err, g.server.exec, g.server, function()
+        local config = require('config')
+        config:reload()
+    end)
+    g.server:exec(function(exp_err)
+        local config = require('config')
+        local info = config:info()
+        t.assert_equals(info.status, 'check_errors')
+        t.assert_equals(#info.alerts, 1)
+        t.assert_equals(info.alerts[1].type, 'error')
+        t.assert(info.alerts[1].timestamp ~= nil)
+        t.assert_equals(info.alerts[1].message, exp_err)
+    end, {exp_err})
+end
+
 return {
+    -- Setup a group of tests that are prepended/postpones with
+    -- hooks to stop servers between tests and to remove temporary
+    -- files after the testing.
+    group = group,
+
+    -- Start a three instance replicaset with the given config
+    -- file.
+    --
+    -- It assumes specific instance/replicaset/group names and
+    -- net.box credentials.
     start_example_replicaset = start_example_replicaset,
+
+    -- Run a single instance and verify some invariants.
+    --
+    -- All the runs are based on the given simple config,
+    -- but the options can be adjusted.
+    simple_config = simple_config,
+    success_case = success_case,
+    failure_case = failure_case,
+    reload_success_case = reload_success_case,
+    reload_failure_case = reload_failure_case,
 }
-- 
GitLab