Skip to content

Commit f4d3ba4

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 420df34 commit f4d3ba4

File tree

6 files changed

+129
-29
lines changed

6 files changed

+129
-29
lines changed

CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020
- Fix reporting of an assertion failure in `Server:exec()` in case verbose
2121
error serialization is enabled in Tarantool (gh-376).
2222
- Added `assert_items_exclude`.
23+
- Fix error trace reporting for functions executed with `Server:exec()`
24+
(gh-396).
2325

2426
## 1.0.1
2527

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

test/server_test.lua

+32-23
Original file line numberDiff line numberDiff line change
@@ -208,10 +208,11 @@ g.test_net_box_exec = function()
208208

209209
do
210210
local _l_fn, fn = l(), function() error('X_x') end
211-
t.assert_error_msg_equals(
212-
efmt(_l_fn, 'X_x'),
213-
exec, fn
214-
)
211+
t.assert_error_covers({
212+
class = 'LuatestError',
213+
status = 'error',
214+
message = efmt(_l_fn, 'X_x'),
215+
}, exec, fn)
215216
end
216217

217218
do
@@ -512,20 +513,24 @@ end)
512513
g.test_error_level_is_correct = function()
513514
local c = require('net.box').connect(g.s.net_box_uri)
514515

515-
t.assert_error_msg_contains( -- error in exec
516-
"My error", g.s.exec, g.s,
517-
function() error("My error") end)
516+
t.assert_error_covers({ -- error in exec
517+
class = 'LuatestError',
518+
status = 'error',
519+
message = "My error",
520+
}, g.s.exec, g.s, function() error("My error", 0) end)
518521

519522
t.assert_error_msg_contains( -- error in eval
520523
"eval", g.s.eval, g.s,
521524
[[error("My error")]])
522525

523-
t.assert_error_msg_contains( -- error in closures
524-
"My error", g.s.exec, g.s,
525-
function()
526-
local function internal() error("My error") end
527-
internal()
528-
end)
526+
t.assert_error_covers({ -- error in closures
527+
class = 'LuatestError',
528+
status = 'error',
529+
message = "My error",
530+
}, g.s.exec, g.s, function()
531+
local function internal() error("My error", 0) end
532+
internal()
533+
end)
529534

530535
t.assert_error_msg_contains( -- error in tx netbox connection
531536
"My error", c.eval, c,
@@ -535,16 +540,20 @@ g.test_error_level_is_correct = function()
535540
"My error", g.s.eval, g.s,
536541
[[box.begin() error("My error")]])
537542

538-
t.assert_error_msg_contains( -- error in tx exec
539-
"My error", g.s.exec, g.s,
540-
function() box.begin() error("My error") end)
541-
542-
t.assert_error_msg_contains( -- error in tx closures
543-
"My error", g.s.exec, g.s,
544-
function()
545-
local function internal() box.begin() error("My error") end
546-
internal()
547-
end)
543+
t.assert_error_covers({ -- error in tx exec
544+
class = 'LuatestError',
545+
status = 'error',
546+
message = "My error",
547+
}, g.s.exec, g.s, function() box.begin() error("My error", 0) end)
548+
549+
t.assert_error_covers({ -- error in tx closures
550+
class = 'LuatestError',
551+
status = 'error',
552+
message = "My error",
553+
}, g.s.exec, g.s, function()
554+
local function internal() box.begin() error("My error", 0) end
555+
internal()
556+
end)
548557
end
549558

550559
g.after_test('test_error_level_is_correct', function()

0 commit comments

Comments
 (0)