testnvim.lua 28 KB

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