Skip to content

Commit 251b35f

Browse files
Oleg Chaplashkinylobankov
Oleg Chaplashkin
authored andcommitted
Fix saving artifacts
> What's the problem? When creating the logic for saving artifacts [1] a global variable has been created as `current_test` in the `runner.lua` module. Luatest has collected all the tests and runs a simple loop. At the beginning of each iteration the current test is written to the `current_test'. Server saved the value of the current test. However at some point the current test in the global variable and in the server object were different (the server stored information about the previous test). Also server does not store information about which test it was used in. > What is the solution? Redesign the logic of working with artifacts: now each test knows which server is linked to it, and each server knows about the test. For example: foo = Server:new() foo:start() -- only `foo` artifacts g.test_with_foo = function() foo:exec(...) end -- no server artifacts g.test_without_servers = function() ... end [1] #296 Helped-by: Igor Munkin <[email protected]> Close #299
1 parent 3fa719d commit 251b35f

12 files changed

+344
-31
lines changed

luatest/output/junit.lua

+9-2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
local utils = require('luatest.utils')
12
local ROCK_VERSION = require('luatest.VERSION')
23

34
-- See directory junitxml for more information about the junit format
@@ -20,17 +21,23 @@ function Output.xml_c_data_escape(str)
2021
end
2122

2223
function Output.node_status_xml(node)
24+
local artifacts = ''
25+
if utils.table_len(node.servers) > 0 then
26+
for _, server in pairs(node.servers) do
27+
artifacts = ('%s%s -> %s'):format(artifacts, server.alias, server.artifacts)
28+
end
29+
end
2330
if node:is('error') then
2431
return table.concat(
2532
{' <error type="', Output.xml_escape(node.message), '">\n',
2633
' <![CDATA[', Output.xml_c_data_escape(node.trace), ']]>\n',
27-
' <artifacts>', Output.xml_escape(node.artifacts or ''), '</artifacts>\n',
34+
' <artifacts>', Output.xml_escape(artifacts), '</artifacts>\n',
2835
' </error>\n'})
2936
elseif node:is('fail') then
3037
return table.concat(
3138
{' <failure type="', Output.xml_escape(node.message), '">\n',
3239
' <![CDATA[', Output.xml_c_data_escape(node.trace), ']]>\n',
33-
' <artifacts>', Output.xml_escape(node.artifacts or ''), '</artifacts>\n',
40+
' <artifacts>', Output.xml_escape(artifacts), '</artifacts>\n',
3441
' </failure>\n'})
3542
elseif node:is('skip') then
3643
return table.concat({' <skipped>', Output.xml_escape(node.message or ''),'</skipped>\n'})

luatest/output/tap.lua

+6-2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
local utils = require('luatest.utils')
12
-- For a good reference for TAP format, check: http://testanything.org/tap-specification.html
23
local Output = require('luatest.output.generic'):new_class()
34

@@ -28,8 +29,11 @@ function Output.mt:update_status(node)
2829
end
2930
if (node:is('fail') or node:is('error')) and self.verbosity >= self.class.VERBOSITY.VERBOSE then
3031
print(prefix .. node.trace:gsub('\n', '\n' .. prefix))
31-
if node.artifacts then
32-
print(prefix .. node.artifacts:gsub('\n', '\n' .. prefix))
32+
if utils.table_len(node.servers) > 0 then
33+
print(prefix .. 'artifacts:')
34+
for _, server in pairs(node.servers) do
35+
print(('%s%s -> %s'):format(prefix, server.alias, server.artifacts))
36+
end
3337
end
3438
end
3539
end

luatest/output/text.lua

+6-3
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
local utils = require('luatest.utils')
12
local Output = require('luatest.output.generic'):new_class()
23

