diff --git a/CHANGELOG.md b/CHANGELOG.md index 6ad0457..c9cab4d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/luatest/output/tap.lua b/luatest/output/tap.lua index 454db40..f210376 100644 --- a/luatest/output/tap.lua +++ b/luatest/output/tap.lua @@ -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 diff --git a/luatest/output/text.lua b/luatest/output/text.lua index 2783de2..29b7c77 100644 --- a/luatest/output/text.lua +++ b/luatest/output/text.lua @@ -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 diff --git a/luatest/runner.lua b/luatest/runner.lua index 82c50ff..8964be1 100644 --- a/luatest/runner.lua +++ b/luatest/runner.lua @@ -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() @@ -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) diff --git a/luatest/test_instance.lua b/luatest/test_instance.lua index a4db250..788af20 100644 --- a/luatest/test_instance.lua +++ b/luatest/test_instance.lua @@ -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) diff --git a/luatest/utils.lua b/luatest/utils.lua index 014179e..bb4cdd6 100644 --- a/luatest/utils.lua +++ b/luatest/utils.lua @@ -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 = {} @@ -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('/')