Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Changelog

## Unreleased

- Show local variables of a failed test (gh-430).

## 1.2.1

- Fixed a bug when `Server:grep_log()` didn't consider the `reset` option.
Expand Down
4 changes: 4 additions & 0 deletions luatest/output/tap.lua
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ function Output.mt:update_status(node)
end
if (node:is('fail') or node:is('error')) and self.verbosity >= self.class.VERBOSITY.VERBOSE then
print(prefix .. node.trace:gsub('\n', '\n' .. prefix))
if node.locals ~= nil then
print(prefix .. 'locals:')
print(prefix .. node.locals:gsub('\n', '\n' .. prefix))
end
if utils.table_len(node.servers) > 0 then
print(prefix .. 'artifacts:')
for _, server in pairs(node.servers) do
Expand Down
8 changes: 8 additions & 0 deletions luatest/output/text.lua
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,14 @@ function Output.mt:display_one_failed_test(index, fail) -- luacheck: no unused
print(index..") " .. fail.name .. self.class.ERROR_COLOR_CODE)
print(fail.message .. self.class.RESET_TERM)
print(fail.trace)

if fail.locals ~= nil then
print(self.class.WARN_COLOR_CODE)
print('locals:')
print(fail.locals)
print(self.class.RESET_TERM)
end

if utils.table_len(fail.servers) > 0 then
print('artifacts:')
for _, server in pairs(fail.servers) do
Expand Down
7 changes: 4 additions & 3 deletions luatest/runner.lua
Original file line number Diff line number Diff line change
Expand Up @@ -376,7 +376,7 @@ function Runner.mt:update_status(node, err)
return
elseif err.status == 'fail' or err.status == 'error' or err.status == 'skip'
or err.status == 'xfail' or err.status == 'xsuccess' then
node:update_status(err.status, err.message, err.trace)
node:update_status(err.status, err.message, err.trace, err.locals)
if utils.table_len(node.servers) > 0 then
for _, server in pairs(node.servers) do
server:save_artifacts()
Expand Down Expand Up @@ -434,10 +434,11 @@ function Runner.mt:protected_call(instance, method, pretty_name)
trace:sub(string.len('stack traceback:\n') + 1)
e = e.error
end
local locals = utils.locals()
if utils.is_luatest_error(e) then
return {status = e.status, message = e.message, trace = trace}
return {status = e.status, message = e.message, trace = trace, locals = locals}
else
return {status = 'error', message = e, trace = trace}
return {status = 'error', message = e, trace = trace, locals = locals}
end
end)

Expand Down
3 changes: 2 additions & 1 deletion luatest/test_instance.lua
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,11 @@ function TestInstance.mt:initialize()
self.servers = {}
end

function TestInstance.mt:update_status(status, message, trace)
function TestInstance.mt:update_status(status, message, trace, locals)
self.status = status
self.message = message
self.trace = trace
self.locals = locals
end

function TestInstance.mt:is(status)
Expand Down
67 changes: 66 additions & 1 deletion luatest/utils.lua
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
local digest = require('digest')
local fio = require('fio')
local fun = require('fun')
local yaml = require('yaml')
local yaml = require('yaml').new()

-- yaml.encode() fails on a function value otherwise.
yaml.cfg({encode_use_tostring = true})

local utils = {}

Expand Down Expand Up @@ -168,6 +171,68 @@ function utils.upvalues(fn)
return ret
end

-- Get local variables from a first call frame outside luatest.
--
-- Returns nil if nothing found due to any reason.
function utils.locals()
-- Determine a first frame with the user code (outside of
-- luatest).
local level = 3
while true do
local info = debug.getinfo(level, 'S')

-- If nothing found, exit earlier.
if info == nil then
return nil
end

-- Stop on first non-luatest frame.
if type(info.source) == 'string' and
info.what ~= 'C' and
not is_luatest_internal_line(info.source) then
break
end

level = level + 1
end

-- Don't try to show more then 100 variables.
local LIMIT = 100

local res = setmetatable({}, {__serialize = 'mapping'})
for i = 1, LIMIT do
local name, value = debug.getlocal(level, i)

-- Stop if there are no more local variables.
if name == nil then
break
end

-- > Variable names starting with '(' (open parentheses)
-- > represent internal variables (loop control variables,
-- > temporaries, and C function locals).
--
-- https://www.lua.org/manual/5.1/manual.html#pdf-debug.getlocal
if not name:startswith('(') then
res[name] = value
end
end

-- If no local variables found, return just nil to don't
-- show a garbage output like the following.
--
-- | locals:
-- | --- {}
-- | ...
if next(res) == nil then
return nil
end

-- Encode right here to hold the state of the locals and
-- ignore all the future changes.
return yaml.encode(res):rstrip()
end

function utils.get_fn_location(fn)
local fn_details = debug.getinfo(fn)
local fn_source = fn_details.source:split('/')
Expand Down
Loading