testnvim.lua 29 KB

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