Explorar el Código

Initial commit

Xavier Wang hace 5 años
commit
65a1e36103
Se han modificado 8 ficheros con 4413 adiciones y 0 borrados
  1. 6 0
      .gitignore
  2. 43 0
      .travis.yml
  3. 4 0
      README.md
  4. 3207 0
      luaunit.lua
  5. 777 0
      mp.c
  6. 26 0
      rockspecs/lua-mp-scm-1.rockspec
  7. 124 0
      serpent.lua
  8. 226 0
      test.lua

+ 6 - 0
.gitignore

@@ -0,0 +1,6 @@
+*.gcov
+*.gcda
+*.gcno
+*.o
+*.so
+*.old

+ 43 - 0
.travis.yml

@@ -0,0 +1,43 @@
+language: c
+sudo: false
+
+env:
+  global:
+    - ROCKSPEC=rockspecs/lua-mp-scm-1.rockspec
+  matrix:
+    - LUA="lua 5.1"
+    - LUA="lua 5.2"
+    - LUA="lua 5.3"
+    - LUA="lua 5.4"
+    - LUA="luajit 2.0"
+    - LUA="luajit 2.1"
+
+branches:
+  only:
+    - master
+
+before_install:
+  - pip install --user urllib3[secure] cpp-coveralls
+  - curl -O https://raw.githubusercontent.com/luarocks/hererocks/master/hererocks.py
+  - python hererocks.py env --$LUA -rlatest
+  - source env/bin/activate
+
+install:
+  # - sudo luarocks make $ROCKSPEC CFLAGS="-O2 -fPIC -ftest-coverage -fprofile-arcs" LIBFLAG="-shared --coverage"
+  - luarocks make $ROCKSPEC CFLAGS="-O3 -fPIC -Wall -Wextra --coverage" LIBFLAG="-shared --coverage"
+
+script: 
+  - lua test.lua
+  # - lunit.sh test.lua
+
+after_success:
+    - coveralls
+#  - coveralls -b .. -r .. --dump c.report.json
+#  - luacov-coveralls -j c.report.json -v
+
+notifications:
+  email:
+    on_success: change
+    on_failure: always
+
+# vim: ft=yaml nu et sw=2 fdc=2 fdm=syntax

+ 4 - 0
README.md

