helpers.lua 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906
  1. require('coxpcall')
  2. local busted = require('busted')
  3. local luv = require('luv')
  4. local lfs = require('lfs')
  5. local mpack = require('mpack')
  6. local global_helpers = require('test.helpers')
  7. -- nvim client: Found in .deps/usr/share/lua/<version>/nvim/ if "bundled".
  8. local Session = require('nvim.session')
  9. local TcpStream = require('nvim.tcp_stream')
  10. local SocketStream = require('nvim.socket_stream')
  11. local ChildProcessStream = require('nvim.child_process_stream')
  12. local check_cores = global_helpers.check_cores
  13. local check_logs = global_helpers.check_logs
  14. local dedent = global_helpers.dedent
  15. local eq = global_helpers.eq
  16. local filter = global_helpers.tbl_filter
  17. local is_os = global_helpers.is_os
  18. local map = global_helpers.tbl_map
  19. local ok = global_helpers.ok
  20. local sleep = global_helpers.sleep
  21. local tbl_contains = global_helpers.tbl_contains
  22. local write_file = global_helpers.write_file
  23. local fail = global_helpers.fail
  24. local module = {
  25. NIL = mpack.NIL,
  26. mkdir = lfs.mkdir,
  27. }
  28. local start_dir = lfs.currentdir()
  29. module.nvim_prog = (
  30. os.getenv('NVIM_PRG')
  31. or global_helpers.test_build_dir .. '/bin/nvim'
  32. )
  33. -- Default settings for the test session.
  34. module.nvim_set = (
  35. 'set shortmess+=IS background=light noswapfile noautoindent startofline'
  36. ..' laststatus=1 undodir=. directory=. viewdir=. backupdir=.'
  37. ..' belloff= wildoptions-=pum joinspaces noshowcmd noruler nomore redrawdebug=invalid')
  38. module.nvim_argv = {
  39. module.nvim_prog, '-u', 'NONE', '-i', 'NONE',
  40. '--cmd', module.nvim_set,
  41. '--cmd', 'unmap Y',
  42. '--cmd', 'unmap <C-L>',
  43. '--cmd', 'iunmap <C-U>',
  44. '--cmd', 'iunmap <C-W>',
  45. '--embed'}
  46. -- Directory containing nvim.
  47. module.nvim_dir = module.nvim_prog:gsub("[/\\][^/\\]+$", "")
  48. if module.nvim_dir == module.nvim_prog then
  49. module.nvim_dir = "."
  50. end
  51. local tmpname = global_helpers.tmpname
  52. local iswin = global_helpers.iswin
  53. local prepend_argv
  54. if os.getenv('VALGRIND') then
  55. local log_file = os.getenv('VALGRIND_LOG') or 'valgrind-%p.log'
  56. prepend_argv = {'valgrind', '-q', '--tool=memcheck',
  57. '--leak-check=yes', '--track-origins=yes',
  58. '--show-possibly-lost=no',
  59. '--suppressions=src/.valgrind.supp',
  60. '--log-file='..log_file}
  61. if os.getenv('GDB') then
  62. table.insert(prepend_argv, '--vgdb=yes')
  63. table.insert(prepend_argv, '--vgdb-error=0')
  64. end
  65. elseif os.getenv('GDB') then
  66. local gdbserver_port = '7777'
  67. if os.getenv('GDBSERVER_PORT') then
  68. gdbserver_port = os.getenv('GDBSERVER_PORT')
  69. end
  70. prepend_argv = {'gdbserver', 'localhost:'..gdbserver_port}
  71. end
  72. if prepend_argv then
  73. local new_nvim_argv = {}
  74. local len = #prepend_argv
  75. for i = 1, len do
  76. new_nvim_argv[i] = prepend_argv[i]
  77. end
  78. for i = 1, #module.nvim_argv do
  79. new_nvim_argv[i + len] = module.nvim_argv[i]
  80. end
  81. module.nvim_argv = new_nvim_argv
  82. module.prepend_argv = prepend_argv
  83. end
  84. local session, loop_running, last_error, method_error
  85. function module.get_session()
  86. return session
  87. end
  88. function module.set_session(s)
  89. session = s
  90. end
  91. function module.request(method, ...)
  92. local status, rv = session:request(method, ...)
  93. if not status then
  94. if loop_running then
  95. last_error = rv[2]
  96. session:stop()
  97. else
  98. error(rv[2])
  99. end
  100. end
  101. return rv
  102. end
  103. function module.next_msg(timeout)
  104. return session:next_message(timeout and timeout or 10000)
  105. end
  106. function module.expect_twostreams(msgs1, msgs2)
  107. local pos1, pos2 = 1, 1
  108. while pos1 <= #msgs1 or pos2 <= #msgs2 do
  109. local msg = module.next_msg()
  110. if pos1 <= #msgs1 and pcall(eq, msgs1[pos1], msg) then
  111. pos1 = pos1 + 1
  112. elseif pos2 <= #msgs2 then
  113. eq(msgs2[pos2], msg)
  114. pos2 = pos2 + 1
  115. else
  116. -- already failed, but show the right error message
  117. eq(msgs1[pos1], msg)
  118. end
  119. end
  120. end
  121. -- Expects a sequence of next_msg() results. If multiple sequences are
  122. -- passed they are tried until one succeeds, in order of shortest to longest.
  123. --
  124. -- Can be called with positional args (list of sequences only):
  125. -- expect_msg_seq(seq1, seq2, ...)
  126. -- or keyword args:
  127. -- expect_msg_seq{ignore={...}, seqs={seq1, seq2, ...}}
  128. --
  129. -- ignore: List of ignored event names.
  130. -- seqs: List of one or more potential event sequences.
  131. function module.expect_msg_seq(...)
  132. if select('#', ...) < 1 then
  133. error('need at least 1 argument')
  134. end
  135. local arg1 = select(1, ...)
  136. if (arg1['seqs'] and select('#', ...) > 1) or type(arg1) ~= 'table' then
  137. error('invalid args')
  138. end
  139. local ignore = arg1['ignore'] and arg1['ignore'] or {}
  140. local seqs = arg1['seqs'] and arg1['seqs'] or {...}
  141. if type(ignore) ~= 'table' then
  142. error("'ignore' arg must be a list of strings")
  143. end
  144. table.sort(seqs, function(a, b) -- Sort ascending, by (shallow) length.
  145. return #a < #b
  146. end)
  147. local actual_seq = {}
  148. local nr_ignored = 0
  149. local final_error = ''
  150. local function cat_err(err1, err2)
  151. if err1 == nil then
  152. return err2
  153. end
  154. return string.format('%s\n%s\n%s', err1, string.rep('=', 78), err2)
  155. end
  156. local msg_timeout = module.load_adjust(10000) -- Big timeout for ASAN/valgrind.
  157. for anum = 1, #seqs do
  158. local expected_seq = seqs[anum]
  159. -- Collect enough messages to compare the next expected sequence.
  160. while #actual_seq < #expected_seq do
  161. local msg = module.next_msg(msg_timeout)
  162. local msg_type = msg and msg[2] or nil
  163. if msg == nil then
  164. error(cat_err(final_error,
  165. string.format('got %d messages (ignored %d), expected %d',
  166. #actual_seq, nr_ignored, #expected_seq)))
  167. elseif tbl_contains(ignore, msg_type) then
  168. nr_ignored = nr_ignored + 1
  169. else
  170. table.insert(actual_seq, msg)
  171. end
  172. end
  173. local status, result = pcall(eq, expected_seq, actual_seq)
  174. if status then
  175. return result
  176. end
  177. local message = result
  178. if type(result) == "table" then
  179. -- 'eq' returns several things
  180. message = result.message
  181. end
  182. final_error = cat_err(final_error, message)
  183. end
  184. error(final_error)
  185. end
  186. local function call_and_stop_on_error(lsession, ...)
  187. local status, result = copcall(...) -- luacheck: ignore
  188. if not status then
  189. lsession:stop()
  190. last_error = result
  191. return ''
  192. end
  193. return result
  194. end
  195. function module.set_method_error(err)
  196. method_error = err
  197. end
  198. function module.run_session(lsession, request_cb, notification_cb, setup_cb, timeout)
  199. local on_request, on_notification, on_setup
  200. if request_cb then
  201. function on_request(method, args)
  202. method_error = nil
  203. local result = call_and_stop_on_error(lsession, request_cb, method, args)
  204. if method_error ~= nil then
  205. return method_error, true
  206. end
  207. return result
  208. end
  209. end
  210. if notification_cb then
  211. function on_notification(method, args)
  212. call_and_stop_on_error(lsession, notification_cb, method, args)
  213. end
  214. end
  215. if setup_cb then
  216. function on_setup()
  217. call_and_stop_on_error(lsession, setup_cb)
  218. end
  219. end
  220. loop_running = true
  221. session:run(on_request, on_notification, on_setup, timeout)
  222. loop_running = false
  223. if last_error then
  224. local err = last_error
  225. last_error = nil
  226. error(err)
  227. end
  228. end
  229. function module.run(request_cb, notification_cb, setup_cb, timeout)
  230. module.run_session(session, request_cb, notification_cb, setup_cb, timeout)
  231. end
  232. function module.stop()
  233. session:stop()
  234. end
  235. function module.nvim_prog_abs()
  236. -- system(['build/bin/nvim']) does not work for whatever reason. It must
  237. -- be executable searched in $PATH or something starting with / or ./.
  238. if module.nvim_prog:match('[/\\]') then
  239. return module.request('nvim_call_function', 'fnamemodify', {module.nvim_prog, ':p'})
  240. else
  241. return module.nvim_prog
  242. end
  243. end
  244. -- Executes an ex-command. VimL errors manifest as client (lua) errors, but
  245. -- v:errmsg will not be updated.
  246. function module.command(cmd)
  247. module.request('nvim_command', cmd)
  248. end
  249. -- Evaluates a VimL expression.
  250. -- Fails on VimL error, but does not update v:errmsg.
  251. function module.eval(expr)
  252. return module.request('nvim_eval', expr)
  253. end
  254. -- Executes a VimL function.
  255. -- Fails on VimL error, but does not update v:errmsg.
  256. function module.call(name, ...)
  257. return module.request('nvim_call_function', name, {...})
  258. end
  259. -- Sends user input to Nvim.
  260. -- Does not fail on VimL error, but v:errmsg will be updated.
  261. local function nvim_feed(input)
  262. while #input > 0 do
  263. local written = module.request('nvim_input', input)
  264. if written == nil then
  265. module.assert_alive()
  266. error('crash? (nvim_input returned nil)')
  267. end
  268. input = input:sub(written + 1)
  269. end
  270. end
  271. function module.feed(...)
  272. for _, v in ipairs({...}) do
  273. nvim_feed(dedent(v))
  274. end
  275. end
  276. function module.rawfeed(...)
  277. for _, v in ipairs({...}) do
  278. nvim_feed(dedent(v))
  279. end
  280. end
  281. function module.merge_args(...)
  282. local i = 1
  283. local argv = {}
  284. for anum = 1,select('#', ...) do
  285. local args = select(anum, ...)
  286. if args then
  287. for _, arg in ipairs(args) do
  288. argv[i] = arg
  289. i = i + 1
  290. end
  291. end
  292. end
  293. return argv
  294. end
  295. -- Removes Nvim startup args from `args` matching items in `args_rm`.
  296. --
  297. -- "-u", "-i", "--cmd" are treated specially: their "values" are also removed.
  298. -- Example:
  299. -- args={'--headless', '-u', 'NONE'}
  300. -- args_rm={'--cmd', '-u'}
  301. -- Result:
  302. -- {'--headless'}
  303. --
  304. -- All cases are removed.
  305. -- Example:
  306. -- args={'--cmd', 'foo', '-N', '--cmd', 'bar'}
  307. -- args_rm={'--cmd', '-u'}
  308. -- Result:
  309. -- {'-N'}
  310. local function remove_args(args, args_rm)
  311. local new_args = {}
  312. local skip_following = {'-u', '-i', '-c', '--cmd', '-s', '--listen'}
  313. if not args_rm or #args_rm == 0 then
  314. return {unpack(args)}
  315. end
  316. for _, v in ipairs(args_rm) do
  317. assert(type(v) == 'string')
  318. end
  319. local last = ''
  320. for _, arg in ipairs(args) do
  321. if tbl_contains(skip_following, last) then
  322. last = ''
  323. elseif tbl_contains(args_rm, arg) then
  324. last = arg
  325. else
  326. table.insert(new_args, arg)
  327. end
  328. end
  329. return new_args
  330. end
  331. function module.spawn(argv, merge, env, keep)
  332. if session and not keep then
  333. session:close()
  334. end
  335. local child_stream = ChildProcessStream.spawn(
  336. merge and module.merge_args(prepend_argv, argv) or argv,
  337. env)
  338. return Session.new(child_stream)
  339. end
  340. -- Creates a new Session connected by domain socket (named pipe) or TCP.
  341. function module.connect(file_or_address)
  342. local addr, port = string.match(file_or_address, "(.*):(%d+)")
  343. local stream = (addr and port) and TcpStream.open(addr, port) or
  344. SocketStream.open(file_or_address)
  345. return Session.new(stream)
  346. end
  347. -- Calls fn() until it succeeds, up to `max` times or until `max_ms`
  348. -- milliseconds have passed.
  349. function module.retry(max, max_ms, fn)
  350. assert(max == nil or max > 0)
  351. assert(max_ms == nil or max_ms > 0)
  352. local tries = 1
  353. local timeout = (max_ms and max_ms or 10000)
  354. local start_time = luv.now()
  355. while true do
  356. local status, result = pcall(fn)
  357. if status then
  358. return result
  359. end
  360. luv.update_time() -- Update cached value of luv.now() (libuv: uv_now()).
  361. if (max and tries >= max) or (luv.now() - start_time > timeout) then
  362. busted.fail(string.format("retry() attempts: %d\n%s", tries, tostring(result)), 2)
  363. end
  364. tries = tries + 1
  365. luv.sleep(20) -- Avoid hot loop...
  366. end
  367. end
  368. -- Starts a new global Nvim session.
  369. --
  370. -- Parameters are interpreted as startup args, OR a map with these keys:
  371. -- args: List: Args appended to the default `nvim_argv` set.
  372. -- args_rm: List: Args removed from the default set. All cases are
  373. -- removed, e.g. args_rm={'--cmd'} removes all cases of "--cmd"
  374. -- (and its value) from the default set.
  375. -- env: Map: Defines the environment of the new session.
  376. --
  377. -- Example:
  378. -- clear('-e')
  379. -- clear{args={'-e'}, args_rm={'-i'}, env={TERM=term}}
  380. function module.clear(...)
  381. local argv, env = module.new_argv(...)
  382. module.set_session(module.spawn(argv, nil, env))
  383. end
  384. -- Builds an argument list for use in clear().
  385. --
  386. ---@see clear() for parameters.
  387. function module.new_argv(...)
  388. local args = {unpack(module.nvim_argv)}
  389. table.insert(args, '--headless')
  390. local new_args
  391. local env = nil
  392. local opts = select(1, ...)
  393. if type(opts) == 'table' then
  394. args = remove_args(args, opts.args_rm)
  395. if opts.env then
  396. local env_tbl = {}
  397. for k, v in pairs(opts.env) do
  398. assert(type(k) == 'string')
  399. assert(type(v) == 'string')
  400. env_tbl[k] = v
  401. end
  402. for _, k in ipairs({
  403. 'HOME',
  404. 'ASAN_OPTIONS',
  405. 'TSAN_OPTIONS',
  406. 'MSAN_OPTIONS',
  407. 'LD_LIBRARY_PATH',
  408. 'PATH',
  409. 'NVIM_LOG_FILE',
  410. 'NVIM_RPLUGIN_MANIFEST',
  411. 'GCOV_ERROR_FILE',
  412. 'XDG_DATA_DIRS',
  413. 'TMPDIR',
  414. }) do
  415. if not env_tbl[k] then
  416. env_tbl[k] = os.getenv(k)
  417. end
  418. end
  419. env = {}
  420. for k, v in pairs(env_tbl) do
  421. env[#env + 1] = k .. '=' .. v
  422. end
  423. end
  424. new_args = opts.args or {}
  425. else
  426. new_args = {...}
  427. end
  428. for _, arg in ipairs(new_args) do
  429. table.insert(args, arg)
  430. end
  431. return args, env
  432. end
  433. function module.insert(...)
  434. nvim_feed('i')
  435. for _, v in ipairs({...}) do
  436. local escaped = v:gsub('<', '<lt>')
  437. module.rawfeed(escaped)
  438. end
  439. nvim_feed('<ESC>')
  440. end
  441. -- Executes an ex-command by user input. Because nvim_input() is used, VimL
  442. -- errors will not manifest as client (lua) errors. Use command() for that.
  443. function module.feed_command(...)
  444. for _, v in ipairs({...}) do
  445. if v:sub(1, 1) ~= '/' then
  446. -- not a search command, prefix with colon
  447. nvim_feed(':')
  448. end
  449. nvim_feed(v:gsub('<', '<lt>'))
  450. nvim_feed('<CR>')
  451. end
  452. end
  453. local sourced_fnames = {}
  454. function module.source(code)
  455. local fname = tmpname()
  456. write_file(fname, code)
  457. module.command('source '..fname)
  458. -- DO NOT REMOVE FILE HERE.
  459. -- do_source() has a habit of checking whether files are “same” by using inode
  460. -- and device IDs. If you run two source() calls in quick succession there is
  461. -- a good chance that underlying filesystem will reuse the inode, making files
  462. -- appear as “symlinks” to do_source when it checks FileIDs. With current
  463. -- setup linux machines (both QB, travis and mine(ZyX-I) with XFS) do reuse
  464. -- inodes, Mac OS machines (again, both QB and travis) do not.
  465. --
  466. -- Files appearing as “symlinks” mean that both the first and the second
  467. -- source() calls will use same SID, which may fail some tests which check for
  468. -- exact numbers after `<SNR>` in e.g. function names.
  469. sourced_fnames[#sourced_fnames + 1] = fname
  470. return fname
  471. end
  472. function module.has_powershell()
  473. return module.eval('executable("'..(iswin() and 'powershell' or 'pwsh')..'")') == 1
  474. end
  475. function module.set_shell_powershell()
  476. local shell = iswin() and 'powershell' or 'pwsh'
  477. assert(module.has_powershell())
  478. local set_encoding = '[Console]::InputEncoding=[Console]::OutputEncoding=[System.Text.Encoding]::UTF8;'
  479. local cmd = set_encoding..'Remove-Item -Force '..table.concat(iswin()
  480. and {'alias:cat', 'alias:echo', 'alias:sleep'}
  481. or {'alias:echo'}, ',')..';'
  482. module.source([[
  483. let &shell = ']]..shell..[['
  484. set shellquote= shellxquote=
  485. let &shellpipe = '2>&1 | Out-File -Encoding UTF8 %s; exit $LastExitCode'
  486. let &shellredir = '2>&1 | Out-File -Encoding UTF8 %s; exit $LastExitCode'
  487. let &shellcmdflag = '-NoLogo -NoProfile -ExecutionPolicy RemoteSigned -Command ]]..cmd..[['
  488. ]])
  489. end
  490. function module.nvim(method, ...)
  491. return module.request('nvim_'..method, ...)
  492. end
  493. local function ui(method, ...)
  494. return module.request('nvim_ui_'..method, ...)
  495. end
  496. function module.nvim_async(method, ...)
  497. session:notify('nvim_'..method, ...)
  498. end
  499. function module.buffer(method, ...)
  500. return module.request('nvim_buf_'..method, ...)
  501. end
  502. function module.window(method, ...)
  503. return module.request('nvim_win_'..method, ...)
  504. end
  505. function module.tabpage(method, ...)
  506. return module.request('nvim_tabpage_'..method, ...)
  507. end
  508. function module.curbuf(method, ...)
  509. if not method then
  510. return module.nvim('get_current_buf')
  511. end
  512. return module.buffer(method, 0, ...)
  513. end
  514. function module.poke_eventloop()
  515. -- Execute 'nvim_eval' (a deferred function) to
  516. -- force at least one main_loop iteration
  517. session:request('nvim_eval', '1')
  518. end
  519. function module.buf_lines(bufnr)
  520. return module.exec_lua("return vim.api.nvim_buf_get_lines((...), 0, -1, false)", bufnr)
  521. end
  522. ---@see buf_lines()
  523. function module.curbuf_contents()
  524. module.poke_eventloop() -- Before inspecting the buffer, do whatever.
  525. return table.concat(module.curbuf('get_lines', 0, -1, true), '\n')
  526. end
  527. function module.curwin(method, ...)
  528. if not method then
  529. return module.nvim('get_current_win')
  530. end
  531. return module.window(method, 0, ...)
  532. end
  533. function module.curtab(method, ...)
  534. if not method then
  535. return module.nvim('get_current_tabpage')
  536. end
  537. return module.tabpage(method, 0, ...)
  538. end
  539. function module.expect(contents)
  540. return eq(dedent(contents), module.curbuf_contents())
  541. end
  542. function module.expect_any(contents)
  543. contents = dedent(contents)
  544. return ok(nil ~= string.find(module.curbuf_contents(), contents, 1, true))
  545. end
  546. function module.expect_events(expected, received, kind)
  547. local inspect = require'vim.inspect'
  548. if not pcall(eq, expected, received) then
  549. local msg = 'unexpected '..kind..' received.\n\n'
  550. msg = msg .. 'received events:\n'
  551. for _, e in ipairs(received) do
  552. msg = msg .. ' ' .. inspect(e) .. ';\n'
  553. end
  554. msg = msg .. '\nexpected events:\n'
  555. for _, e in ipairs(expected) do
  556. msg = msg .. ' ' .. inspect(e) .. ';\n'
  557. end
  558. fail(msg)
  559. end
  560. return received
  561. end
  562. -- Checks that the Nvim session did not terminate.
  563. function module.assert_alive()
  564. assert(2 == module.eval('1+1'), 'crash? request failed')
  565. end
  566. -- Asserts that buffer is loaded and visible in the current tabpage.
  567. function module.assert_visible(bufnr, visible)
  568. assert(type(visible) == 'boolean')
  569. eq(visible, module.bufmeths.is_loaded(bufnr))
  570. if visible then
  571. assert(-1 ~= module.funcs.bufwinnr(bufnr),
  572. 'expected buffer to be visible in current tabpage: '..tostring(bufnr))
  573. else
  574. assert(-1 == module.funcs.bufwinnr(bufnr),
  575. 'expected buffer NOT visible in current tabpage: '..tostring(bufnr))
  576. end
  577. end
  578. local function do_rmdir(path)
  579. local mode, errmsg, errcode = lfs.attributes(path, 'mode')
  580. if mode == nil then
  581. if errcode == 2 then
  582. -- "No such file or directory", don't complain.
  583. return
  584. end
  585. error(string.format('rmdir: %s (%d)', errmsg, errcode))
  586. end
  587. if mode ~= 'directory' then
  588. error(string.format('rmdir: not a directory: %s', path))
  589. end
  590. for file in lfs.dir(path) do
  591. if file ~= '.' and file ~= '..' then
  592. local abspath = path..'/'..file
  593. if lfs.attributes(abspath, 'mode') == 'directory' then
  594. do_rmdir(abspath) -- recurse
  595. else
  596. local ret, err = os.remove(abspath)
  597. if not ret then
  598. if not session then
  599. error('os.remove: '..err)
  600. else
  601. -- Try Nvim delete(): it handles `readonly` attribute on Windows,
  602. -- and avoids Lua cross-version/platform incompatibilities.
  603. if -1 == module.call('delete', abspath) then
  604. local hint = (is_os('win')
  605. and ' (hint: try :%bwipeout! before rmdir())' or '')
  606. error('delete() failed'..hint..': '..abspath)
  607. end
  608. end
  609. end
  610. end
  611. end
  612. end
  613. local ret, err = lfs.rmdir(path)
  614. if not ret then
  615. error('lfs.rmdir('..path..'): '..err)
  616. end
  617. end
  618. function module.rmdir(path)
  619. local ret, _ = pcall(do_rmdir, path)
  620. if not ret and is_os('win') then
  621. -- Maybe "Permission denied"; try again after changing the nvim
  622. -- process to the top-level directory.
  623. module.command([[exe 'cd '.fnameescape(']]..start_dir.."')")
  624. ret, _ = pcall(do_rmdir, path)
  625. end
  626. -- During teardown, the nvim process may not exit quickly enough, then rmdir()
  627. -- will fail (on Windows).
  628. if not ret then -- Try again.
  629. sleep(1000)
  630. do_rmdir(path)
  631. end
  632. end
  633. function module.exc_exec(cmd)
  634. module.command(([[
  635. try
  636. execute "%s"
  637. catch
  638. let g:__exception = v:exception
  639. endtry
  640. ]]):format(cmd:gsub('\n', '\\n'):gsub('[\\"]', '\\%0')))
  641. local ret = module.eval('get(g:, "__exception", 0)')
  642. module.command('unlet! g:__exception')
  643. return ret
  644. end
  645. function module.create_callindex(func)
  646. local table = {}
  647. setmetatable(table, {
  648. __index = function(tbl, arg1)
  649. local ret = function(...) return func(arg1, ...) end
  650. tbl[arg1] = ret
  651. return ret
  652. end,
  653. })
  654. return table
  655. end
  656. -- Helper to skip tests. Returns true in Windows systems.
  657. -- pending_fn is pending() from busted
  658. function module.pending_win32(pending_fn)
  659. if iswin() then
  660. if pending_fn ~= nil then
  661. pending_fn('FIXME: Windows', function() end)
  662. end
  663. return true
  664. else
  665. return false
  666. end
  667. end
  668. function module.pending_c_parser(pending_fn)
  669. local status, msg = unpack(module.exec_lua([[ return {pcall(vim.treesitter.require_language, 'c')} ]]))
  670. if not status then
  671. if module.isCI() then
  672. error("treesitter C parser not found, required on CI: " .. msg)
  673. else
  674. pending_fn 'no C parser, skipping'
  675. return true
  676. end
  677. end
  678. return false
  679. end
  680. -- Calls pending() and returns `true` if the system is too slow to
  681. -- run fragile or expensive tests. Else returns `false`.
  682. function module.skip_fragile(pending_fn, cond)
  683. if pending_fn == nil or type(pending_fn) ~= type(function()end) then
  684. error("invalid pending_fn")
  685. end
  686. if cond then
  687. pending_fn("skipped (test is fragile on this system)", function() end)
  688. return true
  689. elseif os.getenv("TEST_SKIP_FRAGILE") then
  690. pending_fn("skipped (TEST_SKIP_FRAGILE)", function() end)
  691. return true
  692. end
  693. return false
  694. end
  695. module.funcs = module.create_callindex(module.call)
  696. module.meths = module.create_callindex(module.nvim)
  697. module.async_meths = module.create_callindex(module.nvim_async)
  698. module.uimeths = module.create_callindex(ui)
  699. module.bufmeths = module.create_callindex(module.buffer)
  700. module.winmeths = module.create_callindex(module.window)
  701. module.tabmeths = module.create_callindex(module.tabpage)
  702. module.curbufmeths = module.create_callindex(module.curbuf)
  703. module.curwinmeths = module.create_callindex(module.curwin)
  704. module.curtabmeths = module.create_callindex(module.curtab)
  705. function module.exec(code)
  706. return module.meths.exec(code, false)
  707. end
  708. function module.exec_capture(code)
  709. return module.meths.exec(code, true)
  710. end
  711. function module.exec_lua(code, ...)
  712. return module.meths.exec_lua(code, {...})
  713. end
  714. function module.get_pathsep()
  715. return iswin() and '\\' or '/'
  716. end
  717. function module.pathroot()
  718. local pathsep = package.config:sub(1,1)
  719. return iswin() and (module.nvim_dir:sub(1,2)..pathsep) or '/'
  720. end
  721. -- Returns a valid, platform-independent $NVIM_LISTEN_ADDRESS.
  722. -- Useful for communicating with child instances.
  723. function module.new_pipename()
  724. -- HACK: Start a server temporarily, get the name, then stop it.
  725. local pipename = module.eval('serverstart()')
  726. module.funcs.serverstop(pipename)
  727. return pipename
  728. end
  729. function module.missing_provider(provider)
  730. if provider == 'ruby' or provider == 'node' or provider == 'perl' then
  731. local e = module.funcs['provider#'..provider..'#Detect']()[2]
  732. return e ~= '' and e or false
  733. elseif provider == 'python' or provider == 'python3' then
  734. local py_major_version = (provider == 'python3' and 3 or 2)
  735. local e = module.funcs['provider#pythonx#Detect'](py_major_version)[2]
  736. return e ~= '' and e or false
  737. else
  738. assert(false, 'Unknown provider: '..provider)
  739. end
  740. end
  741. function module.alter_slashes(obj)
  742. if not iswin() then
  743. return obj
  744. end
  745. if type(obj) == 'string' then
  746. local ret = obj:gsub('/', '\\')
  747. return ret
  748. elseif type(obj) == 'table' then
  749. local ret = {}
  750. for k, v in pairs(obj) do
  751. ret[k] = module.alter_slashes(v)
  752. end
  753. return ret
  754. else
  755. assert(false, 'expected string or table of strings, got '..type(obj))
  756. end
  757. end
  758. local load_factor = 1
  759. if global_helpers.isCI() then
  760. -- Compute load factor only once (but outside of any tests).
  761. module.clear()
  762. module.request('nvim_command', 'source src/nvim/testdir/load.vim')
  763. load_factor = module.request('nvim_eval', 'g:test_load_factor')
  764. end
  765. function module.load_adjust(num)
  766. return math.ceil(num * load_factor)
  767. end
  768. function module.parse_context(ctx)
  769. local parsed = {}
  770. for _, item in ipairs({'regs', 'jumps', 'bufs', 'gvars'}) do
  771. parsed[item] = filter(function(v)
  772. return type(v) == 'table'
  773. end, module.call('msgpackparse', ctx[item]))
  774. end
  775. parsed['bufs'] = parsed['bufs'][1]
  776. return map(function(v)
  777. if #v == 0 then
  778. return nil
  779. end
  780. return v
  781. end, parsed)
  782. end
  783. function module.add_builddir_to_rtp()
  784. -- Add runtime from build dir for doc/tags (used with :help).
  785. module.command(string.format([[set rtp+=%s/runtime]], module.test_build_dir))
  786. end
  787. -- Kill process with given pid
  788. function module.os_kill(pid)
  789. return os.execute((iswin()
  790. and 'taskkill /f /t /pid '..pid..' > nul'
  791. or 'kill -9 '..pid..' > /dev/null'))
  792. end
  793. -- Create folder with non existing parents
  794. function module.mkdir_p(path)
  795. return os.execute((iswin()
  796. and 'mkdir '..path
  797. or 'mkdir -p '..path))
  798. end
  799. module = global_helpers.tbl_extend('error', module, global_helpers)
  800. return function(after_each)
  801. if after_each then
  802. after_each(function()
  803. for _, fname in ipairs(sourced_fnames) do
  804. os.remove(fname)
  805. end
  806. check_logs()
  807. check_cores('build/bin/nvim')
  808. if session then
  809. local msg = session:next_message(0)
  810. if msg then
  811. if msg[1] == "notification" and msg[2] == "nvim_error_event" then
  812. error(msg[3][2])
  813. end
  814. end
  815. end
  816. end)
  817. end
  818. return module
  819. end