helpers.lua 27 KB

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