helpers.lua 23 KB

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