neovim

Neovim text editor
git clone https://git.dasho.dev/neovim.git
Log | Files | Refs | README

testutil.lua (25886B)


      1 local ffi = require('ffi')
      2 local formatc = require('test.unit.formatc')
      3 local Set = require('test.unit.set')
      4 local Preprocess = require('test.unit.preprocess')
      5 local t_global = require('test.testutil')
      6 local paths = t_global.paths
      7 local assert = require('luassert')
      8 local say = require('say')
      9 
     10 local check_cores = t_global.check_cores
     11 local dedent = t_global.dedent
     12 local neq = t_global.neq
     13 local map = vim.tbl_map
     14 local eq = t_global.eq
     15 local trim = vim.trim
     16 
     17 -- add some standard header locations
     18 for _, p in ipairs(paths.include_paths) do
     19  Preprocess.add_to_include_path(p)
     20 end
     21 
     22 -- add some nonstandard header locations
     23 if paths.apple_sysroot ~= '' then
     24  Preprocess.add_apple_sysroot(paths.apple_sysroot)
     25 end
     26 
     27 local child_pid = nil --- @type integer?
     28 --- @generic F: function
     29 --- @param func F
     30 --- @return F
     31 local function only_separate(func)
     32  return function(...)
     33    if child_pid ~= 0 then
     34      error('This function must be run in a separate process only')
     35    end
     36    return func(...)
     37  end
     38 end
     39 
     40 --- @class ChildCall
     41 --- @field func function
     42 --- @field args any[]
     43 
     44 --- @class ChildCallLog
     45 --- @field func string
     46 --- @field args any[]
     47 --- @field ret any?
     48 
     49 local child_calls_init = {} --- @type ChildCall[]
     50 local child_calls_mod = nil --- @type ChildCall[]
     51 local child_calls_mod_once = nil --- @type ChildCall[]?
     52 
     53 local function child_call(func, ret)
     54  return function(...)
     55    local child_calls = child_calls_mod or child_calls_init
     56    if child_pid ~= 0 then
     57      child_calls[#child_calls + 1] = { func = func, args = { ... } }
     58      return ret
     59    else
     60      return func(...)
     61    end
     62  end
     63 end
     64 
     65 -- Run some code at the start of the child process, before running the test
     66 -- itself. Is supposed to be run in `before_each`.
     67 --- @param func function
     68 local function child_call_once(func, ...)
     69  if child_pid ~= 0 then
     70    child_calls_mod_once[#child_calls_mod_once + 1] = { func = func, args = { ... } }
     71  else
     72    func(...)
     73  end
     74 end
     75 
     76 local child_cleanups_mod_once = nil --- @type ChildCall[]?
     77 
     78 -- Run some code at the end of the child process, before exiting. Is supposed to
     79 -- be run in `before_each` because `after_each` is run after child has exited.
     80 local function child_cleanup_once(func, ...)
     81  local child_cleanups = child_cleanups_mod_once
     82  if child_pid ~= 0 then
     83    child_cleanups[#child_cleanups + 1] = { func = func, args = { ... } }
     84  else
     85    func(...)
     86  end
     87 end
     88 
     89 -- Unittests are run from debug nvim binary in lua interpreter mode.
     90 local libnvim = ffi.C
     91 
     92 local lib = setmetatable({}, {
     93  __index = only_separate(function(_, idx)
     94    return libnvim[idx]
     95  end),
     96  __newindex = child_call(function(_, idx, val)
     97    libnvim[idx] = val
     98  end),
     99 })
    100 
    101 local init = only_separate(function()
    102  for _, c in ipairs(child_calls_init) do
    103    c.func(unpack(c.args))
    104  end
    105  libnvim.event_init()
    106  libnvim.early_init(nil)
    107  if child_calls_mod then
    108    for _, c in ipairs(child_calls_mod) do
    109      c.func(unpack(c.args))
    110    end
    111  end
    112  if child_calls_mod_once then
    113    for _, c in ipairs(child_calls_mod_once) do
    114      c.func(unpack(c.args))
    115    end
    116    child_calls_mod_once = nil
    117  end
    118 end)
    119 
    120 local deinit = only_separate(function()
    121  if child_cleanups_mod_once then
    122    for _, c in ipairs(child_cleanups_mod_once) do
    123      c.func(unpack(c.args))
    124    end
    125    child_cleanups_mod_once = nil
    126  end
    127 end)
    128 
    129 -- a Set that keeps around the lines we've already seen
    130 local cdefs_init = Set:new()
    131 local cdefs_mod = nil
    132 local imported = Set:new()
    133 local pragma_pack_id = 1
    134 
    135 -- some things are just too complex for the LuaJIT C parser to digest. We
    136 -- usually don't need them anyway.
    137 --- @param body string
    138 local function filter_complex_blocks(body)
    139  local result = {} --- @type string[]
    140 
    141  for line in body:gmatch('[^\r\n]+') do
    142    if
    143      not (
    144        string.find(line, '(^)', 1, true) ~= nil
    145        or string.find(line, '_ISwupper', 1, true)
    146        or string.find(line, '_Float')
    147        or string.find(line, '__s128')
    148        or string.find(line, '__u128')
    149        or string.find(line, '__SVFloat32_t')
    150        or string.find(line, '__SVFloat64_t')
    151        or string.find(line, '__SVBool_t')
    152        or string.find(line, '__f32x4_t')
    153        or string.find(line, '__f64x2_t')
    154        or string.find(line, '__sv_f32_t')
    155        or string.find(line, '__sv_f64_t')
    156        or string.find(line, 'msgpack_zone_push_finalizer')
    157        or string.find(line, 'msgpack_unpacker_reserve_buffer')
    158        or string.find(line, 'value_init_')
    159        or string.find(line, 'UUID_NULL') -- static const uuid_t UUID_NULL = {...}
    160        or string.find(line, 'inline _Bool')
    161        -- used by musl libc headers on 32-bit arches via __REDIR marco
    162        or string.find(line, '__typeof__')
    163        -- used by macOS headers
    164        or string.find(line, 'typedef enum : ')
    165        or string.find(line, 'mach_vm_range_recipe')
    166        or string.find(line, 'ipc_info_object_type_t')
    167        or string.find(line, '__Reply__mach_port_kobject_t')
    168      )
    169    then
    170      -- Remove GCC's extension keyword which is just used to disable warnings.
    171      line = string.gsub(line, '__extension__', '')
    172 
    173      -- HACK: remove bitfields from specific structs as luajit can't seem to handle them.
    174      if line:find('struct VTermState') then
    175        line = string.gsub(line, 'state : 8;', 'state;')
    176      end
    177      if line:find('VTermStringFragment') then
    178        line = string.gsub(line, 'size_t.*len : 30;', 'size_t len;')
    179      end
    180      result[#result + 1] = line
    181    end
    182  end
    183 
    184  return table.concat(result, '\n')
    185 end
    186 
    187 local cdef = ffi.cdef
    188 
    189 local cimportstr
    190 
    191 local previous_defines_init = [[
    192 typedef struct { char bytes[16]; } __attribute__((aligned(16))) __uint128_t;
    193 typedef struct { char bytes[16]; } __attribute__((aligned(16))) __float128;
    194 ]]
    195 
    196 local preprocess_cache_init = {} --- @type table<string,string>
    197 local previous_defines_mod = ''
    198 local preprocess_cache_mod = nil --- @type table<string,string>
    199 
    200 local function is_child_cdefs()
    201  return os.getenv('NVIM_TEST_MAIN_CDEFS') ~= '1'
    202 end
    203 
    204 --- use this helper to import C files, you can pass multiple paths at once,
    205 --- this helper will return the C namespace of the nvim library.
    206 ---
    207 --- @param ... string
    208 local function cimport(...)
    209  local previous_defines --- @type string
    210  local preprocess_cache --- @type table<string,string>
    211  local cdefs
    212  if is_child_cdefs() and preprocess_cache_mod then
    213    preprocess_cache = preprocess_cache_mod
    214    previous_defines = previous_defines_mod
    215    cdefs = cdefs_mod
    216  else
    217    preprocess_cache = preprocess_cache_init
    218    previous_defines = previous_defines_init
    219    cdefs = cdefs_init
    220  end
    221  for _, path in ipairs({ ... }) do
    222    if not (path:sub(1, 1) == '/' or path:sub(1, 1) == '.' or path:sub(2, 2) == ':') then
    223      path = './' .. path
    224    end
    225    if not preprocess_cache[path] then
    226      local body --- @type string
    227      body, previous_defines = Preprocess.preprocess(previous_defines, path)
    228      -- format it (so that the lines are "unique" statements), also filter out
    229      -- Objective-C blocks
    230      if os.getenv('NVIM_TEST_PRINT_I') == '1' then
    231        local lnum = 0
    232        for line in body:gmatch('[^\n]+') do
    233          lnum = lnum + 1
    234          print(lnum, line)
    235        end
    236      end
    237      body = formatc(body)
    238      body = filter_complex_blocks(body)
    239      -- add the formatted lines to a set
    240      local new_cdefs = Set:new()
    241      for line in body:gmatch('[^\r\n]+') do
    242        line = trim(line)
    243        -- give each #pragma pack a unique id, so that they don't get removed
    244        -- if they are inserted into the set
    245        -- (they are needed in the right order with the struct definitions,
    246        -- otherwise luajit has wrong memory layouts for the structs)
    247        if line:match('#pragma%s+pack') then
    248          --- @type string
    249          line = line .. ' // ' .. pragma_pack_id
    250          pragma_pack_id = pragma_pack_id + 1
    251        end
    252        new_cdefs:add(line)
    253      end
    254 
    255      -- subtract the lines we've already imported from the new lines, then add
    256      -- the new unique lines to the old lines (so they won't be imported again)
    257      new_cdefs:diff(cdefs)
    258      cdefs:union(new_cdefs)
    259      -- request a sorted version of the new lines (same relative order as the
    260      -- original preprocessed file) and feed that to the LuaJIT ffi
    261      local new_lines = new_cdefs:to_table()
    262      if os.getenv('NVIM_TEST_PRINT_CDEF') == '1' then
    263        for lnum, line in ipairs(new_lines) do
    264          print(lnum, line)
    265        end
    266      end
    267      body = table.concat(new_lines, '\n')
    268 
    269      preprocess_cache[path] = body
    270    end
    271    cimportstr(preprocess_cache, path)
    272  end
    273  return lib
    274 end
    275 
    276 local function cimport_immediate(...)
    277  local saved_pid = child_pid
    278  child_pid = 0
    279  local err, emsg = pcall(cimport, ...)
    280  child_pid = saved_pid
    281  if not err then
    282    io.stderr:write(tostring(emsg) .. '\n')
    283    assert(false)
    284  else
    285    return lib
    286  end
    287 end
    288 
    289 --- @param preprocess_cache table<string,string[]>
    290 --- @param path string
    291 local function _cimportstr(preprocess_cache, path)
    292  if imported:contains(path) then
    293    return lib
    294  end
    295  local body = preprocess_cache[path]
    296  if body == '' then
    297    return lib
    298  end
    299  cdef(body)
    300  imported:add(path)
    301 
    302  return lib
    303 end
    304 
    305 if is_child_cdefs() then
    306  cimportstr = child_call(_cimportstr, lib)
    307 else
    308  cimportstr = _cimportstr
    309 end
    310 
    311 local function alloc_log_new()
    312  local log = {
    313    log = {}, --- @type ChildCallLog[]
    314    lib = cimport('./src/nvim/memory.h'), --- @type table<string,function>
    315    original_functions = {}, --- @type table<string,function>
    316    null = { ['\0:is_null'] = true },
    317  }
    318 
    319  local allocator_functions = { 'malloc', 'free', 'calloc', 'realloc' }
    320 
    321  function log:save_original_functions()
    322    for _, funcname in ipairs(allocator_functions) do
    323      if not self.original_functions[funcname] then
    324        self.original_functions[funcname] = self.lib['mem_' .. funcname]
    325      end
    326    end
    327  end
    328 
    329  log.save_original_functions = child_call(log.save_original_functions)
    330 
    331  function log:set_mocks()
    332    for _, k in ipairs(allocator_functions) do
    333      do
    334        local kk = k
    335        self.lib['mem_' .. k] = function(...)
    336          --- @type ChildCallLog
    337          local log_entry = { func = kk, args = { ... } }
    338          self.log[#self.log + 1] = log_entry
    339          if kk == 'free' then
    340            self.original_functions[kk](...)
    341          else
    342            log_entry.ret = self.original_functions[kk](...)
    343          end
    344          for i, v in ipairs(log_entry.args) do
    345            if v == nil then
    346              -- XXX This thing thinks that {NULL} ~= {NULL}.
    347              log_entry.args[i] = self.null
    348            end
    349          end
    350          if self.hook then
    351            self:hook(log_entry)
    352          end
    353          if log_entry.ret then
    354            return log_entry.ret
    355          end
    356        end
    357      end
    358    end
    359    -- JIT-compiled FFI calls cannot call back into Lua, so disable JIT.
    360    -- Ref: https://luajit.org/ext_ffi_semantics.html#callback
    361    jit.off()
    362  end
    363 
    364  log.set_mocks = child_call(log.set_mocks)
    365 
    366  function log:clear()
    367    self.log = {}
    368  end
    369 
    370  function log:check(exp)
    371    eq(exp, self.log)
    372    self:clear()
    373  end
    374 
    375  function log:clear_tmp_allocs(clear_null_frees)
    376    local toremove = {} --- @type integer[]
    377    local allocs = {} --- @type table<string,integer>
    378    for i, v in ipairs(self.log) do
    379      if v.func == 'malloc' or v.func == 'calloc' then
    380        allocs[tostring(v.ret)] = i
    381      elseif v.func == 'realloc' or v.func == 'free' then
    382        if allocs[tostring(v.args[1])] then
    383          toremove[#toremove + 1] = allocs[tostring(v.args[1])]
    384          if v.func == 'free' then
    385            toremove[#toremove + 1] = i
    386          end
    387        elseif clear_null_frees and v.args[1] == self.null then
    388          toremove[#toremove + 1] = i
    389        end
    390        if v.func == 'realloc' then
    391          allocs[tostring(v.ret)] = i
    392        end
    393      end
    394    end
    395    table.sort(toremove)
    396    for i = #toremove, 1, -1 do
    397      table.remove(self.log, toremove[i])
    398    end
    399  end
    400 
    401  function log:setup()
    402    log:save_original_functions()
    403    log:set_mocks()
    404  end
    405 
    406  function log:before_each() end
    407 
    408  function log:after_each() end
    409 
    410  log:setup()
    411 
    412  return log
    413 end
    414 
    415 -- take a pointer to a C-allocated string and return an interned
    416 -- version while also freeing the memory
    417 local function internalize(cdata, len)
    418  ffi.gc(cdata, ffi.C.free)
    419  return ffi.string(cdata, len)
    420 end
    421 
    422 local cstr = ffi.typeof('char[?]')
    423 local function to_cstr(string)
    424  return cstr(#string + 1, string)
    425 end
    426 
    427 cimport_immediate('./test/unit/fixtures/posix.h')
    428 
    429 local sc = {}
    430 
    431 function sc.fork()
    432  return tonumber(ffi.C.fork())
    433 end
    434 
    435 function sc.pipe()
    436  local ret = ffi.new('int[2]', { -1, -1 })
    437  ffi.errno(0)
    438  local res = ffi.C.pipe(ret)
    439  if res ~= 0 then
    440    local err = ffi.errno(0)
    441    assert(res == 0, ('pipe() error: %u: %s'):format(err, ffi.string(ffi.C.strerror(err))))
    442  end
    443  assert(ret[0] ~= -1 and ret[1] ~= -1)
    444  return ret[0], ret[1]
    445 end
    446 
    447 --- @return string
    448 function sc.read(rd, len)
    449  local ret = ffi.new('char[?]', len, { 0 })
    450  local total_bytes_read = 0
    451  ffi.errno(0)
    452  while total_bytes_read < len do
    453    local bytes_read =
    454      tonumber(ffi.C.read(rd, ffi.cast('void*', ret + total_bytes_read), len - total_bytes_read))
    455    if bytes_read == -1 then
    456      local err = ffi.errno(0)
    457      if err ~= ffi.C.kPOSIXErrnoEINTR then
    458        assert(false, ('read() error: %u: %s'):format(err, ffi.string(ffi.C.strerror(err))))
    459      end
    460    elseif bytes_read == 0 then
    461      break
    462    else
    463      total_bytes_read = total_bytes_read + bytes_read
    464    end
    465  end
    466  return ffi.string(ret, total_bytes_read)
    467 end
    468 
    469 function sc.write(wr, s)
    470  local wbuf = to_cstr(s)
    471  local total_bytes_written = 0
    472  ffi.errno(0)
    473  while total_bytes_written < #s do
    474    local bytes_written = tonumber(
    475      ffi.C.write(wr, ffi.cast('void*', wbuf + total_bytes_written), #s - total_bytes_written)
    476    )
    477    if bytes_written == -1 then
    478      local err = ffi.errno(0)
    479      if err ~= ffi.C.kPOSIXErrnoEINTR then
    480        assert(
    481          false,
    482          ("write() error: %u: %s ('%s')"):format(err, ffi.string(ffi.C.strerror(err)), s)
    483        )
    484      end
    485    elseif bytes_written == 0 then
    486      break
    487    else
    488      total_bytes_written = total_bytes_written + bytes_written
    489    end
    490  end
    491  return total_bytes_written
    492 end
    493 
    494 sc.close = ffi.C.close
    495 
    496 --- @param pid integer
    497 --- @return integer
    498 function sc.wait(pid)
    499  ffi.errno(0)
    500  local stat_loc = ffi.new('int[1]', { 0 })
    501  while true do
    502    local r = ffi.C.waitpid(pid, stat_loc, ffi.C.kPOSIXWaitWUNTRACED)
    503    if r == -1 then
    504      local err = ffi.errno(0)
    505      if err == ffi.C.kPOSIXErrnoECHILD then
    506        break
    507      elseif err ~= ffi.C.kPOSIXErrnoEINTR then
    508        assert(false, ('waitpid() error: %u: %s'):format(err, ffi.string(ffi.C.strerror(err))))
    509      end
    510    else
    511      assert(r == pid)
    512    end
    513  end
    514  return stat_loc[0]
    515 end
    516 
    517 sc.exit = ffi.C._exit
    518 
    519 --- @param lst string[]
    520 --- @return string
    521 local function format_list(lst)
    522  local ret = {} --- @type string[]
    523  for _, v in ipairs(lst) do
    524    ret[#ret + 1] = assert:format({ v, n = 1 })[1]
    525  end
    526  return table.concat(ret, ', ')
    527 end
    528 
    529 if os.getenv('NVIM_TEST_PRINT_SYSCALLS') == '1' then
    530  for k_, v_ in pairs(sc) do
    531    (function(k, v)
    532      sc[k] = function(...)
    533        local rets = { v(...) }
    534        io.stderr:write(('%s(%s) = %s\n'):format(k, format_list({ ... }), format_list(rets)))
    535        return unpack(rets)
    536      end
    537    end)(k_, v_)
    538  end
    539 end
    540 
    541 local function just_fail(_)
    542  return false
    543 end
    544 say:set('assertion.just_fail.positive', '%s')
    545 say:set('assertion.just_fail.negative', '%s')
    546 assert:register(
    547  'assertion',
    548  'just_fail',
    549  just_fail,
    550  'assertion.just_fail.positive',
    551  'assertion.just_fail.negative'
    552 )
    553 
    554 local hook_fnamelen = 30
    555 local hook_sfnamelen = 30
    556 local hook_numlen = 5
    557 local hook_msglen = 1 + 1 + 1 + (1 + hook_fnamelen) + (1 + hook_sfnamelen) + (1 + hook_numlen) + 1
    558 
    559 local tracehelp = dedent([[
    560  Trace: either in the format described below or custom debug output starting
    561  with `>`. Latter lines still have the same width in byte.
    562 
    563   Trace type: _r_eturn from function , function _c_all, _l_ine executed,
    564               _t_ail return, _C_ount (should not actually appear),
    565               _s_aved from previous run for reference, _>_ for custom debug
    566               output.
    567  │┏ Function type: _L_ua function, _C_ function, _m_ain part of chunk,
    568  │┃                function that did _t_ail call.
    569  │┃┌ Function name type: _g_lobal, _l_ocal, _m_ethod, _f_ield, _u_pvalue,
    570  │┃│                     space for unknown.
    571  │┃│  Source file name              Function name                 Line
    572  │┃│  (trunc to 30 bytes, no .lua)  (truncated to last 30 bytes)  number
    573  CWN SSSSSSSSSSSSSSSSSSSSSSSSSSSSSS:FFFFFFFFFFFFFFFFFFFFFFFFFFFFFF:LLLLL\n
    574 ]])
    575 
    576 local function child_sethook(wr)
    577  local trace_level_str = os.getenv('NVIM_TEST_TRACE_LEVEL')
    578  local trace_level = 0
    579  if trace_level_str and trace_level_str ~= '' then
    580    --- @type number
    581    trace_level = assert(tonumber(trace_level_str))
    582  end
    583 
    584  if trace_level <= 0 then
    585    return
    586  end
    587 
    588  local trace_only_c = trace_level <= 1
    589  --- @type debuginfo?, string?, integer
    590  local prev_info, prev_reason, prev_lnum
    591 
    592  --- @param reason string
    593  --- @param lnum integer
    594  --- @param use_prev boolean
    595  local function hook(reason, lnum, use_prev)
    596    local info = nil --- @type debuginfo?
    597    if use_prev then
    598      info = prev_info
    599    elseif reason ~= 'tail return' then -- tail return
    600      info = debug.getinfo(2, 'nSl')
    601    end
    602 
    603    if trace_only_c and (not info or info.what ~= 'C') and not use_prev then
    604      --- @cast info -nil
    605      if info.source:sub(-9) == '_spec.lua' then
    606        prev_info = info
    607        prev_reason = 'saved'
    608        prev_lnum = lnum
    609      end
    610      return
    611    end
    612    if trace_only_c and not use_prev and prev_reason then
    613      hook(prev_reason, prev_lnum, true)
    614      prev_reason = nil
    615    end
    616 
    617    local whatchar = ' '
    618    local namewhatchar = ' '
    619    local funcname = ''
    620    local source = ''
    621    local msgchar = reason:sub(1, 1)
    622 
    623    if reason == 'count' then
    624      msgchar = 'C'
    625    end
    626 
    627    if info then
    628      funcname = (info.name or ''):sub(1, hook_fnamelen)
    629      whatchar = info.what:sub(1, 1)
    630      namewhatchar = info.namewhat:sub(1, 1)
    631      if namewhatchar == '' then
    632        namewhatchar = ' '
    633      end
    634      source = info.source
    635      if source:sub(1, 1) == '@' then
    636        if source:sub(-4, -1) == '.lua' then
    637          source = source:sub(1, -5)
    638        end
    639        source = source:sub(-hook_sfnamelen, -1)
    640      end
    641      lnum = lnum or info.currentline
    642    end
    643 
    644    -- assert(-1 <= lnum and lnum <= 99999)
    645    local lnum_s = lnum == -1 and 'nknwn' or ('%u'):format(lnum)
    646    --- @type string
    647    local msg = ( -- lua does not support %*
    648      ''
    649      .. msgchar
    650      .. whatchar
    651      .. namewhatchar
    652      .. ' '
    653      .. source
    654      .. (' '):rep(hook_sfnamelen - #source)
    655      .. ':'
    656      .. funcname
    657      .. (' '):rep(hook_fnamelen - #funcname)
    658      .. ':'
    659      .. ('0'):rep(hook_numlen - #lnum_s)
    660      .. lnum_s
    661      .. '\n'
    662    )
    663    -- eq(hook_msglen, #msg)
    664    sc.write(wr, msg)
    665  end
    666  debug.sethook(hook, 'crl')
    667 end
    668 
    669 local trace_end_msg = ('E%s\n'):format((' '):rep(hook_msglen - 2))
    670 
    671 --- @type function
    672 local _debug_log
    673 
    674 local debug_log = only_separate(function(...)
    675  return _debug_log(...)
    676 end)
    677 
    678 local function itp_child(wr, func)
    679  --- @param s string
    680  _debug_log = function(s)
    681    s = s:sub(1, hook_msglen - 2)
    682    sc.write(wr, '>' .. s .. (' '):rep(hook_msglen - 2 - #s) .. '\n')
    683  end
    684  local status, result = pcall(init)
    685  if status then
    686    collectgarbage('stop')
    687    child_sethook(wr)
    688    status, result = pcall(func)
    689    debug.sethook()
    690  end
    691  sc.write(wr, trace_end_msg)
    692  if not status then
    693    local emsg = tostring(result)
    694    if #emsg > 99999 then
    695      emsg = emsg:sub(1, 99999)
    696    end
    697    sc.write(wr, ('-\n%05u\n%s'):format(#emsg, emsg))
    698    deinit()
    699  else
    700    sc.write(wr, '+\n')
    701    deinit()
    702  end
    703  collectgarbage('restart')
    704  collectgarbage()
    705  sc.write(wr, '$\n')
    706  sc.close(wr)
    707  sc.exit(status and 0 or 1)
    708 end
    709 
    710 local function check_child_err(rd)
    711  local trace = {} --- @type string[]
    712  local did_traceline = false
    713  local maxtrace = tonumber(os.getenv('NVIM_TEST_MAXTRACE')) or 1024
    714  while true do
    715    local traceline = sc.read(rd, hook_msglen)
    716    if #traceline ~= hook_msglen then
    717      if #traceline == 0 then
    718        break
    719      else
    720        trace[#trace + 1] = 'Partial read: <' .. trace .. '>\n'
    721      end
    722    end
    723    if traceline == trace_end_msg then
    724      did_traceline = true
    725      break
    726    end
    727    trace[#trace + 1] = traceline
    728    if #trace > maxtrace then
    729      table.remove(trace, 1)
    730    end
    731  end
    732  local res = sc.read(rd, 2)
    733  if #res == 2 then
    734    local err = ''
    735    if res ~= '+\n' then
    736      eq('-\n', res)
    737      local len_s = sc.read(rd, 5)
    738      local len = tonumber(len_s)
    739      neq(0, len)
    740      if os.getenv('NVIM_TEST_TRACE_ON_ERROR') == '1' and #trace ~= 0 then
    741        --- @type string
    742        err = '\nTest failed, trace:\n' .. tracehelp
    743        for _, traceline in ipairs(trace) do
    744          --- @type string
    745          err = err .. traceline
    746        end
    747      end
    748      --- @type string
    749      err = err .. sc.read(rd, len + 1)
    750    end
    751    local eres = sc.read(rd, 2)
    752    if eres ~= '$\n' then
    753      if #trace == 0 then
    754        err = '\nTest crashed, no trace available (check NVIM_TEST_TRACE_LEVEL)\n'
    755      else
    756        err = '\nTest crashed, trace:\n' .. tracehelp
    757        for i = 1, #trace do
    758          err = err .. trace[i]
    759        end
    760      end
    761      if not did_traceline then
    762        --- @type string
    763        err = err .. '\nNo end of trace occurred'
    764      end
    765      local cc_err, cc_emsg = pcall(check_cores, paths.test_luajit_prg, true)
    766      if not cc_err then
    767        --- @type string
    768        err = err .. '\ncheck_cores failed: ' .. cc_emsg
    769      end
    770    end
    771    if err ~= '' then
    772      assert.just_fail(err)
    773    end
    774  end
    775 end
    776 
    777 local function itp_parent(rd, pid, allow_failure, location)
    778  local ok, emsg = pcall(check_child_err, rd)
    779  local status = sc.wait(pid)
    780  sc.close(rd)
    781  if not ok then
    782    if allow_failure then
    783      io.stderr:write('Errorred out (' .. status .. '):\n' .. tostring(emsg) .. '\n')
    784      os.execute([[
    785        sh -c "source ci/common/test.sh
    786        check_core_dumps --delete \"]] .. paths.test_luajit_prg .. [[\""]])
    787    else
    788      error(tostring(emsg) .. '\nexit code: ' .. status)
    789    end
    790  elseif status ~= 0 then
    791    if not allow_failure then
    792      error('child process errored out with status ' .. status .. '!\n\n' .. location)
    793    end
    794  end
    795 end
    796 
    797 local function gen_itp(it)
    798  child_calls_mod = {}
    799  child_calls_mod_once = {}
    800  child_cleanups_mod_once = {}
    801  preprocess_cache_mod = map(function(v)
    802    return v
    803  end, preprocess_cache_init)
    804  previous_defines_mod = previous_defines_init
    805  cdefs_mod = cdefs_init:copy()
    806  local function itp(name, func, allow_failure)
    807    if allow_failure and os.getenv('NVIM_TEST_RUN_FAILING_TESTS') ~= '1' then
    808      -- FIXME Fix tests with this true
    809      return
    810    end
    811 
    812    -- Pre-emptively calculating error location, wasteful, ugh!
    813    -- But the way this code messes around with busted implies the real location is strictly
    814    -- not available in the parent when an actual error occurs. so we have to do this here.
    815    local location = debug.traceback()
    816    it(name, function()
    817      local rd, wr = sc.pipe()
    818      child_pid = sc.fork()
    819      if child_pid == 0 then
    820        sc.close(rd)
    821        itp_child(wr, func)
    822      else
    823        sc.close(wr)
    824        local saved_child_pid = child_pid
    825        child_pid = nil
    826        itp_parent(rd, saved_child_pid, allow_failure, location)
    827      end
    828    end)
    829  end
    830  return itp
    831 end
    832 
    833 local function cppimport(path)
    834  return cimport(paths.test_source_path .. '/test/includes/pre/' .. path)
    835 end
    836 
    837 cimport(
    838  './src/nvim/types_defs.h',
    839  './src/nvim/main.h',
    840  './src/nvim/os/time.h',
    841  './src/nvim/os/fs.h'
    842 )
    843 
    844 local function conv_enum(etab, eval)
    845  local n = tonumber(eval)
    846  return etab[n] or n
    847 end
    848 
    849 local function array_size(arr)
    850  return ffi.sizeof(arr) / ffi.sizeof(arr[0])
    851 end
    852 
    853 local function kvi_size(kvi)
    854  return array_size(kvi.init_array)
    855 end
    856 
    857 local function kvi_init(kvi)
    858  kvi.capacity = kvi_size(kvi)
    859  kvi.items = kvi.init_array
    860  return kvi
    861 end
    862 
    863 local function kvi_destroy(kvi)
    864  if kvi.items ~= kvi.init_array then
    865    lib.xfree(kvi.items)
    866  end
    867 end
    868 
    869 local function kvi_new(ct)
    870  return kvi_init(ffi.new(ct))
    871 end
    872 
    873 local function make_enum_conv_tab(m, values, skip_pref, set_cb)
    874  child_call_once(function()
    875    local ret = {}
    876    for _, v in ipairs(values) do
    877      local str_v = v
    878      if v:sub(1, #skip_pref) == skip_pref then
    879        str_v = v:sub(#skip_pref + 1)
    880      end
    881      ret[tonumber(m[v])] = str_v
    882    end
    883    set_cb(ret)
    884  end)
    885 end
    886 
    887 local function ptr2addr(ptr)
    888  return tonumber(ffi.cast('intptr_t', ffi.cast('void *', ptr)))
    889 end
    890 
    891 local s = ffi.new('char[64]', { 0 })
    892 
    893 local function ptr2key(ptr)
    894  ffi.C.snprintf(s, ffi.sizeof(s), '%p', ffi.cast('void *', ptr))
    895  return ffi.string(s)
    896 end
    897 
    898 --- @class test.unit.testutil.module
    899 local M = {
    900  cimport = cimport,
    901  cppimport = cppimport,
    902  internalize = internalize,
    903  ffi = ffi,
    904  lib = lib,
    905  cstr = cstr,
    906  to_cstr = to_cstr,
    907  NULL = ffi.cast('void*', 0),
    908  OK = 1,
    909  FAIL = 0,
    910  alloc_log_new = alloc_log_new,
    911  gen_itp = gen_itp,
    912  only_separate = only_separate,
    913  child_call_once = child_call_once,
    914  child_cleanup_once = child_cleanup_once,
    915  sc = sc,
    916  conv_enum = conv_enum,
    917  array_size = array_size,
    918  kvi_destroy = kvi_destroy,
    919  kvi_size = kvi_size,
    920  kvi_init = kvi_init,
    921  kvi_new = kvi_new,
    922  make_enum_conv_tab = make_enum_conv_tab,
    923  ptr2addr = ptr2addr,
    924  ptr2key = ptr2key,
    925  debug_log = debug_log,
    926 }
    927 --- @class test.unit.testutil: test.unit.testutil.module, test.testutil
    928 M = vim.tbl_extend('error', M, t_global)
    929 
    930 return M