local W=require("\112\108\046\116\097\098\108\101\120")local h=require("\112\108\046\117\116\105\108\115")local K=require("\112\108\046\112\114\101\116\116\121")local Q=require("\112\108\046\112\097\116\104")local N,y,X=type,h.unpack,h.pack local A=os.clock local k=require("\100\101\098\117\103")local Y=io local function j(W)if N(W)=="\116\097\098\108\101"and not(getmetatable(W)and(getmetatable(W)).__tostring)then return K.write(W," ",true)elseif N(W)=="\115\116\114\105\110\103"then return"\""..(W.."\"")else return tostring(W)end end local F={}function F.error_handler(W,K,N,y,X)local A=Y.stderr A:write(Q.basename(W)..("\058"..(K.."\058 \097\115\115\101\114\116\105\111\110 \102\097\105\108\101\100\n")))A:write("\103\111\116\058\t",N,"\n")A:write("\110\101\101\100\101\100\058\t",y,"\n")h.quit(1,X or"\116\104\101\115\101 \118\097\108\117\101\115 \119\101\114\101 \110\111\116 \101\113\117\097\108")end local function G(W,h,K,Q)local N=k.getinfo(3+(Q or 0))F.error_handler(N.short_src,N.currentline,j(W),j(h),K)end F.complain=G function F.asserteq(h,K,Q,N)local y=h==K if not y then y=W.deepcompare(h,K,true,Q)end if not y then G(h,K,nil,N)end end function F.assertmatch(W,h,K)if not W:match(h)then G(W,h,"\116\104\101\115\101 \115\116\114\105\110\103\115 \100\105\100 \110\111\116 \109\097\116\099\104",K)end end function F.assertraise(W,h,K)local Q,X if N(W)=="\116\097\098\108\101"then Q,X=pcall(y(W))else Q,X=pcall(W)end if Q or X:match(h)==nil then G(X,h,"\116\104\101\115\101 \101\114\114\111\114\115 \100\105\100 \110\111\116 \109\097\116\099\104",K)end end function F.asserteq2(W,h,K,Q,N)if W~=K then G(W,K,nil,N)end if h~=Q then G(h,Q,nil,N)end end local n={unpack=y}n.__index=n function n.__tostring(W)local h={}for K=1,W.n,1 do local Q=W[K]h[K]=N(Q)=="\115\116\114\105\110\103"and("\037\113"):format(Q)or tostring(Q)end return"\116\117\112\108\101\040"..(table.concat(h,"\044 ").."\041")end function n.__eq(W,h)if W.n~=h.n then return false end for K=1,W.n,1 do if W[K]~=h[K]then return false end end return true end function n.__len(W)return W.n end function F.tuple(...)return setmetatable(X(...),n)end function F.timer(W,K,Q,...)local N=A()for W=1,K,1 do Q(...)end h.printf("\037\115\058 \116\111\111\107 \037\055\046\050\102 \115\101\099\n",W,A()-N)end return F
--- Useful test utilities.
--
--    test.asserteq({1,2},{1,2}) -- can compare tables
--    test.asserteq(1.2,1.19,0.02) -- compare FP numbers within precision
--    T = test.tuple -- used for comparing multiple results
--    test.asserteq(T(string.find(" me","me")),T(2,3))
--
-- Dependencies: `pl.utils`, `pl.tablex`, `pl.pretty`, `pl.path`, `debug`
-- @module pl.test

local tablex = require 'pl.tablex'
local utils = require 'pl.utils'
local pretty = require 'pl.pretty'
local path = require 'pl.path'
local type,unpack,pack = type,utils.unpack,utils.pack
local clock = os.clock
local debug = require 'debug'
local io = io

local function dump(x)
    if type(x) == 'table' and not (getmetatable(x) and getmetatable(x).__tostring) then
        return pretty.write(x,' ',true)
    elseif type(x) == 'string' then
        return '"'..x..'"'
    else
        return tostring(x)
    end
end

local test = {}

