From ef699ee3f8d2aaf0a98b127f72acf422c630abe5 Mon Sep 17 00:00:00 2001
From: Roman Tsisyk <roman@tsisyk.com>
Date: Thu, 17 Jul 2014 16:56:50 +0400
Subject: [PATCH] Fix #189: implement Test Anything Protocol for Lua

This patch adds Test Anything Protocol (http://testanything.org/)
producer. TAP is a standardized output format for unit tests.
---
 src/CMakeLists.txt    |   1 +
 src/lua/init.cc       |   4 +-
 src/lua/tap.lua       | 215 ++++++++++++++++++++++++++++++++++++++++++
 test/app/tap.result   | 121 ++++++++++++++++++++++++
 test/app/tap.test.lua | 123 ++++++++++++++++++++++++
 5 files changed, 463 insertions(+), 1 deletion(-)
 create mode 100644 src/lua/tap.lua
 create mode 100644 test/app/tap.result
 create mode 100755 test/app/tap.test.lua

diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index fc83ef1265..3893bbb3c4 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 8ebca87f45..fbbfda3bf5 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 0000000000..83356030be
--- /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 0000000000..052e3ad667
--- /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 0000000000..243d1cbdc9
--- /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)
-- 
GitLab