helpers.lua 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875
  1. require('test.compat')
  2. local shared = vim
  3. local assert = require('luassert')
  4. local busted = require('busted')
  5. local luv = require('luv')
  6. local relpath = require('pl.path').relpath
  7. local Paths = require('test.cmakeconfig.paths')
  8. assert:set_parameter('TableFormatLevel', 100)
  9. local quote_me = '[^.%w%+%-%@%_%/]' -- complement (needn't quote)
  10. local function shell_quote(str)
  11. if string.find(str, quote_me) or str == '' then
  12. return '"' .. str:gsub('[$%%"\\]', '\\%0') .. '"'
  13. else
  14. return str
  15. end
  16. end
  17. local module = {
  18. REMOVE_THIS = {},
  19. }
  20. function module.isdir(path)
  21. if not path then
  22. return false
  23. end
  24. local stat = luv.fs_stat(path)
  25. if not stat then
  26. return false
  27. end
  28. return stat.type == 'directory'
  29. end
  30. function module.isfile(path)
  31. if not path then
  32. return false
  33. end
  34. local stat = luv.fs_stat(path)
  35. if not stat then
  36. return false
  37. end
  38. return stat.type == 'file'
  39. end
  40. function module.argss_to_cmd(...)
  41. local cmd = ''
  42. for i = 1, select('#', ...) do
  43. local arg = select(i, ...)
  44. if type(arg) == 'string' then
  45. cmd = cmd .. ' ' ..shell_quote(arg)
  46. else
  47. for _, subarg in ipairs(arg) do
  48. cmd = cmd .. ' ' .. shell_quote(subarg)
  49. end
  50. end
  51. end
  52. return cmd
  53. end
  54. function module.popen_r(...)
  55. return io.popen(module.argss_to_cmd(...), 'r')
  56. end
  57. -- sleeps the test runner (_not_ the nvim instance)
  58. function module.sleep(ms)
  59. luv.sleep(ms)
  60. end
  61. -- Calls fn() until it succeeds, up to `max` times or until `max_ms`
  62. -- milliseconds have passed.
  63. function module.retry(max, max_ms, fn)
  64. assert(max == nil or max > 0)
  65. assert(max_ms == nil or max_ms > 0)
  66. local tries = 1
  67. local timeout = (max_ms and max_ms or 10000)
  68. local start_time = luv.now()
  69. while true do
  70. local status, result = pcall(fn)
  71. if status then
  72. return result
  73. end
  74. luv.update_time() -- Update cached value of luv.now() (libuv: uv_now()).
  75. if (max and tries >= max) or (luv.now() - start_time > timeout) then
  76. busted.fail(string.format("retry() attempts: %d\n%s", tries, tostring(result)), 2)
  77. end
  78. tries = tries + 1
  79. luv.sleep(20) -- Avoid hot loop...
  80. end
  81. end
  82. local check_logs_useless_lines = {
  83. ['Warning: noted but unhandled ioctl']=1,
  84. ['could cause spurious value errors to appear']=2,
  85. ['See README_MISSING_SYSCALL_OR_IOCTL for guidance']=3,
  86. }
  87. function module.eq(expected, actual, context)
  88. return assert.are.same(expected, actual, context)
  89. end
  90. function module.neq(expected, actual, context)
  91. return assert.are_not.same(expected, actual, context)
  92. end
  93. --- Asserts that `cond` is true, or prints a message.
  94. ---
  95. --- @param cond (boolean) expression to assert
  96. --- @param expected (any) description of expected result
  97. --- @param actual (any) description of actual result
  98. function module.ok(cond, expected, actual)
  99. assert((not expected and not actual) or (expected and actual), 'if "expected" is given, "actual" is also required')
  100. local msg = expected and ('expected %s, got: %s'):format(expected, tostring(actual)) or nil
  101. return assert(cond, msg)
  102. end
  103. local function epicfail(state, arguments, _)
  104. state.failure_message = arguments[1]
  105. return false
  106. end
  107. assert:register("assertion", "epicfail", epicfail)
  108. function module.fail(msg)
  109. return assert.epicfail(msg)
  110. end
  111. function module.matches(pat, actual)
  112. if nil ~= string.match(actual, pat) then
  113. return true
  114. end
  115. error(string.format('Pattern does not match.\nPattern:\n%s\nActual:\n%s', pat, actual))
  116. end
  117. --- Asserts that `pat` matches (or *not* if inverse=true) any line in the tail of `logfile`.
  118. ---
  119. --- Retries for 1 second in case of filesystem delay.
  120. ---
  121. ---@param pat (string) Lua pattern to match lines in the log file
  122. ---@param logfile (string) Full path to log file (default=$NVIM_LOG_FILE)
  123. ---@param nrlines (number) Search up to this many log lines
  124. ---@param inverse (boolean) Assert that the pattern does NOT match.
  125. function module.assert_log(pat, logfile, nrlines, inverse)
  126. logfile = logfile or os.getenv('NVIM_LOG_FILE') or '.nvimlog'
  127. assert(logfile ~= nil, 'no logfile')
  128. nrlines = nrlines or 10
  129. inverse = inverse or false
  130. module.retry(nil, 1000, function()
  131. local lines = module.read_file_list(logfile, -nrlines) or {}
  132. local msg = string.format('Pattern %q %sfound in log (last %d lines): %s:\n%s',
  133. pat, (inverse and '' or 'not '), nrlines, logfile, ' '..table.concat(lines, '\n '))
  134. for _,line in ipairs(lines) do
  135. if line:match(pat) then
  136. if inverse then error(msg) else return end
  137. end
  138. end
  139. if not inverse then error(msg) end
  140. end)
  141. end
  142. --- Asserts that `pat` does NOT match any line in the tail of `logfile`.
  143. ---
  144. --- @see assert_log
  145. function module.assert_nolog(pat, logfile, nrlines)
  146. return module.assert_log(pat, logfile, nrlines, true)
  147. end
  148. function module.pcall(fn, ...)
  149. assert(type(fn) == 'function')
  150. local status, rv = pcall(fn, ...)
  151. if status then
  152. return status, rv
  153. end
  154. -- From:
  155. -- C:/long/path/foo.lua:186: Expected string, got number
  156. -- to:
  157. -- .../foo.lua:0: Expected string, got number
  158. local errmsg = tostring(rv):gsub('([%s<])vim[/\\]([^%s:/\\]+):%d+', '%1\xffvim\xff%2:0')
  159. :gsub('[^%s<]-[/\\]([^%s:/\\]+):%d+', '.../%1:0')
  160. :gsub('\xffvim\xff', 'vim/')
  161. -- Scrub numbers in paths/stacktraces:
  162. -- shared.lua:0: in function 'gsplit'
  163. -- shared.lua:0: in function <shared.lua:0>'
  164. errmsg = errmsg:gsub('([^%s]):%d+', '%1:0')
  165. -- Scrub tab chars:
  166. errmsg = errmsg:gsub('\t', ' ')
  167. -- In Lua 5.1, we sometimes get a "(tail call): ?" on the last line.
  168. -- We remove this so that the tests are not lua dependent.
  169. errmsg = errmsg:gsub('%s*%(tail call%): %?', '')
  170. return status, errmsg
  171. end
  172. -- Invokes `fn` and returns the error string (with truncated paths), or raises
  173. -- an error if `fn` succeeds.
  174. --
  175. -- Replaces line/column numbers with zero:
  176. -- shared.lua:0: in function 'gsplit'
  177. -- shared.lua:0: in function <shared.lua:0>'
  178. --
  179. -- Usage:
  180. -- -- Match exact string.
  181. -- eq('e', pcall_err(function(a, b) error('e') end, 'arg1', 'arg2'))
  182. -- -- Match Lua pattern.
  183. -- matches('e[or]+$', pcall_err(function(a, b) error('some error') end, 'arg1', 'arg2'))
  184. --
  185. function module.pcall_err_withfile(fn, ...)
  186. assert(type(fn) == 'function')
  187. local status, rv = module.pcall(fn, ...)
  188. if status == true then
  189. error('expected failure, but got success')
  190. end
  191. return rv
  192. end
  193. function module.pcall_err_withtrace(fn, ...)
  194. local errmsg = module.pcall_err_withfile(fn, ...)
  195. return errmsg:gsub('^%.%.%./helpers%.lua:0: ', '')
  196. :gsub('^Error executing lua:- ' ,'')
  197. :gsub('^%[string "<nvim>"%]:0: ' ,'')
  198. end
  199. function module.pcall_err(...)
  200. return module.remove_trace(module.pcall_err_withtrace(...))
  201. end
  202. function module.remove_trace(s)
  203. return (s:gsub("\n%s*stack traceback:.*", ""))
  204. end
  205. -- initial_path: directory to recurse into
  206. -- re: include pattern (string)
  207. -- exc_re: exclude pattern(s) (string or table)
  208. function module.glob(initial_path, re, exc_re)
  209. exc_re = type(exc_re) == 'table' and exc_re or { exc_re }
  210. local paths_to_check = {initial_path}
  211. local ret = {}
  212. local checked_files = {}
  213. local function is_excluded(path)
  214. for _, pat in pairs(exc_re) do
  215. if path:match(pat) then return true end
  216. end
  217. return false
  218. end
  219. if is_excluded(initial_path) then
  220. return ret
  221. end
  222. while #paths_to_check > 0 do
  223. local cur_path = paths_to_check[#paths_to_check]
  224. paths_to_check[#paths_to_check] = nil
  225. for e in vim.fs.dir(cur_path) do
  226. local full_path = cur_path .. '/' .. e
  227. local checked_path = full_path:sub(#initial_path + 1)
  228. if (not is_excluded(checked_path)) and e:sub(1, 1) ~= '.' then
  229. local stat = luv.fs_stat(full_path)
  230. if stat then
  231. local check_key = stat.dev .. ':' .. tostring(stat.ino)
  232. if not checked_files[check_key] then
  233. checked_files[check_key] = true
  234. if stat.type == 'directory' then
  235. paths_to_check[#paths_to_check + 1] = full_path
  236. elseif not re or checked_path:match(re) then
  237. ret[#ret + 1] = full_path
  238. end
  239. end
  240. end
  241. end
  242. end
  243. end
  244. return ret
  245. end
  246. function module.check_logs()
  247. local log_dir = os.getenv('LOG_DIR')
  248. local runtime_errors = {}
  249. if log_dir and module.isdir(log_dir) then
  250. for tail in vim.fs.dir(log_dir) do
  251. if tail:sub(1, 30) == 'valgrind-' or tail:find('san%.') then
  252. local file = log_dir .. '/' .. tail
  253. local fd = io.open(file)
  254. local start_msg = ('='):rep(20) .. ' File ' .. file .. ' ' .. ('='):rep(20)
  255. local lines = {}
  256. local warning_line = 0
  257. for line in fd:lines() do
  258. local cur_warning_line = check_logs_useless_lines[line]
  259. if cur_warning_line == warning_line + 1 then
  260. warning_line = cur_warning_line
  261. else
  262. lines[#lines + 1] = line
  263. end
  264. end
  265. fd:close()
  266. if #lines > 0 then
  267. local status, f
  268. local out = io.stdout
  269. if os.getenv('SYMBOLIZER') then
  270. status, f = pcall(module.popen_r, os.getenv('SYMBOLIZER'), '-l', file)
  271. end
  272. out:write(start_msg .. '\n')
  273. if status then
  274. for line in f:lines() do
  275. out:write('= '..line..'\n')
  276. end
  277. f:close()
  278. else
  279. out:write('= ' .. table.concat(lines, '\n= ') .. '\n')
  280. end
  281. out:write(select(1, start_msg:gsub('.', '=')) .. '\n')
  282. table.insert(runtime_errors, file)
  283. end
  284. os.remove(file)
  285. end
  286. end
  287. end
  288. assert(0 == #runtime_errors, string.format(
  289. 'Found runtime errors in logfile(s): %s',
  290. table.concat(runtime_errors, ', ')))
  291. end
  292. function module.sysname()
  293. local platform = luv.os_uname()
  294. if platform and platform.sysname then
  295. return platform.sysname:lower()
  296. end
  297. end
  298. function module.is_os(s)
  299. if not (s == 'win'
  300. or s == 'mac'
  301. or s == 'freebsd'
  302. or s == 'openbsd'
  303. or s == 'bsd') then
  304. error('unknown platform: '..tostring(s))
  305. end
  306. return not not ((s == 'win' and (module.sysname():find('windows') or module.sysname():find('mingw')))
  307. or (s == 'mac' and module.sysname() == 'darwin')
  308. or (s == 'freebsd' and module.sysname() == 'freebsd')
  309. or (s == 'openbsd' and module.sysname() == 'openbsd')
  310. or (s == 'bsd' and module.sysname():find('bsd')))
  311. end
  312. local function tmpdir_get()
  313. return os.getenv('TMPDIR') and os.getenv('TMPDIR') or os.getenv('TEMP')
  314. end
  315. -- Is temp directory `dir` defined local to the project workspace?
  316. local function tmpdir_is_local(dir)
  317. return not not (dir and string.find(dir, 'Xtest'))
  318. end
  319. --- Creates a new temporary file for use by tests.
  320. module.tmpname = (function()
  321. local seq = 0
  322. local tmpdir = tmpdir_get()
  323. return (function()
  324. if tmpdir_is_local(tmpdir) then
  325. -- Cannot control os.tmpname() dir, so hack our own tmpname() impl.
  326. seq = seq + 1
  327. -- "…/Xtest_tmpdir/T42.7"
  328. local fname = ('%s/%s.%d'):format(tmpdir, (_G._nvim_test_id or 'nvim-test'), seq)
  329. io.open(fname, 'w'):close()
  330. return fname
  331. else
  332. local fname = os.tmpname()
  333. if module.is_os('win') and fname:sub(1, 2) == '\\s' then
  334. -- In Windows tmpname() returns a filename starting with
  335. -- special sequence \s, prepend $TEMP path
  336. return tmpdir..fname
  337. elseif fname:match('^/tmp') and module.is_os('mac') then
  338. -- In OS X /tmp links to /private/tmp
  339. return '/private'..fname
  340. else
  341. return fname
  342. end
  343. end
  344. end)
  345. end)()
  346. function module.hasenv(name)
  347. local env = os.getenv(name)
  348. if env and env ~= '' then
  349. return env
  350. end
  351. return nil
  352. end
  353. local function deps_prefix()
  354. local env = os.getenv('DEPS_PREFIX')
  355. return (env and env ~= '') and env or '.deps/usr'
  356. end
  357. local tests_skipped = 0
  358. function module.check_cores(app, force) -- luacheck: ignore
  359. -- Temporary workaround: skip core check as it interferes with CI.
  360. if true then
  361. return
  362. end
  363. app = app or 'build/bin/nvim' -- luacheck: ignore
  364. local initial_path, re, exc_re
  365. local gdb_db_cmd = 'gdb -n -batch -ex "thread apply all bt full" "$_NVIM_TEST_APP" -c "$_NVIM_TEST_CORE"'
  366. local lldb_db_cmd = 'lldb -Q -o "bt all" -f "$_NVIM_TEST_APP" -c "$_NVIM_TEST_CORE"'
  367. local random_skip = false
  368. -- Workspace-local $TMPDIR, scrubbed and pattern-escaped.
  369. -- "./Xtest-tmpdir/" => "Xtest%-tmpdir"
  370. local local_tmpdir = (tmpdir_is_local(tmpdir_get())
  371. and relpath(tmpdir_get()):gsub('^[ ./]+',''):gsub('%/+$',''):gsub('([^%w])', '%%%1')
  372. or nil)
  373. local db_cmd
  374. if module.hasenv('NVIM_TEST_CORE_GLOB_DIRECTORY') then
  375. initial_path = os.getenv('NVIM_TEST_CORE_GLOB_DIRECTORY')
  376. re = os.getenv('NVIM_TEST_CORE_GLOB_RE')
  377. exc_re = { os.getenv('NVIM_TEST_CORE_EXC_RE'), local_tmpdir }
  378. db_cmd = os.getenv('NVIM_TEST_CORE_DB_CMD') or gdb_db_cmd
  379. random_skip = os.getenv('NVIM_TEST_CORE_RANDOM_SKIP')
  380. elseif module.is_os('mac') then
  381. initial_path = '/cores'
  382. re = nil
  383. exc_re = { local_tmpdir }
  384. db_cmd = lldb_db_cmd
  385. else
  386. initial_path = '.'
  387. if module.is_os('freebsd') then
  388. re = '/nvim.core$'
  389. else
  390. re = '/core[^/]*$'
  391. end
  392. exc_re = { '^/%.deps$', '^/%'..deps_prefix()..'$', local_tmpdir, '^/%node_modules$' }
  393. db_cmd = gdb_db_cmd
  394. random_skip = true
  395. end
  396. -- Finding cores takes too much time on linux
  397. if not force and random_skip and math.random() < 0.9 then
  398. tests_skipped = tests_skipped + 1
  399. return
  400. end
  401. local cores = module.glob(initial_path, re, exc_re)
  402. local found_cores = 0
  403. local out = io.stdout
  404. for _, core in ipairs(cores) do
  405. local len = 80 - #core - #('Core file ') - 2
  406. local esigns = ('='):rep(len / 2)
  407. out:write(('\n%s Core file %s %s\n'):format(esigns, core, esigns))
  408. out:flush()
  409. os.execute(db_cmd:gsub('%$_NVIM_TEST_APP', app):gsub('%$_NVIM_TEST_CORE', core) .. ' 2>&1')
  410. out:write('\n')
  411. found_cores = found_cores + 1
  412. os.remove(core)
  413. end
  414. if found_cores ~= 0 then
  415. out:write(('\nTests covered by this check: %u\n'):format(tests_skipped + 1))
  416. end
  417. tests_skipped = 0
  418. if found_cores > 0 then
  419. error("crash detected (see above)")
  420. end
  421. end
  422. function module.repeated_read_cmd(...)
  423. for _ = 1, 10 do
  424. local stream = module.popen_r(...)
  425. local ret = stream:read('*a')
  426. stream:close()
  427. if ret then
  428. return ret
  429. end
  430. end
  431. print('ERROR: Failed to execute ' .. module.argss_to_cmd(...) .. ': nil return after 10 attempts')
  432. return nil
  433. end
  434. function module.shallowcopy(orig)
  435. if type(orig) ~= 'table' then
  436. return orig
  437. end
  438. local copy = {}
  439. for orig_key, orig_value in pairs(orig) do
  440. copy[orig_key] = orig_value
  441. end
  442. return copy
  443. end
  444. function module.mergedicts_copy(d1, d2)
  445. local ret = module.shallowcopy(d1)
  446. for k, v in pairs(d2) do
  447. if d2[k] == module.REMOVE_THIS then
  448. ret[k] = nil
  449. elseif type(d1[k]) == 'table' and type(v) == 'table' then
  450. ret[k] = module.mergedicts_copy(d1[k], v)
  451. else
  452. ret[k] = v
  453. end
  454. end
  455. return ret
  456. end
  457. -- dictdiff: find a diff so that mergedicts_copy(d1, diff) is equal to d2
  458. --
  459. -- Note: does not do copies of d2 values used.
  460. function module.dictdiff(d1, d2)
  461. local ret = {}
  462. local hasdiff = false
  463. for k, v in pairs(d1) do
  464. if d2[k] == nil then
  465. hasdiff = true
  466. ret[k] = module.REMOVE_THIS
  467. elseif type(v) == type(d2[k]) then
  468. if type(v) == 'table' then
  469. local subdiff = module.dictdiff(v, d2[k])
  470. if subdiff ~= nil then
  471. hasdiff = true
  472. ret[k] = subdiff
  473. end
  474. elseif v ~= d2[k] then
  475. ret[k] = d2[k]
  476. hasdiff = true
  477. end
  478. else
  479. ret[k] = d2[k]
  480. hasdiff = true
  481. end
  482. end
  483. local shallowcopy = module.shallowcopy
  484. for k, v in pairs(d2) do
  485. if d1[k] == nil then
  486. ret[k] = shallowcopy(v)
  487. hasdiff = true
  488. end
  489. end
  490. if hasdiff then
  491. return ret
  492. else
  493. return nil
  494. end
  495. end
  496. function module.updated(d, d2)
  497. for k, v in pairs(d2) do
  498. d[k] = v
  499. end
  500. return d
  501. end
  502. -- Concat list-like tables.
  503. function module.concat_tables(...)
  504. local ret = {}
  505. for i = 1, select('#', ...) do
  506. local tbl = select(i, ...)
  507. if tbl then
  508. for _, v in ipairs(tbl) do
  509. ret[#ret + 1] = v
  510. end
  511. end
  512. end
  513. return ret
  514. end
  515. function module.dedent(str, leave_indent)
  516. -- find minimum common indent across lines
  517. local indent = nil
  518. for line in str:gmatch('[^\n]+') do
  519. local line_indent = line:match('^%s+') or ''
  520. if indent == nil or #line_indent < #indent then
  521. indent = line_indent
  522. end
  523. end
  524. if indent == nil or #indent == 0 then
  525. -- no minimum common indent
  526. return str
  527. end
  528. local left_indent = (' '):rep(leave_indent or 0)
  529. -- create a pattern for the indent
  530. indent = indent:gsub('%s', '[ \t]')
  531. -- strip it from the first line
  532. str = str:gsub('^'..indent, left_indent)
  533. -- strip it from the remaining lines
  534. str = str:gsub('[\n]'..indent, '\n' .. left_indent)
  535. return str
  536. end
  537. local function format_float(v)
  538. -- On windows exponent appears to have three digits and not two
  539. local ret = ('%.6e'):format(v)
  540. local l, f, es, e = ret:match('^(%-?%d)%.(%d+)e([+%-])0*(%d%d+)$')
  541. return l .. '.' .. f .. 'e' .. es .. e
  542. end
  543. local SUBTBL = {
  544. '\\000', '\\001', '\\002', '\\003', '\\004',
  545. '\\005', '\\006', '\\007', '\\008', '\\t',
  546. '\\n', '\\011', '\\012', '\\r', '\\014',
  547. '\\015', '\\016', '\\017', '\\018', '\\019',
  548. '\\020', '\\021', '\\022', '\\023', '\\024',
  549. '\\025', '\\026', '\\027', '\\028', '\\029',
  550. '\\030', '\\031',
  551. }
  552. -- Formats Lua value `v`.
  553. --
  554. -- TODO(justinmk): redundant with vim.inspect() ?
  555. --
  556. -- "Nice table formatting similar to screen:snapshot_util()".
  557. -- Commit: 520c0b91a528
  558. function module.format_luav(v, indent, opts)
  559. opts = opts or {}
  560. local linesep = '\n'
  561. local next_indent_arg = nil
  562. local indent_shift = opts.indent_shift or ' '
  563. local next_indent
  564. local nl = '\n'
  565. if indent == nil then
  566. indent = ''
  567. linesep = ''
  568. next_indent = ''
  569. nl = ' '
  570. else
  571. next_indent_arg = indent .. indent_shift
  572. next_indent = indent .. indent_shift
  573. end
  574. local ret = ''
  575. if type(v) == 'string' then
  576. if opts.literal_strings then
  577. ret = v
  578. else
  579. local quote = opts.dquote_strings and '"' or '\''
  580. ret = quote .. tostring(v):gsub(
  581. opts.dquote_strings and '["\\]' or '[\'\\]',
  582. '\\%0'):gsub(
  583. '[%z\1-\31]', function(match)
  584. return SUBTBL[match:byte() + 1]
  585. end) .. quote
  586. end
  587. elseif type(v) == 'table' then
  588. if v == module.REMOVE_THIS then
  589. ret = 'REMOVE_THIS'
  590. else
  591. local processed_keys = {}
  592. ret = '{' .. linesep
  593. local non_empty = false
  594. local format_luav = module.format_luav
  595. for i, subv in ipairs(v) do
  596. ret = ('%s%s%s,%s'):format(ret, next_indent,
  597. format_luav(subv, next_indent_arg, opts), nl)
  598. processed_keys[i] = true
  599. non_empty = true
  600. end
  601. for k, subv in pairs(v) do
  602. if not processed_keys[k] then
  603. if type(k) == 'string' and k:match('^[a-zA-Z_][a-zA-Z0-9_]*$') then
  604. ret = ret .. next_indent .. k .. ' = '
  605. else
  606. ret = ('%s%s[%s] = '):format(ret, next_indent,
  607. format_luav(k, nil, opts))
  608. end
  609. ret = ret .. format_luav(subv, next_indent_arg, opts) .. ',' .. nl
  610. non_empty = true
  611. end
  612. end
  613. if nl == ' ' and non_empty then
  614. ret = ret:sub(1, -3)
  615. end
  616. ret = ret .. indent .. '}'
  617. end
  618. elseif type(v) == 'number' then
  619. if v % 1 == 0 then
  620. ret = ('%d'):format(v)
  621. else
  622. ret = format_float(v)
  623. end
  624. elseif type(v) == 'nil' then
  625. ret = 'nil'
  626. elseif type(v) == 'boolean' then
  627. ret = (v and 'true' or 'false')
  628. else
  629. print(type(v))
  630. -- Not implemented yet
  631. assert(false)
  632. end
  633. return ret
  634. end
  635. -- Like Python repr(), "{!r}".format(s)
  636. --
  637. -- Commit: 520c0b91a528
  638. function module.format_string(fmt, ...)
  639. local i = 0
  640. local args = {...}
  641. local function getarg()
  642. i = i + 1
  643. return args[i]
  644. end
  645. local ret = fmt:gsub('%%[0-9*]*%.?[0-9*]*[cdEefgGiouXxqsr%%]', function(match)
  646. local subfmt = match:gsub('%*', function()
  647. return tostring(getarg())
  648. end)
  649. local arg = nil
  650. if subfmt:sub(-1) ~= '%' then
  651. arg = getarg()
  652. end
  653. if subfmt:sub(-1) == 'r' or subfmt:sub(-1) == 'q' then
  654. -- %r is like built-in %q, but it is supposed to single-quote strings and
  655. -- not double-quote them, and also work not only for strings.
  656. -- Builtin %q is replaced here as it gives invalid and inconsistent with
  657. -- luajit results for e.g. "\e" on lua: luajit transforms that into `\27`,
  658. -- lua leaves as-is.
  659. arg = module.format_luav(arg, nil, {dquote_strings = (subfmt:sub(-1) == 'q')})
  660. subfmt = subfmt:sub(1, -2) .. 's'
  661. end
  662. if subfmt == '%e' then
  663. return format_float(arg)
  664. else
  665. return subfmt:format(arg)
  666. end
  667. end)
  668. return ret
  669. end
  670. function module.intchar2lua(ch)
  671. ch = tonumber(ch)
  672. return (20 <= ch and ch < 127) and ('%c'):format(ch) or ch
  673. end
  674. local fixtbl_metatable = {
  675. __newindex = function()
  676. assert(false)
  677. end,
  678. }
  679. function module.fixtbl(tbl)
  680. return setmetatable(tbl, fixtbl_metatable)
  681. end
  682. function module.fixtbl_rec(tbl)
  683. local fixtbl_rec = module.fixtbl_rec
  684. for _, v in pairs(tbl) do
  685. if type(v) == 'table' then
  686. fixtbl_rec(v)
  687. end
  688. end
  689. return module.fixtbl(tbl)
  690. end
  691. function module.hexdump(str)
  692. local len = string.len(str)
  693. local dump = ""
  694. local hex = ""
  695. local asc = ""
  696. for i = 1, len do
  697. if 1 == i % 8 then
  698. dump = dump .. hex .. asc .. "\n"
  699. hex = string.format("%04x: ", i - 1)
  700. asc = ""
  701. end
  702. local ord = string.byte(str, i)
  703. hex = hex .. string.format("%02x ", ord)
  704. if ord >= 32 and ord <= 126 then
  705. asc = asc .. string.char(ord)
  706. else
  707. asc = asc .. "."
  708. end
  709. end
  710. return dump .. hex .. string.rep(" ", 8 - len % 8) .. asc
  711. end
  712. -- Reads text lines from `filename` into a table.
  713. --
  714. -- filename: path to file
  715. -- start: start line (1-indexed), negative means "lines before end" (tail)
  716. function module.read_file_list(filename, start)
  717. local lnum = (start ~= nil and type(start) == 'number') and start or 1
  718. local tail = (lnum < 0)
  719. local maxlines = tail and math.abs(lnum) or nil
  720. local file = io.open(filename, 'r')
  721. if not file then
  722. return nil
  723. end
  724. -- There is no need to read more than the last 2MB of the log file, so seek
  725. -- to that.
  726. local file_size = file:seek("end")
  727. local offset = file_size - 2000000
  728. if offset < 0 then
  729. offset = 0
  730. end
  731. file:seek("set", offset)
  732. local lines = {}
  733. local i = 1
  734. local line = file:read("*l")
  735. while line ~= nil do
  736. if i >= start then
  737. table.insert(lines, line)
  738. if #lines > maxlines then
  739. table.remove(lines, 1)
  740. end
  741. end
  742. i = i + 1
  743. line = file:read("*l")
  744. end
  745. file:close()
  746. return lines
  747. end
  748. -- Reads the entire contents of `filename` into a string.
  749. --
  750. -- filename: path to file
  751. function module.read_file(filename)
  752. local file = io.open(filename, 'r')
  753. if not file then
  754. return nil
  755. end
  756. local ret = file:read('*a')
  757. file:close()
  758. return ret
  759. end
  760. -- Dedent the given text and write it to the file name.
  761. function module.write_file(name, text, no_dedent, append)
  762. local file = io.open(name, (append and 'a' or 'w'))
  763. if type(text) == 'table' then
  764. -- Byte blob
  765. local bytes = text
  766. text = ''
  767. for _, char in ipairs(bytes) do
  768. text = ('%s%c'):format(text, char)
  769. end
  770. elseif not no_dedent then
  771. text = module.dedent(text)
  772. end
  773. file:write(text)
  774. file:flush()
  775. file:close()
  776. end
  777. function module.is_ci(name)
  778. local any = (name == nil)
  779. assert(any or name == 'github' or name == 'cirrus')
  780. local gh = ((any or name == 'github') and nil ~= os.getenv('GITHUB_ACTIONS'))
  781. local cirrus = ((any or name == 'cirrus') and nil ~= os.getenv('CIRRUS_CI'))
  782. return gh or cirrus
  783. end
  784. -- Gets the (tail) contents of `logfile`.
  785. -- Also moves the file to "${NVIM_LOG_FILE}.displayed" on CI environments.
  786. function module.read_nvim_log(logfile, ci_rename)
  787. logfile = logfile or os.getenv('NVIM_LOG_FILE') or '.nvimlog'
  788. local is_ci = module.is_ci()
  789. local keep = is_ci and 100 or 10
  790. local lines = module.read_file_list(logfile, -keep) or {}
  791. local log = (('-'):rep(78)..'\n'
  792. ..string.format('$NVIM_LOG_FILE: %s\n', logfile)
  793. ..(#lines > 0 and '(last '..tostring(keep)..' lines)\n' or '(empty)\n'))
  794. for _,line in ipairs(lines) do
  795. log = log..line..'\n'
  796. end
  797. log = log..('-'):rep(78)..'\n'
  798. if is_ci and ci_rename then
  799. os.rename(logfile, logfile .. '.displayed')
  800. end
  801. return log
  802. end
  803. function module.mkdir(path)
  804. -- 493 is 0755 in decimal
  805. return luv.fs_mkdir(path, 493)
  806. end
  807. module = shared.tbl_extend('error', module, Paths, shared, require('test.deprecated'))
  808. return module