local ffi = require('ffi') local formatc = require('test.unit.formatc') local Set = require('test.unit.set') local Preprocess = require('test.unit.preprocess') local Paths = require('test.config.paths') local global_helpers = require('test.helpers') local assert = require('luassert') local say = require('say') local posix = nil local syscall = nil local check_cores = global_helpers.check_cores local neq = global_helpers.neq local map = global_helpers.map local eq = global_helpers.eq local ok = global_helpers.ok -- C constants. local NULL = ffi.cast('void*', 0) local OK = 1 local FAIL = 0 local cimport -- add some standard header locations for _, p in ipairs(Paths.include_paths) do Preprocess.add_to_include_path(p) end local child_pid = nil local function only_separate(func) return function(...) if child_pid ~= 0 then error('This function must be run in a separate process only') end return func(...) end end local deferred_calls_init = {} local deferred_calls_mod = nil local function deferred_call(func, ret) return function(...) local deferred_calls = deferred_calls_mod or deferred_calls_init if child_pid ~= 0 then deferred_calls[#deferred_calls + 1] = {func=func, args={...}} return ret else return func(...) end end end local separate_cleanups_mod = nil local function separate_cleanup(func) return function(...) local separate_cleanups = separate_cleanups_mod if child_pid ~= 0 then separate_cleanups[#separate_cleanups + 1] = {args={...}} else func(...) end end end local libnvim = nil local lib = setmetatable({}, { __index = only_separate(function(_, idx) return libnvim[idx] end), __newindex = deferred_call(function(_, idx, val) libnvim[idx] = val end), }) local init = only_separate(function() -- load neovim shared library libnvim = ffi.load(Paths.test_libnvim_path) for _, c in ipairs(deferred_calls_init) do c.func(unpack(c.args)) end libnvim.time_init() libnvim.early_init() libnvim.event_init() if deferred_calls_mod then for _, c in ipairs(deferred_calls_mod) do c.func(unpack(c.args)) end end end) local deinit = only_separate(function() if separate_cleanups_mod then for _, c in ipairs(separate_cleanups_mod) do c.func(unpack(c.args)) end end end) local function trim(s) return s:match('^%s*(.*%S)') or '' end -- a Set that keeps around the lines we've already seen local cdefs = Set:new() local imported = Set:new() local pragma_pack_id = 1 -- some things are just too complex for the LuaJIT C parser to digest. We -- usually don't need them anyway. local function filter_complex_blocks(body) local result = {} for line in body:gmatch("[^\r\n]+") do if not (string.find(line, "(^)", 1, true) ~= nil or string.find(line, "_ISwupper", 1, true) or string.find(line, "msgpack_zone_push_finalizer") or string.find(line, "msgpack_unpacker_reserve_buffer") or string.find(line, "UUID_NULL") -- static const uuid_t UUID_NULL = {...} or string.find(line, "inline _Bool")) then result[#result + 1] = line end end return table.concat(result, "\n") end local cdef = ffi.cdef local cimportstr local previous_defines_init = '' local preprocess_cache_init = {} local previous_defines_mod = '' local preprocess_cache_mod = nil -- use this helper to import C files, you can pass multiple paths at once, -- this helper will return the C namespace of the nvim library. cimport = function(...) local previous_defines, preprocess_cache if preprocess_cache_mod then preprocess_cache = preprocess_cache_mod previous_defines = previous_defines_mod else preprocess_cache = preprocess_cache_init previous_defines = previous_defines_init end for _, path in ipairs({...}) do if not (path:sub(1, 1) == '/' or path:sub(1, 1) == '.' or path:sub(2, 2) == ':') then path = './' .. path end if not preprocess_cache[path] then local body body, previous_defines = Preprocess.preprocess(previous_defines, path) -- format it (so that the lines are "unique" statements), also filter out -- Objective-C blocks if os.getenv('NVIM_TEST_PRINT_I') == '1' then local lnum = 0 for line in body:gmatch('[^\n]+') do lnum = lnum + 1 print(lnum, line) end end body = formatc(body) body = filter_complex_blocks(body) preprocess_cache[path] = body end cimportstr(preprocess_cache, path) end return lib end local cimport_immediate = function(...) local saved_pid = child_pid child_pid = 0 local err, emsg = pcall(cimport, ...) child_pid = saved_pid if not err then emsg = tostring(emsg) io.stderr:write(emsg .. '\n') assert(false) else return lib end end cimportstr = deferred_call(function(preprocess_cache, path) if imported:contains(path) then return lib end local body = preprocess_cache[path] -- add the formatted lines to a set local new_cdefs = Set:new() for line in body:gmatch("[^\r\n]+") do line = trim(line) -- give each #pragma pack an unique id, so that they don't get removed -- if they are inserted into the set -- (they are needed in the right order with the struct definitions, -- otherwise luajit has wrong memory layouts for the sturcts) if line:match("#pragma%s+pack") then line = line .. " // " .. pragma_pack_id pragma_pack_id = pragma_pack_id + 1 end new_cdefs:add(line) end -- subtract the lines we've already imported from the new lines, then add -- the new unique lines to the old lines (so they won't be imported again) new_cdefs:diff(cdefs) cdefs:union(new_cdefs) if new_cdefs:size() == 0 then -- if there's no new lines, just return return lib end -- request a sorted version of the new lines (same relative order as the -- original preprocessed file) and feed that to the LuaJIT ffi local new_lines = new_cdefs:to_table() if os.getenv('NVIM_TEST_PRINT_CDEF') == '1' then for lnum, line in ipairs(new_lines) do print(lnum, line) end end cdef(table.concat(new_lines, "\n")) imported:add(path) return lib end, lib) local function alloc_log_new() local log = { log={}, lib=cimport('./src/nvim/memory.h'), original_functions={}, null={['\0:is_null']=true}, } local allocator_functions = {'malloc', 'free', 'calloc', 'realloc'} function log:save_original_functions() for _, funcname in ipairs(allocator_functions) do if not self.original_functions[funcname] then self.original_functions[funcname] = self.lib['mem_' .. funcname] end end end log.save_original_functions = deferred_call(log.save_original_functions) function log:set_mocks() for _, k in ipairs(allocator_functions) do do local kk = k self.lib['mem_' .. k] = function(...) local log_entry = {func=kk, args={...}} self.log[#self.log + 1] = log_entry if kk == 'free' then self.original_functions[kk](...) else log_entry.ret = self.original_functions[kk](...) end for i, v in ipairs(log_entry.args) do if v == nil then -- XXX This thing thinks that {NULL} ~= {NULL}. log_entry.args[i] = self.null end end if self.hook then self:hook(log_entry) end if log_entry.ret then return log_entry.ret end end end end end log.set_mocks = deferred_call(log.set_mocks) function log:clear() self.log = {} end function log:check(exp) eq(exp, self.log) self:clear() end function log:restore_original_functions() -- Do nothing: set mocks live in a separate process return --[[ [ for k, v in pairs(self.original_functions) do [ self.lib['mem_' .. k] = v [ end ]] end function log:setup() log:save_original_functions() log:set_mocks() end function log:before_each() return end function log:after_each() log:restore_original_functions() end log:setup() return log end -- take a pointer to a C-allocated string and return an interned -- version while also freeing the memory local function internalize(cdata, len) ffi.gc(cdata, ffi.C.free) return ffi.string(cdata, len) end local cstr = ffi.typeof('char[?]') local function to_cstr(string) return cstr(#string + 1, string) end local sc if posix ~= nil then sc = { fork = posix.fork, pipe = posix.pipe, read = posix.read, write = posix.write, close = posix.close, wait = posix.wait, exit = posix._exit, } elseif syscall ~= nil then sc = { fork = syscall.fork, pipe = function() local ret = {syscall.pipe()} return ret[3], ret[4] end, read = function(rd, len) return rd:read(nil, len) end, write = function(wr, s) return wr:write(s) end, close = function(p) return p:close() end, wait = syscall.wait, exit = syscall.exit, } else cimport_immediate('./test/unit/fixtures/posix.h') sc = { fork = function() return tonumber(ffi.C.fork()) end, pipe = function() local ret = ffi.new('int[2]', {-1, -1}) ffi.errno(0) local res = ffi.C.pipe(ret) if (res ~= 0) then local err = ffi.errno(0) assert(res == 0, ("pipe() error: %u: %s"):format( err, ffi.string(ffi.C.strerror(err)))) end assert(ret[0] ~= -1 and ret[1] ~= -1) return ret[0], ret[1] end, read = function(rd, len) local ret = ffi.new('char[?]', len, {0}) local total_bytes_read = 0 ffi.errno(0) while total_bytes_read < len do local bytes_read = tonumber(ffi.C.read( rd, ffi.cast('void*', ret + total_bytes_read), len - total_bytes_read)) if bytes_read == -1 then local err = ffi.errno(0) if err ~= ffi.C.kPOSIXErrnoEINTR then assert(false, ("read() error: %u: %s"):format( err, ffi.string(ffi.C.strerror(err)))) end elseif bytes_read == 0 then break else total_bytes_read = total_bytes_read + bytes_read end end return ffi.string(ret, total_bytes_read) end, write = function(wr, s) local wbuf = to_cstr(s) local total_bytes_written = 0 ffi.errno(0) while total_bytes_written < #s do local bytes_written = tonumber(ffi.C.write( wr, ffi.cast('void*', wbuf + total_bytes_written), #s - total_bytes_written)) if bytes_written == -1 then local err = ffi.errno(0) if err ~= ffi.C.kPOSIXErrnoEINTR then assert(false, ("write() error: %u: %s"):format( err, ffi.string(ffi.C.strerror(err)))) end elseif bytes_written == 0 then break else total_bytes_written = total_bytes_written + bytes_written end end return total_bytes_written end, close = ffi.C.close, wait = function(pid) ffi.errno(0) while true do local r = ffi.C.waitpid(pid, nil, ffi.C.kPOSIXWaitWUNTRACED) if r == -1 then local err = ffi.errno(0) if err == ffi.C.kPOSIXErrnoECHILD then break elseif err ~= ffi.C.kPOSIXErrnoEINTR then assert(false, ("waitpid() error: %u: %s"):format( err, ffi.string(ffi.C.strerror(err)))) end else assert(r == pid) end end end, exit = ffi.C._exit, } end local function format_list(lst) local ret = '' for _, v in ipairs(lst) do if ret ~= '' then ret = ret .. ', ' end ret = ret .. assert:format({v, n=1})[1] end return ret end if os.getenv('NVIM_TEST_PRINT_SYSCALLS') == '1' then for k_, v_ in pairs(sc) do (function(k, v) sc[k] = function(...) local rets = {v(...)} io.stderr:write(('%s(%s) = %s\n'):format(k, format_list({...}), format_list(rets))) return unpack(rets) end end)(k_, v_) end end local function gen_itp(it) deferred_calls_mod = {} separate_cleanups_mod = {} preprocess_cache_mod = map(function(v) return v end, preprocess_cache_init) previous_defines_mod = previous_defines_init local function just_fail(_) return false end say:set('assertion.just_fail.positive', '%s') say:set('assertion.just_fail.negative', '%s') assert:register('assertion', 'just_fail', just_fail, 'assertion.just_fail.positive', 'assertion.just_fail.negative') local function itp(name, func, allow_failure) if allow_failure and os.getenv('NVIM_TEST_RUN_FAILING_TESTS') ~= '1' then -- FIXME Fix tests with this true return end it(name, function() local rd, wr = sc.pipe() child_pid = sc.fork() if child_pid == 0 then init() sc.close(rd) collectgarbage('stop') local err, emsg = pcall(func) collectgarbage('restart') emsg = tostring(emsg) if not err then sc.write(wr, ('-\n%05u\n%s'):format(#emsg, emsg)) deinit() sc.close(wr) sc.exit(1) else sc.write(wr, '+\n') deinit() sc.close(wr) sc.exit(0) end else sc.close(wr) sc.wait(child_pid) child_pid = nil local function check() local res = sc.read(rd, 2) eq(2, #res) if res == '+\n' then return end eq('-\n', res) local len_s = sc.read(rd, 5) local len = tonumber(len_s) neq(0, len) local err = sc.read(rd, len + 1) assert.just_fail(err) end local err, emsg = pcall(check) sc.close(rd) if not err then if allow_failure then io.stderr:write('Errorred out:\n' .. tostring(emsg) .. '\n') os.execute([[sh -c "source .ci/common/test.sh ; check_core_dumps --delete \"]] .. Paths.test_luajit_prg .. [[\""]]) else error(emsg) end end end end) end return itp end local function cppimport(path) return cimport(Paths.test_include_path .. '/' .. path) end cimport('./src/nvim/types.h', './src/nvim/main.h', './src/nvim/os/time.h') local module = { cimport = cimport, cppimport = cppimport, internalize = internalize, ok = ok, eq = eq, neq = neq, ffi = ffi, lib = lib, cstr = cstr, to_cstr = to_cstr, NULL = NULL, OK = OK, FAIL = FAIL, alloc_log_new = alloc_log_new, gen_itp = gen_itp, only_separate = only_separate, deferred_call = deferred_call, separate_cleanup = separate_cleanup, } return function(after_each) if after_each then after_each(function() check_cores(Paths.test_luajit_prg) end) end return module end