From caef7c47d98447dff0bffa731c87f532ded253ec Mon Sep 17 00:00:00 2001 From: Vladimir Davydov Date: Tue, 5 May 2026 14:16:11 +0300 Subject: [PATCH] pp: implement verbose serialization of box errors When a test fails with an error, the error is serialized with the `tostring` Lua function, which by default prints only the error message. This means that some useful information, such as the error trace and payload fields, are lost. We could enable verbose error serialization using the `box_error_serialize_verbose` compat module option, but it may break existing tests. Let's instead unpack errors thrown by tests before printing them to make debugging easier. Closes #445 --- CHANGELOG.md | 2 ++ luatest/assertions.lua | 22 ++-------------------- luatest/pp.lua | 3 +++ luatest/utils.lua | 22 ++++++++++++++++++++++ test/pp_test.lua | 8 ++++++++ test/utils_test.lua | 13 +++++++++++++ 6 files changed, 50 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 43261485..e0ab94be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +- Implemented verbose serialization of uncaught box errors thrown in tests + (gh-445). - Fixed a bug when errors reported by the address sanitizer during process termination were ignored (gh-460). diff --git a/luatest/assertions.lua b/luatest/assertions.lua index 66f49d57..08b8ae30 100644 --- a/luatest/assertions.lua +++ b/luatest/assertions.lua @@ -13,7 +13,6 @@ local log = require('luatest.log') local utils = require('luatest.utils') local tarantool = require('tarantool') local fio = require('fio') -local ffi = require('ffi') local prettystr = pp.tostring local prettystr_pairs = pp.tostring_pair @@ -22,8 +21,6 @@ local M = {} local xfail = false -local box_error_type = ffi.typeof(box.error.new(box.error.UNKNOWN)) - -- private exported functions (for testing) M.private = {} @@ -159,7 +156,7 @@ local function pcall_check_trace(level, fn, ...) if ok then return ok, err end - if type(err) ~= 'cdata' or ffi.typeof(err) ~= box_error_type then + if not utils.is_box_error(err) then fail_fmt(level + 1, nil, 'Error raised is not a box.error: %s', prettystr(err)) end @@ -704,21 +701,6 @@ function M.assert_error_msg_matches(pattern, fn, ...) end end --- If it is box.error that unpack it recursively. If it is not then --- return argument unchanged. -local function error_unpack(err) - if type(err) ~= 'cdata' or ffi.typeof(err) ~= box_error_type then - return err - end - local unpacked = err:unpack() - local tmp = unpacked - while tmp.prev ~= nil do - tmp.prev = tmp.prev:unpack() - tmp = tmp.prev - end - return unpacked -end - --- Checks that error raised by function is table that includes expected one. --- box.error is unpacked to convert to table. Stacked errors are supported. --- That is if there is prev field in expected then it should cover prev field @@ -734,7 +716,7 @@ function M.assert_error_covers(expected, fn, ...) 'Function successfully returned: %s\nExpected error: %s', prettystr(actual), prettystr(expected)) end - local unpacked = error_unpack(actual) + local unpacked = utils.error_unpack(actual) if not comparator.equals(table_slice(unpacked, expected), expected) then actual, expected = prettystr_pairs(unpacked, expected) fail_fmt(2, nil, 'Error expected: %s\nError received: %s', diff --git a/luatest/pp.lua b/luatest/pp.lua index eeeb2262..ab5b181f 100644 --- a/luatest/pp.lua +++ b/luatest/pp.lua @@ -1,5 +1,6 @@ local Class = require('luatest.class') local sorted_pairs = require('luatest.sorted_pairs') +local utils = require('luatest.utils') -- Pretty printer. local pp = { @@ -160,6 +161,8 @@ function Formatter.mt:format(v, indentLevel) return tostring(i) end end + elseif utils.is_box_error(v) then + return self:format_table(utils.error_unpack(v), indentLevel) end return tostring(v) diff --git a/luatest/utils.lua b/luatest/utils.lua index b03839e7..7ceb3af6 100644 --- a/luatest/utils.lua +++ b/luatest/utils.lua @@ -1,10 +1,13 @@ local digest = require('digest') +local ffi = require('ffi') local fio = require('fio') local fun = require('fun') local yaml = require('yaml') local utils = {} +local box_error_type = ffi.typeof(box.error.new(box.error.UNKNOWN)) + -- Helper to override methods. -- -- utils.patch(target, 'method_name', function(super) return function(...) @@ -251,4 +254,23 @@ function utils.table_is_array(t) return true end +function utils.is_box_error(err) + return type(err) == 'cdata' and ffi.typeof(err) == box_error_type +end + +-- If it is box.error that unpack it recursively. If it is not then +-- return argument unchanged. +function utils.error_unpack(err) + if not utils.is_box_error(err) then + return err + end + local unpacked = err:unpack() + local tmp = unpacked + while tmp.prev ~= nil do + tmp.prev = tmp.prev:unpack() + tmp = tmp.prev + end + return unpacked +end + return utils diff --git a/test/pp_test.lua b/test/pp_test.lua index e703b6fa..55ec6e5d 100644 --- a/test/pp_test.lua +++ b/test/pp_test.lua @@ -31,3 +31,11 @@ g.test_tostring_huge_table = function() t.assert_equals(result, 0) t.assert_almost_equals(clock.time() - start, 0, 0.5) end + +g.test_tostring_box_error = function() + local s = pp.tostring(box.error.new({ + type = 'MyError', reason = 'FOOBAR', + })) + t.assert_str_contains(s, 'type = "MyError"') + t.assert_str_contains(s, 'message = "FOOBAR"') +end diff --git a/test/utils_test.lua b/test/utils_test.lua index 0c451223..1760a997 100644 --- a/test/utils_test.lua +++ b/test/utils_test.lua @@ -30,3 +30,16 @@ g.test_table_pack = function() t.assert_equals(utils.table_pack(1, 2, nil), {n = 3, 1, 2}) t.assert_equals(utils.table_pack(1, 2, nil, 3), {n = 4, 1, 2, nil, 3}) end + +g.test_box_error = function() + local err = 'FOOBAR' + t.assert_not(utils.is_box_error(err)) + t.assert_equals(utils.error_unpack(err), err) + err = box.error.new({type = 'MyError', reason = 'FOOBAR'}) + err:set_prev(box.error.new({type = 'MyError2', reason = 'FUZZ'})) + t.assert(utils.is_box_error(err)) + t.assert_covers(utils.error_unpack(err), { + type = 'MyError', message = 'FOOBAR', + prev = {type = 'MyError2', message = 'FUZZ'} + }) +end