├── tmp └── .keep ├── debian ├── compat ├── docs ├── source │ └── format ├── prebuild.sh ├── rules ├── control ├── copyright └── changelog ├── luatest ├── VERSION.lua ├── VERSION.lua.in ├── output │ ├── nil.lua │ ├── tap.lua │ ├── generic.lua │ ├── junit.lua │ └── text.lua ├── coverage.lua ├── cli_entrypoint.lua ├── monitor.lua ├── test_instance.lua ├── http_response.lua ├── sandboxed_runner.lua ├── class.lua ├── server_instance.lua ├── loader.lua ├── ffi_io.lua ├── coverage_utils.lua ├── tarantool.lua ├── group.lua ├── parametrizer.lua ├── init.lua ├── sorted_pairs.lua ├── log.lua ├── process.lua ├── helpers.lua ├── replica_conn.lua ├── capture.lua ├── replica_proxy.lua ├── capturing.lua ├── justrun.lua ├── mismatch_formatter.lua ├── treegen.lua ├── output_beautifier.lua └── comparator.lua ├── doc ├── requirements.txt ├── conf.py ├── crowdin.yaml ├── README.md └── cleanup.py ├── rpm ├── prebuild.sh └── luatest.spec ├── test ├── fixtures │ ├── pass.lua │ ├── error.lua │ ├── fail.lua │ ├── flaky.lua │ ├── parametrized.lua │ ├── flaky_group.lua │ └── trace.lua ├── sub_dir │ └── group_test.lua ├── artifacts │ ├── end_group_test.lua │ ├── start_group_test.lua │ ├── replica_set_test.lua │ ├── sequence_test.lua │ ├── hooks_test.lua │ └── common_test.lua ├── helpers │ └── general.lua ├── xfail_test.lua ├── pp_test.lua ├── server_instance.lua ├── utils_test.lua ├── collect_server_artifacts_test.lua ├── http_response_test.lua ├── treegen_test.lua ├── malformed_args_test.lua ├── helpers_test.lua ├── boxcfg_interaction_test.lua ├── class_test.lua ├── proxy_test.lua ├── justrun_test.lua ├── autorequire_luatest_test.lua ├── collect_rs_artifacts_test.lua ├── output_test.lua ├── assertions_test.lua ├── process_test.lua ├── replica_set_test.lua ├── capturing_test.lua └── capture_test.lua ├── bin └── luatest ├── .luacov ├── .luacheckrc ├── .github ├── pull_request_template.md └── workflows │ ├── ldoc.yml │ ├── test_on_push.yaml │ ├── package_test.yml │ └── push_rockspec.yaml ├── .gitignore ├── luarocks └── test │ └── luatest.lua ├── config.ld ├── luatest-scm-1.rockspec ├── LICENSE ├── cmake └── FindTarantool.cmake └── CMakeLists.txt /tmp/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /debian/compat: -------------------------------------------------------------------------------- 1 | 10 2 | -------------------------------------------------------------------------------- /debian/docs: -------------------------------------------------------------------------------- 1 | README.rst 2 | -------------------------------------------------------------------------------- /debian/source/format: -------------------------------------------------------------------------------- 1 | 3.0 (quilt) 2 | -------------------------------------------------------------------------------- /luatest/VERSION.lua: -------------------------------------------------------------------------------- 1 | return '1.3.1' 2 | -------------------------------------------------------------------------------- /luatest/VERSION.lua.in: -------------------------------------------------------------------------------- 1 | return '@VERSION@' 2 | -------------------------------------------------------------------------------- /doc/requirements.txt: -------------------------------------------------------------------------------- 1 | Sphinx==4.0.2 2 | sphinx-intl==2.0.1 3 | polib==1.1.1 4 | -------------------------------------------------------------------------------- /luatest/output/nil.lua: -------------------------------------------------------------------------------- 1 | return require('luatest.output.generic'):new_class() 2 | -------------------------------------------------------------------------------- /rpm/prebuild.sh: -------------------------------------------------------------------------------- 1 | curl -L https://tarantool.io/release/${LUATEST_TARANTOOL_SERIES:-2}/installer.sh | bash 2 | -------------------------------------------------------------------------------- /debian/prebuild.sh: -------------------------------------------------------------------------------- 1 | curl -L https://tarantool.io/release/${LUATEST_TARANTOOL_SERIES:-2}/installer.sh | bash 2 | -------------------------------------------------------------------------------- /test/fixtures/pass.lua: -------------------------------------------------------------------------------- 1 | local t = require('luatest') 2 | local g = t.group('fixtures.pass') 3 | 4 | g.test_1 = function() 5 | t.assert_equals(1, 1) 6 | end 7 | -------------------------------------------------------------------------------- /bin/luatest: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env tarantool 2 | 3 | print(('Tarantool version is %s'):format(require('tarantool').version)) 4 | 5 | require('luatest.cli_entrypoint')() 6 | -------------------------------------------------------------------------------- /.luacov: -------------------------------------------------------------------------------- 1 | statsfile = 'tmp/luacov.stats.out' 2 | reportfile = 'tmp/luacov.report.out' 3 | exclude = { 4 | '/test/.+_test', 5 | '/tmp/', 6 | '/luatest/coverage_utils' 7 | } 8 | -------------------------------------------------------------------------------- /test/sub_dir/group_test.lua: -------------------------------------------------------------------------------- 1 | local t = require('luatest') 2 | local g = t.group() 3 | 4 | g.test_default_group_name = function() 5 | t.assert_is(t.groups['sub_dir.group'], g) 6 | end 7 | -------------------------------------------------------------------------------- /.luacheckrc: -------------------------------------------------------------------------------- 1 | include_files = {"**/*.lua", "*.rockspec", "*.luacheckrc"} 2 | exclude_files = {"build.luarocks/", "lua_modules/", "tmp/", ".luarocks/", ".rocks/"} 3 | 4 | max_line_length = 120 5 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | What has been done? Why? What problem is being solved? 2 | 3 | I didn't forget about 4 | 5 | - [ ] Tests 6 | - [ ] Changelog 7 | - [ ] Documentation 8 | 9 | Close #??? 10 | -------------------------------------------------------------------------------- /test/fixtures/error.lua: -------------------------------------------------------------------------------- 1 | local t = require('luatest') 2 | local g = t.group('fixtures.error') 3 | 4 | g.test_1 = function() 5 | t.assert_equals(1, 1) 6 | end 7 | 8 | g.test_2 = function() 9 | error('custom-error') 10 | end 11 | -------------------------------------------------------------------------------- /test/fixtures/fail.lua: -------------------------------------------------------------------------------- 1 | local t = require('luatest') 2 | local g = t.group('fixtures.fail') 3 | 4 | g.test_1 = function() 5 | t.assert_equals(1, 1) 6 | end 7 | 8 | g.test_2 = function() 9 | t.assert_equals(1, 0) 10 | end 11 | -------------------------------------------------------------------------------- /test/fixtures/flaky.lua: -------------------------------------------------------------------------------- 1 | local t = require('luatest') 2 | 3 | local g_flaky = t.group('flaky') 4 | 5 | local counter = 1 6 | g_flaky.test_flaky = function() 7 | t.fail_if(counter > 1, 'Boo!') 8 | counter = counter + 1 9 | end 10 | -------------------------------------------------------------------------------- /test/fixtures/parametrized.lua: -------------------------------------------------------------------------------- 1 | local t = require('luatest') 2 | local g = t.group('parametrized_fixture', t.helpers.matrix{a = {1, 2, 3}, b = {4, 5, 6}}) 3 | 4 | g.test_something = function(cg) 5 | t.assert_not_equals(cg.params.a, 3) 6 | end 7 | -------------------------------------------------------------------------------- /luatest/coverage.lua: -------------------------------------------------------------------------------- 1 | -- Initialize luacov when script luatest is running with enabled code coverage 2 | -- collector. 3 | -- This file is expected to be run before any other lua code (ex., with `-l` option). 4 | require('luatest.coverage_utils').enable() 5 | -------------------------------------------------------------------------------- /luatest/cli_entrypoint.lua: -------------------------------------------------------------------------------- 1 | -- Entrypoint for cli util to keep interface simple and provide compatibility 2 | -- between different global package versions. 3 | return function() 4 | local result = require('luatest.sandboxed_runner').run() 5 | os.exit(result) 6 | end 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /lua_modules 2 | .luarocks 3 | .rocks 4 | .idea 5 | 6 | /tmp/* 7 | !/tmp/.keep 8 | 9 | Makefile 10 | CMakeCache.txt 11 | cmake_install.cmake 12 | CMakeFiles 13 | CTestTestfile.cmake 14 | packpack 15 | build 16 | build.luarocks 17 | Testing 18 | /rst/ 19 | doc/output 20 | doc/locale/en/ 21 | -------------------------------------------------------------------------------- /luarocks/test/luatest.lua: -------------------------------------------------------------------------------- 1 | local export = {} 2 | 3 | function export.run_tests(_, args) 4 | local result = require('luatest.sandboxed_runner').run(args) 5 | if result == 0 then 6 | return true 7 | else 8 | return nil, 'test suite failed.' 9 | end 10 | end 11 | 12 | return export 13 | -------------------------------------------------------------------------------- /test/fixtures/flaky_group.lua: -------------------------------------------------------------------------------- 1 | local t = require('luatest') 2 | 3 | local g_flaky = t.group('flaky_group') 4 | 5 | local counter_group = 1 6 | 7 | g_flaky.test_flaky_group_one = function() 8 | t.fail_if(counter_group > 3, 'Boo!') 9 | counter_group = counter_group + 1 10 | end 11 | 12 | g_flaky.test_flaky_group_two = function() 13 | t.fail_if(counter_group > 3, 'Boo!') 14 | counter_group = counter_group + 1 15 | end 16 | -------------------------------------------------------------------------------- /doc/conf.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | 4 | sys.path.insert(0, os.path.abspath('')) 5 | 6 | master_doc = 'README' 7 | 8 | source_suffix = '.rst' 9 | 10 | project = u'Luatest' 11 | 12 | exclude_patterns = [ 13 | 'doc/locale', 14 | 'doc/output', 15 | 'doc/README.md', 16 | 'doc/cleanup.py', 17 | 'doc/requirements.txt', 18 | ] 19 | 20 | language = 'en' 21 | locale_dirs = ['./doc/locale'] 22 | gettext_compact = False 23 | gettext_location = True 24 | -------------------------------------------------------------------------------- /debian/rules: -------------------------------------------------------------------------------- 1 | #!/usr/bin/make -f 2 | 3 | DEB_VERSION := $(shell dpkg-parsechangelog | grep ^Version | awk '{print $$2}') 4 | VERSION := $(shell echo $(DEB_VERSION) | sed 's/-[[:digit:]]\+$$//') 5 | 6 | DEB_CMAKE_EXTRA_FLAGS := -DCMAKE_INSTALL_LIBDIR=lib/$(DEB_HOST_MULTIARCH) \ 7 | -DCMAKE_BUILD_TYPE=RelWithDebInfo -DVERSION=$(VERSION) 8 | 9 | DEB_MAKE_CHECK_TARGET := selftest 10 | 11 | include /usr/share/cdbs/1/rules/debhelper.mk 12 | include /usr/share/cdbs/1/class/cmake.mk 13 | -------------------------------------------------------------------------------- /config.ld: -------------------------------------------------------------------------------- 1 | file = { 2 | 'luatest/init.lua', 3 | 'luatest/assertions.lua', 4 | 'luatest/group.lua', 5 | 'luatest/helpers.lua', 6 | 'luatest/http_response.lua', 7 | 'luatest/runner.lua', 8 | 'luatest/server.lua', 9 | 'luatest/replica_set.lua', 10 | 'luatest/justrun.lua', 11 | 'luatest/cbuilder.lua', 12 | 'luatest/hooks.lua', 13 | 'luatest/treegen.lua', 14 | 'luatest/cluster.lua' 15 | } 16 | topics = { 17 | 'CHANGELOG.md', 18 | } 19 | format = 'markdown' 20 | sort = 'true' 21 | -------------------------------------------------------------------------------- /doc/crowdin.yaml: -------------------------------------------------------------------------------- 1 | # https://support.crowdin.com/configuration-file/ 2 | # https://support.crowdin.com/cli-tool-v3/#configuration 3 | 4 | "project_id" : "463362" 5 | "base_path" : "doc/locale" 6 | "base_url": "https://crowdin.com" 7 | "api_token_env": "CROWDIN_PERSONAL_TOKEN" 8 | 9 | 10 | "preserve_hierarchy": true 11 | 12 | files: [ 13 | { 14 | "source" : "/en/**/*.pot", 15 | "translation" : "/%locale_with_underscore%/LC_MESSAGES/**/%file_name%.po", 16 | "update_option" : "update_as_unapproved", 17 | 18 | "languages_mapping" : { 19 | "locale_with_underscore" : { 20 | "ru" : "ru", 21 | } 22 | }, 23 | } 24 | ] 25 | -------------------------------------------------------------------------------- /.github/workflows/ldoc.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Generate docs with LDoc 3 | 4 | on: 5 | push: 6 | pull_request: 7 | 8 | jobs: 9 | generate-docs: 10 | runs-on: ubuntu-latest 11 | container: tarantool/doc-builder:fat-4.1 12 | if: | 13 | github.event_name == 'push' || 14 | github.event_name == 'pull_request' && github.event.pull_request.head.repo.owner.login != 'tarantool' 15 | 16 | steps: 17 | - uses: actions/checkout@v3 18 | id: checkout 19 | with: 20 | submodules: recursive 21 | 22 | - name: Generate docs with LDoc 23 | run: ldoc --fatalwarnings --ext=rst --dir=rst --toctree="API" . 24 | -------------------------------------------------------------------------------- /doc/README.md: -------------------------------------------------------------------------------- 1 | [![Crowdin](https://badges.crowdin.net/tarantool-luatest/localized.svg)](https://crowdin.com/project/tarantool-luatest) 2 | 3 | # Luatest documentation 4 | Part of Tarantool documentation, published to 5 | https://www.tarantool.io/en/doc/latest/reference/reference_rock/luatest/luatest_overview/ 6 | 7 | ## Create pot files from rst 8 | ```bash 9 | python -m sphinx . doc/locale/en -c doc -b gettext 10 | ``` 11 | 12 | ## Create/update po from pot files 13 | ```bash 14 | sphinx-intl update -p doc/locale/en -d doc/locale -l ru 15 | ``` 16 | 17 | ## Build documentation to doc/output 18 | ```bash 19 | python -m sphinx . doc/output -c doc 20 | ``` 21 | -------------------------------------------------------------------------------- /test/artifacts/end_group_test.lua: -------------------------------------------------------------------------------- 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 servers') 23 | 24 | t.fail_if( 25 | utils.table_len(g.bar_test.servers) ~= 0, 26 | 'Test instance `bar` should not contain servers') 27 | g.s:drop() 28 | end) 29 | -------------------------------------------------------------------------------- /luatest-scm-1.rockspec: -------------------------------------------------------------------------------- 1 | package = 'luatest' 2 | version = 'scm-1' 3 | source = { 4 | url = 'git+https://github.com/tarantool/luatest.git', 5 | branch = 'master', 6 | } 7 | description = { 8 | summary = 'Tool for testing tarantool applications', 9 | homepage = 'https://github.com/tarantool/luatest', 10 | license = 'MIT', 11 | } 12 | dependencies = { 13 | 'lua >= 5.1', 14 | 'checks >= 3.0.0', 15 | } 16 | external_dependencies = { 17 | TARANTOOL = { 18 | header = 'tarantool/module.h', 19 | }, 20 | } 21 | build = { 22 | type = 'cmake', 23 | variables = { 24 | TARANTOOL_DIR = '$(TARANTOOL_DIR)', 25 | TARANTOOL_INSTALL_LUADIR = '$(LUADIR)', 26 | TARANTOOL_INSTALL_BINDIR = '$(BINDIR)', 27 | LUAROCKS = 'true', 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /luatest/monitor.lua: -------------------------------------------------------------------------------- 1 | local fiber = require('fiber') 2 | 3 | local Class = require('luatest.class') 4 | local utils = require('luatest.utils') 5 | 6 | -- Reentrant mutex for fibers. 7 | local Monitor = Class.new() 8 | 9 | function Monitor.mt:initialize() 10 | self.mutex = fiber.cond() 11 | self.fiber_id = nil 12 | self.count = 0 13 | end 14 | 15 | function Monitor.mt:synchronize(fn) 16 | local fiber_id = fiber.self():id() 17 | while self.count > 0 and self.fiber_id ~= fiber_id do 18 | self.mutex:wait() 19 | end 20 | self.fiber_id = fiber_id 21 | self.count = self.count + 1 22 | return utils.reraise_and_ensure(fn, nil, function() 23 | self.count = self.count - 1 24 | if self.count == 0 then 25 | self.fiber_id = nil 26 | self.mutex:signal() 27 | end 28 | end) 29 | end 30 | 31 | return Monitor 32 | -------------------------------------------------------------------------------- /test/artifacts/start_group_test.lua: -------------------------------------------------------------------------------- 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.before_all(function() 9 | g.s = Server:new() 10 | g.s:start() 11 | end) 12 | 13 | g.test_foo = function() 14 | g.foo_test = rawget(_G, 'current_test').value 15 | end 16 | 17 | g.after_test('test_foo', function() 18 | t.fail_if( 19 | utils.table_len(g.foo_test.servers) ~= 0, 20 | 'Test instance should not contain a servers') 21 | end) 22 | 23 | g.test_bar = function() 24 | g.bar_test = rawget(_G, 'current_test').value 25 | end 26 | 27 | g.after_test('test_bar', function() 28 | t.fail_if( 29 | utils.table_len(g.bar_test.servers) ~= 0, 30 | 'Test instance should not contain a servers') 31 | end) 32 | 33 | g.after_all(function() 34 | g.s:drop() 35 | end) 36 | -------------------------------------------------------------------------------- /debian/control: -------------------------------------------------------------------------------- 1 | Source: luatest 2 | Priority: optional 3 | Section: database 4 | Maintainer: Konstantin Nazarov 5 | Build-Depends: debhelper (>= 10), 6 | cdbs (>= 0.4.100), 7 | tarantool (>= 1.9.0), 8 | tarantool-dev (>= 1.9.0), 9 | tarantool-checks (>= 3.0.1), 10 | tt (>= 2.2.1) 11 | Standards-Version: 4.5.1 12 | Homepage: https://github.com/tarantool/luatest 13 | Vcs-Git: git://github.com/tarantool/luatest.git 14 | Vcs-Browser: https://github.com/tarantool/luatest 15 | 16 | Package: luatest 17 | Architecture: all 18 | Depends: tarantool (>= 1.9.0), 19 | tarantool-checks (>= 3.0.1), 20 | ${misc:Depends} 21 | Description: Simple Tarantool test framework for both unit and integration testing 22 | This package provides test runner and helpers for testing Tarantool 23 | applications. 24 | -------------------------------------------------------------------------------- /luatest/test_instance.lua: -------------------------------------------------------------------------------- 1 | local TestInstance = require('luatest.class').new() 2 | 3 | function TestInstance:build(group, method_name) 4 | local name = group.name .. '.' .. method_name 5 | local method = assert(group[method_name], 'Could not find method ' .. name) 6 | assert(type(method) == 'function', name .. ' is not a function') 7 | return self:from({ 8 | name = name, 9 | group = group, 10 | method_name = method_name, 11 | method = method, 12 | line = debug.getinfo(method).linedefined or 0, 13 | }) 14 | end 15 | 16 | -- default constructor, test are PASS by default 17 | function TestInstance.mt:initialize() 18 | self.status = 'success' 19 | self.servers = {} 20 | end 21 | 22 | function TestInstance.mt:update_status(status, message, trace) 23 | self.status = status 24 | self.message = message 25 | self.trace = trace 26 | end 27 | 28 | function TestInstance.mt:is(status) 29 | return self.status == status 30 | end 31 | 32 | return TestInstance 33 | -------------------------------------------------------------------------------- /test/helpers/general.lua: -------------------------------------------------------------------------------- 1 | local t = require('luatest') 2 | local Runner = require('luatest.runner') 3 | local utils = require('luatest.utils') 4 | 5 | local helper = {} 6 | 7 | function helper.run_suite(load_tests, args) 8 | local luatest = dofile(package.search('luatest')) 9 | -- Need to supply any option to prevent luatest from taking args from _G 10 | return Runner.run(args or {}, {luatest = luatest, load_tests = function(...) load_tests(luatest, ...) end}) 11 | end 12 | 13 | function helper.assert_failure(...) 14 | local err = t.assert_error(...) 15 | t.assert(utils.is_luatest_error(err), err) 16 | return err 17 | end 18 | 19 | function helper.assert_failure_equals(msg, ...) 20 | t.assert_equals(helper.assert_failure(...).message, msg) 21 | end 22 | 23 | function helper.assert_failure_matches(msg, ...) 24 | t.assert_str_matches(helper.assert_failure(...).message, msg) 25 | end 26 | 27 | function helper.assert_failure_contains(msg, ...) 28 | t.assert_str_contains(helper.assert_failure(...).message, msg) 29 | end 30 | 31 | return helper 32 | -------------------------------------------------------------------------------- /test/xfail_test.lua: -------------------------------------------------------------------------------- 1 | local t = require('luatest') 2 | local g = t.group() 3 | 4 | local helper = require('test.helpers.general') 5 | 6 | g.test_failed = function() 7 | local result = helper.run_suite(function(lu2) 8 | local pg = lu2.group('xfail') 9 | pg.test_fail = function() 10 | lu2.xfail() 11 | lu2.assert_equals(2 + 2, 5) 12 | end 13 | end) 14 | 15 | t.assert_equals(result, 0) 16 | end 17 | 18 | g.test_succeeded = function() 19 | local result = helper.run_suite(function(lu2) 20 | local pg = lu2.group('xfail') 21 | pg.test_success = function() 22 | lu2.xfail() 23 | lu2.assert_equals(2 + 3, 5) 24 | end 25 | end) 26 | 27 | t.assert_equals(result, 1) 28 | end 29 | 30 | g.test_error = function() 31 | local result = helper.run_suite(function(lu2) 32 | local pg = lu2.group('xfail') 33 | pg.test_error = function() 34 | lu2.xfail() 35 | error('Boom!') 36 | end 37 | end) 38 | 39 | t.assert_equals(result, 1) 40 | end 41 | -------------------------------------------------------------------------------- /test/fixtures/trace.lua: -------------------------------------------------------------------------------- 1 | local fio = require('fio') 2 | 3 | local t = require('luatest') 4 | local server = require('luatest.server') 5 | 6 | local g = t.group('fixtures.trace') 7 | 8 | local root = fio.dirname(fio.abspath('test.helpers')) 9 | 10 | g.before_all(function(cg) 11 | cg.server = server:new{ 12 | env = { 13 | LUA_PATH = root .. '/?.lua;' .. 14 | root .. '/?/init.lua;' .. 15 | root .. '/.rocks/share/tarantool/?.lua' 16 | } 17 | } 18 | cg.server:start() 19 | end) 20 | 21 | g.after_all(function(cg) 22 | cg.server:drop() 23 | end) 24 | 25 | g.test_error = function(cg) 26 | local function outer() 27 | cg.server:exec(function() 28 | local function inner() 29 | error('test error') 30 | end 31 | inner() 32 | end) 33 | end 34 | outer() 35 | end 36 | 37 | g.test_fail = function(cg) 38 | local function outer() 39 | cg.server:exec(function() 40 | local function inner() 41 | t.assert(false) 42 | end 43 | inner() 44 | end) 45 | end 46 | outer() 47 | end 48 | -------------------------------------------------------------------------------- /test/artifacts/replica_set_test.lua: -------------------------------------------------------------------------------- 1 | local t = require('luatest') 2 | local utils = require('luatest.utils') 3 | local ReplicaSet = require('luatest.replica_set') 4 | 5 | local g = t.group() 6 | 7 | g.box_cfg = { 8 | replication_timeout = 0.1, 9 | replication_connect_timeout = 3, 10 | replication_sync_lag = 0.01, 11 | replication_connect_quorum = 3 12 | } 13 | 14 | g.rs = ReplicaSet:new() 15 | g.rs:build_and_add_server({alias = 'replica1', box_cfg = g.box_cfg}) 16 | g.rs:build_and_add_server({alias = 'replica2', box_cfg = g.box_cfg}) 17 | 18 | g.test_foo = function() 19 | g.rs:start() 20 | g.foo_test = rawget(_G, 'current_test').value 21 | end 22 | 23 | g.test_bar = function() 24 | g.bar_test = rawget(_G, 'current_test').value 25 | end 26 | 27 | g.after_test('test_foo', function() 28 | t.fail_if( 29 | utils.table_len(g.foo_test.servers) ~= 2, 30 | 'Test instance should contain all servers from replica set' 31 | ) 32 | g.rs:drop() 33 | end) 34 | 35 | g.after_test('test_bar', function() 36 | t.fail_if( 37 | utils.table_len(g.bar_test.servers) ~= 0, 38 | 'Test instance should not contain any servers' 39 | ) 40 | end) 41 | -------------------------------------------------------------------------------- /test/artifacts/sequence_test.lua: -------------------------------------------------------------------------------- 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.s = Server:new() 8 | g.s:start() 9 | 10 | g.before_each(function() 11 | g.each = Server:new() 12 | g.each:start() 13 | end) 14 | 15 | g.test_foo = function() 16 | g.foo_test = rawget(_G, 'current_test').value 17 | g.foo_test_server = g.each.id 18 | end 19 | 20 | g.test_bar = function() 21 | g.bar_test = rawget(_G, 'current_test').value 22 | g.bar_test_server = g.each.id 23 | end 24 | 25 | g.after_test('test_foo', function() 26 | t.fail_if( 27 | utils.table_len(g.foo_test.servers) ~= 1, 28 | 'Test instance should contain server') 29 | end) 30 | 31 | g.after_test('test_bar', function() 32 | t.fail_if( 33 | utils.table_len(g.bar_test.servers) ~= 1, 34 | 'Test instance should contain server') 35 | end) 36 | 37 | g.after_each(function() 38 | g.each:drop() 39 | end) 40 | 41 | g.after_all(function() 42 | g.s:drop() 43 | t.fail_if( 44 | g.foo_test_server == g.bar_test_server, 45 | 'Servers must be unique within the group' 46 | ) 47 | end) 48 | -------------------------------------------------------------------------------- /test/pp_test.lua: -------------------------------------------------------------------------------- 1 | local t = require('luatest') 2 | local g = t.group() 3 | 4 | local helper = require('test.helpers.general') 5 | local clock = require('clock') 6 | 7 | local pp = require('luatest.pp') 8 | 9 | g.test_tostring = function() 10 | local subject = pp.tostring 11 | t.assert_equals(subject({['a-b'] = 1, ab = 2, [10] = 10}), '{[10] = 10, ["a-b"] = 1, ab = 2}') 12 | 13 | local large_table = {} 14 | local expected_large_format = {'{'} 15 | for i = 0, 9 do 16 | large_table['a' .. i] = i 17 | table.insert(expected_large_format, string.format(' a%d = %d,', i, i)) 18 | end 19 | table.insert(expected_large_format, '}') 20 | t.assert_equals(subject(large_table), table.concat(expected_large_format, '\n')) 21 | end 22 | 23 | g.test_tostring_huge_table = function() 24 | local str_table = {} 25 | for _ = 1, 15000 do table.insert(str_table, 'a') end 26 | local str = table.concat(str_table, '\n') 27 | local start = clock.time() 28 | local result = helper.run_suite(function(lu2) 29 | lu2.group().test = function() t.skip(str) end 30 | end) 31 | t.assert_equals(result, 0) 32 | t.assert_almost_equals(clock.time() - start, 0, 0.5) 33 | end 34 | -------------------------------------------------------------------------------- /luatest/http_response.lua: -------------------------------------------------------------------------------- 1 | --- Class to provide helper methods for HTTP responses 2 | -- 3 | -- @classmod luatest.http_response 4 | 5 | local json = require('json') 6 | 7 | local HTTPResponse = require('luatest.class').new() 8 | 9 | function HTTPResponse.mt:__index(method_name) 10 | if HTTPResponse.mt[method_name] then 11 | return HTTPResponse.mt[method_name] 12 | elseif HTTPResponse.getters[method_name] then 13 | return HTTPResponse.getters[method_name](self) 14 | end 15 | end 16 | 17 | --- Instance getter methods 18 | -- @section methods 19 | 20 | --- For backward compatibility this methods should be accessed 21 | -- as object's fields (eg., `response.json.id`). 22 | -- 23 | -- They are not assigned to object's fields on initialization 24 | -- to be evaluated lazily and to be able to throw errors. 25 | HTTPResponse.getters = {} 26 | 27 | --- Parse json from body. 28 | -- @usage response.json.id 29 | function HTTPResponse.getters:json() 30 | self.json = json.decode(self.body) 31 | return self.json 32 | end 33 | 34 | -- @section Instance methods 35 | 36 | --- Check that status code is 2xx. 37 | function HTTPResponse.mt:is_successful() 38 | return self.status >= 200 and self.status <= 299 39 | end 40 | 41 | return HTTPResponse 42 | -------------------------------------------------------------------------------- /test/server_instance.lua: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env tarantool 2 | 3 | local json = require('json') 4 | 5 | local workdir = os.getenv('TARANTOOL_WORKDIR') 6 | local listen = os.getenv('TARANTOOL_LISTEN') 7 | local http_port = os.getenv('TARANTOOL_HTTP_PORT') 8 | 9 | local httpd = require('http.server').new('0.0.0.0', http_port) 10 | 11 | box.cfg({work_dir = workdir}) 12 | box.schema.user.grant('guest', 'super', nil, nil, {if_not_exists = true}) 13 | box.cfg({listen = listen}) 14 | 15 | httpd:route({path = '/ping', method = 'GET'}, function() 16 | return {status = 200, body = 'pong'} 17 | end) 18 | 19 | httpd:route({path = '/test', method = 'GET'}, function() 20 | local result = { 21 | workdir = workdir, 22 | listen = listen, 23 | http_port = http_port, 24 | value = os.getenv('custom_env'), 25 | } 26 | return {status = 200, body = json.encode(result)} 27 | end) 28 | 29 | httpd:route({path = '/echo', method = 'post'}, function(request) 30 | return {status = 200, body = json.encode({ 31 | body = request:read(), 32 | request_headers = request.headers, 33 | })} 34 | end) 35 | 36 | httpd:route({path = '/test', method = 'post'}, function(request) 37 | return {status = 201, body = request:read()} 38 | end) 39 | 40 | httpd:start() 41 | -------------------------------------------------------------------------------- /test/utils_test.lua: -------------------------------------------------------------------------------- 1 | local t = require('luatest') 2 | local g = t.group() 3 | 4 | local utils = require('luatest.utils') 5 | 6 | g.test_is_tarantool_binary = function() 7 | local cases = { 8 | {'/usr/bin/tarantool', true}, 9 | {'/usr/local/bin/tarantool', true}, 10 | {'/usr/local/bin/tt', false}, 11 | {'/usr/bin/ls', false}, 12 | {'/home/myname/app/bin/tarantool', true}, 13 | {'/home/tarantool/app/bin/go-server', false}, 14 | {'/usr/bin/tarantool-ee_gc64-2.11.0-0-r577', true}, 15 | {'/home/tarantool/app/bin/tarantool', true}, 16 | {'/home/tarantool/app/bin/tarantool-ee_gc64-2.11.0-0-r577', true}, 17 | } 18 | 19 | for _, case in ipairs(cases) do 20 | local path, result = unpack(case) 21 | t.assert_equals(utils.is_tarantool_binary(path), result, 22 | ("Unexpected result for %q"):format(path)) 23 | end 24 | end 25 | 26 | g.test_table_pack = function() 27 | t.assert_equals(utils.table_pack(), {n = 0}) 28 | t.assert_equals(utils.table_pack(1), {n = 1, 1}) 29 | t.assert_equals(utils.table_pack(1, 2), {n = 2, 1, 2}) 30 | t.assert_equals(utils.table_pack(1, 2, nil), {n = 3, 1, 2}) 31 | t.assert_equals(utils.table_pack(1, 2, nil, 3), {n = 4, 1, 2, nil, 3}) 32 | end 33 | -------------------------------------------------------------------------------- /luatest/sandboxed_runner.lua: -------------------------------------------------------------------------------- 1 | local fun = require('fun') 2 | 3 | -- This is lightweight module to run luatest suite. 4 | -- It has only basic dependencies (and no top-level luatest dependencies) 5 | -- to clear as much as modules as possible after running tests. 6 | local export = {} 7 | 8 | -- Tarantool does not invoke Lua's GC-callbacks on exit. 9 | -- So this method clears every loaded package and invokes GC explicitly. 10 | -- It does not clear previously loaded packages to not break any GC-callback 11 | -- which relies on built-in package. 12 | function export.gc_sandboxed(fn) 13 | local original = fun.iter(package.loaded):map(function(x) return x, true end):tomap() 14 | local result = fn() 15 | -- Collect list of new packages to not modify map while iterating over it. 16 | local new_packages = fun.iter(package.loaded): 17 | map(function(x) return x end): 18 | filter(function(x) return not original[x] end): 19 | totable() 20 | for _, name in pairs(new_packages) do 21 | package.loaded[name] = nil 22 | end 23 | collectgarbage() 24 | return result 25 | end 26 | 27 | function export.run(...) 28 | local args = {...} 29 | return export.gc_sandboxed(function() return require('luatest.runner').run(unpack(args)) end) 30 | end 31 | 32 | return export 33 | -------------------------------------------------------------------------------- /.github/workflows/test_on_push.yaml: -------------------------------------------------------------------------------- 1 | name: Run tests 2 | 3 | on: 4 | push: 5 | pull_request: 6 | 7 | jobs: 8 | run-tests-ce: 9 | if: | 10 | github.event_name == 'push' || 11 | github.event_name == 'pull_request' && github.event.pull_request.head.repo.owner.login != 'tarantool' 12 | strategy: 13 | matrix: 14 | tarantool: ["2.11", "3.0", "3.1", "3.2", "3.3", "3.4"] 15 | fail-fast: false 16 | runs-on: [ubuntu-latest] 17 | steps: 18 | - uses: actions/checkout@master 19 | - uses: tarantool/setup-tarantool@v3 20 | with: 21 | tarantool-version: '${{ matrix.tarantool }}' 22 | 23 | - name: Install tt utility 24 | run: | 25 | curl -L https://tarantool.io/release/2/installer.sh | bash 26 | sudo apt-get -y install tt 27 | 28 | - name: Install requirements for community 29 | run: | 30 | cmake -S . -B build 31 | make -C build bootstrap 32 | 33 | # This server starts and listen on 8084 port that is used for tests 34 | - name: Stop Mono server 35 | run: sudo kill -9 $(sudo lsof -t -i tcp:8084) || true 36 | 37 | - name: Run linter 38 | run: make -C build lint 39 | 40 | - name: Run tests with coverage 41 | run: make -C build selftest-coverage 42 | -------------------------------------------------------------------------------- /doc/cleanup.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python3 2 | import argparse 3 | from glob import glob 4 | from polib import pofile, POFile, _BaseFile 5 | 6 | parser = argparse.ArgumentParser(description='Cleanup PO and POT files') 7 | parser.add_argument('extension', type=str, choices=['po', 'pot', 'both'], 8 | help='cleanup files with extension: po, pot or both') 9 | 10 | 11 | class PoFile(POFile): 12 | 13 | def __unicode__(self): 14 | return _BaseFile.__unicode__(self) 15 | 16 | def metadata_as_entry(self): 17 | class M: 18 | def __unicode__(self, _): 19 | return '' 20 | return M() 21 | 22 | 23 | def cleanup_files(extension): 24 | mask = f'**/*.{extension}' 25 | for file_path in glob(mask, recursive=True): 26 | print(f'cleanup {file_path}') 27 | po_file: POFile = pofile(file_path, klass=PoFile) 28 | po_file.header = '' 29 | po_file.metadata = {} 30 | po_file.metadata_is_fuzzy = False 31 | 32 | for item in po_file: 33 | item.occurrences = None 34 | 35 | po_file.save() 36 | 37 | 38 | if __name__ == "__main__": 39 | 40 | args = parser.parse_args() 41 | 42 | if args.extension in ['po', 'both']: 43 | cleanup_files('po') 44 | 45 | if args.extension in ['pot', 'both']: 46 | cleanup_files('pot') 47 | -------------------------------------------------------------------------------- /luatest/class.lua: -------------------------------------------------------------------------------- 1 | --- Utils to define classes and their hierarchies. 2 | -- Every class has `:new` method to create an instance of this class. 3 | -- Every instance receives properties from class's `mt` field. 4 | local Class = {mt = {}} 5 | Class.mt.__index = Class.mt 6 | 7 | --- Builds a class with optional superclass. 8 | -- Both instance and class methods are inherited from superclass if given. 9 | -- Otherwise default class methods are copied from Class.mt. 10 | function Class.new(class, super) 11 | class = class or {} 12 | class.__index = class 13 | class.mt = {class = class} 14 | class.mt.__index = class.mt 15 | if super then 16 | class.super = super 17 | setmetatable(class, super) 18 | setmetatable(class.mt, super.mt) 19 | else 20 | class.super = Class.mt 21 | setmetatable(class, Class.mt) 22 | end 23 | return class 24 | end 25 | 26 | --- Create descendant class. 27 | function Class.mt:new_class(class) 28 | return Class.new(class, self) 29 | end 30 | 31 | --- Build an instance of a class. 32 | -- It sets metatable and calls instance's `initialize` method if it's available. 33 | function Class.mt:new(...) 34 | return self:from({}, ...) 35 | end 36 | 37 | --- Initialize instance from given object. 38 | function Class.mt:from(object, ...) 39 | setmetatable(object, self.mt) 40 | if object.initialize then object:initialize(...) end 41 | return object 42 | end 43 | 44 | return Class 45 | -------------------------------------------------------------------------------- /test/collect_server_artifacts_test.lua: -------------------------------------------------------------------------------- 1 | local fio = require('fio') 2 | 3 | local t = require('luatest') 4 | local g = t.group() 5 | 6 | local Server = t.Server 7 | 8 | local function assert_artifacts_path(s) 9 | t.assert_equals(fio.path.exists(s.artifacts), true) 10 | t.assert_equals(fio.path.is_dir(s.artifacts), true) 11 | end 12 | 13 | g.before_all(function() 14 | g.s_all = Server:new({alias = 'all'}) 15 | g.s_all2 = Server:new({alias = 'all2'}) 16 | 17 | g.s_all:start() 18 | g.s_all2:start() 19 | end) 20 | 21 | g.before_each(function() 22 | g.s_each = Server:new({alias = 'each'}) 23 | g.s_each2 = Server:new({alias = 'each2'}) 24 | 25 | g.s_each:start() 26 | g.s_each2:start() 27 | end) 28 | 29 | g.before_test('test_foo', function() 30 | g.s_test = Server:new({alias = 'test'}) 31 | g.s_test2 = Server:new({alias = 'test2'}) 32 | 33 | g.s_test:start() 34 | g.s_test2:start() 35 | end) 36 | 37 | g.test_foo = function() 38 | local test = rawget(_G, 'current_test') 39 | 40 | test.status = 'fail' 41 | g.s_test:drop() 42 | g.s_test2:drop() 43 | g.s_each:drop() 44 | g.s_each2:drop() 45 | g.s_all:drop() 46 | g.s_all2:drop() 47 | test.status = 'success' 48 | 49 | assert_artifacts_path(g.s_test) 50 | assert_artifacts_path(g.s_test2) 51 | assert_artifacts_path(g.s_each) 52 | assert_artifacts_path(g.s_each2) 53 | assert_artifacts_path(g.s_all) 54 | assert_artifacts_path(g.s_all2) 55 | end 56 | -------------------------------------------------------------------------------- /test/http_response_test.lua: -------------------------------------------------------------------------------- 1 | local t = require('luatest') 2 | local g = t.group() 3 | 4 | local json = require('json') 5 | 6 | local HTTPResponse = require('luatest.http_response') 7 | 8 | g.test_is_successfull = function() 9 | local subject = function(http_status_code) 10 | return HTTPResponse:from({status = http_status_code}):is_successful() 11 | end 12 | t.assert_equals(subject(199), false) 13 | t.assert_equals(subject(100), false) 14 | t.assert_equals(subject(200), true) 15 | t.assert_equals(subject(201), true) 16 | t.assert_equals(subject(299), true) 17 | t.assert_equals(subject(300), false) 18 | t.assert_equals(subject(400), false) 19 | t.assert_equals(subject(500), false) 20 | end 21 | 22 | g.test_json = function() 23 | local subject = function(data) 24 | return HTTPResponse:from(data).json 25 | end 26 | local value = {field = 'value'} 27 | local json_value = json.encode(value) 28 | local invalid_json = json_value .. '!!!' 29 | t.assert_equals(subject({status = 200, body = json_value}), value) 30 | t.assert_equals(subject({status = 500, body = json_value}), value) 31 | 32 | local response = HTTPResponse:from({body = invalid_json}) 33 | -- assert no error until .json is accessed and error stays on consecutive calls 34 | for _ = 1, 2 do 35 | t.assert_equals(response.body, invalid_json) 36 | t.assert_error_msg_contains('Expected the end but found invalid token', function() return response.json end) 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2019-2020 Tarantool AUTHORS: please see AUTHORS file. 2 | 3 | Based on luaunit. Copyright 2005-2018, 4 | Philippe Fremy 5 | Ryu, Gwang (http://www.gpgstudy.com/gpgiki/LuaUnit) 6 | 7 | Redistribution and use in source and binary forms, with or 8 | without modification, are permitted provided that the following 9 | conditions are met: 10 | 11 | 1. Redistributions of source code must retain the above 12 | copyright notice, this list of conditions and the 13 | following disclaimer. 14 | 15 | 2. Redistributions in binary form must reproduce the above 16 | copyright notice, this list of conditions and the following 17 | disclaimer in the documentation and/or other materials 18 | provided with the distribution. 19 | 20 | THIS SOFTWARE IS PROVIDED BY AUTHORS ``AS IS'' AND 21 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED 22 | TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 23 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL 24 | AUTHORS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 25 | INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 26 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 27 | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR 28 | BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 29 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 30 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF 31 | THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 32 | SUCH DAMAGE. 33 | -------------------------------------------------------------------------------- /test/treegen_test.lua: -------------------------------------------------------------------------------- 1 | local t = require('luatest') 2 | local fio = require('fio') 3 | 4 | local treegen = require('luatest.treegen') 5 | 6 | local g = t.group() 7 | 8 | 9 | local function assert_file_content_equals(file, expected) 10 | local fh = fio.open(file) 11 | t.assert_equals(fh:read(), expected) 12 | end 13 | 14 | g.test_prepare_directory = function() 15 | treegen.add_template('^.*$', 'test_script') 16 | local dir = treegen.prepare_directory({'foo/bar.lua', 'baz.lua'}) 17 | 18 | t.assert(fio.path.is_dir(dir)) 19 | t.assert(fio.path.exists(dir)) 20 | 21 | t.assert(fio.path.exists(fio.pathjoin(dir, 'foo', 'bar.lua'))) 22 | t.assert(fio.path.exists(fio.pathjoin(dir, 'baz.lua'))) 23 | 24 | assert_file_content_equals(fio.pathjoin(dir, 'foo', 'bar.lua'), 'test_script') 25 | assert_file_content_equals(fio.pathjoin(dir, 'baz.lua'), 'test_script') 26 | end 27 | 28 | g.before_test('test_clean_keep_data', function() 29 | treegen.add_template('^.*$', 'test_script') 30 | 31 | os.setenv('KEEP_DATA', 'true') 32 | 33 | g.dir = treegen.prepare_directory(g, {'foo.lua'}) 34 | 35 | t.assert(fio.path.is_dir(g.dir)) 36 | t.assert(fio.path.exists(g.dir)) 37 | end) 38 | 39 | g.test_clean_keep_data = function() 40 | t.assert(fio.path.is_dir(g.dir)) 41 | t.assert(fio.path.exists(g.dir)) 42 | end 43 | 44 | g.after_test('test_clean_keep_data', function() 45 | os.setenv('KEEP_DATA', '') 46 | t.assert(fio.path.is_dir(g.dir)) 47 | t.assert(fio.path.exists(g.dir)) 48 | end) 49 | -------------------------------------------------------------------------------- /.github/workflows/package_test.yml: -------------------------------------------------------------------------------- 1 | name: Package test 2 | 3 | on: 4 | push: 5 | pull_request: 6 | 7 | jobs: 8 | package-test: 9 | if: github.repository == 'tarantool/luatest' && ( 10 | github.event_name == 'push' || ( github.event_name == 'pull_request' && 11 | github.repository_owner != 'tarantool' ) ) 12 | 13 | strategy: 14 | fail-fast: false 15 | matrix: 16 | include: 17 | - dist: ubuntu 18 | version: jammy 19 | - dist: fedora 20 | version: 36 21 | 22 | runs-on: ubuntu-latest 23 | 24 | steps: 25 | - name: Check out repo 26 | uses: actions/checkout@v3 27 | with: 28 | fetch-depth: 0 29 | 30 | - name: Check out packpack 31 | uses: actions/checkout@v3 32 | with: 33 | repository: packpack/packpack 34 | path: packpack 35 | 36 | - name: Set package version 37 | run: | 38 | GIT_TAG=$(git tag --points-at HEAD) 39 | GIT_DESCRIBE=$(git describe HEAD) 40 | if [ -n "${GIT_TAG}" ]; then 41 | echo "VERSION=${GIT_TAG}" >> $GITHUB_ENV 42 | else 43 | echo "VERSION=$(echo ${GIT_DESCRIBE} | sed ${SED_REPLACE_VERSION_REGEX}).dev" >> $GITHUB_ENV 44 | fi 45 | env: 46 | SED_REPLACE_VERSION_REGEX: s/-\([0-9]\+\)-g[0-9a-f]\+$/.\1/ 47 | 48 | - name: Run packaging 49 | run: ./packpack/packpack 50 | env: 51 | OS: ${{ matrix.dist }} 52 | DIST: ${{ matrix.version }} 53 | -------------------------------------------------------------------------------- /debian/copyright: -------------------------------------------------------------------------------- 1 | Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ 2 | Debianized-By: Roman Tsisyk 3 | Upstream-Name: tarantool-shard 4 | Upstream-Contact: support@tarantool.org 5 | Source: https://github.com/tarantool/shard 6 | 7 | Files: * 8 | Copyright: 2015-2016 Tarantool AUTHORS 9 | License: BSD-2-Clause 10 | Redistribution and use in source and binary forms, with or without 11 | modification, are permitted provided that the following conditions 12 | are met: 13 | 1. Redistributions of source code must retain the above copyright 14 | notice, this list of conditions and the following disclaimer. 15 | 2. Redistributions in binary form must reproduce the above copyright 16 | notice, this list of conditions and the following disclaimer in the 17 | documentation and/or other materials provided with the distribution. 18 | . 19 | THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND 20 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 21 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 22 | ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE 23 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 24 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 25 | OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 26 | HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 27 | LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 28 | OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 29 | SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /luatest/server_instance.lua: -------------------------------------------------------------------------------- 1 | local fun = require('fun') 2 | local json = require('json') 3 | 4 | local TIMEOUT_INFINITY = 500 * 365 * 86400 5 | 6 | local function default_cfg() 7 | return { 8 | work_dir = os.getenv('TARANTOOL_WORKDIR'), 9 | listen = os.getenv('TARANTOOL_LISTEN'), 10 | } 11 | end 12 | 13 | local function env_cfg() 14 | local cfg = os.getenv('TARANTOOL_BOX_CFG') 15 | if cfg == nil then 16 | return {} 17 | end 18 | local res = json.decode(cfg) 19 | assert(type(res) == 'table') 20 | return res 21 | end 22 | 23 | local function box_cfg(cfg) 24 | return fun.chain(default_cfg(), env_cfg(), cfg or {}):tomap() 25 | end 26 | 27 | -- Set the shutdown timeout to infinity to catch tests that leave asynchronous 28 | -- requests. With the default timeout of 3 seconds, such tests would still pass, 29 | -- but slow down the overall test run, because the server would take longer to 30 | -- stop. Setting the timeout to infinity makes such bad tests hang and fail. 31 | if type(box.ctl.set_on_shutdown_timeout) == 'function' then 32 | box.ctl.set_on_shutdown_timeout(TIMEOUT_INFINITY) 33 | end 34 | 35 | local run_before_box_cfg = os.getenv('TARANTOOL_RUN_BEFORE_BOX_CFG') 36 | if run_before_box_cfg then 37 | loadstring(run_before_box_cfg)() 38 | end 39 | 40 | box.cfg(box_cfg()) 41 | 42 | box.schema.user.grant('guest', 'super', nil, nil, {if_not_exists = true}) 43 | 44 | -- server:wait_until_ready() unblocks only when this variable becomes `true`. 45 | -- In this case, it is considered that the instance is fully operable. 46 | -- Use server:start({wait_until_ready = false}) to not wait for setting this 47 | -- variable. 48 | _G.ready = true 49 | -------------------------------------------------------------------------------- /luatest/loader.lua: -------------------------------------------------------------------------------- 1 | -- Module to load test files from certain directory recursievly. 2 | 3 | local fio = require('fio') 4 | local fun = require('fun') 5 | 6 | -- Returns list of all nested files within given path. 7 | -- As fio.glob does not support `**/*` this method adds `/*` to path and glog it 8 | -- until result is empty. 9 | local function glob_recursive(path) 10 | local pattern = path 11 | local result = {} 12 | repeat 13 | pattern = pattern .. '/*' 14 | local last_result = fio.glob(pattern) 15 | for _, item in ipairs(last_result) do 16 | result[#result + 1] = item 17 | end 18 | until #last_result == 0 19 | return result 20 | end 21 | 22 | -- If directory is given then it's scanned recursievly for files ending with `_test.lua`. 23 | -- If `.lua` file is given then it's used as is. 24 | -- Resulting list of files is mapped to lua's module names. 25 | local function get_test_modules_list(path) 26 | local files 27 | if path:endswith('.lua') then 28 | files = fun.iter({path}) 29 | else 30 | local list = glob_recursive(path) 31 | table.sort(list) 32 | files = fun.iter(list):filter(function(x) return x:endswith('_test.lua') end) 33 | end 34 | return files: 35 | map(function(x) return x:gsub('%.lua$', '') end): 36 | map(function(x) return x:gsub('/+', '.') end): 37 | totable() 38 | end 39 | 40 | -- Uses get_test_modules_list to retrieve modules list for given path and requires them. 41 | local function require_tests(path) 42 | for _, mod in ipairs(get_test_modules_list(path)) do 43 | require(mod) 44 | end 45 | end 46 | 47 | return {require_tests = require_tests} 48 | -------------------------------------------------------------------------------- /luatest/ffi_io.lua: -------------------------------------------------------------------------------- 1 | local errno = require('errno') 2 | local ffi = require('ffi') 3 | local socket = require('socket') 4 | 5 | ffi.cdef([[ 6 | int close(int fildes); 7 | int dup2(int oldfd, int newfd); 8 | int fileno(struct FILE *stream); 9 | int pipe(int fildes[2]); 10 | 11 | ssize_t read(int fd, void *buf, size_t count); 12 | ]]) 13 | 14 | local export = {} 15 | 16 | function export.create_pipe() 17 | local fildes = ffi.new('int[?]', 2) 18 | if ffi.C.pipe(fildes) ~= 0 then 19 | error('pipe call failed: ' .. errno.strerror()) 20 | end 21 | ffi.gc(fildes, function(x) 22 | ffi.C.close(x[0]) 23 | ffi.C.close(x[1]) 24 | end) 25 | return fildes 26 | end 27 | 28 | export.READ_BUFFER_SIZE = 4096 29 | export.READ_PIPE_TIMEOUT = 1 30 | 31 | -- Read fd into chunks array while it's readable. 32 | function export.read_fd(fd, chunks, options) 33 | local buffer_size = options and options.buffer_size or export.READ_BUFFER_SIZE 34 | local timeout = options and options.timeout or export.READ_TIMEOUT 35 | chunks = chunks or {} 36 | local buffer 37 | while socket.iowait(fd, 'R', timeout) ~= '' do 38 | timeout = 0 -- next iowait must return immediately 39 | buffer = buffer or ffi.new('char[?]', buffer_size) 40 | local count = ffi.C.read(fd, buffer, buffer_size) 41 | if count < 0 then 42 | error('read pipe failed: ' .. errno.strerror()) 43 | end 44 | table.insert(chunks, ffi.string(buffer, count)) 45 | end 46 | return chunks 47 | end 48 | 49 | -- Call `dup2` for io object to change it's descriptor. 50 | function export.dup2_io(oldfd, newio) 51 | ffi.C.dup2(oldfd, ffi.C.fileno(newio)) 52 | end 53 | 54 | return export 55 | -------------------------------------------------------------------------------- /luatest/coverage_utils.lua: -------------------------------------------------------------------------------- 1 | local fio = require('fio') 2 | local runner = require('luacov.runner') 3 | 4 | -- Fix luacov issue. Without patch it's failed when `assert` is redefined. 5 | function runner.update_stats(old_stats, extra_stats) 6 | old_stats.max = math.max(old_stats.max, extra_stats.max) 7 | for line_nr, run_nr in pairs(extra_stats) do 8 | if type(line_nr) == 'number' then -- This line added instead of `= nil` 9 | old_stats[line_nr] = (old_stats[line_nr] or 0) + run_nr 10 | old_stats.max_hits = math.max(old_stats.max_hits, old_stats[line_nr]) 11 | end 12 | end 13 | end 14 | 15 | -- Module with utilities for collecting code coverage from external processes. 16 | local export = { 17 | DEFAULT_EXCLUDE = { 18 | '^builtin/', 19 | '/luarocks/', 20 | '/build.luarocks/', 21 | '/.rocks/', 22 | }, 23 | } 24 | 25 | local function with_cwd(dir, fn) 26 | local old = fio.cwd() 27 | assert(fio.chdir(dir), 'Failed to chdir to ' .. dir) 28 | fn() 29 | assert(fio.chdir(old), 'Failed to chdir to ' .. old) 30 | end 31 | 32 | function export.enable() 33 | local root = os.getenv('LUATEST_LUACOV_ROOT') 34 | if not root then 35 | root = fio.cwd() 36 | os.setenv('LUATEST_LUACOV_ROOT', root) 37 | end 38 | -- Chdir to original root so luacov can find default config and resolve relative filenames. 39 | with_cwd(root, function() 40 | local config = runner.load_config() 41 | config.exclude = config.exclude or {} 42 | for _, item in pairs(export.DEFAULT_EXCLUDE) do 43 | table.insert(config.exclude, item) 44 | end 45 | runner.init(config) 46 | end) 47 | end 48 | 49 | function export.shutdown() 50 | if runner.initialized then 51 | runner.shutdown() 52 | end 53 | end 54 | 55 | return export 56 | -------------------------------------------------------------------------------- /.github/workflows/push_rockspec.yaml: -------------------------------------------------------------------------------- 1 | name: Push rockspec 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | tags: 8 | - '*' 9 | 10 | env: 11 | ROCK_NAME: "luatest" 12 | 13 | jobs: 14 | push-scm-rockspec: 15 | runs-on: [ ubuntu-latest ] 16 | if: github.ref == 'refs/heads/master' 17 | steps: 18 | - uses: actions/checkout@master 19 | 20 | - uses: tarantool/rocks.tarantool.org/github-action@master 21 | with: 22 | auth: ${{ secrets.ROCKS_AUTH }} 23 | files: ${{ env.ROCK_NAME }}-scm-1.rockspec 24 | 25 | push-tagged-rockspec: 26 | runs-on: [ ubuntu-latest ] 27 | if: startsWith(github.ref, 'refs/tags') 28 | steps: 29 | - uses: actions/checkout@master 30 | 31 | - uses: tarantool/setup-tarantool@v3 32 | with: 33 | tarantool-version: '2.11' 34 | 35 | # https://stackoverflow.com/questions/58177786/get-the-current-pushed-tag-in-github-actions 36 | - name: Set env 37 | run: echo "GIT_TAG=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV 38 | 39 | - run: tarantoolctl rocks new_version --tag ${GIT_TAG} 40 | - run: tarantoolctl rocks make ${{ env.ROCK_NAME }}-${GIT_TAG}-1.rockspec 41 | 42 | # - name: Create release rockspec 43 | # run: | 44 | # sed \ 45 | # -e "s/branch = '.\+'/tag = '${GIT_TAG}'/g" \ 46 | # -e "s/version = '.\+'/version = '${GIT_TAG}-1'/g" \ 47 | # ${{ env.ROCK_NAME }}-scm-1.rockspec > ${{ env.ROCK_NAME }}-${GIT_TAG}-1.rockspec 48 | 49 | - run: tarantoolctl rocks pack ${{ env.ROCK_NAME }} ${GIT_TAG} 50 | 51 | - uses: tarantool/rocks.tarantool.org/github-action@master 52 | with: 53 | auth: ${{ secrets.ROCKS_AUTH }} 54 | files: | 55 | ${{ env.ROCK_NAME }}-${GIT_TAG}-1.rockspec 56 | ${{ env.ROCK_NAME }}-${GIT_TAG}-1.all.rock 57 | -------------------------------------------------------------------------------- /luatest/tarantool.lua: -------------------------------------------------------------------------------- 1 | --- Collection of test helpers related to Tarantool instance. 2 | -- 3 | -- @module luatest.tarantool 4 | 5 | local fiber = require('fiber') 6 | local tarantool = require('tarantool') 7 | 8 | local assertions = require('luatest.assertions') 9 | 10 | local M = {} 11 | 12 | --- Return true if Tarantool build type is Debug. 13 | function M.is_debug_build() 14 | return tarantool.build.target:endswith('-Debug') 15 | end 16 | 17 | --- Return true if Tarantool package is Enterprise. 18 | function M.is_enterprise_package() 19 | return tarantool.package == 'Tarantool Enterprise' 20 | end 21 | 22 | --- Skip a running test unless Tarantool build type is Debug. 23 | -- 24 | -- @string[opt] message Message to describe the reason. 25 | function M.skip_if_not_debug(message) 26 | assertions.skip_if( 27 | not M.is_debug_build(), message or 'build type is not Debug' 28 | ) 29 | end 30 | 31 | --- Skip a running test if Tarantool package is Enterprise. 32 | -- 33 | -- @string[opt] message Message to describe the reason. 34 | function M.skip_if_enterprise(message) 35 | assertions.skip_if( 36 | M.is_enterprise_package(), message or 'package is Enterprise' 37 | ) 38 | end 39 | 40 | --- Skip a running test if Tarantool package is NOT Enterprise. 41 | -- 42 | -- @string[opt] message Message to describe the reason. 43 | function M.skip_if_not_enterprise(message) 44 | assertions.skip_if( 45 | not M.is_enterprise_package(), message or 'package is not Enterprise' 46 | ) 47 | end 48 | 49 | --- Search for a fiber with the specified name and return the fiber object. 50 | -- 51 | -- @string name Fiber name. 52 | function M.find_fiber_by_name(name) 53 | for id, f in pairs(fiber.info()) do 54 | if f.name == name then 55 | return fiber.find(id) 56 | end 57 | end 58 | return nil 59 | end 60 | 61 | return M 62 | -------------------------------------------------------------------------------- /luatest/output/tap.lua: -------------------------------------------------------------------------------- 1 | local utils = require('luatest.utils') 2 | -- For a good reference for TAP format, check: http://testanything.org/tap-specification.html 3 | local Output = require('luatest.output.generic'):new_class() 4 | 5 | function Output.mt:start_suite() 6 | print("TAP version 13") 7 | print("1.."..self.result.selected_count) 8 | print('# Started on ' .. os.date(nil, self.result.start_time)) 9 | end 10 | 11 | function Output.mt:start_group(group) -- luacheck: no unused 12 | print('# Starting group: ' .. group.name) 13 | end 14 | 15 | function Output.mt:update_status(node) 16 | if node:is('xfail') then 17 | return 18 | end 19 | 20 | if node:is('skip') then 21 | io.stdout:write("ok ", node.serial_number, "\t# SKIP ", node.message or '', "\n") 22 | return 23 | end 24 | 25 | io.stdout:write("not ok ", node.serial_number, "\t", node.name, "\n") 26 | local prefix = '# ' 27 | if self.verbosity > self.class.VERBOSITY.QUIET then 28 | print(prefix .. node.message:gsub('\n', '\n' .. prefix)) 29 | end 30 | if (node:is('fail') or node:is('error')) and self.verbosity >= self.class.VERBOSITY.VERBOSE then 31 | print(prefix .. node.trace: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\t%s -> %s'):format(prefix, server.alias, server.artifacts)) 36 | end 37 | end 38 | end 39 | end 40 | 41 | function Output.mt:end_test(node) -- luacheck: no unused 42 | if node:is('success') then 43 | io.stdout:write("ok ", node.serial_number, "\t", node.name, "\n") 44 | end 45 | if node:is('xfail') then 46 | io.stdout:write("ok ", node.serial_number, "\t# XFAIL ", node.message or '', "\n") 47 | end 48 | end 49 | 50 | function Output.mt:end_suite() 51 | print('# ' .. self:status_line()) 52 | end 53 | 54 | return Output 55 | -------------------------------------------------------------------------------- /test/artifacts/hooks_test.lua: -------------------------------------------------------------------------------- 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 | g.test_association_between_test_and_servers = function() 36 | g.internal = Server:new({alias = 'internal'}) 37 | g.internal:start() 38 | 39 | local test = rawget(_G, 'current_test').value 40 | 41 | -- test static association 42 | t.assert(is_server_in_test(g.internal, test)) 43 | t.assert(is_server_in_test(g.each, test)) 44 | t.assert(is_server_in_test(g.test, test)) 45 | t.assert_not(is_server_in_test(g.public, test)) 46 | 47 | g.public:exec(function() return 1 + 1 end) 48 | g.all:exec(function() return 1 + 1 end) 49 | 50 | -- test dynamic association 51 | t.assert(is_server_in_test(g.public, test)) 52 | t.assert(is_server_in_test(g.all, test)) 53 | 54 | t.assert(utils.table_len(test.servers) == 5) 55 | end 56 | 57 | g.after_test('test_association_between_test_and_servers', function() 58 | g.internal:drop() 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 | g.public:drop() 72 | end) 73 | -------------------------------------------------------------------------------- /luatest/group.lua: -------------------------------------------------------------------------------- 1 | --- Tests group. 2 | -- To add new example add function at key starting with `test`. 3 | -- 4 | -- Group hooks run always when test group is changed. 5 | -- So it may run multiple times when `--shuffle` option is used. 6 | -- 7 | -- @classmod luatest.group 8 | local Group = require('luatest.class').new() 9 | 10 | local hooks = require('luatest.hooks') 11 | 12 | local function find_closest_matching_frame(pattern) 13 | local level = 2 14 | while true do 15 | local info = debug.getinfo(level, 'S') 16 | if not info then 17 | return 18 | end 19 | local source = info.source 20 | if source:match(pattern) then 21 | return info 22 | end 23 | level = level + 1 24 | end 25 | end 26 | 27 | --- Instance methods 28 | -- @section methods 29 | 30 | --- Add callback to run once before all tests in the group. 31 | -- @function Group.mt.before_all 32 | -- @param fn 33 | 34 | --- Add callback to run once after all tests in the group. 35 | -- @function Group.mt.after_all 36 | -- @param fn 37 | 38 | --- Add callback to run before each test in the group. 39 | -- @function Group.mt.before_each 40 | -- @param fn 41 | 42 | --- Add callback to run after each test in the group. 43 | -- @function Group.mt.after_each 44 | -- @param fn 45 | 46 | --- 47 | -- @string[opt] name Default name is inferred from caller filename when possible. 48 | -- For `test/a/b/c_d_test.lua` it will be `a.b.c_d`. 49 | -- @return Group instance 50 | function Group.mt:initialize(name) 51 | if not name then 52 | local pattern = '.*/test/(.+)_test%.lua' 53 | local info = assert( 54 | find_closest_matching_frame(pattern), 55 | "Can't derive test name from file name (it should match '.*/test/.*_test.lua')" 56 | ) 57 | local test_filename = info.source:match(pattern) 58 | name = test_filename:gsub('/', '.') 59 | end 60 | if name:find('/') then 61 | error('Group name must not contain `/`: ' .. name) 62 | end 63 | self.name = name 64 | hooks._define_group_hooks(self) 65 | end 66 | 67 | return Group 68 | -------------------------------------------------------------------------------- /test/artifacts/common_test.lua: -------------------------------------------------------------------------------- 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 | g.test_only_private_server_has_been_added = function() 30 | g.private = Server:new({alias = 'private'}) 31 | g.private:start() 32 | end 33 | 34 | g.after_test('test_only_private_server_has_been_added', function() 35 | t.fail_if( 36 | rawget(_G, 'current_test').value.servers[g.private.id] == nil, 37 | 'Test should contain only private server') 38 | end) 39 | 40 | g.before_test('test_add_server_from_test_hooks', function() 41 | g.before = Server:new({alias = 'before'}) 42 | g.before:start() 43 | end) 44 | 45 | g.test_add_server_from_test_hooks = function() 46 | end 47 | 48 | g.after_test('test_add_server_from_test_hooks', function() 49 | g.after = Server:new({alias = 'after'}) 50 | g.after:start() 51 | 52 | local test_servers = rawget(_G, 'current_test').value.servers 53 | 54 | t.fail_if( 55 | utils.table_len(test_servers) ~= 2, 56 | 'Test should contain two servers (from before/after hooks)') 57 | t.fail_if( 58 | test_servers[g.before.id] == nil or test_servers[g.after.id] == nil, 59 | 'Test should contain only `before` and `after` servers') 60 | end) 61 | 62 | g.after_all(function() 63 | g.public:drop() 64 | g.private:drop() 65 | g.before:drop() 66 | g.after:drop() 67 | end) 68 | -------------------------------------------------------------------------------- /cmake/FindTarantool.cmake: -------------------------------------------------------------------------------- 1 | # Define GNU standard installation directories 2 | include(GNUInstallDirs) 3 | 4 | macro(extract_definition name output input) 5 | string(REGEX MATCH "#define[\t ]+${name}[\t ]+\"([^\"]*)\"" 6 | _t "${input}") 7 | string(REGEX REPLACE "#define[\t ]+${name}[\t ]+\"(.*)\"" "\\1" 8 | ${output} "${_t}") 9 | endmacro() 10 | 11 | find_path(TARANTOOL_INCLUDE_DIR tarantool/module.h 12 | HINTS ${TARANTOOL_DIR} ENV TARANTOOL_DIR 13 | PATH_SUFFIXES include 14 | ) 15 | 16 | if(TARANTOOL_INCLUDE_DIR) 17 | set(_config "-") 18 | file(READ "${TARANTOOL_INCLUDE_DIR}/tarantool/module.h" _config0) 19 | string(REPLACE "\\" "\\\\" _config ${_config0}) 20 | unset(_config0) 21 | extract_definition(PACKAGE_VERSION TARANTOOL_VERSION ${_config}) 22 | extract_definition(INSTALL_PREFIX _install_prefix ${_config}) 23 | unset(_config) 24 | endif() 25 | 26 | include(FindPackageHandleStandardArgs) 27 | find_package_handle_standard_args(Tarantool 28 | REQUIRED_VARS TARANTOOL_INCLUDE_DIR VERSION_VAR TARANTOOL_VERSION) 29 | if(TARANTOOL_FOUND) 30 | set(TARANTOOL_INCLUDE_DIRS "${TARANTOOL_INCLUDE_DIR}" 31 | "${TARANTOOL_INCLUDE_DIR}/tarantool/" 32 | CACHE PATH "Include directories for Tarantool") 33 | set(TARANTOOL_INSTALL_LIBDIR "${CMAKE_INSTALL_LIBDIR}/tarantool" 34 | CACHE PATH "Directory for storing Lua modules written in Lua") 35 | set(TARANTOOL_INSTALL_LUADIR "${CMAKE_INSTALL_DATADIR}/tarantool" 36 | CACHE PATH "Directory for storing Lua modules written in C") 37 | set(TARANTOOL_INSTALL_BINDIR "${CMAKE_INSTALL_BINDIR}" 38 | CACHE PATH "Directory for binary files and scripts") 39 | 40 | if (NOT TARANTOOL_FIND_QUIETLY AND NOT FIND_TARANTOOL_DETAILS) 41 | set(FIND_TARANTOOL_DETAILS ON CACHE INTERNAL "Details about TARANTOOL") 42 | message(STATUS "Tarantool LUADIR is ${TARANTOOL_INSTALL_LUADIR}") 43 | message(STATUS "Tarantool LIBDIR is ${TARANTOOL_INSTALL_LIBDIR}") 44 | message(STATUS "Tarantool BINDIR is ${TARANTOOL_INSTALL_BINDIR}") 45 | 46 | endif () 47 | endif() 48 | mark_as_advanced(TARANTOOL_INCLUDE_DIRS TARANTOOL_INSTALL_LIBDIR 49 | TARANTOOL_INSTALL_LUADIR 50 | TARANTOOL_INSTALL_BINDIR) 51 | -------------------------------------------------------------------------------- /test/malformed_args_test.lua: -------------------------------------------------------------------------------- 1 | local fio = require('fio') 2 | local t = require('luatest') 3 | 4 | local g = t.group() 5 | local Server = t.Server 6 | 7 | local root = fio.dirname(fio.abspath('test.helpers')) 8 | local datadir = fio.pathjoin(root, 'tmp', 'malformed_args') 9 | local command = fio.pathjoin(root, 'test', 'server_instance.lua') 10 | 11 | g.before_all(function() 12 | fio.rmtree(datadir) 13 | 14 | local log = fio.pathjoin(datadir, 'malformed_args_server.log') 15 | g.server = Server:new({ 16 | command = command, 17 | workdir = datadir, 18 | env = { 19 | TARANTOOL_LOG = log 20 | }, 21 | http_port = 8186, 22 | net_box_port = 3139, 23 | }) 24 | fio.mktree(g.server.workdir) 25 | 26 | g.server:start() 27 | t.helpers.retrying({timeout = 2}, function() 28 | g.server:http_request('get', '/ping') 29 | end) 30 | 31 | g.server:connect_net_box() 32 | end) 33 | 34 | g.after_all(function() 35 | g.server:drop() 36 | fio.rmtree(datadir) 37 | end) 38 | 39 | g.test_exec_correct_args = function() 40 | local a = g.server:exec(function(a, b) return a + b end, {1, 1}) 41 | t.assert_equals(a, 2) 42 | end 43 | 44 | g.test_exec_no_args = function() 45 | local a = g.server:exec(function() return 1 + 1 end) 46 | t.assert_equals(a, 2) 47 | end 48 | 49 | g.test_exec_specific_args = function() 50 | -- nil 51 | local a = g.server:exec(function(a) return a end) 52 | t.assert_equals(a, nil) 53 | 54 | -- too few args 55 | local b, c = g.server:exec(function(b, c) return b, c end, {1}) 56 | t.assert_equals(b, 1) 57 | t.assert_equals(c, nil) 58 | 59 | -- too many args 60 | local d = g.server:exec(function(d) return d end, {1, 2}) 61 | t.assert_equals(d, 1) 62 | end 63 | 64 | g.test_exec_non_array_args = function() 65 | local function f1() 66 | g.server:exec(function(a, b, c) return a, b, c end, {a="a", 2, 3}) 67 | end 68 | 69 | local function f2() 70 | g.server:exec(function(a, b, c) return a, b, c end, {1, a="a", 2}) 71 | end 72 | 73 | local function f3() 74 | g.server:exec(function(a, b, c) return a, b, c end, {1, 2, a="a"}) 75 | end 76 | 77 | t.assert_error_msg_contains("bad argument #3 for exec at malformed_args_test.lua:66:", f1) 78 | t.assert_error_msg_contains("bad argument #3 for exec at malformed_args_test.lua:70:", f2) 79 | t.assert_error_msg_contains("bad argument #3 for exec at malformed_args_test.lua:74:", f3) 80 | end 81 | -------------------------------------------------------------------------------- /test/helpers_test.lua: -------------------------------------------------------------------------------- 1 | local t = require('luatest') 2 | local g = t.group() 3 | 4 | local helpers = t.helpers 5 | 6 | g.test_uuid = function() 7 | t.assert_equals(helpers.uuid('a'), 'aaaaaaaa-0000-0000-0000-000000000000') 8 | t.assert_equals(helpers.uuid('ab', 1), 'abababab-0000-0000-0000-000000000001') 9 | t.assert_equals(helpers.uuid(1, 2, 3), '00000001-0002-0000-0000-000000000003') 10 | t.assert_equals(helpers.uuid('1', '2', '3'), '11111111-2222-0000-0000-333333333333') 11 | t.assert_equals(helpers.uuid('12', '34', '56', '78', '90'), '12121212-3434-5656-7878-909090909090') 12 | end 13 | 14 | g.test_rescuing = function() 15 | local retry = 0 16 | local result = helpers.retrying({}, function(a, b) 17 | t.assert_equals(a, 1) 18 | t.assert_equals(b, 2) 19 | retry = retry + 1 20 | if (retry < 3) then 21 | error('test') 22 | end 23 | return 'result' 24 | end, 1, 2) 25 | t.assert_equals(retry, 3) 26 | t.assert_equals(result, result) 27 | end 28 | 29 | g.test_rescuing_failure = function() 30 | local retry = 0 31 | t.assert_error_msg_equals('test-error', function() 32 | helpers.retrying({delay = 0.1, timeout = 0.5}, function(a, b) 33 | t.assert_equals(a, 1) 34 | t.assert_equals(b, 2) 35 | retry = retry + 1 36 | error('test-error', 0) 37 | end, 1, 2) 38 | end) 39 | t.assert_almost_equals(retry, 6, 1) 40 | end 41 | 42 | g.test_matrix = function() 43 | t.assert_equals(t.helpers.matrix({}), {{}}) 44 | t.assert_equals(t.helpers.matrix({a = {1}}), {{a = 1}}) 45 | t.assert_equals(t.helpers.matrix({a = {1, 2}}), {{a = 1}, {a = 2}}) 46 | t.assert_equals(t.helpers.matrix({a = {1}, b = {2}}), {{a = 1, b = 2}}) 47 | 48 | t.assert_equals(t.helpers.matrix({a = {1, 3}, b = {{2}, {4}}}), { 49 | {a = 1, b = {2}}, 50 | {a = 3, b = {2}}, 51 | {a = 1, b = {4}}, 52 | {a = 3, b = {4}}, 53 | }) 54 | 55 | t.assert_equals(t.helpers.matrix({a = {1, 3}, b = {2, 4}, c = {5, 6}}), { 56 | {a = 1, b = 2, c = 5}, 57 | {a = 3, b = 2, c = 5}, 58 | {a = 1, b = 4, c = 5}, 59 | {a = 3, b = 4, c = 5}, 60 | {a = 1, b = 2, c = 6}, 61 | {a = 3, b = 2, c = 6}, 62 | {a = 1, b = 4, c = 6}, 63 | {a = 3, b = 4, c = 6}, 64 | }) 65 | 66 | t.assert_equals(t.helpers.matrix({{1, 3}, {2, 4}}), { 67 | {1, 2}, 68 | {3, 2}, 69 | {1, 4}, 70 | {3, 4}, 71 | }) 72 | end 73 | -------------------------------------------------------------------------------- /test/boxcfg_interaction_test.lua: -------------------------------------------------------------------------------- 1 | local fio = require('fio') 2 | local t = require('luatest') 3 | 4 | local g = t.group() 5 | local Server = t.Server 6 | 7 | local root = fio.dirname(fio.abspath('test.helpers')) 8 | local datadir = fio.pathjoin(root, 'tmp', 'boxcfg_interaction') 9 | local command = fio.pathjoin(root, 'test', 'server_instance.lua') 10 | 11 | g.before_all(function() 12 | fio.rmtree(datadir) 13 | 14 | local workdir = fio.tempdir() 15 | local log = fio.pathjoin(workdir, 'boxcfg_interaction.log') 16 | 17 | g.server = Server:new({ 18 | command = command, 19 | workdir = fio.pathjoin(datadir, 'boxcfg_interaction'), 20 | env = { 21 | TARANTOOL_LOG = log 22 | }, 23 | box_cfg = {read_only = false}, 24 | http_port = 8187, 25 | net_box_port = 3138, 26 | }) 27 | fio.mktree(g.server.workdir) 28 | 29 | g.server:start() 30 | t.helpers.retrying({timeout = 2}, function() 31 | g.server:http_request('get', '/ping') 32 | end) 33 | 34 | g.server:connect_net_box() 35 | end) 36 | 37 | g.after_all(function() 38 | g.server:drop() 39 | fio.rmtree(datadir) 40 | end) 41 | 42 | g.test_update_box_cfg = function() 43 | g.server:update_box_cfg{read_only = true} 44 | 45 | local c = g.server:exec(function() return box.cfg end) 46 | 47 | t.assert_type(c, 'table') 48 | t.assert_equals(c.read_only, true) 49 | t.assert( 50 | g.server:grep_log( 51 | "I> set 'read_only' configuration option to true" 52 | ) 53 | ) 54 | end 55 | 56 | g.test_update_box_cfg_multiple_parameters = function() 57 | g.server:update_box_cfg{checkpoint_count = 5, replication_timeout = 2} 58 | 59 | local c = g.server:exec(function() return box.cfg end) 60 | 61 | t.assert_type(c, 'table') 62 | 63 | t.assert_equals(c.checkpoint_count, 5) 64 | t.assert( 65 | g.server:grep_log( 66 | "I> set 'checkpoint_count' configuration option to 5" 67 | ) 68 | ) 69 | 70 | t.assert_equals(c.replication_timeout, 2) 71 | t.assert( 72 | g.server:grep_log( 73 | "I> set 'replication_timeout' configuration option to 2" 74 | ) 75 | ) 76 | end 77 | 78 | g.test_update_box_cfg_bad_type = function() 79 | local function foo() 80 | g.server:update_box_cfg(1) 81 | end 82 | t.assert_error_msg_contains( 83 | 'bad argument #2 to update_box_cfg (table expected, got number)', foo) 84 | 85 | end 86 | 87 | g.test_get_box_cfg = function() 88 | local cfg1 = g.server:get_box_cfg() 89 | local cfg2 = g.server:exec(function() return box.cfg end) 90 | 91 | t.assert_equals(cfg1, cfg2) 92 | end 93 | -------------------------------------------------------------------------------- /test/class_test.lua: -------------------------------------------------------------------------------- 1 | local t = require('luatest') 2 | local g = t.group() 3 | 4 | local Class = require('luatest.class') 5 | 6 | g.test_new_class = function() 7 | local class = Class.new() 8 | function class.mt:get_a() return self.a end 9 | local instance = class:from({a = 123}) 10 | t.assert_equals(getmetatable(instance), class.mt) 11 | t.assert_equals(instance.class, class) 12 | t.assert_equals(instance, {a = 123}) 13 | t.assert_equals(instance:get_a(), 123) 14 | end 15 | 16 | g.test_inheritance = function() 17 | local Parent = Class.new() 18 | Parent.X = 'XX' 19 | function Parent:get_x() return self.X end 20 | function Parent.mt:get_a() return self.a end 21 | local Child = Parent:new_class() 22 | Child.Y = 'YY' 23 | function Child:get_y() return self.Y end 24 | function Child.mt:get_b() return self.b end 25 | 26 | t.assert_equals(Child.super, Parent) 27 | t.assert_equals(Child:get_x(), 'XX') 28 | t.assert_equals(Child:get_y(), 'YY') 29 | t.assert_equals(Parent:get_x(), 'XX') 30 | t.assert_equals(Parent.get_y, nil) 31 | 32 | local instance = Child:from({a = 123, b = 456}) 33 | t.assert_equals(getmetatable(instance), Child.mt) 34 | t.assert_equals(instance.class, Child) 35 | t.assert_equals(instance, {a = 123, b = 456}) 36 | t.assert_equals(instance:get_a(), 123) 37 | t.assert_equals(instance:get_b(), 456) 38 | end 39 | 40 | g.test_super = function() 41 | local Parent = Class.new() 42 | Parent.X = 'XX' 43 | function Parent:get_x() return self.X end 44 | function Parent.mt:get_a() return self.a end 45 | local Child = Parent:new_class({X = 'YY'}) 46 | function Child:get_x() return Child.super.get_x(self) .. '!' end 47 | function Child.mt:get_a() return Child.super.mt.get_a(self) * 2 end 48 | 49 | local instance = Child:from({a = 123}) 50 | t.assert_equals(Child:get_x(), 'YY!') 51 | t.assert_equals(instance:get_a(), 246) 52 | 53 | local Grandchild = Child:new_class({X = 'YY'}) 54 | function Grandchild:get_x() return Grandchild.super.get_x(self) .. '?' end 55 | function Grandchild.mt:get_a() return Grandchild.super.mt.get_a(self) * 3 end 56 | instance = Grandchild:from({a = 2}) 57 | t.assert_equals(Grandchild:get_x(), 'YY!?') 58 | t.assert_equals(instance:get_a(), 12) 59 | end 60 | 61 | g.test_new_and_from = function() 62 | local class = Class.new() 63 | function class.mt:initialize(a, b) 64 | self.a = a 65 | self.b = b 66 | end 67 | 68 | t.assert_equals(class:new(1, 2, 3), {a = 1, b = 2}) 69 | t.assert_equals(class:new(3), {a = 3}) 70 | 71 | local source = {a = 11, c = 33} 72 | local instance = class:from(source, 1, 2, 3) 73 | t.assert_equals(instance, {a = 1, b = 2, c = 33}) 74 | t.assert_is(instance, source) 75 | end 76 | -------------------------------------------------------------------------------- /test/proxy_test.lua: -------------------------------------------------------------------------------- 1 | local t = require('luatest') 2 | local proxy = require('luatest.replica_proxy') 3 | local utils = require('luatest.utils') 4 | local replica_set = require('luatest.replica_set') 5 | local server = require('luatest.server') 6 | 7 | local fiber = require('fiber') 8 | 9 | local g = t.group('proxy-version-check') 10 | 11 | g.test_proxy_errors = function() 12 | t.skip_if(utils.version_current_ge_than(2, 10, 1), 13 | "Proxy works on Tarantool 2.10.1+, nothing to test") 14 | t.assert_error_msg_contains('Proxy requires Tarantool 2.10.1 and newer', 15 | proxy.new, proxy, { 16 | client_socket_path = 'somepath', 17 | server_socket_path = 'somepath' 18 | }) 19 | end 20 | 21 | local g1 = t.group('proxy', { 22 | {is_paused = true}, 23 | {is_paused = false} 24 | }) 25 | 26 | g1.before_all(function(cg) 27 | -- Proxy only works on tarantool 2.10+ 28 | t.run_only_if(utils.version_current_ge_than(2, 10, 1), 29 | [[Proxy works on Tarantool 2.10.1+. 30 | See tarantool/tarantool@57ecb6cd90b4 for details]]) 31 | cg.rs = replica_set:new{} 32 | cg.box_cfg = { 33 | replication_timeout = 0.1, 34 | replication = { 35 | server.build_listen_uri('server2_proxy', cg.rs.id), 36 | }, 37 | } 38 | cg.server1 = cg.rs:build_and_add_server{ 39 | alias = 'server1', 40 | box_cfg = cg.box_cfg, 41 | } 42 | cg.box_cfg.replication = nil 43 | cg.server2 = cg.rs:build_and_add_server{ 44 | alias = 'server2', 45 | box_cfg = cg.box_cfg, 46 | } 47 | cg.proxy = proxy:new{ 48 | client_socket_path = server.build_listen_uri('server2_proxy', cg.rs.id), 49 | server_socket_path = server.build_listen_uri('server2', cg.rs.id), 50 | } 51 | t.assert(cg.proxy:start{force = true}, 'Proxy is started') 52 | cg.rs:start{} 53 | end) 54 | 55 | g1.test_server_disconnect_is_noticed = function(cg) 56 | local id = cg.server2:get_instance_id() 57 | t.helpers.retrying({}, cg.server1.assert_follows_upstream, cg.server1, id) 58 | if cg.params.is_paused then 59 | cg.proxy:pause() 60 | end 61 | cg.server2:stop() 62 | fiber.sleep(cg.box_cfg.replication_timeout) 63 | local upstream = cg.server1:exec(function(upstream_id) 64 | return box.info.replication[upstream_id].upstream 65 | end, {id}) 66 | if cg.params.is_paused then 67 | t.assert_equals(upstream.status, 'follow', 68 | 'Server disconnect is not noticed') 69 | else 70 | t.assert_equals(upstream.system_message, 'Broken pipe', 71 | 'Server disconnect is noticed') 72 | end 73 | if cg.params.is_paused then 74 | cg.proxy:resume() 75 | end 76 | cg.server2:start() 77 | end 78 | 79 | g1.after_all(function(cg) 80 | cg.rs:drop() 81 | end) 82 | -------------------------------------------------------------------------------- /luatest/output/generic.lua: -------------------------------------------------------------------------------- 1 | -- Base output class. 2 | local Output = require('luatest.class').new({ 3 | VERBOSITY = { 4 | DEFAULT = 10, 5 | QUIET = 0, 6 | LOW = 1, 7 | VERBOSE = 20, 8 | REPEAT = 21, 9 | }, 10 | }) 11 | 12 | function Output.mt:initialize(runner) 13 | self.runner = runner 14 | self.result = runner.result 15 | self.verbosity = runner.verbosity 16 | end 17 | 18 | -- luacheck: push no unused 19 | -- abstract ("empty") methods 20 | function Output.mt:start_suite() 21 | -- Called once, when the suite is started 22 | end 23 | 24 | function Output.mt:start_group(group) 25 | -- Called each time a new test group is started 26 | end 27 | 28 | function Output.mt:start_test(test) 29 | -- called each time a new test is started, right before the setUp() 30 | end 31 | 32 | function Output.mt:update_status(node) 33 | -- called with status failed or error as soon as the error/failure is encountered 34 | -- this method is NOT called for a successful test because a test is marked as successful by default 35 | -- and does not need to be updated 36 | end 37 | 38 | function Output.mt:end_test(node) 39 | -- called when the test is finished, after the tearDown() method 40 | end 41 | 42 | function Output.mt:end_group(group) 43 | -- called when executing the group is finished, before moving on to the next group 44 | -- of at the end of the test execution 45 | end 46 | 47 | function Output.mt:end_suite() 48 | -- called at the end of the test suite execution 49 | end 50 | -- luacheck: pop 51 | 52 | function Output.mt:status_line(colors) 53 | colors = colors or {success = '', failure = '', reset = '', xfail = ''} 54 | -- return status line string according to results 55 | local tests = self.result.tests 56 | local s = { 57 | string.format('Ran %d tests in %0.3f seconds', #tests.all - #tests.skip, self.result.duration), 58 | string.format("%s%d %s%s", colors.success, #tests.success, 'succeeded', colors.reset), 59 | } 60 | if #tests.xfail > 0 then 61 | table.insert(s, string.format("%s%d %s%s", colors.xfail, #tests.xfail, 'xfailed', colors.reset)) 62 | end 63 | if #tests.xsuccess > 0 then 64 | table.insert(s, string.format("%s%d %s%s", colors.failure, #tests.xsuccess, 'xsucceeded', colors.reset)) 65 | end 66 | if #tests.fail > 0 then 67 | table.insert(s, string.format("%s%d %s%s", colors.failure, #tests.fail, 'failed', colors.reset)) 68 | end 69 | if #tests.error > 0 then 70 | table.insert(s, string.format("%s%d %s%s", colors.failure, #tests.error, 'errored', colors.reset)) 71 | end 72 | if #tests.fail == 0 and #tests.error == 0 and #tests.xsuccess == 0 then 73 | table.insert(s, '0 failed') 74 | end 75 | if #tests.skip > 0 then 76 | table.insert(s, string.format("%d skipped", #tests.skip)) 77 | end 78 | if self.result.not_selected_count > 0 then 79 | table.insert(s, string.format("%d not selected", self.result.not_selected_count)) 80 | end 81 | return table.concat(s, ', ') 82 | end 83 | 84 | return Output 85 | -------------------------------------------------------------------------------- /luatest/parametrizer.lua: -------------------------------------------------------------------------------- 1 | local checks = require('checks') 2 | 3 | local Group = require('luatest.group') 4 | local pp = require('luatest.pp') 5 | 6 | local export = {} 7 | 8 | local function get_group_name(params) 9 | local params_names = {} 10 | for name, _ in pairs(params) do 11 | table.insert(params_names, name) 12 | end 13 | table.sort(params_names) 14 | 15 | for ind, param_name in ipairs(params_names) do 16 | params_names[ind] = param_name .. ':' .. pp.tostring(params[param_name]) 17 | end 18 | return table.concat(params_names, ".") 19 | end 20 | 21 | local function redefine_hooks(group, hooks_type) 22 | -- Super group shares its hooks with pgroups 23 | for _, pgroup in ipairs(group.pgroups) do 24 | pgroup[hooks_type .. '_hooks'] = group[hooks_type .. '_hooks'] 25 | end 26 | end 27 | 28 | local function redefine_pgroups_hooks(group) 29 | redefine_hooks(group, 'before_each') 30 | redefine_hooks(group, 'after_each') 31 | redefine_hooks(group, 'before_all') 32 | redefine_hooks(group, 'after_all') 33 | 34 | redefine_hooks(group, 'before_test') 35 | redefine_hooks(group, 'after_test') 36 | end 37 | 38 | local function redirect_index(group) 39 | local super_group_mt = getmetatable(group) 40 | local origin_newindex = super_group_mt.__newindex 41 | super_group_mt.__newindex = function(_group, key, value) 42 | if _group.pgroups then 43 | for _, pgroup in ipairs(_group.pgroups) do 44 | pgroup[key] = value 45 | end 46 | end 47 | 48 | if origin_newindex then 49 | return origin_newindex(_group, key, value) 50 | end 51 | 52 | rawset(_group, key, value) 53 | end 54 | end 55 | 56 | function export.parametrize(object, parameters_combinations) 57 | checks('table', 'table') 58 | -- Validate params' name and values 59 | local counter = 0 60 | for _, _ in pairs(parameters_combinations) do 61 | counter = counter + 1 62 | assert(parameters_combinations[counter] ~= nil, 63 | 'parameters_combinations should be a contiguous array') 64 | 65 | assert(type(parameters_combinations[counter]) == 'table', 66 | string.format('parameters_combinations\' entry should be table, got %s', 67 | type(parameters_combinations[counter]))) 68 | 69 | for parameter_name, _ in pairs(parameters_combinations[counter]) do 70 | assert(type(parameter_name) == 'string', 71 | string.format('parameter name should be string, got %s', type(parameter_name))) 72 | end 73 | end 74 | 75 | -- Create a subgroup on every param combination 76 | object.pgroups = {} 77 | for _, pgroup_params in ipairs(parameters_combinations) do 78 | local pgroup_name = get_group_name(pgroup_params) 79 | local pgroup = Group:new(object.name .. '.' .. pgroup_name) 80 | pgroup.params = pgroup_params 81 | 82 | pgroup.super_group = object 83 | table.insert(object.pgroups, pgroup) 84 | end 85 | 86 | redirect_index(object) 87 | redefine_pgroups_hooks(object) 88 | end 89 | 90 | return export 91 | -------------------------------------------------------------------------------- /test/justrun_test.lua: -------------------------------------------------------------------------------- 1 | local t = require('luatest') 2 | local fio = require('fio') 3 | 4 | local justrun = require('luatest.justrun') 5 | local utils = require('luatest.utils') 6 | 7 | local g = t.group() 8 | 9 | g.before_each(function() 10 | g.tempdir = fio.tempdir() 11 | g.tempfile = fio.pathjoin(g.tempdir, 'main.lua') 12 | 13 | local default_flags = {'O_CREAT', 'O_WRONLY', 'O_TRUNC'} 14 | local default_mode = tonumber('644', 8) 15 | 16 | g.tempfile_fh = fio.open(g.tempfile, default_flags, default_mode) 17 | end) 18 | 19 | g.after_each(function() 20 | fio.rmdir(g.tempdir) 21 | end) 22 | 23 | g.before_test('test_stdout_stderr_output', function() 24 | g.tempfile_fh:write([[ 25 | local log = require('log') 26 | 27 | print('hello stdout!') 28 | log.info('hello stderr!') 29 | ]]) 30 | end) 31 | 32 | g.test_stdout_stderr_output = function() 33 | t.skip_if(not utils.version_current_ge_than(2, 4, 1), 34 | "popen module is available since Tarantool 2.4.1.") 35 | local res = justrun.tarantool(g.tempdir, {}, {g.tempfile}, {nojson = true, stderr = true}) 36 | 37 | t.assert_equals(res.exit_code, 0) 38 | t.assert_str_contains(res.stdout, 'hello stdout!') 39 | t.assert_str_contains(res.stderr, 'hello stderr!') 40 | end 41 | 42 | g.before_test('test_decode_stdout_as_json', function() 43 | g.tempfile_fh:write([[ 44 | print('{"a": 1, "b": 2}') 45 | ]]) 46 | end) 47 | 48 | g.test_decode_stdout_as_json = function() 49 | t.skip_if(not utils.version_current_ge_than(2, 4, 1), 50 | "popen module is available since Tarantool 2.4.1.") 51 | local res = justrun.tarantool(g.tempdir, {}, {g.tempfile}, {nojson = false, stdout = true}) 52 | 53 | t.assert_equals(res.exit_code, 0) 54 | t.assert_equals(res.stdout, {{ a = 1, b = 2}}) 55 | end 56 | 57 | g.before_test('test_bad_exit_code', function() 58 | g.tempfile_fh:write([[ 59 | local magic = require('magic_lib') 60 | ]]) 61 | end) 62 | 63 | g.test_bad_exit_code = function() 64 | t.skip_if(not utils.version_current_ge_than(2, 4, 1), 65 | "popen module is available since Tarantool 2.4.1.") 66 | local res = justrun.tarantool(g.tempdir, {}, {g.tempfile}, {nojson = true, stderr = true}) 67 | 68 | t.assert_equals(res.exit_code, 1) 69 | 70 | t.assert_str_contains(res.stderr, "module 'magic_lib' not found") 71 | t.assert_equals(res.stdout, nil) 72 | end 73 | 74 | g.test_error_when_popen_is_not_available = function() 75 | -- Substitute `require` function to test the behavior of `justrun.tarantool` 76 | -- if the `popen` module is not available (on versions below 2.4.1). 77 | 78 | -- luacheck: push ignore 121 79 | local old = require 80 | require = function(name) -- ignore: 81 | if name == 'popen' then 82 | return error("module " .. name .. " not found:") 83 | else 84 | return old(name) 85 | end 86 | end 87 | 88 | local _, err = pcall(justrun.tarantool, g.tempdir, {}, {g.tempfile}, {nojson = true}) 89 | 90 | t.assert_str_contains(err, 'module popen not found:') 91 | 92 | require = old 93 | -- luacheck: pop 94 | end 95 | -------------------------------------------------------------------------------- /luatest/init.lua: -------------------------------------------------------------------------------- 1 | --- 2 | -- @module luatest 3 | local luatest = setmetatable({}, {__index = require('luatest.assertions')}) 4 | 5 | luatest.Process = require('luatest.process') 6 | luatest.VERSION = require('luatest.VERSION') 7 | 8 | --- Module to manage connection to Tarantool replication instances via proxy. 9 | -- 10 | -- @see luatest.replica_proxy 11 | luatest.replica_proxy = require('luatest.replica_proxy') 12 | 13 | --- Module with collection of test helpers related to Tarantool instance. 14 | -- 15 | -- @see luatest.tarantool 16 | luatest.tarantool = require('luatest.tarantool') 17 | 18 | --- Helpers. 19 | -- 20 | -- @see luatest.helpers 21 | luatest.helpers = require('luatest.helpers') 22 | 23 | --- Class to manage Tarantool instances. 24 | -- 25 | -- @see luatest.server 26 | luatest.Server = require('luatest.server') 27 | 28 | --- Class to manage groups of Tarantool instances with the same data set. 29 | -- 30 | -- @see luatest.replica_set 31 | luatest.ReplicaSet = require('luatest.replica_set') 32 | 33 | local Group = require('luatest.group') 34 | local hooks = require('luatest.hooks') 35 | local parametrizer = require('luatest.parametrizer') 36 | 37 | --- Add syntax sugar for logging. 38 | -- 39 | luatest.log = require('luatest.log') 40 | 41 | --- Simple Tarantool runner and output catcher. 42 | -- 43 | -- @see luatest.justrun 44 | luatest.justrun = require('luatest.justrun') 45 | 46 | --- Declarative configuration builder helper. 47 | -- 48 | -- @see luatest.cbuilder 49 | luatest.cbuilder = require('luatest.cbuilder') 50 | 51 | --- Tarantool cluster management utils. 52 | -- 53 | -- @see luatest.cluster 54 | luatest.cluster = require('luatest.cluster') 55 | 56 | --- Add before suite hook. 57 | -- 58 | -- @function before_suite 59 | -- @func fn 60 | 61 | --- Add after suite hook. 62 | -- 63 | -- @function after_suite 64 | -- @func fn 65 | hooks._define_suite_hooks(luatest) 66 | 67 | --- Add extra hooks methods. 68 | -- 69 | -- @see luatest.hooks 70 | luatest.hooks = hooks 71 | 72 | luatest.groups = {} 73 | 74 | --- Create group of tests. 75 | -- 76 | -- @string[opt] name 77 | -- @table[opt] params 78 | -- @return Group object 79 | -- @see luatest.group 80 | function luatest.group(name, params) 81 | local group = Group:new(name) 82 | name = group.name 83 | if luatest.groups[name] then 84 | error('Test group already exists: ' .. name ..'.') 85 | end 86 | 87 | if params then 88 | parametrizer.parametrize(group, params) 89 | end 90 | 91 | -- Register all parametrized groups 92 | if group.pgroups then 93 | for _, pgroup in ipairs(group.pgroups) do 94 | luatest.groups[pgroup.name] = pgroup 95 | end 96 | else 97 | luatest.groups[name] = group 98 | end 99 | 100 | return group 101 | end 102 | 103 | local runner_config = {} 104 | 105 | --- Update default options. 106 | -- See @{luatest.runner:run} for the list of available options. 107 | -- 108 | -- @tab[opt={}] options list of options to update 109 | -- @return options after update 110 | function luatest.configure(options) 111 | for k, v in pairs(options or {}) do 112 | runner_config[k] = v 113 | end 114 | return runner_config 115 | end 116 | 117 | function luatest.defaults(...) 118 | require('log').warn('luatest.defaults is deprecated in favour of luatest.configure') 119 | return luatest.configure(...) 120 | end 121 | 122 | return luatest 123 | -------------------------------------------------------------------------------- /test/autorequire_luatest_test.lua: -------------------------------------------------------------------------------- 1 | local fio = require('fio') 2 | local t = require('luatest') 3 | 4 | local g = t.group() 5 | local Server = t.Server 6 | 7 | local root = fio.dirname(fio.abspath('test.helpers')) 8 | local datadir = fio.pathjoin(root, 'tmp', 'luatest_module') 9 | local command = fio.pathjoin(root, 'test', 'server_instance.lua') 10 | 11 | g.before_all(function() 12 | fio.rmtree(datadir) 13 | 14 | g.server = Server:new({ 15 | command = command, 16 | workdir = fio.pathjoin(datadir, 'common'), 17 | env = { 18 | LUA_PATH = root .. '/?.lua;' .. 19 | root .. '/?/init.lua;' .. 20 | root .. '/.rocks/share/tarantool/?.lua' 21 | }, 22 | http_port = 8182, 23 | net_box_port = 3133, 24 | }) 25 | fio.mktree(g.server.workdir) 26 | 27 | g.server:start() 28 | t.helpers.retrying({timeout = 2}, function() 29 | g.server:http_request('get', '/ping') 30 | end) 31 | 32 | g.server:connect_net_box() 33 | end) 34 | 35 | g.after_all(function() 36 | g.server:stop() 37 | fio.rmtree(datadir) 38 | end) 39 | 40 | g.test_exec_without_upvalue = function() 41 | local actual = g.server:exec(function() 42 | return 1 + 1 43 | end) 44 | t.assert_equals(actual, 2) 45 | end 46 | 47 | g.test_exec_with_upvalue = function() 48 | g.server:exec(function() 49 | t.assert_equals(1, 1) 50 | end) 51 | t.assert_equals(1, 1) 52 | 53 | local lt = require('luatest') 54 | g.server:exec(function() 55 | lt.assert_equals(1, 1) 56 | end) 57 | lt.assert_equals(1, 1) 58 | end 59 | 60 | g.test_exec_with_local_variable = function() 61 | g.server:exec(function() 62 | local t = require('luatest') -- luacheck: ignore 431 63 | t.assert_equals(1, 1) 64 | end) 65 | t.assert_equals(1, 1) 66 | end 67 | 68 | g.test_exec_with_upvalue_and_local_variable = function() 69 | g.server:exec(function() 70 | local tt = require('luatest') 71 | t.assert_equals(1, 1) 72 | tt.assert_equals(1, 1) 73 | t.assert_equals(tt, t) 74 | end) 75 | end 76 | 77 | g.before_test('test_exec_when_luatest_not_found', function() 78 | -- Setup custom server without luatest in LUA_PATH. 79 | g.bad_env_server = Server:new({ 80 | command = command, 81 | workdir = fio.tempdir(), 82 | http_port = 8183, 83 | net_box_port = 3134, 84 | env = { 85 | LUA_PATH = '', 86 | }, 87 | }) 88 | 89 | fio.mktree(g.bad_env_server.workdir) 90 | 91 | g.bad_env_server:start() 92 | 93 | t.helpers.retrying({timeout = 2}, function() 94 | g.bad_env_server:http_request('get', '/ping') 95 | end) 96 | 97 | g.bad_env_server:connect_net_box() 98 | end) 99 | 100 | g.test_exec_when_luatest_not_found = function() 101 | t.assert_error_msg_contains( 102 | "module 'luatest' not found:", g.bad_env_server.exec, g.bad_env_server, 103 | function() t.assert_equals(1, 1) end 104 | ) 105 | end 106 | 107 | g.after_test('test_exec_when_luatest_not_found', function() 108 | g.bad_env_server:drop() 109 | end) 110 | 111 | g.test_exec_with_sparse_output = function() 112 | local res1, res2 = g.server:exec(function() 113 | return nil, 'some error' 114 | end) 115 | 116 | t.assert_equals(res1, nil) 117 | t.assert_equals(res2, 'some error') 118 | end 119 | -------------------------------------------------------------------------------- /luatest/sorted_pairs.lua: -------------------------------------------------------------------------------- 1 | local crossTypeOrdering = { 2 | number = 1, boolean = 2, string = 3, table = 4, other = 5 3 | } 4 | 5 | local crossTypeComparison = { 6 | number = function(a, b) return a < b end, 7 | string = function(a, b) return a < b end, 8 | other = function(a, b) return tostring(a) < tostring(b) end, 9 | } 10 | 11 | local function cross_type_sort(a, b) 12 | local type_a, type_b = type(a), type(b) 13 | if type_a == type_b then 14 | local func = crossTypeComparison[type_a] or crossTypeComparison.other 15 | return func(a, b) 16 | end 17 | type_a = crossTypeOrdering[type_a] or crossTypeOrdering.other 18 | type_b = crossTypeOrdering[type_b] or crossTypeOrdering.other 19 | return type_a < type_b 20 | end 21 | 22 | -- Returns a sequence consisting of t's keys, sorted. 23 | local function gen_index(t) 24 | local sortedIndex = {} 25 | 26 | for key,_ in pairs(t) do 27 | table.insert(sortedIndex, key) 28 | end 29 | 30 | table.sort(sortedIndex, cross_type_sort) 31 | return sortedIndex 32 | end 33 | 34 | -- Equivalent of the next() function of table iteration, but returns the 35 | -- keys in sorted order (see __gen_sorted_index and cross_type_sort). 36 | -- The state is a temporary variable during iteration and contains the 37 | -- sorted key table (state.sortedIdx). It also stores the last index (into 38 | -- the keys) used by the iteration, to find the next one quickly. 39 | local function next(state, control) 40 | local key 41 | 42 | -- print("next: control = "..tostring(control)) 43 | if control == nil then 44 | -- start of iteration 45 | state.count = #state.sortedIdx 46 | state.lastIdx = 1 47 | key = state.sortedIdx[1] 48 | return key, state.t[key] 49 | end 50 | 51 | -- normally, we expect the control variable to match the last key used 52 | if control ~= state.sortedIdx[state.lastIdx] then 53 | -- strange, we have to find the next value by ourselves 54 | -- the key table is sorted in cross_type_sort() order! -> use bisection 55 | local lower, upper = 1, state.count 56 | repeat 57 | state.lastIdx = math.modf((lower + upper) / 2) 58 | key = state.sortedIdx[state.lastIdx] 59 | if key == control then 60 | break -- key found (and thus prev index) 61 | end 62 | if cross_type_sort(key, control) then 63 | -- key < control, continue search "right" (towards upper bound) 64 | lower = state.lastIdx + 1 65 | else 66 | -- key > control, continue search "left" (towards lower bound) 67 | upper = state.lastIdx - 1 68 | end 69 | until lower > upper 70 | if lower > upper then -- only true if the key wasn't found, ... 71 | state.lastIdx = state.count -- ... so ensure no match in code below 72 | end 73 | end 74 | 75 | -- proceed by retrieving the next value (or nil) from the sorted keys 76 | state.lastIdx = state.lastIdx + 1 77 | key = state.sortedIdx[state.lastIdx] 78 | if key then 79 | return key, state.t[key] 80 | end 81 | 82 | -- getting here means returning `nil`, which will end the iteration 83 | end 84 | 85 | -- Equivalent of the pairs() function on tables. Allows to iterate in 86 | -- sorted order. As required by "generic for" loops, this will return the 87 | -- iterator (function), an "invariant state", and the initial control value. 88 | -- (see http://www.lua.org/pil/7.2.html) 89 | return function(tbl) 90 | return next, {t = tbl, sortedIdx = gen_index(tbl)}, nil 91 | end 92 | -------------------------------------------------------------------------------- /test/collect_rs_artifacts_test.lua: -------------------------------------------------------------------------------- 1 | local fio = require('fio') 2 | local t = require('luatest') 3 | local utils = require('luatest.utils') 4 | local ReplicaSet = require('luatest.replica_set') 5 | 6 | local g = t.group() 7 | local Server = t.Server 8 | 9 | local function build_specific_replica_set(alias_suffix) 10 | local rs = ReplicaSet:new() 11 | local box_cfg = { 12 | replication_timeout = 0.1, 13 | replication_connect_timeout = 1, 14 | replication_sync_lag = 0.01, 15 | replication_connect_quorum = 3, 16 | } 17 | 18 | local s1_alias = ('replica1-%s'):format(alias_suffix) 19 | local s2_alias = ('replica2-%s'):format(alias_suffix) 20 | local s3_alias = ('replica3-%s'):format(alias_suffix) 21 | 22 | box_cfg = utils.merge( 23 | table.deepcopy(box_cfg), 24 | { 25 | replication ={ 26 | Server.build_listen_uri(s1_alias, rs.id), 27 | Server.build_listen_uri(s2_alias, rs.id), 28 | Server.build_listen_uri(s3_alias, rs.id) 29 | }}) 30 | 31 | rs:build_and_add_server({alias = s1_alias, box_cfg = box_cfg}) 32 | rs:build_and_add_server({alias = s2_alias, box_cfg = box_cfg}) 33 | rs:build_and_add_server({alias = s3_alias, box_cfg = box_cfg}) 34 | return rs 35 | end 36 | 37 | local function get_replica_set_artifacts_path(rs) 38 | return ('%s/artifacts/%s'):format(rs._server.vardir, rs.id) 39 | end 40 | 41 | local function get_server_artifacts_path_by_alias(rs, position, alias_node) 42 | local rs_artifacts = get_replica_set_artifacts_path(rs) 43 | return ('%s/%s'):format( 44 | rs_artifacts, 45 | rs:get_server(('replica%s-%s'):format(position, alias_node)).id) 46 | end 47 | 48 | local function assert_artifacts_paths(rs, alias_suffix) 49 | t.assert_equals(fio.path.exists(get_replica_set_artifacts_path(rs)), true) 50 | t.assert_equals(fio.path.is_dir(get_replica_set_artifacts_path(rs)), true) 51 | 52 | t.assert_equals( 53 | fio.path.exists(get_server_artifacts_path_by_alias(rs, 1, alias_suffix)), true) 54 | t.assert_equals( 55 | fio.path.is_dir(get_server_artifacts_path_by_alias(rs, 1, alias_suffix)), true) 56 | 57 | t.assert_equals( 58 | fio.path.exists(get_server_artifacts_path_by_alias(rs, 2, alias_suffix)), true) 59 | t.assert_equals( 60 | fio.path.is_dir(get_server_artifacts_path_by_alias(rs, 2, alias_suffix)), true) 61 | 62 | t.assert_equals( 63 | fio.path.exists(get_server_artifacts_path_by_alias(rs, 3, alias_suffix)), true) 64 | t.assert_equals( 65 | fio.path.is_dir(get_server_artifacts_path_by_alias(rs, 3, alias_suffix)), true) 66 | end 67 | 68 | g.before_all(function() 69 | g.rs_all = build_specific_replica_set('all') 70 | 71 | g.rs_all:start() 72 | g.rs_all:wait_for_fullmesh() 73 | end) 74 | 75 | g.before_each(function() 76 | g.rs_each = build_specific_replica_set('each') 77 | 78 | g.rs_each:start() 79 | g.rs_each:wait_for_fullmesh() 80 | end) 81 | 82 | g.before_test('test_foo', function() 83 | g.rs_test = build_specific_replica_set('test') 84 | 85 | g.rs_test:start() 86 | g.rs_test:wait_for_fullmesh() 87 | end) 88 | 89 | g.test_foo = function() 90 | local test = rawget(_G, 'current_test') 91 | 92 | test.status = 'fail' 93 | g.rs_test:drop() 94 | g.rs_each:drop() 95 | g.rs_all:drop() 96 | test.status = 'success' 97 | 98 | assert_artifacts_paths(g.rs_test, 'test') 99 | assert_artifacts_paths(g.rs_each, 'each') 100 | assert_artifacts_paths(g.rs_all, 'all') 101 | end 102 | -------------------------------------------------------------------------------- /luatest/log.lua: -------------------------------------------------------------------------------- 1 | local checks = require('checks') 2 | local fio = require('fio') 3 | local tarantool_log = require('log') 4 | 5 | local OutputBeautifier = require('luatest.output_beautifier') 6 | local utils = require('luatest.utils') 7 | 8 | -- Utils for logging 9 | local log = {} 10 | local default_level = 'info' 11 | local is_initialized = false 12 | 13 | function log.initialize(options) 14 | checks({ 15 | vardir = 'string', 16 | log_file = '?string', 17 | log_prefix = '?string', 18 | }) 19 | if is_initialized then 20 | return 21 | end 22 | 23 | local vardir = options.vardir 24 | local luatest_log_prefix = options.log_prefix or 'luatest' 25 | local luatest_log_file = fio.pathjoin(vardir, luatest_log_prefix .. '.log') 26 | local unified_log_file = options.log_file 27 | 28 | fio.mktree(vardir) 29 | 30 | if unified_log_file then 31 | -- Save the file descriptor as a global variable to use it in 32 | -- the `output_beautifier` module. 33 | local fh = fio.open(unified_log_file, {'O_CREAT', 'O_WRONLY', 'O_TRUNC'}, 34 | tonumber('640', 8)) 35 | rawset(_G, 'log_file', {fh = fh}) 36 | end 37 | 38 | local output_beautifier = OutputBeautifier:new({ 39 | file = luatest_log_file, 40 | prefix = luatest_log_prefix, 41 | }) 42 | output_beautifier:enable() 43 | 44 | -- Redirect all logs to the pipe created by OutputBeautifier. 45 | -- 46 | -- Write through a pipe to enable non-blocking mode. Required to prevent 47 | -- a deadlock in case a test performs a lot of checks without yielding 48 | -- execution to the fiber processing logs (gh-416). 49 | local log_cfg = string.format('| cat > /dev/fd/%d', 50 | output_beautifier.pipes.stdout[1]) 51 | 52 | -- Logging cannot be initialized without configuring the `box` engine 53 | -- on a version less than 2.5.1 (see more details at [1]). Otherwise, 54 | -- this causes the `attempt to call field 'cfg' (a nil value)` error, 55 | -- so there are the following limitations: 56 | -- 1. There is no `luatest.log` file (but logs are still available 57 | -- in stdout and in the `run.log` file); 58 | -- 2. All logs from luatest are non-formatted and look like: 59 | -- 60 | -- luatest | My log message 61 | -- 62 | -- [1]: https://github.com/tarantool/tarantool/issues/689 63 | if utils.version_current_ge_than(2, 5, 1) then 64 | -- Initialize logging for luatest runner. 65 | -- The log format will be as follows: 66 | -- YYYY-MM-DD HH:MM:SS.ZZZ [ID] main/.../luatest I> ... 67 | require('log').cfg{ 68 | log = log_cfg, 69 | nonblock = true, 70 | } 71 | end 72 | 73 | is_initialized = true 74 | end 75 | 76 | local function _log(level, msg, ...) 77 | if utils.version_current_ge_than(2, 5, 1) then 78 | return tarantool_log[level](msg, ...) 79 | end 80 | end 81 | 82 | --- Extra wrapper for `__call` function 83 | -- An additional function that takes `table` as 84 | -- the first argument to call table function. 85 | local function _log_default(t, msg, ...) 86 | return t[default_level](msg, ...) 87 | end 88 | 89 | function log.info(msg, ...) 90 | return _log('info', msg, ...) 91 | end 92 | 93 | function log.warn(msg, ...) 94 | return _log('warn', msg, ...) 95 | end 96 | 97 | function log.error(msg, ...) 98 | return _log('error', msg, ...) 99 | end 100 | 101 | setmetatable(log, {__call = _log_default}) 102 | 103 | return log 104 | -------------------------------------------------------------------------------- /test/output_test.lua: -------------------------------------------------------------------------------- 1 | local t = require('luatest') 2 | local g = t.group() 3 | 4 | local Capture = require('luatest.capture') 5 | local capture = Capture:new() 6 | 7 | local helper = require('test.helpers.general') 8 | 9 | g.before_each(function() 10 | capture:enable() 11 | end) 12 | 13 | g.after_each(function() 14 | capture:flush() 15 | capture:disable() 16 | end) 17 | 18 | g.test_no_fails_summary_on_success = function() 19 | local result = helper.run_suite(function(lu2) 20 | local g2 = lu2.group('group-name') 21 | g2.test = function() end 22 | end) 23 | t.assert_equals(result, 0) 24 | local captured = capture:flush() 25 | t.assert_not_str_contains(captured.stdout, 'Failed tests:') 26 | end 27 | 28 | g.test_fails_summary_on_failure = function() 29 | local result = helper.run_suite(function(lu2) 30 | local g2 = lu2.group('group-name') 31 | g2.test_1 = function() error('custom') end 32 | g2.test_2 = function() end 33 | g2.test_3 = function() lu2.assert_equals(1, 2) end 34 | g2.test_4 = function() end 35 | g2.test_5 = function() lu2.xfail_if(true, 'Nooo') end 36 | g2.test_6 = function() lu2.xfail('Impossible.') lu2.assert_equals(1, 2) end 37 | g2.test_7 = function() lu2.xfail() end 38 | end) 39 | t.assert_equals(result, 4) 40 | local captured = capture:flush() 41 | t.assert_str_contains(captured.stdout, 'Failed tests:') 42 | t.assert_str_contains(captured.stdout, 'group-name.test_1') 43 | t.assert_str_contains(captured.stdout, 'Tests with errors:') 44 | t.assert_str_contains(captured.stdout, 'group-name.test_3') 45 | t.assert_str_contains(captured.stdout, 'Tests with an unexpected success') 46 | t.assert_str_contains(captured.stdout, 'group-name.test_5') 47 | t.assert_str_contains(captured.stdout, 'Nooo') 48 | t.assert_str_contains(captured.stdout, 'group-name.test_7') 49 | t.assert_str_contains(captured.stdout, 'Test expected to fail has succeeded. Consider removing xfail.') 50 | t.assert_not_str_contains(captured.stdout, 'group-name.test_2') 51 | t.assert_not_str_contains(captured.stdout, 'group-name.test_4') 52 | t.assert_not_str_contains(captured.stdout, 'group-name.test_6') 53 | t.assert_not_str_contains(captured.stdout, 'Impossible.') 54 | end 55 | 56 | local output_presets = { 57 | verbose = {'-v'}, 58 | text = {'-o', 'text'}, 59 | junit = {'-o', 'junit', '-n', 'tmp/test_junit'}, 60 | tap = {'-o', 'tap'}, 61 | tap_verbose = {'-o', 'tap', '-v'}, 62 | ['nil'] = {'-o', 'nil'}, 63 | } 64 | 65 | for output_type, options in pairs(output_presets) do 66 | g['test_' .. output_type .. '_output'] = function() 67 | capture:disable() 68 | t.assert_equals(helper.run_suite(function(lu2) 69 | local g2 = lu2.group('group-name') 70 | g2.test_1 = function() error('custom') end 71 | g2.test_2 = function() end 72 | g2.test_3 = function() lu2.assert_equals(1, 2) end 73 | g2.test_4 = function() lu2.skip() end 74 | g2.test_5 = function() lu2.skip('skip-msg') end 75 | g2.test_6 = function() lu2.success() end 76 | g2.test_7 = function() lu2.success('success-msg') end 77 | g2.test_8 = function() end 78 | end, options), 2) 79 | 80 | t.assert_equals(helper.run_suite(function(lu2) 81 | local g2 = lu2.group('group-name') 82 | g2.test_2 = function() end 83 | g2.test_4 = function() lu2.skip() end 84 | g2.test_5 = function() lu2.skip('skip-msg') end 85 | g2.test_6 = function() lu2.success() end 86 | g2.test_7 = function() lu2.success('success-msg') end 87 | g2.test_8 = function() end 88 | end, options), 0) 89 | 90 | t.assert_equals(helper.run_suite(function(lu2) 91 | local g2 = lu2.group('group-name') 92 | g2.test_2 = function() end 93 | end, options), 0) 94 | end 95 | end 96 | -------------------------------------------------------------------------------- /test/assertions_test.lua: -------------------------------------------------------------------------------- 1 | local t = require('luatest') 2 | local g = t.group() 3 | 4 | local helper = require('test.helpers.general') 5 | 6 | g.test_custom_errors = function() 7 | local function assert_no_exception(fn) 8 | local result = helper.run_suite(function(lu2) 9 | lu2.group().test = fn 10 | end) 11 | t.assert_equals(result, 1) 12 | end 13 | assert_no_exception(function() error(123ULL) end) 14 | assert_no_exception(function() error({a = 1}) end) 15 | end 16 | 17 | g.test_assert_equals_for_cdata = function() 18 | t.assert_equals(1, 1ULL) 19 | t.assert_equals(1ULL, 1ULL) 20 | t.assert_equals(1, 1LL) 21 | t.assert_equals(1LL, 1ULL) 22 | 23 | helper.assert_failure_contains('expected: 2ULL, actual: 1', t.assert_equals, 1, 2ULL) 24 | helper.assert_failure_contains('expected: 2LL, actual: 1', t.assert_equals, 1, 2LL) 25 | helper.assert_failure_contains('expected: 2LL, actual: 1ULL', t.assert_equals, 1ULL, 2LL) 26 | helper.assert_failure_contains('expected: cdata: NULL, actual: 1', t.assert_equals, 1, box.NULL) 27 | 28 | t.assert_not_equals(1, 2ULL) 29 | t.assert_not_equals(1, 2LL) 30 | t.assert_not_equals(1ULL, 2LL) 31 | t.assert_not_equals(1ULL, box.NULL) 32 | end 33 | 34 | g.test_assert_almost_equals_for_cdata = function() 35 | t.assert_almost_equals(1, 2ULL, 1) 36 | t.assert_almost_equals(1LL, 2, 1) 37 | 38 | helper.assert_failure_contains('Values are not almost equal', t.assert_almost_equals, 1, 3ULL, 1) 39 | helper.assert_failure_contains('Values are not almost equal', t.assert_almost_equals, 1LL, 3, 1) 40 | helper.assert_failure_contains('must supply only number arguments.\n'.. 41 | 'Arguments supplied: cdata: NULL, 3, 1', t.assert_almost_equals, box.NULL, 3, 1) 42 | 43 | t.assert_not_almost_equals(1, 3ULL, 1) 44 | t.assert_not_almost_equals(1LL, 3, 1LL) 45 | end 46 | 47 | g.test_assert_with_extra_message_not_string = function() 48 | local raw_msg = 'expected: a value evaluating to true, actual: nil' 49 | helper.assert_failure_equals('{custom = "error"}\n' .. raw_msg, t.assert, nil, {custom = 'error'}) 50 | helper.assert_failure_equals(raw_msg, t.assert, nil, nil) 51 | helper.assert_failure_equals(raw_msg, t.assert, nil, box.NULL) 52 | helper.assert_failure_equals('321\n' .. raw_msg, t.assert, nil, 321) 53 | end 54 | 55 | g.test_assert_comparisons_error = function() 56 | helper.assert_failure_contains('must supply only number arguments.\n'.. 57 | 'Arguments supplied: \"one\", 3', t.assert_le, 'one', 3) 58 | helper.assert_failure_contains('must supply only number arguments.\n'.. 59 | 'Arguments supplied: \"one\", 3', t.assert_lt, 'one', 3) 60 | helper.assert_failure_contains('must supply only number arguments.\n'.. 61 | 'Arguments supplied: \"one\", 3', t.assert_ge, 'one', 3) 62 | helper.assert_failure_contains('must supply only number arguments.\n'.. 63 | 'Arguments supplied: \"one\", 3', t.assert_gt, 'one', 3) 64 | end 65 | 66 | local function external_error_fn(msg) 67 | error(msg) 68 | end 69 | 70 | local g2 = t.group('g2', { 71 | {fn = external_error_fn}, 72 | {fn = error}, 73 | {fn = function(msg) error(msg) end} 74 | }) 75 | 76 | g2.test_assert_error_msg_content_equals = function(cg) 77 | local msg = "error" 78 | t.assert_error_msg_content_equals(msg, cg.params.fn, msg) 79 | t.assert_error_msg_content_equals(msg, cg.params.fn, "foo.bar:1: " .. msg) 80 | t.assert_error_msg_content_equals(msg, cg.params.fn, "foo.bar:123: " .. msg) 81 | t.assert_error_msg_content_equals(msg, cg.params.fn, "/foo/bar.lua:1: " .. msg) 82 | t.assert_error_msg_content_equals(msg, cg.params.fn, ".../foo/bar.lua:1: " .. msg) 83 | t.assert_error_msg_content_equals(msg, cg.params.fn, "foo.bar:1: foo.bar:1: " .. msg) 84 | t.assert_error_msg_content_equals(msg, cg.params.fn, "foo.bar:1: .../foo/bar.lua:1: " .. msg) 85 | t.assert_error_msg_content_equals(msg, cg.params.fn, "foo.bar.bar:1: foo.bar.bar:1: " .. msg) 86 | end 87 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.5 FATAL_ERROR) 2 | 3 | project(luatest NONE) 4 | 5 | set(CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake" ${CMAKE_MODULE_PATH}) 6 | 7 | # Find Tarantool and Lua dependecies 8 | set(Tarantool_FIND_REQUIRED ON) 9 | find_package(Tarantool) 10 | 11 | ## VERSION #################################################################### 12 | ############################################################################### 13 | 14 | if (NOT VERSION) 15 | execute_process( 16 | COMMAND git describe --tags --always 17 | WORKING_DIRECTORY ${PROJECT_SOURCE_DIR} 18 | OUTPUT_STRIP_TRAILING_WHITESPACE 19 | OUTPUT_VARIABLE VERSION 20 | ERROR_QUIET 21 | ) 22 | endif() 23 | 24 | if (VERSION) 25 | configure_file ( 26 | "${PROJECT_SOURCE_DIR}/${PROJECT_NAME}/VERSION.lua.in" 27 | "${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}/VERSION.lua" 28 | ) 29 | elseif(NOT LUAROCKS) # allow luarocks to build without passing VERSION 30 | message(FATAL_ERROR "VERSION is not provided") 31 | endif() 32 | 33 | ## Dependencies and custom targets ############################################ 34 | ############################################################################### 35 | 36 | set_property(DIRECTORY APPEND PROPERTY ADDITIONAL_MAKE_CLEAN_FILES build) 37 | 38 | set_property(DIRECTORY APPEND PROPERTY ADDITIONAL_MAKE_CLEAN_FILES .rocks) 39 | add_custom_command( 40 | OUTPUT ${PROJECT_SOURCE_DIR}/.rocks 41 | DEPENDS ${PROJECT_NAME}-scm-1.rockspec 42 | COMMAND tt rocks make ./${PROJECT_NAME}-scm-1.rockspec 43 | COMMAND tt rocks install http 1.1.0 44 | COMMAND tt rocks install luacheck 0.25.0 45 | COMMAND tt rocks install luacov 0.13.0 46 | WORKING_DIRECTORY ${PROJECT_SOURCE_DIR} 47 | ) 48 | 49 | add_custom_target(bootstrap DEPENDS ${PROJECT_SOURCE_DIR}/.rocks) 50 | 51 | add_custom_command( 52 | OUTPUT ${PROJECT_SOURCE_DIR}/.rocks/bin/ldoc 53 | DEPENDS bootstrap 54 | COMMAND tt rocks install ldoc --server=http://rocks.moonscript.org 55 | WORKING_DIRECTORY ${PROJECT_SOURCE_DIR} 56 | ) 57 | 58 | set_property(DIRECTORY APPEND PROPERTY ADDITIONAL_MAKE_CLEAN_FILES doc) 59 | add_custom_target(doc 60 | DEPENDS ${PROJECT_SOURCE_DIR}/.rocks/bin/ldoc 61 | COMMAND .rocks/bin/ldoc -t ${PROJECT_NAME}-scm-1 -p ${PROJECT_NAME} --all . 62 | WORKING_DIRECTORY ${PROJECT_SOURCE_DIR} 63 | ) 64 | 65 | add_custom_target(lint 66 | DEPENDS bootstrap 67 | COMMAND .rocks/bin/luacheck . 68 | WORKING_DIRECTORY ${PROJECT_SOURCE_DIR} 69 | ) 70 | add_custom_target(selftest 71 | DEPENDS bootstrap 72 | COMMAND bin/luatest -v --shuffle group 73 | WORKING_DIRECTORY ${PROJECT_SOURCE_DIR} 74 | ) 75 | 76 | add_custom_target(selftest-coverage 77 | DEPENDS bootstrap 78 | COMMAND rm -f tmp/luacov.*.out* 79 | COMMAND tarantool -l luatest.coverage bin/luatest -v --shuffle group 80 | COMMAND .rocks/bin/luacov . 81 | COMMAND echo 82 | COMMAND grep -A999 '^Summary' tmp/luacov.report.out 83 | WORKING_DIRECTORY ${PROJECT_SOURCE_DIR} 84 | ) 85 | 86 | ## Test ####################################################################### 87 | ############################################################################### 88 | 89 | enable_testing() 90 | add_test(lint ${CMAKE_MAKE_PROGRAM} lint) 91 | add_test(selftest ${CMAKE_MAKE_PROGRAM} selftest) 92 | 93 | ## Install #################################################################### 94 | ############################################################################### 95 | 96 | install( 97 | DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/${PROJECT_NAME} 98 | ${CMAKE_CURRENT_SOURCE_DIR}/luarocks 99 | USE_SOURCE_PERMISSIONS 100 | DESTINATION ${TARANTOOL_INSTALL_LUADIR}/ 101 | PATTERN "*.in" EXCLUDE 102 | ) 103 | 104 | install( 105 | DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME} 106 | DESTINATION ${TARANTOOL_INSTALL_LUADIR}/ 107 | OPTIONAL 108 | ) 109 | 110 | install( 111 | PROGRAMS ${CMAKE_CURRENT_SOURCE_DIR}/bin/luatest 112 | DESTINATION ${TARANTOOL_INSTALL_BINDIR} 113 | ) 114 | -------------------------------------------------------------------------------- /luatest/process.lua: -------------------------------------------------------------------------------- 1 | local checks = require('checks') 2 | local errno = require('errno') 3 | local fun = require('fun') 4 | local ffi = require('ffi') 5 | local fio = require('fio') 6 | 7 | local log = require('luatest.log') 8 | local Class = require('luatest.class') 9 | local OutputBeautifier = require('luatest.output_beautifier') 10 | 11 | ffi.cdef([[ 12 | pid_t fork(void); 13 | int execve(const char *pathname, char *const argv[], char *const envp[]); 14 | int kill(pid_t pid, int sig); 15 | ]]) 16 | 17 | local Process = Class.new() 18 | 19 | -- luacov: disable 20 | local function to_const_char(input) 21 | local result = ffi.new('char const*[?]', #input + 1, input) 22 | result[#input] = nil 23 | return ffi.cast('char *const*', result) 24 | end 25 | -- luacov: enable 26 | 27 | --- Starts process and returns immediately, not waiting until process is finished. 28 | -- @param path Executable path. 29 | -- @param[opt] args 30 | -- @param[opt] env 31 | -- @param[opt] options 32 | -- @param[opt] options.chdir Directory to chdir into before starting process. 33 | -- @param[opt] options.ignore_gc Don't install handler which kills GC'ed processes. 34 | function Process:start(path, args, env, options) 35 | checks('table', 'string', '?table', '?table', { 36 | chdir = '?string', 37 | ignore_gc = '?boolean', 38 | output_prefix = '?string', 39 | output_file = '?string', 40 | }) 41 | args = args and table.copy(args) or {} 42 | env = env or {} 43 | options = options or {} 44 | 45 | table.insert(args, 1, path) 46 | 47 | local output_beautifier 48 | if options.output_prefix or options.output_file then 49 | output_beautifier = OutputBeautifier:new({ 50 | prefix = options.output_prefix, 51 | file = options.output_file, 52 | }) 53 | end 54 | 55 | local env_list = fun.iter(env):map(function(k, v) return k .. '=' .. v end):totable() 56 | local pid = ffi.C.fork() 57 | if pid == -1 then 58 | error('fork failed: ' .. pid) 59 | elseif pid > 0 then 60 | if output_beautifier then 61 | output_beautifier:enable({track_pid = pid}) 62 | end 63 | return self:from({pid = pid, ignore_gc = options.ignore_gc, output_beautifier = output_beautifier}) 64 | end 65 | -- luacov: disable 66 | if options.chdir then 67 | fio.chdir(options.chdir) 68 | end 69 | if output_beautifier then 70 | output_beautifier:hijack_output() 71 | end 72 | local argv = to_const_char(args) 73 | local envp = to_const_char(env_list) 74 | ffi.C.execve(path, argv, envp) 75 | io.stderr:write('execve failed (' .. path .. '): ' .. errno.strerror() .. '\n') 76 | os.exit(1) 77 | -- luacov: enable 78 | end 79 | 80 | function Process.mt:initialize() 81 | if not self.ignore_gc then 82 | self._pid_ull = ffi.cast('void*', 0ULL + self.pid) 83 | ffi.gc(self._pid_ull, function(x) 84 | local pid = tonumber(ffi.cast(ffi.typeof(0ULL), x)) 85 | log.info("Killing GC'ed process %d", pid) 86 | Process.kill_pid(pid, nil, {quiet = true}) 87 | end) 88 | end 89 | end 90 | 91 | function Process.mt:kill(signal, options) 92 | self.class.kill_pid(self.pid, signal, options) 93 | end 94 | 95 | function Process.mt:is_alive() 96 | return self.pid ~= nil and self.class.is_pid_alive(self.pid) 97 | end 98 | 99 | function Process.kill_pid(pid, signal, options) 100 | checks('number|string', '?number|string', {quiet = '?boolean'}) 101 | -- Signal values are platform-dependent so we can not use ffi here 102 | signal = signal or 15 103 | local exit_code = os.execute('kill -' .. signal .. ' ' .. pid .. ' 2> /dev/null') 104 | if exit_code ~= 0 and not (options and options.quiet) then 105 | error('kill failed: ' .. exit_code) 106 | end 107 | end 108 | 109 | function Process.is_pid_alive(pid) 110 | return ffi.C.kill(tonumber(pid), 0) == 0 111 | end 112 | 113 | return Process 114 | -------------------------------------------------------------------------------- /luatest/helpers.lua: -------------------------------------------------------------------------------- 1 | --- Collection of test helpers. 2 | -- 3 | -- @module luatest.helpers 4 | 5 | local clock = require('clock') 6 | local fiber = require('fiber') 7 | local fio = require('fio') 8 | local log = require('luatest.log') 9 | 10 | local helpers = {} 11 | 12 | local function repeat_value(value, length) 13 | if type(value) == 'string' then 14 | return string.rep(value, length / value:len()) 15 | else 16 | return string.format('%0' .. length .. 'd', value) 17 | end 18 | end 19 | 20 | --- Generates uuids from its 5 parts. 21 | -- Strings are repeated and numbers are padded to match required part length. 22 | -- If number of arguments is less than 5 then first and last arguments are used 23 | -- for corresponding parts, missing parts are set to 0. 24 | -- 25 | -- 'aaaaaaaa-0000-0000-0000-000000000000' == uuid('a') 26 | -- 'abababab-0000-0000-0000-000000000001' == uuid('ab', 1) 27 | -- '00000001-0002-0000-0000-000000000003' == uuid(1, 2, 3) 28 | -- '11111111-2222-0000-0000-333333333333' == uuid('1', '2', '3') 29 | -- '12121212-3434-5656-7878-909090909090' == uuid('12', '34', '56', '78', '90') 30 | -- 31 | -- @param a first part 32 | -- @param ... parts 33 | function helpers.uuid(a, ...) 34 | local input = {...} 35 | local e = table.remove(input) 36 | local b, c, d = unpack(input) 37 | return table.concat({ 38 | repeat_value(a, 8), 39 | repeat_value(b or 0, 4), 40 | repeat_value(c or 0, 4), 41 | repeat_value(d or 0, 4), 42 | repeat_value(e or 0, 12), 43 | }, '-') 44 | end 45 | 46 | helpers.RETRYING_TIMEOUT = 5 47 | helpers.RETRYING_DELAY = 0.1 48 | 49 | --- Keep calling fn until it returns without error. 50 | -- Throws last error if config.timeout is elapsed. 51 | -- Default options are taken from helpers.RETRYING_TIMEOUT and helpers.RETRYING_DELAY. 52 | -- 53 | -- helpers.retrying({}, fn, arg1, arg2) 54 | -- helpers.retrying({timeout = 2, delay = 0.5}, fn, arg1, arg2) 55 | -- 56 | -- @tab config 57 | -- @number config.timeout 58 | -- @number config.delay 59 | -- @func fn 60 | -- @param ... args 61 | function helpers.retrying(config, fn, ...) 62 | local timeout = config.timeout or helpers.RETRYING_TIMEOUT 63 | local delay = config.delay or helpers.RETRYING_DELAY 64 | local started_at = clock.time() 65 | while true do 66 | local ok, result = pcall(fn, ...) 67 | if ok then 68 | return result 69 | end 70 | if (clock.time() - started_at) > timeout then 71 | return fn(...) 72 | end 73 | local info = debug.getinfo(2, 'Sl') 74 | log.info('Retrying at %s:%s in %0.3f sec due to error:', 75 | fio.basename(info.short_src), info.currentline, delay) 76 | log.info(result) 77 | fiber.sleep(delay) 78 | end 79 | end 80 | 81 | --- Return all combinations of parameters. 82 | -- Accepts params' names and thier every possible value. 83 | -- 84 | -- helpers.matrix({a = {1, 2}, b = {3, 4}}) 85 | -- 86 | -- { 87 | -- {a = 1, b = 3}, 88 | -- {a = 2, b = 3}, 89 | -- {a = 1, b = 4}, 90 | -- {a = 2, b = 4}, 91 | -- } 92 | -- 93 | -- @tab parameters_values 94 | function helpers.matrix(parameters_values) 95 | local combinations = {} 96 | 97 | local params_names = {} 98 | for name, _ in pairs(parameters_values) do 99 | table.insert(params_names, name) 100 | end 101 | table.sort(params_names) 102 | 103 | local function combinator(_params, ind, ...) 104 | if ind < 1 then 105 | local combination = {} 106 | for _, entry in ipairs({...}) do 107 | combination[entry[1]] = entry[2] 108 | end 109 | table.insert(combinations, combination) 110 | else 111 | local name = params_names[ind] 112 | for i=1,#(_params[name]) do combinator(_params, ind - 1, {name, _params[name][i]}, ...) end 113 | end 114 | end 115 | 116 | combinator(parameters_values, #params_names) 117 | return combinations 118 | end 119 | 120 | return helpers 121 | -------------------------------------------------------------------------------- /luatest/replica_conn.lua: -------------------------------------------------------------------------------- 1 | local checks = require('checks') 2 | local fiber = require('fiber') 3 | local socket = require('socket') 4 | 5 | local TIMEOUT = 0.001 6 | 7 | local Connection = { 8 | constructor_checks = { 9 | client_socket = '?table', 10 | server_socket_path = 'string', 11 | process_client = '?table', 12 | process_server = '?table', 13 | }, 14 | } 15 | 16 | function Connection:inherit(object) 17 | setmetatable(object, self) 18 | self.__index = self 19 | return object 20 | end 21 | 22 | function Connection:new(object) 23 | checks('table', self.constructor_checks) 24 | self:inherit(object) 25 | object:initialize() 26 | return object 27 | end 28 | 29 | function Connection:initialize() 30 | self.running = false 31 | self.client_connected = true 32 | self.server_connected = false 33 | self.client_fiber = nil 34 | self.server_fiber = nil 35 | 36 | if self.process_client == nil then 37 | self.process_client = { 38 | pre = nil, 39 | func = self.forward_to_server, 40 | post = self.stop, 41 | } 42 | end 43 | 44 | if self.process_server == nil then 45 | self.process_server = { 46 | pre = nil, 47 | func = self.forward_to_client, 48 | post = self.stop, 49 | } 50 | end 51 | 52 | self:connect_server_socket() 53 | end 54 | 55 | function Connection:connect_server_socket() 56 | self.server_socket = socket('PF_UNIX', 'SOCK_STREAM', 0) 57 | if self.server_socket:sysconnect('unix/', self.server_socket_path) == false 58 | then 59 | self.server_socket:close() 60 | self.server_socket = nil 61 | return 62 | end 63 | self.server_socket:nonblock(true) 64 | self.server_connected = true 65 | 66 | self.server_fiber = self:process_socket(self.server_socket, 67 | self.process_server) 68 | end 69 | 70 | function Connection:process_socket(sock, process) 71 | local f = fiber.new(function() 72 | if process.pre ~= nil then process.pre(self) end 73 | 74 | while sock:peer() or not self.running do 75 | if not self.running then 76 | fiber.sleep(TIMEOUT) 77 | elseif sock:readable(TIMEOUT) then 78 | local request = sock:recv() 79 | if request == nil or #request == 0 then break end 80 | if process.func ~= nil then process.func(self, request) end 81 | end 82 | end 83 | 84 | if process.post ~= nil then process.post(self) end 85 | end) 86 | f:set_joinable(true) 87 | f:name('ProxyConnectionIO') 88 | return f 89 | end 90 | 91 | function Connection:start() 92 | self.running = true 93 | if self.client_fiber == nil or self.client_fiber:status() == 'dead' then 94 | self.client_fiber = self:process_socket(self.client_socket, 95 | self.process_client) 96 | end 97 | end 98 | 99 | function Connection:pause() 100 | self.running = false 101 | end 102 | 103 | function Connection:resume() 104 | self.running = true 105 | end 106 | 107 | function Connection:stop() 108 | self:close_client_socket() 109 | self:close_server_socket() 110 | end 111 | 112 | function Connection:forward_to_server(data) 113 | if not self.server_connected then 114 | self:connect_server_socket() 115 | end 116 | if self.server_connected and self.server_socket:writable() then 117 | self.server_socket:write(data) 118 | end 119 | end 120 | 121 | function Connection:forward_to_client(data) 122 | if self.client_connected and self.client_socket:writable() then 123 | self.client_socket:write(data) 124 | end 125 | end 126 | 127 | function Connection:close_server_socket() 128 | if self.server_connected then 129 | self.server_socket:shutdown(socket.SHUT_RW) 130 | self.server_socket:close() 131 | self.server_connected = false 132 | self.server_fiber:join() 133 | end 134 | end 135 | 136 | function Connection:close_client_socket() 137 | if self.client_connected then 138 | self.client_socket:shutdown(socket.SHUT_RW) 139 | self.client_socket:close() 140 | self.client_connected = false 141 | self.client_fiber:join() 142 | end 143 | end 144 | 145 | return Connection 146 | -------------------------------------------------------------------------------- /debian/changelog: -------------------------------------------------------------------------------- 1 | luatest (0.5.7-1) unstable; urgency=medium 2 | 3 | * Fix invalid arguments logging in some assertions. 4 | * Fix confusing error message from `assert_not_equals` function. 5 | * Fix confusing error message from `assert_items_equals` function. 6 | * Fix confusing error message from `assert_items_include` function. 7 | * Print `(no reason specified)` message instead of `nil` value when the test is 8 | skipped and no reason is specified. 9 | * Check `net_box_uri` param is less than max Unix domain socket path length. 10 | * Change test run summary report: use verbs in past simple tense (succeeded, 11 | failed, xfailed, etc.) instead of nouns (success(es), fail(s), xfail(s), etc.) 12 | 13 | -- Nikolay Volynkin Fri, 28 Jan 2022 14:00:00 +0300 14 | 15 | luatest (0.5.6-1) unstable; urgency=medium 16 | 17 | * Add `xfail` status. 18 | * Add new `Server:exec()` function which runs a Lua function remotely. 19 | 20 | -- Aleksandr Shemenev Wed, 6 Oct 2021 13:00:00 +0300 21 | 22 | luatest (0.5.5-1) unstable; urgency=medium 23 | 24 | * Repeat `_each` and `_test` hooks when `--repeat` is specified. 25 | * Add group parametrization. 26 | 27 | -- Aleksandr Shemenev Thu, 16 Sept 2021 13:00:00 +0300 28 | 29 | luatest (0.5.4-1) unstable; urgency=medium 30 | 31 | * Add `after_test` and `before_test` hooks. 32 | * Add tap version to the output. 33 | * New `restart` server method. 34 | * Add new `eval` and `call` server methods for convenient net_box calls. 35 | * Server can use a unix socket as a listen port. 36 | * Add `TARANTOOL_ALIAS` in the server env space. 37 | * Server args are updated on start. 38 | 39 | -- Aleksandr Shemenev Thu, 5 Aug 2021 13:00:00 +0300 40 | 41 | luatest (0.5.3-1) unstable; urgency=medium 42 | 43 | * Add `_le`, `_lt`, `_ge`, `_gt` assertions. 44 | * Write execution time for each test in the verbose mode. 45 | * When capture is disabled and verbose mode is on test names are printed 46 | twice: at the start and at the end with result. 47 | * `assert_error_msg_` assertions print return values if no error is generated. 48 | * Fix `--repeat` runner option. 49 | 50 | -- Aleksandr Shemenev Thu, 10 Jun 2021 13:00:00 +0300 51 | 52 | luatest (0.5.2-1) unstable; urgency=medium 53 | 54 | * Throw parser error when .json is accessed on response with invalid body. 55 | * Set `Content-Type: application/json` for `:http_request(..., {json = ...})` requests. 56 | 57 | -- Maxim Melentiev Thu, 25 Jun 2020 13:00:00 +0300 58 | 59 | luatest (0.5.1-1) unstable; urgency=medium 60 | 61 | * Assertions pretty-prints non-string extra messages (useful for custom errors as tables). 62 | * String values in errors are printed as valid Lua strings (with `%q` formatter). 63 | * Add `TARANTOOL_DIR` to rockspec build.variables 64 | * Replace `--error` and `--failure` options with `--fail-fast`. 65 | * Fix stripping luatest trace from backtrace. 66 | * Fix luarocks 3 test engine installation. 67 | 68 | -- Maxim Melentiev Tue, 21 Apr 2020 13:00:00 +0300 69 | 70 | luatest (0.5.0-1) unstable; urgency=medium 71 | 72 | * `assert_is` treats `box.NULL` and `nil` as different values. 73 | * Add luacov integration. 74 | * Fix `assert_items_equals` for repeated values. Add support for `tuple` items. 75 | * Add `assert_items_include` matcher. 76 | * `assert_equals` uses same comparison rules for nested values. 77 | * Fix generated group names when running files within specific directory. 78 | 79 | -- Maxim Melentiev Wed, 22 Jan 2020 13:00:00 +0300 80 | 81 | luatest (0.4.0-1) unstable; urgency=medium 82 | 83 | * Fix not working `--exclude`, `--pattern` options 84 | * Fix error messages for `*_covers` matchers 85 | * Raise error when `group()` is called with existing group name. 86 | * Allow dot in group name. 87 | * Prevent using `/` in group name. 88 | * Decide group name from filename for `group()` call without args. 89 | * `assert` returns input values. 90 | * `assert[_not]_equals` works for Tarantool's box.tuple. 91 | * Print tables in lua-compatible way in errors. 92 | * Fix performance issue with large errors messages. 93 | * Unify hooks definition: group hooks are defined via function calls. 94 | * Keep running other groups when group hook failed. 95 | * Prefix and colorize captured output. 96 | * Fix numeric assertions for cdata values. 97 | 98 | -- Maxim Melentiev Thu, 26 Dec 2019 13:00:00 +0300 99 | 100 | luatest (0.3.0-1) unstable; urgency=medium 101 | 102 | * Initial release. 103 | 104 | -- Konstantin Nazarov Thu, 3 Oct 2019 13:00:00 +0300 105 | -------------------------------------------------------------------------------- /luatest/capture.lua: -------------------------------------------------------------------------------- 1 | -- Module to capture output. It works by replacing stdout and stderr file 2 | -- descriptors with pipes inputs. 3 | 4 | local ffi = require('ffi') 5 | local fiber = require('fiber') 6 | 7 | local Class = require('luatest.class') 8 | local ffi_io = require('luatest.ffi_io') 9 | local utils = require('luatest.utils') 10 | 11 | ffi.cdef([[ 12 | int dup(int oldfd); 13 | int fileno(struct FILE *stream); 14 | ]]) 15 | 16 | -- Duplicate lua's io object to new fd. 17 | local function dup_io(file) 18 | local newfd = ffi.C.dup(ffi.C.fileno(file)) 19 | if newfd < 0 then 20 | error('dup call failed') 21 | end 22 | return newfd 23 | end 24 | 25 | local Capture = Class.new({ 26 | CAPTURED_ERROR_TYPE = 'ERROR_WITH_CAPTURE', 27 | }) 28 | 29 | Capture.Stub = Capture:new_class() 30 | Capture.Stub.mt.enable = function() end 31 | Capture.Stub.mt.disable = function() end 32 | 33 | function Capture:stub() 34 | return self.Stub:new() 35 | end 36 | 37 | function Capture.mt:initialize() 38 | self.enabled = false 39 | self.buffer = {stdout = {}, stderr = {}} 40 | end 41 | 42 | -- Overwrite stdout and stderr fds with pipe inputs. 43 | -- Original fds are copied into original_fds, to be able to restore them later. 44 | function Capture.mt:enable(raise) 45 | if self.enabled then 46 | if raise then 47 | error('Already capturing') 48 | end 49 | return 50 | end 51 | if not self.pipes then 52 | self.pipes = {stdout = ffi_io.create_pipe(), stderr = ffi_io.create_pipe()} 53 | self.original_fds = {stdout = dup_io(io.stdout), stderr = dup_io(io.stderr)} 54 | end 55 | io.flush() 56 | ffi_io.dup2_io(self.pipes.stdout[1], io.stdout) 57 | ffi_io.dup2_io(self.pipes.stderr[1], io.stderr) 58 | self:start_reader_fibers() 59 | self.enabled = true 60 | end 61 | 62 | -- Start the fiber that reads from pipes to the buffer. 63 | function Capture.mt:start_reader_fibers() 64 | assert(not self.reader_fibers, 'reader_fibers are already running') 65 | self.reader_fibers = {} 66 | for name, pipe in pairs(self.pipes) do 67 | self.reader_fibers[name] = fiber.new(function() 68 | while fiber.testcancel() or true do 69 | ffi_io.read_fd(pipe[0], self.buffer[name]) 70 | end 71 | end) 72 | self.reader_fibers[name]:set_joinable(true) 73 | end 74 | end 75 | 76 | -- Stop reader fiber and read available data from pipe after fiber was stopped. 77 | function Capture.mt:stop_reader_fibers() 78 | io.flush() 79 | if not self.reader_fibers then 80 | return false 81 | end 82 | for name, item in pairs(self.reader_fibers) do 83 | item:cancel() 84 | item:join() 85 | ffi_io.read_fd(self.pipes[name][0], self.buffer[name], {timeout = 0}) 86 | end 87 | self.reader_fibers = nil 88 | return true 89 | end 90 | 91 | -- Restore original fds for stdout and stderr. 92 | function Capture.mt:disable(raise) 93 | if not self.enabled then 94 | if raise then 95 | error('Not capturing') 96 | end 97 | return 98 | end 99 | self:stop_reader_fibers() 100 | ffi_io.dup2_io(self.original_fds.stdout, io.stdout) 101 | ffi_io.dup2_io(self.original_fds.stderr, io.stderr) 102 | self.enabled = false 103 | end 104 | 105 | -- Enable/disable depending on passed value. 106 | function Capture.mt:set_enabled(value) 107 | if value then 108 | self:enable() 109 | else 110 | self:disable() 111 | end 112 | end 113 | 114 | -- Read from capture pipes and return results. 115 | function Capture.mt:flush() 116 | if not self.pipes then 117 | return {stdout = '', stderr = ''} 118 | end 119 | local restart_reader_fibers = self:stop_reader_fibers() 120 | local result = { 121 | stdout = table.concat(self.buffer.stdout), 122 | stderr = table.concat(self.buffer.stderr), 123 | } 124 | self.buffer = {stdout = {}, stderr = {}} 125 | if restart_reader_fibers then 126 | self:start_reader_fibers() 127 | end 128 | return result 129 | end 130 | 131 | -- Run function with enabled/disabled capture and restore previous state. 132 | -- In the case of failure it wraps error into map-table with captured output added. 133 | function Capture.mt:wrap(enabled, fn) 134 | local old = self.enabled 135 | return utils.reraise_and_ensure(function() 136 | self:set_enabled(enabled) 137 | return fn() 138 | end, function(err) 139 | -- Don't re-wrap error. 140 | if err.type ~= self.class.CAPTURED_ERROR_TYPE then 141 | err = { 142 | type = self.class.CAPTURED_ERROR_TYPE, 143 | original = err, 144 | traceback = utils.traceback(err), 145 | captured = self:flush(), 146 | } 147 | end 148 | return err 149 | end, function() 150 | self:set_enabled(old) 151 | end) 152 | end 153 | 154 | return Capture 155 | -------------------------------------------------------------------------------- /luatest/replica_proxy.lua: -------------------------------------------------------------------------------- 1 | --- Manage connection to Tarantool replication instances via proxy. 2 | -- 3 | -- @module luatest.replica_proxy 4 | 5 | local checks = require('checks') 6 | local fiber = require('fiber') 7 | local fio = require('fio') 8 | local socket = require('socket') 9 | local uri = require('uri') 10 | 11 | local log = require('luatest.log') 12 | local utils = require('luatest.utils') 13 | local Connection = require('luatest.replica_conn') 14 | 15 | local TIMEOUT = 0.001 16 | local BACKLOG = 512 17 | 18 | local Proxy = { 19 | constructor_checks = { 20 | client_socket_path = 'string', 21 | server_socket_path = 'string', 22 | process_client = '?table', 23 | process_server = '?table', 24 | }, 25 | } 26 | 27 | function Proxy:inherit(object) 28 | setmetatable(object, self) 29 | self.__index = self 30 | return object 31 | end 32 | 33 | local function check_tarantool_version() 34 | if utils.version_current_ge_than(2, 10, 1) then 35 | return 36 | else 37 | error('Proxy requires Tarantool 2.10.1 and newer') 38 | end 39 | end 40 | 41 | --- Build a proxy object. 42 | -- 43 | -- @param object 44 | -- @string object.client_socket_path Path to a UNIX socket where proxy will await new connections. 45 | -- @string object.server_socket_path Path to a UNIX socket where Tarantool server is listening to. 46 | -- @tab[opt] object.process_client Table describing how to process the client socket. 47 | -- @tab[opt] object.process_server Table describing how to process the server socket. 48 | -- @return Input object. 49 | function Proxy:new(object) 50 | checks('table', self.constructor_checks) 51 | check_tarantool_version() 52 | self:inherit(object) 53 | object:initialize() 54 | return object 55 | end 56 | 57 | function Proxy:initialize() 58 | self.connections = {} 59 | self.accept_new_connections = true 60 | self.running = false 61 | 62 | self.client_socket = socket('PF_UNIX', 'SOCK_STREAM', 0) 63 | end 64 | 65 | --- Stop accepting new connections on the client socket. 66 | -- Join the fiber created by proxy:start() and close the client socket. 67 | -- Also, stop all active connections. 68 | function Proxy:stop() 69 | self.running = false 70 | self.worker:join() 71 | for _, c in pairs(self.connections) do 72 | c:stop() 73 | end 74 | end 75 | 76 | --- Pause accepting new connections and pause all active connections. 77 | function Proxy:pause() 78 | self.accept_new_connections = false 79 | for _, c in pairs(self.connections) do 80 | c:pause() 81 | end 82 | end 83 | 84 | --- Resume accepting new connections and resume all paused connections. 85 | function Proxy:resume() 86 | for _, c in pairs(self.connections) do 87 | c:resume() 88 | end 89 | self.accept_new_connections = true 90 | end 91 | 92 | --- Start accepting new connections on the client socket in a new fiber. 93 | -- 94 | -- @tab[opt] opts 95 | -- @bool[opt] opts.force Remove the client socket before start. 96 | function Proxy:start(opts) 97 | checks('table', {force = '?boolean'}) 98 | if opts ~= nil and opts.force then 99 | os.remove(self.client_socket_path) 100 | end 101 | 102 | fio.mktree(fio.dirname(uri.parse(self.client_socket_path).service)) 103 | 104 | if not self.client_socket:bind('unix/', self.client_socket_path) then 105 | log.error("Failed to bind client socket: %s", self.client_socket:error()) 106 | return false 107 | end 108 | 109 | self.client_socket:nonblock(true) 110 | if not self.client_socket:listen(BACKLOG) then 111 | log.error("Failed to listen on client socket: %s", 112 | self.client_socket:error()) 113 | return false 114 | end 115 | 116 | self.running = true 117 | self.worker = fiber.new(function() 118 | while self.running do 119 | if not self.accept_new_connections then 120 | fiber.sleep(TIMEOUT) 121 | goto continue 122 | end 123 | 124 | if not self.client_socket:readable(TIMEOUT) then 125 | goto continue 126 | end 127 | 128 | local client = self.client_socket:accept() 129 | if client == nil then goto continue end 130 | client:nonblock(true) 131 | 132 | local conn = Connection:new({ 133 | client_socket = client, 134 | server_socket_path = self.server_socket_path, 135 | process_client = self.process_client, 136 | process_server = self.process_server, 137 | }) 138 | table.insert(self.connections, conn) 139 | conn:start() 140 | :: continue :: 141 | end 142 | 143 | self.client_socket:shutdown(socket.SHUT_RW) 144 | self.client_socket:close() 145 | os.remove(self.client_socket_path) 146 | end) 147 | self.worker:set_joinable(true) 148 | self.worker:name('ProxyWorker') 149 | 150 | return true 151 | end 152 | 153 | return Proxy 154 | -------------------------------------------------------------------------------- /rpm/luatest.spec: -------------------------------------------------------------------------------- 1 | Name: luatest 2 | Version: 0.5.7 3 | Release: 1%{?dist} 4 | Summary: Tarantool test framework 5 | Group: Applications/Databases 6 | License: MIT 7 | URL: https://github.com/tarantool/luatest 8 | Source0: https://github.com/tarantool/luatest/archive/%{version}/luatest-%{version}.tar.gz 9 | BuildArch: noarch 10 | BuildRequires: tarantool >= 1.9.0 11 | BuildRequires: tarantool-devel >= 1.9.0 12 | BuildRequires: tarantool-checks >= 3.0.1 13 | BuildRequires: tt >= 2.2.1 14 | Requires: tarantool >= 1.9.0 15 | Requires: tarantool-checks >= 3.0.1 16 | %description 17 | Simple Tarantool test framework for both unit and integration testing. 18 | 19 | %prep 20 | %setup -q -n %{name}-%{version} 21 | 22 | %build 23 | %cmake -B . -DCMAKE_BUILD_TYPE=RelWithDebInfo -DVERSION=%{version} 24 | make %{?_smp_mflags} 25 | 26 | %check 27 | make selftest 28 | 29 | %install 30 | %make_install 31 | 32 | %files 33 | %{_datarootdir}/tarantool/*/ 34 | %{_bindir}/luatest 35 | %doc README.rst 36 | %{!?_licensedir:%global license %doc} 37 | %license LICENSE 38 | 39 | %changelog 40 | 41 | * Fri Jan 28 2022 Nikolay Volynkin 0.5.7-1 42 | - Fix invalid arguments logging in some assertions. 43 | - Fix confusing error message from `assert_not_equals` function. 44 | - Fix confusing error message from `assert_items_equals` function. 45 | - Fix confusing error message from `assert_items_include` function. 46 | - Print `(no reason specified)` message instead of `nil` value when the test is 47 | skipped and no reason is specified. 48 | - Check `net_box_uri` param is less than max Unix domain socket path length. 49 | - Change test run summary report: use verbs in past simple tense (succeeded, 50 | failed, xfailed, etc.) instead of nouns (success(es), fail(s), xfail(s), etc.) 51 | 52 | * Wed Oct 6 2021 Aleksandr Shemenev 0.5.6-1 53 | - Add `xfail` status. 54 | - Add new `Server:exec()` function which runs a Lua function remotely. 55 | 56 | * Thu Sep 16 2021 Aleksandr Shemenev 0.5.5-1 57 | - Repeat `_each` and `_test` hooks when `--repeat` is specified. 58 | - Add group parametrization. 59 | 60 | * Thu Aug 5 2021 Aleksandr Shemenev 0.5.4-1 61 | - Add `after_test` and `before_test` hooks. 62 | - Add tap version to the output. 63 | - New `restart` server method. 64 | - Add new `eval` and `call` server methods for convenient net_box calls. 65 | - Server can use a unix socket as a listen port. 66 | - Add `TARANTOOL_ALIAS` in the server env space. 67 | - Server args are updated on start. 68 | 69 | * Thu Jun 10 2021 Aleksandr Shemenev 0.5.3-1 70 | - Add `_le`, `_lt`, `_ge`, `_gt` assertions. 71 | - Write execution time for each test in the verbose mode. 72 | - When capture is disabled and verbose mode is on test names are printed 73 | twice: at the start and at the end with result. 74 | - `assert_error_msg_` assertions print return values if no error is generated. 75 | - Fix `--repeat` runner option. 76 | 77 | * Thu Jun 25 2020 Maxim Melentiev 0.5.2-1 78 | - Throw parser error when .json is accessed on response with invalid body. 79 | - Set `Content-Type: application/json` for `:http_request(..., {json = ...})` requests. 80 | 81 | * Tue Apr 21 2020 Maxim Melentiev 0.5.1-1 82 | - Assertions pretty-prints non-string extra messages (useful for custom errors as tables). 83 | - String values in errors are printed as valid Lua strings (with `%q` formatter). 84 | - Add `TARANTOOL_DIR` to rockspec build.variables 85 | - Replace `--error` and `--failure` options with `--fail-fast`. 86 | - Fix stripping luatest trace from backtrace. 87 | - Fix luarocks 3 test engine installation. 88 | 89 | * Wed Jan 22 2020 Maxim Melentiev 0.5.0-1 90 | - `assert_is` treats `box.NULL` and `nil` as different values. 91 | - Add luacov integration. 92 | - Fix `assert_items_equals` for repeated values. Add support for `tuple` items. 93 | - Add `assert_items_include` matcher. 94 | - `assert_equals` uses same comparison rules for nested values. 95 | - Fix generated group names when running files within specific directory. 96 | 97 | * Thu Dec 26 2019 Maxim Melentiev 0.4.0-1 98 | - Fix not working `--exclude`, `--pattern` options 99 | - Fix error messages for `*_covers` matchers 100 | - Raise error when `group()` is called with existing group name. 101 | - Allow dot in group name. 102 | - Prevent using `/` in group name. 103 | - Decide group name from filename for `group()` call without args. 104 | - `assert` returns input values. 105 | - `assert[_not]_equals` works for Tarantool's box.tuple. 106 | - Print tables in lua-compatible way in errors. 107 | - Fix performance issue with large errors messages. 108 | - Unify hooks definition: group hooks are defined via function calls. 109 | - Keep running other groups when group hook failed. 110 | - Prefix and colorize captured output. 111 | - Fix numeric assertions for cdata values. 112 | 113 | * Wed Oct 2 2019 Konstantin Nazarov 0.3.0-1 114 | - Initial release 115 | -------------------------------------------------------------------------------- /luatest/capturing.lua: -------------------------------------------------------------------------------- 1 | local Capture = require('luatest.capture') 2 | local GenericOutput = require('luatest.output.generic') 3 | local OutputBeautifier = require('luatest.output_beautifier') 4 | local utils = require('luatest.utils') 5 | 6 | local function format_captured(name, text) 7 | if text and text:len() > 0 then 8 | return 'Captured ' .. name .. ':\n' .. text .. '\n\n' 9 | else 10 | return '' 11 | end 12 | end 13 | 14 | -- Shortcut to create proxy methods wrapped with `capture.wrap`. 15 | local function wrap_method(enabled, object, name) 16 | utils.patch(object, name, function(super) return function(self, ...) 17 | local args = {self, ...} 18 | if enabled then 19 | return self.capture:wrap(enabled, function() return super(unpack(args)) end) 20 | end 21 | -- Pause OutputBeautifier when capturing is disabled because 22 | -- yields inside can make beautified output bypass the capture. 23 | return OutputBeautifier:synchronize(function() 24 | return self.capture:wrap(enabled, function() return super(unpack(args)) end) 25 | end) 26 | end end) 27 | end 28 | 29 | -- Create new output instance with patched methods. 30 | local function patch_output(capture, output) 31 | output = table.copy(output) 32 | output.capture = capture 33 | 34 | -- Disable capturing when printing output 35 | for name, val in pairs(GenericOutput.mt) do 36 | if type(val) == 'function' then 37 | wrap_method(false, output, name) 38 | end 39 | end 40 | 41 | if output.display_one_failed_test then 42 | -- Print captured output for failed test 43 | utils.patch(output, 'display_one_failed_test', function(super) return function(self, index, node) 44 | super(self, index, node) 45 | if node.capture then 46 | io.stdout:write(format_captured('stdout', node.capture.stdout)) 47 | io.stdout:write(format_captured('stderr', node.capture.stderr)) 48 | end 49 | end end) 50 | end 51 | 52 | return output 53 | end 54 | 55 | -- Patch Runner to capture output in tests and show it only for failed ones. 56 | return function(Runner) 57 | utils.patch(Runner.mt, 'initialize', function(super) return function(self, ...) 58 | if not self.capture then 59 | if self.enable_capture or self.enable_capture == nil then 60 | self.capture = Capture:new() 61 | else 62 | self.capture = Capture:stub() 63 | end 64 | end 65 | 66 | super(self, ...) 67 | 68 | self.output = patch_output(self.capture, self.output) 69 | end end) 70 | 71 | -- Print captured output for any unexpected error. 72 | utils.patch(Runner.mt, 'run', function(super) return function(self, ...) 73 | local args = {self, ...} 74 | local _, code = xpcall(function() return super(unpack(args)) end, function(err) 75 | local message 76 | local captured = {} 77 | if err.type == self.capture.class.CAPTURED_ERROR_TYPE then 78 | message = err.traceback 79 | captured = err.captured 80 | else 81 | message = utils.traceback(err) 82 | if self.capture.enabled then 83 | captured = self.capture:flush() 84 | end 85 | end 86 | message = message .. 87 | format_captured('stdout', captured.stdout) .. 88 | format_captured('stderr', captured.stderr) 89 | self.capture:wrap(false, function() io.stderr:write(message) end) 90 | return -1 91 | end) 92 | return code 93 | end end) 94 | 95 | -- This methods are run outside of the suite, so output needs to be captured. 96 | wrap_method(true, Runner.mt, 'bootstrap') 97 | 98 | -- Main capturing wrapper. 99 | wrap_method(true, Runner.mt, 'run_tests') 100 | 101 | -- Disable capturing to print possible notices. 102 | wrap_method(false, Runner.mt, 'end_test') 103 | 104 | -- Save captured output into result in the case of failure. 105 | utils.patch(Runner.mt, 'protected_call', function(super) return function(self, ...) 106 | local result = super(self, ...) 107 | if self.capture.enabled and result and result.status ~= 'success' then 108 | result.capture = self.capture:flush() 109 | end 110 | return result 111 | end end) 112 | 113 | -- Copy captured output from result to the test node. 114 | utils.patch(Runner.mt, 'update_status', function(super) return function(self, node, result) 115 | if result.capture then 116 | node.capture = result.capture 117 | end 118 | return super(self, node, result) 119 | end end) 120 | 121 | -- Flush capture before and after running a test. 122 | utils.patch(Runner.mt, 'run_test', function(super) return function(self, ...) 123 | self.capture:flush() 124 | local result = super(self, ...) 125 | self.capture:flush() 126 | return result 127 | end end) 128 | end 129 | -------------------------------------------------------------------------------- /luatest/output/junit.lua: -------------------------------------------------------------------------------- 1 | local utils = require('luatest.utils') 2 | local ROCK_VERSION = require('luatest.VERSION') 3 | 4 | -- See directory junitxml for more information about the junit format 5 | local Output = require('luatest.output.generic'):new_class() 6 | 7 | -- Escapes string for XML attributes 8 | function Output.xml_escape(str) 9 | return string.gsub(str, '.', { 10 | ['&'] = "&", 11 | ['"'] = """, 12 | ["'"] = "'", 13 | ['<'] = "<", 14 | ['>'] = ">", 15 | }) 16 | end 17 | 18 | -- Escapes string for CData section 19 | function Output.xml_c_data_escape(str) 20 | return string.gsub(str, ']]>', ']]>') 21 | end 22 | 23 | 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 30 | if node:is('error') then 31 | return table.concat( 32 | {' \n', 33 | ' \n', 34 | ' ', Output.xml_escape(artifacts), '\n', 35 | ' \n'}) 36 | elseif node:is('fail') then 37 | return table.concat( 38 | {' \n', 39 | ' \n', 40 | ' ', Output.xml_escape(artifacts), '\n', 41 | ' \n'}) 42 | elseif node:is('skip') then 43 | return table.concat({' ', Output.xml_escape(node.message or ''),'\n'}) 44 | end 45 | return ' \n' -- (not XSD-compliant! normally shouldn't get here) 46 | end 47 | 48 | function Output.mt:start_suite() 49 | self.output_file_name = assert(self.runner.output_file_name) 50 | -- open xml file early to deal with errors 51 | if string.sub(self.output_file_name,-4) ~= '.xml' then 52 | self.output_file_name = self.output_file_name..'.xml' 53 | end 54 | self.fd = io.open(self.output_file_name, "w") 55 | if self.fd == nil then 56 | error("Could not open file for writing: "..self.output_file_name) 57 | end 58 | 59 | print('# XML output to '..self.output_file_name) 60 | print('# Started on ' .. os.date(nil, self.result.start_time)) 61 | end 62 | 63 | function Output.mt:start_group(group) -- luacheck: no unused 64 | print('# Starting group: ' .. group.name) 65 | end 66 | 67 | function Output.mt:start_test(test) -- luacheck: no unused 68 | print('# Starting test: ' .. test.name) 69 | end 70 | 71 | function Output.mt:update_status(node) -- luacheck: no unused 72 | if node:is('fail') or node:is('xsuccess') then 73 | print('# Failure: ' .. node.message:gsub('\n', '\n# ')) 74 | -- print('# ' .. node.trace) 75 | elseif node:is('error') then 76 | print('# Error: ' .. node.message:gsub('\n', '\n# ')) 77 | -- print('# ' .. node.trace) 78 | end 79 | end 80 | 81 | function Output.mt:end_suite() 82 | print('# ' .. self:status_line()) 83 | 84 | -- XML file writing 85 | self.fd:write('\n') 86 | self.fd:write('\n') 87 | self.fd:write(string.format( 88 | ' \n', 90 | #self.result.tests.all - #self.result.tests.skip, os.date('%Y-%m-%dT%H:%M:%S', self.result.start_time), 91 | self.result.duration, #self.result.tests.error, #self.result.tests.fail + #self.result.tests.xsuccess, 92 | #self.result.tests.skip 93 | )) 94 | self.fd:write(" \n") 95 | self.fd:write(string.format(' \n', _VERSION)) 96 | self.fd:write(string.format(' \n', ROCK_VERSION)) 97 | -- XXX please include system name and version if possible 98 | self.fd:write(" \n") 99 | 100 | for _, node in ipairs(self.result.tests.all) do 101 | self.fd:write(string.format(' \n', 102 | Output.xml_escape(node.group.name or ''), Output.xml_escape(node.name or ''), node.duration)) 103 | if not node:is('success') then 104 | self.fd:write(self.class.node_status_xml(node)) 105 | end 106 | self.fd:write(' \n') 107 | end 108 | 109 | -- Next two lines are needed to validate junit ANT xsd, but really not useful in general: 110 | self.fd:write(' \n') 111 | self.fd:write(' \n') 112 | 113 | self.fd:write(' \n') 114 | self.fd:write('\n') 115 | self.fd:close() 116 | end 117 | 118 | return Output 119 | -------------------------------------------------------------------------------- /test/process_test.lua: -------------------------------------------------------------------------------- 1 | local t = require('luatest') 2 | local g = t.group() 3 | 4 | local fiber = require('fiber') 5 | local fio = require('fio') 6 | local fun = require('fun') 7 | 8 | local Process = t.Process 9 | local Capture = require('luatest.capture') 10 | 11 | local process, kill_after_test 12 | 13 | g.before_each(function() 14 | kill_after_test = true 15 | end) 16 | 17 | g.after_each(function() 18 | if process and kill_after_test then 19 | process:kill() 20 | end 21 | process = nil 22 | end) 23 | 24 | g.test_start = function() 25 | process = Process:start('/bin/sleep', {'5'}) 26 | t.helpers.retrying({timeout = 0.5}, function() 27 | t.assert(process:is_alive()) 28 | end) 29 | process:kill() 30 | t.helpers.retrying({timeout = 0.5}, function() 31 | t.assert_not(process:is_alive()) 32 | end) 33 | kill_after_test = false 34 | end 35 | 36 | g.test_start_with_output_prefix = function() 37 | local capture = Capture:new() 38 | capture:wrap(true, function() 39 | process = Process:start('/bin/echo', {'test content'}, nil, {output_prefix = 'test_prefix'}) 40 | t.helpers.retrying({timeout = 0.5}, function() 41 | t.assert_not(process:is_alive()) 42 | end) 43 | process.output_beautifier.class:synchronize(function() end) 44 | process = nil 45 | end) 46 | local captured = capture:flush() 47 | -- Split on 2 assertions, because of color codes 48 | t.assert_str_contains(captured.stdout, 'test_prefix |') 49 | t.assert_str_contains(captured.stdout, 'test content') 50 | _G.collectgarbage() 51 | end 52 | 53 | g.test_start_with_output_prefix_and_large_output = function() 54 | local capture = Capture:new() 55 | local count = 8000 56 | capture:wrap(true, function() 57 | process = Process:start('/bin/bash', {'-c', "printf 'Hello\n%.0s' {1.." .. count .. "}"}, 58 | nil, {output_prefix = 'test_prefix'}) 59 | t.helpers.retrying({timeout = 0.5, delay = 0.01}, function() 60 | t.assert_not(process:is_alive()) 61 | end) 62 | process.output_beautifier.class:synchronize(function() end) 63 | process = nil 64 | end) 65 | local captured = capture:flush() 66 | -- Split on 2 assertions, because of color codes 67 | t.assert_str_contains(captured.stdout, 'test_prefix |') 68 | t.assert_str_contains(captured.stdout, 'Hello') 69 | t.assert_equals(fun.wrap(captured.stdout:gmatch('Hello')):length(), count) 70 | end 71 | 72 | g.test_start_with_ignore_gc = function() 73 | local process1 = Process:start('/bin/sleep', {'5'}) 74 | local pid1 = process1.pid 75 | local process2 = Process:start('/bin/sleep', {'5'}, {}, {ignore_gc = true}) 76 | local pid2 = process2.pid 77 | t.assert(Process.is_pid_alive(pid1)) 78 | t.assert(Process.is_pid_alive(pid2)) 79 | process1 = nil -- luacheck: no unused 80 | process2 = nil -- luacheck: no unused 81 | _G.collectgarbage() 82 | t.helpers.retrying({timeout = 0.5}, function() 83 | t.assert_not(Process.is_pid_alive(pid1)) 84 | t.assert(Process.is_pid_alive(pid2)) 85 | end) 86 | Process.kill_pid(pid2) 87 | end 88 | 89 | g.test_autokill_gced_process_with_output_prefix = function() 90 | local process1 = Process:start('/bin/sleep', {'5'}, {}, {output_prefix = 'test_prefix'}) 91 | local pid1 = process1.pid 92 | process1 = nil -- luacheck: no unused 93 | _G.collectgarbage() 94 | t.helpers.retrying({timeout = 0.5}, function() 95 | t.assert_not(Process.is_pid_alive(pid1)) 96 | end) 97 | end 98 | 99 | g.test_kill_non_posix = function() 100 | process = Process:start('/bin/sleep', {'5'}) 101 | fiber.sleep(0.1) 102 | process:kill('STOP') 103 | fiber.sleep(0.1) 104 | process:kill('CONT') 105 | end 106 | 107 | g.test_chdir = function() 108 | local file = 'luatest-tmp-file' 109 | local file_copy = file .. '-copy' 110 | if fio.stat('./tmp/' .. file_copy) ~= nil then 111 | assert(fio.unlink('./tmp/' .. file_copy)) 112 | end 113 | os.execute('touch ./tmp/' .. file) 114 | 115 | local proc = Process:start('/bin/cp', {file, file_copy}) 116 | t.helpers.retrying({timeout = 0.5}, function() 117 | t.assert_not(proc:is_alive()) 118 | t.assert_equals(fio.stat('./tmp/' .. file_copy), nil) 119 | end) 120 | 121 | Process:start('/bin/cp', {file, file_copy}, {}, {chdir = './tmp'}) 122 | t.helpers.retrying({timeout = 0.5}, function() 123 | t.assert_not_equals(fio.stat('./tmp/' .. file_copy), nil) 124 | end) 125 | end 126 | 127 | g.test_start_with_debug_hook = function() 128 | local original_hook = {debug.gethook()} 129 | -- Hook is extracted from luacov. This is minimal implementation which makes fork-execve fail. 130 | debug.sethook(function(_, _, level) debug.getinfo(level or 2, 'S') end, 'l') 131 | local n = 100 132 | local env = table.copy(os.environ()) 133 | local processes = fun.range(n):map(function() 134 | return t.Process:start('/bin/sleep', {'10'}, env) 135 | end):totable() 136 | fiber.sleep(0.5) -- wait until all processes called execve 137 | debug.sethook(unpack(original_hook)) 138 | local running = fun.iter(processes):filter(function(x) return x:is_alive() end):totable() 139 | t.assert_equals(#running, n) 140 | end 141 | -------------------------------------------------------------------------------- /test/replica_set_test.lua: -------------------------------------------------------------------------------- 1 | local fio = require('fio') 2 | local t = require('luatest') 3 | local ReplicaSet = require('luatest.replica_set') 4 | 5 | local g = t.group() 6 | local Server = t.Server 7 | 8 | g.before_each(function() 9 | g.rs = ReplicaSet:new() 10 | g.box_cfg = { 11 | replication_timeout = 0.1, 12 | replication_connect_timeout = 10, 13 | replication_sync_lag = 0.01, 14 | replication_connect_quorum = 3, 15 | replication = { 16 | Server.build_listen_uri('replica1', g.rs.id), 17 | Server.build_listen_uri('replica2', g.rs.id), 18 | Server.build_listen_uri('replica3', g.rs.id), 19 | } 20 | } 21 | end) 22 | 23 | g.before_test('test_save_rs_artifacts_when_test_failed', function() 24 | g.rs:build_and_add_server({alias = 'replica1', box_cfg = g.box_cfg}) 25 | g.rs:build_and_add_server({alias = 'replica2', box_cfg = g.box_cfg}) 26 | g.rs:build_and_add_server({alias = 'replica3', box_cfg = g.box_cfg}) 27 | g.rs:start() 28 | 29 | g.rs_artifacts = ('%s/artifacts/%s'):format(Server.vardir, g.rs.id) 30 | g.s1_artifacts = ('%s/%s'):format(g.rs_artifacts, g.rs:get_server('replica1').id) 31 | g.s2_artifacts = ('%s/%s'):format(g.rs_artifacts, g.rs:get_server('replica2').id) 32 | g.s3_artifacts = ('%s/%s'):format(g.rs_artifacts, g.rs:get_server('replica3').id) 33 | end) 34 | 35 | g.test_save_rs_artifacts_when_test_failed = function() 36 | local test = rawget(_G, 'current_test') 37 | -- the test must be failed to save artifacts 38 | test.status = 'fail' 39 | g.rs:drop() 40 | test.status = 'success' 41 | 42 | t.assert_equals(fio.path.exists(g.rs_artifacts), true) 43 | t.assert_equals(fio.path.is_dir(g.rs_artifacts), true) 44 | 45 | t.assert_equals(fio.path.exists(g.s1_artifacts), true) 46 | t.assert_equals(fio.path.is_dir(g.s1_artifacts), true) 47 | 48 | t.assert_equals(fio.path.exists(g.s2_artifacts), true) 49 | t.assert_equals(fio.path.is_dir(g.s2_artifacts), true) 50 | 51 | t.assert_equals(fio.path.exists(g.s3_artifacts), true) 52 | t.assert_equals(fio.path.is_dir(g.s3_artifacts), true) 53 | end 54 | 55 | g.before_test('test_save_rs_artifacts_when_server_workdir_passed', function() 56 | local s1_workdir = ('%s/%s'):format(Server.vardir, os.tmpname()) 57 | local s2_workdir = ('%s/%s'):format(Server.vardir, os.tmpname()) 58 | local s3_workdir = ('%s/%s'):format(Server.vardir, os.tmpname()) 59 | 60 | g.rs:build_and_add_server({workdir = s1_workdir, alias = 'replica1', box_cfg = g.box_cfg}) 61 | g.rs:build_and_add_server({workdir = s2_workdir, alias = 'replica2', box_cfg = g.box_cfg}) 62 | g.rs:build_and_add_server({workdir = s3_workdir, alias = 'replica3', box_cfg = g.box_cfg}) 63 | g.rs:start() 64 | 65 | g.rs_artifacts = ('%s/artifacts/%s'):format(Server.vardir, g.rs.id) 66 | g.s1_artifacts = ('%s/%s'):format(g.rs_artifacts, g.rs:get_server('replica1').id) 67 | g.s2_artifacts = ('%s/%s'):format(g.rs_artifacts, g.rs:get_server('replica2').id) 68 | g.s3_artifacts = ('%s/%s'):format(g.rs_artifacts, g.rs:get_server('replica3').id) 69 | end) 70 | 71 | g.test_save_rs_artifacts_when_server_workdir_passed = function() 72 | local test = rawget(_G, 'current_test') 73 | -- the test must be failed to save artifacts 74 | test.status = 'fail' 75 | g.rs:drop() 76 | test.status = 'success' 77 | 78 | t.assert_equals(fio.path.exists(g.rs_artifacts), true) 79 | t.assert_equals(fio.path.is_dir(g.rs_artifacts), true) 80 | 81 | t.assert_equals(fio.path.exists(g.s1_artifacts), true) 82 | t.assert_equals(fio.path.is_dir(g.s1_artifacts), true) 83 | 84 | t.assert_equals(fio.path.exists(g.s2_artifacts), true) 85 | t.assert_equals(fio.path.is_dir(g.s2_artifacts), true) 86 | 87 | t.assert_equals(fio.path.exists(g.s3_artifacts), true) 88 | t.assert_equals(fio.path.is_dir(g.s3_artifacts), true) 89 | 90 | end 91 | 92 | g.test_rs_no_socket_collision_with_custom_alias = function() 93 | local s1 = g.rs:build_server({alias = 'foo'}) 94 | local s2 = g.rs:build_server({alias = 'bar'}) 95 | 96 | t.assert(s1.vardir:find(g.rs.id, 1, true)) 97 | t.assert(s2.vardir:find(g.rs.id, 1, true)) 98 | t.assert_equals(s1.net_box_uri, ('%s/foo.sock'):format(s1.vardir)) 99 | t.assert_equals(s2.net_box_uri, ('%s/bar.sock'):format(s2.vardir)) 100 | end 101 | 102 | g.after_test('test_rs_no_socket_collision_with_custom_alias', function() 103 | g.rs:drop() 104 | end) 105 | 106 | g.test_rs_custom_properties_are_not_overridden = function() 107 | local socket = ('%s/custom.sock'):format(Server.vardir) 108 | local workdir = ('%s/custom'):format(Server.vardir) 109 | 110 | local s = g.rs:build_server({net_box_uri = socket, workdir = workdir}) 111 | 112 | t.assert_equals(s.net_box_uri, socket) 113 | t.assert_equals(s.workdir, workdir) 114 | end 115 | 116 | g.after_test('test_rs_custom_properties_are_not_overridden', function() 117 | g.rs:drop() 118 | end) 119 | 120 | g.test_rs_raise_error_when_add_custom_server = function() 121 | local s = Server:new() 122 | 123 | t.assert_error_msg_contains( 124 | 'Server should be built via `ReplicaSet:build_server` function', 125 | function() g.rs:add_server(s) end) 126 | end 127 | 128 | g.after_test('test_rs_raise_error_when_add_custom_server', function() 129 | g.rs:drop() 130 | end) 131 | -------------------------------------------------------------------------------- /luatest/output/text.lua: -------------------------------------------------------------------------------- 1 | local utils = require('luatest.utils') 2 | local Output = require('luatest.output.generic'):new_class() 3 | 4 | Output.BOLD_CODE = '\x1B[1m' 5 | Output.ERROR_COLOR_CODE = Output.BOLD_CODE .. '\x1B[31m' -- red 6 | Output.SUCCESS_COLOR_CODE = Output.BOLD_CODE .. '\x1B[32m' -- green 7 | Output.WARN_COLOR_CODE = Output.BOLD_CODE .. '\x1B[33m' -- yellow 8 | Output.RESET_TERM = '\x1B[0m' 9 | 10 | function Output.mt:start_suite() 11 | if self.runner.seed then 12 | print('Running with --shuffle ' .. self.runner.shuffle .. ':' .. self.runner.seed) 13 | end 14 | if self.verbosity >= self.class.VERBOSITY.VERBOSE then 15 | print('Started on '.. os.date(nil, self.result.start_time)) 16 | end 17 | end 18 | 19 | function Output.mt:start_test(test) -- luacheck: no unused 20 | if self.verbosity >= self.class.VERBOSITY.VERBOSE then 21 | io.stdout:write(" ", test.name, " ... ") 22 | if self.verbosity >= self.class.VERBOSITY.REPEAT then 23 | io.stdout:write("\n") 24 | end 25 | end 26 | end 27 | 28 | function Output.mt:end_test(node) 29 | if node:is('success') or node:is('xfail') then 30 | if self.verbosity >= self.class.VERBOSITY.VERBOSE then 31 | if self.verbosity >= self.class.VERBOSITY.REPEAT then 32 | io.stdout:write(" ", node.name, " ... ") 33 | end 34 | local duration = string.format("(%0.3fs) ", node.duration) 35 | io.stdout:write(duration) 36 | io.stdout:write(node:is('xfail') and "xfail\n" or "Ok\n") 37 | if node:is('xfail') then 38 | print(node.message) 39 | end 40 | else 41 | io.stdout:write(".") 42 | io.stdout:flush() 43 | end 44 | else 45 | if self.verbosity >= self.class.VERBOSITY.VERBOSE then 46 | if self.verbosity >= self.class.VERBOSITY.REPEAT then 47 | io.stdout:write(" ", node.name, " ... ") 48 | end 49 | local duration = string.format("(%0.3fs) ", node.duration) 50 | print(duration .. node.status) 51 | print(node.message) 52 | else 53 | -- write only the first character of status E, F, S or X 54 | io.stdout:write(string.sub(node.status, 1, 1):upper()) 55 | io.stdout:flush() 56 | end 57 | end 58 | end 59 | 60 | function Output.mt:display_one_failed_test(index, fail) -- luacheck: no unused 61 | print(index..") " .. fail.name .. self.class.ERROR_COLOR_CODE) 62 | print(fail.message .. self.class.RESET_TERM) 63 | print(fail.trace) 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 69 | end 70 | end 71 | 72 | function Output.mt:display_errored_tests() 73 | if #self.result.tests.error > 0 then 74 | print(self.class.BOLD_CODE) 75 | print("Tests with errors:") 76 | print("------------------") 77 | print(self.class.RESET_TERM) 78 | for i, v in ipairs(self.result.tests.error) do 79 | self:display_one_failed_test(i, v) 80 | end 81 | end 82 | end 83 | 84 | function Output.mt:display_failed_tests() 85 | if #self.result.tests.fail > 0 then 86 | print(self.class.BOLD_CODE) 87 | print("Failed tests:") 88 | print("-------------") 89 | print(self.class.RESET_TERM) 90 | for i, v in ipairs(self.result.tests.fail) do 91 | self:display_one_failed_test(i, v) 92 | end 93 | end 94 | end 95 | 96 | function Output.mt:display_xsucceeded_tests() 97 | if #self.result.tests.xsuccess > 0 then 98 | print(self.class.BOLD_CODE) 99 | print("Tests with an unexpected success:") 100 | print("-------------") 101 | print(self.class.RESET_TERM) 102 | for i, v in ipairs(self.result.tests.xsuccess) do 103 | self:display_one_failed_test(i, v) 104 | end 105 | end 106 | end 107 | 108 | function Output.mt:end_suite() 109 | if self.verbosity >= self.class.VERBOSITY.VERBOSE then 110 | print("=========================================================") 111 | else 112 | print() 113 | end 114 | self:display_errored_tests() 115 | self:display_failed_tests() 116 | self:display_xsucceeded_tests() 117 | print(self:status_line({ 118 | success = self.class.SUCCESS_COLOR_CODE, 119 | failure = self.class.ERROR_COLOR_CODE, 120 | reset = self.class.RESET_TERM, 121 | xfail = self.class.WARN_COLOR_CODE, 122 | })) 123 | if self.result.notSuccessCount == 0 then 124 | print('OK') 125 | end 126 | 127 | local list = table.copy(self.result.tests.fail) 128 | for _, x in pairs(self.result.tests.error) do 129 | table.insert(list, x) 130 | end 131 | if #list > 0 then 132 | table.sort(list, function(a, b) return a.name < b.name end) 133 | if self.verbosity >= self.class.VERBOSITY.VERBOSE then 134 | print("\n=========================================================") 135 | else 136 | print() 137 | end 138 | print(self.class.BOLD_CODE .. 'Failed tests:\n' .. self.class.ERROR_COLOR_CODE) 139 | for _, x in pairs(list) do 140 | print(x.name) 141 | end 142 | io.stdout:write(self.class.RESET_TERM) 143 | end 144 | end 145 | 146 | return Output 147 | -------------------------------------------------------------------------------- /luatest/justrun.lua: -------------------------------------------------------------------------------- 1 | --- Simple Tarantool runner and output catcher. 2 | -- 3 | -- Sometimes it is necessary to run tarantool with particular arguments and 4 | -- verify its output. `luatest.server` provides a supervisor like 5 | -- interface: an instance is started, calls box.cfg() and we can 6 | -- communicate with it using net.box. Another helper in tarantool/tarantool, 7 | -- `test.interactive_tarantool`, aims to solve all the problems around 8 | -- readline console and also provides ability to communicate with the 9 | -- instance interactively. 10 | -- 11 | -- However, there is nothing like 'just run tarantool with given args and 12 | -- give me its output'. 13 | -- 14 | -- @module luatest.justrun 15 | 16 | local checks = require('checks') 17 | local fun = require('fun') 18 | local json = require('json') 19 | local fiber = require('fiber') 20 | 21 | local log = require('luatest.log') 22 | 23 | local justrun = {} 24 | 25 | local function collect_stderr(ph) 26 | local f = fiber.new(function() 27 | local fiber_name = "child's stderr collector" 28 | fiber.name(fiber_name, {truncate = true}) 29 | 30 | local chunks = {} 31 | 32 | while true do 33 | local chunk, err = ph:read({stderr = true}) 34 | if chunk == nil then 35 | log.warn('%s: got error, exiting: %s', fiber_name, err) 36 | break 37 | end 38 | if chunk == '' then 39 | log.info('%s: got EOF, exiting', fiber_name) 40 | break 41 | end 42 | table.insert(chunks, chunk) 43 | end 44 | 45 | -- Glue all chunks, strip trailing newline. 46 | return table.concat(chunks):rstrip() 47 | end) 48 | f:set_joinable(true) 49 | return f 50 | end 51 | 52 | local function cancel_stderr_fiber(stderr_fiber) 53 | if stderr_fiber == nil then 54 | return 55 | end 56 | stderr_fiber:cancel() 57 | end 58 | 59 | local function join_stderr_fiber(stderr_fiber) 60 | if stderr_fiber == nil then 61 | return 62 | end 63 | return select(2, assert(stderr_fiber:join())) 64 | end 65 | 66 | --- Run tarantool in given directory with given environment and 67 | -- command line arguments and catch its output. 68 | -- 69 | -- Expects JSON lines as the output and parses it into an array 70 | -- (it can be disabled using `nojson` option). 71 | -- 72 | -- Options: 73 | -- 74 | -- - nojson (boolean, default: false) 75 | -- 76 | -- Don't attempt to decode stdout as a stream of JSON lines, 77 | -- return as is. 78 | -- 79 | -- - stderr (boolean, default: false) 80 | -- 81 | -- Collect stderr and place it into the `stderr` field of the 82 | -- return value 83 | -- 84 | -- - quote_args (boolean, default: false) 85 | -- 86 | -- Quote CLI arguments before concatenating them into a shell 87 | -- command. 88 | -- 89 | -- @string dir Directory where the process will run. 90 | -- @tparam table env Environment variables for the process. 91 | -- @tparam table args Options that will be passed when the process starts. 92 | -- @tparam[opt] table opts Custom options: nojson, stderr and quote_args. 93 | -- @treturn table 94 | function justrun.tarantool(dir, env, args, opts) 95 | checks('string', 'table', 'table', '?table') 96 | opts = opts or {} 97 | 98 | local popen = require('popen') 99 | 100 | -- Prevent system/user inputrc configuration file from 101 | -- influencing testing code. 102 | env['INPUTRC'] = '/dev/null' 103 | 104 | local tarantool_exe = arg[-1] 105 | -- Use popen.shell() instead of popen.new() due to lack of 106 | -- cwd option in popen (gh-5633). 107 | local env_str = table.concat(fun.iter(env):map(function(k, v) 108 | return ('%s=%q'):format(k, v) 109 | end):totable(), ' ') 110 | local args_str = table.concat(fun.iter(args):map(function(v) 111 | return opts.quote_args and ('%q'):format(v) or v 112 | end):totable(), ' ') 113 | local command = ('cd %s && %s %s %s'):format(dir, env_str, tarantool_exe, 114 | args_str) 115 | log.info('Running a command: %s', command) 116 | local mode = opts.stderr and 'rR' or 'r' 117 | local ph = popen.shell(command, mode) 118 | 119 | local stderr_fiber 120 | if opts.stderr then 121 | stderr_fiber = collect_stderr(ph) 122 | end 123 | 124 | -- Read everything until EOF. 125 | local chunks = {} 126 | while true do 127 | local chunk, err = ph:read() 128 | if chunk == nil then 129 | cancel_stderr_fiber(stderr_fiber) 130 | ph:close() 131 | error(err) 132 | end 133 | if chunk == '' then -- EOF 134 | break 135 | end 136 | table.insert(chunks, chunk) 137 | end 138 | 139 | local exit_code = ph:wait().exit_code 140 | local stderr = join_stderr_fiber(stderr_fiber) 141 | ph:close() 142 | 143 | -- If an error occurs, discard the output and return only the 144 | -- exit code. However, return stderr. 145 | if exit_code ~= 0 then 146 | return { 147 | exit_code = exit_code, 148 | stderr = stderr, 149 | } 150 | end 151 | 152 | -- Glue all chunks, strip trailing newline. 153 | local res = table.concat(chunks):rstrip() 154 | log.info('Command output:\n%s', res) 155 | 156 | -- Decode JSON object per line into array of tables (if 157 | -- `nojson` option is not passed). 158 | local decoded 159 | if opts.nojson then 160 | decoded = res 161 | else 162 | decoded = fun.iter(res:split('\n')):map(json.decode):totable() 163 | end 164 | 165 | return { 166 | exit_code = exit_code, 167 | stdout = decoded, 168 | stderr = stderr, 169 | } 170 | end 171 | 172 | return justrun 173 | -------------------------------------------------------------------------------- /test/capturing_test.lua: -------------------------------------------------------------------------------- 1 | local t = require('luatest') 2 | local g = t.group() 3 | 4 | local helper = require('test.helpers.general') 5 | local Capture = require('luatest.capture') 6 | local capture = Capture:new() 7 | 8 | -- Disable luatest logging to avoid capturing it. 9 | require('luatest.log').info = function() end 10 | 11 | g.before_each(function() 12 | capture:enable() 13 | end) 14 | 15 | g.after_each(function() 16 | capture:flush() 17 | capture:disable() 18 | end) 19 | 20 | local function assert_capture_restored() 21 | io.stdout:write('capture-restored') 22 | t.assert_equals(capture:flush(), {stdout = 'capture-restored', stderr = ''}) 23 | end 24 | 25 | local function assert_captured(fn) 26 | helper.run_suite(fn) 27 | local captured = capture:flush() 28 | t.assert_not_str_contains(captured.stdout, 'test-out') 29 | t.assert_not_str_contains(captured.stderr, 'test-err') 30 | assert_capture_restored() 31 | end 32 | 33 | local function assert_shown(fn) 34 | helper.run_suite(fn) 35 | local captured = capture:flush() 36 | t.assert_str_contains(captured.stdout, 'Captured stdout:\ntest-out') 37 | t.assert_str_contains(captured.stdout, 'Captured stderr:\ntest-err') 38 | t.assert_equals(captured.stderr, '') 39 | assert_capture_restored() 40 | end 41 | 42 | local function assert_error(fn) 43 | t.assert_equals(helper.run_suite(fn), -1) 44 | local captured = capture:flush() 45 | t.assert_str_contains(captured.stderr, 'custom-error') 46 | t.assert_str_contains(captured.stderr, 'Captured stdout:\ntest-out') 47 | t.assert_str_contains(captured.stderr, 'Captured stderr:\ntest-err') 48 | assert_capture_restored() 49 | end 50 | 51 | local function write_to_io() 52 | io.stdout:write('test-out') 53 | io.stderr:write('test-err') 54 | end 55 | 56 | g.test_example = function() 57 | assert_captured(function(lu2) 58 | lu2.group('test').test = write_to_io 59 | end) 60 | end 61 | 62 | g.test_example_failed = function() 63 | assert_shown(function(lu2) 64 | lu2.group('test').test = function() 65 | write_to_io() 66 | error('test') 67 | end 68 | end) 69 | 70 | -- Don't show captures from group hooks when test failed. 71 | assert_captured(function(lu2) 72 | local group = lu2.group('test') 73 | group.before_all(write_to_io) 74 | group.after_all(write_to_io) 75 | group.test = function() error('custom-error') end 76 | end) 77 | 78 | assert_shown(function(lu2) 79 | local group = lu2.group('test') 80 | group.before_each(write_to_io) 81 | group.after_each(write_to_io) 82 | group.test = function() error('custom-error') end 83 | end) 84 | end 85 | 86 | g.test_example_hook = function() 87 | assert_captured(function(lu2) 88 | local group = lu2.group('test') 89 | group.before_each(write_to_io) 90 | group.after_each(write_to_io) 91 | group.test = function() end 92 | end) 93 | end 94 | 95 | g.test_example_hook_failed = function() 96 | assert_shown(function(lu2) 97 | local group = lu2.group('test') 98 | group.before_each(function() 99 | write_to_io() 100 | error('test') 101 | end) 102 | group.test = function() end 103 | end) 104 | 105 | assert_shown(function(lu2) 106 | local group = lu2.group('test') 107 | group.after_each(function() 108 | write_to_io() 109 | error('test') 110 | end) 111 | group.test = function() end 112 | end) 113 | 114 | assert_shown(function(lu2) 115 | local group = lu2.group('test') 116 | group.before_each(write_to_io) 117 | group.test = function() error('test') end 118 | end) 119 | end 120 | 121 | g.test_load_tests = function() 122 | assert_captured(write_to_io) 123 | end 124 | 125 | g.test_load_tests_failed = function() 126 | assert_error(function() 127 | write_to_io() 128 | error('custom-error') 129 | end) 130 | end 131 | 132 | g.test_group_hook = function() 133 | assert_captured(function(lu2) 134 | local group = lu2.group('test') 135 | local hook = write_to_io 136 | group.before_all(hook) 137 | group.after_all(hook) 138 | group.test = function() end 139 | 140 | local group2 = lu2.group('test2') 141 | group2.before_all(hook) 142 | group2.after_all(hook) 143 | group2.test = function() end 144 | end) 145 | end 146 | 147 | g.test_group_hook_failed = function() 148 | assert_shown(function(lu2) 149 | local group = lu2.group('test') 150 | group.before_all(function() 151 | write_to_io() 152 | error('custom-error') 153 | end) 154 | group.test = function() end 155 | end) 156 | 157 | assert_shown(function(lu2) 158 | local group = lu2.group('test') 159 | group.after_all(function() 160 | write_to_io() 161 | error('custom-error') 162 | end) 163 | group.test = function() end 164 | end) 165 | end 166 | 167 | g.test_suite_hook = function() 168 | assert_captured(function(lu2) 169 | local group = lu2.group('test') 170 | local hook = write_to_io 171 | lu2.before_suite(hook) 172 | lu2.after_all(hook) 173 | group.test = function() end 174 | end) 175 | end 176 | 177 | g.test_suite_hook_failed = function() 178 | assert_error(function(lu2) 179 | lu2.group('test').test = function() end 180 | lu2.before_suite(function() 181 | write_to_io() 182 | error('custom-error') 183 | end) 184 | end) 185 | 186 | assert_error(function(lu2) 187 | lu2.group('test').test = function() end 188 | lu2.after_suite(function() 189 | write_to_io() 190 | error('custom-error') 191 | end) 192 | end) 193 | end 194 | -------------------------------------------------------------------------------- /luatest/mismatch_formatter.lua: -------------------------------------------------------------------------------- 1 | local comparator = require('luatest.comparator') 2 | local pp = require('luatest.pp') 3 | 4 | local export = { 5 | LIST_DIFF_ANALYSIS_THRESHOLD = 10, -- display deep analysis for more than 10 items 6 | } 7 | 8 | local function extend_with_str_fmt(res, ...) 9 | table.insert(res, string.format(...)) 10 | end 11 | 12 | -- Prepares a nice error message when comparing tables which are lists, performing a deeper 13 | -- analysis. 14 | -- 15 | -- Returns: {success, result} 16 | -- * success: false if deep analysis could not be performed 17 | -- in this case, just use standard assertion message 18 | -- * result: if success is true, a multi-line string with deep analysis of the two lists 19 | local function mismatch_formatting_pure_list(table_a, table_b) 20 | local result = {} 21 | 22 | local len_a, len_b, refa, refb = #table_a, #table_b, '', '' 23 | if pp.TABLE_REF_IN_ERROR_MSG then 24 | refa, refb = string.format('<%s> ', pp.table_ref(table_a)), string.format('<%s> ', pp.table_ref(table_b)) 25 | end 26 | local longest, shortest = math.max(len_a, len_b), math.min(len_a, len_b) 27 | local deltalv = longest - shortest 28 | 29 | local commonUntil = shortest 30 | for i = 1, shortest do 31 | if not comparator.equals(table_a[i], table_b[i]) then 32 | commonUntil = i - 1 33 | break 34 | end 35 | end 36 | 37 | local commonBackTo = shortest - 1 38 | for i = 0, shortest - 1 do 39 | if not comparator.equals(table_a[len_a-i], table_b[len_b-i]) then 40 | commonBackTo = i - 1 41 | break 42 | end 43 | end 44 | 45 | 46 | table.insert(result, 'List difference analysis:') 47 | if len_a == len_b then 48 | -- TODO: handle expected/actual naming 49 | extend_with_str_fmt(result, '* lists %sA (actual) and %sB (expected) have the same size', refa, refb) 50 | else 51 | extend_with_str_fmt(result, 52 | '* list sizes differ: list %sA (actual) has %d items, list %sB (expected) has %d items', 53 | refa, len_a, refb, len_b 54 | ) 55 | end 56 | 57 | extend_with_str_fmt(result, '* lists A and B start differing at index %d', commonUntil+1) 58 | if commonBackTo >= 0 then 59 | if deltalv > 0 then 60 | extend_with_str_fmt(result, '* lists A and B are equal again from index %d for A, %d for B', 61 | len_a-commonBackTo, len_b-commonBackTo) 62 | else 63 | extend_with_str_fmt(result, '* lists A and B are equal again from index %d', len_a-commonBackTo) 64 | end 65 | end 66 | 67 | local function insert_ab_value(ai, bi) 68 | bi = bi or ai 69 | if comparator.equals(table_a[ai], table_b[bi]) then 70 | return extend_with_str_fmt(result, ' = A[%d], B[%d]: %s', ai, bi, pp.tostring(table_a[ai])) 71 | else 72 | extend_with_str_fmt(result, ' - A[%d]: %s', ai, pp.tostring(table_a[ai])) 73 | extend_with_str_fmt(result, ' + B[%d]: %s', bi, pp.tostring(table_b[bi])) 74 | end 75 | end 76 | 77 | -- common parts to list A & B, at the beginning 78 | if commonUntil > 0 then 79 | table.insert(result, '* Common parts:') 80 | for i = 1, commonUntil do 81 | insert_ab_value(i) 82 | end 83 | end 84 | 85 | -- diffing parts to list A & B 86 | if commonUntil < shortest - commonBackTo - 1 then 87 | table.insert(result, '* Differing parts:') 88 | for i = commonUntil + 1, shortest - commonBackTo - 1 do 89 | insert_ab_value(i) 90 | end 91 | end 92 | 93 | -- display indexes of one list, with no match on other list 94 | if shortest - commonBackTo <= longest - commonBackTo - 1 then 95 | table.insert(result, '* Present only in one list:') 96 | for i = shortest - commonBackTo, longest - commonBackTo - 1 do 97 | if len_a > len_b then 98 | extend_with_str_fmt(result, ' - A[%d]: %s', i, pp.tostring(table_a[i])) 99 | -- table.insert(result, '+ (no matching B index)') 100 | else 101 | -- table.insert(result, '- no matching A index') 102 | extend_with_str_fmt(result, ' + B[%d]: %s', i, pp.tostring(table_b[i])) 103 | end 104 | end 105 | end 106 | 107 | -- common parts to list A & B, at the end 108 | if commonBackTo >= 0 then 109 | table.insert(result, '* Common parts at the end of the lists') 110 | for i = longest - commonBackTo, longest do 111 | if len_a > len_b then 112 | insert_ab_value(i, i-deltalv) 113 | else 114 | insert_ab_value(i-deltalv, i) 115 | end 116 | end 117 | end 118 | 119 | return true, table.concat(result, '\n') 120 | end 121 | 122 | -- Prepares a nice error message when comparing tables, performing a deeper 123 | -- analysis. 124 | -- 125 | -- Arguments: 126 | -- * table_a, table_b: tables to be compared 127 | -- * doDeepAnalysis: 128 | -- nil: (the default if not specified) perform deep analysis 129 | -- only for big lists and big dictionnaries 130 | -- true: always perform deep analysis 131 | -- false: never perform deep analysis 132 | -- 133 | -- Returns: {success, result} 134 | -- * success: false if deep analysis could not be performed 135 | -- in this case, just use standard assertion message 136 | -- * result: if success is true, a multi-line string with deep analysis of the two lists 137 | function export.format(table_a, table_b, doDeepAnalysis) 138 | -- check if table_a & table_b are suitable for deep analysis 139 | if type(table_a) ~= 'table' or type(table_b) ~= 'table' then 140 | return false 141 | end 142 | 143 | if doDeepAnalysis == false then 144 | return false 145 | end 146 | 147 | local len_a, len_b, isPureList = #table_a, #table_b, true 148 | 149 | for k1 in pairs(table_a) do 150 | if type(k1) ~= 'number' or k1 > len_a then 151 | -- this table a mapping 152 | isPureList = false 153 | break 154 | end 155 | end 156 | 157 | if isPureList then 158 | for k2 in pairs(table_b) do 159 | if type(k2) ~= 'number' or k2 > len_b then 160 | -- this table a mapping 161 | isPureList = false 162 | break 163 | end 164 | end 165 | end 166 | 167 | if isPureList and math.min(len_a, len_b) < export.LIST_DIFF_ANALYSIS_THRESHOLD then 168 | if not (doDeepAnalysis == true) then 169 | return false 170 | end 171 | end 172 | 173 | if isPureList then 174 | return mismatch_formatting_pure_list(table_a, table_b) 175 | else 176 | return false 177 | end 178 | end 179 | 180 | return export 181 | -------------------------------------------------------------------------------- /luatest/treegen.lua: -------------------------------------------------------------------------------- 1 | --- Working tree generator. 2 | -- 3 | -- Generates a tree of Lua files using provided templates and 4 | -- filenames. 5 | -- 6 | -- @usage 7 | -- 8 | -- local t = require('luatest') 9 | -- local treegen = require('luatest.treegen') 10 | -- 11 | -- local g = t.group() 12 | -- 13 | -- g.test_foo = function(g) 14 | -- treegen.add_template('^.*$', 'test_script') 15 | -- local dir = treegen.prepare_directory({'foo/bar.lua', 'main.lua'}) 16 | -- ... 17 | -- end 18 | -- 19 | -- @module luatest.treegen 20 | 21 | local hooks = require('luatest.hooks') 22 | 23 | local fio = require('fio') 24 | local fun = require('fun') 25 | local checks = require('checks') 26 | 27 | local log = require('luatest.log') 28 | 29 | local treegen = { 30 | _group = {} 31 | } 32 | 33 | local function find_template(group, script) 34 | for position, template_def in ipairs(group._treegen.templates) do 35 | if script:match(template_def.pattern) then 36 | return position, template_def.template 37 | end 38 | end 39 | error(("treegen: can't find a template for script %q"):format(script)) 40 | end 41 | 42 | --- Write provided content into the given directory. 43 | -- 44 | -- @string directory Directory where the content will be created. 45 | -- @string filename File to write (possible nested path: /foo/bar/main.lua). 46 | -- @string content The body to write. 47 | -- @return string 48 | function treegen.write_file(directory, filename, content) 49 | checks('string', 'string', 'string') 50 | local content_abspath = fio.pathjoin(directory, filename) 51 | local flags = {'O_CREAT', 'O_WRONLY', 'O_TRUNC'} 52 | local mode = tonumber('644', 8) 53 | 54 | local contentdir_abspath = fio.dirname(content_abspath) 55 | log.info('Creating a directory: %s', contentdir_abspath) 56 | fio.mktree(contentdir_abspath) 57 | 58 | log.info('Writing a content: %s', content_abspath) 59 | local fh = fio.open(content_abspath, flags, mode) 60 | fh:write(content) 61 | fh:close() 62 | return content_abspath 63 | end 64 | 65 | -- Generate a content that follows a template and write it at the 66 | -- given path in the given directory. 67 | -- 68 | -- @table group Group of tests. 69 | -- @string directory Directory where the content will be created. 70 | -- @string filename File to write (possible nested path: /foo/bar/main.lua). 71 | -- @table replacements List of replacement templates. 72 | -- @return string 73 | local function gen_content(group, directory, filename, replacements) 74 | checks('table', 'string', 'string', 'table') 75 | local _, template = find_template(group, filename) 76 | replacements = fun.chain({filename = filename}, replacements):tomap() 77 | local body = template:gsub('<(.-)>', replacements) 78 | return treegen.write_file(directory, filename, body) 79 | end 80 | 81 | --- Initialize treegen module in the given group of tests. 82 | -- 83 | -- @tab group Group of tests. 84 | local function init(group) 85 | checks('table') 86 | group._treegen = { 87 | tempdirs = {}, 88 | templates = {} 89 | } 90 | treegen._group = group 91 | end 92 | 93 | --- Remove all temporary directories created by the test 94 | -- unless KEEP_DATA environment variable is set to a 95 | -- non-empty value. 96 | local function clean() 97 | if treegen._group._treegen == nil then 98 | return 99 | end 100 | 101 | local dirs = table.copy(treegen._group._treegen.tempdirs) or {} 102 | treegen._group._treegen.tempdirs = nil 103 | 104 | local keep_data = (os.getenv('KEEP_DATA') or '') ~= '' 105 | 106 | for _, dir in ipairs(dirs) do 107 | if keep_data then 108 | log.info('Left intact due to KEEP_DATA env var: %s', dir) 109 | else 110 | log.info('Recursively removing: %s', dir) 111 | fio.rmtree(dir) 112 | end 113 | end 114 | 115 | treegen._group._treegen.templates = nil 116 | end 117 | 118 | --- Save the template with the given pattern. 119 | -- 120 | -- @string pattern File name template 121 | -- @string template A content template for creating a file. 122 | function treegen.add_template(pattern, template) 123 | checks('string', 'string') 124 | table.insert(treegen._group._treegen.templates, { 125 | pattern = pattern, 126 | template = template, 127 | }) 128 | end 129 | 130 | --- Remove the template by pattern. 131 | -- 132 | -- @string pattern File name template 133 | function treegen.remove_template(pattern) 134 | checks('string') 135 | local is_found, position, _ = pcall(find_template, treegen._group, pattern) 136 | if is_found then 137 | table.remove(treegen._group._treegen.templates, position) 138 | end 139 | end 140 | 141 | --- Create a temporary directory with given contents. 142 | -- 143 | -- The contents are generated using templates added by 144 | -- treegen.add_template(). 145 | -- 146 | -- @usage 147 | -- 148 | -- Example for {'foo/bar.lua', 'baz.lua'}: 149 | -- 150 | -- / 151 | -- + tmp/ 152 | -- + rfbWOJ/ 153 | -- + foo/ 154 | -- | + bar.lua 155 | -- + baz.lua 156 | -- 157 | -- The return value is '/tmp/rfbWOJ' for this example. 158 | -- 159 | -- @tab contents List of bodies of the content to write. 160 | -- @tab[opt] replacements List of replacement templates. 161 | -- @return string 162 | function treegen.prepare_directory(contents, replacements) 163 | checks('?table', '?table') 164 | replacements = replacements or {} 165 | 166 | local dir = fio.tempdir() 167 | 168 | -- fio.tempdir() follows the TMPDIR environment variable. 169 | -- If it ends with a slash, the return value contains a double 170 | -- slash in the middle: for example, if TMPDIR=/tmp/, the 171 | -- result is like `/tmp//rfbWOJ`. 172 | -- 173 | -- It looks harmless on the first glance, but this directory 174 | -- path may be used later to form an URI for a Unix domain 175 | -- socket. As result the URI looks like 176 | -- `unix/:/tmp//rfbWOJ/instance-001.iproto`. 177 | -- 178 | -- It confuses net_box.connect(): it reports EAI_NONAME error 179 | -- from getaddrinfo(). 180 | -- 181 | -- It seems, the reason is a peculiar of the URI parsing: 182 | -- 183 | -- tarantool> uri.parse('unix/:/foo/bar.iproto') 184 | -- --- 185 | -- - host: unix/ 186 | -- service: /foo/bar.iproto 187 | -- unix: /foo/bar.iproto 188 | -- ... 189 | -- 190 | -- tarantool> uri.parse('unix/:/foo//bar.iproto') 191 | -- --- 192 | -- - host: unix 193 | -- path: /foo//bar.iproto 194 | -- ... 195 | -- 196 | -- Let's normalize the path using fio.abspath(), which 197 | -- eliminates the double slashes. 198 | dir = fio.abspath(dir) 199 | 200 | table.insert(treegen._group._treegen.tempdirs, dir) 201 | 202 | for _, content in ipairs(contents) do 203 | gen_content(treegen._group, dir, content, replacements) 204 | end 205 | 206 | return dir 207 | end 208 | 209 | hooks.before_all_preloaded(init) 210 | hooks.after_all_preloaded(clean) 211 | 212 | return treegen 213 | -------------------------------------------------------------------------------- /test/capture_test.lua: -------------------------------------------------------------------------------- 1 | local fio = require('fio') 2 | local t = require('luatest') 3 | local g = t.group() 4 | 5 | local Capture = require('luatest.capture') 6 | local capture = Capture:new() 7 | 8 | -- Disable luatest logging to avoid capturing it. 9 | require('luatest.log').info = function() end 10 | 11 | g.before_each(function() 12 | capture:enable() 13 | end) 14 | 15 | g.after_each(function() 16 | capture:flush() 17 | capture:disable() 18 | end) 19 | 20 | g.before_all(function() 21 | local err 22 | g.fd, err = fio.open('/dev/null') 23 | assert(err == nil, tostring(err)) 24 | 25 | -- It is not really needed 26 | g.fd:close() 27 | end) 28 | 29 | -- Hack until https://github.com/tarantool/tarantool/issues/1338 30 | -- is not implemented. 31 | local function stdout_write(s) 32 | g.fd.fh = 1 33 | local res, err = g.fd:write(s) 34 | g.fd.fh = -1 35 | return res, err 36 | end 37 | 38 | local function stderr_write(s) 39 | g.fd.fh = 2 40 | local res, err = g.fd:write(s) 41 | g.fd.fh = -1 42 | return res, err 43 | end 44 | 45 | g.test_flush = function() 46 | t.assert_equals(capture:flush(), {stdout = '', stderr = ''}) 47 | io.stdout:write('test-out') 48 | io.stderr:write('test-err') 49 | t.assert_equals(capture:flush(), {stdout = 'test-out', stderr = 'test-err'}) 50 | t.assert_equals(capture:flush(), {stdout = '', stderr = ''}) 51 | end 52 | 53 | g.test_flush_large_strings = function() 54 | local buffer_size = 65536 55 | local out = ('out'):rep(buffer_size / 3) 56 | local err = ('error'):rep(buffer_size / 5 + 1) 57 | stdout_write(out) 58 | stderr_write(err) 59 | -- manually compare strings to avoid large diffs 60 | local captured = capture:flush() 61 | t.assert_equals(#captured.stdout, #out) 62 | t.assert(captured.stdout == out, 'invalid captured stdout') 63 | t.assert_equals(#captured.stderr, #err) 64 | t.assert(captured.stderr == err, 'invalid captured stdout') 65 | capture:disable() 66 | end 67 | 68 | g.test_wrap = function() 69 | local test_capture = Capture:new() 70 | assert(not test_capture.enabled) 71 | local result = {test_capture:wrap(true, function() 72 | assert(test_capture.enabled) 73 | io.stdout:write('test-out') 74 | io.stderr:write('test-err') 75 | return 'result' 76 | end)} 77 | t.assert_equals(result, {'result'}) 78 | assert(not test_capture.enabled) 79 | t.assert_equals(capture:flush(), {stdout = '', stderr = ''}) 80 | t.assert_equals(test_capture:flush(), {stdout = 'test-out', stderr = 'test-err'}) 81 | t.assert_equals(capture:flush(), {stdout = '', stderr = ''}) 82 | end 83 | 84 | g.test_wrap_disabled = function() 85 | local test_capture = Capture:new() 86 | assert(not test_capture.enabled) 87 | local result = {test_capture:wrap(false, function() 88 | assert(not test_capture.enabled) 89 | io.stdout:write('test-out') 90 | io.stderr:write('test-err') 91 | return 'result' 92 | end)} 93 | t.assert_equals(result, {'result'}) 94 | assert(not test_capture.enabled) 95 | t.assert_equals(capture:flush(), {stdout = 'test-out', stderr = 'test-err'}) 96 | t.assert_equals(test_capture:flush(), {stdout = '', stderr = ''}) 97 | end 98 | 99 | g.test_wrap_with_error = function() 100 | local test_capture = Capture:new() 101 | assert(not test_capture.enabled) 102 | local ok, err = pcall(function() test_capture:wrap(true, function() 103 | assert(test_capture.enabled) 104 | io.stdout:write('test-out') 105 | io.stderr:write('test-err') 106 | error('custom_error') 107 | return 'result' 108 | end) end) 109 | t.assert_equals(ok, false) 110 | assert(not test_capture.enabled) 111 | t.assert_str_contains(err.original, 'custom_error') 112 | t.assert_str_contains(err.traceback, 'custom_error') 113 | t.assert_str_contains(err.traceback, 'stack traceback:') 114 | t.assert_equals(err.captured.stdout, 'test-out') 115 | t.assert_equals(err.captured.stderr, 'test-err') 116 | t.assert_equals(test_capture:flush(), {stdout = '', stderr = ''}) 117 | t.assert_equals(capture:flush(), {stdout = '', stderr = ''}) 118 | end 119 | 120 | g.test_wrap_disabled_with_error = function() 121 | local test_capture = Capture:new() 122 | assert(not test_capture.enabled) 123 | local ok, err = pcall(function() test_capture:wrap(false, function() 124 | assert(not test_capture.enabled) 125 | io.stdout:write('test-out') 126 | io.stderr:write('test-err') 127 | error('custom_error') 128 | return 'result' 129 | end) end) 130 | t.assert_equals(ok, false) 131 | assert(not test_capture.enabled) 132 | t.assert_str_contains(err.original, 'custom_error') 133 | t.assert_str_contains(err.traceback, 'custom_error') 134 | t.assert_str_contains(err.traceback, 'stack traceback:') 135 | t.assert_equals(err.captured.stdout, '') 136 | t.assert_equals(err.captured.stderr, '') 137 | t.assert_equals(test_capture:flush(), {stdout = '', stderr = ''}) 138 | t.assert_equals(capture:flush(), {stdout = 'test-out', stderr = 'test-err'}) 139 | end 140 | 141 | g.test_wrap_with_error_table = function() 142 | local test_capture = Capture:new() 143 | assert(not test_capture.enabled) 144 | local err_table = {type = 'err-class', message = 'hey'} 145 | local ok, err = pcall(function() test_capture:wrap(true, function() 146 | assert(test_capture.enabled) 147 | io.stdout:write('test-out') 148 | io.stderr:write('test-err') 149 | error(err_table) 150 | return 'result' 151 | end) end) 152 | t.assert_equals(ok, false) 153 | assert(not test_capture.enabled) 154 | t.assert_equals(err.original, err_table) 155 | t.assert_str_contains(err.traceback, "type: err-class\nmessage: hey") 156 | t.assert_str_contains(err.traceback, 'stack traceback:') 157 | t.assert_equals(err.captured.stdout, 'test-out') 158 | t.assert_equals(err.captured.stderr, 'test-err') 159 | t.assert_equals(test_capture:flush(), {stdout = '', stderr = ''}) 160 | t.assert_equals(capture:flush(), {stdout = '', stderr = ''}) 161 | end 162 | 163 | g.test_wrap_nested = function() 164 | local test_capture = Capture:new() 165 | assert(not test_capture.enabled) 166 | test_capture:wrap(true, function() 167 | assert(test_capture.enabled) 168 | io.stdout:write('test-out') 169 | io.stderr:write('test-err') 170 | test_capture:wrap(false, function() 171 | assert(not test_capture.enabled) 172 | io.stdout:write('test-out-2') 173 | io.stderr:write('test-err-2') 174 | end) 175 | assert(test_capture.enabled) 176 | end) 177 | assert(not test_capture.enabled) 178 | t.assert_equals(capture:flush(), {stdout = 'test-out-2', stderr = 'test-err-2'}) 179 | t.assert_equals(test_capture:flush(), {stdout = 'test-out', stderr = 'test-err'}) 180 | end 181 | 182 | g.test_re_enable_disable = function() 183 | capture:enable() 184 | t.assert_error_msg_contains('Already capturing', function() capture:enable(true) end) 185 | capture:disable() 186 | capture:disable() 187 | t.assert_error_msg_contains('Not capturing', function() capture:disable(true) end) 188 | end 189 | -------------------------------------------------------------------------------- /luatest/output_beautifier.lua: -------------------------------------------------------------------------------- 1 | local checks = require('checks') 2 | local fiber = require('fiber') 3 | local fio = require('fio') 4 | local fun = require('fun') 5 | 6 | local Class = require('luatest.class') 7 | local ffi_io = require('luatest.ffi_io') 8 | local Monitor = require('luatest.monitor') 9 | local Process -- later require to avoid circular dependency 10 | 11 | local OutputBeautifier = Class.new({ 12 | monitor = Monitor:new(), 13 | PID_TRACKER_INTERVAL = 0.2, 14 | 15 | RESET_TERM = '\x1B[0m', 16 | COLORS = { 17 | {'magenta', '\x1B[35m'}, 18 | {'blue', '\x1B[34m'}, 19 | {'cyan', '\x1B[36m'}, 20 | {'green', '\x1B[32m'}, 21 | {'bright_magenta', '\x1B[95m'}, 22 | {'bright_cyan', '\x1B[96m'}, 23 | {'bright_blue', '\x1B[94m'}, 24 | {'bright_green', '\x1B[92m'}, 25 | }, 26 | ERROR_COLOR_CODE = '\x1B[31m', -- red 27 | WARN_COLOR_CODE = '\x1B[33m', -- yellow 28 | 29 | ERROR_LOG_LINE_PATTERN = ' (%u)> ', 30 | }) 31 | 32 | OutputBeautifier.COLOR_BY_NAME = fun.iter(OutputBeautifier.COLORS): 33 | map(function(x) return unpack(x) end): 34 | tomap() 35 | 36 | -- Map of `log_level_letter => color_code`. 37 | OutputBeautifier.COLOR_CODE_BY_LOG_LEVEL = fun.iter({ 38 | S_FATAL = 'ERROR_COLOR_CODE', 39 | S_SYSERROR = 'ERROR_COLOR_CODE', 40 | S_ERROR = 'ERROR_COLOR_CODE', 41 | S_CRIT = 'ERROR_COLOR_CODE', 42 | S_WARN = 'WARN_COLOR_CODE', 43 | S_INFO = 'RESET_TERM', 44 | S_VERBOSE = 'RESET_TERM', 45 | S_DEBUG = 'RESET_TERM', 46 | }):map(function(k, v) return k:sub(3, 3), OutputBeautifier[v] end):tomap() 47 | 48 | -- Generates color code from the list of `self.COLORS`. 49 | function OutputBeautifier:next_color_code() 50 | self._NEXT_COLOR = (self._NEXT_COLOR and self._NEXT_COLOR + 1 or 0) % #self.COLORS 51 | return self.COLORS[self._NEXT_COLOR + 1][2] 52 | end 53 | 54 | function OutputBeautifier:synchronize(...) 55 | return self.monitor:synchronize(...) 56 | end 57 | 58 | --- Build OutputBeautifier object. 59 | -- @param object 60 | -- @string object.prefix String to prefix each output line with. 61 | -- @string[opt] object.file Path to the file to append all output too. 62 | -- @string[opt] object.color Color name for prefix. 63 | -- @string[opt] object.color_code Color code for prefix. 64 | -- @return input object. 65 | function OutputBeautifier:new(object) 66 | checks('table', {prefix = 'string', file = '?string', 67 | color = '?string', color_code = '?string'}) 68 | return self:from(object) 69 | end 70 | 71 | function OutputBeautifier.mt:initialize() 72 | self.color_code = self.color_code or 73 | self.class.COLOR_BY_NAME[self.color] or 74 | OutputBeautifier:next_color_code() 75 | self.pipes = {stdout = ffi_io.create_pipe(), stderr = ffi_io.create_pipe()} 76 | end 77 | 78 | -- Replace standard output descriptors with pipes. 79 | function OutputBeautifier.mt:hijack_output() 80 | ffi_io.dup2_io(self.pipes.stdout[1], io.stdout) 81 | ffi_io.dup2_io(self.pipes.stderr[1], io.stderr) 82 | end 83 | 84 | -- Start fibers that reads from pipes and prints formatted output. 85 | -- Pass `track_pid` option to automatically stop forwarder once process is finished. 86 | function OutputBeautifier.mt:enable(options) 87 | if self.fibers then 88 | return 89 | end 90 | self.fibers = {} 91 | for i, pipe in pairs(self.pipes) do 92 | self.fibers[i] = fiber.new(self.run, self, pipe[0]) 93 | end 94 | self.fibers.pid_tracker = options and options.track_pid and fiber.new(function() 95 | Process = Process or require('luatest.process') 96 | while fiber.testcancel() or true do 97 | if not Process.is_pid_alive(options.track_pid) then 98 | fiber.sleep(self.class.PID_TRACKER_INTERVAL) 99 | return self:disable() 100 | end 101 | fiber.sleep(self.class.PID_TRACKER_INTERVAL) 102 | end 103 | end) 104 | if self.file then 105 | self.fh = fio.open(self.file, {'O_CREAT', 'O_WRONLY', 'O_APPEND'}, 106 | tonumber('640', 8)) 107 | end 108 | end 109 | 110 | -- Stop fibers. 111 | function OutputBeautifier.mt:disable() 112 | if self.fibers then 113 | for _, item in pairs(self.fibers) do 114 | if item:status() ~= 'dead' then 115 | item:cancel() 116 | end 117 | end 118 | end 119 | self.fibers = nil 120 | if self.fh then 121 | self.fh:close() 122 | end 123 | self.fh = nil 124 | end 125 | 126 | -- Process all available data from fd using synchronization with monitor. 127 | -- It prevents intensive output from breaking into chunks, interfering 128 | -- with other output or getting out of active capture. 129 | -- First it tries to read from fd with yielding call. If any data is available 130 | -- the it enters critical section and 131 | function OutputBeautifier.mt:process_fd_output(fd, fn) 132 | local chunks = ffi_io.read_fd(fd) 133 | if #chunks == 0 then 134 | return 135 | end 136 | self.class:synchronize(function() 137 | while true do 138 | fn(chunks) 139 | chunks = ffi_io.read_fd(fd, nil, {timeout = 0}) 140 | if #chunks == 0 then 141 | return 142 | end 143 | end 144 | end) 145 | end 146 | 147 | -- Reads from file desccriptor and prints colored and prefixed lines. 148 | -- Prefix is colored with `self.color_code` and error lines are printed in red. 149 | -- 150 | -- Every line with log level mark (` X> `) changes the color for all the following 151 | -- lines until the next one with the mark. 152 | function OutputBeautifier.mt:run(fd) 153 | local prefix = self.prefix .. ' | ' 154 | local colored_prefix = self.color_code .. prefix 155 | local line_color_code = self.class.RESET_TERM 156 | local log_file = rawget(_G, 'log_file') 157 | while fiber.testcancel() or true do 158 | self:process_fd_output(fd, function(chunks) 159 | local lines = table.concat(chunks):split('\n') 160 | if lines[#lines] == '' then 161 | table.remove(lines) 162 | end 163 | for _, line in pairs(lines) do 164 | if self.fh ~= nil then 165 | self.fh:write(table.concat({prefix, line, '\n'})) 166 | end 167 | if log_file ~= nil then 168 | -- Redirect all output to the log file if unified logging 169 | -- is enabled. 170 | log_file.fh:write(table.concat({prefix, line, '\n'})) 171 | else 172 | line_color_code = self:color_for_line(line) or line_color_code 173 | io.stdout:write( 174 | table.concat( 175 | {colored_prefix, line_color_code, line, self.class.RESET_TERM,'\n'} 176 | ) 177 | ) 178 | end 179 | fiber.yield() 180 | end 181 | end) 182 | end 183 | end 184 | 185 | -- Returns new color code for line or nil if it should not be changed. 186 | function OutputBeautifier.mt:color_for_line(line) 187 | local mark = line:match(self.class.ERROR_LOG_LINE_PATTERN) 188 | return mark and self.class.COLOR_CODE_BY_LOG_LEVEL[mark] 189 | end 190 | 191 | return OutputBeautifier 192 | -------------------------------------------------------------------------------- /luatest/comparator.lua: -------------------------------------------------------------------------------- 1 | local Class = require('luatest.class') 2 | 3 | -- Utils for smart comparison. 4 | local comparator = {} 5 | 6 | function comparator.cast(value) 7 | if type(value) == 'cdata' then 8 | local ok, table_value = pcall(function() return value:totable() end) 9 | if ok then 10 | return table_value 11 | end 12 | end 13 | return value 14 | end 15 | 16 | -- Compare items by value: casts cdata values to tables, and compare tables by their content. 17 | function comparator.equals(a, b, recursions) 18 | a = comparator.cast(a) 19 | b = comparator.cast(b) 20 | if type(a) == 'table' and type(b) == 'table' then 21 | return comparator.table_equals(a, b, recursions) 22 | else 23 | return a == b 24 | end 25 | end 26 | 27 | function comparator.lt(a, b) 28 | return a < b 29 | end 30 | 31 | function comparator.le(a, b) 32 | return a <= b 33 | end 34 | 35 | -- Checks that actual is subset of expected. 36 | -- Returns number of elements that are present in expected but not in actual. 37 | function comparator.is_subset(actual, expected) 38 | if (type(actual) ~= 'table') or (type(expected) ~= 'table') then 39 | return false 40 | end 41 | 42 | local expected_array = {} 43 | local expected_casted = {} 44 | local found_ids = {} 45 | local found_count = 0 46 | for _, v in pairs(expected) do 47 | table.insert(expected_array, v) 48 | end 49 | 50 | local function search(a) 51 | for i = 1, #expected_array do 52 | if not found_ids[i] then 53 | if not expected_casted[i] then 54 | expected_casted[i] = comparator.cast(expected_array[i]) 55 | end 56 | if comparator.equals(a, expected_casted[i]) then 57 | found_ids[i] = true 58 | found_count = found_count + 1 59 | return true 60 | end 61 | end 62 | end 63 | end 64 | 65 | for _, a in pairs(actual) do 66 | if not search(a) then 67 | return false 68 | end 69 | end 70 | return #expected_array - found_count 71 | end 72 | 73 | -- Returns false if 'actual' or 'expected' are not tables or their value sets 74 | -- intersect. Returns true otherwise. 75 | function comparator.are_disjoint(actual, expected) 76 | if (type(actual) ~= 'table') or (type(expected) ~= 'table') then 77 | return false 78 | end 79 | 80 | for _, a in pairs(actual) do 81 | for _, b in pairs(expected) do 82 | if comparator.equals(a, b) then 83 | return false 84 | end 85 | end 86 | end 87 | return true 88 | end 89 | 90 | -- This is a specialized metatable to help with the bookkeeping of recursions 91 | -- in table_equals(). It provides an __index table that implements utility 92 | -- functions for easier management of the table. The "cached" method queries 93 | -- the state of a specific (actual,expected) pair; and the "store" method sets 94 | -- this state to the given value. The state of pairs not "seen" / visited is 95 | -- assumed to be `nil`. 96 | local RecursionCache = Class.new() 97 | 98 | -- Return the cached value for an (actual,expected) pair (or `nil`) 99 | function RecursionCache.mt:cached(actual, expected) 100 | local subtable = self[actual] or {} 101 | return subtable[expected] 102 | end 103 | 104 | -- Store cached value for a specific (actual,expected) pair. 105 | -- Returns the value, so it's easy to use for a "tailcall" (return ...). 106 | function RecursionCache.mt:store(actual, expected, value, asymmetric) 107 | local subtable = self[actual] 108 | if not subtable then 109 | subtable = {} 110 | self[actual] = subtable 111 | end 112 | subtable[expected] = value 113 | 114 | -- Unless explicitly marked "asymmetric": Consider the recursion 115 | -- on (expected,actual) to be equivalent to (actual,expected) by 116 | -- default, and thus cache the value for both. 117 | if not asymmetric then 118 | self:store(expected, actual, value, true) 119 | end 120 | 121 | return value 122 | end 123 | 124 | function comparator.table_equals(actual, expected, recursions) 125 | recursions = recursions or RecursionCache:new() 126 | 127 | if actual == expected then 128 | -- Both reference the same table, so they are actually identical 129 | return recursions:store(actual, expected, true) 130 | end 131 | 132 | -- If we've tested this (actual,expected) pair before: return cached value 133 | local previous = recursions:cached(actual, expected) 134 | if previous ~= nil then 135 | return previous 136 | end 137 | 138 | -- Mark this (actual,expected) pair, so we won't recurse it again. For 139 | -- now, assume a "false" result, which we might adjust later if needed. 140 | recursions:store(actual, expected, false) 141 | 142 | -- We used to verify that table count is identical here by comparing their length 143 | -- but this is unreliable when table is not a sequence. 144 | local actualKeysMatched, actualTableKeys = {}, {} 145 | 146 | for k, v in pairs(actual) do 147 | if type(k) == "table" then 148 | -- If the keys are tables, things get a bit tricky here as we 149 | -- can have table_equals(t[k1], t[k2]) despite k1 ~= k2. So 150 | -- we first collect table keys from "actual", and then later try 151 | -- to match each table key from "expected" to actualTableKeys. 152 | table.insert(actualTableKeys, k) 153 | else 154 | if not comparator.equals(v, expected[k], recursions) then 155 | return false -- Mismatch on value, tables can't be equal 156 | end 157 | actualKeysMatched[k] = true -- Keep track of matched keys 158 | end 159 | end 160 | 161 | for k, v in pairs(expected) do 162 | if type(k) == "table" then 163 | local found = false 164 | -- Note: DON'T use ipairs() here, table may be non-sequential! 165 | for i, candidate in pairs(actualTableKeys) do 166 | if comparator.equals(candidate, k, recursions) then 167 | if comparator.equals(actual[candidate], v, recursions) then 168 | found = true 169 | -- Remove the candidate we matched against from the list 170 | -- of table keys, so each key in actual can only match 171 | -- one key in expected. 172 | actualTableKeys[i] = nil 173 | break 174 | end 175 | -- keys match but values don't, keep searching 176 | end 177 | end 178 | if not found then 179 | return false -- no matching (key,value) pair 180 | end 181 | else 182 | if not actualKeysMatched[k] then 183 | -- Found a key that we did not see in "actual" -> mismatch 184 | return false 185 | end 186 | -- Otherwise actual[k] was already matched against v = expected[k]. 187 | end 188 | end 189 | 190 | if next(actualTableKeys) then 191 | -- If there is any key left in actualTableKeys, then that is 192 | -- a table-type key in actual with no matching counterpart 193 | -- (in expected), and so the tables aren't equal. 194 | return false 195 | end 196 | 197 | -- The tables are actually considered equal, update cache and return result 198 | return recursions:store(actual, expected, true) 199 | end 200 | 201 | return comparator 202 | --------------------------------------------------------------------------------