diff --git a/CHANGELOG.md b/CHANGELOG.md index a778e36..9f7c6af 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ - Make `assert_error_*` additionally check error trace if required. - Add `--list-test-cases` and `--run-test-case` CLI options. - Introduce preloaded hooks (gh-380). +- Add `treegen` helper as a tree generator (gh-364). ## 1.0.1 diff --git a/config.ld b/config.ld index e90fa30..c4c7a35 100644 --- a/config.ld +++ b/config.ld @@ -8,7 +8,8 @@ file = { 'luatest/server.lua', 'luatest/replica_set.lua', 'luatest/justrun.lua', - 'luatest/cbuilder.lua' + 'luatest/cbuilder.lua', + 'luatest/treegen.lua' } topics = { 'CHANGELOG.md', diff --git a/luatest/treegen.lua b/luatest/treegen.lua new file mode 100644 index 0000000..0bde87e --- /dev/null +++ b/luatest/treegen.lua @@ -0,0 +1,215 @@ +--- Working tree generator. +-- +-- Generates a tree of Lua files using provided templates and +-- filenames. +-- +-- @usage +-- +-- local t = require('luatest') +-- local treegen = require('test.treegen') +-- +-- local g = t.group() +-- +-- local SCRIPT_TEMPLATE = [[ +-- <...> +-- ]] +-- +-- g.foobar_test = function(g) +-- local dir = treegen.prepare_directory(g, +-- {'foo/bar.lua', 'main.lua'}) +-- <..test case..> +-- end +-- +-- @module luatest.treegen + +local hooks = require('luatest.hooks') + +local fio = require('fio') +local fun = require('fun') +local checks = require('checks') + +local log = require('luatest.log') + +local treegen = {} + +local function find_template(group, script) + for position, template_def in ipairs(group._treegen.templates) do + if script:match(template_def.pattern) then + return position, template_def.template + end + end + error(("treegen: can't find a template for script %q"):format(script)) +end + +--- Write provided content into the given directory. +-- +-- @string directory Directory where the content will be created. +-- @string filename File to write (possible nested path: /foo/bar/main.lua). +-- @string content The body to write. +-- @return string +function treegen.write_file(directory, filename, content) + checks('string', 'string', 'string') + local content_abspath = fio.pathjoin(directory, filename) + local flags = {'O_CREAT', 'O_WRONLY', 'O_TRUNC'} + local mode = tonumber('644', 8) + + local contentdir_abspath = fio.dirname(content_abspath) + log.info('Creating a directory: %s', contentdir_abspath) + fio.mktree(contentdir_abspath) + + log.info('Writing a content: %s', content_abspath) + local fh = fio.open(content_abspath, flags, mode) + fh:write(content) + fh:close() + return content_abspath +end + +-- Generate a content that follows a template and write it at the +-- given path in the given directory. +-- +-- @table group Group of tests. +-- @string directory Directory where the content will be created. +-- @string filename File to write (possible nested path: /foo/bar/main.lua). +-- @table replacements List of replacement templates. +-- @return string +local function gen_content(group, directory, filename, replacements) + checks('table', 'string', 'string', 'table') + local _, template = find_template(group, filename) + replacements = fun.chain({filename = filename}, replacements):tomap() + local body = template:gsub('<(.-)>', replacements) + return treegen.write_file(directory, filename, body) +end + +--- Initialize treegen module in the given group of tests. +-- +-- @tab group Group of tests. +local function init(group) + checks('table') + group._treegen = { + tempdirs = {}, + templates = {} + } +end + +--- Remove all temporary directories created by the test +-- unless KEEP_DATA environment variable is set to a +-- non-empty value. +-- +-- @tab group Group of tests. +local function clean(group) + checks('table') + local dirs = table.copy(group._treegen.tempdirs) or {} + group._treegen.tempdirs = nil + + local keep_data = (os.getenv('KEEP_DATA') or '') ~= '' + + for _, dir in ipairs(dirs) do + if keep_data then + log.info('Left intact due to KEEP_DATA env var: %s', dir) + else + log.info('Recursively removing: %s', dir) + fio.rmtree(dir) + end + end + + group._treegen.templates = nil +end + +--- Save the template with the given pattern. +-- +-- @tab group Group of tests. +-- @string pattern File name template +-- @string template A content template for creating a file. +function treegen.add_template(group, pattern, template) + checks('table', 'string', 'string') + table.insert(group._treegen.templates, { + pattern = pattern, + template = template, + }) +end + +--- Remove the template by pattern. +-- +-- @tab group Group of tests. +-- @string pattern File name template +function treegen.remove_template(group, pattern) + checks('table', 'string') + local is_found, position, _ = pcall(find_template, group, pattern) + if is_found then + table.remove(group._treegen.templates, position) + end +end + +--- Create a temporary directory with given contents. +-- +-- The contents are generated using templates added by +-- treegen.add_template(). +-- +-- @usage +-- +-- Example for {'foo/bar.lua', 'baz.lua'}: +-- +-- / +-- + tmp/ +-- + rfbWOJ/ +-- + foo/ +-- | + bar.lua +-- + baz.lua +-- +-- The return value is '/tmp/rfbWOJ' for this example. +-- +-- @tab group Group of tests. +-- @tab contents List of bodies of the content to write. +-- @tab[opt] replacements List of replacement templates. +-- @return string +function treegen.prepare_directory(group, contents, replacements) + checks('table', '?table', '?table') + replacements = replacements or {} + + local dir = fio.tempdir() + + -- fio.tempdir() follows the TMPDIR environment variable. + -- If it ends with a slash, the return value contains a double + -- slash in the middle: for example, if TMPDIR=/tmp/, the + -- result is like `/tmp//rfbWOJ`. + -- + -- It looks harmless on the first glance, but this directory + -- path may be used later to form an URI for a Unix domain + -- socket. As result the URI looks like + -- `unix/:/tmp//rfbWOJ/instance-001.iproto`. + -- + -- It confuses net_box.connect(): it reports EAI_NONAME error + -- from getaddrinfo(). + -- + -- It seems, the reason is a peculiar of the URI parsing: + -- + -- tarantool> uri.parse('unix/:/foo/bar.iproto') + -- --- + -- - host: unix/ + -- service: /foo/bar.iproto + -- unix: /foo/bar.iproto + -- ... + -- + -- tarantool> uri.parse('unix/:/foo//bar.iproto') + -- --- + -- - host: unix + -- path: /foo//bar.iproto + -- ... + -- + -- Let's normalize the path using fio.abspath(), which + -- eliminates the double slashes. + dir = fio.abspath(dir) + + table.insert(group._treegen.tempdirs, dir) + + for _, content in ipairs(contents) do + gen_content(group, dir, content, replacements) + end + + return dir +end + +hooks.before_all_preloaded(init) +hooks.after_all_preloaded(clean) + +return treegen diff --git a/test/treegen_test.lua b/test/treegen_test.lua new file mode 100644 index 0000000..21661c3 --- /dev/null +++ b/test/treegen_test.lua @@ -0,0 +1,48 @@ +local t = require('luatest') +local fio = require('fio') + +local treegen = require('luatest.treegen') + +local g = t.group() + + +local function assert_file_content_equals(file, expected) + local fh = fio.open(file) + t.assert_equals(fh:read(), expected) +end + +g.test_prepare_directory = function() + treegen.add_template(g, '^.*$', 'test_script') + local dir = treegen.prepare_directory(g, {'foo/bar.lua', 'baz.lua'}) + + t.assert(fio.path.is_dir(dir)) + t.assert(fio.path.exists(dir)) + + t.assert(fio.path.exists(fio.pathjoin(dir, 'foo', 'bar.lua'))) + t.assert(fio.path.exists(fio.pathjoin(dir, 'baz.lua'))) + + assert_file_content_equals(fio.pathjoin(dir, 'foo', 'bar.lua'), 'test_script') + assert_file_content_equals(fio.pathjoin(dir, 'baz.lua'), 'test_script') +end + +g.before_test('test_clean_keep_data', function() + treegen.add_template(g, '^.*$', 'test_script') + + os.setenv('KEEP_DATA', 'true') + + g.dir = treegen.prepare_directory(g, {'foo.lua'}) + + t.assert(fio.path.is_dir(g.dir)) + t.assert(fio.path.exists(g.dir)) +end) + +g.test_clean_keep_data = function() + t.assert(fio.path.is_dir(g.dir)) + t.assert(fio.path.exists(g.dir)) +end + +g.after_test('test_clean_keep_data', function() + os.setenv('KEEP_DATA', '') + t.assert(fio.path.is_dir(g.dir)) + t.assert(fio.path.exists(g.dir)) +end)