Skip to content

Commit e7667b0

Browse files
nshyylobankov
authored andcommitted
Add trace check for error assertions
In the scope of the referenced Tarantool issue we are going to change error trace of API to point to the caller place. The error should be box.error and the trace will be changed for several modules at the beginning (fix all API at once is difficult). We are going to use existing tests to test the change. In particular in case of Luatest let's check trace in `assert_error*` assertions besides the main assertion. Required for tarantool/tarantool#9914
1 parent 718b58b commit e7667b0

File tree

5 files changed

+173
-5
lines changed

5 files changed

+173
-5
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
- Add `justrun` helper as a tarantool runner and output catcher (gh-365).
1111
- Changed error message for too long Unix domain socket paths (gh-341).
1212
- Add `cbuilder` helper as a declarative configuration builder (gh-366).
13+
- Make `assert_error_*` additionally check error trace if required.
1314

1415
## 1.0.1
1516

luatest/assertions.lua

+84-5
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ local mismatch_formatter = require('luatest.mismatch_formatter')
1010
local pp = require('luatest.pp')
1111
local log = require('luatest.log')
1212
local utils = require('luatest.utils')
13+
local tarantool = require('tarantool')
14+
local ffi = require('ffi')
1315

1416
local prettystr = pp.tostring
1517
local prettystr_pairs = pp.tostring_pair
@@ -18,6 +20,8 @@ local M = {}
1820

1921
local xfail = false
2022

23+
local box_error_type = ffi.typeof(box.error.new(box.error.UNKNOWN))
24+
2125
-- private exported functions (for testing)
2226
M.private = {}
2327

@@ -85,12 +89,87 @@ local function error_msg_equality(actual, expected, deep_analysis)
8589
end
8690
M.private.error_msg_equality = error_msg_equality
8791

92+
--
93+
-- The wrapper is used when trace check is required. See pcall_check_trace.
94+
--
95+
-- Without wrapper the trace will point to the pcall implementation. So trace
96+
-- check is not strict enough (the trace can point to any pcall in below in
97+
-- call trace).
98+
--
99+
local trace_line = debug.getinfo(1, 'l').currentline + 2
100+
local function wrapped_call(fn, ...)
101+
local res = utils.table_pack(fn(...))
102+
-- With `return fn(...)` wrapper does not work due to tail call
103+
-- optimization.
104+
return unpack(res, 1, res.n)
105+
end
106+
107+
-- Expected trace for trace check. See pcall_check_trace.
108+
local wrapped_trace = {
109+
file = debug.getinfo(1, 'S').short_src,
110+
line = trace_line,
111+
}
112+
113+
-- Used in tests to force check for given module.
114+
M.private.check_trace_module = nil
115+
116+
--
117+
-- Return true if error trace check is required for function. Basically it is
118+
-- just a wrapper around Tarantool's utils.proper_trace_required. Additionally
119+
-- old Tarantool versions where this function is not present are handled.
120+
--
121+
local function trace_check_is_required(fn)
122+
local src = debug.getinfo(fn, 'S').short_src
123+
if M.private.check_trace_module == src then
124+
return true
125+
end
126+
if tarantool._internal ~= nil and
127+
tarantool._internal.trace_check_is_required ~= nil then
128+
local path = debug.getinfo(fn, 'S').short_src
129+
return tarantool._internal.trace_check_is_required(path)
130+
end
131+
return false
132+
end
133+
134+
--
135+
-- Substitute for pcall but additionally checks error trace if required.
136+
--
137+
-- The error should be box.error and trace should point to the place
138+
-- where fn is called.
139+
--
140+
-- level is used to set proper level in error assertions that use this function.
141+
--
142+
local function pcall_check_trace(level, fn, ...)
143+
local fn_explicit = fn
144+
if type(fn) ~= 'function' then
145+
fn_explicit = debug.getmetatable(fn).__call
146+
end
147+
if not trace_check_is_required(fn_explicit) then
148+
return pcall(fn, ...)
149+
end
150+
local ok, err = pcall(wrapped_call, fn, ...)
151+
if ok then
152+
return ok, err
153+
end
154+
if type(err) ~= 'cdata' or ffi.typeof(err) ~= box_error_type then
155+
fail_fmt(level + 1, nil, 'Error raised is not a box.error: %s',
156+
prettystr(err))
157+
end
158+
local unpacked = err:unpack()
159+
if not comparator.equals(unpacked.trace[1], wrapped_trace) then
160+
fail_fmt(level + 1, nil,
161+
'Unexpected error trace, expected: %s, actual: %s',
162+
prettystr(wrapped_trace), prettystr(unpacked.trace[1]))
163+
end
164+
return ok, err
165+
end
166+
88167
--- Check that calling fn raises an error.
89168
--
90169
-- @func fn
91170
-- @param ... arguments for function
92171
function M.assert_error(fn, ...)
93-
local ok, err = pcall(fn, ...)
172+
local ok, err = pcall_check_trace(2, fn, ...)
94173
if ok then
95174
failure("Expected an error when calling function but no error generated", nil, 2)
96175
end
@@ -464,7 +543,7 @@ function M.assert_str_matches(value, pattern, start, final, message)
464543
end
465544