@@ -0,0 +1,4 @@
+# lua-mp - Yet another msgpack implement for Lua
+
+[![Build Status](https://travis-ci.org/starwing/lua-mp.svg?branch=master)](https://travis-ci.org/starwing/lua-mp)[![Coverage Status](https://coveralls.io/repos/github/starwing/lua-mp/badge.svg?branch=master)](https://coveralls.io/github/starwing/lua-mp?branch=master)
+

+ 3207 - 0
luaunit.lua

@@ -0,0 +1,3207 @@
+--[[
+        luaunit.lua
+
+Description: A unit testing framework
+Homepage: https://github.com/bluebird75/luaunit
+Development by Philippe Fremy <[email protected]>
+Based on initial work of Ryu, Gwang (http://www.gpgstudy.com/gpgiki/LuaUnit)
+License: BSD License, see LICENSE.txt
+]]--
+
+require("math")
+local M={}
+
+-- private exported functions (for testing)
+M.private = {}
+
+M.VERSION='3.4-dev'
+M._VERSION=M.VERSION -- For LuaUnit v2 compatibility
+
+-- a version which distinguish between regular Lua and LuaJit
+M._LUAVERSION = (jit and jit.version) or _VERSION
+
+--[[ Some people like assertEquals( actual, expected ) and some people prefer
+assertEquals( expected, actual ).
+]]--
+M.ORDER_ACTUAL_EXPECTED = true
+M.PRINT_TABLE_REF_IN_ERROR_MSG = false
+M.LINE_LENGTH = 80
+M.TABLE_DIFF_ANALYSIS_THRESHOLD = 10    -- display deep analysis for more than 10 items
+M.LIST_DIFF_ANALYSIS_THRESHOLD  = 10    -- display deep analysis for more than 10 items
+
+--[[ EPS is meant to help with Lua's floating point math in simple corner
+cases like almostEquals(1.1-0.1, 1), which may not work as-is (e.g. on numbers
+with rational binary representation) if the user doesn't provide some explicit
+error margin.
+
+The default margin used by almostEquals() in such cases is EPS; and since
+Lua may be compiled with different numeric precisions (single vs. double), we
+try to select a useful default for it dynamically. Note: If the initial value
+is not acceptable, it can be changed by the user to better suit specific needs.
+
+See also: https://en.wikipedia.org/wiki/Machine_epsilon
+]]
+M.EPS = 2^-52 -- = machine epsilon for "double", ~2.22E-16
+if math.abs(1.1 - 1 - 0.1) > M.EPS then
+    -- rounding error is above EPS, assume single precision
+    M.EPS = 2^-23 -- = machine epsilon for "float", ~1.19E-07
+end
+
+-- set this to false to debug luaunit
+local STRIP_LUAUNIT_FROM_STACKTRACE = true
+
+M.VERBOSITY_DEFAULT = 10
+M.VERBOSITY_LOW     = 1
+M.VERBOSITY_QUIET   = 0
+M.VERBOSITY_VERBOSE = 20
+M.DEFAULT_DEEP_ANALYSIS = nil
+M.FORCE_DEEP_ANALYSIS   = true
+M.DISABLE_DEEP_ANALYSIS = false
+
+-- set EXPORT_ASSERT_TO_GLOBALS to have all asserts visible as global values
+-- EXPORT_ASSERT_TO_GLOBALS = true
+
+-- we need to keep a copy of the script args before it is overriden
+local cmdline_argv = rawget(_G, "arg")
+
+M.FAILURE_PREFIX = 'LuaUnit test FAILURE: ' -- prefix string for failed tests
+M.SUCCESS_PREFIX = 'LuaUnit test SUCCESS: ' -- prefix string for successful tests finished early
+M.SKIP_PREFIX    = 'LuaUnit test SKIP:    ' -- prefix string for skipped tests
+
+
+
+M.USAGE=[[Usage: lua <your_test_suite.lua> [options] [testname1 [testname2] ... ]
+Options:
+  -h, --help:             Print this help
+  --version:              Print version information
+  -v, --verbose:          Increase verbosity
+  -q, --quiet:            Set verbosity to minimum
+  -e, --error:            Stop on first error
+  -f, --failure:          Stop on first failure or error
+  -s, --shuffle:          Shuffle tests before running them
+  -o, --output OUTPUT:    Set output type to OUTPUT
+                          Possible values: text, tap, junit, nil
+  -n, --name NAME:        For junit only, mandatory name of xml file
+  -r, --repeat NUM:       Execute all tests NUM times, e.g. to trig the JIT
+  -p, --pattern PATTERN:  Execute all test names matching the Lua PATTERN
+                          May be repeated to include several patterns
+                          Make sure you escape magic chars like +? with %
+  -x, --exclude PATTERN:  Exclude all test names matching the Lua PATTERN
+                          May be repeated to exclude several patterns
+                          Make sure you escape magic chars like +? with %
+  testname1, testname2, ... : tests to run in the form of testFunction,
+                              TestClass or TestClass.testMethod
+]]
+
+local is_equal -- defined here to allow calling from mismatchFormattingPureList
+
+----------------------------------------------------------------
+--
+--                 general utility functions
+--
+----------------------------------------------------------------
+
+local function pcall_or_abort(func, ...)
+    -- unpack is a global function for Lua 5.1, otherwise use table.unpack
+    local unpack = rawget(_G, "unpack") or table.unpack
+    local result = {pcall(func, ...)}
+    if not result[1] then
+        -- an error occurred
+        print(result[2]) -- error message
+        print()
+        print(M.USAGE)
+        os.exit(-1)
+    end
+    return unpack(result, 2)
+end
+
+local crossTypeOrdering = {
+    number = 1, boolean = 2, string = 3, table = 4, other = 5
+}
+local crossTypeComparison = {
+    number = function(a, b) return a < b end,
+    string = function(a, b) return a < b end,
+    other = function(a, b) return tostring(a) < tostring(b) end,
+}
+
+local function crossTypeSort(a, b)
+    local type_a, type_b = type(a), type(b)
+    if type_a == type_b then
+        local func = crossTypeComparison[type_a] or crossTypeComparison.other
+        return func(a, b)
+    end
+    type_a = crossTypeOrdering[type_a] or crossTypeOrdering.other
+    type_b = crossTypeOrdering[type_b] or crossTypeOrdering.other
+    return type_a < type_b
+end
+
+local function __genSortedIndex( t )
+    -- Returns a sequence consisting of t's keys, sorted.
+    local sortedIndex = {}
+
+    for key,_ in pairs(t) do
+        table.insert(sortedIndex, key)
+    end
+
+    table.sort(sortedIndex, crossTypeSort)
+    return sortedIndex
+end
+M.private.__genSortedIndex = __genSortedIndex
+
+local function sortedNext(state, control)
+    -- Equivalent of the next() function of table iteration, but returns the
+    -- keys in sorted order (see __genSortedIndex and crossTypeSort).
+    -- The state is a temporary variable during iteration and contains the
+    -- sorted key table (state.sortedIdx). It also stores the last index (into
+    -- the keys) used by the iteration, to find the next one quickly.
+    local key
+
+    --print("sortedNext: control = "..tostring(control) )
+    if control == nil then
+        -- start of iteration
+        state.count = #state.sortedIdx
+        state.lastIdx = 1
+        key = state.sortedIdx[1]
+        return key, state.t[key]
+    end
+
+    -- normally, we expect the control variable to match the last key used
+    if control ~= state.sortedIdx[state.lastIdx] then
+        -- strange, we have to find the next value by ourselves
+        -- the key table is sorted in crossTypeSort() order! -> use bisection
+        local lower, upper = 1, state.count
+        repeat
+            state.lastIdx = math.modf((lower + upper) / 2)
+            key = state.sortedIdx[state.lastIdx]
+            if key == control then
+                break -- key found (and thus prev index)
+            end
+            if crossTypeSort(key, control) then
+                -- key < control, continue search "right" (towards upper bound)
+                lower = state.lastIdx + 1
+            else
+                -- key > control, continue search "left" (towards lower bound)
+                upper = state.lastIdx - 1
+            end
+        until lower > upper
+        if lower > upper then -- only true if the key wasn't found, ...
+            state.lastIdx = state.count -- ... so ensure no match in code below
+        end
+    end
+
+    -- proceed by retrieving the next value (or nil) from the sorted keys
+    state.lastIdx = state.lastIdx + 1
+    key = state.sortedIdx[state.lastIdx]
+    if key then
+        return key, state.t[key]
+    end
+
+    -- getting here means returning `nil`, which will end the iteration
+end
+
+local function sortedPairs(tbl)
+    -- Equivalent of the pairs() function on tables. Allows to iterate in
+    -- sorted order. As required by "generic for" loops, this will return the
+    -- iterator (function), an "invariant state", and the initial control value.
+    -- (see http://www.lua.org/pil/7.2.html)
+    return sortedNext, {t = tbl, sortedIdx = __genSortedIndex(tbl)}, nil
+end
+M.private.sortedPairs = sortedPairs
+
+-- seed the random with a strongly varying seed
+math.randomseed(os.clock()*1E11)
+
+local function randomizeTable( t )
+    -- randomize the item orders of the table t
+    for i = #t, 2, -1 do
+        local j = math.random(i)
+        if i ~= j then
+            t[i], t[j] = t[j], t[i]
+        end
+    end
+end
+M.private.randomizeTable = randomizeTable
+
+local function strsplit(delimiter, text)
+-- Split text into a list consisting of the strings in text, separated
+-- by strings matching delimiter (which may _NOT_ be a pattern).
+-- Example: strsplit(", ", "Anna, Bob, Charlie, Dolores")
+    if delimiter == "" or delimiter == nil then -- this would result in endless loops
+        error("delimiter is nil or empty string!")
+    end
+    if text == nil then
+        return nil
+    end
+
+    local list, pos, first, last = {}, 1
+    while true do
+        first, last = text:find(delimiter, pos, true)
+        if first then -- found?
+            table.insert(list, text:sub(pos, first - 1))
+            pos = last + 1
+        else
+            table.insert(list, text:sub(pos))
+            break
+        end
+    end
+    return list
+end
+M.private.strsplit = strsplit
+
+local function hasNewLine( s )
+    -- return true if s has a newline
+    return (string.find(s, '\n', 1, true) ~= nil)
+end
+M.private.hasNewLine = hasNewLine
+
+local function prefixString( prefix, s )
+    -- Prefix all the lines of s with prefix
+    return prefix .. string.gsub(s, '\n', '\n' .. prefix)
+end
+M.private.prefixString = prefixString
+
+local function strMatch(s, pattern, start, final )
+    -- return true if s matches completely the pattern from index start to index end
+    -- return false in every other cases
+    -- if start is nil, matches from the beginning of the string
+    -- if final is nil, matches to the end of the string
+    start = start or 1
+    final = final or string.len(s)
+
+    local foundStart, foundEnd = string.find(s, pattern, start, false)
+    return foundStart == start and foundEnd == final
+end
+M.private.strMatch = strMatch
+
+local function patternFilter(patterns, expr)
+    -- Run `expr` through the inclusion and exclusion rules defined in patterns
+    -- and return true if expr shall be included, false for excluded.
+    -- Inclusion pattern are defined as normal patterns, exclusions 
+    -- patterns start with `!` and are followed by a normal pattern
+
+    -- result: nil = UNKNOWN (not matched yet), true = ACCEPT, false = REJECT
+    -- default: true if no explicit "include" is found, set to false otherwise
+    local default, result = true, nil
+
+    if patterns ~= nil then
+        for _, pattern in ipairs(patterns) do
+            local exclude = pattern:sub(1,1) == '!'
+            if exclude then
+                pattern = pattern:sub(2)
+            else
+                -- at least one include pattern specified, a match is required
+                default = false
+            end
+            -- print('pattern: ',pattern)
+            -- print('exclude: ',exclude)
+            -- print('default: ',default)
+
+            if string.find(expr, pattern) then
+                -- set result to false when excluding, true otherwise
+                result = not exclude
+            end
+        end
+    end
+
+    if result ~= nil then
+        return result
+    end
+    return default
+end
+M.private.patternFilter = patternFilter
+
+local function xmlEscape( s )
+    -- Return s escaped for XML attributes
+    -- escapes table:
+    -- "   &quot;
+    -- '   &apos;
+    -- <   &lt;
+    -- >   &gt;
+    -- &   &amp;
+
+    return string.gsub( s, '.', {
+        ['&'] = "&amp;",
+        ['"'] = "&quot;",
+        ["'"] = "&apos;",
+        ['<'] = "&lt;",
+        ['>'] = "&gt;",
+    } )
+end
+M.private.xmlEscape = xmlEscape
+
+local function xmlCDataEscape( s )
+    -- Return s escaped for CData section, escapes: "]]>"
+    return string.gsub( s, ']]>', ']]&gt;' )
+end
+M.private.xmlCDataEscape = xmlCDataEscape
+
+local function stripLuaunitTrace( stackTrace )
+    --[[
+    -- Example of  a traceback:
+    <<stack traceback:
+        example_with_luaunit.lua:130: in function 'test2_withFailure'
+        ./luaunit.lua:1449: in function <./luaunit.lua:1449>
+        [C]: in function 'xpcall'
+        ./luaunit.lua:1449: in function 'protectedCall'
+        ./luaunit.lua:1508: in function 'execOneFunction'
+        ./luaunit.lua:1596: in function 'runSuiteByInstances'
+        ./luaunit.lua:1660: in function 'runSuiteByNames'
+        ./luaunit.lua:1736: in function 'runSuite'
+        example_with_luaunit.lua:140: in main chunk
+        [C]: in ?>>
+
+        Other example:
+    <<stack traceback:
+        ./luaunit.lua:545: in function 'assertEquals'
+        example_with_luaunit.lua:58: in function 'TestToto.test7'
+        ./luaunit.lua:1517: in function <./luaunit.lua:1517>
+        [C]: in function 'xpcall'
+        ./luaunit.lua:1517: in function 'protectedCall'
+        ./luaunit.lua:1578: in function 'execOneFunction'
+        ./luaunit.lua:1677: in function 'runSuiteByInstances'
+        ./luaunit.lua:1730: in function 'runSuiteByNames'
+        ./luaunit.lua:1806: in function 'runSuite'
+        example_with_luaunit.lua:140: in main chunk
+        [C]: in ?>>
+
+    <<stack traceback:
+        luaunit2/example_with_luaunit.lua:124: in function 'test1_withFailure'
+        luaunit2/luaunit.lua:1532: in function <luaunit2/luaunit.lua:1532>
+        [C]: in function 'xpcall'
+        luaunit2/luaunit.lua:1532: in function 'protectedCall'
+        luaunit2/luaunit.lua:1591: in function 'execOneFunction'
+        luaunit2/luaunit.lua:1679: in function 'runSuiteByInstances'
+        luaunit2/luaunit.lua:1743: in function 'runSuiteByNames'
+        luaunit2/luaunit.lua:1819: in function 'runSuite'
+        luaunit2/example_with_luaunit.lua:140: in main chunk
+        [C]: in ?>>
+
+
+    -- first line is "stack traceback": KEEP
+    -- next line may be luaunit line: REMOVE
+    -- next lines are call in the program under testOk: REMOVE
+    -- next lines are calls from luaunit to call the program under test: KEEP
+
+    -- Strategy:
+    -- keep first line
+    -- remove lines that are part of luaunit
+    -- kepp lines until we hit a luaunit line
+    ]]
+
+    local function isLuaunitInternalLine( s )
+        -- return true if line of stack trace comes from inside luaunit
+        return s:find('[/\\]luaunit%.lua:%d+: ') ~= nil
+    end
+
+    -- print( '<<'..stackTrace..'>>' )
+
+    local t = strsplit( '\n', stackTrace )
+    -- print( prettystr(t) )
+
+    local idx = 2
+
+    -- remove lines that are still part of luaunit
+    while t[idx] and isLuaunitInternalLine( t[idx] ) do
+        -- print('Removing : '..t[idx] )
+        table.remove(t, idx)
+    end
+
+    -- keep lines until we hit luaunit again
+    while t[idx] and (not isLuaunitInternalLine(t[idx])) do
+        -- print('Keeping : '..t[idx] )
+        idx = idx + 1
+    end
+
+    -- remove remaining luaunit lines
+    while t[idx] do
+        -- print('Removing : '..t[idx] )
+        table.remove(t, idx)
+    end
+
+    -- print( prettystr(t) )
+    return table.concat( t, '\n')
+
+end
+M.private.stripLuaunitTrace = stripLuaunitTrace
+
+
+local function prettystr_sub(v, indentLevel, printTableRefs, cycleDetectTable )
+    local type_v = type(v)
+    if "string" == type_v  then
+        -- use clever delimiters according to content:
+        -- enclose with single quotes if string contains ", but no '
+        if v:find('"', 1, true) and not v:find("'", 1, true) then
+            return "'" .. v .. "'"
+        end
+        -- use double quotes otherwise, escape embedded "
+        return '"' .. v:gsub('"', '\\"') .. '"'
+
+    elseif "table" == type_v then
+        --if v.__class__ then
+        --    return string.gsub( tostring(v), 'table', v.__class__ )
+        --end
+        return M.private._table_tostring(v, indentLevel, printTableRefs, cycleDetectTable)
+
+    elseif "number" == type_v then
+        -- eliminate differences in formatting between various Lua versions
+        if v ~= v then
+            return "#NaN" -- "not a number"
+        end
+        if v == math.huge then
+            return "#Inf" -- "infinite"
+        end
+        if v == -math.huge then
+            return "-#Inf"
+        end
+        if _VERSION == "Lua 5.3" then
+            local i = math.tointeger(v)
+            if i then
+                return tostring(i)
+            end
+        end
+    end
+
+    return tostring(v)
+end
+
+local function prettystr( v )
+    --[[ Pretty string conversion, to display the full content of a variable of any type.
+
+    * string are enclosed with " by default, or with ' if string contains a "
+    * tables are expanded to show their full content, with indentation in case of nested tables
+    ]]--
+    local cycleDetectTable = {}
+    local s = prettystr_sub(v, 1, M.PRINT_TABLE_REF_IN_ERROR_MSG, cycleDetectTable)
+    if cycleDetectTable.detected and not M.PRINT_TABLE_REF_IN_ERROR_MSG then
+        -- some table contain recursive references,
+        -- so we must recompute the value by including all table references
+        -- else the result looks like crap
+        cycleDetectTable = {}
+        s = prettystr_sub(v, 1, true, cycleDetectTable)
+    end
+    return s
+end
+M.prettystr = prettystr
+
+function M.adjust_err_msg_with_iter( err_msg, iter_msg )
+    --[[ Adjust the error message err_msg: trim the FAILURE_PREFIX or SUCCESS_PREFIX information if needed, 
+    add the iteration message if any and return the result.
+
+    err_msg:  string, error message captured with pcall
+    iter_msg: a string describing the current iteration ("iteration N") or nil
+              if there is no iteration in this test.
+
+    Returns: (new_err_msg, test_status)
+        new_err_msg: string, adjusted error message, or nil in case of success
+        test_status: M.NodeStatus.FAIL, SUCCESS or ERROR according to the information
+                     contained in the error message.
+    ]]
+    if iter_msg then
+        iter_msg = iter_msg..', '
+    else
+        iter_msg = ''
+    end
+
+    local RE_FILE_LINE = '.*:%d+: '
+
+    -- error message is not necessarily a string, 
+    -- so convert the value to string with prettystr()
+    if type( err_msg ) ~= 'string' then
+        err_msg = prettystr( err_msg )
+    end
+
+    if (err_msg:find( M.SUCCESS_PREFIX ) == 1) or err_msg:match( '('..RE_FILE_LINE..')' .. M.SUCCESS_PREFIX .. ".*" ) then
+        -- test finished early with success()
+        return nil, M.NodeStatus.SUCCESS
+    end
+
+    if (err_msg:find( M.SKIP_PREFIX ) == 1) or (err_msg:match( '('..RE_FILE_LINE..')' .. M.SKIP_PREFIX .. ".*" ) ~= nil) then
+        -- substitute prefix by iteration message
+        err_msg = err_msg:gsub('.*'..M.SKIP_PREFIX, iter_msg, 1)
+        -- print("failure detected")
+        return err_msg, M.NodeStatus.SKIP
+    end
+
+    if (err_msg:find( M.FAILURE_PREFIX ) == 1) or (err_msg:match( '('..RE_FILE_LINE..')' .. M.FAILURE_PREFIX .. ".*" ) ~= nil) then
+        -- substitute prefix by iteration message
+        err_msg = err_msg:gsub(M.FAILURE_PREFIX, iter_msg, 1)
+        -- print("failure detected")
+        return err_msg, M.NodeStatus.FAIL
+    end
+
+
+
+    -- print("error detected")
+    -- regular error, not a failure
+    if iter_msg then
+        local match
+        -- "./test\\test_luaunit.lua:2241: some error msg
+        match = err_msg:match( '(.*:%d+: ).*' ) 
+        if match then
+            err_msg = err_msg:gsub( match, match .. iter_msg )
+        else
+            -- no file:line: infromation, just add the iteration info at the beginning of the line
+            err_msg = iter_msg .. err_msg
+        end
+    end
+    return err_msg, M.NodeStatus.ERROR
+end
+
+local function tryMismatchFormatting( table_a, table_b, doDeepAnalysis )
+    --[[
+    Prepares a nice error message when comparing tables, performing a deeper 
+    analysis.
+
+    Arguments:
+    * table_a, table_b: tables to be compared
+    * doDeepAnalysis:
+        M.DEFAULT_DEEP_ANALYSIS: (the default if not specified) perform deep analysis only for big lists and big dictionnaries
+        M.FORCE_DEEP_ANALYSIS  : always perform deep analysis
+        M.DISABLE_DEEP_ANALYSIS: never perform deep analysis
+
+    Returns: {success, result}
+    * success: false if deep analysis could not be performed 
+               in this case, just use standard assertion message
+    * result: if success is true, a multi-line string with deep analysis of the two lists
+    ]]
+
+    -- check if table_a & table_b are suitable for deep analysis
+    if type(table_a) ~= 'table' or type(table_b) ~= 'table' then
+        return false
+    end
+
+    if doDeepAnalysis == M.DISABLE_DEEP_ANALYSIS then
+        return false
+    end
+
+    local len_a, len_b, isPureList = #table_a, #table_b, true
+
+    for k1, v1 in pairs(table_a) do
+        if type(k1) ~= 'number' or k1 > len_a then
+            -- this table a mapping
+            isPureList = false
+            break
+        end
+    end
+
+    if isPureList then
+        for k2, v2 in pairs(table_b) do
+            if type(k2) ~= 'number' or k2 > len_b then
+                -- this table a mapping
+                isPureList = false
+                break
+            end
+        end
+    end
+
+    if isPureList and math.min(len_a, len_b) < M.LIST_DIFF_ANALYSIS_THRESHOLD then
+        if not (doDeepAnalysis == M.FORCE_DEEP_ANALYSIS) then
+            return false
+        end
+    end
+
+    if isPureList then
+        return M.private.mismatchFormattingPureList( table_a, table_b )
+    else
+        -- only work on mapping for the moment
+        -- return M.private.mismatchFormattingMapping( table_a, table_b, doDeepAnalysis )
+        return false
+    end
+end
+M.private.tryMismatchFormatting = tryMismatchFormatting
+
+local function getTaTbDescr()
+    if not M.ORDER_ACTUAL_EXPECTED then
+        return 'expected', 'actual'
+    end
+    return 'actual', 'expected'
+end
+
+local function extendWithStrFmt( res, ... )
+    table.insert( res, string.format( ... ) )
+end
+
+local function mismatchFormattingMapping( table_a, table_b, doDeepAnalysis )
+    --[[
+    Prepares a nice error message when comparing tables which are not pure lists, performing a deeper 
+    analysis.
+
+    Returns: {success, result}
+    * success: false if deep analysis could not be performed 
+               in this case, just use standard assertion message
+    * result: if success is true, a multi-line string with deep analysis of the two lists
+    ]]
+
+    -- disable for the moment
+    --[[
+    local result = {}
+    local descrTa, descrTb = getTaTbDescr()
+
+    local keysCommon = {}
+    local keysOnlyTa = {}
+    local keysOnlyTb = {}
+    local keysDiffTaTb = {}
+
+    local k, v
+
+    for k,v in pairs( table_a ) do
+        if is_equal( v, table_b[k] ) then
+            table.insert( keysCommon, k )
+        else 
+            if table_b[k] == nil then
+                table.insert( keysOnlyTa, k )
+            else
+                table.insert( keysDiffTaTb, k )
+            end
+        end
+    end
+
+    for k,v in pairs( table_b ) do
+        if not is_equal( v, table_a[k] ) and table_a[k] == nil then
+            table.insert( keysOnlyTb, k )
+        end
+    end
+
+    local len_a = #keysCommon + #keysDiffTaTb + #keysOnlyTa
+    local len_b = #keysCommon + #keysDiffTaTb + #keysOnlyTb
+    local limited_display = (len_a < 5 or len_b < 5)
+
+    if math.min(len_a, len_b) < M.TABLE_DIFF_ANALYSIS_THRESHOLD then
+        return false
+    end
+
+    if not limited_display then
+        if len_a == len_b then
+            extendWithStrFmt( result, 'Table A (%s) and B (%s) both have %d items', descrTa, descrTb, len_a )
+        else
+            extendWithStrFmt( result, 'Table A (%s) has %d items and table B (%s) has %d items', descrTa, len_a, descrTb, len_b )
+            end
+
+        if #keysCommon == 0 and #keysDiffTaTb == 0 then
+            table.insert( result, 'Table A and B have no keys in common, they are totally different')
+        else
+            local s_other = 'other '
+            if #keysCommon then
+                extendWithStrFmt( result, 'Table A and B have %d identical items', #keysCommon )
+            else
+                table.insert( result, 'Table A and B have no identical items' )
+                s_other = ''
+            end
+
+            if #keysDiffTaTb ~= 0 then
+                result[#result] = string.format( '%s and %d items differing present in both tables', result[#result], #keysDiffTaTb)
+            else
+                result[#result] = string.format( '%s and no %sitems differing present in both tables', result[#result], s_other, #keysDiffTaTb)
+            end
+        end
+
+        extendWithStrFmt( result, 'Table A has %d keys not present in table B and table B has %d keys not present in table A', #keysOnlyTa, #keysOnlyTb ) 
+    end
+
+    local function keytostring(k)
+        if "string" == type(k) and k:match("^[_%a][_%w]*$") then
+            return k
+        end
+        return prettystr(k)
+    end
+
+    if #keysDiffTaTb ~= 0 then
+        table.insert( result, 'Items differing in A and B:')
+        for k,v in sortedPairs( keysDiffTaTb ) do
+            extendWithStrFmt( result, '  - A[%s]: %s', keytostring(v), prettystr(table_a[v]) )
+            extendWithStrFmt( result, '  + B[%s]: %s', keytostring(v), prettystr(table_b[v]) )
+        end
+    end    
+
+    if #keysOnlyTa ~= 0 then
+        table.insert( result, 'Items only in table A:' )
+        for k,v in sortedPairs( keysOnlyTa ) do
+            extendWithStrFmt( result, '  - A[%s]: %s', keytostring(v), prettystr(table_a[v]) )
+        end
+    end
+
+    if #keysOnlyTb ~= 0 then
+        table.insert( result, 'Items only in table B:' )
+        for k,v in sortedPairs( keysOnlyTb ) do
+            extendWithStrFmt( result, '  + B[%s]: %s', keytostring(v), prettystr(table_b[v]) )
+        end
+    end
+
+    if #keysCommon ~= 0 then
+        table.insert( result, 'Items common to A and B:')
+        for k,v in sortedPairs( keysCommon ) do
+            extendWithStrFmt( result, '  = A and B [%s]: %s', keytostring(v), prettystr(table_a[v]) )
+        end
+    end    
+
+    return true, table.concat( result, '\n')
+    ]]
+end
+M.private.mismatchFormattingMapping = mismatchFormattingMapping
+
+local function mismatchFormattingPureList( table_a, table_b )
+    --[[
+    Prepares a nice error message when comparing tables which are lists, performing a deeper 
+    analysis.
+
+    Returns: {success, result}
+    * success: false if deep analysis could not be performed 
+               in this case, just use standard assertion message
+    * result: if success is true, a multi-line string with deep analysis of the two lists
+    ]]
+    local result, descrTa, descrTb = {}, getTaTbDescr()
+
+    local len_a, len_b, refa, refb = #table_a, #table_b, '', ''
+    if M.PRINT_TABLE_REF_IN_ERROR_MSG then
+        refa, refb = string.format( '<%s> ', M.private.table_ref(table_a)), string.format('<%s> ', M.private.table_ref(table_b) )
+    end
+    local longest, shortest = math.max(len_a, len_b), math.min(len_a, len_b)
+    local deltalv  = longest - shortest
+
+    local commonUntil = shortest
+    for i = 1, shortest do
+        if not is_equal(table_a[i], table_b[i]) then
+            commonUntil = i - 1
+            break
+        end
+    end
+
+    local commonBackTo = shortest - 1
+    for i = 0, shortest - 1 do
+        if not is_equal(table_a[len_a-i], table_b[len_b-i]) then
+            commonBackTo = i - 1
+            break
+        end
+    end
+
+
+    table.insert( result, 'List difference analysis:' )    
+    if len_a == len_b then
+        -- TODO: handle expected/actual naming
+        extendWithStrFmt( result, '* lists %sA (%s) and %sB (%s) have the same size', refa, descrTa, refb, descrTb )
+    else 
+        extendWithStrFmt( result, '* list sizes differ: list %sA (%s) has %d items, list %sB (%s) has %d items', refa, descrTa, len_a, refb, descrTb, len_b )
+    end
+
+    extendWithStrFmt( result, '* lists A and B start differing at index %d', commonUntil+1 ) 
+    if commonBackTo >= 0 then
+        if deltalv > 0 then
+            extendWithStrFmt( result, '* lists A and B are equal again from index %d for A, %d for B', len_a-commonBackTo, len_b-commonBackTo )
+        else
+            extendWithStrFmt( result, '* lists A and B are equal again from index %d', len_a-commonBackTo )
+        end
+    end
+
+    local function insertABValue(ai, bi)
+        bi = bi or ai
+        if is_equal( table_a[ai], table_b[bi]) then
+            return extendWithStrFmt( result, '  = A[%d], B[%d]: %s', ai, bi, prettystr(table_a[ai]) )
+        else
+            extendWithStrFmt( result, '  - A[%d]: %s', ai, prettystr(table_a[ai]))
+            extendWithStrFmt( result, '  + B[%d]: %s', bi, prettystr(table_b[bi]))
+        end
+    end
+
+    -- common parts to list A & B, at the beginning
+    if commonUntil > 0 then
+        table.insert( result, '* Common parts:' )
+        for i = 1, commonUntil do
+            insertABValue( i )
+        end
+    end
+
+    -- diffing parts to list A & B
+    if commonUntil < shortest - commonBackTo - 1 then
+        table.insert( result, '* Differing parts:' )
+        for i = commonUntil + 1, shortest - commonBackTo - 1 do
+            insertABValue( i )
+        end
+    end
+
+    -- display indexes of one list, with no match on other list
+    if shortest - commonBackTo <= longest - commonBackTo - 1 then
+        table.insert( result, '* Present only in one list:' )
+        for i = shortest - commonBackTo, longest - commonBackTo - 1 do
+            if len_a > len_b then
+                extendWithStrFmt( result, '  - A[%d]: %s', i, prettystr(table_a[i]) )
+                -- table.insert( result, '+ (no matching B index)')
+            else
+                -- table.insert( result, '- no matching A index')
+                extendWithStrFmt( result, '  + B[%d]: %s', i, prettystr(table_b[i]) )
+            end
+        end
+    end
+
+    -- common parts to list A & B, at the end
+    if commonBackTo >= 0 then
+        table.insert( result, '* Common parts at the end of the lists' )
+        for i = longest - commonBackTo, longest do
+            if len_a > len_b then
+                insertABValue( i, i-deltalv )
+            else
+                insertABValue( i-deltalv, i )
+            end
+        end
+    end
+
+    return true, table.concat( result, '\n')
+end
+M.private.mismatchFormattingPureList = mismatchFormattingPureList
+
+local function prettystrPairs(value1, value2, suffix_a, suffix_b)
+    --[[
+    This function helps with the recurring task of constructing the "expected
+    vs. actual" error messages. It takes two arbitrary values and formats
+    corresponding strings with prettystr().
+
+    To keep the (possibly complex) output more readable in case the resulting
+    strings contain line breaks, they get automatically prefixed with additional
+    newlines. Both suffixes are optional (default to empty strings), and get
+    appended to the "value1" string. "suffix_a" is used if line breaks were
+    encountered, "suffix_b" otherwise.
+
+    Returns the two formatted strings (including padding/newlines).
+    ]]
+    local str1, str2 = prettystr(value1), prettystr(value2)
+    if hasNewLine(str1) or hasNewLine(str2) then
+        -- line break(s) detected, add padding
+        return "\n" .. str1 .. (suffix_a or ""), "\n" .. str2
+    end
+    return str1 .. (suffix_b or ""), str2
+end
+M.private.prettystrPairs = prettystrPairs
+
+local UNKNOWN_REF = 'table 00-unknown ref'
+local ref_generator = { value=1, [UNKNOWN_REF]=0 }
+
+local function table_ref( t )
+    -- return the default tostring() for tables, with the table ID, even if the table has a metatable
+    -- with the __tostring converter
+    local ref = ''
+    local mt = getmetatable( t )
+    if mt == nil then
+        ref = tostring(t)
+    else
+        local success, result
+        success, result = pcall(setmetatable, t, nil)
+        if not success then
+            -- protected table, if __tostring is defined, we can
+            -- not get the reference. And we can not know in advance.
+            ref = tostring(t) 
+            if not ref:match( 'table: 0?x?[%x]+' ) then
+                return UNKNOWN_REF
+            end
+        else
+            ref = tostring(t)
+            setmetatable( t, mt )
+        end
+    end
+    -- strip the "table: " part
+    ref = ref:sub(8)
+    if ref ~= UNKNOWN_REF and ref_generator[ref] == nil then
+        -- Create a new reference number
+        ref_generator[ref] = ref_generator.value
+        ref_generator.value = ref_generator.value+1
+    end
+    if M.PRINT_TABLE_REF_IN_ERROR_MSG then
+        return string.format('table %02d-%s', ref_generator[ref], ref)
+    else
+        return string.format('table %02d', ref_generator[ref])
+    end
+end
+M.private.table_ref = table_ref
+
+local TABLE_TOSTRING_SEP = ", "
+local TABLE_TOSTRING_SEP_LEN = string.len(TABLE_TOSTRING_SEP)
+
+local function _table_tostring( tbl, indentLevel, printTableRefs, cycleDetectTable )
+    printTableRefs = printTableRefs or M.PRINT_TABLE_REF_IN_ERROR_MSG
+    cycleDetectTable = cycleDetectTable or {}
+    cycleDetectTable[tbl] = true
+
+    local result, dispOnMultLines = {}, false
+
+    -- like prettystr but do not enclose with "" if the string is just alphanumerical
+    -- this is better for displaying table keys who are often simple strings
+    local function keytostring(k)
+        if "string" == type(k) and k:match("^[_%a][_%w]*$") then
+            return k
+        end
+        return prettystr_sub(k, indentLevel+1, printTableRefs, cycleDetectTable)
+    end
+
+    local mt = getmetatable( tbl )
+
+    if mt and mt.__tostring then
+        -- if table has a __tostring() function in its metatable, use it to display the table
+        -- else, compute a regular table
+        result = tostring(tbl)
+        if type(result) ~= 'string' then
+            return string.format( '<invalid tostring() result: "%s" >', prettystr(result) )
+        end
+        result = strsplit( '\n', result )
+        return M.private._table_tostring_format_multiline_string( result, indentLevel )
+
+    else
+        -- no metatable, compute the table representation
+
+        local entry, count, seq_index = nil, 0, 1
+        for k, v in sortedPairs( tbl ) do
+
+            -- key part
+            if k == seq_index then
+                -- for the sequential part of tables, we'll skip the "<key>=" output
+                entry = ''
+                seq_index = seq_index + 1
+            elseif cycleDetectTable[k] then
+                -- recursion in the key detected
+                cycleDetectTable.detected = true
+                entry = "<"..table_ref(k)..">="
+            else
+                entry = keytostring(k) .. "="
+            end
+
+            -- value part 
+            if cycleDetectTable[v] then
+                -- recursion in the value detected!
+                cycleDetectTable.detected = true
+                entry = entry .. "<"..table_ref(v)..">"
+            else
+                entry = entry ..
+                    prettystr_sub( v, indentLevel+1, printTableRefs, cycleDetectTable )
+            end
+            count = count + 1
+            result[count] = entry
+        end
+        return M.private._table_tostring_format_result( tbl, result, indentLevel, printTableRefs )
+    end
+
+end
+M.private._table_tostring = _table_tostring -- prettystr_sub() needs it
+
+local function _table_tostring_format_multiline_string( tbl_str, indentLevel )
+    local indentString = '\n'..string.rep("    ", indentLevel - 1)
+    return table.concat( tbl_str, indentString )
+
+end
+M.private._table_tostring_format_multiline_string = _table_tostring_format_multiline_string
+
+
+local function _table_tostring_format_result( tbl, result, indentLevel, printTableRefs )
+    -- final function called in _table_to_string() to format the resulting list of 
+    -- string describing the table.
+
+    local dispOnMultLines = false
+
+    -- set dispOnMultLines to true if the maximum LINE_LENGTH would be exceeded with the values
+    local totalLength = 0
+    for k, v in ipairs( result ) do
+        totalLength = totalLength + string.len( v )
+        if totalLength >= M.LINE_LENGTH then
+            dispOnMultLines = true
+            break
+        end
+    end
+
+    -- set dispOnMultLines to true if the max LINE_LENGTH would be exceeded
+    -- with the values and the separators.
+    if not dispOnMultLines then
+        -- adjust with length of separator(s):
+        -- two items need 1 sep, three items two seps, ... plus len of '{}'
+        if #result > 0 then
+            totalLength = totalLength + TABLE_TOSTRING_SEP_LEN * (#result - 1)
+        end
+        dispOnMultLines = (totalLength + 2 >= M.LINE_LENGTH)
+    end
+
+    -- now reformat the result table (currently holding element strings)
+    if dispOnMultLines then
+        local indentString = string.rep("    ", indentLevel - 1)
+        result = {  
+                    "{\n    ", 
+                    indentString,
+                    table.concat(result, ",\n    " .. indentString), 
+                    "\n",
+                    indentString, 
+                    "}"
+                }
+    else
+        result = {"{", table.concat(result, TABLE_TOSTRING_SEP), "}"}
+    end
+    if printTableRefs then
+        table.insert(result, 1, "<"..table_ref(tbl).."> ") -- prepend table ref
+    end
+    return table.concat(result)
+end
+M.private._table_tostring_format_result = _table_tostring_format_result -- prettystr_sub() needs it
+
+local function table_findkeyof(t, element)
+    -- Return the key k of the given element in table t, so that t[k] == element
+    -- (or `nil` if element is not present within t). Note that we use our
+    -- 'general' is_equal comparison for matching, so this function should
+    -- handle table-type elements gracefully and consistently.
+    if type(t) == "table" then
+        for k, v in pairs(t) do
+            if is_equal(v, element) then
+                return k
+            end
+        end
+    end
+    return nil
+end
+
+local function _is_table_items_equals(actual, expected )
+    local type_a, type_e = type(actual), type(expected)
+
+    if type_a ~= type_e then
+        return false
+
+    elseif (type_a == 'table') --[[and (type_e == 'table')]] then
+        for k, v in pairs(actual) do
+            if table_findkeyof(expected, v) == nil then
+                return false -- v not contained in expected
+            end
+        end
+        for k, v in pairs(expected) do
+            if table_findkeyof(actual, v) == nil then
+                return false -- v not contained in actual
+            end
+        end
+        return true
+
+    elseif actual ~= expected then
+        return false
+    end
+
+    return true
+end
+
+--[[
+This is a specialized metatable to help with the bookkeeping of recursions
+in _is_table_equals(). It provides an __index table that implements utility
+functions for easier management of the table. The "cached" method queries
+the state of a specific (actual,expected) pair; and the "store" method sets
+this state to the given value. The state of pairs not "seen" / visited is
+assumed to be `nil`.
+]]
+local _recursion_cache_MT = {
+    __index = {
+        -- Return the cached value for an (actual,expected) pair (or `nil`)
+        cached = function(t, actual, expected)
+            local subtable = t[actual] or {}
+            return subtable[expected]
+        end,
+
+        -- Store cached value for a specific (actual,expected) pair.
+        -- Returns the value, so it's easy to use for a "tailcall" (return ...).
+        store = function(t, actual, expected, value, asymmetric)
+            local subtable = t[actual]
+            if not subtable then
+                subtable = {}
+                t[actual] = subtable
+            end
+            subtable[expected] = value
+
+            -- Unless explicitly marked "asymmetric": Consider the recursion
+            -- on (expected,actual) to be equivalent to (actual,expected) by
+            -- default, and thus cache the value for both.
+            if not asymmetric then
+                t:store(expected, actual, value, true)
+            end
+
+            return value
+        end
+    }
+}
+
+local function _is_table_equals(actual, expected, cycleDetectTable)
+    local type_a, type_e = type(actual), type(expected)
+
+    if type_a ~= type_e then
+        return false -- different types won't match
+    end
+
+    if type_a ~= 'table' then
+        -- other typtes compare directly
+        return actual == expected
+    end
+
+    -- print('_is_table_equals( \n     '..prettystr(actual)..'\n      , '..prettystr(expected)..'\n     , '..prettystr(recursions)..' \n )')
+
+    cycleDetectTable = cycleDetectTable or { actual={}, expected={} }
+    if cycleDetectTable.actual[ actual ] then
+        -- oh, we hit a cycle in actual
+        if cycleDetectTable.expected[ expected ] then
+            -- uh, we hit a cycle at the same time in expected
+            -- so the two tables have similar structure
+            return true
+        end
+
+        -- cycle was hit only in actual, the structure differs from expected
+        return false
+    end
+
+    if cycleDetectTable.expected[ expected ] then
+        -- no cycle in actual, but cycle in expected
+        -- the structure differ
+        return false
+    end
+
+    -- at this point, no table cycle detected, we are
+    -- seeing this table for the first time
+
+    -- mark the cycle detection
+    cycleDetectTable.actual[ actual ] = true
+    cycleDetectTable.expected[ expected ] = true
+
+
+    local actualKeysMatched = {}
+    for k, v in pairs(actual) do
+        actualKeysMatched[k] = true -- Keep track of matched keys
+        if not _is_table_equals(v, expected[k], cycleDetectTable) then
+            -- table differs on this key
+            -- clear the cycle detection before returning
+            cycleDetectTable.actual[ actual ] = nil
+            cycleDetectTable.expected[ expected ] = nil
+            return false
+        end
+    end
+
+    for k, v in pairs(expected) do
+        if not actualKeysMatched[k] then
+            -- Found a key that we did not see in "actual" -> mismatch
+            -- clear the cycle detection before returning
+            cycleDetectTable.actual[ actual ] = nil
+            cycleDetectTable.expected[ expected ] = nil
+            return false
+        end
+        -- Otherwise actual[k] was already matched against v = expected[k].
+    end
+
+    -- all key match, we have a match !
+    cycleDetectTable.actual[ actual ] = nil
+    cycleDetectTable.expected[ expected ] = nil
+    return true
+end
+M.private._is_table_equals = _is_table_equals
+is_equal = _is_table_equals
+
+local function failure(main_msg, extra_msg_or_nil, level)
+    -- raise an error indicating a test failure
+    -- for error() compatibility we adjust "level" here (by +1), to report the
+    -- calling context
+    local msg
+    if type(extra_msg_or_nil) == 'string' and extra_msg_or_nil:len() > 0 then
+        msg = extra_msg_or_nil .. '\n' .. main_msg
+    else
+        msg = main_msg
+    end
+    error(M.FAILURE_PREFIX .. msg, (level or 1) + 1)
+end
+
+local function fail_fmt(level, extra_msg_or_nil, ...)
+     -- failure with printf-style formatted message and given error level
+    failure(string.format(...), extra_msg_or_nil, (level or 1) + 1)
+end
+M.private.fail_fmt = fail_fmt
+
+local function error_fmt(level, ...)
+     -- printf-style error()
+    error(string.format(...), (level or 1) + 1)
+end
+
+----------------------------------------------------------------
+--
+--                     assertions
+--
+----------------------------------------------------------------
+
+local function errorMsgEquality(actual, expected, doDeepAnalysis)
+
+    if not M.ORDER_ACTUAL_EXPECTED then
+        expected, actual = actual, expected
+    end
+    if type(expected) == 'string' or type(expected) == 'table' then
+        local strExpected, strActual = prettystrPairs(expected, actual)
+        local result = string.format("expected: %s\nactual: %s", strExpected, strActual)
+
+        -- extend with mismatch analysis if possible:
+        local success, mismatchResult
+        success, mismatchResult = tryMismatchFormatting( actual, expected, doDeepAnalysis )
+        if success then 
+            result = table.concat( { result, mismatchResult }, '\n' )
+        end
+        return result
+    end
+    return string.format("expected: %s, actual: %s",
+                         prettystr(expected), prettystr(actual))
+end
+
+function M.assertError(f, ...)
+    -- assert that calling f with the arguments will raise an error
+    -- example: assertError( f, 1, 2 ) => f(1,2) should generate an error
+    if pcall( f, ... ) then
+        failure( "Expected an error when calling function but no error generated", nil, 2 )
+    end
+end
+
+function M.fail( msg )
+    -- stops a test due to a failure
+    failure( msg, nil, 2 )
+end
+
+function M.failIf( cond, msg )
+    -- Fails a test with "msg" if condition is true
+    if cond then
+        failure( msg, nil, 2 )
+    end
+end
+
+function M.skip(msg)
+    -- skip a running test
+    error(M.SKIP_PREFIX .. msg, 2)
+end
+
+function M.skipIf( cond, msg )
+    -- skip a running test if condition is met
+    if cond then
+        error(M.SKIP_PREFIX .. msg, 2)
+    end
+end
+
+function M.runOnlyIf( cond, msg )
+    -- continue a running test if condition is met, else skip it
+    if not cond then
+        error(M.SKIP_PREFIX .. prettystr(msg), 2)
+    end
+end
+
+function M.success()
+    -- stops a test with a success
+    error(M.SUCCESS_PREFIX, 2)
+end
+
+function M.successIf( cond )
+    -- stops a test with a success if condition is met
+    if cond then
+        error(M.SUCCESS_PREFIX, 2)
+    end
+end
+
+
+------------------------------------------------------------------
+--                  Equality assertions
+------------------------------------------------------------------
+
+function M.assertEquals(actual, expected, extra_msg_or_nil, doDeepAnalysis)
+    if type(actual) == 'table' and type(expected) == 'table' then
+        if not _is_table_equals(actual, expected) then
+            failure( errorMsgEquality(actual, expected, doDeepAnalysis), extra_msg_or_nil, 2 )
+        end
+    elseif type(actual) ~= type(expected) then
+        failure( errorMsgEquality(actual, expected), extra_msg_or_nil, 2 )
+    elseif actual ~= expected then
+        failure( errorMsgEquality(actual, expected), extra_msg_or_nil, 2 )
+    end
+end
+
+function M.almostEquals( actual, expected, margin )
+    if type(actual) ~= 'number' or type(expected) ~= 'number' or type(margin) ~= 'number' then
+        error_fmt(3, 'almostEquals: must supply only number arguments.\nArguments supplied: %s, %s, %s',
+            prettystr(actual), prettystr(expected), prettystr(margin))
+    end
+    if margin < 0 then
+        error('almostEquals: margin must not be negative, current value is ' .. margin, 3)
+    end
+    return math.abs(expected - actual) <= margin
+end
+
+function M.assertAlmostEquals( actual, expected, margin, extra_msg_or_nil )
+    -- check that two floats are close by margin
+    margin = margin or M.EPS
+    if not M.almostEquals(actual, expected, margin) then
+        if not M.ORDER_ACTUAL_EXPECTED then
+            expected, actual = actual, expected
+        end
+        local delta = math.abs(actual - expected) 
+        fail_fmt(2, extra_msg_or_nil, 'Values are not almost equal\n' ..
+                    'Actual: %s, expected: %s, delta %s above margin of %s',
+                    actual, expected, delta, margin)
+    end
+end
+
+function M.assertNotEquals(actual, expected, extra_msg_or_nil)
+    if type(actual) ~= type(expected) then
+        return
+    end
+
+    if type(actual) == 'table' and type(expected) == 'table' then
+        if not _is_table_equals(actual, expected) then
+            return
+        end
+    elseif actual ~= expected then
+        return
+    end
+    fail_fmt(2, extra_msg_or_nil, 'Received the not expected value: %s', prettystr(actual))
+end
+
+function M.assertNotAlmostEquals( actual, expected, margin, extra_msg_or_nil )
+    -- check that two floats are not close by margin
+    margin = margin or M.EPS
+    if M.almostEquals(actual, expected, margin) then
+        if not M.ORDER_ACTUAL_EXPECTED then
+            expected, actual = actual, expected
+        end
+        local delta = math.abs(actual - expected)
+        fail_fmt(2, extra_msg_or_nil, 'Values are almost equal\nActual: %s, expected: %s' ..
+                    ', delta %s below margin of %s',
+                    actual, expected, delta, margin)
+    end
+end
+
+function M.assertItemsEquals(actual, expected, extra_msg_or_nil)
+    -- checks that the items of table expected
+    -- are contained in table actual. Warning, this function
+    -- is at least O(n^2)
+    if not _is_table_items_equals(actual, expected ) then
+        expected, actual = prettystrPairs(expected, actual)
+        fail_fmt(2, extra_msg_or_nil, 'Content of the tables are not identical:\nExpected: %s\nActual: %s',
+                 expected, actual)
+    end
+end
+
+------------------------------------------------------------------
+--                  String assertion
+------------------------------------------------------------------
+
+function M.assertStrContains( str, sub, isPattern, extra_msg_or_nil )
+    -- this relies on lua string.find function
+    -- a string always contains the empty string
+    -- assert( type(str) == 'string', 'Argument 1 of assertStrContains() should be a string.' ) )
+    -- assert( type(sub) == 'string', 'Argument 2 of assertStrContains() should be a string.' ) )
+    if not string.find(str, sub, 1, not isPattern) then
+        sub, str = prettystrPairs(sub, str, '\n')
+        fail_fmt(2, extra_msg_or_nil, 'Could not find %s %s in string %s',
+                 isPattern and 'pattern' or 'substring', sub, str)
+    end
+end
+
+function M.assertStrIContains( str, sub, extra_msg_or_nil )
+    -- this relies on lua string.find function
+    -- a string always contains the empty string
+    if not string.find(str:lower(), sub:lower(), 1, true) then
+        sub, str = prettystrPairs(sub, str, '\n')
+        fail_fmt(2, extra_msg_or_nil, 'Could not find (case insensitively) substring %s in string %s',
+                 sub, str)
+    end
+end
+
+function M.assertNotStrContains( str, sub, isPattern, extra_msg_or_nil )
+    -- this relies on lua string.find function
+    -- a string always contains the empty string
+    if string.find(str, sub, 1, not isPattern) then
+        sub, str = prettystrPairs(sub, str, '\n')
+        fail_fmt(2, extra_msg_or_nil, 'Found the not expected %s %s in string %s',
+                 isPattern and 'pattern' or 'substring', sub, str)
+    end
+end
+
+function M.assertNotStrIContains( str, sub, extra_msg_or_nil )
+    -- this relies on lua string.find function
+    -- a string always contains the empty string
+    if string.find(str:lower(), sub:lower(), 1, true) then
+        sub, str = prettystrPairs(sub, str, '\n')
+        fail_fmt(2, extra_msg_or_nil, 'Found (case insensitively) the not expected substring %s in string %s',
+                 sub, str)
+    end
+end
+
+function M.assertStrMatches( str, pattern, start, final, extra_msg_or_nil )
+    -- Verify a full match for the string
+    if not strMatch( str, pattern, start, final ) then
+        pattern, str = prettystrPairs(pattern, str, '\n')
+        fail_fmt(2, extra_msg_or_nil, 'Could not match pattern %s with string %s',
+                 pattern, str)
+    end
+end
+
+local function _assertErrorMsgEquals( stripFileAndLine, expectedMsg, func, ... )
+    local no_error, error_msg = pcall( func, ... )
+    if no_error then
+        failure( 'No error generated when calling function but expected error: '..M.prettystr(expectedMsg), nil, 3 )
+    end
+    if type(expectedMsg) == "string" and type(error_msg) ~= "string" then
+        -- table are converted to string automatically
+        error_msg = tostring(error_msg)
+    end
+    local differ = false
+    if stripFileAndLine then
+        if error_msg:gsub("^.+:%d+: ", "") ~= expectedMsg then
+            differ = true
+        end
+    else
+        if error_msg ~= expectedMsg then
+            local tr = type(error_msg)
+            local te = type(expectedMsg)
+            if te == 'table' then
+                if tr ~= 'table' then
+                    differ = true
+                else
+                     local ok = pcall(M.assertItemsEquals, error_msg, expectedMsg)
+                     if not ok then
+                         differ = true
+                     end
+                end
+            else
+               differ = true
+            end
+        end
+    end
+
+    if differ then
+        error_msg, expectedMsg = prettystrPairs(error_msg, expectedMsg)
+        fail_fmt(3, nil, 'Error message expected: %s\nError message received: %s\n',
+                 expectedMsg, error_msg)
+    end
+end
+
+function M.assertErrorMsgEquals( expectedMsg, func, ... )
+    -- assert that calling f with the arguments will raise an error
+    -- example: assertError( f, 1, 2 ) => f(1,2) should generate an error
+    _assertErrorMsgEquals(false, expectedMsg, func, ...)
+end
+
+function M.assertErrorMsgContentEquals(expectedMsg, func, ...)
+     _assertErrorMsgEquals(true, expectedMsg, func, ...)
+end
+
+function M.assertErrorMsgContains( partialMsg, func, ... )
+    -- assert that calling f with the arguments will raise an error
+    -- example: assertError( f, 1, 2 ) => f(1,2) should generate an error
+    local no_error, error_msg = pcall( func, ... )
+    if no_error then
+        failure( 'No error generated when calling function but expected error containing: '..prettystr(partialMsg), nil, 2 )
+    end
+    if type(error_msg) ~= "string" then
+        error_msg = tostring(error_msg)
+    end
+    if not string.find( error_msg, partialMsg, nil, true ) then
+        error_msg, partialMsg = prettystrPairs(error_msg, partialMsg)
+        fail_fmt(2, nil, 'Error message does not contain: %s\nError message received: %s\n',
+                 partialMsg, error_msg)
+    end
+end
+
+function M.assertErrorMsgMatches( expectedMsg, func, ... )
+    -- assert that calling f with the arguments will raise an error
+    -- example: assertError( f, 1, 2 ) => f(1,2) should generate an error
+    local no_error, error_msg = pcall( func, ... )
+    if no_error then
+        failure( 'No error generated when calling function but expected error matching: "'..expectedMsg..'"', nil, 2 )
+    end
+    if type(error_msg) ~= "string" then
+        error_msg = tostring(error_msg)
+    end
+    if not strMatch( error_msg, expectedMsg ) then
+        expectedMsg, error_msg = prettystrPairs(expectedMsg, error_msg)
+        fail_fmt(2, nil, 'Error message does not match pattern: %s\nError message received: %s\n',
+                 expectedMsg, error_msg)
+    end
+end
+
+------------------------------------------------------------------
+--              Type assertions
+------------------------------------------------------------------
+
+function M.assertEvalToTrue(value, extra_msg_or_nil)
+    if not value then
+        failure("expected: a value evaluating to true, actual: " ..prettystr(value), extra_msg_or_nil, 2)
+    end
+end
+
+function M.assertEvalToFalse(value, extra_msg_or_nil)
+    if value then
+        failure("expected: false or nil, actual: " ..prettystr(value), extra_msg_or_nil, 2)
+    end
+end
+
+function M.assertIsTrue(value, extra_msg_or_nil)
+    if value ~= true then
+        failure("expected: true, actual: " ..prettystr(value), extra_msg_or_nil, 2)
+    end
+end
+
+function M.assertNotIsTrue(value, extra_msg_or_nil)
+    if value == true then
+        failure("expected: not true, actual: " ..prettystr(value), extra_msg_or_nil, 2)
+    end
+end
+
+function M.assertIsFalse(value, extra_msg_or_nil)
+    if value ~= false then
+        failure("expected: false, actual: " ..prettystr(value), extra_msg_or_nil, 2)
+    end
+end
+
+function M.assertNotIsFalse(value, extra_msg_or_nil)
+    if value == false then
+        failure("expected: not false, actual: " ..prettystr(value), extra_msg_or_nil, 2)
+    end
+end
+
+function M.assertIsNil(value, extra_msg_or_nil)
+    if value ~= nil then
+        failure("expected: nil, actual: " ..prettystr(value), extra_msg_or_nil, 2)
+    end
+end
+
+function M.assertNotIsNil(value, extra_msg_or_nil)
+    if value == nil then
+        failure("expected: not nil, actual: nil", extra_msg_or_nil, 2)
+    end
+end
+
+--[[
+Add type assertion functions to the module table M. Each of these functions
+takes a single parameter "value", and checks that its Lua type matches the
+expected string (derived from the function name):
+
+M.assertIsXxx(value) -> ensure that type(value) conforms to "xxx"
+]]
+for _, funcName in ipairs(
+    {'assertIsNumber', 'assertIsString', 'assertIsTable', 'assertIsBoolean',
+     'assertIsFunction', 'assertIsUserdata', 'assertIsThread'}
+) do
+    local typeExpected = funcName:match("^assertIs([A-Z]%a*)$")
+    -- Lua type() always returns lowercase, also make sure the match() succeeded
+    typeExpected = typeExpected and typeExpected:lower()
+                   or error("bad function name '"..funcName.."' for type assertion")
+
+    M[funcName] = function(value, extra_msg_or_nil)
+        if type(value) ~= typeExpected then
+            if type(value) == 'nil' then
+                fail_fmt(2, extra_msg_or_nil, 'expected: a %s value, actual: nil',
+                         typeExpected, type(value), prettystrPairs(value))
+            else
+                fail_fmt(2, extra_msg_or_nil, 'expected: a %s value, actual: type %s, value %s',
+                         typeExpected, type(value), prettystrPairs(value))
+            end
+        end
+    end
+end
+
+--[[
+Add shortcuts for verifying type of a variable, without failure (luaunit v2 compatibility)
+M.isXxx(value) -> returns true if type(value) conforms to "xxx"
+]]
+for _, typeExpected in ipairs(
+    {'Number', 'String', 'Table', 'Boolean',
+     'Function', 'Userdata', 'Thread', 'Nil' }
+) do
+    local typeExpectedLower = typeExpected:lower()
+    local isType = function(value)
+        return (type(value) == typeExpectedLower)
+    end
+    M['is'..typeExpected] = isType
+    M['is_'..typeExpectedLower] = isType
+end
+
+--[[
+Add non-type assertion functions to the module table M. Each of these functions
+takes a single parameter "value", and checks that its Lua type differs from the
+expected string (derived from the function name):
+
+M.assertNotIsXxx(value) -> ensure that type(value) is not "xxx"
+]]
+for _, funcName in ipairs(
+    {'assertNotIsNumber', 'assertNotIsString', 'assertNotIsTable', 'assertNotIsBoolean',
+     'assertNotIsFunction', 'assertNotIsUserdata', 'assertNotIsThread'}
+) do
+    local typeUnexpected = funcName:match("^assertNotIs([A-Z]%a*)$")
+    -- Lua type() always returns lowercase, also make sure the match() succeeded
+    typeUnexpected = typeUnexpected and typeUnexpected:lower()
+                   or error("bad function name '"..funcName.."' for type assertion")
+
+    M[funcName] = function(value, extra_msg_or_nil)
+        if type(value) == typeUnexpected then
+            fail_fmt(2, extra_msg_or_nil, 'expected: not a %s type, actual: value %s',
+                     typeUnexpected, prettystrPairs(value))
+        end
+    end
+end
+
+function M.assertIs(actual, expected, extra_msg_or_nil)
+    if actual ~= expected then
+        if not M.ORDER_ACTUAL_EXPECTED then
+            actual, expected = expected, actual
+        end
+        local old_print_table_ref_in_error_msg = M.PRINT_TABLE_REF_IN_ERROR_MSG
+        M.PRINT_TABLE_REF_IN_ERROR_MSG = true
+        expected, actual = prettystrPairs(expected, actual, '\n', '')
+        M.PRINT_TABLE_REF_IN_ERROR_MSG = old_print_table_ref_in_error_msg
+        fail_fmt(2, extra_msg_or_nil, 'expected and actual object should not be different\nExpected: %s\nReceived: %s',
+                 expected, actual)
+    end
+end
+
+function M.assertNotIs(actual, expected, extra_msg_or_nil)
+    if actual == expected then
+        local old_print_table_ref_in_error_msg = M.PRINT_TABLE_REF_IN_ERROR_MSG
+        M.PRINT_TABLE_REF_IN_ERROR_MSG = true
+        local s_expected
+        if not M.ORDER_ACTUAL_EXPECTED then
+            s_expected = prettystrPairs(actual)
+        else
+            s_expected = prettystrPairs(expected)
+        end
+        M.PRINT_TABLE_REF_IN_ERROR_MSG = old_print_table_ref_in_error_msg
+        fail_fmt(2, extra_msg_or_nil, 'expected and actual object should be different: %s', s_expected )
+    end
+end
+
+
+------------------------------------------------------------------
+--              Scientific assertions
+------------------------------------------------------------------
+
+
+function M.assertIsNaN(value, extra_msg_or_nil)
+    if type(value) ~= "number" or value == value then
+        failure("expected: NaN, actual: " ..prettystr(value), extra_msg_or_nil, 2)
+    end
+end
+
+function M.assertNotIsNaN(value, extra_msg_or_nil)
+    if type(value) == "number" and value ~= value then
+        failure("expected: not NaN, actual: NaN", extra_msg_or_nil, 2)
+    end
+end
+
+function M.assertIsInf(value, extra_msg_or_nil)
+    if type(value) ~= "number" or math.abs(value) ~= math.huge then
+        failure("expected: #Inf, actual: " ..prettystr(value), extra_msg_or_nil, 2)
+    end
+end
+
+function M.assertIsPlusInf(value, extra_msg_or_nil)
+    if type(value) ~= "number" or value ~= math.huge then
+        failure("expected: #Inf, actual: " ..prettystr(value), extra_msg_or_nil, 2)
+    end
+end
+
+function M.assertIsMinusInf(value, extra_msg_or_nil)
+    if type(value) ~= "number" or value ~= -math.huge then
+        failure("expected: -#Inf, actual: " ..prettystr(value), extra_msg_or_nil, 2)
+    end
+end
+
+function M.assertNotIsPlusInf(value, extra_msg_or_nil)
+    if type(value) == "number" and value == math.huge then
+        failure("expected: not #Inf, actual: #Inf", extra_msg_or_nil, 2)
+    end
+end
+
+function M.assertNotIsMinusInf(value, extra_msg_or_nil)
+    if type(value) == "number" and value == -math.huge then
+        failure("expected: not -#Inf, actual: -#Inf", extra_msg_or_nil, 2)
+    end
+end
+
+function M.assertNotIsInf(value, extra_msg_or_nil)
+    if type(value) == "number" and math.abs(value) == math.huge then
+        failure("expected: not infinity, actual: " .. prettystr(value), extra_msg_or_nil, 2)
+    end
+end
+
+function M.assertIsPlusZero(value, extra_msg_or_nil)
+    if type(value) ~= 'number' or value ~= 0 then
+        failure("expected: +0.0, actual: " ..prettystr(value), extra_msg_or_nil, 2)
+    else if (1/value == -math.huge) then
+            -- more precise error diagnosis
+            failure("expected: +0.0, actual: -0.0", extra_msg_or_nil, 2)
+        else if (1/value ~= math.huge) then
+                -- strange, case should have already been covered
+                failure("expected: +0.0, actual: " ..prettystr(value), extra_msg_or_nil, 2)
+            end
+        end
+    end
+end
+
+function M.assertIsMinusZero(value, extra_msg_or_nil)
+    if type(value) ~= 'number' or value ~= 0 then
+        failure("expected: -0.0, actual: " ..prettystr(value), extra_msg_or_nil, 2)
+    else if (1/value == math.huge) then
+            -- more precise error diagnosis
+            failure("expected: -0.0, actual: +0.0", extra_msg_or_nil, 2)
+        else if (1/value ~= -math.huge) then
+                -- strange, case should have already been covered
+                failure("expected: -0.0, actual: " ..prettystr(value), extra_msg_or_nil, 2)
+            end
+        end
+    end
+end
+
+function M.assertNotIsPlusZero(value, extra_msg_or_nil)
+    if type(value) == 'number' and (1/value == math.huge) then
+        failure("expected: not +0.0, actual: +0.0", extra_msg_or_nil, 2)
+    end
+end
+
+function M.assertNotIsMinusZero(value, extra_msg_or_nil)
+    if type(value) == 'number' and (1/value == -math.huge) then
+        failure("expected: not -0.0, actual: -0.0", extra_msg_or_nil, 2)
+    end
+end
+
+function M.assertTableContains(t, expected)
+    -- checks that table t contains the expected element
+    if table_findkeyof(t, expected) == nil then
+        t, expected = prettystrPairs(t, expected)
+        fail_fmt(2, 'Table %s does NOT contain the expected element %s',
+                 t, expected)
+    end
+end
+
+function M.assertNotTableContains(t, expected)
+    -- checks that table t doesn't contain the expected element
+    local k = table_findkeyof(t, expected)
+    if k ~= nil then
+        t, expected = prettystrPairs(t, expected)
+        fail_fmt(2, 'Table %s DOES contain the unwanted element %s (at key %s)',
+                 t, expected, prettystr(k))
+    end
+end
+
+----------------------------------------------------------------
+--                     Compatibility layer
+----------------------------------------------------------------
+
+-- for compatibility with LuaUnit v2.x
+function M.wrapFunctions()
+    -- In LuaUnit version <= 2.1 , this function was necessary to include
+    -- a test function inside the global test suite. Nowadays, the functions
+    -- are simply run directly as part of the test discovery process.
+    -- so just do nothing !
+    io.stderr:write[[Use of WrapFunctions() is no longer needed.
+Just prefix your test function names with "test" or "Test" and they
+will be picked up and run by LuaUnit.
+]]
+end
+
+local list_of_funcs = {
+    -- { official function name , alias }
+
+    -- general assertions
+    { 'assertEquals'            , 'assert_equals' },
+    { 'assertItemsEquals'       , 'assert_items_equals' },
+    { 'assertNotEquals'         , 'assert_not_equals' },
+    { 'assertAlmostEquals'      , 'assert_almost_equals' },
+    { 'assertNotAlmostEquals'   , 'assert_not_almost_equals' },
+    { 'assertEvalToTrue'        , 'assert_eval_to_true' },
+    { 'assertEvalToFalse'       , 'assert_eval_to_false' },
+    { 'assertStrContains'       , 'assert_str_contains' },
+    { 'assertStrIContains'      , 'assert_str_icontains' },
+    { 'assertNotStrContains'    , 'assert_not_str_contains' },
+    { 'assertNotStrIContains'   , 'assert_not_str_icontains' },
+    { 'assertStrMatches'        , 'assert_str_matches' },
+    { 'assertError'             , 'assert_error' },
+    { 'assertErrorMsgEquals'    , 'assert_error_msg_equals' },
+    { 'assertErrorMsgContains'  , 'assert_error_msg_contains' },
+    { 'assertErrorMsgMatches'   , 'assert_error_msg_matches' },
+    { 'assertErrorMsgContentEquals', 'assert_error_msg_content_equals' },
+    { 'assertIs'                , 'assert_is' },
+    { 'assertNotIs'             , 'assert_not_is' },
+    { 'assertTableContains'     , 'assert_table_contains' },
+    { 'assertNotTableContains'  , 'assert_not_table_contains' },
+    { 'wrapFunctions'           , 'WrapFunctions' },
+    { 'wrapFunctions'           , 'wrap_functions' },
+
+    -- type assertions: assertIsXXX -> assert_is_xxx
+    { 'assertIsNumber'          , 'assert_is_number' },
+    { 'assertIsString'          , 'assert_is_string' },
+    { 'assertIsTable'           , 'assert_is_table' },
+    { 'assertIsBoolean'         , 'assert_is_boolean' },
+    { 'assertIsNil'             , 'assert_is_nil' },
+    { 'assertIsTrue'            , 'assert_is_true' },
+    { 'assertIsFalse'           , 'assert_is_false' },
+    { 'assertIsNaN'             , 'assert_is_nan' },
+    { 'assertIsInf'             , 'assert_is_inf' },
+    { 'assertIsPlusInf'         , 'assert_is_plus_inf' },
+    { 'assertIsMinusInf'        , 'assert_is_minus_inf' },
+    { 'assertIsPlusZero'        , 'assert_is_plus_zero' },
+    { 'assertIsMinusZero'       , 'assert_is_minus_zero' },
+    { 'assertIsFunction'        , 'assert_is_function' },
+    { 'assertIsThread'          , 'assert_is_thread' },
+    { 'assertIsUserdata'        , 'assert_is_userdata' },
+
+    -- type assertions: assertIsXXX -> assertXxx
+    { 'assertIsNumber'          , 'assertNumber' },
+    { 'assertIsString'          , 'assertString' },
+    { 'assertIsTable'           , 'assertTable' },
+    { 'assertIsBoolean'         , 'assertBoolean' },
+    { 'assertIsNil'             , 'assertNil' },
+    { 'assertIsTrue'            , 'assertTrue' },
+    { 'assertIsFalse'           , 'assertFalse' },
+    { 'assertIsNaN'             , 'assertNaN' },
+    { 'assertIsInf'             , 'assertInf' },
+    { 'assertIsPlusInf'         , 'assertPlusInf' },
+    { 'assertIsMinusInf'        , 'assertMinusInf' },
+    { 'assertIsPlusZero'        , 'assertPlusZero' },
+    { 'assertIsMinusZero'       , 'assertMinusZero'},
+    { 'assertIsFunction'        , 'assertFunction' },
+    { 'assertIsThread'          , 'assertThread' },
+    { 'assertIsUserdata'        , 'assertUserdata' },
+
+    -- type assertions: assertIsXXX -> assert_xxx (luaunit v2 compat)
+    { 'assertIsNumber'          , 'assert_number' },
+    { 'assertIsString'          , 'assert_string' },
+    { 'assertIsTable'           , 'assert_table' },
+    { 'assertIsBoolean'         , 'assert_boolean' },
+    { 'assertIsNil'             , 'assert_nil' },
+    { 'assertIsTrue'            , 'assert_true' },
+    { 'assertIsFalse'           , 'assert_false' },
+    { 'assertIsNaN'             , 'assert_nan' },
+    { 'assertIsInf'             , 'assert_inf' },
+    { 'assertIsPlusInf'         , 'assert_plus_inf' },
+    { 'assertIsMinusInf'        , 'assert_minus_inf' },
+    { 'assertIsPlusZero'        , 'assert_plus_zero' },
+    { 'assertIsMinusZero'       , 'assert_minus_zero' },
+    { 'assertIsFunction'        , 'assert_function' },
+    { 'assertIsThread'          , 'assert_thread' },
+    { 'assertIsUserdata'        , 'assert_userdata' },
+
+    -- type assertions: assertNotIsXXX -> assert_not_is_xxx
+    { 'assertNotIsNumber'       , 'assert_not_is_number' },
+    { 'assertNotIsString'       , 'assert_not_is_string' },
+    { 'assertNotIsTable'        , 'assert_not_is_table' },
+    { 'assertNotIsBoolean'      , 'assert_not_is_boolean' },
+    { 'assertNotIsNil'          , 'assert_not_is_nil' },
+    { 'assertNotIsTrue'         , 'assert_not_is_true' },
+    { 'assertNotIsFalse'        , 'assert_not_is_false' },
+    { 'assertNotIsNaN'          , 'assert_not_is_nan' },
+    { 'assertNotIsInf'          , 'assert_not_is_inf' },
+    { 'assertNotIsPlusInf'      , 'assert_not_plus_inf' },
+    { 'assertNotIsMinusInf'     , 'assert_not_minus_inf' },
+    { 'assertNotIsPlusZero'     , 'assert_not_plus_zero' },
+    { 'assertNotIsMinusZero'    , 'assert_not_minus_zero' },
+    { 'assertNotIsFunction'     , 'assert_not_is_function' },
+    { 'assertNotIsThread'       , 'assert_not_is_thread' },
+    { 'assertNotIsUserdata'     , 'assert_not_is_userdata' },
+
+    -- type assertions: assertNotIsXXX -> assertNotXxx (luaunit v2 compat)
+    { 'assertNotIsNumber'       , 'assertNotNumber' },
+    { 'assertNotIsString'       , 'assertNotString' },
+    { 'assertNotIsTable'        , 'assertNotTable' },
+    { 'assertNotIsBoolean'      , 'assertNotBoolean' },
+    { 'assertNotIsNil'          , 'assertNotNil' },
+    { 'assertNotIsTrue'         , 'assertNotTrue' },
+    { 'assertNotIsFalse'        , 'assertNotFalse' },
+    { 'assertNotIsNaN'          , 'assertNotNaN' },
+    { 'assertNotIsInf'          , 'assertNotInf' },
+    { 'assertNotIsPlusInf'      , 'assertNotPlusInf' },
+    { 'assertNotIsMinusInf'     , 'assertNotMinusInf' },
+    { 'assertNotIsPlusZero'     , 'assertNotPlusZero' },
+    { 'assertNotIsMinusZero'    , 'assertNotMinusZero' },
+    { 'assertNotIsFunction'     , 'assertNotFunction' },
+    { 'assertNotIsThread'       , 'assertNotThread' },
+    { 'assertNotIsUserdata'     , 'assertNotUserdata' },
+
+    -- type assertions: assertNotIsXXX -> assert_not_xxx
+    { 'assertNotIsNumber'       , 'assert_not_number' },
+    { 'assertNotIsString'       , 'assert_not_string' },
+    { 'assertNotIsTable'        , 'assert_not_table' },
+    { 'assertNotIsBoolean'      , 'assert_not_boolean' },
+    { 'assertNotIsNil'          , 'assert_not_nil' },
+    { 'assertNotIsTrue'         , 'assert_not_true' },
+    { 'assertNotIsFalse'        , 'assert_not_false' },
+    { 'assertNotIsNaN'          , 'assert_not_nan' },
+    { 'assertNotIsInf'          , 'assert_not_inf' },
+    { 'assertNotIsPlusInf'      , 'assert_not_plus_inf' },
+    { 'assertNotIsMinusInf'     , 'assert_not_minus_inf' },
+    { 'assertNotIsPlusZero'     , 'assert_not_plus_zero' },
+    { 'assertNotIsMinusZero'    , 'assert_not_minus_zero' },
+    { 'assertNotIsFunction'     , 'assert_not_function' },
+    { 'assertNotIsThread'       , 'assert_not_thread' },
+    { 'assertNotIsUserdata'     , 'assert_not_userdata' },
+
+    -- all assertions with Coroutine duplicate Thread assertions
+    { 'assertIsThread'          , 'assertIsCoroutine' },
+    { 'assertIsThread'          , 'assertCoroutine' },
+    { 'assertIsThread'          , 'assert_is_coroutine' },
+    { 'assertIsThread'          , 'assert_coroutine' },
+    { 'assertNotIsThread'       , 'assertNotIsCoroutine' },
+    { 'assertNotIsThread'       , 'assertNotCoroutine' },
+    { 'assertNotIsThread'       , 'assert_not_is_coroutine' },
+    { 'assertNotIsThread'       , 'assert_not_coroutine' },
+}
+
+-- Create all aliases in M
+for _,v in ipairs( list_of_funcs ) do
+    local funcname, alias = v[1], v[2]
+    M[alias] = M[funcname]
+
+    if EXPORT_ASSERT_TO_GLOBALS then
+        _G[funcname] = M[funcname]
+        _G[alias] = M[funcname]
+    end
+end
+
+----------------------------------------------------------------
+--
+--                     Outputters
+--
+----------------------------------------------------------------
+
+-- A common "base" class for outputters
+-- For concepts involved (class inheritance) see http://www.lua.org/pil/16.2.html
+
+local genericOutput = { __class__ = 'genericOutput' } -- class
+local genericOutput_MT = { __index = genericOutput } -- metatable
+M.genericOutput = genericOutput -- publish, so that custom classes may derive from it
+
+function genericOutput.new(runner, default_verbosity)
+    -- runner is the "parent" object controlling the output, usually a LuaUnit instance
+    local t = { runner = runner }
+    if runner then
+        t.result = runner.result
+        t.verbosity = runner.verbosity or default_verbosity
+        t.fname = runner.fname
+    else
+        t.verbosity = default_verbosity
+    end
+    return setmetatable( t, genericOutput_MT)
+end
+
+-- abstract ("empty") methods
+function genericOutput:startSuite() 
+    -- Called once, when the suite is started
+end
+
+function genericOutput:startClass(className) 
+    -- Called each time a new test class is started
+end
+
+function genericOutput:startTest(testName) 
+    -- called each time a new test is started, right before the setUp()
+    -- the current test status node is already created and available in: self.result.currentNode
+end
+
+function genericOutput:updateStatus(node) 
+    -- called with status failed or error as soon as the error/failure is encountered
+    -- this method is NOT called for a successful test because a test is marked as successful by default
+    -- and does not need to be updated
+end
+
+function genericOutput:endTest(node) 
+    -- called when the test is finished, after the tearDown() method
+end
+
+function genericOutput:endClass() 
+    -- called when executing the class is finished, before moving on to the next class of at the end of the test execution
+end
+
+function genericOutput:endSuite() 
+    -- called at the end of the test suite execution
+end
+
+
+----------------------------------------------------------------
+--                     class TapOutput
+----------------------------------------------------------------
+
+local TapOutput = genericOutput.new() -- derived class
+local TapOutput_MT = { __index = TapOutput } -- metatable
+TapOutput.__class__ = 'TapOutput'
+
+    -- For a good reference for TAP format, check: http://testanything.org/tap-specification.html
+
+    function TapOutput.new(runner)
+        local t = genericOutput.new(runner, M.VERBOSITY_LOW)
+        return setmetatable( t, TapOutput_MT)
+    end
+    function TapOutput:startSuite()
+        print("1.."..self.result.selectedCount)
+        print('# Started on '..self.result.startDate)
+    end
+    function TapOutput:startClass(className)
+        if className ~= '[TestFunctions]' then
+            print('# Starting class: '..className)
+        end
+    end
+
+    function TapOutput:updateStatus( node )
+        if node:isSkipped() then
+            io.stdout:write("ok ", self.result.currentTestNumber, "\t# SKIP ", node.msg, "\n" )
+            return
+        end
+
+        io.stdout:write("not ok ", self.result.currentTestNumber, "\t", node.testName, "\n")
+        if self.verbosity > M.VERBOSITY_LOW then
+           print( prefixString( '#   ', node.msg ) )
+        end
+        if (node:isFailure() or node:isError()) and self.verbosity > M.VERBOSITY_DEFAULT then
+           print( prefixString( '#   ', node.stackTrace ) )
+        end
+    end
+
+    function TapOutput:endTest( node )
+        if node:isSuccess() then
+            io.stdout:write("ok     ", self.result.currentTestNumber, "\t", node.testName, "\n")
+        end
+    end
+
+    function TapOutput:endSuite()
+        print( '# '..M.LuaUnit.statusLine( self.result ) )
+        return self.result.notSuccessCount
+    end
+
+
+-- class TapOutput end
+
+----------------------------------------------------------------
+--                     class JUnitOutput
+----------------------------------------------------------------
+
+-- See directory junitxml for more information about the junit format
+local JUnitOutput = genericOutput.new() -- derived class
+local JUnitOutput_MT = { __index = JUnitOutput } -- metatable
+JUnitOutput.__class__ = 'JUnitOutput'
+
+    function JUnitOutput.new(runner)
+        local t = genericOutput.new(runner, M.VERBOSITY_LOW)
+        t.testList = {}
+        return setmetatable( t, JUnitOutput_MT )
+    end
+
+    function JUnitOutput:startSuite()
+        -- open xml file early to deal with errors
+        if self.fname == nil then
+            error('With Junit, an output filename must be supplied with --name!')
+        end
+        if string.sub(self.fname,-4) ~= '.xml' then
+            self.fname = self.fname..'.xml'
+        end
+        self.fd = io.open(self.fname, "w")
+        if self.fd == nil then
+            error("Could not open file for writing: "..self.fname)
+        end
+
+        print('# XML output to '..self.fname)
+        print('# Started on '..self.result.startDate)
+    end
+    function JUnitOutput:startClass(className)
+        if className ~= '[TestFunctions]' then
+            print('# Starting class: '..className)
+        end
+    end
+    function JUnitOutput:startTest(testName)
+        print('# Starting test: '..testName)
+    end
+
+    function JUnitOutput:updateStatus( node )
+        if node:isFailure() then
+            print( '#   Failure: ' .. prefixString( '#   ', node.msg ):sub(4, nil) )
+            -- print('# ' .. node.stackTrace)
+        elseif node:isError() then
+            print( '#   Error: ' .. prefixString( '#   '  , node.msg ):sub(4, nil) )
+            -- print('# ' .. node.stackTrace)
+        end
+    end
+
+    function JUnitOutput:endSuite()
+        print( '# '..M.LuaUnit.statusLine(self.result))
+
+        -- XML file writing
+        self.fd:write('<?xml version="1.0" encoding="UTF-8" ?>\n')
+        self.fd:write('<testsuites>\n')
+        self.fd:write(string.format(
+            '    <testsuite name="LuaUnit" id="00001" package="" hostname="localhost" tests="%d" timestamp="%s" time="%0.3f" errors="%d" failures="%d" skipped="%d">\n',
+            self.result.runCount, self.result.startIsodate, self.result.duration, self.result.errorCount, self.result.failureCount, self.result.skippedCount ))
+        self.fd:write("        <properties>\n")
+        self.fd:write(string.format('            <property name="Lua Version" value="%s"/>\n', _VERSION ) )
+        self.fd:write(string.format('            <property name="LuaUnit Version" value="%s"/>\n', M.VERSION) )
+        -- XXX please include system name and version if possible
+        self.fd:write("        </properties>\n")
+
+        for i,node in ipairs(self.result.allTests) do
+            self.fd:write(string.format('        <testcase classname="%s" name="%s" time="%0.3f">\n',
+                node.className, node.testName, node.duration ) )
+            if node:isNotSuccess() then
+                self.fd:write(node:statusXML())
+            end
+            self.fd:write('        </testcase>\n')
+        end
+
+        -- Next two lines are needed to validate junit ANT xsd, but really not useful in general:
+        self.fd:write('    <system-out/>\n')
+        self.fd:write('    <system-err/>\n')
+
+        self.fd:write('    </testsuite>\n')
+        self.fd:write('</testsuites>\n')
+        self.fd:close()
+        return self.result.notSuccessCount
+    end
+
+
+-- class TapOutput end
+
+----------------------------------------------------------------
+--                     class TextOutput
+----------------------------------------------------------------
+
+--[[    Example of other unit-tests suite text output
+
+-- Python Non verbose:
+
+For each test: . or F or E
+
+If some failed tests:
+    ==============
+    ERROR / FAILURE: TestName (testfile.testclass)
+    ---------
+    Stack trace
+
+
+then --------------
+then "Ran x tests in 0.000s"
+then OK or FAILED (failures=1, error=1)
+
+-- Python Verbose:
+testname (filename.classname) ... ok
+testname (filename.classname) ... FAIL
+testname (filename.classname) ... ERROR
+
+then --------------
+then "Ran x tests in 0.000s"
+then OK or FAILED (failures=1, error=1)
+
+-- Ruby:
+Started
+ .
+ Finished in 0.002695 seconds.
+
+ 1 tests, 2 assertions, 0 failures, 0 errors
+
+-- Ruby:
+>> ruby tc_simple_number2.rb
+Loaded suite tc_simple_number2
+Started
+F..
+Finished in 0.038617 seconds.
+
+  1) Failure:
+test_failure(TestSimpleNumber) [tc_simple_number2.rb:16]:
+Adding doesn't work.
+<3> expected but was
+<4>.
+
+3 tests, 4 assertions, 1 failures, 0 errors
+
+-- Java Junit
+.......F.
+Time: 0,003
+There was 1 failure:
+1) testCapacity(junit.samples.VectorTest)junit.framework.AssertionFailedError
+    at junit.samples.VectorTest.testCapacity(VectorTest.java:87)
+    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
+    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
+    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
+
+FAILURES!!!
+Tests run: 8,  Failures: 1,  Errors: 0
+
+
+-- Maven
+
+# mvn test
+-------------------------------------------------------
+ T E S T S
+-------------------------------------------------------
+Running math.AdditionTest
+Tests run: 2, Failures: 1, Errors: 0, Skipped: 0, Time elapsed:
+0.03 sec <<< FAILURE!
+
+Results :
+
+Failed tests:
+  testLireSymbole(math.AdditionTest)
+
+Tests run: 2, Failures: 1, Errors: 0, Skipped: 0
+
+
+-- LuaUnit
+---- non verbose
+* display . or F or E when running tests
+---- verbose
+* display test name + ok/fail
+----
+* blank line
+* number) ERROR or FAILURE: TestName
+   Stack trace
+* blank line
+* number) ERROR or FAILURE: TestName
+   Stack trace
+
+then --------------
+then "Ran x tests in 0.000s (%d not selected, %d skipped)"
+then OK or FAILED (failures=1, error=1)
+
+
+]]
+
+local TextOutput = genericOutput.new() -- derived class
+local TextOutput_MT = { __index = TextOutput } -- metatable
+TextOutput.__class__ = 'TextOutput'
+
+    function TextOutput.new(runner)
+        local t = genericOutput.new(runner, M.VERBOSITY_DEFAULT)
+        t.errorList = {}
+        return setmetatable( t, TextOutput_MT )
+    end
+
+    function TextOutput:startSuite()
+        if self.verbosity > M.VERBOSITY_DEFAULT then
+            print( 'Started on '.. self.result.startDate )
+        end
+    end
+
+    function TextOutput:startTest(testName)
+        if self.verbosity > M.VERBOSITY_DEFAULT then
+            io.stdout:write( "    ", self.result.currentNode.testName, " ... " )
+        end
+    end
+
+    function TextOutput:endTest( node )
+        if node:isSuccess() then
+            if self.verbosity > M.VERBOSITY_DEFAULT then
+                io.stdout:write("Ok\n")
+            else
+                io.stdout:write(".")
+                io.stdout:flush()
+            end
+        else
+            if self.verbosity > M.VERBOSITY_DEFAULT then
+                print( node.status )
+                print( node.msg )
+                --[[
+                -- find out when to do this:
+                if self.verbosity > M.VERBOSITY_DEFAULT then
+                    print( node.stackTrace )
+                end
+                ]]
+            else
+                -- write only the first character of status E, F or S
+                io.stdout:write(string.sub(node.status, 1, 1))
+                io.stdout:flush()
+            end
+        end
+    end
+
+    function TextOutput:displayOneFailedTest( index, fail )
+        print(index..") "..fail.testName )
+        print( fail.msg )
+        print( fail.stackTrace )
+        print()
+    end
+
+    function TextOutput:displayErroredTests()
+        if #self.result.errorTests ~= 0 then
+            print("Tests with errors:")
+            print("------------------")
+            for i, v in ipairs(self.result.errorTests) do
+                self:displayOneFailedTest(i, v)
+            end
+        end
+    end
+
+    function TextOutput:displayFailedTests()
+        if #self.result.failedTests ~= 0 then
+            print("Failed tests:")
+            print("-------------")
+            for i, v in ipairs(self.result.failedTests) do
+                self:displayOneFailedTest(i, v)
+            end
+        end
+    end
+
+    function TextOutput:endSuite()
+        if self.verbosity > M.VERBOSITY_DEFAULT then
+            print("=========================================================")
+        else
+            print()
+        end
+        self:displayErroredTests()
+        self:displayFailedTests()
+        print( M.LuaUnit.statusLine( self.result ) )
+        if self.result.notSuccessCount == 0 then
+            print('OK')
+        end
+    end
+
+-- class TextOutput end
+
+
+----------------------------------------------------------------
+--                     class NilOutput
+----------------------------------------------------------------
+
+local function nopCallable()
+    --print(42)
+    return nopCallable
+end
+
+local NilOutput = { __class__ = 'NilOuptut' } -- class
+local NilOutput_MT = { __index = nopCallable } -- metatable
+
+function NilOutput.new(runner)
+    return setmetatable( { __class__ = 'NilOutput' }, NilOutput_MT )
+end
+
+----------------------------------------------------------------
+--
+--                     class LuaUnit
+--
+----------------------------------------------------------------
+
+M.LuaUnit = {
+    outputType = TextOutput,
+    verbosity = M.VERBOSITY_DEFAULT,
+    __class__ = 'LuaUnit'
+}
+local LuaUnit_MT = { __index = M.LuaUnit }
+
+if EXPORT_ASSERT_TO_GLOBALS then
+    LuaUnit = M.LuaUnit
+end
+
+    function M.LuaUnit.new()
+        return setmetatable( {}, LuaUnit_MT )
+    end
+
+    -----------------[[ Utility methods ]]---------------------
+
+    function M.LuaUnit.asFunction(aObject)
+        -- return "aObject" if it is a function, and nil otherwise
+        if 'function' == type(aObject) then
+            return aObject
+        end
+    end
+
+    function M.LuaUnit.splitClassMethod(someName)
+        --[[
+        Return a pair of className, methodName strings for a name in the form
+        "class.method". If no class part (or separator) is found, will return
+        nil, someName instead (the latter being unchanged).
+
+        This convention thus also replaces the older isClassMethod() test:
+        You just have to check for a non-nil className (return) value.
+        ]]
+        local separator = string.find(someName, '.', 1, true)
+        if separator then
+            return someName:sub(1, separator - 1), someName:sub(separator + 1)
+        end
+        return nil, someName
+    end
+
+    function M.LuaUnit.isMethodTestName( s )
+        -- return true is the name matches the name of a test method
+        -- default rule is that is starts with 'Test' or with 'test'
+        return string.sub(s, 1, 4):lower() == 'test'
+    end
+
+    function M.LuaUnit.isTestName( s )
+        -- return true is the name matches the name of a test
+        -- default rule is that is starts with 'Test' or with 'test'
+        return string.sub(s, 1, 4):lower() == 'test'
+    end
+
+    function M.LuaUnit.collectTests()
+        -- return a list of all test names in the global namespace
+        -- that match LuaUnit.isTestName
+
+        local testNames = {}
+        for k, _ in pairs(_G) do
+            if type(k) == "string" and M.LuaUnit.isTestName( k ) then
+                table.insert( testNames , k )
+            end
+        end
+        table.sort( testNames )
+        return testNames
+    end
+
+    function M.LuaUnit.parseCmdLine( cmdLine )
+        -- parse the command line
+        -- Supported command line parameters:
+        -- --verbose, -v: increase verbosity
+        -- --quiet, -q: silence output
+        -- --error, -e: treat errors as fatal (quit program)
+        -- --output, -o, + name: select output type
+        -- --pattern, -p, + pattern: run test matching pattern, may be repeated
+        -- --exclude, -x, + pattern: run test not matching pattern, may be repeated
+        -- --shuffle, -s, : shuffle tests before reunning them
+        -- --name, -n, + fname: name of output file for junit, default to stdout
+        -- --repeat, -r, + num: number of times to execute each test
+        -- [testnames, ...]: run selected test names
+        --
+        -- Returns a table with the following fields:
+        -- verbosity: nil, M.VERBOSITY_DEFAULT, M.VERBOSITY_QUIET, M.VERBOSITY_VERBOSE
+        -- output: nil, 'tap', 'junit', 'text', 'nil'
+        -- testNames: nil or a list of test names to run
+        -- exeRepeat: num or 1
+        -- pattern: nil or a list of patterns
+        -- exclude: nil or a list of patterns
+
+        local result, state = {}, nil
+        local SET_OUTPUT = 1
+        local SET_PATTERN = 2
+        local SET_EXCLUDE = 3
+        local SET_FNAME = 4
+        local SET_REPEAT = 5
+
+        if cmdLine == nil then
+            return result
+        end
+
+        local function parseOption( option )
+            if option == '--help' or option == '-h' then
+                result['help'] = true
+                return
+            elseif option == '--version' then
+                result['version'] = true
+                return
+            elseif option == '--verbose' or option == '-v' then
+                result['verbosity'] = M.VERBOSITY_VERBOSE
+                return
+            elseif option == '--quiet' or option == '-q' then
+                result['verbosity'] = M.VERBOSITY_QUIET
+                return
+            elseif option == '--error' or option == '-e' then
+                result['quitOnError'] = true
+                return
+            elseif option == '--failure' or option == '-f' then
+                result['quitOnFailure'] = true
+                return
+            elseif option == '--shuffle' or option == '-s' then
+                result['shuffle'] = true
+                return
+            elseif option == '--output' or option == '-o' then
+                state = SET_OUTPUT
+                return state
+            elseif option == '--name' or option == '-n' then
+                state = SET_FNAME
+                return state
+            elseif option == '--repeat' or option == '-r' then
+                state = SET_REPEAT
+                return state
+            elseif option == '--pattern' or option == '-p' then
+                state = SET_PATTERN
+                return state
+            elseif option == '--exclude' or option == '-x' then
+                state = SET_EXCLUDE
+                return state
+            end
+            error('Unknown option: '..option,3)
+        end
+
+        local function setArg( cmdArg, state )
+            if state == SET_OUTPUT then
+                result['output'] = cmdArg
+                return
+            elseif state == SET_FNAME then
+                result['fname'] = cmdArg
+                return
+            elseif state == SET_REPEAT then
+                result['exeRepeat'] = tonumber(cmdArg)
+                                     or error('Malformed -r argument: '..cmdArg)
+                return
+            elseif state == SET_PATTERN then
+                if result['pattern'] then
+                    table.insert( result['pattern'], cmdArg )
+                else
+                    result['pattern'] = { cmdArg }
+                end
+                return
+            elseif state == SET_EXCLUDE then
+                local notArg = '!'..cmdArg
+                if result['pattern'] then
+                    table.insert( result['pattern'],  notArg )
+                else
+                    result['pattern'] = { notArg }
+                end
+                return
+            end
+            error('Unknown parse state: '.. state)
+        end
+
+
+        for i, cmdArg in ipairs(cmdLine) do
+            if state ~= nil then
+                setArg( cmdArg, state, result )
+                state = nil
+            else
+                if cmdArg:sub(1,1) == '-' then
+                    state = parseOption( cmdArg )
+                else
+                    if result['testNames'] then
+                        table.insert( result['testNames'], cmdArg )
+                    else
+                        result['testNames'] = { cmdArg }
+                    end
+                end
+            end
+        end
+
+        if result['help'] then
+            M.LuaUnit.help()
+        end
+
+        if result['version'] then
+            M.LuaUnit.version()
+        end
+
+        if state ~= nil then
+            error('Missing argument after '..cmdLine[ #cmdLine ],2 )
+        end
+
+        return result
+    end
+
+    function M.LuaUnit.help()
+        print(M.USAGE)
+        os.exit(0)
+    end
+
+    function M.LuaUnit.version()
+        print('LuaUnit v'..M.VERSION..' by Philippe Fremy <[email protected]>')
+        os.exit(0)
+    end
+
+----------------------------------------------------------------
+--                     class NodeStatus
+----------------------------------------------------------------
+
+    local NodeStatus = { __class__ = 'NodeStatus' } -- class
+    local NodeStatus_MT = { __index = NodeStatus } -- metatable
+    M.NodeStatus = NodeStatus
+
+    -- values of status
+    NodeStatus.SUCCESS  = 'SUCCESS'
+    NodeStatus.SKIP     = 'SKIP'
+    NodeStatus.FAIL     = 'FAIL'
+    NodeStatus.ERROR    = 'ERROR'
+
+    function NodeStatus.new( number, testName, className )
+        -- default constructor, test are PASS by default
+        local t = { number = number, testName = testName, className = className }
+        setmetatable( t, NodeStatus_MT )
+        t:success()
+        return t
+    end
+
+    function NodeStatus:success()
+        self.status = self.SUCCESS
+        -- useless because lua does this for us, but it helps me remembering the relevant field names
+        self.msg = nil
+        self.stackTrace = nil
+    end
+
+    function NodeStatus:skip(msg)
+        self.status = self.SKIP
+        self.msg = msg
+        self.stackTrace = nil
+    end
+
+    function NodeStatus:fail(msg, stackTrace)
+        self.status = self.FAIL
+        self.msg = msg
+        self.stackTrace = stackTrace
+    end
+
+    function NodeStatus:error(msg, stackTrace)
+        self.status = self.ERROR
+        self.msg = msg
+        self.stackTrace = stackTrace
+    end
+
+    function NodeStatus:isSuccess()
+        return self.status == NodeStatus.SUCCESS
+    end
+
+    function NodeStatus:isNotSuccess()
+        -- Return true if node is either failure or error or skip
+        return (self.status == NodeStatus.FAIL or self.status == NodeStatus.ERROR or self.status == NodeStatus.SKIP)
+    end
+
+    function NodeStatus:isSkipped()
+        return self.status == NodeStatus.SKIP
+    end
+
+    function NodeStatus:isFailure()
+        return self.status == NodeStatus.FAIL
+    end
+
+    function NodeStatus:isError()
+        return self.status == NodeStatus.ERROR
+    end
+
+    function NodeStatus:statusXML()
+        if self:isError() then
+            return table.concat(
+                {'            <error type="', xmlEscape(self.msg), '">\n',
+                 '                <![CDATA[', xmlCDataEscape(self.stackTrace),
+                 ']]></error>\n'})
+        elseif self:isFailure() then
+            return table.concat(
+                {'            <failure type="', xmlEscape(self.msg), '">\n',
+                 '                <![CDATA[', xmlCDataEscape(self.stackTrace),
+                 ']]></failure>\n'})
+        elseif self:isSkipped() then
+            return table.concat({'            <skipped>', xmlEscape(self.msg),'</skipped>\n' } )
+        end
+        return '            <passed/>\n' -- (not XSD-compliant! normally shouldn't get here)
+    end
+
+    --------------[[ Output methods ]]-------------------------
+
+    local function conditional_plural(number, singular)
+        -- returns a grammatically well-formed string "%d <singular/plural>"
+        local suffix = ''
+        if number ~= 1 then -- use plural
+            suffix = (singular:sub(-2) == 'ss') and 'es' or 's'
+        end
+        return string.format('%d %s%s', number, singular, suffix)
+    end
+
+    function M.LuaUnit.statusLine(result)
+        -- return status line string according to results
+        local s = {
+            string.format('Ran %d tests in %0.3f seconds',
+                          result.runCount, result.duration),
+            conditional_plural(result.successCount, 'success'),
+        }
+        if result.notSuccessCount > 0 then
+            if result.failureCount > 0 then
+                table.insert(s, conditional_plural(result.failureCount, 'failure'))
+            end
+            if result.errorCount > 0 then
+                table.insert(s, conditional_plural(result.errorCount, 'error'))
+            end
+        else
+            table.insert(s, '0 failures')
+        end
+        if result.skippedCount > 0 then
+            table.insert(s, string.format("%d skipped", result.skippedCount))
+        end
+        if result.nonSelectedCount > 0 then
+            table.insert(s, string.format("%d non-selected", result.nonSelectedCount))
+        end
+        return table.concat(s, ', ')
+    end
+
+    function M.LuaUnit:startSuite(selectedCount, nonSelectedCount)
+        self.result = {
+            selectedCount = selectedCount,
+            nonSelectedCount = nonSelectedCount,
+            successCount = 0,
+            runCount = 0,
+            currentTestNumber = 0,
+            currentClassName = "",
+            currentNode = nil,
+            suiteStarted = true,
+            startTime = os.clock(),
+            startDate = os.date(os.getenv('LUAUNIT_DATEFMT')),
+            startIsodate = os.date('%Y-%m-%dT%H:%M:%S'),
+            patternIncludeFilter = self.patternIncludeFilter,
+
+            -- list of test node status
+            allTests = {},
+            failedTests = {},
+            errorTests = {},
+            skippedTests = {},
+
+            failureCount = 0,
+            errorCount = 0,
+            notSuccessCount = 0,
+            skippedCount = 0,
+        }
+
+        self.outputType = self.outputType or TextOutput
+        self.output = self.outputType.new(self)
+        self.output:startSuite()
+    end
+
+    function M.LuaUnit:startClass( className )
+        self.result.currentClassName = className
+        self.output:startClass( className )
+    end
+
+    function M.LuaUnit:startTest( testName  )
+        self.result.currentTestNumber = self.result.currentTestNumber + 1
+        self.result.runCount = self.result.runCount + 1
+        self.result.currentNode = NodeStatus.new(
+            self.result.currentTestNumber,
+            testName,
+            self.result.currentClassName
+        )
+        self.result.currentNode.startTime = os.clock()
+        table.insert( self.result.allTests, self.result.currentNode )
+        self.output:startTest( testName )
+    end
+
+    function M.LuaUnit:updateStatus( err )
+        -- "err" is expected to be a table / result from protectedCall()
+        if err.status == NodeStatus.SUCCESS then
+            return
+        end
+
+        local node = self.result.currentNode
+
+        --[[ As a first approach, we will report only one error or one failure for one test.
+
+        However, we can have the case where the test is in failure, and the teardown is in error.
+        In such case, it's a good idea to report both a failure and an error in the test suite. This is
+        what Python unittest does for example. However, it mixes up counts so need to be handled carefully: for
+        example, there could be more (failures + errors) count that tests. What happens to the current node ?
+
+        We will do this more intelligent version later.
+        ]]
+
+        -- if the node is already in failure/error, just don't report the new error (see above)
+        if node.status ~= NodeStatus.SUCCESS then
+            return
+        end
+
+        if err.status == NodeStatus.FAIL then
+            node:fail( err.msg, err.trace )
+            table.insert( self.result.failedTests, node )
+        elseif err.status == NodeStatus.ERROR then
+            node:error( err.msg, err.trace )
+            table.insert( self.result.errorTests, node )
+        elseif err.status == NodeStatus.SKIP then
+            node:skip( err.msg )
+            table.insert( self.result.skippedTests, node )
+        else
+            error('No such status: ' .. prettystr(err.status))
+        end
+
+        self.output:updateStatus( node )
+    end
+
+    function M.LuaUnit:endTest()
+        local node = self.result.currentNode
+        -- print( 'endTest() '..prettystr(node))
+        -- print( 'endTest() '..prettystr(node:isNotSuccess()))
+        node.duration = os.clock() - node.startTime
+        node.startTime = nil
+        self.output:endTest( node )
+
+        if node:isSuccess() then
+            self.result.successCount = self.result.successCount + 1
+        elseif node:isError() then
+            if self.quitOnError or self.quitOnFailure then
+                -- Runtime error - abort test execution as requested by
+                -- "--error" option. This is done by setting a special
+                -- flag that gets handled in runSuiteByInstances().
+                print("\nERROR during LuaUnit test execution:\n" .. node.msg)
+                self.result.aborted = true
+            end
+        elseif node:isFailure() then
+            if self.quitOnFailure then
+                -- Failure - abort test execution as requested by
+                -- "--failure" option. This is done by setting a special
+                -- flag that gets handled in runSuiteByInstances().
+                print("\nFailure during LuaUnit test execution:\n" .. node.msg)
+                self.result.aborted = true
+            end
+        elseif node:isSkipped() then
+            self.result.runCount = self.result.runCount - 1
+        else
+            error('No such node status: ' .. prettystr(node.status))
+        end
+        self.result.currentNode = nil
+    end
+
+    function M.LuaUnit:endClass()
+        self.output:endClass()
+    end
+
+    function M.LuaUnit:endSuite()
+        if self.result.suiteStarted == false then
+            error('LuaUnit:endSuite() -- suite was already ended' )
+        end
+        self.result.duration = os.clock()-self.result.startTime
+        self.result.suiteStarted = false
+
+        -- Expose test counts for outputter's endSuite(). This could be managed
+        -- internally instead by using the length of the lists of failed tests
+        -- but unit tests rely on these fields being present.
+        self.result.failureCount = #self.result.failedTests
+        self.result.errorCount = #self.result.errorTests
+        self.result.notSuccessCount = self.result.failureCount + self.result.errorCount
+        self.result.skippedCount = #self.result.skippedTests
+
+        self.output:endSuite()
+    end
+
+    function M.LuaUnit:setOutputType(outputType, fname)
+        -- Configures LuaUnit runner output
+        -- outputType is one of: NIL, TAP, JUNIT, TEXT
+        -- when outputType is junit, the additional argument fname is used to set the name of junit output file
+        -- for other formats, fname is ignored
+        if outputType:upper() == "NIL" then
+            self.outputType = NilOutput
+            return
+        end
+        if outputType:upper() == "TAP" then
+            self.outputType = TapOutput
+            return
+        end
+        if outputType:upper() == "JUNIT" then
+            self.outputType = JUnitOutput
+            if fname then
+                self.fname = fname
+            end
+            return
+        end
+        if outputType:upper() == "TEXT" then
+            self.outputType = TextOutput
+            return
+        end
+        error( 'No such format: '..outputType,2)
+    end
+
+    --------------[[ Runner ]]-----------------
+
+    function M.LuaUnit:protectedCall(classInstance, methodInstance, prettyFuncName)
+        -- if classInstance is nil, this is just a function call
+        -- else, it's method of a class being called.
+
+        local function err_handler(e)
+            -- transform error into a table, adding the traceback information
+            return {
+                status = NodeStatus.ERROR,
+                msg = e,
+                trace = string.sub(debug.traceback("", 3), 2)
+            }
+        end
+
+        local ok, err
+        if classInstance then
+            -- stupid Lua < 5.2 does not allow xpcall with arguments so let's use a workaround
+            ok, err = xpcall( function () methodInstance(classInstance) end, err_handler )
+        else
+            ok, err = xpcall( function () methodInstance() end, err_handler )
+        end
+        if ok then
+            return {status = NodeStatus.SUCCESS}
+        end
+
+        local iter_msg
+        iter_msg = self.exeRepeat and 'iteration '..self.currentCount
+
+        err.msg, err.status = M.adjust_err_msg_with_iter( err.msg, iter_msg )
+
+        if err.status == NodeStatus.SUCCESS or err.status == NodeStatus.SKIP then
+            err.trace = nil
+            return err
+        end
+
+        -- reformat / improve the stack trace
+        if prettyFuncName then -- we do have the real method name
+            err.trace = err.trace:gsub("in (%a+) 'methodInstance'", "in %1 '"..prettyFuncName.."'")
+        end
+        if STRIP_LUAUNIT_FROM_STACKTRACE then
+            err.trace = stripLuaunitTrace(err.trace)
+        end
+
+        return err -- return the error "object" (table)
+    end
+
+
+    function M.LuaUnit:execOneFunction(className, methodName, classInstance, methodInstance)
+        -- When executing a test function, className and classInstance must be nil
+        -- When executing a class method, all parameters must be set
+
+        if type(methodInstance) ~= 'function' then
+            error( tostring(methodName)..' must be a function, not '..type(methodInstance))
+        end
+
+        local prettyFuncName
+        if className == nil then
+            className = '[TestFunctions]'
+            prettyFuncName = methodName
+        else
+            prettyFuncName = className..'.'..methodName
+        end
+
+        if self.lastClassName ~= className then
+            if self.lastClassName ~= nil then
+                self:endClass()
+            end
+            self:startClass( className )
+            self.lastClassName = className
+        end
+
+        self:startTest(prettyFuncName)
+
+        local node = self.result.currentNode
+        for iter_n = 1, self.exeRepeat or 1 do
+            if node:isNotSuccess() then
+                break
+            end
+            self.currentCount = iter_n
+
+            -- run setUp first (if any)
+            if classInstance then
+                local func = self.asFunction( classInstance.setUp ) or
+                             self.asFunction( classInstance.Setup ) or
+                             self.asFunction( classInstance.setup ) or
+                             self.asFunction( classInstance.SetUp )
+                if func then
+                    self:updateStatus(self:protectedCall(classInstance, func, className..'.setUp'))
+                end
+            end
+
+            -- run testMethod()
+            if node:isSuccess() then
+                self:updateStatus(self:protectedCall(classInstance, methodInstance, prettyFuncName))
+            end
+
+            -- lastly, run tearDown (if any)
+            if classInstance then
+                local func = self.asFunction( classInstance.tearDown ) or
+                             self.asFunction( classInstance.TearDown ) or
+                             self.asFunction( classInstance.teardown ) or
+                             self.asFunction( classInstance.Teardown )
+                if func then
+                    self:updateStatus(self:protectedCall(classInstance, func, className..'.tearDown'))
+                end
+            end
+        end
+
+        self:endTest()
+    end
+
+    function M.LuaUnit.expandOneClass( result, className, classInstance )
+        --[[
+        Input: a list of { name, instance }, a class name, a class instance
+        Ouptut: modify result to add all test method instance in the form:
+        { className.methodName, classInstance }
+        ]]
+        for methodName, methodInstance in sortedPairs(classInstance) do
+            if M.LuaUnit.asFunction(methodInstance) and M.LuaUnit.isMethodTestName( methodName ) then
+                table.insert( result, { className..'.'..methodName, classInstance } )
+            end
+        end
+    end
+
+    function M.LuaUnit.expandClasses( listOfNameAndInst )
+        --[[
+        -- expand all classes (provided as {className, classInstance}) to a list of {className.methodName, classInstance}
+        -- functions and methods remain untouched
+
+        Input: a list of { name, instance }
+
+        Output:
+        * { function name, function instance } : do nothing
+        * { class.method name, class instance }: do nothing
+        * { class name, class instance } : add all method names in the form of (className.methodName, classInstance)
+        ]]
+        local result = {}
+
+        for i,v in ipairs( listOfNameAndInst ) do
+            local name, instance = v[1], v[2]
+            if M.LuaUnit.asFunction(instance) then
+                table.insert( result, { name, instance } )
+            else
+                if type(instance) ~= 'table' then
+                    error( 'Instance must be a table or a function, not a '..type(instance)..' with value '..prettystr(instance))
+                end
+                local className, methodName = M.LuaUnit.splitClassMethod( name )
+                if className then
+                    local methodInstance = instance[methodName]
+                    if methodInstance == nil then
+                        error( "Could not find method in class "..tostring(className).." for method "..tostring(methodName) )
+                    end
+                    table.insert( result, { name, instance } )
+                else
+                    M.LuaUnit.expandOneClass( result, name, instance )
+                end
+            end
+        end
+
+        return result
+    end
+
+    function M.LuaUnit.applyPatternFilter( patternIncFilter, listOfNameAndInst )
+        local included, excluded = {}, {}
+        for i, v in ipairs( listOfNameAndInst ) do
+            -- local name, instance = v[1], v[2]
+            if  patternFilter( patternIncFilter, v[1] ) then
+                table.insert( included, v )
+            else
+                table.insert( excluded, v )
+            end
+        end
+        return included, excluded
+    end
+
+    function M.LuaUnit:runSuiteByInstances( listOfNameAndInst )
+        --[[ Run an explicit list of tests. Each item of the list must be one of:
+        * { function name, function instance }
+        * { class name, class instance }
+        * { class.method name, class instance }
+        ]]
+
+        local expandedList = self.expandClasses( listOfNameAndInst )
+        if self.shuffle then
+            randomizeTable( expandedList )
+        end
+        local filteredList, filteredOutList = self.applyPatternFilter(
+            self.patternIncludeFilter, expandedList )
+
+        self:startSuite( #filteredList, #filteredOutList )
+
+        for i,v in ipairs( filteredList ) do
+            local name, instance = v[1], v[2]
+            if M.LuaUnit.asFunction(instance) then
+                self:execOneFunction( nil, name, nil, instance )
+            else
+                -- expandClasses() should have already taken care of sanitizing the input
+                assert( type(instance) == 'table' )
+                local className, methodName = M.LuaUnit.splitClassMethod( name )
+                assert( className ~= nil )
+                local methodInstance = instance[methodName]
+                assert(methodInstance ~= nil)
+                self:execOneFunction( className, methodName, instance, methodInstance )
+            end
+            if self.result.aborted then
+                break -- "--error" or "--failure" option triggered
+            end
+        end
+
+        if self.lastClassName ~= nil then
+            self:endClass()
+        end
+
+        self:endSuite()
+
+        if self.result.aborted then
+            print("LuaUnit ABORTED (as requested by --error or --failure option)")
+            os.exit(-2)
+        end
+    end
+
+    function M.LuaUnit:runSuiteByNames( listOfName )
+        --[[ Run LuaUnit with a list of generic names, coming either from command-line or from global
+            namespace analysis. Convert the list into a list of (name, valid instances (table or function))
+            and calls runSuiteByInstances.
+        ]]
+
+        local instanceName, instance
+        local listOfNameAndInst = {}
+
+        for i,name in ipairs( listOfName ) do
+            local className, methodName = M.LuaUnit.splitClassMethod( name )
+            if className then
+                instanceName = className
+                instance = _G[instanceName]
+
+                if instance == nil then
+                    error( "No such name in global space: "..instanceName )
+                end
+
+                if type(instance) ~= 'table' then
+                    error( 'Instance of '..instanceName..' must be a table, not '..type(instance))
+                end
+
+                local methodInstance = instance[methodName]
+                if methodInstance == nil then
+                    error( "Could not find method in class "..tostring(className).." for method "..tostring(methodName) )
+                end
+
+            else
+                -- for functions and classes
+                instanceName = name
+                instance = _G[instanceName]
+            end
+
+            if instance == nil then
+                error( "No such name in global space: "..instanceName )
+            end
+
+            if (type(instance) ~= 'table' and type(instance) ~= 'function') then
+                error( 'Name must match a function or a table: '..instanceName )
+            end
+
+            table.insert( listOfNameAndInst, { name, instance } )
+        end
+
+        self:runSuiteByInstances( listOfNameAndInst )
+    end
+
+    function M.LuaUnit.run(...)
+        -- Run some specific test classes.
+        -- If no arguments are passed, run the class names specified on the
+        -- command line. If no class name is specified on the command line
+        -- run all classes whose name starts with 'Test'
+        --
+        -- If arguments are passed, they must be strings of the class names
+        -- that you want to run or generic command line arguments (-o, -p, -v, ...)
+
+        local runner = M.LuaUnit.new()
+        return runner:runSuite(...)
+    end
+
+    function M.LuaUnit:runSuite( ... )
+
+        local args = {...}
+        if type(args[1]) == 'table' and args[1].__class__ == 'LuaUnit' then
+            -- run was called with the syntax M.LuaUnit:runSuite()
+            -- we support both M.LuaUnit.run() and M.LuaUnit:run()
+            -- strip out the first argument
+            table.remove(args,1)
+        end
+
+        if #args == 0 then
+            args = cmdline_argv
+        end
+
+        local options = pcall_or_abort( M.LuaUnit.parseCmdLine, args )
+
+        -- We expect these option fields to be either `nil` or contain
+        -- valid values, so it's safe to always copy them directly.
+        self.verbosity     = options.verbosity
+        self.quitOnError   = options.quitOnError
+        self.quitOnFailure = options.quitOnFailure
+
+        self.exeRepeat            = options.exeRepeat
+        self.patternIncludeFilter = options.pattern
+        self.shuffle              = options.shuffle
+
+        if options.output then
+            if options.output:lower() == 'junit' and options.fname == nil then
+                print('With junit output, a filename must be supplied with -n or --name')
+                os.exit(-1)
+            end
+            pcall_or_abort(self.setOutputType, self, options.output, options.fname)
+        end
+
+        self:runSuiteByNames( options.testNames or M.LuaUnit.collectTests() )
+
+        return self.result.notSuccessCount
+    end
+-- class LuaUnit
+
+-- For compatbility with LuaUnit v2
+M.run = M.LuaUnit.run
+M.Run = M.LuaUnit.run
+
+function M:setVerbosity( verbosity )
+    M.LuaUnit.verbosity = verbosity
+end
+M.set_verbosity = M.setVerbosity
+M.SetVerbosity = M.setVerbosity
+
+
+return M

+ 777 - 0
mp.c

@@ -0,0 +1,777 @@
+#define LUA_LIB
+#include <lua.h>
+#include <lauxlib.h>
+
+#include <stdlib.h>
+#include <string.h>
+
+#if LUA_VERSION_NUM == 501
+# define LUA_OK 0
+# define luaL_setfuncs(L,l,u) luaL_register(L,NULL,l)
+# define luaL_newlib(L,l)     (lua_newtable(L), luaL_register(L,NULL,l))
+# define luaL_len(L,i)        lua_objlen(L,i)
+
+static const char *luaL_tolstring (lua_State *L, int idx, size_t *len) {
+    if (luaL_callmeta(L, idx, "__tostring")) {
+        if (!lua_isstring(L, -1))
+            luaL_error(L, "'__tostring' must return a string");
+    } else {
+        int tt;
+        const char *kind;
+        switch (lua_type(L, idx)) {
+        case LUA_TNUMBER:
+            lua_pushfstring(L, "%f", (LUAI_UACNUMBER)lua_tonumber(L, idx));
+            break;
+        case LUA_TSTRING:
+            lua_pushvalue(L, idx); break;
+        case LUA_TBOOLEAN:
+            lua_pushstring(L, (lua_toboolean(L, idx) ? "true" : "false"));
+            break;
+        case LUA_TNIL:
+            lua_pushliteral(L, "nil");
+            break;
+        default: 
+            tt = luaL_getmetafield(L, idx, "__name");
+            kind = (tt == LUA_TSTRING) ? lua_tostring(L, -1) :
+                luaL_typename(L, idx);
+            lua_pushfstring(L, "%s: %p", kind, lua_topointer(L, idx));
+            if (tt != LUA_TNIL) lua_remove(L, -2);
+        }
+    }
+    return lua_tolstring(L, -1, len);
+}
+#endif
+
+#if LUA_VERSION_NUM >= 503
+# define lua53_getfield   lua_getfield
+# define lua53_geti       lua_geti
+# define lua53_tointegerx lua_tointegerx
+#else
+static int lua53_getfield(lua_State *L, int idx, const char *f)
+{ lua_getfield(L, idx, f); return lua_type(L, -1); }
+static int lua53_geti(lua_State *L, int idx, int i)
+{ lua_rawgeti(L, idx, i); return lua_type(L, -1); }
+static void lua_rotate(lua_State *L, int start, int n)
+{ int i; for (i = 0; i < n; ++i) lua_insert(L, start+i); }
+
+static lua_Integer lua53_tointegerx(lua_State *L, int idx, int *isint) { 
+    lua_Integer i = lua_tointeger(L, idx);
+    *isint = i == 0 ? lua_type(L, idx)==LUA_TNUMBER : lua_tonumber(L, idx)==i;
+    return i;
+}
+#endif
+
+typedef signed   long long lmp_I64;
+typedef unsigned long long lmp_U64;
+
+
+/* type info */
+
+#define LMP_TYPE_BOX   "msgpack.Types"
+#define LMP_TYPE_FIELD "msgpack.type"
+
+static int Lnull_tostring(lua_State *L)
+{ lua_pushliteral(L, "null"); return 1; }
+
+static int Lnull_index(lua_State *L)
+{ return luaL_error(L, "attempt to index a msgpack.null value"); }
+
+static int lmp_fetchtable(lua_State *L, const char *name) {
+    if (!luaL_newmetatable(L, LMP_TYPE_BOX)) {
+        if (lua53_getfield(L, -1, name) != LUA_TNIL) {
+            lua_remove(L, -2);
+            return 0;
+        }
+        lua_pop(L, 1);
+    }
+    lua_createtable(L, 0, 2); /* 1 */
+    lua_pushstring(L, name); /* 2 */
+    lua_pushvalue(L, -2); /* 1->3 */
+    lua_pushvalue(L, -2); /* 2->4 */
+    lua_pushvalue(L, -3); /* 2->5 */
+    lua_setfield(L, -3, LMP_TYPE_FIELD); /* 5->2 */
+    lua_setfield(L, -2, "__name"); /* 4->2 */
+    lua_rawset(L, -4); /* 2,3 -> mt */
+    lua_remove(L, -2); /* (mt) */
+    return 1;
+}
+
+static void lmp_pushnull(lua_State *L) {
+    lua_newtable(L);
+    if (lmp_fetchtable(L, "null")) {
+        luaL_Reg libs[] = {
+            { "__index",    Lnull_index },
+            { "__newindex", Lnull_index },
+            { "__tostring", Lnull_tostring },
+            { NULL, NULL }
+        };
+        luaL_setfuncs(L, libs, 0);
+    }
+    lua_setmetatable(L, -2);
+}
+
+static const char *lmp_type(lua_State *L, int idx) {
+    const char *r = "";
+    if (lua_getmetatable(L, idx)) {
+        if (lua53_getfield(L, -1, LMP_TYPE_FIELD) == LUA_TSTRING)
+            r = lua_tostring(L, -1);
+        lua_pop(L, 2);
+    }
+    return r;
+}
+
+static int lmp_object(lua_State *L, int i, const char *name) {
+    int start = i, top = lua_gettop(L);
+    for (; i <= top; ++i) {
+        if (lua_type(L, i) != LUA_TTABLE) {
+            lua_createtable(L, 0, 1);
+            lua_pushvalue(L, i);
+            lua_setfield(L, -2, "value");
+            lua_replace(L, i);
+            lmp_fetchtable(L, name);
+            lua_setmetatable(L, i);
+        } else if (lua_getmetatable(L, i)) {
+            lua_pushstring(L, name);
+            lua_setfield(L, -2, LMP_TYPE_FIELD);
+            lua_pop(L, 1);
+        } else {
+            lmp_fetchtable(L, name);
+            lua_setmetatable(L, i);
+        }
+    }
+    return top - start + 1;
+}
+
+static int Lmeta(lua_State *L) {
+    const char *types[] = { "null", "False", "True", "int", "uint",
+        "float", "double", "string", "binary", "value", "handler",
+        "array", "map", "extension", NULL };
+    return lmp_object(L, 2, types[luaL_checkoption(L, 1, NULL, types)]);
+}
+
+static int Larray(lua_State *L)   { return lmp_object(L, 1, "array"); }
+static int Lmap(lua_State *L)     { return lmp_object(L, 1, "map"); }
+
+
+/* encode */
+
+#define LMP_SSO_SIZE  (sizeof(lmp_HeapBuffer))
+#define LMP_MAX_SIZE  (~(unsigned)0>>1)
+#define LMP_MAX_STACK 100
+
+typedef struct lmp_HeapBuffer {
+    unsigned cap;
+    char    *data;
+} lmp_HeapBuffer;
+
+typedef struct lmp_Buffer {
+    lua_State *L;
+    unsigned len : sizeof(unsigned) * CHAR_BIT - 1;
+    unsigned sso : 1;
+    union {
+        lmp_HeapBuffer heap;
+        char buff[LMP_SSO_SIZE];
+    } u;
+} lmp_Buffer;
+
+#define lmp_data(B)         ((B)->sso ? (B)->u.heap.data : (B)->u.buff)
+#define lmp_addchar(B,ch)   (*lmp_prepare(B,1)=(ch),++(B)->len)
+#define lmp_addchars(B,s,l) (memcpy(lmp_prepare(B,l),s,l),(B)->len+=(unsigned)(l))
+
+#define lmp_nomem(B)        luaL_error((B)->L, "out of memory")
+#define lmp_toobig(B,i,n,l) lmp_error(B,i,0,"%s too large (count=%d)",(n),(int)(l))
+
+static int lmp_encode (lmp_Buffer *B, int idx, int type);
+static int lmp_pack   (lmp_Buffer *B, int idx, const char *type, int fetch);
+
+static void lmp_resetbuffer(lmp_Buffer *B)
+{ if (B->sso) { free(lmp_data(B)); memset(B, 0, sizeof(lmp_Buffer)); } }
+
+static char *lmp_prepare(lmp_Buffer *B, size_t len) {
+    unsigned expected = B->len + (unsigned)len;
+    unsigned cap = B->sso ? B->u.heap.cap : LMP_SSO_SIZE;
+    if (expected > cap) {
+        unsigned newsize = LMP_SSO_SIZE;
+        void *newptr, *oldptr = B->sso ? lmp_data(B) : NULL;
+        while (newsize < expected && newsize < LMP_MAX_SIZE)
+            newsize += newsize >> 1;
+        if (newsize < expected) lmp_nomem(B);
+        if (!(newptr = realloc(oldptr, newsize))) lmp_nomem(B);
+        if (!B->sso) memcpy(newptr, lmp_data(B), B->len);
+        B->sso = 1;
+        B->u.heap.data = (char*)newptr;
+        B->u.heap.cap  = newsize;
+    }
+    return lmp_data(B) + B->len;
+}
+
+static void lmp_writeuint(lmp_Buffer *B, lmp_U64 v, int len) {
+    char buff[8];
+    switch (len) {
+    case 8: buff[0] = (v >> 56) & 0xFF;
+            buff[1] = (v >> 48) & 0xFF;
+            buff[2] = (v >> 40) & 0xFF;
+            buff[3] = (v >> 32) & 0xFF; /* FALLTHROUGH */
+    case 4: buff[4] = (v >> 24) & 0xFF;
+            buff[5] = (v >> 16) & 0xFF; /* FALLTHROUGH */
+    case 2: buff[6] = (v >>  8) & 0xFF;
+    case 1: buff[7] = (v      ) & 0xFF; /* FALLTHROUGH */
+    }
+    lmp_addchars(B, buff+8-len, len);
+}
+
+static int lmp_calcbytes(lmp_U64 v)
+{ return v < 0x100 ? 0 : v < 0x10000 ? 1 : v < 0x100000000 ? 2 : 3; }
+
+static void lmp_prefix(lmp_Buffer *B, lmp_U64 v, int base, int o)
+{ lmp_addchar(B, base + o); lmp_writeuint(B, v, 1<<o); }
+
+static void lmp_writeint(lmp_Buffer *B, lmp_U64 v, int uint) {
+    if (v < 128) lmp_addchar(B, v & 0xFF);
+    else if (uint) lmp_prefix(B, v, 0xCC, lmp_calcbytes(v));
+    else if (~v < 32) lmp_addchar(B, v & 0xFF);
+    else lmp_prefix(B, v, 0xD0, lmp_calcbytes(v >> 63 ? ~v + 1 : v<<1));
+}
+
+static void lmp_writefloat(lmp_Buffer *B, lua_Number n, int len) {
+    union {
+        float    f32;
+        double   f64;
+        lmp_U64  u64;
+        unsigned u32;
+    } u;
+    if (len == 4) {
+        u.f32 = (float)n; 
+        lmp_addchar(B, 0xCA);
+        lmp_writeuint(B, u.u32, 4);
+    } else {
+        u.f64 = (double)n;
+        lmp_addchar(B, 0xCB);
+        lmp_writeuint(B, u.u64, 8);
+    }
+}
+
+static void lmp_writestring(lmp_Buffer *B, int base, const char *s, size_t len) {
+    if (len < 32 && base == 0xD9) { /* str */
+        lmp_addchar(B, (char)(0xA0 + len));
+        lmp_addchars(B, s, len);
+    } else {
+        lmp_prefix(B, len, base, lmp_calcbytes(len));
+        lmp_addchars(B, s, len);
+    }
+}
+
+static void lmp_writeext(lmp_Buffer *B, int type, const char *s, size_t len) {
+    char *buff = lmp_prepare(B, 2);
+    int o;
+    buff[1] = type, B->len += 2;
+    switch (len) {
+    case 1:  buff[0] = 0xD4; break;
+    case 2:  buff[0] = 0xD5; break;
+    case 4:  buff[0] = 0xD6; break;
+    case 8:  buff[0] = 0xD7; break;
+    case 16: buff[0] = 0xD8; break;
+    default: buff[0] = 0xC7 + (o = lmp_calcbytes(len));
+             lmp_writeuint(B, len, 1<<o);
+    }
+    lmp_addchars(B, s, len);
+}
+
+static int lmp_addfloat(lmp_Buffer *B, int idx, int len)
+{ lmp_writefloat(B, lua_tonumber(B->L, idx), len); return 1; }
+
+static int lmp_error(lmp_Buffer *B, int idx, int prev, const char *fmt, ...) {
+    va_list l;
+    va_start(l, fmt);
+    lua_pushvfstring(B->L, fmt, l);
+    va_end(l);
+    if (prev) lua_pushfstring(B->L, "%s;\n\t%s",
+                 lua_tostring(B->L, -1), lua_tostring(B->L, prev));
+    lua_replace(B->L, idx), lua_settop(B->L, idx);
+    return 0;
+}
+
+static int lmp_addinteger(lmp_Buffer *B, int idx, int uint) {
+    int isint;
+    lua_Integer i = lua53_tointegerx(B->L, idx, &isint);
+    if (!isint)
+        return lmp_error(B, idx,0, "integer expected, got %s",
+                luaL_typename(B->L, idx));
+    if (uint) lmp_writeint(B, (lmp_U64)i, 1);
+    else      lmp_writeint(B, (lmp_U64)i, 0);
+    return 1;
+}
+
+static int lmp_addstring(lmp_Buffer *B, int idx, int type) {
+    size_t len;
+    const char *s = lua_tolstring(B->L, idx, &len);
+    if (len > LMP_MAX_SIZE) return lmp_toobig(B,idx,"string",(int)len);
+    lmp_writestring(B, type, s, len);
+    return 1;
+}
+
+static int lmp_addarray(lmp_Buffer *B, int idx) {
+    int i, len = (int)luaL_len(B->L, idx);
+    int top = lua_gettop(B->L);
+    if (top > LMP_MAX_STACK || !lua_checkstack(B->L, 5))
+        return lmp_error(B, idx,0, "array level too deep");
+    if (len < 16)
+        lmp_addchar(B, 0x90 + len);
+    else if (len < 0x10000) {
+        lmp_addchar(B, 0xDC);
+        lmp_writeuint(B, len, 2);
+    } else {
+        lmp_addchar(B, 0xDD);
+        lmp_writeuint(B, len, 4);
+    }
+    for (i = 1; i <= len; ++i) {
+        if (!lmp_encode(B, top+1, lua53_geti(B->L, idx, i)))
+            return lmp_error(B, idx,top+1, "invalid element '%d' in array", i);
+        lua_pop(B->L, 1);
+    }
+    return 1;
+}
+
+static void lmp_fixmapszie(lmp_Buffer *B, unsigned off, unsigned count) {
+    unsigned len = B->len - off;
+    char *buff;
+    lmp_prepare(B, 5);
+    buff = lmp_data(B) + off;
+    if (count < 16)
+        buff[-1] = (char)(0x80 + count);
+    else if (count < 0x10000) {
+        buff[-1] = 0xDE;
+        memmove(buff+2, buff, len);
+        B->len = off, lmp_writeuint(B, count, 2); B->len += len;
+    } else {
+        buff[-1] = 0xDF;
+        memmove(buff+4, buff, len);
+        B->len = off, lmp_writeuint(B, count, 4); B->len += len;
+    }
+}
+
+static int lmp_addmap(lmp_Buffer *B, int idx) {
+    unsigned off = B->len + 1, count = 0;
+    int top = lua_gettop(B->L);
+    if (top > LMP_MAX_STACK || !lua_checkstack(B->L, 10))
+        return lmp_error(B, idx,0, "map level too deep");
+    lmp_addchar(B, 0x80);
+    lua_pushnil(B->L);
+    for (; lua_next(B->L, idx); ++count) {
+        if (!lmp_encode(B, top+1, 0))
+            return lmp_error(B, idx,top+1, "invalid key in map");
+        if (!lmp_encode(B, top+2, 0))
+            return lmp_error(B, idx,top+2, "invalid value for key '%s' in map",
+                    luaL_tolstring(B->L, top+1, NULL));
+        lua_pop(B->L, 1);
+    }
+    lmp_fixmapszie(B, off, count);
+    return 1;
+}
+
+static int lmp_addext(lmp_Buffer *B, int idx, int fetch) {
+    int type, tt, vt;
+    size_t len;
+    const char *s;
+    tt = fetch ? lua53_getfield(B->L, idx, "type") : lua_type(B->L, idx);
+    if (tt != LUA_TNUMBER)
+        return lmp_error(B, idx,0,
+                "integer expected for extension type, got %s",
+                lua_typename(B->L, tt));
+    type = (int)lua_tointeger(B->L, fetch ? -1 : idx);
+    if (type < -128 || type > 127)
+        return lmp_error(B, idx,0, "invalid extension type: %d", type);
+    vt = fetch ? lua53_getfield(B->L, idx, "value") : lua_type(B->L, idx+1);
+    if (vt != LUA_TSTRING)
+        return lmp_error(B, idx,0,
+                "string expected for extension value, got %s",
+                lua_typename(B->L, vt));
+    s = lua_tolstring(B->L, fetch ? -1 : idx+1, &len);
+    if (len > LMP_MAX_SIZE) return lmp_toobig(B,idx,"extension",(int)len);
+    lmp_writeext(B, type, s, len);
+    if (fetch) lua_pop(B->L, 2);
+    return 1;
+}
+
+static int lmp_addhandler(lmp_Buffer *B, int idx, int fetch) {
+    const char *type;
+    int r, top = lua_gettop(B->L)+1;
+    if (!fetch)
+        lua_pushvalue(B->L, idx);
+    else if (lua53_getfield(B->L, idx, "pack") == LUA_TNIL)
+        return lmp_error(B, idx,0, "'pack' field expected in handler object");
+    if (fetch) lua_pushvalue(B->L, idx);
+    lua_call(B->L, fetch, 3);
+    if ((type = lua_tostring(B->L, top)) == NULL)
+        return lmp_error(B, idx,0, "type expected from handler, got %s",
+                luaL_typename(B->L, top));
+    r = *type == 'e' ? lmp_addext(B, top+1, 0) : lmp_pack(B, top+1, type, 0);
+    if (r ==  0) return lmp_error(B, idx,top+1, "error from handler");
+    if (r == -1) return lmp_error(B, idx,0, "invalid msgpack.type '%s'", type);
+    return (lua_pop(B->L, 3), 1);
+}
+
+static int lmp_check(lmp_Buffer *B, int idx, int fetch, int type) {
+    int rt;
+    if (!fetch)
+        rt = lua_type(B->L, idx);
+    else {
+        if ((rt = lua53_getfield(B->L, idx, "value")) == LUA_TNIL)
+            return lmp_error(B, idx,0, "'value' field expected in wrapper object");
+        lua_replace(B->L, idx);
+    }
+    return !type || rt == type ? 1 :
+        lmp_error(B, idx,0, "%s expected, got %s",
+                lua_typename(B->L, type), luaL_typename(B->L, idx));
+}
+
+static int lmp_pack(lmp_Buffer *B, int idx, const char *type, int fetch) {
+#define check(t,c) (lmp_check(B,idx,fetch,t) && (c))
+    switch (*type) {
+    case 'n': lmp_addchar(B,0xC0); return 1;
+    case 'F': lmp_addchar(B,0xC2); return 1;
+    case 'T': lmp_addchar(B,0xC3); return 1;
+    case 'i': return check(LUA_TNUMBER, lmp_addinteger(B,idx,0));
+    case 'u': return check(LUA_TNUMBER, lmp_addinteger(B,idx,1));
+    case 'f': return check(LUA_TNUMBER, lmp_addfloat(B,idx,4));
+    case 'd': return check(LUA_TNUMBER, lmp_addfloat(B,idx,8));
+    case 's': return check(LUA_TSTRING, lmp_addstring(B,idx,0xD9));
+    case 'b': return check(LUA_TSTRING, lmp_addstring(B,idx,0xC4));
+    case 'v': return check(0,           lmp_encode(B, idx, 0));
+    case 'h': return lmp_addhandler(B, idx, 1);
+    case 'a': return lmp_addarray(B, idx);
+    case 'm': return lmp_addmap(B, idx);
+    case 'e': return lmp_addext(B, idx, 1);
+    }
+#undef check
+    return -1;
+}
+
+static int lmp_addnumber(lmp_Buffer *B, int idx) {
+    int isint;
+    lua_Integer i = lua53_tointegerx(B->L, idx, &isint);
+    if (isint)
+        lmp_writeint(B, (lmp_U64)i, 0);
+    else {
+        lua_Number n = lua_tonumber(B->L, idx);
+        int float_ok = (lua_Number)(float)n == n;
+        if (float_ok) lmp_writefloat(B, n, 4);
+        else          lmp_writefloat(B, n, 8);
+    }
+    return 1;
+}
+
+static int lmp_addtable(lmp_Buffer *B, int idx) {
+    const char *type = lmp_type(B->L, idx);
+    int r = lmp_pack(B, idx, type, 1);
+    if (r == 0 || r == 1) return r;
+    return luaL_len(B->L, idx) > 0 ? lmp_addarray(B, idx) : lmp_addmap(B, idx);
+}
+
+static int lmp_encode(lmp_Buffer *B, int idx, int type) {
+    switch (type ? type : (type = lua_type(B->L, idx))) {
+    case LUA_TNONE:
+    case LUA_TNIL:      lmp_addchar(B, 0xC0); return 1;
+    case LUA_TBOOLEAN:  lmp_addchar(B, 0xC2+lua_toboolean(B->L,idx)); return 1;
+    case LUA_TNUMBER:   return lmp_addnumber(B, idx);
+    case LUA_TSTRING:   return lmp_addstring(B, idx, 0xD9);
+    case LUA_TFUNCTION: return lmp_addhandler(B, idx, 0);
+    case LUA_TTABLE:    return lmp_addtable(B, idx);
+    }
+    return lmp_error(B, idx,0, "invalid type '%s'", lua_typename(B->L, type));
+}
+
+static int Lencode_aux(lua_State *L) {
+    lmp_Buffer *B = (lmp_Buffer*)lua_touserdata(L, 1);
+    int i, top = lua_gettop(L);
+    B->L = L;
+    for (i = 2; i <= top; ++i)
+        if (!lmp_encode(B, i, 0))
+            return luaL_error(L, "bad argument to #%d: %s",
+                    i-1, lua_tostring(L, i));
+    lua_pushlstring(L, lmp_data(B), B->len);
+    return 1;
+}
+
+static int Lencode(lua_State *L) {
+    lmp_Buffer B;
+    int r;
+    memset(&B, 0, sizeof(B));
+    lua_pushcfunction(L, Lencode_aux);
+    lua_insert(L, 1);
+    lua_pushlightuserdata(L, &B);
+    lua_insert(L, 2);
+    r = lua_pcall(L, lua_gettop(L)-1, 1, 0) == LUA_OK;
+    lmp_resetbuffer(&B);
+    return r ? 1 : luaL_error(L, "%s", lua_tostring(L, -1));
+}
+
+
+/* decode */
+
+typedef struct lmp_Slice {
+    const char *p, *e, *s;
+    lua_State  *L;
+    int         ext;
+} lmp_Slice;
+
+#define lmp_readsize(S,l)  ((int)lmp_readuint(S,(int)l,"size"))
+#define lmp_readluint(S,l) ((lua_Integer)lmp_readuint(S,l,"uint"))
+#define lmp_readlint(S,l)  ((lua_Integer)lmp_u2s(lmp_readuint(S,l,"int"),l))
+
+static void lmp_decode(lmp_Slice *S);
+
+static size_t lmp_off(lmp_Slice *S) { return S->p - S->s + 1; }
+static size_t lmp_len(lmp_Slice *S) { return S->e - S->p; }
+
+static void lmp_ensure(lmp_Slice *S, size_t len, const char *tname) {
+    size_t rem = lmp_len(S);
+    if (rem >= len) return;
+    luaL_error(S->L, "invalid %s at offset %d: "
+            "%d bytes expected, got %d bytes",
+            tname, (int)lmp_off(S), (int)len, (int)rem);
+}
+
+static void lmp_pushstring(lmp_Slice *S, size_t len)
+{ lmp_ensure(S, len, "string"); lua_pushlstring(S->L,S->p,len); S->p += len; }
+
+static void lmp_checkend(lmp_Slice *S, const char *tname) {
+    if (S->p < S->e) return;
+    luaL_error(S->L, "unexpected end of message at offset %d: %s expected",
+            (int)lmp_off(S), tname);
+}
+
+static lmp_U64 lmp_readuint(lmp_Slice *S, int len, const char *tname) {
+    lmp_U64 r = 0;
+    lmp_ensure(S, len, tname);
+#define ch(i) ((lmp_U64)(unsigned char)S->p[i])
+    switch (len) {
+    case 8: r |= ch(0) << 56 | ch(1) << 48
+              |  ch(2) << 40 | ch(3) << 32; S->p += 4; /* FALLTHROUGH */
+    case 4: r |= ch(0) << 24 | ch(1) << 16; S->p += 2; /* FALLTHROUGH */
+    case 2: r |= ch(0) << 8;                S->p += 1; /* FALLTHROUGH */
+    case 1: r |= ch(0);                     S->p += 1;
+    }
+#undef ch
+    return r;
+}
+
+static lmp_I64 lmp_u2s(lmp_U64 v, int len) {
+    const lmp_I64 m = 1LL << (len*8 - 1);
+    return v&m ? (lmp_I64)(v^m) - m : (lmp_I64)v;
+}
+
+static lua_Number lmp_readfloat(lmp_Slice *S, int len) {
+    union {
+        float    f32;
+        double   f64;
+        lmp_U64  u64;
+        unsigned u32;
+    } u;
+    if (len == 4) {
+        u.u32 = (unsigned)lmp_readuint(S, len, "float");
+        return (lua_Number)u.f32;
+    } else {
+        u.u64 = lmp_readuint(S, len, "float");
+        return (lua_Number)u.f64;
+    }
+}
+
+static void lmp_pushext(lmp_Slice *S, int fix, size_t len) {
+    int type;
+    lmp_ensure(S, len+1, "extension");
+    type = (int)(signed char)*S->p++;
+    if (!fix) len = lmp_readsize(S, len), lmp_ensure(S, len, "extension");
+    lua_pushinteger(S->L, type);
+    lua_pushlstring(S->L, S->p, len);
+    S->p += len;
+    if (S->ext) {
+        lua_pushvalue(S->L, S->ext);
+        lua_pushvalue(S->L, -3);
+        lua_pushvalue(S->L, -3);
+        lua_call(S->L, 2, 1);
+        if (!lua_isnil(S->L, -1)) {
+            lua_insert(S->L, -3), lua_pop(S->L, 2);
+            return;
+        }
+        lua_pop(S->L, 1);
+    }
+    lua_createtable(S->L, 0, 2);
+    lua_rotate(S->L, -3, 1);
+    lua_setfield(S->L, -3, "value");
+    lua_setfield(S->L, -2, "type");
+}
+
+static void lmp_pusharray(lmp_Slice *S, int count) {
+    int i;
+    lua_createtable(S->L, count, 0);
+    for (i = 0; i < count; ++i) {
+        lmp_checkend(S, "array element"); lmp_decode(S);
+        lua_rawseti(S->L, -2, (lua_Integer)i+1);
+    }
+}
+
+static void lmp_pushmap(lmp_Slice *S, int count) {
+    int i;
+    lua_createtable(S->L, 0, count);
+    for (i = 0; i < count; ++i) {
+        lmp_checkend(S, "map key");   lmp_decode(S);
+        lmp_checkend(S, "map value"); lmp_decode(S);
+        lua_rawset(S->L, -3);
+    }
+}
+
+static void lmp_decode(lmp_Slice *S) {
+    int ch = *S->p++ & 0xFF;
+    switch (ch) {
+    case 0x80: case 0x81: case 0x82: case 0x83:
+    case 0x84: case 0x85: case 0x86: case 0x87:
+    case 0x88: case 0x89: case 0x8A: case 0x8B:
+    case 0x8C: case 0x8D: case 0x8E: case 0x8F:
+        lmp_pushmap(S, ch - 0x80); break;
+    case 0x90: case 0x91: case 0x92: case 0x93:
+    case 0x94: case 0x95: case 0x96: case 0x97:
+    case 0x98: case 0x99: case 0x9A: case 0x9B:
+    case 0x9C: case 0x9D: case 0x9E: case 0x9F:
+        lmp_pusharray(S, ch - 0x90); break;
+    case 0xA0: case 0xA1: case 0xA2: case 0xA3:
+    case 0xA4: case 0xA5: case 0xA6: case 0xA7:
+    case 0xA8: case 0xA9: case 0xAA: case 0xAB:
+    case 0xAC: case 0xAD: case 0xAE: case 0xAF:
+    case 0xB0: case 0xB1: case 0xB2: case 0xB3:
+    case 0xB4: case 0xB5: case 0xB6: case 0xB7:
+    case 0xB8: case 0xB9: case 0xBA: case 0xBB:
+    case 0xBC: case 0xBD: case 0xBE: case 0xBF:
+        lmp_pushstring(S, (size_t)ch - 0xA0); break;
+    case 0xC0: lua_pushnil(S->L);         break;
+    case 0xC2: lua_pushboolean(S->L, 0);  break;
+    case 0xC3: lua_pushboolean(S->L, 1);  break;
+    case 0xC4: case 0xD9: lmp_pushstring(S, lmp_readsize(S,1)); break;
+    case 0xC5: case 0xDA: lmp_pushstring(S, lmp_readsize(S,2)); break;
+    case 0xC6: case 0xDB: lmp_pushstring(S, lmp_readsize(S,4)); break;
+    case 0xC7: lmp_pushext(S, 0, 1); break;
+    case 0xC8: lmp_pushext(S, 0, 2); break;
+    case 0xC9: lmp_pushext(S, 0, 4); break;
+    case 0xCA: lua_pushnumber(S->L, lmp_readfloat(S, 4)); break;
+    case 0xCB: lua_pushnumber(S->L, lmp_readfloat(S, 8)); break;
+    case 0xCC: lua_pushinteger(S->L, lmp_readluint(S, 1)); break;
+    case 0xCD: lua_pushinteger(S->L, lmp_readluint(S, 2)); break;
+    case 0xCE: lua_pushinteger(S->L, lmp_readluint(S, 4)); break;
+    case 0xCF: lua_pushinteger(S->L, lmp_readluint(S, 8)); break;
+    case 0xD0: lua_pushinteger(S->L, lmp_readlint(S, 1)); break;
+    case 0xD1: lua_pushinteger(S->L, lmp_readlint(S, 2)); break;
+    case 0xD2: lua_pushinteger(S->L, lmp_readlint(S, 4)); break;
+    case 0xD3: lua_pushinteger(S->L, lmp_readlint(S, 8)); break;
+    case 0xD4: lmp_pushext(S, 1,  1); break;
+    case 0xD5: lmp_pushext(S, 1,  2); break;
+    case 0xD6: lmp_pushext(S, 1,  4); break;
+    case 0xD7: lmp_pushext(S, 1,  8); break;
+    case 0xD8: lmp_pushext(S, 1, 16); break;
+    case 0xDC: lmp_pusharray(S, lmp_readsize(S, 2)); break;
+    case 0xDD: lmp_pusharray(S, lmp_readsize(S, 4)); break;
+    case 0xDE: lmp_pushmap(S, lmp_readsize(S, 2)); break;
+    case 0xDF: lmp_pushmap(S, lmp_readsize(S, 4)); break;
+    default:
+        if      (ch < 0x80) lua_pushinteger(S->L, ch);
+        else if (ch > 0xDF) lua_pushinteger(S->L, (lua_Integer)ch - 256);
+        else luaL_error(S->L, "invalid char '%d' at offset %d", ch, lmp_off(S));
+    }
+}
+
+static size_t lmp_posrelat(lua_Integer pos, size_t len) {
+  if (pos > 0) return (size_t)pos;
+  else if (pos == 0) return 1;
+  else if (pos < -(lua_Integer)len) return 1;
+  else return len + (size_t)pos + 1;
+}
+
+static int Ldecode(lua_State *L) {
+    lmp_Slice S;
+    size_t len;
+    const char *s = luaL_checklstring(L, 1, &len);
+    size_t i = lmp_posrelat(luaL_optinteger(L, 2, 1), len);
+    size_t j = lmp_posrelat(luaL_optinteger(L, 3, -1), len);
+    if (i > j || i > len) return 0;
+    lua_settop(L, 4);
+    S.s = s;
+    S.e = s + j;
+    S.p = s + (i ? i : 1) - 1;
+    S.L = L;
+    S.ext = lua_isnoneornil(L, 4) ? 0 : 4;
+    lmp_decode(&S);
+    if (S.p >= S.e) return 1;
+    lua_pushinteger(L, lmp_off(&S));
+    return 2;
+}
+
+
+/* entry point */
+
+static int Ltohex(lua_State *L) {
+    size_t i, len;
+    const char *s = luaL_checklstring(L, 1, &len);
+    const char *hexa = "0123456789ABCDEF";
+    char hex[4] = "XX ";
+    luaL_Buffer lb;
+    luaL_buffinit(L, &lb);
+    for (i = 0; i < len; ++i) {
+        unsigned int ch = s[i] & 0xFF;
+        hex[0] = hexa[(ch>>4)&0xF];
+        hex[1] = hexa[(ch   )&0xF];
+        if (i == len-1) hex[2] = '\0';
+        luaL_addstring(&lb, hex);
+    }
+    luaL_pushresult(&lb);
+    return 1;
+}
+
+static int Lfromhex(lua_State *L) {
+    size_t i, len;
+    const char *s = luaL_checklstring(L, 1, &len);
+    luaL_Buffer lb;
+    int curr = 0, idx = 0, num;
+    luaL_buffinit(L, &lb);
+    for (i = 0; i < len; ++i) {
+        switch (num = s[i]) {
+        case '0': case '1': case '2': case '3':
+        case '4': case '5': case '6': case '7':
+        case '8': case '9': num -= '0'; break;
+        case 'A': case 'a': num  =  10; break;
+        case 'B': case 'b': num  =  11; break;
+        case 'C': case 'c': num  =  12; break;
+        case 'D': case 'd': num  =  13; break;
+        case 'E': case 'e': num  =  14; break;
+        case 'F': case 'f': num  =  15; break;
+        default: continue;
+        }
+        curr = curr<<4 | num;
+        if (++idx % 2 == 0) luaL_addchar(&lb, curr), curr = 0;
+    }
+    luaL_pushresult(&lb);
+    return 1;
+}
+
+LUALIB_API int luaopen_mp(lua_State *L) {
+    luaL_Reg libs[] = {
+        { "null", NULL },
+#define ENTRY(name) { #name, L##name }
+        ENTRY(array),
+        ENTRY(map),
+        ENTRY(meta),
+        ENTRY(encode),
+        ENTRY(decode),
+        ENTRY(fromhex),
+        ENTRY(tohex),
+#undef  ENTRY
+        { NULL, NULL }
+    };
+    luaL_newlib(L, libs);
+    lmp_pushnull(L), lua_setfield(L, -2, "null");
+    return 1;
+}
+
+/* cc: flags+='-march=native -O3 -Wextra -pedantic --coverage'
+ * unixcc: flags+='-shared -fPIC ' output='mp.so'
+ * maccc: flags+='-undefined dynamic_lookup'
+ * win32cc: flags+='-mdll -DLUA_BUILD_AS_DLL ' libs+='-llua53' output='mp.dll' */
+

+ 26 - 0
rockspecs/lua-mp-scm-1.rockspec

@@ -0,0 +1,26 @@
+package = "lua-mp"
+version = "scm-1"
+
+source = {
+  url = "git://github.com/starwing/lua-mp.git",
+}
+
+description = {
+  summary = "yet another msgpack implement for Lua",
+  detailed = [[
+This project implements a tiny msgpack C module for Lua
+  ]],
+  homepage = "https://github.com/starwing/lua-mp",
+  license = "MIT",
+}
+
+dependencies = {
+  "lua >= 5.1"
+}
+
+build = {
+  type = "builtin",
+  modules = {
+    mp = "mp.c";
+  }
+}

+ 124 - 0
serpent.lua

@@ -0,0 +1,124 @@
+local n, v = "serpent", 0.28 -- (C) 2012-15 Paul Kulchenko; MIT License
+local c, d = "Paul Kulchenko", "Lua serializer and pretty printer"
+local snum = {[tostring(1/0)]='1/0 --[[math.huge]]',[tostring(-1/0)]='-1/0 --[[-math.huge]]',[tostring(0/0)]='0/0'}
+local badtype = {thread = true, userdata = true, cdata = true}
+local keyword, globals, G = {}, {}, (_G or _ENV)
+for _,k in ipairs({'and', 'break', 'do', 'else', 'elseif', 'end', 'false',
+  'for', 'function', 'goto', 'if', 'in', 'local', 'nil', 'not', 'or', 'repeat',
+  'return', 'then', 'true', 'until', 'while'}) do keyword[k] = true end
+for k,v in pairs(G) do globals[v] = k end -- build func to name mapping
+for _,g in ipairs({'coroutine', 'debug', 'io', 'math', 'string', 'table', 'os'}) do
+  for k,v in pairs(G[g] or {}) do globals[v] = g..'.'..k end end
+
+local function s(t, opts)
+  local name, indent, fatal, maxnum = opts.name, opts.indent, opts.fatal, opts.maxnum
+  local sparse, custom, huge = opts.sparse, opts.custom, not opts.nohuge
+  local space, maxl = (opts.compact and '' or ' '), (opts.maxlevel or math.huge)
+  local iname, comm = '_'..(name or ''), opts.comment and (tonumber(opts.comment) or math.huge)
+  local seen, sref, syms, symn = {}, {'local '..iname..'={}'}, {}, 0
+  local function gensym(val) return '_'..(tostring(tostring(val)):gsub("[^%w]",""):gsub("(%d%w+)",
+    -- tostring(val) is needed because __tostring may return a non-string value
+    function(s) if not syms[s] then symn = symn+1; syms[s] = symn end return tostring(syms[s]) end)) end
+  local function safestr(s) return type(s) == "number" and tostring(huge and snum[tostring(s)] or s)
+    or type(s) ~= "string" and tostring(s) -- escape NEWLINE/010 and EOF/026
+    or ("%q"):format(s):gsub("\010","n"):gsub("\026","\\026") end
+  local function comment(s,l) return comm and (l or 0) < comm and ' --[['..tostring(s)..']]' or '' end
+  local function globerr(s,l) return globals[s] and globals[s]..comment(s,l) or not fatal
+    and safestr(select(2, pcall(tostring, s))) or error("Can't serialize "..tostring(s)) end
+  local function safename(path, name) -- generates foo.bar, foo[3], or foo['b a r']
+    local n = name == nil and '' or name
+    local plain = type(n) == "string" and n:match("^[%l%u_][%w_]*$") and not keyword[n]
+    local safe = plain and n or '['..safestr(n)..']'
+    return (path or '')..(plain and path and '.' or '')..safe, safe end
+  local alphanumsort = type(opts.sortkeys) == 'function' and opts.sortkeys or function(k, o, n) -- k=keys, o=originaltable, n=padding
+    local maxn, to = tonumber(n) or 12, {number = 'a', string = 'b'}
+    local function padnum(d) return ("%0"..tostring(maxn).."d"):format(tonumber(d)) end
+    table.sort(k, function(a,b)
+      -- sort numeric keys first: k[key] is not nil for numerical keys
+      return (k[a] ~= nil and 0 or to[type(a)] or 'z')..(tostring(a):gsub("%d+",padnum))
+           < (k[b] ~= nil and 0 or to[type(b)] or 'z')..(tostring(b):gsub("%d+",padnum)) end) end
+  local function val2str(t, name, indent, insref, path, plainindex, level)
+    local ttype, level, mt = type(t), (level or 0), getmetatable(t)
+    local spath, sname = safename(path, name)
+    local tag = plainindex and
+      ((type(name) == "number") and '' or name..space..'='..space) or
+      (name ~= nil and sname..space..'='..space or '')
+    if seen[t] then -- already seen this element
+      sref[#sref+1] = spath..space..'='..space..seen[t]
+      return tag..'nil'..comment('ref', level) end
+    if type(mt) == 'table' and (mt.__serialize or mt.__tostring) then -- knows how to serialize itself
+      seen[t] = insref or spath
+      if mt.__serialize then t = mt.__serialize(t) else t = tostring(t) end
+      ttype = type(t) end -- new value falls through to be serialized
+    if ttype == "table" then
+      if level >= maxl then return tag..'{}'..comment('max', level) end
+      seen[t] = insref or spath
+      if next(t) == nil then return tag..'{}'..comment(t, level) end -- table empty
+      local maxn, o, out = math.min(#t, maxnum or #t), {}, {}
+      for key = 1, maxn do o[key] = key end
+      if not maxnum or #o < maxnum then
+        local n = #o -- n = n + 1; o[n] is much faster than o[#o+1] on large tables
+        for key in pairs(t) do if o[key] ~= key then n = n + 1; o[n] = key end end end
+      if maxnum and #o > maxnum then o[maxnum+1] = nil end
+      if opts.sortkeys and #o > maxn then alphanumsort(o, t, opts.sortkeys) end
+      local sparse = sparse and #o > maxn -- disable sparsness if only numeric keys (shorter output)
+      for n, key in ipairs(o) do
+        local value, ktype, plainindex = t[key], type(key), n <= maxn and not sparse
+        if opts.valignore and opts.valignore[value] -- skip ignored values; do nothing
+        or opts.keyallow and not opts.keyallow[key]
+        or opts.valtypeignore and opts.valtypeignore[type(value)] -- skipping ignored value types
+        or sparse and value == nil then -- skipping nils; do nothing
+        elseif ktype == 'table' or ktype == 'function' or badtype[ktype] then
+          if not seen[key] and not globals[key] then
+            sref[#sref+1] = 'placeholder'
+            local sname = safename(iname, gensym(key)) -- iname is table for local variables
+            sref[#sref] = val2str(key,sname,indent,sname,iname,true) end
+          sref[#sref+1] = 'placeholder'
+          local path = seen[t]..'['..tostring(seen[key] or globals[key] or gensym(key))..']'
+          sref[#sref] = path..space..'='..space..tostring(seen[value] or val2str(value,nil,indent,path))
+        else
+          out[#out+1] = val2str(value,key,indent,insref,seen[t],plainindex,level+1)
+        end
+      end
+      local prefix = string.rep(indent or '', level)
+      local head = indent and '{\n'..prefix..indent or '{'
+      local body = table.concat(out, ','..(indent and '\n'..prefix..indent or space))
+      local tail = indent and "\n"..prefix..'}' or '}'
+      return (custom and custom(tag,head,body,tail) or tag..head..body..tail)..comment(t, level)
+    elseif badtype[ttype] then
+      seen[t] = insref or spath
+      return tag..globerr(t, level)
+    elseif ttype == 'function' then
+      seen[t] = insref or spath
+      local ok, res = pcall(string.dump, t)
+      local func = ok and ((opts.nocode and "function() --[[..skipped..]] end" or
+        "((loadstring or load)("..safestr(res)..",'@serialized'))")..comment(t, level))
+      return tag..(func or globerr(t, level))
+    else return tag..safestr(t) end -- handle all other types
+  end
+  local sepr = indent and "\n" or ";"..space
+  local body = val2str(t, name, indent) -- this call also populates sref
+  local tail = #sref>1 and table.concat(sref, sepr)..sepr or ''
+  local warn = opts.comment and #sref>1 and space.."--[[incomplete output with shared/self-references skipped]]" or ''
+  return not name and body..warn or "do local "..body..sepr..tail.."return "..name..sepr.."end"
+end
+
+local function deserialize(data, opts)
+  local env = (opts and opts.safe == false) and G
+    or setmetatable({}, {
+        __index = function(t,k) return t end,
+        __call = function(t,...) error("cannot call functions") end
+      })
+  local f, res = (loadstring or load)('return '..data, nil, nil, env)
+  if not f then f, res = (loadstring or load)(data, nil, nil, env) end
+  if not f then return f, res end
+  if setfenv then setfenv(f, env) end
+  return pcall(f)
+end
+
+local function merge(a, b) if b then for k,v in pairs(b) do a[k] = v end end; return a; end
+return { _NAME = n, _COPYRIGHT = c, _DESCRIPTION = d, _VERSION = v, serialize = s,
+  load = deserialize,
+  dump = function(a, opts) return s(a, merge({name = '_', compact = true, sparse = true}, opts)) end,
+  line = function(a, opts) return s(a, merge({sortkeys = true, comment = true}, opts)) end,
+  block = function(a, opts) return s(a, merge({indent = '  ', sortkeys = true, comment = true}, opts)) end }

+ 226 - 0
test.lua

@@ -0,0 +1,226 @@
+local u  = require 'luaunit'
+local mp = require "mp"
+
+local eq   = u.assertEquals
+local fail = u.assertErrorMsgContains
+
+local function check_eq(t, t2)
+   local bytes = mp.encode(t)
+   local ok, r = pcall(mp.decode, bytes)
+   if not ok then
+      print(mp.tohex(bytes))
+      error(r)
+   end
+   eq(mp.decode(bytes), t2 or t)
+end
+
+function _G.test_basic()
+   local a = "\0\1\2\3\4\5\6\7\8\9\10\11\12\13\14\15\16"
+   eq(mp.fromhex(mp.tohex(a)), a)
+   eq({mp.decode "\1\2"}, {1,2})
+   check_eq {
+      foo = 1,
+      bar = 2,
+      array = {1,2,3,4}
+   }
+   check_eq {
+      foo = 1,
+      bar = 2,
+      array = mp.array {1,2,3,4}
+   }
+   check_eq {
+      foo = 1,
+      bar = 2,
+      array = mp.array {1,2,3,4}
+   }
+   check_eq {
+      foo = 1,
+      bar = 2,
+      array = mp.map {1,2,3,4}
+   }
+   check_eq {
+      f1 = 1,
+      f2 = 2,
+      fs1 = "foo",
+      fs2 = "bar",
+      fm = {
+         ft1 = 1,
+         ft2 = "foo",
+         ft3 = {1,2,3},
+         ft4 = { ft5 = { ft6 = { ft7 = { ft8 = { ft9 = mp.array { } } } } } }
+      },
+      fa_long = {1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17},
+      fm_long = {
+         f1 = 1, f2 = 2, f3 = 3, f4 = 4, f5 = 5, f6 = 6, f7 = 7, f8 = 8,
+         f9 = 9, f10 = 10, f11 = 11, f12 = 12, f13 = 13, f14 = 14, f15 = 15, f16 = 16,
+         f17 = 17, f18 = 18, f19 = 19, f20 = 20,
+      }
+   }
+end
+
+function _G.test_boolean()
+   check_eq(true)
+   check_eq(false)
+end
+
+function _G.test_number()
+   check_eq(0)
+   check_eq(127)
+   check_eq(128)
+   check_eq(0x100 + 100)
+   check_eq(0x10000 + 100)
+   check_eq(0x100000000 + 100)
+   check_eq(-1)
+   check_eq(-32)
+   check_eq(-33)
+   check_eq(-100)
+   check_eq(-0x100 - 100)
+   check_eq(-0x10000 - 100)
+   check_eq(-0x100000000 - 100)
+   check_eq("a")
+   check_eq("aaa")
+   check_eq(("a"):rep(31))
+   check_eq(("a"):rep(32))
+   check_eq(("a"):rep(0x100+100))
+   check_eq(("a"):rep(0x10000+100))
+   check_eq(1.123)
+   check_eq(function() return "u",100 end, 100)
+   check_eq(function() return "u",200 end, 200)
+   check_eq(function() return "u",0x100 + 100 end, 0x100 + 100)
+   check_eq(function() return "u",0x10000 + 100 end, 0x10000 + 100)
+   check_eq(function() return "u",0x100000000 + 100 end, 0x100000000 + 100)
+   check_eq(function() return "i",100 end, 100)
+   check_eq(function() return "i",0x100 + 100 end, 0x100 + 100)
+   check_eq(function() return "i",0x10000 + 100 end, 0x10000 + 100)
+   check_eq(function() return "i",0x100000000 + 100 end, 0x100000000 + 100)
+   check_eq(function() return "f",1.5 end, 1.5)
+   check_eq(function() return "d",1.5 end, 1.5)
+   check_eq(mp.meta("int", { value = 1 }), 1)
+   check_eq(mp.meta("value", { value = 1 }), 1)
+end
+
+function _G.test_list()
+   do
+      local a = {}
+      for i = 1, 1000 do
+         a[#a+1] = i
+      end
+      check_eq(a)
+      for i = 1, 65535 do
+         a[#a+1] = i
+      end
+      check_eq(a)
+   end
+   do
+      local a = {}
+      for i = 1, 1000 do
+         a["foo"..i] = i
+      end
+      check_eq(a)
+      for i = 1, 65536 do
+         a["foo"..(i+1000)] = i
+      end
+      check_eq(a)
+   end
+end
+
+function _G.test_nil()
+   check_eq(nil)
+   eq(mp.decode(mp.encode(mp.null)), nil)
+   eq(tostring(mp.null), "null")
+end
+
+function _G.test_ext()
+   local e = { type = 1, value = "foo" }
+   local function check_ext(t, v)
+      check_eq(function() return "e", t, v end, { type = t, value = v })
+   end
+   check_ext(1, "a")
+   check_ext(1, ("a"):rep(2))
+   check_ext(1, ("a"):rep(4))
+   check_ext(1, ("a"):rep(8))
+   check_ext(1, ("a"):rep(16))
+   check_ext(1, ("a"):rep(50))
+   check_ext(1, ("a"):rep(30000))
+   check_ext(1, ("a"):rep(65537))
+   check_ext(1, ("a"):rep(3))
+   eq(mp.decode(mp.encode(function() return "e", 1, "foo" end),
+         nil, nil, function(t, v)
+            eq(t, 1)
+            eq(v, "foo")
+            return v
+         end), "foo")
+   eq(mp.decode(mp.encode(function() return "e", 1, "foo" end),
+         nil, nil, function() end), e)
+   check_eq(mp.meta("extension", e), e)
+   --check_eq(mp.meta("t", mp.meta("ext", { value = e })), e)
+   local e1 = mp.meta("extension", "foo")
+   e1.type = 1
+   check_eq(e1, e)
+   local e2 = mp.meta("extension", setmetatable({type = 1, value = "foo"}, {}))
+   check_eq(e2, e)
+end
+
+function _G.test_handler()
+   check_eq(function() return "T" end, true)
+   eq(mp.decode(mp.encode(function() return "F" end)), false)
+   check_eq(function() return "s", "foo" end, "foo")
+   check_eq(function() return "b", "foo" end, "foo")
+
+end
+
+function _G.test_error()
+   fail("number expected, got nil",
+      function() mp.encode(function() return "u", nil end) end)
+   fail("integer expected, got number",
+      function() mp.encode(function() return "u", 1.2 end) end)
+   fail("attempt to index a msgpack.null value",
+      function() return mp.null.abc end)
+   fail("attempt to index a msgpack.null value",
+      function() mp.null.abc = 10 end)
+   local a = {} a[1] = a
+   fail("array level too deep", function() mp.encode(a) end)
+   a = {} a.foo = a
+   fail("map level too deep", function() mp.encode(a) end)
+   fail("invalid key in map", function()
+      mp.encode { [function() end] = 1 }
+   end)
+   fail("integer expected for extension type, got boolean", function()
+      mp.encode(function() return "e", true end)
+   end)
+   fail("invalid extension type: 10000", function()
+      mp.encode(function() return "e", 10000 end)
+   end)
+   fail("string expected for extension value, got boolean", function()
+      mp.encode(function() return "e", 1, true end)
+   end)
+   fail("'pack' field expected in handler object", function()
+      mp.encode(mp.meta("handler", {}))
+   end)
+   fail("'value' field expected in wrapper object", function()
+      mp.encode(mp.meta("int", {}))
+   end)
+   fail("invalid type 'thread'", function()
+      mp.encode(coroutine.create(function() end))
+   end)
+   fail("invalid string at offset 2: 1 bytes expected, got 0 bytes",
+      function() mp.decode("\161") end)
+   fail("invalid string at offset 2: 1 bytes expected, got 0 bytes",
+      function() mp.decode("\161") end)
+   fail("unexpected end of message at offset 2: map key expected",
+      function() mp.decode("\129") end)
+   fail("unexpected end of message at offset 3: map value expected",
+      function() mp.decode("\129\1") end)
+   fail("unexpected end of message at offset 2: array element expected",
+      function() mp.decode("\145") end)
+   fail("invalid char '193' at offset 2",
+      function() mp.decode("\193") end)
+end
+
+if _VERSION == "Lua 5.1" and not _G.jit then
+   u.LuaUnit.run()
+else
+   os.exit(u.LuaUnit.run(), true)
+end
+
+-- cc: run='rm -f *.gcda; time lua test.lua; gcov mp.c'