34
Output.BOLD_CODE = '\x1B[1m'
@@ -60,10 +61,12 @@ function Output.mt:display_one_failed_test(index, fail) -- luacheck: no unused
6061
print(index..") " .. fail.name .. self.class.ERROR_COLOR_CODE)
6162
print(fail.message .. self.class.RESET_TERM)
6263
print(fail.trace)
63-
if fail.artifacts then
64-
print(fail.artifacts)
64+
if utils.table_len(fail.servers) > 0 then
65+
print('artifacts:')
66+
for _, server in pairs(fail.servers) do
67+
print(('\t%s -> %s'):format(server.alias, server.artifacts))
68+
end
6569
end
66-
print()
6770
end
6871

6972
function Output.mt:display_errored_tests()

luatest/runner.lua

+10-1
Original file line numberDiff line numberDiff line change
@@ -413,17 +413,19 @@ end
413413
function Runner.mt:run_tests(tests_list)
414414
-- Make seed for ordering not affect other random numbers.
415415
math.randomseed(os.time())
416+
rawset(_G, 'current_test', {value = nil})
416417
for _ = 1, self.exe_repeat_group or 1 do
417418
local last_group
418419
for _, test in ipairs(tests_list) do
419-
rawset(_G, 'current_test', test)
420420
if last_group ~= test.group then
421421
if last_group then
422+
rawget(_G, 'current_test').value = nil
422423
self:end_group(last_group)
423424
end
424425
self:start_group(test.group)
425426
last_group = test.group
426427
end
428+
rawget(_G, 'current_test').value = test
427429
self:run_test(test)
428430
if self.result.aborted then
429431
break
@@ -444,6 +446,13 @@ end
444446
function Runner.mt:invoke_test_function(test)
445447
local err = self:protected_call(test.group, test.method, test.name)
446448
self:update_status(test, err)
449+
if not test:is('success') then
450+
if utils.table_len(test.servers) > 0 then
451+
for _, server in pairs(test.servers) do
452+
server:save_artifacts()
453+
end
454+
end
455+
end
447456
end
448457

449458
function Runner.mt:find_test(groups, name)

luatest/server.lua

+39-14
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,27 @@ function Server:new(object, extra)
101101
object = utils.merge(object, extra)
102102
self:inherit(object)
103103
object:initialize()
104+
105+
-- Each method of the server instance will be overridden by a new function
106+
-- in which the association of the current test and server is performed first
107+
-- and then the method itself.
108+
-- It solves the problem when the server is not used in the test (should not
109+
-- save artifacts) and when used.
110+
for k, v in pairs(self) do
111+
if type(v) == 'function' then
112+
object[k] = function(...)
113+
local t = rawget(_G, 'current_test')
114+
if t and t.value then
115+
t = t.value
116+
if not object.tests[t.name] then
117+
object.tests[t.name] = t
118+
t.servers[object.id] = object
119+
end
120+
end
121+
return v(...)
122+
end
123+
end
124+
end
104125
return object
105126
end
106127

@@ -187,14 +208,12 @@ function Server:initialize()
187208
self.env.LUATEST_LUACOV_ROOT = os.getenv('LUATEST_LUACOV_ROOT')
188209
end
189210

190-
if self.current_test == nil then
191-
self.current_test = rawget(_G, 'current_test')
192-
if self.current_test then
193-
local prefix = fio.pathjoin(Server.vardir, 'artifacts', self.rs_id or '')
194-
self.artifacts = fio.pathjoin(prefix, self.id)
195-
self.current_test:add_server_artifacts(self.alias, self.artifacts)
196-
end
211+
if not self.tests then
212+
self.tests = {}
197213
end
214+
215+
local prefix = fio.pathjoin(Server.vardir, 'artifacts', self.rs_id or '')
216+
self.artifacts = fio.pathjoin(prefix, self.id)
198217
end
199218

200219
-- Create a table with env variables based on the constructor params.
@@ -329,19 +348,31 @@ function Server:restart(params, opts)
329348
log.debug('Restarted server PID: ' .. self.process.pid)
330349
end
331350

