fs_spec.lua 38 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104
  1. local lfs = require('lfs')
  2. local bit = require('bit')
  3. local helpers = require('test.unit.helpers')(after_each)
  4. local itp = helpers.gen_itp(it)
  5. local cimport = helpers.cimport
  6. local cppimport = helpers.cppimport
  7. local internalize = helpers.internalize
  8. local ok = helpers.ok
  9. local eq = helpers.eq
  10. local neq = helpers.neq
  11. local ffi = helpers.ffi
  12. local cstr = helpers.cstr
  13. local to_cstr = helpers.to_cstr
  14. local OK = helpers.OK
  15. local FAIL = helpers.FAIL
  16. local NULL = helpers.NULL
  17. local NODE_NORMAL = 0
  18. local NODE_WRITABLE = 1
  19. cimport('./src/nvim/os/shell.h')
  20. cimport('./src/nvim/option_defs.h')
  21. cimport('./src/nvim/main.h')
  22. cimport('./src/nvim/fileio.h')
  23. local fs = cimport('./src/nvim/os/os.h', './src/nvim/path.h')
  24. cppimport('sys/stat.h')
  25. cppimport('fcntl.h')
  26. cppimport('uv.h')
  27. local s = ''
  28. for i = 0, 255 do
  29. s = s .. (i == 0 and '\0' or ('%c'):format(i))
  30. end
  31. local fcontents = s:rep(16)
  32. local directory = nil
  33. local absolute_executable = nil
  34. local executable_name = nil
  35. local function set_bit(number, to_set)
  36. return bit.bor(number, to_set)
  37. end
  38. local function unset_bit(number, to_unset)
  39. return bit.band(number, (bit.bnot(to_unset)))
  40. end
  41. local function assert_file_exists(filepath)
  42. neq(nil, lfs.attributes(filepath))
  43. end
  44. local function assert_file_does_not_exist(filepath)
  45. eq(nil, lfs.attributes(filepath))
  46. end
  47. local function os_setperm(filename, perm)
  48. return fs.os_setperm((to_cstr(filename)), perm)
  49. end
  50. local function os_getperm(filename)
  51. local perm = fs.os_getperm((to_cstr(filename)))
  52. return tonumber(perm)
  53. end
  54. describe('fs.c', function()
  55. local function os_isdir(name)
  56. return fs.os_isdir(to_cstr(name))
  57. end
  58. before_each(function()
  59. lfs.mkdir('unit-test-directory');
  60. io.open('unit-test-directory/test.file', 'w').close()
  61. io.open('unit-test-directory/test_2.file', 'w').close()
  62. lfs.link('test.file', 'unit-test-directory/test_link.file', true)
  63. lfs.link('non_existing_file.file', 'unit-test-directory/test_broken_link.file', true)
  64. -- The tests are invoked with an absolute path to `busted` executable.
  65. absolute_executable = arg[0]
  66. -- Split the absolute_executable path into a directory and filename.
  67. directory, executable_name = string.match(absolute_executable, '^(.*)/(.*)$')
  68. end)
  69. after_each(function()
  70. os.remove('unit-test-directory/test.file')
  71. os.remove('unit-test-directory/test_2.file')
  72. os.remove('unit-test-directory/test_link.file')
  73. os.remove('unit-test-directory/test_hlink.file')
  74. os.remove('unit-test-directory/test_broken_link.file')
  75. lfs.rmdir('unit-test-directory')
  76. end)
  77. describe('os_dirname', function()
  78. itp('returns OK and writes current directory to the buffer', function()
  79. local length = string.len(lfs.currentdir()) + 1
  80. local buf = cstr(length, '')
  81. eq(OK, fs.os_dirname(buf, length))
  82. eq(lfs.currentdir(), ffi.string(buf))
  83. end)
  84. itp('returns FAIL if the buffer is too small', function()
  85. local length = string.len(lfs.currentdir()) + 1
  86. local buf = cstr(length - 1, '')
  87. eq(FAIL, fs.os_dirname(buf, length - 1))
  88. end)
  89. end)
  90. describe('os_chdir', function()
  91. itp('fails with path="~"', function()
  92. eq(false, os_isdir('~')) -- sanity check: no literal "~" directory.
  93. local length = 4096
  94. local expected_cwd = cstr(length, '')
  95. local cwd = cstr(length, '')
  96. eq(OK, fs.os_dirname(expected_cwd, length))
  97. -- os_chdir returns 0 for success, not OK (1).
  98. neq(0, fs.os_chdir('~')) -- fail
  99. neq(0, fs.os_chdir('~/')) -- fail
  100. eq(OK, fs.os_dirname(cwd, length))
  101. -- CWD did not change.
  102. eq(ffi.string(expected_cwd), ffi.string(cwd))
  103. end)
  104. end)
  105. describe('os_isdir', function()
  106. itp('returns false if an empty string is given', function()
  107. eq(false, (os_isdir('')))
  108. end)
  109. itp('returns false if a nonexisting directory is given', function()
  110. eq(false, (os_isdir('non-existing-directory')))
  111. end)
  112. itp('returns false if a nonexisting absolute directory is given', function()
  113. eq(false, (os_isdir('/non-existing-directory')))
  114. end)
  115. itp('returns false if an existing file is given', function()
  116. eq(false, (os_isdir('unit-test-directory/test.file')))
  117. end)
  118. itp('returns true if the current directory is given', function()
  119. eq(true, (os_isdir('.')))
  120. end)
  121. itp('returns true if the parent directory is given', function()
  122. eq(true, (os_isdir('..')))
  123. end)
  124. itp('returns true if an arbitrary directory is given', function()
  125. eq(true, (os_isdir('unit-test-directory')))
  126. end)
  127. itp('returns true if an absolute directory is given', function()
  128. eq(true, (os_isdir(directory)))
  129. end)
  130. end)
  131. describe('os_can_exe', function()
  132. local function os_can_exe(name)
  133. local buf = ffi.new('char *[1]')
  134. buf[0] = NULL
  135. local ce_ret = fs.os_can_exe(to_cstr(name), buf, true)
  136. -- When os_can_exe returns true, it must set the path.
  137. -- When it returns false, the path must be NULL.
  138. if ce_ret then
  139. neq(NULL, buf[0])
  140. return internalize(buf[0])
  141. else
  142. eq(NULL, buf[0])
  143. return nil
  144. end
  145. end
  146. local function cant_exe(name)
  147. eq(nil, os_can_exe(name))
  148. end
  149. local function exe(name)
  150. return os_can_exe(name)
  151. end
  152. itp('returns false when given a directory', function()
  153. cant_exe('./unit-test-directory')
  154. end)
  155. itp('returns false when given a regular file without executable bit set', function()
  156. cant_exe('unit-test-directory/test.file')
  157. end)
  158. itp('returns false when the given file does not exists', function()
  159. cant_exe('does-not-exist.file')
  160. end)
  161. itp('returns the absolute path when given an executable inside $PATH', function()
  162. local fullpath = exe('ls')
  163. eq(1, fs.path_is_absolute(to_cstr(fullpath)))
  164. end)
  165. itp('returns the absolute path when given an executable relative to the current dir', function()
  166. local old_dir = lfs.currentdir()
  167. lfs.chdir(directory)
  168. -- Rely on currentdir to resolve symlinks, if any. Testing against
  169. -- the absolute path taken from arg[0] may result in failure where
  170. -- the path has a symlink in it.
  171. local canonical = lfs.currentdir() .. '/' .. executable_name
  172. local expected = exe(canonical)
  173. local relative_executable = './' .. executable_name
  174. local res = exe(relative_executable)
  175. -- Don't test yet; we need to chdir back first.
  176. lfs.chdir(old_dir)
  177. eq(expected, res)
  178. end)
  179. end)
  180. describe('file permissions', function()
  181. local function os_fchown(filename, user_id, group_id)
  182. local fd = ffi.C.open(filename, 0)
  183. local res = fs.os_fchown(fd, user_id, group_id)
  184. ffi.C.close(fd)
  185. return res
  186. end
  187. local function os_file_is_readable(filename)
  188. return fs.os_file_is_readable((to_cstr(filename)))
  189. end
  190. local function os_file_is_writable(filename)
  191. return fs.os_file_is_writable((to_cstr(filename)))
  192. end
  193. local function bit_set(number, check_bit)
  194. return 0 ~= (bit.band(number, check_bit))
  195. end
  196. describe('os_getperm', function()
  197. itp('returns UV_ENOENT when the given file does not exist', function()
  198. eq(ffi.C.UV_ENOENT, (os_getperm('non-existing-file')))
  199. end)
  200. itp('returns a perm > 0 when given an existing file', function()
  201. assert.is_true((os_getperm('unit-test-directory')) > 0)
  202. end)
  203. itp('returns S_IRUSR when the file is readable', function()
  204. local perm = os_getperm('unit-test-directory')
  205. assert.is_true((bit_set(perm, ffi.C.kS_IRUSR)))
  206. end)
  207. end)
  208. describe('os_setperm', function()
  209. itp('can set and unset the executable bit of a file', function()
  210. local perm = os_getperm('unit-test-directory/test.file')
  211. perm = unset_bit(perm, ffi.C.kS_IXUSR)
  212. eq(OK, (os_setperm('unit-test-directory/test.file', perm)))
  213. perm = os_getperm('unit-test-directory/test.file')
  214. assert.is_false((bit_set(perm, ffi.C.kS_IXUSR)))
  215. perm = set_bit(perm, ffi.C.kS_IXUSR)
  216. eq(OK, os_setperm('unit-test-directory/test.file', perm))
  217. perm = os_getperm('unit-test-directory/test.file')
  218. assert.is_true((bit_set(perm, ffi.C.kS_IXUSR)))
  219. end)
  220. itp('fails if given file does not exist', function()
  221. local perm = ffi.C.kS_IXUSR
  222. eq(FAIL, (os_setperm('non-existing-file', perm)))
  223. end)
  224. end)
  225. describe('os_fchown', function()
  226. local filename = 'unit-test-directory/test.file'
  227. itp('does not change owner and group if respective IDs are equal to -1', function()
  228. local uid = lfs.attributes(filename, 'uid')
  229. local gid = lfs.attributes(filename, 'gid')
  230. eq(0, os_fchown(filename, -1, -1))
  231. eq(uid, lfs.attributes(filename, 'uid'))
  232. return eq(gid, lfs.attributes(filename, 'gid'))
  233. end)
  234. -- Some systems may not have `id` utility.
  235. if (os.execute('id -G > /dev/null 2>&1') ~= 0) then
  236. pending('skipped (missing `id` utility)', function() end)
  237. else
  238. itp('owner of a file may change the group of the file to any group of which that owner is a member', function()
  239. local file_gid = lfs.attributes(filename, 'gid')
  240. -- Gets ID of any group of which current user is a member except the
  241. -- group that owns the file.
  242. local id_fd = io.popen('id -G')
  243. local new_gid = id_fd:read('*n')
  244. if (new_gid == file_gid) then
  245. new_gid = id_fd:read('*n')
  246. end
  247. id_fd:close()
  248. -- User can be a member of only one group.
  249. -- In that case we can not perform this test.
  250. if new_gid then
  251. eq(0, (os_fchown(filename, -1, new_gid)))
  252. eq(new_gid, (lfs.attributes(filename, 'gid')))
  253. end
  254. end)
  255. end
  256. if (ffi.os == 'Windows' or ffi.C.geteuid() == 0) then
  257. pending('skipped (uv_fs_chown is no-op on Windows)', function() end)
  258. else
  259. itp('returns nonzero if process has not enough permissions', function()
  260. -- chown to root
  261. neq(0, os_fchown(filename, 0, 0))
  262. end)
  263. end
  264. end)
  265. describe('os_file_is_readable', function()
  266. itp('returns false if the file is not readable', function()
  267. local perm = os_getperm('unit-test-directory/test.file')
  268. perm = unset_bit(perm, ffi.C.kS_IRUSR)
  269. perm = unset_bit(perm, ffi.C.kS_IRGRP)
  270. perm = unset_bit(perm, ffi.C.kS_IROTH)
  271. eq(OK, (os_setperm('unit-test-directory/test.file', perm)))
  272. eq(false, os_file_is_readable('unit-test-directory/test.file'))
  273. end)
  274. itp('returns false if the file does not exist', function()
  275. eq(false, os_file_is_readable(
  276. 'unit-test-directory/what_are_you_smoking.gif'))
  277. end)
  278. itp('returns true if the file is readable', function()
  279. eq(true, os_file_is_readable(
  280. 'unit-test-directory/test.file'))
  281. end)
  282. end)
  283. describe('os_file_is_writable', function()
  284. itp('returns 0 if the file is readonly', function()
  285. local perm = os_getperm('unit-test-directory/test.file')
  286. perm = unset_bit(perm, ffi.C.kS_IWUSR)
  287. perm = unset_bit(perm, ffi.C.kS_IWGRP)
  288. perm = unset_bit(perm, ffi.C.kS_IWOTH)
  289. eq(OK, (os_setperm('unit-test-directory/test.file', perm)))
  290. eq(0, os_file_is_writable('unit-test-directory/test.file'))
  291. end)
  292. itp('returns 1 if the file is writable', function()
  293. eq(1, os_file_is_writable('unit-test-directory/test.file'))
  294. end)
  295. itp('returns 2 when given a folder with rights to write into', function()
  296. eq(2, os_file_is_writable('unit-test-directory'))
  297. end)
  298. end)
  299. end)
  300. describe('file operations', function()
  301. local function os_path_exists(filename)
  302. return fs.os_path_exists((to_cstr(filename)))
  303. end
  304. local function os_rename(path, new_path)
  305. return fs.os_rename((to_cstr(path)), (to_cstr(new_path)))
  306. end
  307. local function os_remove(path)
  308. return fs.os_remove((to_cstr(path)))
  309. end
  310. local function os_open(path, flags, mode)
  311. return fs.os_open((to_cstr(path)), flags, mode)
  312. end
  313. local function os_close(fd)
  314. return fs.os_close(fd)
  315. end
  316. -- For some reason if length of NUL-bytes-string is the same as `char[?]`
  317. -- size luajit crashes. Though it does not do so in this test suite, better
  318. -- be cautios and allocate more elements then needed. I only did this to
  319. -- strings.
  320. local function os_read(fd, size)
  321. local buf = nil
  322. if size == nil then
  323. size = 0
  324. else
  325. buf = ffi.new('char[?]', size + 1, ('\0'):rep(size))
  326. end
  327. local eof = ffi.new('bool[?]', 1, {true})
  328. local ret2 = fs.os_read(fd, eof, buf, size, false)
  329. local ret1 = eof[0]
  330. local ret3 = ''
  331. if buf ~= nil then
  332. ret3 = ffi.string(buf, size)
  333. end
  334. return ret1, ret2, ret3
  335. end
  336. local function os_readv(fd, sizes)
  337. local bufs = {}
  338. for i, size in ipairs(sizes) do
  339. bufs[i] = {
  340. iov_base=ffi.new('char[?]', size + 1, ('\0'):rep(size)),
  341. iov_len=size,
  342. }
  343. end
  344. local iov = ffi.new('struct iovec[?]', #sizes, bufs)
  345. local eof = ffi.new('bool[?]', 1, {true})
  346. local ret2 = fs.os_readv(fd, eof, iov, #sizes, false)
  347. local ret1 = eof[0]
  348. local ret3 = {}
  349. for i = 1,#sizes do
  350. -- Warning: iov may not be used.
  351. ret3[i] = ffi.string(bufs[i].iov_base, bufs[i].iov_len)
  352. end
  353. return ret1, ret2, ret3
  354. end
  355. local function os_write(fd, data)
  356. return fs.os_write(fd, data, data and #data or 0, false)
  357. end
  358. describe('os_path_exists', function()
  359. itp('returns false when given a non-existing file', function()
  360. eq(false, (os_path_exists('non-existing-file')))
  361. end)
  362. itp('returns true when given an existing file', function()
  363. eq(true, (os_path_exists('unit-test-directory/test.file')))
  364. end)
  365. itp('returns false when given a broken symlink', function()
  366. eq(false, (os_path_exists('unit-test-directory/test_broken_link.file')))
  367. end)
  368. itp('returns true when given a directory', function()
  369. eq(true, (os_path_exists('unit-test-directory')))
  370. end)
  371. end)
  372. describe('os_rename', function()
  373. local test = 'unit-test-directory/test.file'
  374. local not_exist = 'unit-test-directory/not_exist.file'
  375. itp('can rename file if destination file does not exist', function()
  376. eq(OK, (os_rename(test, not_exist)))
  377. eq(false, (os_path_exists(test)))
  378. eq(true, (os_path_exists(not_exist)))
  379. eq(OK, (os_rename(not_exist, test))) -- restore test file
  380. end)
  381. itp('fail if source file does not exist', function()
  382. eq(FAIL, (os_rename(not_exist, test)))
  383. end)
  384. itp('can overwrite destination file if it exists', function()
  385. local other = 'unit-test-directory/other.file'
  386. local file = io.open(other, 'w')
  387. file:write('other')
  388. file:flush()
  389. file:close()
  390. eq(OK, (os_rename(other, test)))
  391. eq(false, (os_path_exists(other)))
  392. eq(true, (os_path_exists(test)))
  393. file = io.open(test, 'r')
  394. eq('other', (file:read('*all')))
  395. file:close()
  396. end)
  397. end)
  398. describe('os_remove', function()
  399. before_each(function()
  400. io.open('unit-test-directory/test_remove.file', 'w').close()
  401. end)
  402. after_each(function()
  403. os.remove('unit-test-directory/test_remove.file')
  404. end)
  405. itp('returns non-zero when given a non-existing file', function()
  406. neq(0, (os_remove('non-existing-file')))
  407. end)
  408. itp('removes the given file and returns 0', function()
  409. local f = 'unit-test-directory/test_remove.file'
  410. assert_file_exists(f)
  411. eq(0, (os_remove(f)))
  412. assert_file_does_not_exist(f)
  413. end)
  414. end)
  415. describe('os_dup', function()
  416. itp('returns new file descriptor', function()
  417. local dup0 = fs.os_dup(0)
  418. local dup1 = fs.os_dup(1)
  419. local dup2 = fs.os_dup(2)
  420. local tbl = {[0]=true, [1]=true, [2]=true,
  421. [tonumber(dup0)]=true, [tonumber(dup1)]=true,
  422. [tonumber(dup2)]=true}
  423. local i = 0
  424. for _, _ in pairs(tbl) do
  425. i = i + 1
  426. end
  427. eq(i, 6) -- All fds must be unique
  428. end)
  429. end)
  430. describe('os_open', function()
  431. local new_file = 'test_new_file'
  432. local existing_file = 'unit-test-directory/test_existing.file'
  433. before_each(function()
  434. (io.open(existing_file, 'w')):close()
  435. end)
  436. after_each(function()
  437. os.remove(existing_file)
  438. os.remove(new_file)
  439. end)
  440. itp('returns UV_ENOENT for O_RDWR on a non-existing file', function()
  441. eq(ffi.C.UV_ENOENT, (os_open('non-existing-file', ffi.C.kO_RDWR, 0)))
  442. end)
  443. itp('returns non-negative for O_CREAT on a non-existing file which then can be closed', function()
  444. assert_file_does_not_exist(new_file)
  445. local fd = os_open(new_file, ffi.C.kO_CREAT, 0)
  446. assert.is_true(0 <= fd)
  447. eq(0, os_close(fd))
  448. end)
  449. itp('returns non-negative for O_CREAT on a existing file which then can be closed', function()
  450. assert_file_exists(existing_file)
  451. local fd = os_open(existing_file, ffi.C.kO_CREAT, 0)
  452. assert.is_true(0 <= fd)
  453. eq(0, os_close(fd))
  454. end)
  455. itp('returns UV_EEXIST for O_CREAT|O_EXCL on a existing file', function()
  456. assert_file_exists(existing_file)
  457. eq(ffi.C.kUV_EEXIST, (os_open(existing_file, (bit.bor(ffi.C.kO_CREAT, ffi.C.kO_EXCL)), 0)))
  458. end)
  459. itp('sets `rwx` permissions for O_CREAT 700 which then can be closed', function()
  460. assert_file_does_not_exist(new_file)
  461. --create the file
  462. local fd = os_open(new_file, ffi.C.kO_CREAT, tonumber("700", 8))
  463. --verify permissions
  464. eq('rwx------', lfs.attributes(new_file)['permissions'])
  465. eq(0, os_close(fd))
  466. end)
  467. itp('sets `rw` permissions for O_CREAT 600 which then can be closed', function()
  468. assert_file_does_not_exist(new_file)
  469. --create the file
  470. local fd = os_open(new_file, ffi.C.kO_CREAT, tonumber("600", 8))
  471. --verify permissions
  472. eq('rw-------', lfs.attributes(new_file)['permissions'])
  473. eq(0, os_close(fd))
  474. end)
  475. itp('returns a non-negative file descriptor for an existing file which then can be closed', function()
  476. local fd = os_open(existing_file, ffi.C.kO_RDWR, 0)
  477. assert.is_true(0 <= fd)
  478. eq(0, os_close(fd))
  479. end)
  480. end)
  481. describe('os_close', function()
  482. itp('returns EBADF for negative file descriptors', function()
  483. eq(ffi.C.UV_EBADF, os_close(-1))
  484. eq(ffi.C.UV_EBADF, os_close(-1000))
  485. end)
  486. end)
  487. describe('os_read', function()
  488. local file = 'test-unit-os-fs_spec-os_read.dat'
  489. before_each(function()
  490. local f = io.open(file, 'w')
  491. f:write(fcontents)
  492. f:close()
  493. end)
  494. after_each(function()
  495. os.remove(file)
  496. end)
  497. itp('can read zero bytes from a file', function()
  498. local fd = os_open(file, ffi.C.kO_RDONLY, 0)
  499. ok(fd >= 0)
  500. eq({false, 0, ''}, {os_read(fd, nil)})
  501. eq({false, 0, ''}, {os_read(fd, 0)})
  502. eq(0, os_close(fd))
  503. end)
  504. itp('can read from a file multiple times', function()
  505. local fd = os_open(file, ffi.C.kO_RDONLY, 0)
  506. ok(fd >= 0)
  507. eq({false, 2, '\000\001'}, {os_read(fd, 2)})
  508. eq({false, 2, '\002\003'}, {os_read(fd, 2)})
  509. eq(0, os_close(fd))
  510. end)
  511. itp('can read the whole file at once and then report eof', function()
  512. local fd = os_open(file, ffi.C.kO_RDONLY, 0)
  513. ok(fd >= 0)
  514. eq({false, #fcontents, fcontents}, {os_read(fd, #fcontents)})
  515. eq({true, 0, ('\0'):rep(#fcontents)}, {os_read(fd, #fcontents)})
  516. eq(0, os_close(fd))
  517. end)
  518. itp('can read the whole file in two calls, one partially', function()
  519. local fd = os_open(file, ffi.C.kO_RDONLY, 0)
  520. ok(fd >= 0)
  521. eq({false, #fcontents * 3/4, fcontents:sub(1, #fcontents * 3/4)},
  522. {os_read(fd, #fcontents * 3/4)})
  523. eq({true,
  524. (#fcontents * 1/4),
  525. fcontents:sub(#fcontents * 3/4 + 1) .. ('\0'):rep(#fcontents * 2/4)},
  526. {os_read(fd, #fcontents * 3/4)})
  527. eq(0, os_close(fd))
  528. end)
  529. end)
  530. describe('os_readv', function()
  531. -- Function may be absent
  532. if not pcall(function() return fs.os_readv end) then
  533. return
  534. end
  535. local file = 'test-unit-os-fs_spec-os_readv.dat'
  536. before_each(function()
  537. local f = io.open(file, 'w')
  538. f:write(fcontents)
  539. f:close()
  540. end)
  541. after_each(function()
  542. os.remove(file)
  543. end)
  544. itp('can read zero bytes from a file', function()
  545. local fd = os_open(file, ffi.C.kO_RDONLY, 0)
  546. ok(fd >= 0)
  547. eq({false, 0, {}}, {os_readv(fd, {})})
  548. eq({false, 0, {'', '', ''}}, {os_readv(fd, {0, 0, 0})})
  549. eq(0, os_close(fd))
  550. end)
  551. itp('can read from a file multiple times to a differently-sized buffers', function()
  552. local fd = os_open(file, ffi.C.kO_RDONLY, 0)
  553. ok(fd >= 0)
  554. eq({false, 2, {'\000\001'}}, {os_readv(fd, {2})})
  555. eq({false, 5, {'\002\003', '\004\005\006'}}, {os_readv(fd, {2, 3})})
  556. eq(0, os_close(fd))
  557. end)
  558. itp('can read the whole file at once and then report eof', function()
  559. local fd = os_open(file, ffi.C.kO_RDONLY, 0)
  560. ok(fd >= 0)
  561. eq({false,
  562. #fcontents,
  563. {fcontents:sub(1, #fcontents * 1/4),
  564. fcontents:sub(#fcontents * 1/4 + 1, #fcontents * 3/4),
  565. fcontents:sub(#fcontents * 3/4 + 1, #fcontents * 15/16),
  566. fcontents:sub(#fcontents * 15/16 + 1, #fcontents)}},
  567. {os_readv(fd, {#fcontents * 1/4,
  568. #fcontents * 2/4,
  569. #fcontents * 3/16,
  570. #fcontents * 1/16})})
  571. eq({true, 0, {'\0'}}, {os_readv(fd, {1})})
  572. eq(0, os_close(fd))
  573. end)
  574. itp('can read the whole file in two calls, one partially', function()
  575. local fd = os_open(file, ffi.C.kO_RDONLY, 0)
  576. ok(fd >= 0)
  577. eq({false, #fcontents * 3/4, {fcontents:sub(1, #fcontents * 3/4)}},
  578. {os_readv(fd, {#fcontents * 3/4})})
  579. eq({true,
  580. (#fcontents * 1/4),
  581. {fcontents:sub(#fcontents * 3/4 + 1) .. ('\0'):rep(#fcontents * 2/4)}},
  582. {os_readv(fd, {#fcontents * 3/4})})
  583. eq(0, os_close(fd))
  584. end)
  585. end)
  586. describe('os_write', function()
  587. -- Function may be absent
  588. local file = 'test-unit-os-fs_spec-os_write.dat'
  589. before_each(function()
  590. local f = io.open(file, 'w')
  591. f:write(fcontents)
  592. f:close()
  593. end)
  594. after_each(function()
  595. os.remove(file)
  596. end)
  597. itp('can write zero bytes to a file', function()
  598. local fd = os_open(file, ffi.C.kO_WRONLY, 0)
  599. ok(fd >= 0)
  600. eq(0, os_write(fd, ''))
  601. eq(0, os_write(fd, nil))
  602. eq(fcontents, io.open(file, 'r'):read('*a'))
  603. eq(0, os_close(fd))
  604. end)
  605. itp('can write some data to a file', function()
  606. local fd = os_open(file, ffi.C.kO_WRONLY, 0)
  607. ok(fd >= 0)
  608. eq(3, os_write(fd, 'abc'))
  609. eq(4, os_write(fd, ' def'))
  610. eq('abc def' .. fcontents:sub(8), io.open(file, 'r'):read('*a'))
  611. eq(0, os_close(fd))
  612. end)
  613. end)
  614. describe('os_nodetype', function()
  615. before_each(function()
  616. os.remove('non-existing-file')
  617. end)
  618. itp('returns NODE_NORMAL for non-existing file', function()
  619. eq(NODE_NORMAL, fs.os_nodetype(to_cstr('non-existing-file')))
  620. end)
  621. itp('returns NODE_WRITABLE for /dev/stderr', function()
  622. eq(NODE_WRITABLE, fs.os_nodetype(to_cstr('/dev/stderr')))
  623. end)
  624. end)
  625. end)
  626. describe('folder operations', function()
  627. local function os_mkdir(path, mode)
  628. return fs.os_mkdir(to_cstr(path), mode)
  629. end
  630. local function os_rmdir(path)
  631. return fs.os_rmdir(to_cstr(path))
  632. end
  633. local function os_mkdir_recurse(path, mode)
  634. local failed_str = ffi.new('char *[1]', {nil})
  635. local ret = fs.os_mkdir_recurse(path, mode, failed_str)
  636. local str = failed_str[0]
  637. if str ~= nil then
  638. str = ffi.string(str)
  639. end
  640. return ret, str
  641. end
  642. describe('os_mkdir', function()
  643. itp('returns non-zero when given an already existing directory', function()
  644. local mode = ffi.C.kS_IRUSR + ffi.C.kS_IWUSR + ffi.C.kS_IXUSR
  645. neq(0, (os_mkdir('unit-test-directory', mode)))
  646. end)
  647. itp('creates a directory and returns 0', function()
  648. local mode = ffi.C.kS_IRUSR + ffi.C.kS_IWUSR + ffi.C.kS_IXUSR
  649. eq(false, (os_isdir('unit-test-directory/new-dir')))
  650. eq(0, (os_mkdir('unit-test-directory/new-dir', mode)))
  651. eq(true, (os_isdir('unit-test-directory/new-dir')))
  652. lfs.rmdir('unit-test-directory/new-dir')
  653. end)
  654. end)
  655. describe('os_mkdir_recurse', function()
  656. itp('returns zero when given an already existing directory', function()
  657. local mode = ffi.C.kS_IRUSR + ffi.C.kS_IWUSR + ffi.C.kS_IXUSR
  658. local ret, failed_str = os_mkdir_recurse('unit-test-directory', mode)
  659. eq(0, ret)
  660. eq(nil, failed_str)
  661. end)
  662. itp('fails to create a directory where there is a file', function()
  663. local mode = ffi.C.kS_IRUSR + ffi.C.kS_IWUSR + ffi.C.kS_IXUSR
  664. local ret, failed_str = os_mkdir_recurse(
  665. 'unit-test-directory/test.file', mode)
  666. neq(0, ret)
  667. eq('unit-test-directory/test.file', failed_str)
  668. end)
  669. itp('fails to create a directory where there is a file in path', function()
  670. local mode = ffi.C.kS_IRUSR + ffi.C.kS_IWUSR + ffi.C.kS_IXUSR
  671. local ret, failed_str = os_mkdir_recurse(
  672. 'unit-test-directory/test.file/test', mode)
  673. neq(0, ret)
  674. eq('unit-test-directory/test.file', failed_str)
  675. end)
  676. itp('succeeds to create a directory', function()
  677. local mode = ffi.C.kS_IRUSR + ffi.C.kS_IWUSR + ffi.C.kS_IXUSR
  678. local ret, failed_str = os_mkdir_recurse(
  679. 'unit-test-directory/new-dir-recurse', mode)
  680. eq(0, ret)
  681. eq(nil, failed_str)
  682. eq(true, os_isdir('unit-test-directory/new-dir-recurse'))
  683. lfs.rmdir('unit-test-directory/new-dir-recurse')
  684. eq(false, os_isdir('unit-test-directory/new-dir-recurse'))
  685. end)
  686. itp('succeeds to create a directory ending with ///', function()
  687. local mode = ffi.C.kS_IRUSR + ffi.C.kS_IWUSR + ffi.C.kS_IXUSR
  688. local ret, failed_str = os_mkdir_recurse(
  689. 'unit-test-directory/new-dir-recurse///', mode)
  690. eq(0, ret)
  691. eq(nil, failed_str)
  692. eq(true, os_isdir('unit-test-directory/new-dir-recurse'))
  693. lfs.rmdir('unit-test-directory/new-dir-recurse')
  694. eq(false, os_isdir('unit-test-directory/new-dir-recurse'))
  695. end)
  696. itp('succeeds to create a directory ending with /', function()
  697. local mode = ffi.C.kS_IRUSR + ffi.C.kS_IWUSR + ffi.C.kS_IXUSR
  698. local ret, failed_str = os_mkdir_recurse(
  699. 'unit-test-directory/new-dir-recurse/', mode)
  700. eq(0, ret)
  701. eq(nil, failed_str)
  702. eq(true, os_isdir('unit-test-directory/new-dir-recurse'))
  703. lfs.rmdir('unit-test-directory/new-dir-recurse')
  704. eq(false, os_isdir('unit-test-directory/new-dir-recurse'))
  705. end)
  706. itp('succeeds to create a directory tree', function()
  707. local mode = ffi.C.kS_IRUSR + ffi.C.kS_IWUSR + ffi.C.kS_IXUSR
  708. local ret, failed_str = os_mkdir_recurse(
  709. 'unit-test-directory/new-dir-recurse/1/2/3', mode)
  710. eq(0, ret)
  711. eq(nil, failed_str)
  712. eq(true, os_isdir('unit-test-directory/new-dir-recurse'))
  713. eq(true, os_isdir('unit-test-directory/new-dir-recurse/1'))
  714. eq(true, os_isdir('unit-test-directory/new-dir-recurse/1/2'))
  715. eq(true, os_isdir('unit-test-directory/new-dir-recurse/1/2/3'))
  716. lfs.rmdir('unit-test-directory/new-dir-recurse/1/2/3')
  717. lfs.rmdir('unit-test-directory/new-dir-recurse/1/2')
  718. lfs.rmdir('unit-test-directory/new-dir-recurse/1')
  719. lfs.rmdir('unit-test-directory/new-dir-recurse')
  720. eq(false, os_isdir('unit-test-directory/new-dir-recurse'))
  721. end)
  722. end)
  723. describe('os_rmdir', function()
  724. itp('returns non_zero when given a non-existing directory', function()
  725. neq(0, (os_rmdir('non-existing-directory')))
  726. end)
  727. itp('removes the given directory and returns 0', function()
  728. lfs.mkdir('unit-test-directory/new-dir')
  729. eq(0, os_rmdir('unit-test-directory/new-dir'))
  730. eq(false, (os_isdir('unit-test-directory/new-dir')))
  731. end)
  732. end)
  733. end)
  734. describe('FileInfo', function()
  735. local function file_info_new()
  736. local file_info = ffi.new('FileInfo[1]')
  737. file_info[0].stat.st_ino = 0
  738. file_info[0].stat.st_dev = 0
  739. return file_info
  740. end
  741. local function is_file_info_filled(file_info)
  742. return file_info[0].stat.st_ino > 0 and file_info[0].stat.st_dev > 0
  743. end
  744. local function file_id_new()
  745. local file_info = ffi.new('FileID[1]')
  746. file_info[0].inode = 0
  747. file_info[0].device_id = 0
  748. return file_info
  749. end
  750. describe('os_fileinfo', function()
  751. itp('returns false if path=NULL', function()
  752. local file_info = file_info_new()
  753. assert.is_false((fs.os_fileinfo(nil, file_info)))
  754. end)
  755. itp('returns false if given a non-existing file', function()
  756. local file_info = file_info_new()
  757. assert.is_false((fs.os_fileinfo('/non-existent', file_info)))
  758. end)
  759. itp('returns true if given an existing file and fills file_info', function()
  760. local file_info = file_info_new()
  761. local path = 'unit-test-directory/test.file'
  762. assert.is_true((fs.os_fileinfo(path, file_info)))
  763. assert.is_true((is_file_info_filled(file_info)))
  764. end)
  765. itp('returns the file info of the linked file, not the link', function()
  766. local file_info = file_info_new()
  767. local path = 'unit-test-directory/test_link.file'
  768. assert.is_true((fs.os_fileinfo(path, file_info)))
  769. assert.is_true((is_file_info_filled(file_info)))
  770. local mode = tonumber(file_info[0].stat.st_mode)
  771. return eq(ffi.C.kS_IFREG, (bit.band(mode, ffi.C.kS_IFMT)))
  772. end)
  773. end)
  774. describe('os_fileinfo_link', function()
  775. itp('returns false if given a non-existing file', function()
  776. local file_info = file_info_new()
  777. assert.is_false((fs.os_fileinfo_link('/non-existent', file_info)))
  778. end)
  779. itp('returns true if given an existing file and fills file_info', function()
  780. local file_info = file_info_new()
  781. local path = 'unit-test-directory/test.file'
  782. assert.is_true((fs.os_fileinfo_link(path, file_info)))
  783. assert.is_true((is_file_info_filled(file_info)))
  784. end)
  785. itp('returns the file info of the link, not the linked file', function()
  786. local file_info = file_info_new()
  787. local path = 'unit-test-directory/test_link.file'
  788. assert.is_true((fs.os_fileinfo_link(path, file_info)))
  789. assert.is_true((is_file_info_filled(file_info)))
  790. local mode = tonumber(file_info[0].stat.st_mode)
  791. eq(ffi.C.kS_IFLNK, (bit.band(mode, ffi.C.kS_IFMT)))
  792. end)
  793. end)
  794. describe('os_fileinfo_fd', function()
  795. itp('returns false if given an invalid file descriptor', function()
  796. local file_info = file_info_new()
  797. assert.is_false((fs.os_fileinfo_fd(-1, file_info)))
  798. end)
  799. itp('returns true if given a file descriptor and fills file_info', function()
  800. local file_info = file_info_new()
  801. local path = 'unit-test-directory/test.file'
  802. local fd = ffi.C.open(path, 0)
  803. assert.is_true((fs.os_fileinfo_fd(fd, file_info)))
  804. assert.is_true((is_file_info_filled(file_info)))
  805. ffi.C.close(fd)
  806. end)
  807. end)
  808. describe('os_fileinfo_id_equal', function()
  809. itp('returns false if file infos represent different files', function()
  810. local file_info_1 = file_info_new()
  811. local file_info_2 = file_info_new()
  812. local path_1 = 'unit-test-directory/test.file'
  813. local path_2 = 'unit-test-directory/test_2.file'
  814. assert.is_true((fs.os_fileinfo(path_1, file_info_1)))
  815. assert.is_true((fs.os_fileinfo(path_2, file_info_2)))
  816. assert.is_false((fs.os_fileinfo_id_equal(file_info_1, file_info_2)))
  817. end)
  818. itp('returns true if file infos represent the same file', function()
  819. local file_info_1 = file_info_new()
  820. local file_info_2 = file_info_new()
  821. local path = 'unit-test-directory/test.file'
  822. assert.is_true((fs.os_fileinfo(path, file_info_1)))
  823. assert.is_true((fs.os_fileinfo(path, file_info_2)))
  824. assert.is_true((fs.os_fileinfo_id_equal(file_info_1, file_info_2)))
  825. end)
  826. itp('returns true if file infos represent the same file (symlink)', function()
  827. local file_info_1 = file_info_new()
  828. local file_info_2 = file_info_new()
  829. local path_1 = 'unit-test-directory/test.file'
  830. local path_2 = 'unit-test-directory/test_link.file'
  831. assert.is_true((fs.os_fileinfo(path_1, file_info_1)))
  832. assert.is_true((fs.os_fileinfo(path_2, file_info_2)))
  833. assert.is_true((fs.os_fileinfo_id_equal(file_info_1, file_info_2)))
  834. end)
  835. end)
  836. describe('os_fileinfo_id', function()
  837. itp('extracts ino/dev from file_info into file_id', function()
  838. local file_info = file_info_new()
  839. local file_id = file_id_new()
  840. local path = 'unit-test-directory/test.file'
  841. assert.is_true((fs.os_fileinfo(path, file_info)))
  842. fs.os_fileinfo_id(file_info, file_id)
  843. eq(file_info[0].stat.st_ino, file_id[0].inode)
  844. eq(file_info[0].stat.st_dev, file_id[0].device_id)
  845. end)
  846. end)
  847. describe('os_fileinfo_inode', function()
  848. itp('returns the inode from file_info', function()
  849. local file_info = file_info_new()
  850. local path = 'unit-test-directory/test.file'
  851. assert.is_true((fs.os_fileinfo(path, file_info)))
  852. local inode = fs.os_fileinfo_inode(file_info)
  853. eq(file_info[0].stat.st_ino, inode)
  854. end)
  855. end)
  856. describe('os_fileinfo_size', function()
  857. itp('returns the correct size of a file', function()
  858. local path = 'unit-test-directory/test.file'
  859. local file = io.open(path, 'w')
  860. file:write('some bytes to get filesize != 0')
  861. file:flush()
  862. file:close()
  863. local size = lfs.attributes(path, 'size')
  864. local file_info = file_info_new()
  865. assert.is_true(fs.os_fileinfo(path, file_info))
  866. eq(size, fs.os_fileinfo_size(file_info))
  867. end)
  868. end)
  869. describe('os_fileinfo_hardlinks', function()
  870. itp('returns the correct number of hardlinks', function()
  871. local path = 'unit-test-directory/test.file'
  872. local path_link = 'unit-test-directory/test_hlink.file'
  873. local file_info = file_info_new()
  874. assert.is_true(fs.os_fileinfo(path, file_info))
  875. eq(1, fs.os_fileinfo_hardlinks(file_info))
  876. lfs.link(path, path_link)
  877. assert.is_true(fs.os_fileinfo(path, file_info))
  878. eq(2, fs.os_fileinfo_hardlinks(file_info))
  879. end)
  880. end)
  881. describe('os_fileinfo_blocksize', function()
  882. itp('returns the correct blocksize of a file', function()
  883. local path = 'unit-test-directory/test.file'
  884. -- there is a bug in luafilesystem where
  885. -- `lfs.attributes path, 'blksize'` returns the worng value:
  886. -- https://github.com/keplerproject/luafilesystem/pull/44
  887. -- using this workaround for now:
  888. local blksize = lfs.attributes(path).blksize
  889. local file_info = file_info_new()
  890. assert.is_true(fs.os_fileinfo(path, file_info))
  891. if blksize then
  892. eq(blksize, fs.os_fileinfo_blocksize(file_info))
  893. else
  894. -- luafs dosn't support blksize on windows
  895. -- libuv on windows returns a constant value as blocksize
  896. -- checking for this constant value should be enough
  897. eq(2048, fs.os_fileinfo_blocksize(file_info))
  898. end
  899. end)
  900. end)
  901. describe('os_fileid', function()
  902. itp('returns false if given an non-existing file', function()
  903. local file_id = file_id_new()
  904. assert.is_false((fs.os_fileid('/non-existent', file_id)))
  905. end)
  906. itp('returns true if given an existing file and fills file_id', function()
  907. local file_id = file_id_new()
  908. local path = 'unit-test-directory/test.file'
  909. assert.is_true((fs.os_fileid(path, file_id)))
  910. assert.is_true(0 < file_id[0].inode)
  911. assert.is_true(0 < file_id[0].device_id)
  912. end)
  913. end)
  914. describe('os_fileid_equal', function()
  915. itp('returns true if two FileIDs are equal', function()
  916. local file_id = file_id_new()
  917. local path = 'unit-test-directory/test.file'
  918. assert.is_true((fs.os_fileid(path, file_id)))
  919. assert.is_true((fs.os_fileid_equal(file_id, file_id)))
  920. end)
  921. itp('returns false if two FileIDs are not equal', function()
  922. local file_id_1 = file_id_new()
  923. local file_id_2 = file_id_new()
  924. local path_1 = 'unit-test-directory/test.file'
  925. local path_2 = 'unit-test-directory/test_2.file'
  926. assert.is_true((fs.os_fileid(path_1, file_id_1)))
  927. assert.is_true((fs.os_fileid(path_2, file_id_2)))
  928. assert.is_false((fs.os_fileid_equal(file_id_1, file_id_2)))
  929. end)
  930. end)
  931. describe('os_fileid_equal_fileinfo', function()
  932. itp('returns true if file_id and file_info represent the same file', function()
  933. local file_id = file_id_new()
  934. local file_info = file_info_new()
  935. local path = 'unit-test-directory/test.file'
  936. assert.is_true((fs.os_fileid(path, file_id)))
  937. assert.is_true((fs.os_fileinfo(path, file_info)))
  938. assert.is_true((fs.os_fileid_equal_fileinfo(file_id, file_info)))
  939. end)
  940. itp('returns false if file_id and file_info represent different files', function()
  941. local file_id = file_id_new()
  942. local file_info = file_info_new()
  943. local path_1 = 'unit-test-directory/test.file'
  944. local path_2 = 'unit-test-directory/test_2.file'
  945. assert.is_true((fs.os_fileid(path_1, file_id)))
  946. assert.is_true((fs.os_fileinfo(path_2, file_info)))
  947. assert.is_false((fs.os_fileid_equal_fileinfo(file_id, file_info)))
  948. end)
  949. end)
  950. end)
  951. end)