Browse Source

Rework lua tests one more time.

tags/0.9.0
Vsevolod Stakhov 9 years ago
parent
commit
e4cc017c5d

+ 2
- 0
config.h.in View File

@@ -244,6 +244,8 @@

#define MODULES_NUM ${RSPAMD_MODULES_NUM}

#define BUILDROOT "${CMAKE_BINARY_DIR}"

/* sys/types */
#ifdef HAVE_SYS_TYPES_H
#include <sys/types.h>

+ 10
- 2
test/CMakeLists.txt View File

@@ -24,10 +24,18 @@ TARGET_LINK_LIBRARIES(rspamd-test stemmer)

IF(NOT "${CMAKE_CURRENT_SOURCE_DIR}" STREQUAL "${CMAKE_CURRENT_BINARY_DIR}")
# Also add dependencies for convenience
FILE(GLOB LUA_TESTS "${CMAKE_CURRENT_SOURCE_DIR}/lua/*")
FILE(GLOB_RECURSE LUA_TESTS "${CMAKE_CURRENT_SOURCE_DIR}/lua/*")
ADD_CUSTOM_TARGET(units-dir COMMAND
${CMAKE_COMMAND} -E make_directory "${CMAKE_CURRENT_BINARY_DIR}/lua/unit"
)
ADD_DEPENDENCIES(rspamd-test units-dir)
FOREACH(_LF IN LISTS LUA_TESTS)
GET_FILENAME_COMPONENT(_NM "${_LF}" NAME)
SET(_DS "${CMAKE_CURRENT_BINARY_DIR}/lua/${_NM}")
IF("${_LF}" MATCHES "^.*/unit/.*$")
SET(_DS "${CMAKE_CURRENT_BINARY_DIR}/lua/unit/${_NM}")
ELSE()
SET(_DS "${CMAKE_CURRENT_BINARY_DIR}/lua/${_NM}")
ENDIF()
ADD_CUSTOM_TARGET("${_NM}" COMMAND
${CMAKE_COMMAND} -E copy_if_different ${_LF} ${_DS}
SOURCES "${_LF}"

+ 391
- 0
test/lua/compat_env.lua View File

@@ -0,0 +1,391 @@
--[[

compat_env v$(_VERSION) - Lua 5.1/5.2 environment compatibility functions

SYNOPSIS

-- Get load/loadfile compatibility functions only if using 5.1.
local CL = pcall(load, '') and _G or require 'compat_env'
local load = CL.load
local loadfile = CL.loadfile
-- The following now works in both Lua 5.1 and 5.2:
assert(load('return 2*pi', nil, 't', {pi=math.pi}))()
assert(loadfile('ex.lua', 't', {print=print}))()
-- Get getfenv/setfenv compatibility functions only if using 5.2.
local getfenv = _G.getfenv or require 'compat_env'.getfenv
local setfenv = _G.setfenv or require 'compat_env'.setfenv
local function f() return x end
setfenv(f, {x=2})
print(x, getfenv(f).x) --> 2, 2

DESCRIPTION

This module provides Lua 5.1/5.2 environment related compatibility functions.
This includes implementations of Lua 5.2 style `load` and `loadfile`
for use in Lua 5.1. It also includes Lua 5.1 style `getfenv` and `setfenv`
for use in Lua 5.2.
API

local CL = require 'compat_env'
CL.load (ld [, source [, mode [, env] ] ]) --> f [, err]

This behaves the same as the Lua 5.2 `load` in both
Lua 5.1 and 5.2.
http://www.lua.org/manual/5.2/manual.html#pdf-load
CL.loadfile ([filename [, mode [, env] ] ]) --> f [, err]
This behaves the same as the Lua 5.2 `loadfile` in both
Lua 5.1 and 5.2.
http://www.lua.org/manual/5.2/manual.html#pdf-loadfile
CL.getfenv ([f]) --> t

This is identical to the Lua 5.1 `getfenv` in Lua 5.1.
This behaves similar to the Lua 5.1 `getfenv` in Lua 5.2.
When a global environment is to be returned, or when `f` is a
C function, this returns `_G` since Lua 5.2 doesn't have
(thread) global and C function environments. This will also
return `_G` if the Lua function `f` lacks an `_ENV`
upvalue, but it will raise an error if uncertain due to lack of
debug info. It is not normally considered good design to use
this function; when possible, use `load` or `loadfile` instead.
http://www.lua.org/manual/5.1/manual.html#pdf-getfenv
CL.setfenv (f, t)
This is identical to the Lua 5.1 `setfenv` in Lua 5.1.
This behaves similar to the Lua 5.1 `setfenv` in Lua 5.2.
This will do nothing if `f` is a Lua function that
lacks an `_ENV` upvalue, but it will raise an error if uncertain
due to lack of debug info. See also Design Notes below.
It is not normally considered good design to use
this function; when possible, use `load` or `loadfile` instead.
http://www.lua.org/manual/5.1/manual.html#pdf-setfenv
DESIGN NOTES

This module intends to provide robust and fairly complete reimplementations
of the environment related Lua 5.1 and Lua 5.2 functions.
No effort is made, however, to simulate rare or difficult to simulate features,
such as thread environments, although this is liable to change in the future.
Such 5.1 capabilities are discouraged and ideally
removed from 5.1 code, thereby allowing your code to work in both 5.1 and 5.2.
In Lua 5.2, a `setfenv(f, {})`, where `f` lacks any upvalues, will be silently
ignored since there is no `_ENV` in this function to write to, and the
environment will have no effect inside the function anyway. However,
this does mean that `getfenv(setfenv(f, t))` does not necessarily equal `t`,
which is incompatible with 5.1 code (a possible workaround would be [1]).
If `setfenv(f, {})` has an upvalue but no debug info, then this will raise
an error to prevent inadvertently executing potentially untrusted code in the
global environment.
It is not normally considered good design to use `setfenv` and `getfenv`
(one reason they were removed in 5.2). When possible, consider replacing
these with `load` or `loadfile`, which are more restrictive and have native
implementations in 5.2.
This module might be merged into a more general Lua 5.1/5.2 compatibility
library (e.g. a full reimplementation of Lua 5.2 `_G`). However,
`load/loadfile/getfenv/setfenv` perhaps are among the more cumbersome
functions not to have.

INSTALLATION

Download compat_env.lua:
wget https://raw.github.com/gist/1654007/compat_env.lua

Copy compat_env.lua into your LUA_PATH.
Alternately, unpack, test, and install into LuaRocks:
wget https://raw.github.com/gist/1422205/sourceunpack.lua
lua sourceunpack.lua compat_env.lua
(cd out && luarocks make)

Related work

http://lua-users.org/wiki/LuaVersionCompatibility
https://github.com/stevedonovan/Penlight/blob/master/lua/pl/utils.lua
- penlight implementations of getfenv/setfenv
http://lua-users.org/lists/lua-l/2010-06/msg00313.html
- initial getfenv/setfenv implementation
References

[1] http://lua-users.org/lists/lua-l/2010-06/msg00315.html

Copyright

(c) 2012 David Manura. Licensed under the same terms as Lua 5.1/5.2 (MIT license).

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

--]]---------------------------------------------------------------------

local M = {_TYPE='module', _NAME='compat_env', _VERSION='0.2.20120124'}

local function check_chunk_type(s, mode)
local nmode = mode or 'bt'
local is_binary = s and #s > 0 and s:byte(1) == 27
if is_binary and not nmode:match'b' then
return nil, ("attempt to load a binary chunk (mode is '%s')"):format(mode)
elseif not is_binary and not nmode:match't' then
return nil, ("attempt to load a text chunk (mode is '%s')"):format(mode)
end
return true
end

local IS_52_LOAD = pcall(load, '')
if IS_52_LOAD then
M.load = _G.load
M.loadfile = _G.loadfile
else
-- 5.2 style `load` implemented in 5.1
function M.load(ld, source, mode, env)
local f
if type(ld) == 'string' then
local s = ld
local ok, err = check_chunk_type(s, mode); if not ok then return ok, err end
local err; f, err = loadstring(s, source); if not f then return f, err end
elseif type(ld) == 'function' then
local ld2 = ld
if (mode or 'bt') ~= 'bt' then
local first = ld()
local ok, err = check_chunk_type(first, mode); if not ok then return ok, err end
ld2 = function()
if first then
local chunk=first; first=nil; return chunk
else return ld() end
end
end
local err; f, err = load(ld2, source); if not f then return f, err end
else
error(("bad argument #1 to 'load' (function expected, got %s)"):format(type(ld)), 2)
end
if env then setfenv(f, env) end
return f
end

-- 5.2 style `loadfile` implemented in 5.1
function M.loadfile(filename, mode, env)
if (mode or 'bt') ~= 'bt' then
local ioerr
local fh, err = io.open(filename, 'rb'); if not fh then return fh, err end
local function ld() local chunk; chunk,ioerr = fh:read(4096); return chunk end
local f, err = M.load(ld, filename and '@'..filename, mode, env)
fh:close()
if not f then return f, err end
if ioerr then return nil, ioerr end
return f
else
local f, err = loadfile(filename); if not f then return f, err end
if env then setfenv(f, env) end
return f
end
end
end

if _G.setfenv then -- Lua 5.1
M.setfenv = _G.setfenv
M.getfenv = _G.getfenv
else -- >= Lua 5.2
-- helper function for `getfenv`/`setfenv`
local function envlookup(f)
local name, val
local up = 0
local unknown
repeat
up=up+1; name, val = debug.getupvalue(f, up)
if name == '' then unknown = true end
until name == '_ENV' or name == nil
if name ~= '_ENV' then
up = nil
if unknown then error("upvalues not readable in Lua 5.2 when debug info missing", 3) end
end
return (name == '_ENV') and up, val, unknown
end

-- helper function for `getfenv`/`setfenv`
local function envhelper(f, name)
if type(f) == 'number' then
if f < 0 then
error(("bad argument #1 to '%s' (level must be non-negative)"):format(name), 3)
elseif f < 1 then
error("thread environments unsupported in Lua 5.2", 3) --[*]
end
f = debug.getinfo(f+2, 'f').func
elseif type(f) ~= 'function' then
error(("bad argument #1 to '%s' (number expected, got %s)"):format(type(name, f)), 2)
end
return f
end
-- [*] might simulate with table keyed by coroutine.running()
-- 5.1 style `setfenv` implemented in 5.2
function M.setfenv(f, t)
local f = envhelper(f, 'setfenv')
local up, val, unknown = envlookup(f)
if up then
debug.upvaluejoin(f, up, function() return up end, 1) -- unique upvalue [*]
debug.setupvalue(f, up, t)
else
local what = debug.getinfo(f, 'S').what
if what ~= 'Lua' and what ~= 'main' then -- not Lua func
error("'setfenv' cannot change environment of given object", 2)
end -- else ignore no _ENV upvalue (warning: incompatible with 5.1)
end
-- added in https://gist.github.com/2255007
return f
end
-- [*] http://lua-users.org/lists/lua-l/2010-06/msg00313.html

-- 5.1 style `getfenv` implemented in 5.2
function M.getfenv(f)
if f == 0 or f == nil then return _G end -- simulated behavior
local f = envhelper(f, 'setfenv')
local up, val = envlookup(f)
if not up then return _G end -- simulated behavior [**]
return val
end
-- [**] possible reasons: no _ENV upvalue, C function
end


return M

--[[ FILE rockspec.in

package = 'compat_env'
version = '$(_VERSION)-1'
source = {
url = 'https://raw.github.com/gist/1654007/$(GITID)/compat_env.lua',
--url = 'https://raw.github.com/gist/1654007/compat_env.lua', -- latest raw
--url = 'https://gist.github.com/gists/1654007/download',
md5 = '$(MD5)'
}
description = {
summary = 'Lua 5.1/5.2 environment compatibility functions',
detailed = [=[
Provides Lua 5.1/5.2 environment related compatibility functions.
This includes implementations of Lua 5.2 style `load` and `loadfile`
for use in Lua 5.1. It also includes Lua 5.1 style `getfenv` and `setfenv`
for use in Lua 5.2.
]=],
license = 'MIT/X11',
homepage = 'https://gist.github.com/1654007',
maintainer = 'David Manura'
}
dependencies = {} -- Lua 5.1 or 5.2
build = {
type = 'builtin',
modules = {
['compat_env'] = 'compat_env.lua'
}
}

--]]---------------------------------------------------------------------

--[[ FILE test.lua

-- test.lua - test suite for compat_env module.

local CL = require 'compat_env'
local load = CL.load
local loadfile = CL.loadfile
local setfenv = CL.setfenv
local getfenv = CL.getfenv

local function checkeq(a, b, e)
if a ~= b then error(
'not equal ['..tostring(a)..'] ['..tostring(b)..'] ['..tostring(e)..']')
end
end
local function checkerr(pat, ok, err)
assert(not ok, 'checkerr')
assert(type(err) == 'string' and err:match(pat), err)
end

-- test `load`
checkeq(load('return 2')(), 2)
checkerr('expected near', load'return 2 2')
checkerr('text chunk', load('return 2', nil, 'b'))
checkerr('text chunk', load('', nil, 'b'))
checkerr('binary chunk', load('\027', nil, 't'))
checkeq(load('return 2*x',nil,'bt',{x=5})(), 10)
checkeq(debug.getinfo(load('')).source, '')
checkeq(debug.getinfo(load('', 'foo')).source, 'foo')

-- test `loadfile`
local fh = assert(io.open('tmp.lua', 'wb'))
fh:write('return (...) or x')
fh:close()
checkeq(loadfile('tmp.lua')(2), 2)
checkeq(loadfile('tmp.lua', 't')(2), 2)
checkerr('text chunk', loadfile('tmp.lua', 'b'))
checkeq(loadfile('tmp.lua', nil, {x=3})(), 3)
checkeq(debug.getinfo(loadfile('tmp.lua')).source, '@tmp.lua')
checkeq(debug.getinfo(loadfile('tmp.lua', 't', {})).source, '@tmp.lua')
os.remove'tmp.lua'

-- test `setfenv`/`getfenv`
x = 5
local a,b=true; local function f(c) if a then return x,b,c end end
setfenv(f, {x=3})
checkeq(f(), 3)
checkeq(getfenv(f).x, 3)
checkerr('cannot change', pcall(setfenv, string.len, {})) -- C function
checkeq(getfenv(string.len), _G) -- C function
local function g()
setfenv(1, {x=4})
checkeq(getfenv(1).x, 4)
return x
end
checkeq(g(), 4) -- numeric level
if _G._VERSION ~= 'Lua 5.1' then
checkerr('unsupported', pcall(setfenv, 0, {}))
end
checkeq(getfenv(0), _G)
checkeq(getfenv(), _G) -- no arg
checkeq(x, 5) -- main unaltered
setfenv(function()end, {}) -- no upvalues, ignore
checkeq(getfenv(function()end), _G) -- no upvaluse
if _G._VERSION ~= 'Lua 5.1' then
checkeq(getfenv(setfenv(function()end, {})), _G) -- warning: incompatible with 5.1
end
x = nil

print 'OK'

--]]---------------------------------------------------------------------

--[[ FILE CHANGES.txt
0.2.20120124
Renamed module to compat_env (from compat_load)
Add getfenv/setfenv functions

0.1.20120121
Initial public release
--]]

+ 621
- 0
test/lua/telescope.lua View File

@@ -0,0 +1,621 @@
--[[
The MIT License

Copyright (c) 2009-2012 [Norman Clarke](mailto:norman@njclarke.com)

Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
]]--

--- Telescope is a test library for Lua that allows for flexible, declarative
-- tests. The documentation produced here is intended largely for developers
-- working on Telescope. For information on using Telescope, please visit the
-- project homepage at: <a href="http://github.com/norman/telescope">http://github.com/norman/telescope#readme</a>.
-- @release 0.6
-- @class module
-- @module 'telescope'
local _M = {}

local compat_env = require 'compat_env'

local getfenv = _G.getfenv or compat_env.getfenv
local setfenv = _G.setfenv or compat_env.setfenv


local _VERSION = "0.6.0"

--- The status codes that can be returned by an invoked test. These should not be overidden.
-- @name status_codes
-- @class table
-- @field err - This is returned when an invoked test results in an error
-- rather than a passed or failed assertion.
-- @field fail - This is returned when an invoked test contains one or more failing assertions.
-- @field pass - This is returned when all of a test's assertions pass.
-- @field pending - This is returned when a test does not have a corresponding function.
-- @field unassertive - This is returned when an invoked test does not produce
-- errors, but does not contain any assertions.
local status_codes = {
err = 2,
fail = 4,
pass = 8,
pending = 16,
unassertive = 32
}

--- Labels used to show the various <tt>status_codes</tt> as a single character.
-- These can be overidden if you wish.
-- @name status_labels
-- @class table
-- @see status_codes
-- @field status_codes.err 'E'
-- @field status_codes.fail 'F'
-- @field status_codes.pass 'P'
-- @field status_codes.pending '?'
-- @field status_codes.unassertive 'U'

local status_labels = {
[status_codes.err] = 'E',
[status_codes.fail] = 'F',
[status_codes.pass] = 'P',
[status_codes.pending] = '?',
[status_codes.unassertive] = 'U'
}

--- The default names for context blocks. It defaults to "context", "spec" and
-- "describe."
-- @name context_aliases
-- @class table
local context_aliases = {"context", "describe", "spec"}
--- The default names for test blocks. It defaults to "test," "it", "expect",
-- "they" and "should."
-- @name test_aliases
-- @class table
local test_aliases = {"test", "it", "expect", "should", "they"}

--- The default names for "before" blocks. It defaults to "before" and "setup."
-- The function in the before block will be run before each sibling test function
-- or context.
-- @name before_aliases
-- @class table
local before_aliases = {"before", "setup"}

--- The default names for "after" blocks. It defaults to "after" and "teardown."
-- The function in the after block will be run after each sibling test function
-- or context.
-- @name after_aliases
-- @class table
local after_aliases = {"after", "teardown"}

-- Prefix to place before all assertion messages. Used by make_assertion().
local assertion_message_prefix = "Assert failed: expected "

--- The default assertions.
-- These are the assertions built into telescope. You can override them or
-- create your own custom assertions using <tt>make_assertion</tt>.
-- <ul>
-- <tt><li>assert_blank(a)</tt> - true if a is nil, or the empty string</li>
-- <tt><li>assert_empty(a)</tt> - true if a is an empty table</li>
-- <tt><li>assert_equal(a, b)</tt> - true if a == b</li>
-- <tt><li>assert_error(f)</tt> - true if function f produces an error</li>
-- <tt><li>assert_false(a)</tt> - true if a is false</li>
-- <tt><li>assert_greater_than(a, b)</tt> - true if a > b</li>
-- <tt><li>assert_gte(a, b)</tt> - true if a >= b</li>
-- <tt><li>assert_less_than(a, b)</tt> - true if a < b</li>
-- <tt><li>assert_lte(a, b)</tt> - true if a <= b</li>
-- <tt><li>assert_match(a, b)</tt> - true if b is a string that matches pattern a</li>
-- <tt><li>assert_nil(a)</tt> - true if a is nil</li>
-- <tt><li>assert_true(a)</tt> - true if a is true</li>
-- <tt><li>assert_type(a, b)</tt> - true if a is of type b</li>
-- <tt><li>assert_not_blank(a)</tt> - true if a is not nil and a is not the empty string</li>
-- <tt><li>assert_not_empty(a)</tt> - true if a is a table, and a is not empty</li>
-- <tt><li>assert_not_equal(a, b)</tt> - true if a ~= b</li>
-- <tt><li>assert_not_error(f)</tt> - true if function f does not produce an error</li>
-- <tt><li>assert_not_false(a)</tt> - true if a is not false</li>
-- <tt><li>assert_not_greater_than(a, b)</tt> - true if not (a > b)</li>
-- <tt><li>assert_not_gte(a, b)</tt> - true if not (a >= b)</li>
-- <tt><li>assert_not_less_than(a, b)</tt> - true if not (a < b)</li>
-- <tt><li>assert_not_lte(a, b)</tt> - true if not (a <= b)</li>
-- <tt><li>assert_not_match(a, b)</tt> - true if the string b does not match the pattern a</li>
-- <tt><li>assert_not_nil(a)</tt> - true if a is not nil</li>
-- <tt><li>assert_not_true(a)</tt> - true if a is not true</li>
-- <tt><li>assert_not_type(a, b)</tt> - true if a is not of type b</li>
-- </ul>
-- @see make_assertion
-- @name assertions
-- @class table
local assertions = {}

--- Create a custom assertion.
-- This creates an assertion along with a corresponding negative assertion. It
-- is used internally by telescope to create the default assertions.
-- @param name The base name of the assertion.
-- <p>
-- The name will be used as the basis of the positive and negative assertions;
-- i.e., the name <tt>equal</tt> would be used to create the assertions
-- <tt>assert_equal</tt> and <tt>assert_not_equal</tt>.
-- </p>
-- @param message The base message that will be shown.
-- <p>
-- The assertion message is what is shown when the assertion fails. It will be
-- prefixed with the string in <tt>telescope.assertion_message_prefix</tt>.
-- The variables passed to <tt>telescope.make_assertion</tt> are interpolated
-- in the message string using <tt>string.format</tt>. When creating the
-- inverse assertion, the message is reused, with <tt>" to be "</tt> replaced
-- by <tt>" not to be "</tt>. Hence a recommended format is something like:
-- <tt>"%s to be similar to %s"</tt>.
-- </p>
-- @param func The assertion function itself.
-- <p>
-- The assertion function can have any number of arguments.
-- </p>
-- @usage <tt>make_assertion("equal", "%s to be equal to %s", function(a, b)
-- return a == b end)</tt>
-- @function make_assertion
local function make_assertion(name, message, func)
local num_vars = 0
-- if the last vararg ends up nil, we'll need to pad the table with nils so
-- that string.format gets the number of args it expects
local format_message
if type(message) == "function" then
format_message = message
else
for _, _ in message:gmatch("%%s") do num_vars = num_vars + 1 end
format_message = function(message, ...)
local a = {}
local args = {...}
local nargs = select('#', ...)
if nargs > num_vars then
local userErrorMessage = args[num_vars+1]
if type(userErrorMessage) == "string" then
return(assertion_message_prefix .. userErrorMessage)
else
error(string.format('assert_%s expected %d arguments but got %d', name, num_vars, #args))
end
end
for i = 1, nargs do a[i] = tostring(v) end
for i = nargs+1, num_vars do a[i] = 'nil' end
return (assertion_message_prefix .. message):format(unpack(a))
end
end

assertions["assert_" .. name] = function(...)
if assertion_callback then assertion_callback(...) end
if not func(...) then
error({format_message(message, ...), debug.traceback()})
end
end
end

--- (local) Return a table with table t's values as keys and keys as values.
-- @param t The table.
local function invert_table(t)
local t2 = {}
for k, v in pairs(t) do t2[v] = k end
return t2
end

-- (local) Truncate a string "s" to length "len", optionally followed by the
-- string given in "after" if truncated; for example, truncate_string("hello
-- world", 3, "...")
-- @param s The string to truncate.
-- @param len The desired length.
-- @param after A string to append to s, if it is truncated.
local function truncate_string(s, len, after)
if #s <= len then
return s
else
local s = s:sub(1, len):gsub("%s*$", '')
if after then return s .. after else return s end
end
end

--- (local) Filter a table's values by function. This function iterates over a
-- table , returning only the table entries that, when passed into function f,
-- yield a truthy value.
-- @param t The table over which to iterate.
-- @param f The filter function.
local function filter(t, f)
local a, b
return function()
repeat a, b = next(t, a)
if not b then return end
if f(a, b) then return a, b end
until not b
end
end

--- (local) Finds the value in the contexts table indexed with i, and returns a table
-- of i's ancestor contexts.
-- @param i The index in the <tt>contexts</tt> table to get ancestors for.
-- @param contexts The table in which to find the ancestors.
local function ancestors(i, contexts)
if i == 0 then return end
local a = {}
local function func(j)
if contexts[j].parent == 0 then return nil end
table.insert(a, contexts[j].parent)
func(contexts[j].parent)
end
func(i)
return a
end

make_assertion("blank", "'%s' to be blank", function(a) return a == '' or a == nil end)
make_assertion("empty", "'%s' to be an empty table", function(a) return not next(a) end)
make_assertion("equal", "'%s' to be equal to '%s'", function(a, b) return a == b end)
make_assertion("error", "result to be an error", function(f) return not pcall(f) end)
make_assertion("false", "'%s' to be false", function(a) return a == false end)
make_assertion("greater_than", "'%s' to be greater than '%s'", function(a, b) return a > b end)
make_assertion("gte", "'%s' to be greater than or equal to '%s'", function(a, b) return a >= b end)
make_assertion("less_than", "'%s' to be less than '%s'", function(a, b) return a < b end)
make_assertion("lte", "'%s' to be less than or equal to '%s'", function(a, b) return a <= b end)
make_assertion("match", "'%s' to be a match for %s", function(a, b) return (tostring(b)):match(a) end)
make_assertion("nil", "'%s' to be nil", function(a) return a == nil end)
make_assertion("true", "'%s' to be true", function(a) return a == true end)
make_assertion("type", "'%s' to be a %s", function(a, b) return type(a) == b end)

make_assertion("not_blank", "'%s' not to be blank", function(a) return a ~= '' and a ~= nil end)
make_assertion("not_empty", "'%s' not to be an empty table", function(a) return not not next(a) end)
make_assertion("not_equal", "'%s' not to be equal to '%s'", function(a, b) return a ~= b end)
make_assertion("not_error", "result not to be an error", function(f) return not not pcall(f) end)
make_assertion("not_match", "'%s' not to be a match for %s", function(a, b) return not (tostring(b)):match(a) end)
make_assertion("not_nil", "'%s' not to be nil", function(a) return a ~= nil end)
make_assertion("not_type", "'%s' not to be a %s", function(a, b) return type(a) ~= b end)

--- Build a contexts table from the test file or function given in <tt>target</tt>.
-- If the optional <tt>contexts</tt> table argument is provided, then the
-- resulting contexts will be added to it.
-- <p>
-- The resulting contexts table's structure is as follows:
-- </p>
-- <code>
-- {
-- {parent = 0, name = "this is a context", context = true},
-- {parent = 1, name = "this is a nested context", context = true},
-- {parent = 2, name = "this is a test", test = function},
-- {parent = 2, name = "this is another test", test = function},
-- {parent = 0, name = "this is test outside any context", test = function},
-- }
-- </code>
-- @param contexts A optional table in which to collect the resulting contexts
-- and function.
-- @function load_contexts
local function load_contexts(target, contexts)
local env = {}
local current_index = 0
local context_table = contexts or {}

local function context_block(name, func)
table.insert(context_table, {parent = current_index, name = name, context = true})
local previous_index = current_index
current_index = #context_table
func()
current_index = previous_index
end

local function test_block(name, func)
local test_table = {name = name, parent = current_index, test = func or true}
if current_index ~= 0 then
test_table.context_name = context_table[current_index].name
else
test_table.context_name = 'top level'
end
table.insert(context_table, test_table)
end

local function before_block(func)
context_table[current_index].before = func
end

local function after_block(func)
context_table[current_index].after = func
end

for _, v in ipairs(after_aliases) do env[v] = after_block end
for _, v in ipairs(before_aliases) do env[v] = before_block end
for _, v in ipairs(context_aliases) do env[v] = context_block end
for _, v in ipairs(test_aliases) do env[v] = test_block end

-- Set these functions in the module's meta table to allow accessing
-- telescope's test and context functions without env tricks. This will
-- however add tests to a context table used inside the module, so multiple
-- test files will add tests to the same top-level context, which may or may
-- not be desired.
setmetatable(_M, {__index = env})

setmetatable(env, {__index = _G})

local func, err = type(target) == 'string' and assert(loadfile(target)) or target
if err then error(err) end
setfenv(func, env)()
return context_table
end

-- in-place table reverse.
function table.reverse(t)
local len = #t+1
for i=1, (len-1)/2 do
t[i], t[len-i] = t[len-i], t[i]
end
end

--- Run all tests.
-- This function will exectute each function in the contexts table.
-- @param contexts The contexts created by <tt>load_contexts</tt>.
-- @param callbacks A table of callback functions to be invoked before or after
-- various test states.
-- <p>
-- There is a callback for each test <tt>status_code</tt>, and callbacks to run
-- before or after each test invocation regardless of outcome.
-- </p>
-- <ul>
-- <li>after - will be invoked after each test</li>
-- <li>before - will be invoked before each test</li>
-- <li>err - will be invoked after each test which results in an error</li>
-- <li>fail - will be invoked after each failing test</li>
-- <li>pass - will be invoked after each passing test</li>
-- <li>pending - will be invoked after each pending test</li>
-- <li>unassertive - will be invoked after each test which doesn't assert
-- anything</li>
-- </ul>
-- <p>
-- Callbacks can be used, for example, to drop into a debugger upon a failed
-- assertion or error, for profiling, or updating a GUI progress meter.
-- </p>
-- @param test_filter A function to filter tests that match only conditions that you specify.
-- <p>
-- For example, the folling would allow you to run only tests whose name matches a pattern:
-- </p>
-- <p>
-- <code>
-- function(t) return t.name:match("%s* lexer") end
-- </code>
-- </p>
-- @return A table of result tables. Each result table has the following
-- fields:
-- <ul>
-- <li>assertions_invoked - the number of assertions the test invoked</li>
-- <li>context - the name of the context</li>
-- <li>message - a table with an error message and stack trace</li>
-- <li>name - the name of the test</li>
-- <li>status_code - the resulting status code</li>
-- <li>status_label - the label for the status_code</li>
-- </ul>
-- @see load_contexts
-- @see status_codes
-- @function run
local function run(contexts, callbacks, test_filter)

local results = {}
local status_names = invert_table(status_codes)
local test_filter = test_filter or function(a) return a end

-- Setup a new environment suitable for running a new test
local function newEnv()
local env = {}

-- Make sure globals are accessible in the new environment
setmetatable(env, {__index = _G})

-- Setup all the assert functions in the new environment
for k, v in pairs(assertions) do
setfenv(v, env)
env[k] = v
end

return env
end

local env = newEnv()

local function invoke_callback(name, test)
if not callbacks then return end
if type(callbacks[name]) == "table" then
for _, c in ipairs(callbacks[name]) do c(test) end
elseif callbacks[name] then
callbacks[name](test)
end
end

local function invoke_test(func)
local assertions_invoked = 0
env.assertion_callback = function()
assertions_invoked = assertions_invoked + 1
end
setfenv(func, env)
local result, message = xpcall(func, debug.traceback)
if result and assertions_invoked > 0 then
return status_codes.pass, assertions_invoked, nil
elseif result then
return status_codes.unassertive, 0, nil
elseif type(message) == "table" then
return status_codes.fail, assertions_invoked, message
else
return status_codes.err, assertions_invoked, {message, debug.traceback()}
end
end

for i, v in filter(contexts, function(i, v) return v.test and test_filter(v) end) do
env = newEnv() -- Setup a new environment for this test

local ancestors = ancestors(i, contexts)
local context_name = 'Top level'
if contexts[i].parent ~= 0 then
context_name = contexts[contexts[i].parent].name
end
local result = {
assertions_invoked = 0,
name = contexts[i].name,
context = context_name,
test = i
}
table.sort(ancestors)
-- this "before" is the test callback passed into the runner
invoke_callback("before", result)
-- run all the "before" blocks/functions
for _, a in ipairs(ancestors) do
if contexts[a].before then
setfenv(contexts[a].before, env)
contexts[a].before()
end
end

-- check if it's a function because pending tests will just have "true"
if type(v.test) == "function" then
result.status_code, result.assertions_invoked, result.message = invoke_test(v.test)
invoke_callback(status_names[result.status_code], result)
else
result.status_code = status_codes.pending
invoke_callback("pending", result)
end
result.status_label = status_labels[result.status_code]

-- Run all the "after" blocks/functions
table.reverse(ancestors)
for _, a in ipairs(ancestors) do
if contexts[a].after then
setfenv(contexts[a].after, env)
contexts[a].after()
end
end

invoke_callback("after", result)
results[i] = result
end

return results

end

--- Return a detailed report for each context, with the status of each test.
-- @param contexts The contexts returned by <tt>load_contexts</tt>.
-- @param results The results returned by <tt>run</tt>.
-- @function test_report
local function test_report(contexts, results)

local buffer = {}
local leading_space = " "
local level = 0
local line_char = "-"
local previous_level = 0
local status_format_len = 3
local status_format = "[%s]"
local width = 72
local context_name_format = "%-" .. width - status_format_len .. "s"
local function_name_format = "%-" .. width - status_format_len .. "s"

local function space()
return leading_space:rep(level - 1)
end

local function add_divider()
table.insert(buffer, line_char:rep(width))
end
add_divider()
for i, item in ipairs(contexts) do
local ancestors = ancestors(i, contexts)
previous_level = level or 0
level = #ancestors
-- the 4 here is the length of "..." plus one space of padding
local name = truncate_string(item.name, width - status_format_len - 4 - #ancestors, '...')
if previous_level ~= level and level == 0 then add_divider() end
if item.context then
table.insert(buffer, context_name_format:format(space() .. name .. ':'))
elseif results[i] then
table.insert(buffer, function_name_format:format(space() .. name) ..
status_format:format(results[i].status_label))
end
end
add_divider()
return table.concat(buffer, "\n")

end

--- Return a table of stack traces for tests which produced a failure or an error.
-- @param contexts The contexts returned by <tt>load_contexts</tt>.
-- @param results The results returned by <tt>run</tt>.
-- @function error_report
local function error_report(contexts, results)
local buffer = {}
for _, r in filter(results, function(i, r) return r.message end) do
local name = contexts[r.test].name
table.insert(buffer, name .. ":\n" .. r.message[1] .. "\n" .. r.message[2])
end
if #buffer > 0 then return table.concat(buffer, "\n") end
end

--- Get a one-line report and a summary table with the status counts. The
-- counts given are: total tests, assertions, passed tests, failed tests,
-- pending tests, and tests which didn't assert anything.
-- @return A report that can be printed
-- @return A table with the various counts. Its fields are:
-- <tt>assertions</tt>, <tt>errors</tt>, <tt>failed</tt>, <tt>passed</tt>,
-- <tt>pending</tt>, <tt>tests</tt>, <tt>unassertive</tt>.
-- @param contexts The contexts returned by <tt>load_contexts</tt>.
-- @param results The results returned by <tt>run</tt>.
-- @function summary_report
local function summary_report(contexts, results)
local r = {
assertions = 0,
errors = 0,
failed = 0,
passed = 0,
pending = 0,
tests = 0,
unassertive = 0
}
for _, v in pairs(results) do
r.tests = r.tests + 1
r.assertions = r.assertions + v.assertions_invoked
if v.status_code == status_codes.err then r.errors = r.errors + 1
elseif v.status_code == status_codes.fail then r.failed = r.failed + 1
elseif v.status_code == status_codes.pass then r.passed = r.passed + 1
elseif v.status_code == status_codes.pending then r.pending = r.pending + 1
elseif v.status_code == status_codes.unassertive then r.unassertive = r.unassertive + 1
end
end
local buffer = {}
for _, k in ipairs({"tests", "passed", "assertions", "failed", "errors", "unassertive", "pending"}) do
local number = r[k]
local label = k
if number == 1 then
label = label:gsub("s$", "")
end
table.insert(buffer, ("%d %s"):format(number, label))
end
return table.concat(buffer, " "), r
end

_M.after_aliases = after_aliases
_M.make_assertion = make_assertion
_M.assertion_message_prefix = assertion_message_prefix
_M.before_aliases = before_aliases
_M.context_aliases = context_aliases
_M.error_report = error_report
_M.load_contexts = load_contexts
_M.run = run
_M.test_report = test_report
_M.status_codes = status_codes
_M.status_labels = status_labels
_M.summary_report = summary_report
_M.test_aliases = test_aliases
_M.version = _VERSION
_M._VERSION = _VERSION

return _M

+ 22
- 0
test/lua/tests.lua View File

@@ -0,0 +1,22 @@
-- Run all unit tests in 'unit' directory

local telescope = require "telescope"

local contexts = {}

for _,t in ipairs(tests_list) do
telescope.load_contexts(t, contexts)
end
local buffer = {}
local results = telescope.run(contexts, callbacks, test_pattern)
local summary, data = telescope.summary_report(contexts, results)
table.insert(buffer, telescope.test_report(contexts, results))

if #buffer > 0 then print(table.concat(buffer, "\n")) end

for _, v in pairs(results) do
if v.status_code == telescope.status_codes.err or
v.status_code == telescope.status_codes.fail then
os.exit(1)
end
end

test/lua/rsa.lua → test/lua/unit/rsa.lua View File


test/lua/test.data → test/lua/unit/test.data View File


test/lua/testkey → test/lua/unit/testkey View File


test/lua/testkey.pub → test/lua/unit/testkey.pub View File


+ 64
- 23
test/rspamd_lua_test.c View File

@@ -26,54 +26,95 @@
#include "util.h"
#include "lua/lua_common.h"

static const char *lua_src = "./lua";
static const char *lua_src = BUILDROOT "/test/lua/tests.lua";

static int
traceback (lua_State *L)
{
if (!lua_isstring (L, 1)) {
return 1;
}

lua_getfield (L, LUA_GLOBALSINDEX, "debug");

if (!lua_istable(L, -1)) {
lua_pop(L, 1);
return 1;
}

lua_getfield (L, -1, "traceback");

if (!lua_isfunction(L, -1)) {
lua_pop(L, 2);
return 1;
}
lua_pushvalue (L, 1);
lua_pushinteger (L, 2);
lua_call(L, 2, 1);

return 1;
}

void
rspamd_lua_test_func (int argc, char **argv)
rspamd_lua_test_func (void)
{
lua_State *L = rspamd_lua_init (NULL);
gchar rp[PATH_MAX], path_buf[PATH_MAX];
gchar *rp, rp_buf[PATH_MAX], path_buf[PATH_MAX], *tmp, *dir, *pattern;
const gchar *old_path;
guint i;
glob_t globbuf;
gint i, len;

msg_info ("Starting lua tests");
rspamd_printf ("Starting lua tests\n");

if (realpath (lua_src, rp) == NULL) {
msg_err ("cannod find path %s: %s", lua_src, strerror (errno));
if ((rp = realpath (lua_src, rp_buf)) == NULL) {
msg_err ("cannot find path %s: %s",
lua_src, strerror (errno));
g_assert (0);
}

tmp = g_strdup (rp);
dir = dirname (tmp);
/* Set lua path */
lua_getglobal (L, "package");
lua_getfield (L, -1, "path");
old_path = luaL_checkstring (L, -1);

rspamd_snprintf (path_buf, sizeof (path_buf), "%s;%s/?.lua;%s/busted/?.lua",
old_path, rp, rp);
rspamd_snprintf (path_buf, sizeof (path_buf), "%s;%s/?.lua;%s/unit/?.lua",
old_path, dir, dir);
lua_pop (L, 1);
lua_pushstring (L, path_buf);
lua_setfield (L, -2, "path");
lua_pop (L, 1);

lua_getglobal (L, "arg");
lua_newtable (L);

if (lua_type (L, -1) != LUA_TTABLE) {
lua_newtable (L);
}
globbuf.gl_offs = 0;
len = strlen (dir) + sizeof ("/unit/") + sizeof ("*.lua");
pattern = g_malloc (len);
rspamd_snprintf (pattern, len, "%s/unit/%s", dir, "*.lua");

for (i = 0; i < argc - 1; i ++) {
lua_pushinteger (L, i + 1);
lua_pushstring (L, argv[i]);
lua_settable (L, -3);
if (glob (pattern, GLOB_DOOFFS, NULL, &globbuf) == 0) {
for (i = 0; i < (gint)globbuf.gl_pathc; i++) {
lua_pushinteger (L, i + 1);
lua_pushstring (L, globbuf.gl_pathv[i]);
lua_settable (L, -3);
}
globfree (&globbuf);
g_free (pattern);
}
else {
msg_err ("pattern %s doesn't match: %s", pattern,
strerror (errno));
g_assert (0);
}

lua_setglobal (L, "arg");
lua_pop (L, 1);
lua_setglobal (L, "tests_list");

lua_pushcfunction (L, traceback);
luaL_loadfile (L, rp);

rspamd_snprintf (path_buf, sizeof (path_buf),
"require 'busted.runner'({ batch = true })");
if (luaL_dostring (L, path_buf) != 0) {
rspamd_fprintf (stderr, "run test failed: %s", lua_tostring (L, -1));
if (lua_pcall (L, 0, 0, lua_gettop (L) - 1) != 0) {
msg_err ("run test failed: %s", lua_tostring (L, -1));
g_assert (0);
}


+ 16
- 19
test/rspamd_test_suite.c View File

@@ -13,39 +13,35 @@ main (int argc, char **argv)
{
struct rspamd_config *cfg;

if (argc > 0 && strcmp (argv[1], "lua") == 0) {
/* Special lua testing mode */
rspamd_lua_test_func (argc - 1, &argv[2]);
}
rspamd_main = (struct rspamd_main *)g_malloc (sizeof (struct rspamd_main));
memset (rspamd_main, 0, sizeof (struct rspamd_main));
rspamd_main->server_pool = rspamd_mempool_new (rspamd_mempool_suggest_size ());
rspamd_main->cfg = (struct rspamd_config *)g_malloc (sizeof (struct rspamd_config));
cfg = rspamd_main->cfg;
bzero (cfg, sizeof (struct rspamd_config));
cfg->cfg_pool = rspamd_mempool_new (rspamd_mempool_suggest_size ());
cfg->log_type = RSPAMD_LOG_CONSOLE;
cfg->log_level = G_LOG_LEVEL_INFO;

g_test_init (&argc, &argv, NULL);
rspamd_set_logger (cfg, g_quark_from_static_string("rspamd-test"), rspamd_main);
(void)rspamd_log_open (rspamd_main->logger);

rspamd_main = (struct rspamd_main *)g_malloc (sizeof (struct rspamd_main));
g_test_init (&argc, &argv, NULL);

#if ((GLIB_MAJOR_VERSION == 2) && (GLIB_MINOR_VERSION <= 30))
g_thread_init (NULL);
#endif

g_mime_init (0);
memset (rspamd_main, 0, sizeof (struct rspamd_main));
rspamd_main->server_pool = rspamd_mempool_new (rspamd_mempool_suggest_size ());
rspamd_main->cfg = (struct rspamd_config *)g_malloc (sizeof (struct rspamd_config));
cfg = rspamd_main->cfg;
bzero (cfg, sizeof (struct rspamd_config));
cfg->cfg_pool = rspamd_mempool_new (rspamd_mempool_suggest_size ());

base = event_init ();

if (g_test_verbose ()) {
cfg->log_level = G_LOG_LEVEL_DEBUG;
rspamd_set_logger (cfg, g_quark_from_static_string("rspamd-test"), rspamd_main);
(void)rspamd_log_reopen (rspamd_main->logger);
}
else {
cfg->log_level = G_LOG_LEVEL_INFO;
}
cfg->log_type = RSPAMD_LOG_CONSOLE;
/* First set logger to console logger */
rspamd_set_logger (cfg, g_quark_from_static_string("rspamd-test"), rspamd_main);
(void)rspamd_log_open (rspamd_main->logger);

g_log_set_default_handler (rspamd_glib_log_function, rspamd_main->logger);

g_test_add_func ("/rspamd/mem_pool", rspamd_mem_pool_test_func);
@@ -61,6 +57,7 @@ main (int argc, char **argv)
g_test_add_func ("/rspamd/upstream", rspamd_upstream_test_func);
g_test_add_func ("/rspamd/shingles", rspamd_shingles_test_func);
g_test_add_func ("/rspamd/http", rspamd_http_test_func);
g_test_add_func ("/rspamd/lua", rspamd_lua_test_func);

g_test_run ();


+ 1
- 1
test/tests.h View File

@@ -41,6 +41,6 @@ void rspamd_shingles_test_func (void);

void rspamd_http_test_func (void);

void rspamd_lua_test_func (int argc, char **argv);
void rspamd_lua_test_func (void);

#endif

Loading…
Cancel
Save