helpers.lua 26 KB

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