Skip to content

Commit ad991cc

Browse files
committed
Propagate stack trace from Server:exec()
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
1 parent db932f3 commit ad991cc

File tree

5 files changed

+97
-6
lines changed

5 files changed

+97
-6
lines changed

CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222
- Added `assert_items_exclude`.
2323
- Strip useless `...` lines from error trace.
2424
- Make `assert_error_*` ignore errors raised by assertion failures.
25+
- Fix error trace reporting for functions executed with `Server:exec()`
26+
(gh-396).
2527

2628
## 1.0.1
2729

luatest/runner.lua

+4
Original file line numberDiff line numberDiff line change
@@ -475,6 +475,10 @@ function Runner.mt:protected_call(instance, method, pretty_name)
475475
-- transform error into a table, adding the traceback information
476476
local trace = debug.traceback('', 3):sub(2)
477477
if utils.is_luatest_error(e) then
478+
if e.trace ~= nil then
479+
trace = e.trace .. '\n' ..
480+
trace:sub(string.len('stack traceback:\n') + 1)
481+
end
478482
return {status = e.status, message = e.message, trace = trace}
479483
else
480484
return {status = 'error', message = e, trace = trace}

luatest/server.lua

+17-6
Original file line numberDiff line numberDiff line change
@@ -823,6 +823,10 @@ function Server:exec(fn, args, options)
823823
error(('bad argument #3 for exec at %s: an array is required'):format(utils.get_fn_location(fn)))
824824
end
825825

826+
-- Note that we wrap any error raised by the executed function to save
827+
-- the original trace. It will be used by the runner to report the full
828+
-- error trace, see Runner:protected_call().
829+
--
826830
-- If the function fails an assertion in an open transaction, Tarantool
827831
-- will raise the "Transaction is active at return from function" error,
828832
-- thus overwriting the original error raised by the assertion. To avoid
@@ -836,12 +840,19 @@ function Server:exec(fn, args, options)
836840
debug.setupvalue(fn, i, require(passthrough_ups[name]))
837841
end
838842
end
839-
local result
840-
if args == nil then
841-
result = {pcall(fn)}
842-
else
843-
result = {pcall(fn, unpack(args))}
844-
end
843+
local result = {xpcall(function()
844+
if args == nil then
845+
return fn()
846+
else
847+
return fn(unpack(args))
848+
end
849+
end, function(e)
850+
if not (type(e) == 'table' and e.class == 'LuatestError') then
851+
e = {class = 'LuatestError', status = 'error', message = e}
852+
end
853+
e.trace = debug.traceback('', 3):sub(2)
854+
return e
855+
end)}
845856
if not result[1] then
846857
box.rollback()
847858
end

test/fixtures/trace.lua

+47
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
local fio = require('fio')
2+
3+
local t = require('luatest')
4+
local server = require('luatest.server')
5+
6+
local g = t.group('fixtures.trace')
7+
8+
local root = fio.dirname(fio.abspath('test.helpers'))
9+
10+
g.before_all(function(cg)
11+
cg.server = server:new{
12+
env = {
13+
LUA_PATH = root .. '/?.lua;' ..
14+
root .. '/?/init.lua;' ..
15+
root .. '/.rocks/share/tarantool/?.lua'
16+
}
17+
}
18+
cg.server:start()
19+
end)
20+
21+
g.after_all(function(cg)
22+
cg.server:drop()
23+
end)
24+
25+
g.test_error = function(cg)
26+
local function outer()
27+
cg.server:exec(function()
28+
local function inner()
29+
error('test error')
30+
end
31+
inner()
32+
end)
33+
end
34+
outer()
35+
end
36+
37+
g.test_fail = function(cg)
38+
local function outer()
39+
cg.server:exec(function()
40+
local function inner()
41+
t.assert(false)
42+
end
43+
inner()
44+
end)
45+
end
46+
outer()
47+
end

test/runner_test.lua

+27
Original file line numberDiff line numberDiff line change
@@ -224,3 +224,30 @@ g.test_show_help = function()
224224
local captured = capture:flush()
225225
t.assert_str_contains(captured.stdout, 'Usage: luatest')
226226
end
227+
228+
g.test_trace = function()
229+
local f = io.popen('bin/luatest test/fixtures/trace.lua')
230+
local output = f:read('*a')
231+
f:close()
232+
t.assert_str_matches(
233+
output,
234+
".*" ..
235+
"[^\n]*trace%.lua:29: test error[^\n]*\n" ..
236+
"stack traceback:\n" ..
237+
"[^\n]*trace%.lua:29: in function 'inner'\n" ..
238+
"[^\n]*trace%.lua:31: in function <[^\n]*trace%.lua:27>\n" ..
239+
"[^\n]*trace%.lua:27: in function 'outer'\n" ..
240+
"[^\n]*trace%.lua:34: in function 'fixtures%.trace%.test_error'\n" ..
241+
".*")
242+
t.assert_str_matches(
243+
output,
244+
".*" ..
245+
"[^\n]*trace%.lua:41: expected: a value evaluating to true, " ..
246+
"actual: false[^\n]*\n" ..
247+
"stack traceback:\n" ..
248+
"[^\n]*trace%.lua:41: in function 'inner'\n" ..
249+
"[^\n]*trace%.lua:43: in function <[^\n]*trace%.lua:39>\n" ..
250+
"[^\n]*trace%.lua:39: in function 'outer'\n" ..
251+
"[^\n]*trace%.lua:46: in function 'fixtures%.trace%.test_fail'\n" ..
252+
".*")
253+
end

0 commit comments

Comments
 (0)