Skip to content

Commit c1b2622

Browse files
Oleg Chaplashkinylobankov
authored andcommitted
Migrate justrun module and adapt it
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
1 parent d985997 commit c1b2622

File tree

5 files changed

+273
-0
lines changed

5 files changed

+273
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
- Improve `luatest.log` function if a `nil` value is passed (gh-360).
88
- Added `assert_error_covers`.
99
- Add more logs (gh-326).
10+
- Add `justrun` helper as a tarantool runner and output catcher (gh-365).
1011

1112
## 1.0.1
1213

config.ld

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ file = {
77
'luatest/runner.lua',
88
'luatest/server.lua',
99
'luatest/replica_set.lua',
10+
'luatest/justrun.lua',
1011
}
1112
topics = {
1213
'CHANGELOG.md',

luatest/init.lua

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,11 @@ local parametrizer = require('luatest.parametrizer')
3838
--
3939
luatest.log = require('luatest.log')
4040

41+
--- Simple Tarantool runner and output catcher.
42+
--
43+
-- @see luatest.justrun
44+
luatest.justrun = require('luatest.justrun')
45+
4146
--- Add before suite hook.
4247
--
4348
-- @function before_suite

luatest/justrun.lua

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
--- Simple Tarantool runner and output catcher.
2+
--
3+
-- Sometimes it is necessary to run tarantool with particular arguments and
4+
-- verify its output. `luatest.server` provides a supervisor like
5+
-- interface: an instance is started, calls box.cfg() and we can
6+
-- communicate with it using net.box. Another helper in tarantool/tarantool,
7+
-- `test.interactive_tarantool`, aims to solve all the problems around
8+
-- readline console and also provides ability to communicate with the
9+
-- instance interactively.
10+
--
11+
-- However, there is nothing like 'just run tarantool with given args and
12+
-- give me its output'.
13+
--
14+
-- @module luatest.justrun
15+
16+
local checks = require('checks')
17+
local fun = require('fun')
18+
local json = require('json')
19+
local fiber = require('fiber')
20+
21+
local log = require('luatest.log')
22+
23+
local justrun = {}
24+
25+
local function collect_stderr(ph)
26+
local f = fiber.new(function()
27+
local fiber_name = "child's stderr collector"
28+
fiber.name(fiber_name, {truncate = true})
29+
30+
local chunks = {}
31+
32+
while true do
33+
local chunk, err = ph:read({stderr = true})
34+
if chunk == nil then
35+
log.warn('%s: got error, exiting: %s', fiber_name, err)
36+
break
37+
end
38+
if chunk == '' then
39+
log.info('%s: got EOF, exiting', fiber_name)
40+
break
41+
end
42+
table.insert(chunks, chunk)
43+
end
44+
45+
-- Glue all chunks, strip trailing newline.
46+
return table.concat(chunks):rstrip()
47+
end)
48+
f:set_joinable(true)
49+
return f
50+
end
51+
52+
local function cancel_stderr_fiber(stderr_fiber)
53+
if stderr_fiber == nil then
54+
return
55+
end
56+
stderr_fiber:cancel()
57+
end
58+
59+
local function join_stderr_fiber(stderr_fiber)
60+
if stderr_fiber == nil then
61+
return
62+
end
63+
return select(2, assert(stderr_fiber:join()))
64+
end
65+
66+
--- Run tarantool in given directory with given environment and
67+
-- command line arguments and catch its output.
68+
--
69+
-- Expects JSON lines as the output and parses it into an array
70+
-- (it can be disabled using `nojson` option).
71+
--
72+
-- Options:
73+
--
74+
-- - nojson (boolean, default: false)
75+
--
76+
-- Don't attempt to decode stdout as a stream of JSON lines,
77+
-- return as is.
78+
--
79+
-- - stderr (boolean, default: false)
80+
--
81+
-- Collect stderr and place it into the `stderr` field of the
82+
-- return value
83+
--
84+
-- - quote_args (boolean, default: false)
85+
--
86+
-- Quote CLI arguments before concatenating them into a shell
87+
-- command.
88+
--
89+
-- @string dir Directory where the process will run.
90+
-- @tparam table env Environment variables for the process.
91+
-- @tparam table args Options that will be passed when the process starts.
92+
-- @tparam[opt] table opts Custom options: nojson, stderr and quote_args.
93+
-- @treturn table
94+
function justrun.tarantool(dir, env, args, opts)
95+
checks('string', 'table', 'table', '?table')
96+
opts = opts or {}
97+
98+
local popen = require('popen')
99+
100+
-- Prevent system/user inputrc configuration file from
101+
-- influencing testing code.
102+
env['INPUTRC'] = '/dev/null'
103+
104+
local tarantool_exe = arg[-1]
105+
-- Use popen.shell() instead of popen.new() due to lack of
106+
-- cwd option in popen (gh-5633).
107+
local env_str = table.concat(fun.iter(env):map(function(k, v)
108+
return ('%s=%q'):format(k, v)
109+
end):totable(), ' ')
110+
local args_str = table.concat(fun.iter(args):map(function(v)
111+
return opts.quote_args and ('%q'):format(v) or v
112+
end):totable(), ' ')
113+
local command = ('cd %s && %s %s %s'):format(dir, env_str, tarantool_exe,
114+
args_str)
115+
log.info('Running a command: %s', command)
116+
local mode = opts.stderr and 'rR' or 'r'
117+
local ph = popen.shell(command, mode)
118+
119+
local stderr_fiber
120+
if opts.stderr then
121+
stderr_fiber = collect_stderr(ph)
122+
end
123+
124+
-- Read everything until EOF.
125+
local chunks = {}
126+
while true do
127+
local chunk, err = ph:read()
128+
if chunk == nil then
129+
cancel_stderr_fiber(stderr_fiber)
130+
ph:close()
131+
error(err)
132+
end
133+
if chunk == '' then -- EOF
134+
break
135+
end
136+
table.insert(chunks, chunk)
137+
end
138+
139+
local exit_code = ph:wait().exit_code
140+
local stderr = join_stderr_fiber(stderr_fiber)
141+
ph:close()
142+
143+
-- If an error occurs, discard the output and return only the
144+
-- exit code. However, return stderr.
145+
if exit_code ~= 0 then
146+
return {
147+
exit_code = exit_code,
148+
stderr = stderr,
149+
}
150+
end
151+
152+
-- Glue all chunks, strip trailing newline.
153+
local res = table.concat(chunks):rstrip()
154+
log.info('Command output:\n%s', res)
155+
156+
-- Decode JSON object per line into array of tables (if
157+
-- `nojson` option is not passed).
158+
local decoded
159+
if opts.nojson then
160+
decoded = res
161+
else
162+
decoded = fun.iter(res:split('\n')):map(json.decode):totable()
163+
end
164+
165+
return {
166+
exit_code = exit_code,
167+
stdout = decoded,
168+
stderr = stderr,
169+
}
170+
end
171+
172+
return justrun

test/justrun_test.lua

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
local t = require('luatest')
2+
local fio = require('fio')
3+
4+
local justrun = require('luatest.justrun')
5+
local utils = require('luatest.utils')
6+
7+
local g = t.group()
8+
9+
g.before_each(function()
10+
g.tempdir = fio.tempdir()
11+
g.tempfile = fio.pathjoin(g.tempdir, 'main.lua')
12+
13+
local default_flags = {'O_CREAT', 'O_WRONLY', 'O_TRUNC'}
14+
local default_mode = tonumber('644', 8)
15+
16+
g.tempfile_fh = fio.open(g.tempfile, default_flags, default_mode)
17+
end)
18+
19+
g.after_each(function()
20+
fio.rmdir(g.tempdir)
21+
end)
22+
23+
g.before_test('test_stdout_stderr_output', function()
24+
g.tempfile_fh:write([[
25+
local log = require('log')
26+
27+
print('hello stdout!')
28+
log.info('hello stderr!')
29+
]])
30+
end)
31+
32+
g.test_stdout_stderr_output = function()
33+
t.skip_if(not utils.version_current_ge_than(2, 4, 1),
34+
"popen module is available since Tarantool 2.4.1.")
35+
local res = justrun.tarantool(g.tempdir, {}, {g.tempfile}, {nojson = true, stderr = true})
36+
37+
t.assert_equals(res.exit_code, 0)
38+
t.assert_str_contains(res.stdout, 'hello stdout!')
39+
t.assert_str_contains(res.stderr, 'hello stderr!')
40+
end
41+
42+
g.before_test('test_decode_stdout_as_json', function()
43+
g.tempfile_fh:write([[
44+
print('{"a": 1, "b": 2}')
45+
]])
46+
end)
47+
48+
g.test_decode_stdout_as_json = function()
49+
t.skip_if(not utils.version_current_ge_than(2, 4, 1),
50+
"popen module is available since Tarantool 2.4.1.")
51+
local res = justrun.tarantool(g.tempdir, {}, {g.tempfile}, {nojson = false, stdout = true})
52+
53+
t.assert_equals(res.exit_code, 0)
54+
t.assert_equals(res.stdout, {{ a = 1, b = 2}})
55+
end
56+
57+
g.before_test('test_bad_exit_code', function()
58+
g.tempfile_fh:write([[
59+
local magic = require('magic_lib')
60+
]])
61+
end)
62+
63+
g.test_bad_exit_code = function()
64+
t.skip_if(not utils.version_current_ge_than(2, 4, 1),
65+
"popen module is available since Tarantool 2.4.1.")
66+
local res = justrun.tarantool(g.tempdir, {}, {g.tempfile}, {nojson = true, stderr = true})
67+
68+
t.assert_equals(res.exit_code, 1)
69+
70+
t.assert_str_contains(res.stderr, "module 'magic_lib' not found")
71+
t.assert_equals(res.stdout, nil)
72+
end
73+
74+
g.test_error_when_popen_is_not_available = function()
75+
-- Substitute `require` function to test the behavior of `justrun.tarantool`
76+
-- if the `popen` module is not available (on versions below 2.4.1).
77+
78+
-- luacheck: push ignore 121
79+
local old = require
80+
require = function(name) -- ignore:
81+
if name == 'popen' then
82+
return error("module " .. name .. " not found:")
83+
else
84+
return old(name)
85+
end
86+
end
87+
88+
local _, err = pcall(justrun.tarantool, g.tempdir, {}, {g.tempfile}, {nojson = true})
89+
90+
t.assert_str_contains(err, 'module popen not found:')
91+
92+
require = old
93+
-- luacheck: pop
94+
end

0 commit comments

Comments
 (0)