351+
-- Save server artifacts by copying the working directory.
352+
-- Throws an error when the copying is not successful.
353+
function Server:save_artifacts()
354+
local ok, err = fio.copytree(self.workdir, self.artifacts)
355+
if not ok then
356+
error(('Failed to copy artifacts for server (alias: %s, workdir: %s, pid: %d): %s')
357+
:format(self.alias, fio.basename(self.workdir), self.process.pid, err))
358+
end
359+
end
360+
332361
-- Wait until the given condition is `true` (anything except `false` and `nil`).
333362
-- Throws an error when the server process is terminated or timeout exceeds.
334363
local function wait_for_condition(cond_desc, server, func, ...)
335364
local deadline = clock.time() + WAIT_TIMEOUT
336365
while true do
337366
if not server.process:is_alive() then
367+
server:save_artifacts()
338368
error(('Process is terminated when waiting for "%s" condition for server (alias: %s, workdir: %s, pid: %d)')
339369
:format(cond_desc, server.alias, fio.basename(server.workdir), server.process.pid))
340370
end
341371
if func(...) then
342372
return
343373
end
344374
if clock.time() > deadline then
375+
server:save_artifacts()
345376
error(('Timed out to wait for "%s" condition for server (alias: %s, workdir: %s, pid: %d) within %ds')
346377
:format(cond_desc, server.alias, fio.basename(server.workdir), server.process.pid, WAIT_TIMEOUT))
347378
end
@@ -387,13 +418,7 @@ end
387418
--- Stop the server and clean its working directory.
388419
function Server:drop()
389420
self:stop()
390-
391-
if self.current_test and not self.current_test:is('success') then
392-
local ok, err = fio.copytree(self.workdir, self.artifacts)
393-
if not ok then
394-
error(('Failed to copy server artifacts: %s'):format(err))
395-
end
396-
end
421+
self:save_artifacts()
397422

398423
fio.rmtree(self.workdir)
399424

luatest/test_instance.lua

+1-9
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ end
1616
-- default constructor, test are PASS by default
1717
function TestInstance.mt:initialize()
1818
self.status = 'success'
19-
self.artifacts = nil
19+
self.servers = {}
2020
end
2121

2222
function TestInstance.mt:update_status(status, message, trace)
@@ -25,14 +25,6 @@ function TestInstance.mt:update_status(status, message, trace)
2525
self.trace = trace
2626
end
2727

28-
function TestInstance.mt:add_server_artifacts(alias, workdir)
29-
local server_workdir = string.format('\n\t%s -> %s', alias, workdir)
30-
if not self.artifacts then
31-
self.artifacts = 'artifacts:'
32-
end
33-
self.artifacts = self.artifacts .. server_workdir
34-
end
35-
3628
function TestInstance.mt:is(status)
3729
return self.status == status
3830
end

test/artifacts/common_test.lua

+63
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
local t = require('luatest')
2+
local utils = require('luatest.utils')
3+
4+
local g = t.group()
5+
local Server = t.Server
6+
7+
g.public = Server:new({ alias = 'public'})
8+
g.public:start()
9+
10+
g.test_servers_not_added_if_they_are_not_used = function()
11+
end
12+
13+
g.after_test('test_servers_not_added_if_they_are_not_used', function()
14+
t.fail_if(
15+
utils.table_len(rawget(_G, 'current_test').value.servers) ~= 0,
16+
'Test instance should not contain a servers')
17+
end)
18+
19+
g.test_only_public_server_has_been_added = function()
20+
g.public:get_vclock()
21+
end
22+
23+
g.after_test('test_only_public_server_has_been_added', function()
24+
t.fail_if(
25+
rawget(_G, 'current_test').value.servers[g.public.id] == nil,
26+
'Test should contain only public server')
27+
end)
28+
29+
30+
g.test_only_private_server_has_been_added = function()
31+
g.private = Server:new({alias = 'private'})
32+
g.private:start()
33+
end
34+
35+
g.after_test('test_only_private_server_has_been_added', function()
36+
t.fail_if(
37+
rawget(_G, 'current_test').value.servers[g.private.id] == nil,
38+
'Test should contain only private server')
39+
40+
end)
41+
42+
g.before_test('test_add_server_from_test_hooks', function()
43+
g.before = Server:new({ alias = 'before' })
44+
g.before:start()
45+
end)
46+
47+
g.test_add_server_from_test_hooks = function()
48+
end
49+
50+
g.after_test('test_add_server_from_test_hooks', function()
51+
g.after = Server:new({ alias = 'after' })
52+
g.after:start()
53+
54+
local test_servers = rawget(_G, 'current_test').value.servers
55+
56+
t.fail_if(
57+
utils.table_len(test_servers) ~= 2,
58+
'Test should contain two servers (from before/after hooks)')
59+
t.fail_if(
60+
test_servers[g.before.id] == nil or
61+
test_servers[g.after.id] == nil,
62+
'Test should contain only `before` and `after` servers')
63+
end)

