diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index fc83ef126591539af8c8af6f38d96d15d19c06d6..3893bbb3c417164026213966155fbe6531a6fd77 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -27,6 +27,7 @@ lua_source(lua_sources lua/errno.lua) lua_source(lua_sources lua/log.lua) lua_source(lua_sources lua/box_net_box.lua) lua_source(lua_sources lua/help.lua) +lua_source(lua_sources lua/tap.lua) file(MAKE_DIRECTORY ${CMAKE_BINARY_DIR}/third_party/luafun) lua_source(lua_sources ../third_party/luafun/fun.lua) diff --git a/src/lua/init.cc b/src/lua/init.cc index 8ebca87f4505a1cc93cd4d6315ee9c18cc2abff3..fbbfda3bf542fb5bdfc324e19f8ef4b75bc18e9d 100644 --- a/src/lua/init.cc +++ b/src/lua/init.cc @@ -78,7 +78,8 @@ extern char uuid_lua[], log_lua[], console_lua[], box_net_box_lua[], - help_lua[]; + help_lua[], + tap_lua[]; static const char *lua_sources[] = { init_lua, @@ -95,6 +96,7 @@ static const char *lua_modules[] = { "uuid", uuid_lua, "log", log_lua, "net.box", box_net_box_lua, + "tap", tap_lua, NULL }; diff --git a/src/lua/tap.lua b/src/lua/tap.lua new file mode 100644 index 0000000000000000000000000000000000000000..83356030be80a6053c915e7d6650a71e2ea4a379 --- /dev/null +++ b/src/lua/tap.lua @@ -0,0 +1,215 @@ +--- tap.lua internal file +--- +--- The Test Anything Protocol vesion 13 producer +--- + +local yaml = require('yaml') +local ffi = require('ffi') -- for iscdata + +local function traceback(level) + local trace = {} + level = level or 3 + while true do + local info = debug.getinfo(level, "nSl") + if not info then break end + local frame = { + source = info.source; + src = info.short_src; + line = info.linedefined or 0; + what = info.what; + name = info.name; + namewhat = info.namewhat; + filename = info.source:sub(1, 1) == "@" and info.source:sub(2) or + 'eval' + } + table.insert(trace, frame) + level = level + 1 + end + return trace +end + +local function diag(test, fmt, ...) + io.write(string.rep(' ', 4 * test.level), "# ", string.format(fmt, ...), + "\n") +end + +local function ok(test, cond, message, extra) + test.total = test.total + 1 + io.write(string.rep(' ', 4 * test.level)) + if cond then + io.write(string.format("ok - %s\n", message)) + return true + end + + test.failed = test.failed + 1 + io.write(string.format("not ok - %s\n", message)) + extra = extra or {} + if test.trace then + local frame = debug.getinfo(3, "Sl") + extra.trace = traceback() + extra.filename = extra.trace[#extra.trace].filename + extra.line = extra.trace[#extra.trace].line + end + if next(extra) == nil then + return false -- don't have extra information + end + -- print aligned yaml output + for line in yaml.encode(extra):gmatch("[^\n]+") do + io.write(string.rep(' ', 2 + 4 * test.level), line, "\n") + end + return false +end + +local function fail(test, message, extra) + return ok(test, false, message, extra) +end + +local function skip(test, message, extra) + ok(test, true, message.." # skip", extra) +end + +local function is(test, got, expected, message, extra) + extra = extra or {} + extra.got = got + extra.expected = expected + return ok(test, got == expected, message, extra) +end + +local function isnt(test, got, unexpected, message, extra) + extra = extra or {} + extra.got = got + extra.unexpected = unexpected + return ok(test, got ~= unexpected, message, extra) +end + +local function isnil(test, v, message, extra) + return is(test, not v and 'nil' or v, 'nil', message, extra) +end + +local function isnumber(test, v, message, extra) + return is(test, type(v), 'number', message, extra) +end + +local function isstring(test, v, message, extra) + return is(test, type(v), 'string', message, extra) +end + +local function istable(test, v, message, extra) + return is(test, type(v), 'table', message, extra) +end + +local function isboolean(test, v, message, extra) + return is(test, type(v), 'boolean', message, extra) +end + +local function isudata(test, v, utype, message, extra) + extra = extra or {} + extra.expected = 'userdata<'..utype..'>' + if type(v) == 'userdata' then + extra.got = 'userdata<'..getmetatable(v)..'>' + return ok(test, getmetatable(v) == utype, message, extra) + else + extra.got = type(v) + return fail(test, message, extra) + end +end + +local function iscdata(test, v, ctype, message, extra) + extra = extra or {} + extra.expected = ffi.typeof(ctype) + if type(v) == 'cdata' then + extra.got = ffi.typeof(v) + return ok(test, ffi.istype(ctype, v), message, extra) + else + extra.got = type(v) + return fail(test, message, extra) + end +end + +local test_mt +local function test(parent, name, fun) + local level = parent ~= nil and parent.level + 1 or 0 + local test = setmetatable({ + parent = parent; + name = name; + level = level; + total = 0; + failed = 0; + planned = 0; + trace = parent == nil and true or parent.trace; + }, test_mt) + if fun ~= nil then + test:diag('%s', test.name) + fun(test) + test:diag('%s: end', test.name) + return test:check() + else + return test + end +end + +local function plan(test, planned) + test.planned = planned + io.write(string.rep(' ', 4 * test.level), string.format("1..%d\n", planned)) +end + +local function check(test) + if test.checked then + error('check called twice') + end + test.checked = true + if test.planned ~= test.total then + if test.parent ~= nil then + ok(test.parent, false, "bad plan", { planned = test.planned; + run = test.total}) + else + diag(test, string.format("bad plan: planned %d run %d", + test.planned, test.total)) + end + elseif test.failed > 0 then + if test.parent ~= nil then + ok(test.parent, false, "failed subtests", { + failed = test.failed; + planned = test.planned; + }) + return false + else + diag(test, "failed subtest: %d", test.failed) + end + else + if test.parent ~= nil then + ok(test.parent, true, test.name) + end + end + return true +end + +test_mt = { + __index = { + test = test; + plan = plan; + check = check; + diag = diag; + ok = ok; + fail = fail; + skip = skip; + is = is; + isnt = isnt; + isnil = isnil; + isnumber = isnumber; + isstring = isstring; + istable = istable; + isboolean = isboolean; + isudata = isudata; + iscdata = iscdata; + } +} + +local function root_test(...) + io.write('TAP version 13', '\n') + return test(nil, ...) +end + +return { + test = root_test; +} diff --git a/test/app/tap.result b/test/app/tap.result new file mode 100644 index 0000000000000000000000000000000000000000..052e3ad667e5a3812a36a102ce4a7d0051eb16cf --- /dev/null +++ b/test/app/tap.result @@ -0,0 +1,121 @@ +TAP version 13 +1..30 +ok - true +ok - extra information is not printed on success +not ok - extra printed using yaml only on failure + --- + state: some userful information to debug on failure + details: a table argument formatted using yaml.encode() + ... +not ok - failed +ok - test marked as ok and skipped # skip +ok - tonumber(48) is 48 +ok - 0xff is not 64 +not ok - 1 is not 1 + --- + unexpected: 1 + got: 1 + ... +ok - nil is nil +not ok - 48 is nil + --- + expected: nil + got: 48 + ... +ok - 10 is a number +ok - 0 is also a number +ok - "blabla" is string +not ok - 48 is string + --- + expected: string + got: number + ... +not ok - nil is string + --- + expected: string + got: nil + ... +ok - true is boolean +not ok - 1 is boolean + --- + expected: boolean + got: number + ... +ok - {} is a table +not ok - udata + --- + expected: userdata<fiber> + got: nil + ... +not ok - udata + --- + expected: userdata<some utype> + got: userdata<fiber> + ... +ok - udata +not ok - cdata type + --- + expected: ctype<int> + got: string + ... +not ok - cdata type + --- + expected: ctype<int> + got: number + ... +ok - cdata type +not ok - cdata type + --- + expected: ctype<int> + got: ctype<unsigned int> + ... + # subtest 1 + 1..2 + ok - true + ok - true + # subtest 1: end +ok - subtest 1 + 1..1 + ok - true in subtest + # hello from subtest +ok - subtest 2 + # 1 level + 1..1 + # 2 level + 1..1 + # 3 level + 1..1 + # 4 level + 1..1 + # 5 level + 1..1 + ok - ok + # 5 level: end + ok - 5 level + # 4 level: end + ok - 4 level + # 3 level: end + ok - 3 level + # 2 level: end + ok - 2 level + # 1 level: end +ok - 1 level + # bad plan + 1..3 + ok - true + # bad plan: end +not ok - bad plan + --- + planned: 3 + run: 1 + ... + # failed subtest + 1..1 + not ok - failed subtest + # failed subtest: end +not ok - failed subtests + --- + planned: 1 + failed: 1 + ... +# failed subtest: 14 diff --git a/test/app/tap.test.lua b/test/app/tap.test.lua new file mode 100755 index 0000000000000000000000000000000000000000..243d1cbdc90f92e20f4c4a7f1af920f78c60c60e --- /dev/null +++ b/test/app/tap.test.lua @@ -0,0 +1,123 @@ +#!/usr/bin/env tarantool + +-- +-- Test suite for The Test Anything Protocol module implemented +-- using module itself. +-- + +-- Load 'tap' module +local tap = require "tap" + +-- +-- Create a root test +-- +test = tap.test("root test") +-- Disable stack traces for this test because Tarantool test system also +-- checks test output. +test.trace = false + +-- +-- ok, fail and skip predicates +-- + +test:plan(30) -- plan to run 3 test +test:ok(true, 'true') -- basic function +local extra = { state = 'some userful information to debug on failure', + details = 'a table argument formatted using yaml.encode()' } +test:ok(true, "extra information is not printed on success", extra) +test:ok(false, "extra printed using yaml only on failure", extra) + +test:fail('failed') -- always fail the test +test:skip('test marked as ok and skipped') + +-- +-- is and isnt predicates +-- +test:is(tonumber("48"), 48, "tonumber(48) is 48") +test:isnt(0xff, 64, "0xff is not 64") +test:isnt(1, 1, "1 is not 1") + +-- +-- type predicates +-- +test:isnil(nil, 'nil is nil') +test:isnil(48, '48 is nil') +test:isnumber(10, '10 is a number') +test:isnumber(0, '0 is also a number') +test:isstring("blabla", '"blabla" is string') +test:isstring(48, '48 is string') +test:isstring(nil, 'nil is string') +test:isboolean(true, 'true is boolean') +test:isboolean(1, '1 is boolean') +test:istable({}, '{} is a table') +local udata = require('fiber').self() +test:isudata(nil, 'fiber', 'udata') +test:isudata(udata, 'some utype', 'udata') +test:isudata(udata, 'fiber', 'udata') +local ffi = require('ffi') +test:iscdata('xx', 'int', 'cdata type') +test:iscdata(10, 'int', 'cdata type') +test:iscdata(ffi.new('int', 10), 'int', 'cdata type') +test:iscdata(ffi.new('unsigned int', 10), 'int', 'cdata type') + +-- +-- Any test also can create unlimited number of sub tests. +-- Subtest with callbacks (preferred). +-- +test:test("subtest 1", function(t) + t:plan(2) + t:ok(true, 'true') + t:ok(true, 'true') + -- test:check() is called automatically +end) +-- each subtest is counted in parent + +-- +-- Subtest without callbacks. +-- +sub2 = test:test("subtest 2") + sub2:plan(1) + sub2:ok(true, 'true in subtest') + sub2:diag('hello from subtest') + sub2:check() -- please call check() explicitly + +-- +-- Multisubtest +-- +test:test("1 level", function(t) + t:plan(1) + t:test("2 level", function(t) + t:plan(1) + t:test("3 level", function(t) + t:plan(1) + t:test("4 level", function(t) + t:plan(1) + t:test("5 level", function(t) + t:plan(1) + t:ok(true, 'ok') + end) + end) + end) + end) +end) + +--- +--- Subtest with bad plan() +--- +test:test("bad plan", function(t) + t:plan(3) + t:ok(true, 'true') +end) + + +test:test("failed subtest", function(t) + t:plan(1) + t:fail("failed subtest") +end) + +-- +-- Finish root test. Since we used non-callback variant, we have to +-- call check explicitly. +-- +test:check() -- call check() explicitly +os.exit(0)