summaryrefslogtreecommitdiff
path: root/macros/luatex/generic/penlight/pl/test.lua
blob: 694bbc531a0b82e4f014416780821686ee15365c (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
--- 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