Skip to content

Commit

Permalink
Propagate stack trace from Server:exec()
Browse files Browse the repository at this point in the history
If a function executed with `Server:exec()` fails, luatest prints the
stack trace only up to the `Server:exec()` call. If the failure happens
to be in a nested function, this makes it impossible to figure out what
happened.

Let's propagate the error stack trace from the server to the runner
and make the runner concatenate it with its own stack trace. To achieve
that, we wrap errors raised by the function executed by `Serer:exec()`
in `LuatestError` and set `trace` for it.

Closes #396
  • Loading branch information
locker committed Oct 25, 2024
1 parent 027d67b commit c4d9c7a
Show file tree
Hide file tree
Showing 5 changed files with 97 additions and 6 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
- Added `assert_items_exclude`.
- Strip useless `...` lines from error trace.
- Make `assert_error_*` ignore errors raised by assertion failures.
- Fix error trace reporting for functions executed with `Server:exec()`
(gh-396).

## 1.0.1

Expand Down
4 changes: 4 additions & 0 deletions luatest/runner.lua
Original file line number Diff line number Diff line change
Expand Up @@ -475,6 +475,10 @@ function Runner.mt:protected_call(instance, method, pretty_name)
-- transform error into a table, adding the traceback information
local trace = debug.traceback('', 3):sub(2)
if utils.is_luatest_error(e) then
if e.trace ~= nil then
trace = e.trace .. '\n' ..
trace:sub(string.len('stack traceback:\n') + 1)
end
return {status = e.status, message = e.message, trace = trace}
else
return {status = 'error', message = e, trace = trace}
Expand Down
23 changes: 17 additions & 6 deletions luatest/server.lua
Original file line number Diff line number Diff line change
Expand Up @@ -823,6 +823,10 @@ function Server:exec(fn, args, options)
error(('bad argument #3 for exec at %s: an array is required'):format(utils.get_fn_location(fn)))
end

-- Note that we wrap any error raised by the executed function to save
-- the original trace. It will be used by the runner to report the full
-- error trace, see Runner:protected_call().
--
-- If the function fails an assertion in an open transaction, Tarantool
-- will raise the "Transaction is active at return from function" error,
-- thus overwriting the original error raised by the assertion. To avoid
Expand All @@ -836,12 +840,19 @@ function Server:exec(fn, args, options)
debug.setupvalue(fn, i, require(passthrough_ups[name]))
end
end
local result
if args == nil then
result = {pcall(fn)}
else
result = {pcall(fn, unpack(args))}
end
local result = {xpcall(function()
if args == nil then
return fn()
else
return fn(unpack(args))
end
end, function(e)
if not (type(e) == 'table' and e.class == 'LuatestError') then
e = {class = 'LuatestError', status = 'error', message = e}
end
e.trace = debug.traceback('', 3):sub(2)
return e
end)}
if not result[1] then
box.rollback()
end
Expand Down
47 changes: 47 additions & 0 deletions test/fixtures/trace.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
local fio = require('fio')

local t = require('luatest')
local server = require('luatest.server')

local g = t.group('fixtures.trace')

local root = fio.dirname(fio.abspath('test.helpers'))

g.before_all(function(cg)
cg.server = server:new{
env = {
LUA_PATH = root .. '/?.lua;' ..
root .. '/?/init.lua;' ..
root .. '/.rocks/share/tarantool/?.lua'
}
}
cg.server:start()
end)

g.after_all(function(cg)
cg.server:drop()
end)

g.test_error = function(cg)
local function outer()
cg.server:exec(function()
local function inner()
error('test error')
end
inner()
end)
end
outer()
end

g.test_fail = function(cg)
local function outer()
cg.server:exec(function()
local function inner()
t.assert(false)
end
inner()
end)
end
outer()
end
27 changes: 27 additions & 0 deletions test/runner_test.lua
Original file line number Diff line number Diff line change
Expand Up @@ -224,3 +224,30 @@ g.test_show_help = function()
local captured = capture:flush()
t.assert_str_contains(captured.stdout, 'Usage: luatest')
end

g.test_trace = function()
local f = io.popen('bin/luatest test/fixtures/trace.lua')
local output = f:read('*a')
f:close()
t.assert_str_matches(
output,
".*" ..
"[^\n]*trace%.lua:29: test error[^\n]*\n" ..
"stack traceback:\n" ..
"[^\n]*trace%.lua:29: in function 'inner'\n" ..
"[^\n]*trace%.lua:31: in function <[^\n]*trace%.lua:27>\n" ..
"[^\n]*trace%.lua:27: in function 'outer'\n" ..
"[^\n]*trace%.lua:34: in function 'fixtures%.trace%.test_error'\n" ..
".*")
t.assert_str_matches(
output,
".*" ..
"[^\n]*trace%.lua:41: expected: a value evaluating to true, " ..
"actual: false[^\n]*\n" ..
"stack traceback:\n" ..
"[^\n]*trace%.lua:41: in function 'inner'\n" ..
"[^\n]*trace%.lua:43: in function <[^\n]*trace%.lua:39>\n" ..
"[^\n]*trace%.lua:39: in function 'outer'\n" ..
"[^\n]*trace%.lua:46: in function 'fixtures%.trace%.test_fail'\n" ..
".*")
end

0 comments on commit c4d9c7a

Please sign in to comment.