Skip to content

Commit

Permalink
Migrate justrun module and adapt it
Browse files Browse the repository at this point in the history
The original `justrun` module (path: tarantool/test/justrun.lua) has been
moved to the current project with minor changes and will be available as
follows:

    local t = require('luatest')
    t.justrun.tarantool(...)

This module works with the `popen` module which requires Tarantool 2.4.1
and newer. Otherwise `justrun.tarantool(dir, env, args[, opts])` will
cause an error.

Closes #365
  • Loading branch information
Oleg Chaplashkin authored and ylobankov committed May 14, 2024
1 parent d985997 commit c1b2622
Show file tree
Hide file tree
Showing 5 changed files with 273 additions and 0 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
- Improve `luatest.log` function if a `nil` value is passed (gh-360).
- Added `assert_error_covers`.
- Add more logs (gh-326).
- Add `justrun` helper as a tarantool runner and output catcher (gh-365).

## 1.0.1

Expand Down
1 change: 1 addition & 0 deletions config.ld
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ file = {
'luatest/runner.lua',
'luatest/server.lua',
'luatest/replica_set.lua',
'luatest/justrun.lua',
}
topics = {
'CHANGELOG.md',
Expand Down
5 changes: 5 additions & 0 deletions luatest/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,11 @@ local parametrizer = require('luatest.parametrizer')
--
luatest.log = require('luatest.log')

--- Simple Tarantool runner and output catcher.
--
-- @see luatest.justrun
luatest.justrun = require('luatest.justrun')

--- Add before suite hook.
--
-- @function before_suite
Expand Down
172 changes: 172 additions & 0 deletions luatest/justrun.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
--- Simple Tarantool runner and output catcher.
--
-- Sometimes it is necessary to run tarantool with particular arguments and
-- verify its output. `luatest.server` provides a supervisor like
-- interface: an instance is started, calls box.cfg() and we can
-- communicate with it using net.box. Another helper in tarantool/tarantool,
-- `test.interactive_tarantool`, aims to solve all the problems around
-- readline console and also provides ability to communicate with the
-- instance interactively.
--
-- However, there is nothing like 'just run tarantool with given args and
-- give me its output'.
--
-- @module luatest.justrun

local checks = require('checks')
local fun = require('fun')
local json = require('json')
local fiber = require('fiber')

local log = require('luatest.log')

local justrun = {}

local function collect_stderr(ph)
local f = fiber.new(function()
local fiber_name = "child's stderr collector"
fiber.name(fiber_name, {truncate = true})

local chunks = {}

while true do
local chunk, err = ph:read({stderr = true})
if chunk == nil then
log.warn('%s: got error, exiting: %s', fiber_name, err)
break
end
if chunk == '' then
log.info('%s: got EOF, exiting', fiber_name)
break
end
table.insert(chunks, chunk)
end

-- Glue all chunks, strip trailing newline.
return table.concat(chunks):rstrip()
end)
f:set_joinable(true)
return f
end

local function cancel_stderr_fiber(stderr_fiber)
if stderr_fiber == nil then
return
end
stderr_fiber:cancel()
end

local function join_stderr_fiber(stderr_fiber)
if stderr_fiber == nil then
return
end
return select(2, assert(stderr_fiber:join()))
end

--- Run tarantool in given directory with given environment and
-- command line arguments and catch its output.
--
-- Expects JSON lines as the output and parses it into an array
-- (it can be disabled using `nojson` option).
--
-- Options:
--
-- - nojson (boolean, default: false)
--
-- Don't attempt to decode stdout as a stream of JSON lines,
-- return as is.
--
-- - stderr (boolean, default: false)
--
-- Collect stderr and place it into the `stderr` field of the
-- return value
--
-- - quote_args (boolean, default: false)
--
-- Quote CLI arguments before concatenating them into a shell
-- command.
--
-- @string dir Directory where the process will run.
-- @tparam table env Environment variables for the process.
-- @tparam table args Options that will be passed when the process starts.
-- @tparam[opt] table opts Custom options: nojson, stderr and quote_args.
-- @treturn table
function justrun.tarantool(dir, env, args, opts)
checks('string', 'table', 'table', '?table')
opts = opts or {}

local popen = require('popen')

-- Prevent system/user inputrc configuration file from
-- influencing testing code.
env['INPUTRC'] = '/dev/null'

local tarantool_exe = arg[-1]
-- Use popen.shell() instead of popen.new() due to lack of
-- cwd option in popen (gh-5633).
local env_str = table.concat(fun.iter(env):map(function(k, v)
return ('%s=%q'):format(k, v)
end):totable(), ' ')
local args_str = table.concat(fun.iter(args):map(function(v)
return opts.quote_args and ('%q'):format(v) or v
end):totable(), ' ')
local command = ('cd %s && %s %s %s'):format(dir, env_str, tarantool_exe,
args_str)
log.info('Running a command: %s', command)
local mode = opts.stderr and 'rR' or 'r'
local ph = popen.shell(command, mode)

local stderr_fiber
if opts.stderr then
stderr_fiber = collect_stderr(ph)
end

-- Read everything until EOF.
local chunks = {}
while true do
local chunk, err = ph:read()
if chunk == nil then
cancel_stderr_fiber(stderr_fiber)
ph:close()
error(err)
end
if chunk == '' then -- EOF
break
end
table.insert(chunks, chunk)
end

local exit_code = ph:wait().exit_code
local stderr = join_stderr_fiber(stderr_fiber)
ph:close()

-- If an error occurs, discard the output and return only the
-- exit code. However, return stderr.
if exit_code ~= 0 then
return {
exit_code = exit_code,
stderr = stderr,
}
end

-- Glue all chunks, strip trailing newline.
local res = table.concat(chunks):rstrip()
log.info('Command output:\n%s', res)

-- Decode JSON object per line into array of tables (if
-- `nojson` option is not passed).
local decoded
if opts.nojson then
decoded = res
else
decoded = fun.iter(res:split('\n')):map(json.decode):totable()
end

return {
exit_code = exit_code,
stdout = decoded,
stderr = stderr,
}
end

return justrun
94 changes: 94 additions & 0 deletions test/justrun_test.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
local t = require('luatest')
local fio = require('fio')

local justrun = require('luatest.justrun')
local utils = require('luatest.utils')

local g = t.group()

g.before_each(function()
g.tempdir = fio.tempdir()
g.tempfile = fio.pathjoin(g.tempdir, 'main.lua')

local default_flags = {'O_CREAT', 'O_WRONLY', 'O_TRUNC'}
local default_mode = tonumber('644', 8)

g.tempfile_fh = fio.open(g.tempfile, default_flags, default_mode)
end)

g.after_each(function()
fio.rmdir(g.tempdir)
end)

g.before_test('test_stdout_stderr_output', function()
g.tempfile_fh:write([[
local log = require('log')
print('hello stdout!')
log.info('hello stderr!')
]])
end)

g.test_stdout_stderr_output = function()
t.skip_if(not utils.version_current_ge_than(2, 4, 1),
"popen module is available since Tarantool 2.4.1.")
local res = justrun.tarantool(g.tempdir, {}, {g.tempfile}, {nojson = true, stderr = true})

t.assert_equals(res.exit_code, 0)
t.assert_str_contains(res.stdout, 'hello stdout!')
t.assert_str_contains(res.stderr, 'hello stderr!')
end

g.before_test('test_decode_stdout_as_json', function()
g.tempfile_fh:write([[
print('{"a": 1, "b": 2}')
]])
end)

g.test_decode_stdout_as_json = function()
t.skip_if(not utils.version_current_ge_than(2, 4, 1),
"popen module is available since Tarantool 2.4.1.")
local res = justrun.tarantool(g.tempdir, {}, {g.tempfile}, {nojson = false, stdout = true})

t.assert_equals(res.exit_code, 0)
t.assert_equals(res.stdout, {{ a = 1, b = 2}})
end

g.before_test('test_bad_exit_code', function()
g.tempfile_fh:write([[
local magic = require('magic_lib')
]])
end)

g.test_bad_exit_code = function()
t.skip_if(not utils.version_current_ge_than(2, 4, 1),
"popen module is available since Tarantool 2.4.1.")
local res = justrun.tarantool(g.tempdir, {}, {g.tempfile}, {nojson = true, stderr = true})

t.assert_equals(res.exit_code, 1)

t.assert_str_contains(res.stderr, "module 'magic_lib' not found")
t.assert_equals(res.stdout, nil)
end

g.test_error_when_popen_is_not_available = function()
-- Substitute `require` function to test the behavior of `justrun.tarantool`
-- if the `popen` module is not available (on versions below 2.4.1).

-- luacheck: push ignore 121
local old = require
require = function(name) -- ignore:
if name == 'popen' then
return error("module " .. name .. " not found:")
else
return old(name)
end
end

local _, err = pcall(justrun.tarantool, g.tempdir, {}, {g.tempfile}, {nojson = true})

t.assert_str_contains(err, 'module popen not found:')

require = old
-- luacheck: pop
end

0 comments on commit c1b2622

Please sign in to comment.