|
| 1 | +local fun = require('fun') |
| 2 | +local log = require('log') |
| 3 | +local json = require('json') |
| 4 | +local fiber = require('fiber') |
| 5 | +local popen = require('popen') |
| 6 | + |
| 7 | +local justrun = {} |
| 8 | + |
| 9 | +local function collect_stderr(ph) |
| 10 | + local f = fiber.new(function() |
| 11 | + local fiber_name = "child's stderr collector" |
| 12 | + fiber.name(fiber_name, {truncate = true}) |
| 13 | + |
| 14 | + local chunks = {} |
| 15 | + |
| 16 | + while true do |
| 17 | + local chunk, err = ph:read({stderr = true}) |
| 18 | + if chunk == nil then |
| 19 | + log.warn(('%s: got error, exiting: %s'):format( |
| 20 | + fiber_name, tostring(err))) |
| 21 | + break |
| 22 | + end |
| 23 | + if chunk == '' then |
| 24 | + log.info(('%s: got EOF, exiting'):format(fiber_name)) |
| 25 | + break |
| 26 | + end |
| 27 | + table.insert(chunks, chunk) |
| 28 | + end |
| 29 | + |
| 30 | + -- Glue all chunks, strip trailing newline. |
| 31 | + return table.concat(chunks):rstrip() |
| 32 | + end) |
| 33 | + f:set_joinable(true) |
| 34 | + return f |
| 35 | +end |
| 36 | + |
| 37 | +local function cancel_stderr_fiber(stderr_fiber) |
| 38 | + if stderr_fiber == nil then |
| 39 | + return |
| 40 | + end |
| 41 | + stderr_fiber:cancel() |
| 42 | +end |
| 43 | + |
| 44 | +local function join_stderr_fiber(stderr_fiber) |
| 45 | + if stderr_fiber == nil then |
| 46 | + return |
| 47 | + end |
| 48 | + return select(2, assert(stderr_fiber:join())) |
| 49 | +end |
| 50 | + |
| 51 | +-- Run tarantool in given directory with given environment and |
| 52 | +-- command line arguments and catch its output. |
| 53 | +-- |
| 54 | +-- Expects JSON lines as the output and parses it into an array |
| 55 | +-- (it can be disabled using `nojson` option). |
| 56 | +-- |
| 57 | +-- Options: |
| 58 | +-- |
| 59 | +-- - nojson (boolean, default: false) |
| 60 | +-- |
| 61 | +-- Don't attempt to decode stdout as a stream of JSON lines, |
| 62 | +-- return as is. |
| 63 | +-- |
| 64 | +-- - stderr (boolean, default: false) |
| 65 | +-- |
| 66 | +-- Collect stderr and place it into the `stderr` field of the |
| 67 | +-- return value |
| 68 | +function justrun.tarantool(dir, env, args, opts) |
| 69 | + assert(type(dir) == 'string') |
| 70 | + assert(type(env) == 'table') |
| 71 | + assert(type(args) == 'table') |
| 72 | + local opts = opts or {} |
| 73 | + assert(type(opts) == 'table') |
| 74 | + |
| 75 | + -- Prevent system/user inputrc configuration file from |
| 76 | + -- influencing testing code. |
| 77 | + env['INPUTRC'] = '/dev/null' |
| 78 | + |
| 79 | + local tarantool_exe = arg[-1] |
| 80 | + -- Use popen.shell() instead of popen.new() due to lack of |
| 81 | + -- cwd option in popen (gh-5633). |
| 82 | + local env_str = table.concat(fun.iter(env):map(function(k, v) |
| 83 | + return ('%s=%q'):format(k, v) |
| 84 | + end):totable(), ' ') |
| 85 | + local command = ('cd %s && %s %s %s'):format(dir, env_str, tarantool_exe, |
| 86 | + table.concat(args, ' ')) |
| 87 | + log.info(('Running a command: %s'):format(command)) |
| 88 | + local mode = opts.stderr and 'rR' or 'r' |
| 89 | + local ph = popen.shell(command, mode) |
| 90 | + |
| 91 | + local stderr_fiber |
| 92 | + if opts.stderr then |
| 93 | + stderr_fiber = collect_stderr(ph) |
| 94 | + end |
| 95 | + |
| 96 | + -- Read everything until EOF. |
| 97 | + local chunks = {} |
| 98 | + while true do |
| 99 | + local chunk, err = ph:read() |
| 100 | + if chunk == nil then |
| 101 | + cancel_stderr_fiber(stderr_fiber) |
| 102 | + ph:close() |
| 103 | + error(err) |
| 104 | + end |
| 105 | + if chunk == '' then -- EOF |
| 106 | + break |
| 107 | + end |
| 108 | + table.insert(chunks, chunk) |
| 109 | + end |
| 110 | + |
| 111 | + local exit_code = ph:wait().exit_code |
| 112 | + local stderr = join_stderr_fiber(stderr_fiber) |
| 113 | + ph:close() |
| 114 | + |
| 115 | + -- If an error occurs, discard the output and return only the |
| 116 | + -- exit code. However, return stderr. |
| 117 | + if exit_code ~= 0 then |
| 118 | + return { |
| 119 | + exit_code = exit_code, |
| 120 | + stderr = stderr, |
| 121 | + } |
| 122 | + end |
| 123 | + |
| 124 | + -- Glue all chunks, strip trailing newline. |
| 125 | + local res = table.concat(chunks):rstrip() |
| 126 | + log.info(('Command output:\n%s'):format(res)) |
| 127 | + |
| 128 | + -- Decode JSON object per line into array of tables (if |
| 129 | + -- `nojson` option is not passed). |
| 130 | + local decoded |
| 131 | + if opts.nojson then |
| 132 | + decoded = res |
| 133 | + else |
| 134 | + decoded = fun.iter(res:split('\n')):map(json.decode):totable() |
| 135 | + end |
| 136 | + |
| 137 | + return { |
| 138 | + exit_code = exit_code, |
| 139 | + stdout = decoded, |
| 140 | + stderr = stderr, |
| 141 | + } |
| 142 | +end |
| 143 | + |
| 144 | +return justrun |
0 commit comments