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