---- error handling for test results.
-- By default, this writes to stderr and exits the program.
-- Re-define this function to raise an error and/or redirect output
function test.error_handler(file,line,got_text, needed_text,msg)
    local err = io.stderr
    err:write(path.basename(file)..':'..line..': assertion failed\n')
    err:write("got:\t",got_text,'\n')
    err:write("needed:\t",needed_text,'\n')
    utils.quit(1,msg or "these values were not equal")
end

local function complain (x,y,msg,where)
    local i = debug.getinfo(3 + (where or 0))
    test.error_handler(i.short_src,i.currentline,dump(x),dump(y),msg)
end

--- general test complain message.
-- Useful for composing new test functions (see tests/tablex.lua for an example)
-- @param x a value
-- @param y value to compare first value against
-- @param msg message
-- @param where extra level offset for errors
-- @function complain
test.complain = complain

--- like assert, except takes two arguments that must be equal and can be tables.
-- If they are plain tables, it will use tablex.deepcompare.
-- @param x any value
-- @param y a value equal to x
-- @param eps an optional tolerance for numerical comparisons
-- @param where extra level offset
function test.asserteq (x,y,eps,where)
    local res = x == y
    if not res then
        res = tablex.deepcompare(x,y,true,eps)
    end
    if not res then
        complain(x,y,nil,where)
    end
end

--- assert that the first string matches the second.
-- @param s1 a string
-- @param s2 a string
-- @param where extra level offset
function test.assertmatch (s1,s2,where)
    if not s1:match(s2) then
        complain (s1,s2,"these strings did not match",where)
    end
end

--- assert that the function raises a particular error.
-- @param fn a function or a table of the form {function,arg1,...}
-- @param e a string to match the error against
-- @param where extra level offset
function test.assertraise(fn,e,where)
    local ok, err
    if type(fn) == 'table' then
        ok, err = pcall(unpack(fn))
    else
        ok, err = pcall(fn)
    end
    if ok or err:match(e)==nil then
        complain (err,e,"these errors did not match",where)
    end
end

--- a version of asserteq that takes two pairs of values.
-- <code>x1==y1 and x2==y2</code> must be true. Useful for functions that naturally
-- return two values.
-- @param x1 any value
-- @param x2 any value
-- @param y1 any value
-- @param y2 any value
-- @param where extra level offset
function test.asserteq2 (x1,x2,y1,y2,where)
    if x1 ~= y1 then complain(x1,y1,nil,where) end
    if x2 ~= y2 then complain(x2,y2,nil,where) end
end

-- tuple type --

local tuple_mt = {
    unpack = unpack
}
tuple_mt.__index = tuple_mt

function tuple_mt.__tostring(self)
    local ts = {}
    for i=1, self.n do
        local s = self[i]
        ts[i] = type(s) == 'string' and ('%q'):format(s) or tostring(s)
    end
    return 'tuple(' .. table.concat(ts, ', ') .. ')'
end

function tuple_mt.__eq(a, b)
    if a.n ~= b.n then return false end
    for i=1, a.n do
        if a[i] ~= b[i] then return false end
    end
    return true
end

function tuple_mt.__len(self)
    return self.n
end

--- encode an arbitrary argument list as a tuple.
-- This can be used to compare to other argument lists, which is
-- very useful for testing functions which return a number of values.
-- Unlike regular array-like tables ('sequences') they may contain nils.
-- Tuples understand equality and know how to print themselves out.
-- The # operator is defined to be the size, irrespecive of any nils,
-- and there is an `unpack` method.
-- @usage asserteq(tuple( ('ab'):find 'a'), tuple(1,1))
function test.tuple(...)
    return setmetatable(pack(...), tuple_mt)
end

--- Time a function. Call the function a given number of times, and report the number of seconds taken,
-- together with a message.  Any extra arguments will be passed to the function.
-- @string msg a descriptive message
-- @int n number of times to call the function
-- @func fun the function
-- @param ... optional arguments to fun
function test.timer(msg,n,fun,...)
    local start = clock()
    for i = 1,n do fun(...) end
    utils.printf("%s: took %7.2f sec\n",msg,clock()-start)
end

return test
