Skip to content

Commit 701e551

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 92a8a6f commit 701e551

File tree

6 files changed

+126
-12
lines changed

6 files changed

+126
-12
lines changed

CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121
error serialization is enabled in Tarantool (gh-376).
2222
- Added `assert_items_exclude`.
2323
- Strip useless `...` lines from error trace.
24+
- Fix error trace reporting for functions executed with `Server:exec()`
25+
(gh-396).
2426

2527
## 1.0.1
2628

luatest/assertions.lua

+19-5
Original file line numberDiff line numberDiff line change
@@ -164,12 +164,26 @@ local function pcall_check_trace(level, fn, ...)
164164
return ok, err
165165
end
166166

167+
--
168+
-- Wrapper around pcall that:
169+
-- * checks the stack trace for box errors;
170+
-- * unwraps the error if it was wrapped by Server:exec().
171+
--
172+
local function pcall_wrapper(level, fn, ...)
173+
local ok, err = pcall_check_trace(level + 1, fn, ...)
174+
if not ok and utils.is_luatest_error(err) and
175+
err.status == 'wrapped_by_exec' then
176+
err = err.message
177+
end
178+
return ok, err
179+
end
180+
167181
--- Check that calling fn raises an error.
168182
--
169183
-- @func fn
170184
-- @param ... arguments for function
171185
function M.assert_error(fn, ...)
172-
local ok, err = pcall_check_trace(2, fn, ...)
186+
local ok, err = pcall_wrapper(2, fn, ...)
173187
if ok then
174188
failure("Expected an error when calling function but no error generated", nil, 2)
175189
end
@@ -565,7 +579,7 @@ function M.assert_str_matches(value, pattern, start, final, message)
565579
end
566580

567581
local function _assert_error_msg_equals(stripFileAndLine, expectedMsg, func, ...)
568-
local no_error, error_msg = pcall_check_trace(3, func, ...)
582+
local no_error, error_msg = pcall_wrapper(3, func, ...)
569583
if no_error then
570584
local failure_message = string.format(
571585
'Function successfully returned: %s\nExpected error: %s',
@@ -631,7 +645,7 @@ end
631645
-- @func fn
632646
-- @param ... arguments for function
633647
function M.assert_error_msg_contains(expected_partial, fn, ...)
634-
local no_error, error_msg = pcall_check_trace(2, fn, ...)
648+
local no_error, error_msg = pcall_wrapper(2, fn, ...)
635649
log.info('Assert error message %s contains %s', error_msg, expected_partial)
636650
if no_error then
637651
local failure_message = string.format(
@@ -654,7 +668,7 @@ end
654668
-- @func fn
655669
-- @param ... arguments for function
656670
function M.assert_error_msg_matches(pattern, fn, ...)
657-
local no_error, error_msg = pcall_check_trace(2, fn, ...)
671+
local no_error, error_msg = pcall_wrapper(2, fn, ...)
658672
if no_error then
659673
local failure_message = string.format(
660674
'Function successfully returned: %s\nExpected error matching: %s',
@@ -694,7 +708,7 @@ end
694708
-- @func fn
695709
-- @param ... arguments for function
696710
function M.assert_error_covers(expected, fn, ...)
697-
local ok, actual = pcall_check_trace(2, fn, ...)
711+
local ok, actual = pcall_wrapper(2, fn, ...)
698712
if ok then
699713
fail_fmt(2, nil,
700714
'Function successfully returned: %s\nExpected error: %s',

luatest/runner.lua

+10-1
Original file line numberDiff line numberDiff line change
@@ -475,7 +475,16 @@ 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-
return {status = e.status, message = e.message, trace = trace}
478+
local status = e.status
479+
if status == 'wrapped_by_exec' then
480+
-- This is an error wrapped by Server:exec() to save the trace.
481+
-- Concatenate the current trace with the saved trace.
482+
assert(e.trace ~= nil)
483+
trace = e.trace .. '\n' ..
484+
trace:sub(string.len('stack traceback:\n') + 1)
485+
status = 'error'
486+
end
487+
return {status = status, message = e.message, trace = trace}
479488
else
480489
return {status = 'error', message = e, trace = trace}
481490
end

luatest/server.lua

+21-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,23 @@ 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 = {
852+
class = 'LuatestError',
853+
status = 'wrapped_by_exec',
854+
message = e,
855+
}
856+
end
857+
e.trace = debug.traceback('', 3):sub(2)
858+
return e
859+
end)}
845860
if not result[1] then
846861
box.rollback()
847862
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)