466545
local function _assert_error_msg_equals(stripFileAndLine, expectedMsg, func, ...)
467-
local no_error, error_msg = pcall(func, ...)
546+
local no_error, error_msg = pcall_check_trace(3, func, ...)
468547
if no_error then
469548
local failure_message = string.format(
470549
'Function successfully returned: %s\nExpected error: %s',
@@ -530,7 +609,7 @@ end
530609
-- @func fn
531610
-- @param ... arguments for function
532611
function M.assert_error_msg_contains(expected_partial, fn, ...)
533-
local no_error, error_msg = pcall(fn, ...)
612+
local no_error, error_msg = pcall_check_trace(2, fn, ...)
534613
log.info('Assert error message %s contains %s', error_msg, expected_partial)
535614
if no_error then
536615
local failure_message = string.format(
@@ -553,7 +632,7 @@ end
553632
-- @func fn
554633
-- @param ... arguments for function
555634
function M.assert_error_msg_matches(pattern, fn, ...)
556-
local no_error, error_msg = pcall(fn, ...)
635+
local no_error, error_msg = pcall_check_trace(2, fn, ...)
557636
if no_error then
558637
local failure_message = string.format(
559638
'Function successfully returned: %s\nExpected error matching: %s',
@@ -578,7 +657,7 @@ end
578657
-- @func fn
579658
-- @param ... arguments for function
580659
function M.assert_error_covers(expected, fn, ...)
581-
local ok, actual = pcall(fn, ...)
660+
local ok, actual = pcall_check_trace(2, fn, ...)
582661
if ok then
583662
fail_fmt(2, nil,
584663
'Function successfully returned: %s\nExpected error: %s',

luatest/utils.lua

+5
Original file line numberDiff line numberDiff line change
@@ -191,4 +191,9 @@ function utils.is_tarantool_binary(path)
191191
return path:find('^.*/tarantool[^/]*$') ~= nil
192192
end
193193

194+
-- Return args as table with 'n' set to args number.
195+
function utils.table_pack(...)
196+
return {n = select('#', ...), ...}
197+
end
198+
194199
return utils

test/luaunit/assertions_error_test.lua

+75
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ local g = t.group()
44
local helper = require('test.helpers.general')
55
local assert_failure = helper.assert_failure
66
local assert_failure_equals = helper.assert_failure_equals
7+
local assert_failure_contains = helper.assert_failure_contains
78

89
local function f()
910
end
@@ -17,6 +18,28 @@ local function f_with_table_error()
1718
error(setmetatable({this_table="has error"}, ts))
1819
end
1920

21+
local f_check_trace = function(level)
22+
box.error(box.error.UNKNOWN, level)
23+
end
24+
25+
local line = debug.getinfo(1, 'l').currentline + 2
26+
local f_check_trace_wrapper = function()
27+
f_check_trace(2)
28+
end
29+
30+
local _, err = pcall(f_check_trace_wrapper)
31+
local box_error_has_level = err:unpack().trace[1].line == line
32+
33+
local f_check_success = function()
34+
return {1, 'foo'}
35+
end
36+
37+
local THIS_MODULE = debug.getinfo(1, 'S').short_src
38+
39+
g.after_each(function()
40+
t.private.check_trace_module = nil
41+
end)
42+
2043
function g.test_assert_error()
2144
local x = 1
2245

@@ -51,6 +74,10 @@ function g.test_assert_error()
5174
-- error generated as table
5275
t.assert_error(f_with_table_error, 1)
5376

77+
-- test assert failure due to unexpected error trace
78+
t.private.check_trace_module = THIS_MODULE
79+
assert_failure_contains('Unexpected error trace, expected:',
80+
t.assert_error, f_check_trace, 1)
5481
end
5582

5683
function g.test_assert_errorMsgContains()
@@ -64,6 +91,12 @@ function g.test_assert_errorMsgContains()
6491

6592
-- error message is a table which converts to a string
6693
t.assert_error_msg_contains('This table has error', f_with_table_error, 1)
94+
95+
-- test assert failure due to unexpected error trace
96+
t.private.check_trace_module = THIS_MODULE
97+
assert_failure_contains('Unexpected error trace, expected:',
98+
t.assert_error_msg_contains, 'bar', f_check_trace,
99+
1)
67100
end
68101

69102
function g.test_assert_error_msg_equals()
@@ -103,6 +136,11 @@ function g.test_assert_error_msg_equals()
103136

104137
-- expected table, error generated as string, no match
105138
assert_failure(t.assert_error_msg_equals, {1}, function() error("{1}") end, 33)
139+
140+
-- test assert failure due to unexpected error trace
141+
t.private.check_trace_module = THIS_MODULE
142+
assert_failure_contains('Unexpected error trace, expected:',
143+
t.assert_error_msg_equals, 'bar', f_check_trace, 1)
106144
end
107145

108146
function g.test_assert_errorMsgMatches()
@@ -117,6 +155,11 @@ function g.test_assert_errorMsgMatches()
117155
-- one space added to cause failure
118156
assert_failure(t.assert_error_msg_matches, ' This is an error', f_with_error, x)
119157
assert_failure(t.assert_error_msg_matches, "This", f_with_table_error, 33)
158+
159+
-- test assert failure due to unexpected error trace
160+
t.private.check_trace_module = THIS_MODULE
161+
assert_failure_contains('Unexpected error trace, expected:',
162+
t.assert_error_msg_matches, 'bar', f_check_trace, 1)
120163
end
121164

122165
function g.test_assert_errorCovers()
@@ -140,4 +183,36 @@ function g.test_assert_errorCovers()
140183
-- bad error coverage
141184
assert_failure(t.assert_error_covers, {b = 2},
142185
function(a, b) error({a = a, b = b}) end, 1, 3)
186+
187+
-- test assert failure due to unexpected error trace
188+
t.private.check_trace_module = THIS_MODULE
189+
assert_failure_contains('Unexpected error trace, expected:',
190+
t.assert_error_covers, 'bar', f_check_trace, 1)
191+
end
192+
193+
function g.test_error_trace_check()
194+
local foo = function(a) error(a) end
195+
-- test when trace check is NOT required
196+
t.assert_error_msg_content_equals('foo', foo, 'foo')
197+
198+
local ftor = setmetatable({}, {
199+
__call = function(_, ...) return f_check_trace(...) end
200+
})
201+
t.private.check_trace_module = THIS_MODULE
202+
203+
-- test when trace check IS required
204+
if box_error_has_level then
205+
t.assert_error_covers({code = box.error.UNKNOWN}, f_check_trace, 2)
206+
t.assert_error_covers({code = box.error.UNKNOWN}, ftor, 2)
207+
end
208+
209+
-- check if there is no error then the returned value is reported correctly
210+
assert_failure_contains('Function successfully returned: {1, "foo"}',
211+
t.assert_error_msg_equals, 'bar', f_check_success)
212+
-- test assert failure due to unexpected error type
213+
assert_failure_contains('Error raised is not a box.error:',
214+
t.assert_error, foo, 'foo')
215+
-- test assert failure due to unexpected error trace
216+
assert_failure_contains('Unexpected error trace, expected:',
217+
t.assert_error, f_check_trace, 1)
143218
end

test/utils_test.lua

+8
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,11 @@ g.test_is_tarantool_binary = function()
2222
("Unexpected result for %q"):format(path))
2323
end
2424
end
25+
26+
g.test_table_pack = function()
27+
t.assert_equals(utils.table_pack(), {n = 0})
28+
t.assert_equals(utils.table_pack(1), {n = 1, 1})
29+
t.assert_equals(utils.table_pack(1, 2), {n = 2, 1, 2})
30+
t.assert_equals(utils.table_pack(1, 2, nil), {n = 3, 1, 2})
31+
t.assert_equals(utils.table_pack(1, 2, nil, 3), {n = 4, 1, 2, nil, 3})
32+
end

0 commit comments

Comments
 (0)