fs_spec.lua 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458
  1. local t = require('test.testutil')
  2. local n = require('test.functional.testnvim')()
  3. local clear = n.clear
  4. local exec_lua = n.exec_lua
  5. local eq = t.eq
  6. local mkdir_p = n.mkdir_p
  7. local rmdir = n.rmdir
  8. local nvim_dir = n.nvim_dir
  9. local command = n.command
  10. local api = n.api
  11. local test_build_dir = t.paths.test_build_dir
  12. local test_source_path = t.paths.test_source_path
  13. local nvim_prog = n.nvim_prog
  14. local is_os = t.is_os
  15. local mkdir = t.mkdir
  16. local nvim_prog_basename = is_os('win') and 'nvim.exe' or 'nvim'
  17. local test_basename_dirname_eq = {
  18. '~/foo/',
  19. '~/foo',
  20. '~/foo/bar.lua',
  21. 'foo.lua',
  22. ' ',
  23. '',
  24. '.',
  25. '..',
  26. '../',
  27. '~',
  28. '/usr/bin',
  29. '/usr/bin/gcc',
  30. '/',
  31. '/usr/',
  32. '/usr',
  33. 'c:/usr',
  34. 'c:/',
  35. 'c:',
  36. 'c:/users/foo',
  37. 'c:/users/foo/bar.lua',
  38. 'c:/users/foo/bar/../',
  39. '~/foo/bar\\baz',
  40. }
  41. local tests_windows_paths = {
  42. 'c:\\usr',
  43. 'c:\\',
  44. 'c:',
  45. 'c:\\users\\foo',
  46. 'c:\\users\\foo\\bar.lua',
  47. 'c:\\users\\foo\\bar\\..\\',
  48. }
  49. before_each(clear)
  50. describe('vim.fs', function()
  51. describe('parents()', function()
  52. it('works', function()
  53. local test_dir = nvim_dir .. '/test'
  54. mkdir_p(test_dir)
  55. local dirs = {} --- @type string[]
  56. for dir in vim.fs.parents(test_dir .. '/foo.txt') do
  57. dirs[#dirs + 1] = dir
  58. if dir == test_build_dir then
  59. break
  60. end
  61. end
  62. eq({ test_dir, nvim_dir, test_build_dir }, dirs)
  63. rmdir(test_dir)
  64. end)
  65. end)
  66. describe('dirname()', function()
  67. it('works', function()
  68. eq(test_build_dir, vim.fs.dirname(nvim_dir))
  69. ---@param paths string[]
  70. ---@param is_win? boolean
  71. local function test_paths(paths, is_win)
  72. local gsub = is_win and [[:gsub('\\', '/')]] or ''
  73. local code = string.format(
  74. [[
  75. local path = ...
  76. return vim.fn.fnamemodify(path,':h')%s
  77. ]],
  78. gsub
  79. )
  80. for _, path in ipairs(paths) do
  81. eq(exec_lua(code, path), vim.fs.dirname(path), path)
  82. end
  83. end
  84. test_paths(test_basename_dirname_eq)
  85. if is_os('win') then
  86. test_paths(tests_windows_paths, true)
  87. end
  88. end)
  89. end)
  90. describe('basename()', function()
  91. it('works', function()
  92. eq(nvim_prog_basename, vim.fs.basename(nvim_prog))
  93. ---@param paths string[]
  94. ---@param is_win? boolean
  95. local function test_paths(paths, is_win)
  96. local gsub = is_win and [[:gsub('\\', '/')]] or ''
  97. local code = string.format(
  98. [[
  99. local path = ...
  100. return vim.fn.fnamemodify(path,':t')%s
  101. ]],
  102. gsub
  103. )
  104. for _, path in ipairs(paths) do
  105. eq(exec_lua(code, path), vim.fs.basename(path), path)
  106. end
  107. end
  108. test_paths(test_basename_dirname_eq)
  109. if is_os('win') then
  110. test_paths(tests_windows_paths, true)
  111. end
  112. end)
  113. end)
  114. describe('dir()', function()
  115. before_each(function()
  116. mkdir('testd')
  117. mkdir('testd/a')
  118. mkdir('testd/a/b')
  119. mkdir('testd/a/b/c')
  120. end)
  121. after_each(function()
  122. rmdir('testd')
  123. end)
  124. it('works', function()
  125. eq(
  126. true,
  127. exec_lua(function()
  128. for name, type in vim.fs.dir(nvim_dir) do
  129. if name == nvim_prog_basename and type == 'file' then
  130. return true
  131. end
  132. end
  133. return false
  134. end)
  135. )
  136. end)
  137. it('works with opts.depth and opts.skip', function()
  138. io.open('testd/a1', 'w'):close()
  139. io.open('testd/b1', 'w'):close()
  140. io.open('testd/c1', 'w'):close()
  141. io.open('testd/a/a2', 'w'):close()
  142. io.open('testd/a/b2', 'w'):close()
  143. io.open('testd/a/c2', 'w'):close()
  144. io.open('testd/a/b/a3', 'w'):close()
  145. io.open('testd/a/b/b3', 'w'):close()
  146. io.open('testd/a/b/c3', 'w'):close()
  147. io.open('testd/a/b/c/a4', 'w'):close()
  148. io.open('testd/a/b/c/b4', 'w'):close()
  149. io.open('testd/a/b/c/c4', 'w'):close()
  150. local function run(dir, depth, skip)
  151. return exec_lua(function()
  152. local r = {}
  153. local skip_f
  154. if skip then
  155. skip_f = function(n0)
  156. if vim.tbl_contains(skip or {}, n0) then
  157. return false
  158. end
  159. end
  160. end
  161. for name, type_ in vim.fs.dir(dir, { depth = depth, skip = skip_f }) do
  162. r[name] = type_
  163. end
  164. return r
  165. end)
  166. end
  167. local exp = {}
  168. exp['a1'] = 'file'
  169. exp['b1'] = 'file'
  170. exp['c1'] = 'file'
  171. exp['a'] = 'directory'
  172. eq(exp, run('testd', 1))
  173. exp['a/a2'] = 'file'
  174. exp['a/b2'] = 'file'
  175. exp['a/c2'] = 'file'
  176. exp['a/b'] = 'directory'
  177. eq(exp, run('testd', 2))
  178. exp['a/b/a3'] = 'file'
  179. exp['a/b/b3'] = 'file'
  180. exp['a/b/c3'] = 'file'
  181. exp['a/b/c'] = 'directory'
  182. eq(exp, run('testd', 3))
  183. eq(exp, run('testd', 999, { 'a/b/c' }))
  184. exp['a/b/c/a4'] = 'file'
  185. exp['a/b/c/b4'] = 'file'
  186. exp['a/b/c/c4'] = 'file'
  187. eq(exp, run('testd', 999))
  188. end)
  189. end)
  190. describe('find()', function()
  191. it('works', function()
  192. eq(
  193. { test_build_dir .. '/build' },
  194. vim.fs.find('build', { path = nvim_dir, upward = true, type = 'directory' })
  195. )
  196. eq({ nvim_prog }, vim.fs.find(nvim_prog_basename, { path = test_build_dir, type = 'file' }))
  197. local parent, name = nvim_dir:match('^(.*/)([^/]+)$')
  198. eq({ nvim_dir }, vim.fs.find(name, { path = parent, upward = true, type = 'directory' }))
  199. end)
  200. it('accepts predicate as names', function()
  201. local opts = { path = nvim_dir, upward = true, type = 'directory' }
  202. eq(
  203. { test_build_dir .. '/build' },
  204. vim.fs.find(function(x)
  205. return x == 'build'
  206. end, opts)
  207. )
  208. eq(
  209. { nvim_prog },
  210. vim.fs.find(function(x)
  211. return x == nvim_prog_basename
  212. end, { path = test_build_dir, type = 'file' })
  213. )
  214. eq(
  215. {},
  216. vim.fs.find(function(x)
  217. return x == 'no-match'
  218. end, opts)
  219. )
  220. opts = { path = test_source_path .. '/contrib', limit = math.huge }
  221. eq(
  222. exec_lua(function()
  223. return vim.tbl_map(
  224. vim.fs.basename,
  225. vim.fn.glob(test_source_path .. '/contrib/*', false, true)
  226. )
  227. end),
  228. vim.tbl_map(
  229. vim.fs.basename,
  230. vim.fs.find(function(_, d)
  231. return d:match('[\\/]contrib$')
  232. end, opts)
  233. )
  234. )
  235. end)
  236. end)
  237. describe('root()', function()
  238. before_each(function()
  239. command('edit test/functional/fixtures/tty-test.c')
  240. end)
  241. it('works with a single marker', function()
  242. eq(test_source_path, exec_lua([[return vim.fs.root(0, 'CMakePresets.json')]]))
  243. end)
  244. it('works with multiple markers', function()
  245. local bufnr = api.nvim_get_current_buf()
  246. eq(
  247. vim.fs.joinpath(test_source_path, 'test/functional/fixtures'),
  248. exec_lua([[return vim.fs.root(..., {'CMakeLists.txt', 'CMakePresets.json'})]], bufnr)
  249. )
  250. end)
  251. it('works with a function', function()
  252. ---@type string
  253. local result = exec_lua(function()
  254. return vim.fs.root(0, function(name, _)
  255. return name:match('%.txt$')
  256. end)
  257. end)
  258. eq(vim.fs.joinpath(test_source_path, 'test/functional/fixtures'), result)
  259. end)
  260. it('works with a filename argument', function()
  261. eq(test_source_path, exec_lua([[return vim.fs.root(..., 'CMakePresets.json')]], nvim_prog))
  262. end)
  263. it('works with a relative path', function()
  264. eq(
  265. test_source_path,
  266. exec_lua([[return vim.fs.root(..., 'CMakePresets.json')]], vim.fs.basename(nvim_prog))
  267. )
  268. end)
  269. it('uses cwd for unnamed buffers', function()
  270. command('new')
  271. eq(test_source_path, exec_lua([[return vim.fs.root(0, 'CMakePresets.json')]]))
  272. end)
  273. it("uses cwd for buffers with non-empty 'buftype'", function()
  274. command('new')
  275. command('set buftype=nofile')
  276. command('file lua://')
  277. eq(test_source_path, exec_lua([[return vim.fs.root(0, 'CMakePresets.json')]]))
  278. end)
  279. end)
  280. describe('joinpath()', function()
  281. it('works', function()
  282. eq('foo/bar/baz', vim.fs.joinpath('foo', 'bar', 'baz'))
  283. eq('foo/bar/baz', vim.fs.joinpath('foo', '/bar/', '/baz'))
  284. end)
  285. end)
  286. describe('normalize()', function()
  287. it('removes trailing /', function()
  288. eq('/home/user', vim.fs.normalize('/home/user/'))
  289. end)
  290. it('works with /', function()
  291. eq('/', vim.fs.normalize('/'))
  292. end)
  293. it('works with ~', function()
  294. eq(vim.fs.normalize(assert(vim.uv.os_homedir())) .. '/src/foo', vim.fs.normalize('~/src/foo'))
  295. end)
  296. it('works with environment variables', function()
  297. local xdg_config_home = test_build_dir .. '/.config'
  298. eq(
  299. xdg_config_home .. '/nvim',
  300. exec_lua(function()
  301. vim.env.XDG_CONFIG_HOME = xdg_config_home
  302. return vim.fs.normalize('$XDG_CONFIG_HOME/nvim')
  303. end)
  304. )
  305. end)
  306. -- Opts required for testing posix paths and win paths
  307. local posix_opts = is_os('win') and { win = false } or {}
  308. local win_opts = is_os('win') and {} or { win = true }
  309. it('preserves leading double slashes in POSIX paths', function()
  310. eq('//foo', vim.fs.normalize('//foo', posix_opts))
  311. eq('//foo/bar', vim.fs.normalize('//foo//bar////', posix_opts))
  312. eq('/foo', vim.fs.normalize('///foo', posix_opts))
  313. eq('//', vim.fs.normalize('//', posix_opts))
  314. eq('/', vim.fs.normalize('///', posix_opts))
  315. eq('/foo/bar', vim.fs.normalize('/foo//bar////', posix_opts))
  316. end)
  317. it('allows backslashes on unix-based os', function()
  318. eq('/home/user/hello\\world', vim.fs.normalize('/home/user/hello\\world', posix_opts))
  319. end)
  320. it('preserves / after drive letters', function()
  321. eq('C:/', vim.fs.normalize([[C:\]], win_opts))
  322. end)
  323. it('works with UNC and DOS device paths', function()
  324. eq('//server/share/foo/bar', vim.fs.normalize([[\\server\\share\\\foo\bar\\\]], win_opts))
  325. eq('//system07/C$/', vim.fs.normalize([[\\system07\C$\\\\]], win_opts))
  326. eq('//./C:/foo/bar', vim.fs.normalize([[\\.\\C:\foo\\\\bar]], win_opts))
  327. eq('//?/C:/foo/bar', vim.fs.normalize([[\\?\C:\\\foo\bar\\\\]], win_opts))
  328. eq(
  329. '//?/UNC/server/share/foo/bar',
  330. vim.fs.normalize([[\\?\UNC\server\\\share\\\\foo\\\bar]], win_opts)
  331. )
  332. eq('//./BootPartition/foo/bar', vim.fs.normalize([[\\.\BootPartition\\foo\bar]], win_opts))
  333. eq(
  334. '//./Volume{12345678-1234-1234-1234-1234567890AB}/foo/bar',
  335. vim.fs.normalize([[\\.\Volume{12345678-1234-1234-1234-1234567890AB}\\\foo\bar\\]], win_opts)
  336. )
  337. end)
  338. it('handles invalid UNC and DOS device paths', function()
  339. eq('//server/share', vim.fs.normalize([[\\server\share]], win_opts))
  340. eq('//server/', vim.fs.normalize([[\\server\]], win_opts))
  341. eq('//./UNC/server/share', vim.fs.normalize([[\\.\UNC\server\share]], win_opts))
  342. eq('//?/UNC/server/', vim.fs.normalize([[\\?\UNC\server\]], win_opts))
  343. eq('//?/UNC/server/..', vim.fs.normalize([[\\?\UNC\server\..]], win_opts))
  344. eq('//./', vim.fs.normalize([[\\.\]], win_opts))
  345. eq('//./foo', vim.fs.normalize([[\\.\foo]], win_opts))
  346. eq('//./BootPartition', vim.fs.normalize([[\\.\BootPartition]], win_opts))
  347. end)
  348. it('converts backward slashes', function()
  349. eq('C:/Users/jdoe', vim.fs.normalize([[C:\Users\jdoe]], win_opts))
  350. end)
  351. describe('. and .. component resolving', function()
  352. it('works', function()
  353. -- Windows paths
  354. eq('C:/Users', vim.fs.normalize([[C:\Users\jdoe\Downloads\.\..\..\]], win_opts))
  355. eq('C:/Users/jdoe', vim.fs.normalize([[C:\Users\jdoe\Downloads\.\..\.\.\]], win_opts))
  356. eq('C:/', vim.fs.normalize('C:/Users/jdoe/Downloads/./../../../', win_opts))
  357. eq('C:foo', vim.fs.normalize([[C:foo\bar\.\..\.]], win_opts))
  358. -- POSIX paths
  359. eq('/home', vim.fs.normalize('/home/jdoe/Downloads/./../..', posix_opts))
  360. eq('/home/jdoe', vim.fs.normalize('/home/jdoe/Downloads/./../././', posix_opts))
  361. eq('/', vim.fs.normalize('/home/jdoe/Downloads/./../../../', posix_opts))
  362. -- OS-agnostic relative paths
  363. eq('foo/bar/baz', vim.fs.normalize('foo/bar/foobar/../baz/./'))
  364. eq('foo/bar', vim.fs.normalize('foo/bar/foobar/../baz/./../../bar/./.'))
  365. end)
  366. it('works when relative path reaches current directory', function()
  367. eq('C:', vim.fs.normalize('C:foo/bar/../../.', win_opts))
  368. eq('.', vim.fs.normalize('.'))
  369. eq('.', vim.fs.normalize('././././'))
  370. eq('.', vim.fs.normalize('foo/bar/../../.'))
  371. end)
  372. it('works when relative path goes outside current directory', function()
  373. eq('../../foo/bar', vim.fs.normalize('../../foo/bar'))
  374. eq('../foo', vim.fs.normalize('foo/bar/../../../foo'))
  375. eq('C:../foo', vim.fs.normalize('C:../foo', win_opts))
  376. eq('C:../../foo/bar', vim.fs.normalize('C:foo/../../../foo/bar', win_opts))
  377. end)
  378. it('.. in root directory resolves to itself', function()
  379. eq('C:/', vim.fs.normalize('C:/../../', win_opts))
  380. eq('C:/foo', vim.fs.normalize('C:/foo/../../foo', win_opts))
  381. eq('//server/share/', vim.fs.normalize([[\\server\share\..\..]], win_opts))
  382. eq('//server/share/foo', vim.fs.normalize([[\\server\\share\foo\..\..\foo]], win_opts))
  383. eq('//./C:/', vim.fs.normalize([[\\.\C:\..\..]], win_opts))
  384. eq('//?/C:/foo', vim.fs.normalize([[\\?\C:\..\..\foo]], win_opts))
  385. eq('//./UNC/server/share/', vim.fs.normalize([[\\.\UNC\\server\share\..\..\]], win_opts))
  386. eq(
  387. '//?/UNC/server/share/foo',
  388. vim.fs.normalize([[\\?\UNC\server\\share\..\..\foo]], win_opts)
  389. )
  390. eq('//?/BootPartition/', vim.fs.normalize([[\\?\BootPartition\..\..]], win_opts))
  391. eq('//./BootPartition/foo', vim.fs.normalize([[\\.\BootPartition\..\..\foo]], win_opts))
  392. eq('/', vim.fs.normalize('/../../', posix_opts))
  393. eq('/foo', vim.fs.normalize('/foo/../../foo', posix_opts))
  394. end)
  395. end)
  396. end)
  397. end)