From e4cc017c5d44c1046e6afdfc1e68d756c8748b6b Mon Sep 17 00:00:00 2001 From: Vsevolod Stakhov Date: Sat, 28 Feb 2015 00:11:56 +0000 Subject: [PATCH] Rework lua tests one more time. --- config.h.in | 2 + test/CMakeLists.txt | 12 +- test/lua/compat_env.lua | 391 ++++++++++++++++++++ test/lua/telescope.lua | 621 ++++++++++++++++++++++++++++++++ test/lua/tests.lua | 22 ++ test/lua/{ => unit}/rsa.lua | 0 test/lua/{ => unit}/test.data | 0 test/lua/{ => unit}/testkey | 0 test/lua/{ => unit}/testkey.pub | 0 test/rspamd_lua_test.c | 87 +++-- test/rspamd_test_suite.c | 35 +- test/tests.h | 2 +- 12 files changed, 1127 insertions(+), 45 deletions(-) create mode 100644 test/lua/compat_env.lua create mode 100644 test/lua/telescope.lua create mode 100644 test/lua/tests.lua rename test/lua/{ => unit}/rsa.lua (100%) rename test/lua/{ => unit}/test.data (100%) rename test/lua/{ => unit}/testkey (100%) rename test/lua/{ => unit}/testkey.pub (100%) diff --git a/config.h.in b/config.h.in index e45f95f4c..37267d468 100644 --- a/config.h.in +++ b/config.h.in @@ -244,6 +244,8 @@ #define MODULES_NUM ${RSPAMD_MODULES_NUM} +#define BUILDROOT "${CMAKE_BINARY_DIR}" + /* sys/types */ #ifdef HAVE_SYS_TYPES_H #include diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index b4fa018d2..18a826026 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -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}" diff --git a/test/lua/compat_env.lua b/test/lua/compat_env.lua new file mode 100644 index 000000000..2ecd4b6be --- /dev/null +++ b/test/lua/compat_env.lua @@ -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 +--]] diff --git a/test/lua/telescope.lua b/test/lua/telescope.lua new file mode 100644 index 000000000..abe96bcf9 --- /dev/null +++ b/test/lua/telescope.lua @@ -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: http://github.com/norman/telescope#readme. +-- @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 status_codes 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 make_assertion. +-- +-- @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. +--

+-- The name will be used as the basis of the positive and negative assertions; +-- i.e., the name equal would be used to create the assertions +-- assert_equal and assert_not_equal. +--

+-- @param message The base message that will be shown. +--

+-- The assertion message is what is shown when the assertion fails. It will be +-- prefixed with the string in telescope.assertion_message_prefix. +-- The variables passed to telescope.make_assertion are interpolated +-- in the message string using string.format. When creating the +-- inverse assertion, the message is reused, with " to be " replaced +-- by " not to be ". Hence a recommended format is something like: +-- "%s to be similar to %s". +--

+-- @param func The assertion function itself. +--

+-- The assertion function can have any number of arguments. +--

+-- @usage make_assertion("equal", "%s to be equal to %s", function(a, b) +-- return a == b end) +-- @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 contexts 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 target. +-- If the optional contexts table argument is provided, then the +-- resulting contexts will be added to it. +--

+-- The resulting contexts table's structure is as follows: +--

+-- +-- { +-- {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}, +-- } +-- +-- @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 load_contexts. +-- @param callbacks A table of callback functions to be invoked before or after +-- various test states. +--

+-- There is a callback for each test status_code, and callbacks to run +-- before or after each test invocation regardless of outcome. +--

+-- +--

+-- 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. +--

+-- @param test_filter A function to filter tests that match only conditions that you specify. +--

+-- For example, the folling would allow you to run only tests whose name matches a pattern: +--

+--

+-- +-- function(t) return t.name:match("%s* lexer") end +-- +--

+-- @return A table of result tables. Each result table has the following +-- fields: +-- +-- @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 load_contexts. +-- @param results The results returned by run. +-- @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 load_contexts. +-- @param results The results returned by run. +-- @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: +-- assertions, errors, failed, passed, +-- pending, tests, unassertive. +-- @param contexts The contexts returned by load_contexts. +-- @param results The results returned by run. +-- @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 diff --git a/test/lua/tests.lua b/test/lua/tests.lua new file mode 100644 index 000000000..0f6b06b4b --- /dev/null +++ b/test/lua/tests.lua @@ -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 \ No newline at end of file diff --git a/test/lua/rsa.lua b/test/lua/unit/rsa.lua similarity index 100% rename from test/lua/rsa.lua rename to test/lua/unit/rsa.lua diff --git a/test/lua/test.data b/test/lua/unit/test.data similarity index 100% rename from test/lua/test.data rename to test/lua/unit/test.data diff --git a/test/lua/testkey b/test/lua/unit/testkey similarity index 100% rename from test/lua/testkey rename to test/lua/unit/testkey diff --git a/test/lua/testkey.pub b/test/lua/unit/testkey.pub similarity index 100% rename from test/lua/testkey.pub rename to test/lua/unit/testkey.pub diff --git a/test/rspamd_lua_test.c b/test/rspamd_lua_test.c index cd3b152cd..bdc23e91c 100644 --- a/test/rspamd_lua_test.c +++ b/test/rspamd_lua_test.c @@ -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); } diff --git a/test/rspamd_test_suite.c b/test/rspamd_test_suite.c index a70a0102f..1023c5cf0 100644 --- a/test/rspamd_test_suite.c +++ b/test/rspamd_test_suite.c @@ -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 (); diff --git a/test/tests.h b/test/tests.h index ae69d9d96..1d14bde4b 100644 --- a/test/tests.h +++ b/test/tests.h @@ -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 -- 2.39.5