neovim

Neovim text editor
git clone https://git.dasho.dev/neovim.git
Log | Files | Refs | README

fs_spec.lua (39252B)


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