test/artifacts/end_group_test.lua

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
local t = require('luatest')
2+
local utils = require('luatest.utils')
3+
4+
local Server = t.Server
5+
6+
local g = t.group()
7+
8+
g.test_foo = function()
9+
g.foo_test = rawget(_G, 'current_test').value
10+
end
11+
12+
g.test_bar = function()
13+
g.bar_test = rawget(_G, 'current_test').value
14+
end
15+
16+
g.after_all(function()
17+
g.s = Server:new()
18+
g.s:start()
19+
20+
t.fail_if(
21+
utils.table_len(g.foo_test.servers) ~= 0,
22+
'Test instance `foo` should not contain a servers')
23+
24+
t.fail_if(
25+
utils.table_len(g.bar_test.servers) ~= 0,
26+
'Test instance `bar` should not contain a servers')
27+
end)

test/artifacts/hooks_test.lua

+71
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
local t = require('luatest')
2+
local utils = require('luatest.utils')
3+
local fio = require('fio')
4+
5+
local g = t.group()
6+
local Server = t.Server
7+
8+
local function is_server_in_test(server, test)
9+
for _, s in pairs(test.servers) do
10+
if server.id == s.id then
11+
return true
12+
end
13+
end
14+
return false
15+
end
16+
17+
g.public = Server:new({alias = 'public'})
18+
g.public:start()
19+
20+
g.before_all(function()
21+
g.all = Server:new({alias = 'all9'})
22+
g.all:start()
23+
end)
24+
25+
g.before_each(function()
26+
g.each = Server:new({alias = 'each'})
27+
g.each:start()
28+
end)
29+
30+
g.before_test('test_association_between_test_and_servers', function()
31+
g.test = Server:new({alias = 'test'})
32+
g.test:start()
33+
end)
34+
35+
36+
g.test_association_between_test_and_servers = function()
37+
g.internal = Server:new({alias = 'internal'})
38+
g.internal:start()
39+
40+
local test = rawget(_G, 'current_test').value
41+
42+
-- test static association
43+
t.assert(is_server_in_test(g.internal, test))
44+
t.assert(is_server_in_test(g.each, test))
45+
t.assert(is_server_in_test(g.test, test))
46+
t.assert_not(is_server_in_test(g.public, test))
47+
48+
g.public:exec(function() return 1 + 1 end)
49+
g.all:exec(function() return 1 + 1 end)
50+
51+
-- test dynamic association
52+
t.assert(is_server_in_test(g.public, test))
53+
t.assert(is_server_in_test(g.all, test))
54+
55+
t.assert(utils.table_len(test.servers) == 5)
56+
end
57+
58+
g.after_test('test_association_between_test_and_servers', function()
59+
g.test:drop()
60+
t.assert(fio.path.exists(g.test.artifacts))
61+
end)
62+
63+
g.after_each(function()
64+
g.each:drop()
65+
t.assert(fio.path.exists(g.each.artifacts))
66+
end)
67+
68+
g.after_all(function()
69+
g.all:drop()
70+
t.assert(fio.path.exists(g.all.artifacts))
71+
end)

0 commit comments

Comments
 (0)