aboutsummaryrefslogtreecommitdiff
path: root/test/unit/testutil.lua
diff options
context:
space:
mode:
Diffstat (limited to 'test/unit/testutil.lua')
-rw-r--r--test/unit/testutil.lua912
1 files changed, 912 insertions, 0 deletions
diff --git a/test/unit/testutil.lua b/test/unit/testutil.lua
new file mode 100644
index 0000000000..a6db7beab1
--- /dev/null
+++ b/test/unit/testutil.lua
@@ -0,0 +1,912 @@
+local ffi = require('ffi')
+local formatc = require('test.unit.formatc')
+local Set = require('test.unit.set')
+local Preprocess = require('test.unit.preprocess')
+local t_global = require('test.testutil')
+local paths = t_global.paths
+local assert = require('luassert')
+local say = require('say')
+
+local check_cores = t_global.check_cores
+local dedent = t_global.dedent
+local neq = t_global.neq
+local map = vim.tbl_map
+local eq = t_global.eq
+local trim = vim.trim
+
+-- add some standard header locations
+for _, p in ipairs(paths.include_paths) do
+ Preprocess.add_to_include_path(p)
+end
+
+local child_pid = nil --- @type integer?
+--- @generic F: function
+--- @param func F
+--- @return F
+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
+
+--- @class ChildCall
+--- @field func function
+--- @field args any[]
+
+--- @class ChildCallLog
+--- @field func string
+--- @field args any[]
+--- @field ret any?
+
+local child_calls_init = {} --- @type ChildCall[]
+local child_calls_mod = nil --- @type ChildCall[]
+local child_calls_mod_once = nil --- @type ChildCall[]?
+
+local function child_call(func, ret)
+ return function(...)
+ local child_calls = child_calls_mod or child_calls_init
+ if child_pid ~= 0 then
+ child_calls[#child_calls + 1] = { func = func, args = { ... } }
+ return ret
+ else
+ return func(...)
+ end
+ end
+end
+
+-- Run some code at the start of the child process, before running the test
+-- itself. Is supposed to be run in `before_each`.
+--- @param func function
+local function child_call_once(func, ...)
+ if child_pid ~= 0 then
+ child_calls_mod_once[#child_calls_mod_once + 1] = { func = func, args = { ... } }
+ else
+ func(...)
+ end
+end
+
+local child_cleanups_mod_once = nil --- @type ChildCall[]?
+
+-- Run some code at the end of the child process, before exiting. Is supposed to
+-- be run in `before_each` because `after_each` is run after child has exited.
+local function child_cleanup_once(func, ...)
+ local child_cleanups = child_cleanups_mod_once
+ if child_pid ~= 0 then
+ child_cleanups[#child_cleanups + 1] = { func = func, args = { ... } }
+ else
+ func(...)
+ end
+end
+
+-- Unittests are run from debug nvim binary in lua interpreter mode.
+local libnvim = ffi.C
+
+local lib = setmetatable({}, {
+ __index = only_separate(function(_, idx)
+ return libnvim[idx]
+ end),
+ __newindex = child_call(function(_, idx, val)
+ libnvim[idx] = val
+ end),
+})
+
+local init = only_separate(function()
+ for _, c in ipairs(child_calls_init) do
+ c.func(unpack(c.args))
+ end
+ libnvim.event_init()
+ libnvim.early_init(nil)
+ if child_calls_mod then
+ for _, c in ipairs(child_calls_mod) do
+ c.func(unpack(c.args))
+ end
+ end
+ if child_calls_mod_once then
+ for _, c in ipairs(child_calls_mod_once) do
+ c.func(unpack(c.args))
+ end
+ child_calls_mod_once = nil
+ end
+end)
+
+local deinit = only_separate(function()
+ if child_cleanups_mod_once then
+ for _, c in ipairs(child_cleanups_mod_once) do
+ c.func(unpack(c.args))
+ end
+ child_cleanups_mod_once = nil
+ end
+end)
+
+-- a Set that keeps around the lines we've already seen
+local cdefs_init = Set:new()
+local cdefs_mod = nil
+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.
+--- @param body string
+local function filter_complex_blocks(body)
+ local result = {} --- @type string[]
+
+ 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, '_Float')
+ or string.find(line, '__s128')
+ or string.find(line, '__u128')
+ or string.find(line, 'msgpack_zone_push_finalizer')
+ or string.find(line, 'msgpack_unpacker_reserve_buffer')
+ or string.find(line, 'value_init_')
+ or string.find(line, 'UUID_NULL') -- static const uuid_t UUID_NULL = {...}
+ or string.find(line, 'inline _Bool')
+ -- used by macOS headers
+ or string.find(line, 'typedef enum : ')
+ or string.find(line, 'mach_vm_range_recipe')
+ )
+ then
+ result[#result + 1] = line
+ end
+ end
+
+ return table.concat(result, '\n')
+end
+
+local cdef = ffi.cdef
+
+local cimportstr
+
+local previous_defines_init = [[
+typedef struct { char bytes[16]; } __attribute__((aligned(16))) __uint128_t;
+typedef struct { char bytes[16]; } __attribute__((aligned(16))) __float128;
+]]
+
+local preprocess_cache_init = {} --- @type table<string,string>
+local previous_defines_mod = ''
+local preprocess_cache_mod = nil --- @type table<string,string>
+
+local function is_child_cdefs()
+ return os.getenv('NVIM_TEST_MAIN_CDEFS') ~= '1'
+end
+
+-- 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.
+local function cimport(...)
+ local previous_defines --- @type string
+ local preprocess_cache --- @type table<string,string>
+ local cdefs
+ if is_child_cdefs() and preprocess_cache_mod then
+ preprocess_cache = preprocess_cache_mod
+ previous_defines = previous_defines_mod
+ cdefs = cdefs_mod
+ else
+ preprocess_cache = preprocess_cache_init
+ previous_defines = previous_defines_init
+ cdefs = cdefs_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 --- @type string
+ 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)
+ -- 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 a 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 structs)
+ if line:match('#pragma%s+pack') then
+ --- @type string
+ 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)
+ -- 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
+ body = table.concat(new_lines, '\n')
+
+ preprocess_cache[path] = body
+ end
+ cimportstr(preprocess_cache, path)
+ end
+ return lib
+end
+
+local function cimport_immediate(...)
+ local saved_pid = child_pid
+ child_pid = 0
+ local err, emsg = pcall(cimport, ...)
+ child_pid = saved_pid
+ if not err then
+ io.stderr:write(tostring(emsg) .. '\n')
+ assert(false)
+ else
+ return lib
+ end
+end
+
+--- @param preprocess_cache table<string,string[]>
+--- @param path string
+local function _cimportstr(preprocess_cache, path)
+ if imported:contains(path) then
+ return lib
+ end
+ local body = preprocess_cache[path]
+ if body == '' then
+ return lib
+ end
+ cdef(body)
+ imported:add(path)
+
+ return lib
+end
+
+if is_child_cdefs() then
+ cimportstr = child_call(_cimportstr, lib)
+else
+ cimportstr = _cimportstr
+end
+
+local function alloc_log_new()
+ local log = {
+ log = {}, --- @type ChildCallLog[]
+ lib = cimport('./src/nvim/memory.h'), --- @type table<string,function>
+ original_functions = {}, --- @type table<string,function>
+ 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 = child_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(...)
+ --- @type ChildCallLog
+ 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 = child_call(log.set_mocks)
+
+ function log:clear()
+ self.log = {}
+ end
+
+ function log:check(exp)
+ eq(exp, self.log)
+ self:clear()
+ end
+
+ function log:clear_tmp_allocs(clear_null_frees)
+ local toremove = {} --- @type integer[]
+ local allocs = {} --- @type table<string,integer>
+ for i, v in ipairs(self.log) do
+ if v.func == 'malloc' or v.func == 'calloc' then
+ allocs[tostring(v.ret)] = i
+ elseif v.func == 'realloc' or v.func == 'free' then
+ if allocs[tostring(v.args[1])] then
+ toremove[#toremove + 1] = allocs[tostring(v.args[1])]
+ if v.func == 'free' then
+ toremove[#toremove + 1] = i
+ end
+ elseif clear_null_frees and v.args[1] == self.null then
+ toremove[#toremove + 1] = i
+ end
+ if v.func == 'realloc' then
+ allocs[tostring(v.ret)] = i
+ end
+ end
+ end
+ table.sort(toremove)
+ for i = #toremove, 1, -1 do
+ table.remove(self.log, toremove[i])
+ end
+ end
+
+ function log:setup()
+ log:save_original_functions()
+ log:set_mocks()
+ end
+
+ function log:before_each() end
+
+ function log:after_each() 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
+
+cimport_immediate('./test/unit/fixtures/posix.h')
+
+local sc = {}
+
+function sc.fork()
+ return tonumber(ffi.C.fork())
+end
+
+function sc.pipe()
+ 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
+
+--- @return string
+function sc.read(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
+
+function sc.write(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 ('%s')"):format(err, ffi.string(ffi.C.strerror(err)), s)
+ )
+ end
+ elseif bytes_written == 0 then
+ break
+ else
+ total_bytes_written = total_bytes_written + bytes_written
+ end
+ end
+ return total_bytes_written
+end
+
+sc.close = ffi.C.close
+
+--- @param pid integer
+--- @return integer
+function sc.wait(pid)
+ ffi.errno(0)
+ local stat_loc = ffi.new('int[1]', { 0 })
+ while true do
+ local r = ffi.C.waitpid(pid, stat_loc, 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
+ return stat_loc[0]
+end
+
+sc.exit = ffi.C._exit
+
+--- @param lst string[]
+--- @return string
+local function format_list(lst)
+ local ret = {} --- @type string[]
+ for _, v in ipairs(lst) do
+ ret[#ret + 1] = assert:format({ v, n = 1 })[1]
+ end
+ return table.concat(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 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 hook_fnamelen = 30
+local hook_sfnamelen = 30
+local hook_numlen = 5
+local hook_msglen = 1 + 1 + 1 + (1 + hook_fnamelen) + (1 + hook_sfnamelen) + (1 + hook_numlen) + 1
+
+local tracehelp = dedent([[
+ Trace: either in the format described below or custom debug output starting
+ with `>`. Latter lines still have the same width in byte.
+
+ ┌ Trace type: _r_eturn from function , function _c_all, _l_ine executed,
+ │ _t_ail return, _C_ount (should not actually appear),
+ │ _s_aved from previous run for reference, _>_ for custom debug
+ │ output.
+ │┏ Function type: _L_ua function, _C_ function, _m_ain part of chunk,
+ │┃ function that did _t_ail call.
+ │┃┌ Function name type: _g_lobal, _l_ocal, _m_ethod, _f_ield, _u_pvalue,
+ │┃│ space for unknown.
+ │┃│ ┏ Source file name ┌ Function name ┏ Line
+ │┃│ ┃ (trunc to 30 bytes, no .lua) │ (truncated to last 30 bytes) ┃ number
+ CWN SSSSSSSSSSSSSSSSSSSSSSSSSSSSSS:FFFFFFFFFFFFFFFFFFFFFFFFFFFFFF:LLLLL\n
+]])
+
+local function child_sethook(wr)
+ local trace_level_str = os.getenv('NVIM_TEST_TRACE_LEVEL')
+ local trace_level = 0
+ if trace_level_str and trace_level_str ~= '' then
+ --- @type number
+ trace_level = assert(tonumber(trace_level_str))
+ end
+
+ if trace_level <= 0 then
+ return
+ end
+
+ local trace_only_c = trace_level <= 1
+ --- @type debuginfo?, string?, integer
+ local prev_info, prev_reason, prev_lnum
+
+ --- @param reason string
+ --- @param lnum integer
+ --- @param use_prev boolean
+ local function hook(reason, lnum, use_prev)
+ local info = nil --- @type debuginfo?
+ if use_prev then
+ info = prev_info
+ elseif reason ~= 'tail return' then -- tail return
+ info = debug.getinfo(2, 'nSl')
+ end
+
+ if trace_only_c and (not info or info.what ~= 'C') and not use_prev then
+ --- @cast info -nil
+ if info.source:sub(-9) == '_spec.lua' then
+ prev_info = info
+ prev_reason = 'saved'
+ prev_lnum = lnum
+ end
+ return
+ end
+ if trace_only_c and not use_prev and prev_reason then
+ hook(prev_reason, prev_lnum, true)
+ prev_reason = nil
+ end
+
+ local whatchar = ' '
+ local namewhatchar = ' '
+ local funcname = ''
+ local source = ''
+ local msgchar = reason:sub(1, 1)
+
+ if reason == 'count' then
+ msgchar = 'C'
+ end
+
+ if info then
+ funcname = (info.name or ''):sub(1, hook_fnamelen)
+ whatchar = info.what:sub(1, 1)
+ namewhatchar = info.namewhat:sub(1, 1)
+ if namewhatchar == '' then
+ namewhatchar = ' '
+ end
+ source = info.source
+ if source:sub(1, 1) == '@' then
+ if source:sub(-4, -1) == '.lua' then
+ source = source:sub(1, -5)
+ end
+ source = source:sub(-hook_sfnamelen, -1)
+ end
+ lnum = lnum or info.currentline
+ end
+
+ -- assert(-1 <= lnum and lnum <= 99999)
+ local lnum_s = lnum == -1 and 'nknwn' or ('%u'):format(lnum)
+ --- @type string
+ local msg = ( -- lua does not support %*
+ ''
+ .. msgchar
+ .. whatchar
+ .. namewhatchar
+ .. ' '
+ .. source
+ .. (' '):rep(hook_sfnamelen - #source)
+ .. ':'
+ .. funcname
+ .. (' '):rep(hook_fnamelen - #funcname)
+ .. ':'
+ .. ('0'):rep(hook_numlen - #lnum_s)
+ .. lnum_s
+ .. '\n'
+ )
+ -- eq(hook_msglen, #msg)
+ sc.write(wr, msg)
+ end
+ debug.sethook(hook, 'crl')
+end
+
+local trace_end_msg = ('E%s\n'):format((' '):rep(hook_msglen - 2))
+
+--- @type function
+local _debug_log
+
+local debug_log = only_separate(function(...)
+ return _debug_log(...)
+end)
+
+local function itp_child(wr, func)
+ --- @param s string
+ _debug_log = function(s)
+ s = s:sub(1, hook_msglen - 2)
+ sc.write(wr, '>' .. s .. (' '):rep(hook_msglen - 2 - #s) .. '\n')
+ end
+ local status, result = pcall(init)
+ if status then
+ collectgarbage('stop')
+ child_sethook(wr)
+ status, result = pcall(func)
+ debug.sethook()
+ end
+ sc.write(wr, trace_end_msg)
+ if not status then
+ local emsg = tostring(result)
+ if #emsg > 99999 then
+ emsg = emsg:sub(1, 99999)
+ end
+ sc.write(wr, ('-\n%05u\n%s'):format(#emsg, emsg))
+ deinit()
+ else
+ sc.write(wr, '+\n')
+ deinit()
+ end
+ collectgarbage('restart')
+ collectgarbage()
+ sc.write(wr, '$\n')
+ sc.close(wr)
+ sc.exit(status and 0 or 1)
+end
+
+local function check_child_err(rd)
+ local trace = {} --- @type string[]
+ local did_traceline = false
+ local maxtrace = tonumber(os.getenv('NVIM_TEST_MAXTRACE')) or 1024
+ while true do
+ local traceline = sc.read(rd, hook_msglen)
+ if #traceline ~= hook_msglen then
+ if #traceline == 0 then
+ break
+ else
+ trace[#trace + 1] = 'Partial read: <' .. trace .. '>\n'
+ end
+ end
+ if traceline == trace_end_msg then
+ did_traceline = true
+ break
+ end
+ trace[#trace + 1] = traceline
+ if #trace > maxtrace then
+ table.remove(trace, 1)
+ end
+ end
+ local res = sc.read(rd, 2)
+ if #res == 2 then
+ local err = ''
+ if res ~= '+\n' then
+ eq('-\n', res)
+ local len_s = sc.read(rd, 5)
+ local len = tonumber(len_s)
+ neq(0, len)
+ if os.getenv('NVIM_TEST_TRACE_ON_ERROR') == '1' and #trace ~= 0 then
+ --- @type string
+ err = '\nTest failed, trace:\n' .. tracehelp
+ for _, traceline in ipairs(trace) do
+ --- @type string
+ err = err .. traceline
+ end
+ end
+ --- @type string
+ err = err .. sc.read(rd, len + 1)
+ end
+ local eres = sc.read(rd, 2)
+ if eres ~= '$\n' then
+ if #trace == 0 then
+ err = '\nTest crashed, no trace available (check NVIM_TEST_TRACE_LEVEL)\n'
+ else
+ err = '\nTest crashed, trace:\n' .. tracehelp
+ for i = 1, #trace do
+ err = err .. trace[i]
+ end
+ end
+ if not did_traceline then
+ --- @type string
+ err = err .. '\nNo end of trace occurred'
+ end
+ local cc_err, cc_emsg = pcall(check_cores, paths.test_luajit_prg, true)
+ if not cc_err then
+ --- @type string
+ err = err .. '\ncheck_cores failed: ' .. cc_emsg
+ end
+ end
+ if err ~= '' then
+ assert.just_fail(err)
+ end
+ end
+end
+
+local function itp_parent(rd, pid, allow_failure, location)
+ local ok, emsg = pcall(check_child_err, rd)
+ local status = sc.wait(pid)
+ sc.close(rd)
+ if not ok then
+ if allow_failure then
+ io.stderr:write('Errorred out (' .. status .. '):\n' .. tostring(emsg) .. '\n')
+ os.execute([[
+ sh -c "source ci/common/test.sh
+ check_core_dumps --delete \"]] .. paths.test_luajit_prg .. [[\""]])
+ else
+ error(tostring(emsg) .. '\nexit code: ' .. status)
+ end
+ elseif status ~= 0 then
+ if not allow_failure then
+ error('child process errored out with status ' .. status .. '!\n\n' .. location)
+ end
+ end
+end
+
+local function gen_itp(it)
+ child_calls_mod = {}
+ child_calls_mod_once = {}
+ child_cleanups_mod_once = {}
+ preprocess_cache_mod = map(function(v)
+ return v
+ end, preprocess_cache_init)
+ previous_defines_mod = previous_defines_init
+ cdefs_mod = cdefs_init:copy()
+ 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
+
+ -- Pre-emptively calculating error location, wasteful, ugh!
+ -- But the way this code messes around with busted implies the real location is strictly
+ -- not available in the parent when an actual error occurs. so we have to do this here.
+ local location = debug.traceback()
+ it(name, function()
+ local rd, wr = sc.pipe()
+ child_pid = sc.fork()
+ if child_pid == 0 then
+ sc.close(rd)
+ itp_child(wr, func)
+ else
+ sc.close(wr)
+ local saved_child_pid = child_pid
+ child_pid = nil
+ itp_parent(rd, saved_child_pid, allow_failure, location)
+ end
+ end)
+ end
+ return itp
+end
+
+local function cppimport(path)
+ return cimport(paths.test_source_path .. '/test/includes/pre/' .. path)
+end
+
+cimport(
+ './src/nvim/types_defs.h',
+ './src/nvim/main.h',
+ './src/nvim/os/time.h',
+ './src/nvim/os/fs.h'
+)
+
+local function conv_enum(etab, eval)
+ local n = tonumber(eval)
+ return etab[n] or n
+end
+
+local function array_size(arr)
+ return ffi.sizeof(arr) / ffi.sizeof(arr[0])
+end
+
+local function kvi_size(kvi)
+ return array_size(kvi.init_array)
+end
+
+local function kvi_init(kvi)
+ kvi.capacity = kvi_size(kvi)
+ kvi.items = kvi.init_array
+ return kvi
+end
+
+local function kvi_destroy(kvi)
+ if kvi.items ~= kvi.init_array then
+ lib.xfree(kvi.items)
+ end
+end
+
+local function kvi_new(ct)
+ return kvi_init(ffi.new(ct))
+end
+
+local function make_enum_conv_tab(m, values, skip_pref, set_cb)
+ child_call_once(function()
+ local ret = {}
+ for _, v in ipairs(values) do
+ local str_v = v
+ if v:sub(1, #skip_pref) == skip_pref then
+ str_v = v:sub(#skip_pref + 1)
+ end
+ ret[tonumber(m[v])] = str_v
+ end
+ set_cb(ret)
+ end)
+end
+
+local function ptr2addr(ptr)
+ return tonumber(ffi.cast('intptr_t', ffi.cast('void *', ptr)))
+end
+
+local s = ffi.new('char[64]', { 0 })
+
+local function ptr2key(ptr)
+ ffi.C.snprintf(s, ffi.sizeof(s), '%p', ffi.cast('void *', ptr))
+ return ffi.string(s)
+end
+
+local function is_asan()
+ cimport('./src/nvim/version.h')
+ local status, res = pcall(function()
+ return lib.version_cflags
+ end)
+ if status then
+ return ffi.string(res):match('-fsanitize=[a-z,]*address')
+ else
+ return false
+ end
+end
+
+--- @class test.unit.testutil.module
+local M = {
+ cimport = cimport,
+ cppimport = cppimport,
+ internalize = internalize,
+ ffi = ffi,
+ lib = lib,
+ cstr = cstr,
+ to_cstr = to_cstr,
+ NULL = ffi.cast('void*', 0),
+ OK = 1,
+ FAIL = 0,
+ alloc_log_new = alloc_log_new,
+ gen_itp = gen_itp,
+ only_separate = only_separate,
+ child_call_once = child_call_once,
+ child_cleanup_once = child_cleanup_once,
+ sc = sc,
+ conv_enum = conv_enum,
+ array_size = array_size,
+ kvi_destroy = kvi_destroy,
+ kvi_size = kvi_size,
+ kvi_init = kvi_init,
+ kvi_new = kvi_new,
+ make_enum_conv_tab = make_enum_conv_tab,
+ ptr2addr = ptr2addr,
+ ptr2key = ptr2key,
+ debug_log = debug_log,
+ is_asan = is_asan,
+}
+--- @class test.unit.testutil: test.unit.testutil.module, test.testutil
+M = vim.tbl_extend('error', M, t_global)
+
+return M