diff --git a/luatest/output/junit.lua b/luatest/output/junit.lua index a643b7ce..90e26ba5 100644 --- a/luatest/output/junit.lua +++ b/luatest/output/junit.lua @@ -1,3 +1,4 @@ +local utils = require('luatest.utils') local ROCK_VERSION = require('luatest.VERSION') -- See directory junitxml for more information about the junit format @@ -20,17 +21,23 @@ function Output.xml_c_data_escape(str) end function Output.node_status_xml(node) + local artifacts = '' + if utils.table_len(node.servers) > 0 then + for _, server in pairs(node.servers) do + artifacts = ('%s%s -> %s'):format(artifacts, server.alias, server.artifacts) + end + end if node:is('error') then return table.concat( {' \n', ' \n', - ' ', Output.xml_escape(node.artifacts or ''), '\n', + ' ', Output.xml_escape(artifacts), '\n', ' \n'}) elseif node:is('fail') then return table.concat( {' \n', ' \n', - ' ', Output.xml_escape(node.artifacts or ''), '\n', + ' ', Output.xml_escape(artifacts), '\n', ' \n'}) elseif node:is('skip') then return table.concat({' ', Output.xml_escape(node.message or ''),'\n'}) diff --git a/luatest/output/tap.lua b/luatest/output/tap.lua index 1292f650..d28782c6 100644 --- a/luatest/output/tap.lua +++ b/luatest/output/tap.lua @@ -1,3 +1,4 @@ +local utils = require('luatest.utils') -- For a good reference for TAP format, check: http://testanything.org/tap-specification.html local Output = require('luatest.output.generic'):new_class() @@ -28,8 +29,11 @@ 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.artifacts then - print(prefix .. node.artifacts:gsub('\n', '\n' .. prefix)) + if utils.table_len(node.servers) > 0 then + print(prefix .. 'artifacts:') + for _, server in pairs(node.servers) do + print(prefix .. server.alias .. server.artifacts) + end end end end diff --git a/luatest/output/text.lua b/luatest/output/text.lua index bf405969..2783de20 100644 --- a/luatest/output/text.lua +++ b/luatest/output/text.lua @@ -1,3 +1,4 @@ +local utils = require('luatest.utils') local Output = require('luatest.output.generic'):new_class() Output.BOLD_CODE = '\x1B[1m' @@ -60,10 +61,12 @@ 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.artifacts then - print(fail.artifacts) + if utils.table_len(fail.servers) > 0 then + print('artifacts:') + for _, server in pairs(fail.servers) do + print(('\t%s -> %s'):format(server.alias, server.artifacts)) + end end - print() end function Output.mt:display_errored_tests() diff --git a/luatest/runner.lua b/luatest/runner.lua index e141869d..41244d06 100644 --- a/luatest/runner.lua +++ b/luatest/runner.lua @@ -413,10 +413,10 @@ end function Runner.mt:run_tests(tests_list) -- Make seed for ordering not affect other random numbers. math.randomseed(os.time()) + rawset(_G, 'current_test', {value = nil}) for _ = 1, self.exe_repeat_group or 1 do local last_group for _, test in ipairs(tests_list) do - rawset(_G, 'current_test', test) if last_group ~= test.group then if last_group then self:end_group(last_group) @@ -424,6 +424,7 @@ function Runner.mt:run_tests(tests_list) self:start_group(test.group) last_group = test.group end + rawget(_G, 'current_test').value = test self:run_test(test) if self.result.aborted then break @@ -444,6 +445,13 @@ end function Runner.mt:invoke_test_function(test) local err = self:protected_call(test.group, test.method, test.name) self:update_status(test, err) + if not test:is('success') then + if utils.table_len(test.servers) > 0 then + for _, server in pairs(test.servers) do + server:save_artifacts() + end + end + end end function Runner.mt:find_test(groups, name) diff --git a/luatest/server.lua b/luatest/server.lua index a08faa7b..ada8988a 100644 --- a/luatest/server.lua +++ b/luatest/server.lua @@ -100,6 +100,30 @@ function Server:new(object, extra) object = utils.merge(object, extra) self:inherit(object) object:initialize() + + -- Each method of the server instance will be overridden by a new function + -- in which the association of the current test and server is performed first + -- and then the method itself. + -- It solves the problem when the server is not used in the test (should not + -- save artifacts) and when used. + for k, v in pairs(self) do + if type(v) == 'function' then + object[k] = function(...) + local t = rawget(_G, 'current_test') + if t == nil then + return v(...) + elseif t.value == nil then + return v(...) + end + t = t.value + if object.tests[t.name] == nil then + object.tests[t.name] = t + t.servers[object.id] = object + end + return v(...) + end + end + end return object end @@ -186,14 +210,12 @@ function Server:initialize() self.env.LUATEST_LUACOV_ROOT = os.getenv('LUATEST_LUACOV_ROOT') end - if self.current_test == nil then - self.current_test = rawget(_G, 'current_test') - if self.current_test then - local prefix = fio.pathjoin(Server.vardir, 'artifacts', self.rs_id or '') - self.artifacts = fio.pathjoin(prefix, self.id) - self.current_test:add_server_artifacts(self.alias, self.artifacts) - end + if self.tests == nil then + self.tests = {} end + + local prefix = fio.pathjoin(Server.vardir, 'artifacts', self.rs_id or '') + self.artifacts = fio.pathjoin(prefix, self.id) end -- Create a table with env variables based on the constructor params. @@ -328,12 +350,22 @@ function Server:restart(params, opts) log.debug('Restarted server PID: ' .. self.process.pid) end +-- Save server artifacts by copying the working directory +-- Throws an error when the copying is not successful. +function Server:save_artifacts() + local ok, err = fio.copytree(self.workdir, self.artifacts) + if not ok then + error(('Failed to copy server (%s) artifacts: %s'):format(self.alias, err)) + end +end + -- Wait until the given condition is `true` (anything except `false` and `nil`). -- Throws an error when the server process is terminated or timeout exceeds. local function wait_for_condition(cond_desc, server, func, ...) local deadline = clock.time() + WAIT_TIMEOUT while true do if not server.process:is_alive() then + server:save_artifacts() error(('Process is terminated when waiting for "%s" condition for server (alias: %s, workdir: %s, pid: %d)') :format(cond_desc, server.alias, fio.basename(server.workdir), server.process.pid)) end @@ -341,6 +373,7 @@ local function wait_for_condition(cond_desc, server, func, ...) return end if clock.time() > deadline then + server:save_artifacts() error(('Timed out to wait for "%s" condition for server (alias: %s, workdir: %s, pid: %d) within %ds') :format(cond_desc, server.alias, fio.basename(server.workdir), server.process.pid, WAIT_TIMEOUT)) end @@ -386,13 +419,7 @@ end --- Stop the server and clean its working directory. function Server:drop() self:stop() - - if self.current_test and not self.current_test:is('success') then - local ok, err = fio.copytree(self.workdir, self.artifacts) - if not ok then - error(('Failed to copy server artifacts: %s'):format(err)) - end - end + self:save_artifacts() fio.rmtree(self.workdir) diff --git a/luatest/test_instance.lua b/luatest/test_instance.lua index 549a1026..a4db250e 100644 --- a/luatest/test_instance.lua +++ b/luatest/test_instance.lua @@ -16,7 +16,7 @@ end -- default constructor, test are PASS by default function TestInstance.mt:initialize() self.status = 'success' - self.artifacts = nil + self.servers = {} end function TestInstance.mt:update_status(status, message, trace) @@ -25,14 +25,6 @@ function TestInstance.mt:update_status(status, message, trace) self.trace = trace end -function TestInstance.mt:add_server_artifacts(alias, workdir) - local server_workdir = string.format('\n\t%s -> %s', alias, workdir) - if not self.artifacts then - self.artifacts = 'artifacts:' - end - self.artifacts = self.artifacts .. server_workdir -end - function TestInstance.mt:is(status) return self.status == status end diff --git a/test/artifacts_test.lua b/test/artifacts_test.lua new file mode 100644 index 00000000..b8ccded3 --- /dev/null +++ b/test/artifacts_test.lua @@ -0,0 +1,71 @@ +local t = require('luatest') +local utils = require('luatest.utils') +local fio = require('fio') + +local g = t.group() +local Server = t.Server + +local function is_server_in_test(server, test) + for _, s in pairs(test.servers) do + if server.id == s.id then + return true + end + end + return false +end + +g.public = Server:new({alias = 'public'}) +g.public:start() + +g.before_all(function() + g.all = Server:new({alias = 'all'}) + g.all:start() +end) + +g.before_each(function() + g.each = Server:new({alias = 'each'}) + g.each:start() +end) + +g.before_test('test_association_between_test_and_servers', function() + g.test = Server:new({alias = 'test'}) + g.test:start() +end) + + +g.test_association_between_test_and_servers = function() + g.internal = Server:new({alias = 'internal'}) + g.internal:start() + + local test = rawget(_G, 'current_test').value + + -- test static association + t.assert(is_server_in_test(g.internal, test)) + t.assert(is_server_in_test(g.each, test)) + t.assert(is_server_in_test(g.test, test)) + t.assert_not(is_server_in_test(g.public, test)) + + g.public:exec(function() return 1 + 1 end) + g.all:exec(function() return 1 + 1 end) + + -- test dynamic association + t.assert(is_server_in_test(g.public, test)) + t.assert(is_server_in_test(g.all, test)) + + t.assert(utils.table_len(test.servers) == 5) +end + +g.after_test('test_association_between_test_and_servers', function() + g.test:drop() + t.assert(fio.path.exists(g.test.artifacts)) +end) + +g.after_each(function() + g.each:drop() + t.assert(fio.path.exists(g.each.artifacts)) +end) + +g.after_all(function() + g.all:drop() + t.assert(fio.path.exists(g.all.artifacts)) +end)