--[[ General Lua Libraries for Lua 5.1, 5.2 & 5.3 Copyright (C) 2011-2018 stdlib authors ]] local typecheck have_typecheck, typecheck = pcall(require, 'typecheck') local inprocess = require 'specl.inprocess' local hell = require 'specl.shell' local std = require 'specl.std' badargs = require 'specl.badargs' local top_srcdir = os.getenv 'top_srcdir' or '.' local top_builddir = os.getenv 'top_builddir' or '.' package.path = std.package.normalize( top_builddir .. '/lib/?.lua', top_builddir .. '/lib/?/init.lua', top_srcdir .. '/lib/?.lua', top_srcdir .. '/lib/?/init.lua', package.path ) -- Allow user override of LUA binary used by hell.spawn, falling -- back to environment PATH search for 'lua' if nothing else works. local LUA = os.getenv 'LUA' or 'lua' -- Simplified version for specifications, does not support functable -- valued __len metamethod, so don't write examples that need that! function len(x) local __len = getmetatable(x) or {} if type(__len) == 'function' then return __len(x) end if type(x) ~= 'table' then return #x end local n = #x for i = 1, n do if x[i] == nil then return i -1 end end return n end -- Make sure we have a maxn even when _VERSION ~= 5.1 -- @fixme remove this when we get unpack from specl.std maxn = table.maxn or function(t) local n = 0 for k in pairs(t) do if type(k) == 'number' and k > n then n = k end end return n end pack = table.pack or function(...) return {n=select('#', ...), ...} end -- Take care to always unpack upto the highest numeric index, for -- consistency across Lua versions. local _unpack = table.unpack or unpack -- @fixme pick this up from specl.std with the next release function unpack(t, i, j) return _unpack(t, tonumber(i) or 1, tonumber(j or t.n or len(t))) end -- In case we're not using a bleeding edge release of Specl... _diagnose = badargs.diagnose badargs.diagnose = function(...) if have_typecheck then return _diagnose(...) end end badargs.result = badargs.result or function(fname, i, want, got) if want == nil then -- numbers only for narg error i, want = i - 1, i end if got == nil and type(want) == 'number' then local s = "bad result #%d from '%s'(no more than %d result%s expected, got %d)" return s:format(i + 1, fname, i, i == 1 and '' or 's', want) end local function showarg(s) return('|' .. s .. '|'): gsub('|%?', '|nil|'): gsub('|nil|', '|no value|'): gsub('|any|', '|any value|'): gsub('|#', '|non-empty '): gsub('|func|', '|function|'): gsub('|file|', '|FILE*|'): gsub('^|', ''): gsub('|$', ''): gsub('|([^|]+)$', 'or %1'): gsub('|', ', ') end return string.format("bad result #%d from '%s'(%s expected, got %s)", i, fname, showarg(want), got or 'no value') end -- Wrap up badargs function in a succinct single call. function init(M, mname, fname) local name =(mname .. '.' .. fname):gsub('^%.', '') return M[fname], function(...) return badargs.format(name, ...) end, function(...) return badargs.result(name, ...) end end -- A copy of base.lua:type, so that an unloadable base.lua doesn't -- prevent everything else from working. function objtype(o) return(getmetatable(o) or {})._type or io.type(o) or type(o) end function nop() end -- Error message specifications use this to shorten argument lists. -- Copied from functional.lua to avoid breaking all tests if functional -- cannot be loaded correctly. function bind(f, fix) return function(...) local arg = {} for i, v in pairs(fix) do arg[i] = v end local i = 1 for _, v in pairs {...} do while arg[i] ~= nil do i = i + 1 end arg[i] = v end return f(unpack(arg)) end end local function mkscript(code) local f = os.tmpname() local h = io.open(f, 'w') h:write(code) h:close() return f end --- Run some Lua code with the given arguments and input. -- @string code valid Lua code -- @tparam[opt={}] string|table arg single argument, or table of -- arguments for the script invocation. -- @string[opt] stdin standard input contents for the script process -- @treturn specl.shell.Process|nil status of resulting process if -- execution was successful, otherwise nil function luaproc(code, arg, stdin) local f = mkscript(code) if type(arg) ~= 'table' then arg = {arg} end local cmd = {LUA, f, unpack(arg)} -- inject env and stdin keys separately to avoid truncating `...` in -- cmd constructor cmd.env = {LUA_PATH=package.path, LUA_INIT='', LUA_INIT_5_2=''} cmd.stdin = stdin local proc = hell.spawn(cmd) os.remove(f) return proc end --- Check deprecation output when calling a named function in the given module. -- Note that the script fragments passed in *argstr* and *objectinit* -- can reference the module table as `M`, and(where it would make sense) -- an object prototype as `P` and instance as `obj`. -- @param deprecate value of `std._debug.deprecate` -- @string module dot delimited module path to load -- @string fname name of a function in the table returned by requiring *module* -- @param[opt=''] args arguments to pass to *fname* call, must be stringifiable -- @string[opt=nil] objectinit object initializer to instantiate an -- object for object method deprecation check -- @treturn specl.shell.Process|nil status of resulting process if -- execution was successful, otherwise nil function deprecation(deprecate, module, fname, args, objectinit) args = args or '' local script if objectinit == nil then script = string.format([[ require 'std._debug'.deprecate = %s M = require '%s' P = M.prototype print(M.%s(%s)) ]], tostring(deprecate), module, fname, tostring(args)) else script = string.format([[ require 'std._debug'.deprecate = %s local M = require '%s' local P = M.prototype local obj = P(%s) print(obj:%s(%s)) ]], tostring(deprecate), module, objectinit, fname, tostring(args)) end return luaproc(script) end --- Concatenate the contents of listed existing files. -- @string ... names of existing files -- @treturn string concatenated contents of those files function concat_file_content(...) local t = {} for _, name in ipairs {...} do h = io.open(name) t[#t + 1] = h:read '*a' end return table.concat(t) end local function tabulate_output(code) local proc = luaproc(code) if proc.status ~= 0 then return error(proc.errout) end local r = {} proc.output:gsub('(%S*)[%s]*', function(x) if x ~= '' then r[x] = true end end) return r end --- Show changes to tables wrought by a require statement. -- There are a few modes to this function, controlled by what named -- arguments are given. Lists new keys in T1 after `require 'import'`: -- -- show_apis {added_to=T1, by=import} -- -- List keys returned from `require 'import'`, which have the same -- value in T1: -- -- show_apis {from=T1, used_by=import} -- -- List keys from `require 'import'`, which are also in T1 but with -- a different value: -- -- show_apis {from=T1, enhanced_by=import} -- -- List keys from T2, which are also in T1 but with a different value: -- -- show_apis {from=T1, enhanced_in=T2} -- -- @tparam table argt one of the combinations above -- @treturn table a list of keys according to criteria above function show_apis(argt) local added_to, from, not_in, enhanced_in, enhanced_after, by = argt.added_to, argt.from, argt.not_in, argt.enhanced_in, argt.enhanced_after, argt.by if added_to and by then return tabulate_output([[ local before, after = {}, {} for k in pairs(]] .. added_to .. [[) do before[k] = true end local M = require ']] .. by .. [[' for k in pairs(]] .. added_to .. [[) do after[k] = true end for k in pairs(after) do if not before[k] then print(k) end end ]]) elseif from and not_in then return tabulate_output([[ local _ENV = require 'std.normalize' { from = ']] .. from .. [[', M = require ']] .. not_in .. [[', } for k in pairs(M) do -- M[1] is typically the module namespace name, don't match -- that! if k ~= 1 and from[k] ~= M[k] then print(k) end end ]]) elseif from and enhanced_in then return tabulate_output([[ local _ENV = require 'std.normalize' { from = ']] .. from .. [[', M = require ']] .. enhanced_in .. [[', } for k, v in pairs(M) do if from[k] ~= M[k] and from[k] ~= nil then print(k) end end ]]) elseif from and enhanced_after then return tabulate_output([[ local _ENV = require 'std.normalize' { from = ']] .. from .. [[', } local before, after = {}, {} for k, v in pairs(from) do before[k] = v end ]] .. enhanced_after .. [[ for k, v in pairs(from) do after[k] = v end for k, v in pairs(before) do if after[k] ~= nil and after[k] ~= v then print(k) end end ]]) end assert(false, 'missing argument to show_apis') end -- Stub inprocess.capture if necessary; new in Specl 12. capture = inprocess.capture or function(f, arg) return nil, nil, f(unpack(arg or {})) end do -- Custom matcher for set size and set membership. local util = require 'specl.util' local matchers = require 'specl.matchers' local Matcher, matchers, q = matchers.Matcher, matchers.matchers, matchers.stringify matchers.have_size = Matcher { function(self, actual, expect) local size = 0 for _ in pairs(actual) do size = size + 1 end return size == expect end, actual = 'table', format_expect = function(self, expect) return ' a table containing ' .. expect .. ' elements, ' end, format_any_of = function(self, alternatives) return ' a table with any of ' .. util.concat(alternatives, util.QUOTED) .. ' elements, ' end, } matchers.have_member = Matcher { function(self, actual, expect) return actual[expect] ~= nil end, actual = 'set', format_expect = function(self, expect) return ' a set containing ' .. q(expect) .. ', ' end, format_any_of = function(self, alternatives) return ' a set containing any of ' .. util.concat(alternatives, util.QUOTED) .. ', ' end, } -- Alias that doesn't tickle sc_error_message_uppercase. matchers.raise = matchers.error end