testnvim.lua 28 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037
  1. local uv = vim.uv
  2. local t = require('test.testutil')
  3. local Session = require('test.client.session')
  4. local uv_stream = require('test.client.uv_stream')
  5. local SocketStream = uv_stream.SocketStream
  6. local ProcStream = uv_stream.ProcStream
  7. local check_cores = t.check_cores
  8. local check_logs = t.check_logs
  9. local dedent = t.dedent
  10. local eq = t.eq
  11. local is_os = t.is_os
  12. local ok = t.ok
  13. local sleep = uv.sleep
  14. --- Functions executing in the current nvim session/process being tested.
  15. local M = {}
  16. local runtime_set = 'set runtimepath^=./build/lib/nvim/'
  17. M.nvim_prog = (os.getenv('NVIM_PRG') or t.paths.test_build_dir .. '/bin/nvim')
  18. -- Default settings for the test session.
  19. M.nvim_set = (
  20. 'set shortmess+=IS background=light noswapfile noautoindent startofline'
  21. .. ' laststatus=1 undodir=. directory=. viewdir=. backupdir=.'
  22. .. ' belloff= wildoptions-=pum joinspaces noshowcmd noruler nomore redrawdebug=invalid'
  23. )
  24. M.nvim_argv = {
  25. M.nvim_prog,
  26. '-u',
  27. 'NONE',
  28. '-i',
  29. 'NONE',
  30. -- XXX: find treesitter parsers.
  31. '--cmd',
  32. runtime_set,
  33. '--cmd',
  34. M.nvim_set,
  35. -- Remove default user commands and mappings.
  36. '--cmd',
  37. 'comclear | mapclear | mapclear!',
  38. -- Make screentest work after changing to the new default color scheme
  39. -- Source 'vim' color scheme without side effects
  40. -- TODO: rewrite tests
  41. '--cmd',
  42. 'lua dofile("runtime/colors/vim.lua")',
  43. '--cmd',
  44. 'unlet g:colors_name',
  45. '--embed',
  46. }
  47. if os.getenv('OSV_PORT') then
  48. table.insert(M.nvim_argv, '--cmd')
  49. table.insert(
  50. M.nvim_argv,
  51. string.format(
  52. "lua require('osv').launch({ port = %s, blocking = true })",
  53. os.getenv('OSV_PORT')
  54. )
  55. )
  56. end
  57. -- Directory containing nvim.
  58. M.nvim_dir = M.nvim_prog:gsub('[/\\][^/\\]+$', '')
  59. if M.nvim_dir == M.nvim_prog then
  60. M.nvim_dir = '.'
  61. end
  62. local prepend_argv --- @type string[]?
  63. if os.getenv('VALGRIND') then
  64. local log_file = os.getenv('VALGRIND_LOG') or 'valgrind-%p.log'
  65. prepend_argv = {
  66. 'valgrind',
  67. '-q',
  68. '--tool=memcheck',
  69. '--leak-check=yes',
  70. '--track-origins=yes',
  71. '--show-possibly-lost=no',
  72. '--suppressions=src/.valgrind.supp',
  73. '--log-file=' .. log_file,
  74. }
  75. if os.getenv('GDB') then
  76. table.insert(prepend_argv, '--vgdb=yes')
  77. table.insert(prepend_argv, '--vgdb-error=0')
  78. end
  79. elseif os.getenv('GDB') then
  80. local gdbserver_port = os.getenv('GDBSERVER_PORT') or '7777'
  81. prepend_argv = { 'gdbserver', 'localhost:' .. gdbserver_port }
  82. end
  83. if prepend_argv then
  84. local new_nvim_argv = {} --- @type string[]
  85. local len = #prepend_argv
  86. for i = 1, len do
  87. new_nvim_argv[i] = prepend_argv[i]
  88. end
  89. for i = 1, #M.nvim_argv do
  90. new_nvim_argv[i + len] = M.nvim_argv[i]
  91. end
  92. M.nvim_argv = new_nvim_argv
  93. M.prepend_argv = prepend_argv
  94. end
  95. local session --- @type test.Session?
  96. local loop_running --- @type boolean?
  97. local last_error --- @type string?
  98. local method_error --- @type string?
  99. if not is_os('win') then
  100. local sigpipe_handler = assert(uv.new_signal())
  101. uv.signal_start(sigpipe_handler, 'sigpipe', function()
  102. print('warning: got SIGPIPE signal. Likely related to a crash in nvim')
  103. end)
  104. end
  105. function M.get_session()
  106. return session
  107. end
  108. function M.set_session(s)
  109. session = s
  110. end
  111. --- @param method string
  112. --- @param ... any
  113. --- @return any
  114. function M.request(method, ...)
  115. assert(session, 'no Nvim session')
  116. local status, rv = session:request(method, ...)
  117. if not status then
  118. if loop_running then
  119. --- @type string
  120. last_error = rv[2]
  121. session:stop()
  122. else
  123. error(rv[2])
  124. end
  125. end
  126. return rv
  127. end
  128. --- @param method string
  129. --- @param ... any
  130. --- @return any
  131. function M.request_lua(method, ...)
  132. return M.exec_lua([[return vim.api[...](select(2, ...))]], method, ...)
  133. end
  134. --- @param timeout? integer
  135. --- @return string?
  136. function M.next_msg(timeout)
  137. assert(session)
  138. return session:next_message(timeout or 10000)
  139. end
  140. function M.expect_twostreams(msgs1, msgs2)
  141. local pos1, pos2 = 1, 1
  142. while pos1 <= #msgs1 or pos2 <= #msgs2 do
  143. local msg = M.next_msg()
  144. if pos1 <= #msgs1 and pcall(eq, msgs1[pos1], msg) then
  145. pos1 = pos1 + 1
  146. elseif pos2 <= #msgs2 then
  147. eq(msgs2[pos2], msg)
  148. pos2 = pos2 + 1
  149. else
  150. -- already failed, but show the right error message
  151. eq(msgs1[pos1], msg)
  152. end
  153. end
  154. end
  155. -- Expects a sequence of next_msg() results. If multiple sequences are
  156. -- passed they are tried until one succeeds, in order of shortest to longest.
  157. --
  158. -- Can be called with positional args (list of sequences only):
  159. -- expect_msg_seq(seq1, seq2, ...)
  160. -- or keyword args:
  161. -- expect_msg_seq{ignore={...}, seqs={seq1, seq2, ...}}
  162. --
  163. -- ignore: List of ignored event names.
  164. -- seqs: List of one or more potential event sequences.
  165. function M.expect_msg_seq(...)
  166. if select('#', ...) < 1 then
  167. error('need at least 1 argument')
  168. end
  169. local arg1 = select(1, ...)
  170. if (arg1['seqs'] and select('#', ...) > 1) or type(arg1) ~= 'table' then
  171. error('invalid args')
  172. end
  173. local ignore = arg1['ignore'] and arg1['ignore'] or {}
  174. --- @type string[]
  175. local seqs = arg1['seqs'] and arg1['seqs'] or { ... }
  176. if type(ignore) ~= 'table' then
  177. error("'ignore' arg must be a list of strings")
  178. end
  179. table.sort(seqs, function(a, b) -- Sort ascending, by (shallow) length.
  180. return #a < #b
  181. end)
  182. local actual_seq = {}
  183. local nr_ignored = 0
  184. local final_error = ''
  185. local function cat_err(err1, err2)
  186. if err1 == nil then
  187. return err2
  188. end
  189. return string.format('%s\n%s\n%s', err1, string.rep('=', 78), err2)
  190. end
  191. local msg_timeout = M.load_adjust(10000) -- Big timeout for ASAN/valgrind.
  192. for anum = 1, #seqs do
  193. local expected_seq = seqs[anum]
  194. -- Collect enough messages to compare the next expected sequence.
  195. while #actual_seq < #expected_seq do
  196. local msg = M.next_msg(msg_timeout)
  197. local msg_type = msg and msg[2] or nil
  198. if msg == nil then
  199. error(
  200. cat_err(
  201. final_error,
  202. string.format(
  203. 'got %d messages (ignored %d), expected %d',
  204. #actual_seq,
  205. nr_ignored,
  206. #expected_seq
  207. )
  208. )
  209. )
  210. elseif vim.tbl_contains(ignore, msg_type) then
  211. nr_ignored = nr_ignored + 1
  212. else
  213. table.insert(actual_seq, msg)
  214. end
  215. end
  216. local status, result = pcall(eq, expected_seq, actual_seq)
  217. if status then
  218. return result
  219. end
  220. local message = result
  221. if type(result) == 'table' then
  222. -- 'eq' returns several things
  223. --- @type string
  224. message = result.message
  225. end
  226. final_error = cat_err(final_error, message)
  227. end
  228. error(final_error)
  229. end
  230. local function call_and_stop_on_error(lsession, ...)
  231. local status, result = Session.safe_pcall(...) -- luacheck: ignore
  232. if not status then
  233. lsession:stop()
  234. last_error = result
  235. return ''
  236. end
  237. return result
  238. end
  239. function M.set_method_error(err)
  240. method_error = err
  241. end
  242. --- Runs the event loop of the given session.
  243. ---
  244. --- @param lsession test.Session
  245. --- @param request_cb function?
  246. --- @param notification_cb function?
  247. --- @param setup_cb function?
  248. --- @param timeout integer
  249. --- @return [integer, string]
  250. function M.run_session(lsession, request_cb, notification_cb, setup_cb, timeout)
  251. local on_request --- @type function?
  252. local on_notification --- @type function?
  253. local on_setup --- @type function?
  254. if request_cb then
  255. function on_request(method, args)
  256. method_error = nil
  257. local result = call_and_stop_on_error(lsession, request_cb, method, args)
  258. if method_error ~= nil then
  259. return method_error, true
  260. end
  261. return result
  262. end
  263. end
  264. if notification_cb then
  265. function on_notification(method, args)
  266. call_and_stop_on_error(lsession, notification_cb, method, args)
  267. end
  268. end
  269. if setup_cb then
  270. function on_setup()
  271. call_and_stop_on_error(lsession, setup_cb)
  272. end
  273. end
  274. loop_running = true
  275. lsession:run(on_request, on_notification, on_setup, timeout)
  276. loop_running = false
  277. if last_error then
  278. local err = last_error
  279. last_error = nil
  280. error(err)
  281. end
  282. return lsession.eof_err
  283. end
  284. --- Runs the event loop of the current global session.
  285. function M.run(request_cb, notification_cb, setup_cb, timeout)
  286. assert(session)
  287. return M.run_session(session, request_cb, notification_cb, setup_cb, timeout)
  288. end
  289. function M.stop()
  290. assert(session):stop()
  291. end
  292. -- Use for commands which expect nvim to quit.
  293. -- The first argument can also be a timeout.
  294. function M.expect_exit(fn_or_timeout, ...)
  295. local eof_err_msg = 'EOF was received from Nvim. Likely the Nvim process crashed.'
  296. if type(fn_or_timeout) == 'function' then
  297. t.matches(eof_err_msg, t.pcall_err(fn_or_timeout, ...))
  298. else
  299. t.matches(
  300. eof_err_msg,
  301. t.pcall_err(function(timeout, fn, ...)
  302. fn(...)
  303. assert(session)
  304. while session:next_message(timeout) do
  305. end
  306. if session.eof_err then
  307. error(session.eof_err[2])
  308. end
  309. end, fn_or_timeout, ...)
  310. )
  311. end
  312. end
  313. --- Executes a Vimscript function via Lua.
  314. --- Fails on Vimscript error, but does not update v:errmsg.
  315. --- @param name string
  316. --- @param ... any
  317. --- @return any
  318. function M.call_lua(name, ...)
  319. return M.exec_lua([[return vim.call(...)]], name, ...)
  320. end
  321. --- Sends user input to Nvim.
  322. --- Does not fail on Vimscript error, but v:errmsg will be updated.
  323. --- @param input string
  324. local function nvim_feed(input)
  325. while #input > 0 do
  326. local written = M.request('nvim_input', input)
  327. if written == nil then
  328. M.assert_alive()
  329. error('crash? (nvim_input returned nil)')
  330. end
  331. input = input:sub(written + 1)
  332. end
  333. end
  334. --- @param ... string
  335. function M.feed(...)
  336. for _, v in ipairs({ ... }) do
  337. nvim_feed(dedent(v))
  338. end
  339. end
  340. ---@param ... string[]?
  341. ---@return string[]
  342. function M.merge_args(...)
  343. local i = 1
  344. local argv = {} --- @type string[]
  345. for anum = 1, select('#', ...) do
  346. --- @type string[]?
  347. local args = select(anum, ...)
  348. if args then
  349. for _, arg in ipairs(args) do
  350. argv[i] = arg
  351. i = i + 1
  352. end
  353. end
  354. end
  355. return argv
  356. end
  357. --- Removes Nvim startup args from `args` matching items in `args_rm`.
  358. ---
  359. --- - Special case: "-u", "-i", "--cmd" are treated specially: their "values" are also removed.
  360. --- - Special case: "runtimepath" will remove only { '--cmd', 'set runtimepath^=…', }
  361. ---
  362. --- Example:
  363. --- args={'--headless', '-u', 'NONE'}
  364. --- args_rm={'--cmd', '-u'}
  365. --- Result:
  366. --- {'--headless'}
  367. ---
  368. --- All matching cases are removed.
  369. ---
  370. --- Example:
  371. --- args={'--cmd', 'foo', '-N', '--cmd', 'bar'}
  372. --- args_rm={'--cmd', '-u'}
  373. --- Result:
  374. --- {'-N'}
  375. --- @param args string[]
  376. --- @param args_rm string[]
  377. --- @return string[]
  378. local function remove_args(args, args_rm)
  379. local new_args = {} --- @type string[]
  380. local skip_following = { '-u', '-i', '-c', '--cmd', '-s', '--listen' }
  381. if not args_rm or #args_rm == 0 then
  382. return { unpack(args) }
  383. end
  384. for _, v in ipairs(args_rm) do
  385. assert(type(v) == 'string')
  386. end
  387. local last = ''
  388. for _, arg in ipairs(args) do
  389. if vim.tbl_contains(skip_following, last) then
  390. last = ''
  391. elseif vim.tbl_contains(args_rm, arg) then
  392. last = arg
  393. elseif arg == runtime_set and vim.tbl_contains(args_rm, 'runtimepath') then
  394. table.remove(new_args) -- Remove the preceding "--cmd".
  395. last = ''
  396. else
  397. table.insert(new_args, arg)
  398. end
  399. end
  400. return new_args
  401. end
  402. function M.check_close()
  403. if not session then
  404. return
  405. end
  406. local start_time = uv.now()
  407. session:close()
  408. uv.update_time() -- Update cached value of luv.now() (libuv: uv_now()).
  409. local end_time = uv.now()
  410. local delta = end_time - start_time
  411. if delta > 500 then
  412. print(
  413. 'nvim took '
  414. .. delta
  415. .. ' milliseconds to exit after last test\n'
  416. .. 'This indicates a likely problem with the test even if it passed!\n'
  417. )
  418. io.stdout:flush()
  419. end
  420. session = nil
  421. end
  422. -- Creates a new Session connected by domain socket (named pipe) or TCP.
  423. function M.connect(file_or_address)
  424. local addr, port = string.match(file_or_address, '(.*):(%d+)')
  425. local stream = (addr and port) and SocketStream.connect(addr, port)
  426. or SocketStream.open(file_or_address)
  427. return Session.new(stream)
  428. end
  429. --- Starts a new, global Nvim session and clears the current one.
  430. ---
  431. --- Note: Use `new_session()` to start a session without replacing the current one.
  432. ---
  433. --- Parameters are interpreted as startup args, OR a map with these keys:
  434. --- - args: List: Args appended to the default `nvim_argv` set.
  435. --- - args_rm: List: Args removed from the default set. All cases are
  436. --- removed, e.g. args_rm={'--cmd'} removes all cases of "--cmd"
  437. --- (and its value) from the default set.
  438. --- - env: Map: Defines the environment of the new session.
  439. ---
  440. --- Example:
  441. --- ```
  442. --- clear('-e')
  443. --- clear{args={'-e'}, args_rm={'-i'}, env={TERM=term}}
  444. --- ```
  445. ---
  446. --- @param ... string Nvim CLI args
  447. --- @return test.Session
  448. --- @overload fun(opts: test.session.Opts): test.Session
  449. function M.clear(...)
  450. M.set_session(M.new_session(false, ...))
  451. return M.get_session()
  452. end
  453. --- Starts a new Nvim process with the given args and returns a msgpack-RPC session.
  454. ---
  455. --- Does not replace the current global session, unlike `clear()`.
  456. ---
  457. --- @param keep boolean (default: false) Don't close the current global session.
  458. --- @param ... string Nvim CLI args (or see overload)
  459. --- @return test.Session
  460. --- @overload fun(keep: boolean, opts: test.session.Opts): test.Session
  461. function M.new_session(keep, ...)
  462. if not keep then
  463. M.check_close()
  464. end
  465. local argv, env, io_extra = M._new_argv(...)
  466. local proc = ProcStream.spawn(argv, env, io_extra)
  467. return Session.new(proc)
  468. end
  469. --- Starts a (non-RPC, `--headless --listen "Tx"`) Nvim process, waits for exit, and returns result.
  470. ---
  471. --- @param ... string Nvim CLI args, or `test.session.Opts` table.
  472. --- @return test.ProcStream
  473. --- @overload fun(opts: test.session.Opts): test.ProcStream
  474. function M.spawn_wait(...)
  475. local opts = type(...) == 'string' and { args = { ... } } or ...
  476. opts.args_rm = opts.args_rm and opts.args_rm or {}
  477. table.insert(opts.args_rm, '--embed')
  478. local argv, env, io_extra = M._new_argv(opts)
  479. local proc = ProcStream.spawn(argv, env, io_extra)
  480. proc.collect_text = true
  481. proc:read_start()
  482. proc:wait()
  483. proc:close()
  484. return proc
  485. end
  486. --- @class test.session.Opts
  487. --- Nvim CLI args
  488. --- @field args? string[]
  489. --- Remove these args from the default `nvim_argv` args set. Ignored if `merge=false`.
  490. --- @field args_rm? string[]
  491. --- (default: true) Merge `args` with the default set. Else use only the provided `args`.
  492. --- @field merge? boolean
  493. --- Environment variables
  494. --- @field env? table<string,string>
  495. --- Used for stdin_fd, see `:help ui-option`
  496. --- @field io_extra? uv.uv_pipe_t
  497. --- @private
  498. ---
  499. --- Builds an argument list for use in `new_session()`, `clear()`, and `spawn_wait()`.
  500. ---
  501. --- @param ... string Nvim CLI args, or `test.session.Opts` table.
  502. --- @return string[]
  503. --- @return string[]?
  504. --- @return uv.uv_pipe_t?
  505. --- @overload fun(opts: test.session.Opts): string[], string[]?, uv.uv_pipe_t?
  506. function M._new_argv(...)
  507. --- @type test.session.Opts|string
  508. local opts = select(1, ...)
  509. local merge = type(opts) ~= 'table' and true or opts.merge ~= false
  510. local args = merge and { unpack(M.nvim_argv) } or { M.nvim_prog }
  511. if merge then
  512. table.insert(args, '--headless')
  513. if _G._nvim_test_id then
  514. -- Set the server name to the test-id for logging. #8519
  515. table.insert(args, '--listen')
  516. table.insert(args, _G._nvim_test_id)
  517. end
  518. end
  519. local new_args --- @type string[]
  520. local io_extra --- @type uv.uv_pipe_t?
  521. local env --- @type string[]? List of "key=value" env vars.
  522. if type(opts) ~= 'table' then
  523. new_args = { ... }
  524. else
  525. args = merge and remove_args(args, opts.args_rm) or args
  526. if opts.env then
  527. local env_opt = {} --- @type table<string,string>
  528. for k, v in pairs(opts.env) do
  529. assert(type(k) == 'string')
  530. assert(type(v) == 'string')
  531. env_opt[k] = v
  532. end
  533. for _, k in ipairs({
  534. 'HOME',
  535. 'ASAN_OPTIONS',
  536. 'TSAN_OPTIONS',
  537. 'MSAN_OPTIONS',
  538. 'LD_LIBRARY_PATH',
  539. 'PATH',
  540. 'NVIM_LOG_FILE',
  541. 'NVIM_RPLUGIN_MANIFEST',
  542. 'GCOV_ERROR_FILE',
  543. 'XDG_DATA_DIRS',
  544. 'TMPDIR',
  545. 'VIMRUNTIME',
  546. }) do
  547. -- Set these from the environment unless the caller defined them.
  548. if not env_opt[k] then
  549. env_opt[k] = os.getenv(k)
  550. end
  551. end
  552. env = {}
  553. for k, v in pairs(env_opt) do
  554. env[#env + 1] = k .. '=' .. v
  555. end
  556. end
  557. new_args = opts.args or {}
  558. io_extra = opts.io_extra
  559. end
  560. for _, arg in ipairs(new_args) do
  561. table.insert(args, arg)
  562. end
  563. return args, env, io_extra
  564. end
  565. --- @param ... string
  566. function M.insert(...)
  567. nvim_feed('i')
  568. for _, v in ipairs({ ... }) do
  569. local escaped = v:gsub('<', '<lt>')
  570. M.feed(escaped)
  571. end
  572. nvim_feed('<ESC>')
  573. end
  574. --- Executes an ex-command by user input. Because nvim_input() is used, Vimscript
  575. --- errors will not manifest as client (lua) errors. Use command() for that.
  576. --- @param ... string
  577. function M.feed_command(...)
  578. for _, v in ipairs({ ... }) do
  579. if v:sub(1, 1) ~= '/' then
  580. -- not a search command, prefix with colon
  581. nvim_feed(':')
  582. end
  583. nvim_feed(v:gsub('<', '<lt>'))
  584. nvim_feed('<CR>')
  585. end
  586. end
  587. -- @deprecated use nvim_exec2()
  588. function M.source(code)
  589. M.exec(dedent(code))
  590. end
  591. function M.has_powershell()
  592. return M.eval('executable("' .. (is_os('win') and 'powershell' or 'pwsh') .. '")') == 1
  593. end
  594. --- Sets Nvim shell to powershell.
  595. ---
  596. --- @param fake (boolean) If true, a fake will be used if powershell is not
  597. --- found on the system.
  598. --- @returns true if powershell was found on the system, else false.
  599. function M.set_shell_powershell(fake)
  600. local found = M.has_powershell()
  601. if not fake then
  602. assert(found)
  603. end
  604. local shell = found and (is_os('win') and 'powershell' or 'pwsh') or M.testprg('pwsh-test')
  605. local cmd = 'Remove-Item -Force '
  606. .. table.concat(
  607. is_os('win') and { 'alias:cat', 'alias:echo', 'alias:sleep', 'alias:sort', 'alias:tee' }
  608. or { 'alias:echo' },
  609. ','
  610. )
  611. .. ';'
  612. M.exec([[
  613. let &shell = ']] .. shell .. [['
  614. set shellquote= shellxquote=
  615. let &shellcmdflag = '-NoLogo -NoProfile -ExecutionPolicy RemoteSigned -Command '
  616. let &shellcmdflag .= '[Console]::InputEncoding=[Console]::OutputEncoding=[System.Text.UTF8Encoding]::new();'
  617. let &shellcmdflag .= '$PSDefaultParameterValues[''Out-File:Encoding'']=''utf8'';'
  618. let &shellcmdflag .= ']] .. cmd .. [['
  619. let &shellredir = '2>&1 | %%{ "$_" } | Out-File %s; exit $LastExitCode'
  620. let &shellpipe = '2>&1 | %%{ "$_" } | tee %s; exit $LastExitCode'
  621. ]])
  622. return found
  623. end
  624. ---@param func function
  625. ---@return table<string,function>
  626. function M.create_callindex(func)
  627. return setmetatable({}, {
  628. --- @param tbl table<any,function>
  629. --- @param arg1 string
  630. --- @return function
  631. __index = function(tbl, arg1)
  632. local ret = function(...)
  633. return func(arg1, ...)
  634. end
  635. tbl[arg1] = ret
  636. return ret
  637. end,
  638. })
  639. end
  640. --- @param method string
  641. --- @param ... any
  642. function M.nvim_async(method, ...)
  643. assert(session):notify(method, ...)
  644. end
  645. --- Executes a Vimscript function via RPC.
  646. --- Fails on Vimscript error, but does not update v:errmsg.
  647. --- @param name string
  648. --- @param ... any
  649. --- @return any
  650. function M.call(name, ...)
  651. return M.request('nvim_call_function', name, { ... })
  652. end
  653. M.async_meths = M.create_callindex(M.nvim_async)
  654. M.rpc = {
  655. fn = M.create_callindex(M.call),
  656. api = M.create_callindex(M.request),
  657. }
  658. M.lua = {
  659. fn = M.create_callindex(M.call_lua),
  660. api = M.create_callindex(M.request_lua),
  661. }
  662. M.describe_lua_and_rpc = function(describe)
  663. return function(what, tests)
  664. local function d(flavour)
  665. describe(string.format('%s (%s)', what, flavour), function(...)
  666. return tests(M[flavour].api, ...)
  667. end)
  668. end
  669. d('rpc')
  670. d('lua')
  671. end
  672. end
  673. --- add for typing. The for loop after will overwrite this
  674. M.api = vim.api
  675. M.fn = vim.fn
  676. for name, fns in pairs(M.rpc) do
  677. --- @diagnostic disable-next-line:no-unknown
  678. M[name] = fns
  679. end
  680. -- Executes an ex-command. Vimscript errors manifest as client (lua) errors, but
  681. -- v:errmsg will not be updated.
  682. M.command = M.api.nvim_command
  683. -- Evaluates a Vimscript expression.
  684. -- Fails on Vimscript error, but does not update v:errmsg.
  685. M.eval = M.api.nvim_eval
  686. function M.poke_eventloop()
  687. -- Execute 'nvim_eval' (a deferred function) to
  688. -- force at least one main_loop iteration
  689. M.api.nvim_eval('1')
  690. end
  691. function M.buf_lines(bufnr)
  692. return M.exec_lua('return vim.api.nvim_buf_get_lines((...), 0, -1, false)', bufnr)
  693. end
  694. ---@see buf_lines()
  695. function M.curbuf_contents()
  696. M.poke_eventloop() -- Before inspecting the buffer, do whatever.
  697. return table.concat(M.api.nvim_buf_get_lines(0, 0, -1, true), '\n')
  698. end
  699. function M.expect(contents)
  700. return eq(dedent(contents), M.curbuf_contents())
  701. end
  702. function M.expect_any(contents)
  703. contents = dedent(contents)
  704. return ok(nil ~= string.find(M.curbuf_contents(), contents, 1, true))
  705. end
  706. -- Checks that the Nvim session did not terminate.
  707. function M.assert_alive()
  708. assert(2 == M.eval('1+1'), 'crash? request failed')
  709. end
  710. -- Asserts that buffer is loaded and visible in the current tabpage.
  711. function M.assert_visible(bufnr, visible)
  712. assert(type(visible) == 'boolean')
  713. eq(visible, M.api.nvim_buf_is_loaded(bufnr))
  714. if visible then
  715. assert(
  716. -1 ~= M.fn.bufwinnr(bufnr),
  717. 'expected buffer to be visible in current tabpage: ' .. tostring(bufnr)
  718. )
  719. else
  720. assert(
  721. -1 == M.fn.bufwinnr(bufnr),
  722. 'expected buffer NOT visible in current tabpage: ' .. tostring(bufnr)
  723. )
  724. end
  725. end
  726. local start_dir = uv.cwd()
  727. function M.rmdir(path)
  728. local ret, _ = pcall(vim.fs.rm, path, { recursive = true, force = true })
  729. if not ret and is_os('win') then
  730. -- Maybe "Permission denied"; try again after changing the nvim
  731. -- process to the top-level directory.
  732. M.command([[exe 'cd '.fnameescape(']] .. start_dir .. "')")
  733. ret, _ = pcall(vim.fs.rm, path, { recursive = true, force = true })
  734. end
  735. -- During teardown, the nvim process may not exit quickly enough, then rmdir()
  736. -- will fail (on Windows).
  737. if not ret then -- Try again.
  738. sleep(1000)
  739. vim.fs.rm(path, { recursive = true, force = true })
  740. end
  741. end
  742. function M.exc_exec(cmd)
  743. M.command(([[
  744. try
  745. execute "%s"
  746. catch
  747. let g:__exception = v:exception
  748. endtry
  749. ]]):format(cmd:gsub('\n', '\\n'):gsub('[\\"]', '\\%0')))
  750. local ret = M.eval('get(g:, "__exception", 0)')
  751. M.command('unlet! g:__exception')
  752. return ret
  753. end
  754. function M.exec(code)
  755. M.api.nvim_exec2(code, {})
  756. end
  757. --- @param code string
  758. --- @return string
  759. function M.exec_capture(code)
  760. return M.api.nvim_exec2(code, { output = true }).output
  761. end
  762. --- Execute Lua code in the wrapped Nvim session.
  763. ---
  764. --- When `code` is passed as a function, it is converted into Lua byte code.
  765. ---
  766. --- Direct upvalues are copied over, however upvalues contained
  767. --- within nested functions are not. Upvalues are also copied back when `code`
  768. --- finishes executing. See `:help lua-upvalue`.
  769. ---
  770. --- Only types which can be serialized can be transferred over, e.g:
  771. --- `table`, `number`, `boolean`, `string`.
  772. ---
  773. --- `code` runs with a different environment and thus will have a different global
  774. --- environment. See `:help lua-environments`.
  775. ---
  776. --- Example:
  777. --- ```lua
  778. --- local upvalue1 = 'upvalue1'
  779. --- exec_lua(function(a, b, c)
  780. --- print(upvalue1, a, b, c)
  781. --- (function()
  782. --- print(upvalue2)
  783. --- end)()
  784. --- end, 'a', 'b', 'c'
  785. --- ```
  786. --- Prints:
  787. --- ```
  788. --- upvalue1 a b c
  789. --- nil
  790. --- ```
  791. ---
  792. --- Not supported:
  793. --- ```lua
  794. --- local a = vim.uv.new_timer()
  795. --- exec_lua(function()
  796. --- print(a) -- Error: a is of type 'userdata' which cannot be serialized.
  797. --- end)
  798. --- ```
  799. --- @param code string|function
  800. --- @param ... any
  801. --- @return any
  802. function M.exec_lua(code, ...)
  803. if type(code) == 'string' then
  804. return M.api.nvim_exec_lua(code, { ... })
  805. end
  806. assert(session, 'no Nvim session')
  807. return require('test.functional.testnvim.exec_lua')(session, 2, code, ...)
  808. end
  809. function M.get_pathsep()
  810. return is_os('win') and '\\' or '/'
  811. end
  812. --- Gets the filesystem root dir, namely "/" or "C:/".
  813. function M.pathroot()
  814. local pathsep = package.config:sub(1, 1)
  815. return is_os('win') and (M.nvim_dir:sub(1, 2) .. pathsep) or '/'
  816. end
  817. --- Gets the full `…/build/bin/{name}` path of a test program produced by
  818. --- `test/functional/fixtures/CMakeLists.txt`.
  819. ---
  820. --- @param name (string) Name of the test program.
  821. function M.testprg(name)
  822. local ext = is_os('win') and '.exe' or ''
  823. return ('%s/%s%s'):format(M.nvim_dir, name, ext)
  824. end
  825. function M.is_asan()
  826. local version = M.eval('execute("verbose version")')
  827. return version:match('-fsanitize=[a-z,]*address')
  828. end
  829. -- Returns a valid, platform-independent Nvim listen address.
  830. -- Useful for communicating with child instances.
  831. function M.new_pipename()
  832. -- HACK: Start a server temporarily, get the name, then stop it.
  833. local pipename = M.eval('serverstart()')
  834. M.fn.serverstop(pipename)
  835. -- Remove the pipe so that trying to connect to it without a server listening
  836. -- will be an error instead of a hang.
  837. os.remove(pipename)
  838. return pipename
  839. end
  840. --- @param provider string
  841. --- @return string|boolean?
  842. function M.missing_provider(provider)
  843. if provider == 'ruby' or provider == 'perl' then
  844. --- @type string?
  845. local e = M.exec_lua("return {require('vim.provider." .. provider .. "').detect()}")[2]
  846. return e ~= '' and e or false
  847. elseif provider == 'node' then
  848. --- @type string?
  849. local e = M.fn['provider#node#Detect']()[2]
  850. return e ~= '' and e or false
  851. elseif provider == 'python' then
  852. return M.exec_lua([[return {require('vim.provider.python').detect_by_module('neovim')}]])[2]
  853. end
  854. assert(false, 'Unknown provider: ' .. provider)
  855. end
  856. local load_factor = 1
  857. if t.is_ci() then
  858. -- Compute load factor only once (but outside of any tests).
  859. M.clear()
  860. M.request('nvim_command', 'source test/old/testdir/load.vim')
  861. load_factor = M.request('nvim_eval', 'g:test_load_factor')
  862. end
  863. --- @param num number
  864. --- @return number
  865. function M.load_adjust(num)
  866. return math.ceil(num * load_factor)
  867. end
  868. --- @param ctx table<string,any>
  869. --- @return table
  870. function M.parse_context(ctx)
  871. local parsed = {} --- @type table<string,any>
  872. for _, item in ipairs({ 'regs', 'jumps', 'bufs', 'gvars' }) do
  873. --- @param v any
  874. parsed[item] = vim.tbl_filter(function(v)
  875. return type(v) == 'table'
  876. end, M.call('msgpackparse', ctx[item]))
  877. end
  878. parsed['bufs'] = parsed['bufs'][1]
  879. --- @param v any
  880. return vim.tbl_map(function(v)
  881. if #v == 0 then
  882. return nil
  883. end
  884. return v
  885. end, parsed)
  886. end
  887. function M.add_builddir_to_rtp()
  888. -- Add runtime from build dir for doc/tags (used with :help).
  889. M.command(string.format([[set rtp+=%s/runtime]], t.paths.test_build_dir))
  890. end
  891. --- Kill (reap) a process by PID.
  892. --- @param pid string
  893. --- @return boolean?
  894. function M.os_kill(pid)
  895. return os.execute(
  896. (
  897. is_os('win') and 'taskkill /f /t /pid ' .. pid .. ' > nul'
  898. or 'kill -9 ' .. pid .. ' > /dev/null'
  899. )
  900. )
  901. end
  902. --- Create folder with non existing parents
  903. --- @param path string
  904. --- @return boolean?
  905. function M.mkdir_p(path)
  906. return os.execute((is_os('win') and 'mkdir ' .. path or 'mkdir -p ' .. path))
  907. end
  908. local testid = (function()
  909. local id = 0
  910. return function()
  911. id = id + 1
  912. return id
  913. end
  914. end)()
  915. return function()
  916. local g = getfenv(2)
  917. --- @type function?
  918. local before_each = g.before_each
  919. --- @type function?
  920. local after_each = g.after_each
  921. if before_each then
  922. before_each(function()
  923. local id = ('T%d'):format(testid())
  924. _G._nvim_test_id = id
  925. end)
  926. end
  927. if after_each then
  928. after_each(function()
  929. check_logs()
  930. check_cores('build/bin/nvim')
  931. if session then
  932. local msg = session:next_message(0)
  933. if msg then
  934. if msg[1] == 'notification' and msg[2] == 'nvim_error_event' then
  935. error(msg[3][2])
  936. end
  937. end
  938. end
  939. end)
  940. end
  941. return M
  942. end