diff options
-rw-r--r-- | src/nvim/file.c | 36 | ||||
-rw-r--r-- | src/nvim/file.h | 17 | ||||
-rw-r--r-- | test/unit/file_spec.lua | 361 | ||||
-rw-r--r-- | test/unit/helpers.lua | 5 | ||||
-rw-r--r-- | test/unit/os/fs_spec.lua | 29 |
5 files changed, 404 insertions, 44 deletions
diff --git a/src/nvim/file.c b/src/nvim/file.c index 2fb0e1cd87..2730e5b5f9 100644 --- a/src/nvim/file.c +++ b/src/nvim/file.c @@ -29,8 +29,6 @@ # include "file.c.generated.h" #endif -#define RWBUFSIZE (IOSIZE - 1) - /// Open file /// /// @param[out] ret_fp Address where information needed for reading from or @@ -80,7 +78,7 @@ int file_open(FileDescriptor *const ret_fp, const char *const fname, ret_fp->wr = (wr == kTrue); ret_fp->fd = fd; ret_fp->eof = false; - ret_fp->rv = rbuffer_new(RWBUFSIZE); + ret_fp->rv = rbuffer_new(kRWBufferSize); ret_fp->_error = 0; if (ret_fp->wr) { ret_fp->rv->data = ret_fp; @@ -162,7 +160,7 @@ int file_fsync(FileDescriptor *const fp) /// Buffer used for writing /// /// Like IObuff, but allows file_\* callers not to care about spoiling it. -static char writebuf[RWBUFSIZE]; +static char writebuf[kRWBufferSize]; /// Function run when RBuffer is full when writing to a file /// @@ -175,7 +173,10 @@ static void file_rb_write_full_cb(RBuffer *const rv, FileDescriptor *const fp) { assert(fp->wr); assert(rv->data == (void *)fp); - const size_t read_bytes = rbuffer_read(rv, writebuf, RWBUFSIZE); + if (rbuffer_size(rv) == 0) { + return; + } + const size_t read_bytes = rbuffer_read(rv, writebuf, kRWBufferSize); const ptrdiff_t wres = os_write(fp->fd, writebuf, read_bytes); if (wres != (ptrdiff_t)read_bytes) { if (wres >= 0) { @@ -215,22 +216,25 @@ ptrdiff_t file_read(FileDescriptor *const fp, char *const ret_buf, if (read_remaining) { assert(rbuffer_size(rv) == 0); rbuffer_reset(rv); - if (read_remaining >= RWBUFSIZE) { + if (read_remaining >= kRWBufferSize) { #ifdef HAVE_READV // If there is readv() syscall, then take an opportunity to populate // both target buffer and RBuffer at once, … - size_t read_count; + size_t write_count; struct iovec iov[] = { { .iov_base = buf, .iov_len = read_remaining }, - { .iov_base = rbuffer_read_ptr(rv, &read_count), .iov_len = RWBUFSIZE - }, + { .iov_base = rbuffer_write_ptr(rv, &write_count), + .iov_len = kRWBufferSize }, }; - assert(read_count == RWBUFSIZE); + assert(write_count == kRWBufferSize); const ptrdiff_t r_ret = os_readv(fp->fd, &fp->eof, iov, ARRAY_SIZE(iov)); if (r_ret > 0) { if (r_ret > (ptrdiff_t)read_remaining) { + read_remaining = 0; rbuffer_produced(rv, (size_t)(r_ret - (ptrdiff_t)read_remaining)); + } else { + read_remaining -= (size_t)r_ret; } } else if (r_ret < 0) { return r_ret; @@ -250,8 +254,8 @@ ptrdiff_t file_read(FileDescriptor *const fp, char *const ret_buf, size_t write_count; const ptrdiff_t r_ret = os_read(fp->fd, &fp->eof, rbuffer_write_ptr(rv, &write_count), - RWBUFSIZE); - assert(write_count == RWBUFSIZE); + kRWBufferSize); + assert(write_count == kRWBufferSize); if (r_ret > 0) { rbuffer_produced(rv, (size_t)r_ret); } else if (r_ret < 0) { @@ -288,7 +292,7 @@ ptrdiff_t file_write(FileDescriptor *const fp, const char *const buf, /// Buffer used for skipping. Its contents is undefined and should never be /// used. -static char skipbuf[RWBUFSIZE]; +static char skipbuf[kRWBufferSize]; /// Skip some bytes /// @@ -300,10 +304,8 @@ ptrdiff_t file_skip(FileDescriptor *const fp, const size_t size) assert(!fp->wr); size_t read_bytes = 0; do { - ptrdiff_t new_read_bytes = file_read( - fp, skipbuf, (size_t)(size - read_bytes > sizeof(skipbuf) - ? sizeof(skipbuf) - : size - read_bytes)); + const ptrdiff_t new_read_bytes = file_read( + fp, skipbuf, MIN(size - read_bytes, sizeof(skipbuf))); if (new_read_bytes < 0) { return new_read_bytes; } else if (new_read_bytes == 0) { diff --git a/src/nvim/file.h b/src/nvim/file.h index 5ee572750d..2ba415d2b9 100644 --- a/src/nvim/file.h +++ b/src/nvim/file.h @@ -32,6 +32,9 @@ typedef enum { ///< kFileCreateOnly. } FileOpenFlags; +static inline bool file_eof(const FileDescriptor *const fp) + REAL_FATTR_PURE REAL_FATTR_WARN_UNUSED_RESULT REAL_FATTR_NONNULL_ALL; + /// Check whether end of file was encountered /// /// @param[in] fp File to check. @@ -39,24 +42,30 @@ typedef enum { /// @return true if it was, false if it was not or read operation was never /// performed. static inline bool file_eof(const FileDescriptor *const fp) - FUNC_ATTR_PURE FUNC_ATTR_WARN_UNUSED_RESULT FUNC_ATTR_NONNULL_ALL - FUNC_ATTR_ALWAYS_INLINE { return fp->eof && rbuffer_size(fp->rv) == 0; } +static inline int file_fd(const FileDescriptor *const fp) + REAL_FATTR_PURE REAL_FATTR_WARN_UNUSED_RESULT REAL_FATTR_NONNULL_ALL; + /// Return the file descriptor associated with the FileDescriptor structure /// /// @param[in] fp File to check. /// /// @return File descriptor. static inline int file_fd(const FileDescriptor *const fp) - FUNC_ATTR_PURE FUNC_ATTR_WARN_UNUSED_RESULT FUNC_ATTR_NONNULL_ALL - FUNC_ATTR_ALWAYS_INLINE { return fp->fd; } +enum { + /// Read or write buffer size + /// + /// Currently equal to (IOSIZE - 1), but they do not need to be connected. + kRWBufferSize = 1024 +}; + #ifdef INCLUDE_GENERATED_DECLARATIONS # include "file.h.generated.h" #endif diff --git a/test/unit/file_spec.lua b/test/unit/file_spec.lua new file mode 100644 index 0000000000..ac78dffe6e --- /dev/null +++ b/test/unit/file_spec.lua @@ -0,0 +1,361 @@ +local lfs = require('lfs') + +local helpers = require('test.unit.helpers') + +local eq = helpers.eq +local ok = helpers.ok +local ffi = helpers.ffi +local cimport = helpers.cimport + +local m = cimport('./src/nvim/file.h') + +local s = '' +for i = 0, 255 do + s = s .. (i == 0 and '\0' or ('%c'):format(i)) +end +local fcontents = s:rep(16) + +local dir = 'Xtest-unit-file_spec.d' +local file1 = dir .. '/file1.dat' +local file2 = dir .. '/file2.dat' +local linkf = dir .. '/file.lnk' +local linkb = dir .. '/broken.lnk' +local filec = dir .. '/created-file.dat' + +before_each(function() + lfs.mkdir(dir); + + local f1 = io.open(file1, 'w') + f1:write(fcontents) + f1:close() + + local f2 = io.open(file2, 'w') + f2:write(fcontents) + f2:close() + + lfs.link('file1.dat', linkf, true) + lfs.link('broken.dat', linkb, true) +end) + +after_each(function() + os.remove(file1) + os.remove(file2) + os.remove(linkf) + os.remove(linkb) + os.remove(filec) + lfs.rmdir(dir) +end) + +local function file_open(fname, flags, mode) + local ret2 = ffi.new('FileDescriptor') + local ret1 = m.file_open(ret2, fname, flags, mode) + return ret1, ret2 +end + +local function file_open_new(fname, flags, mode) + local ret1 = ffi.new('int[?]', 1, {0}) + local ret2 = ffi.gc(m.file_open_new(ret1, fname, flags, mode), nil) + return ret1[0], ret2 +end + +local function file_write(fp, buf) + return m.file_write(fp, buf, #buf) +end + +local function file_read(fp, size) + local buf = nil + if size == nil then + size = 0 + else + buf = ffi.new('char[?]', size, ('\0'):rep(size)) + end + local ret1 = m.file_read(fp, buf, size) + local ret2 = '' + if buf ~= nil then + ret2 = ffi.string(buf, size) + end + return ret1, ret2 +end + +local function file_fsync(fp) + return m.file_fsync(fp) +end + +local function file_skip(fp, size) + return m.file_skip(fp, size) +end + +describe('file_open', function() + it('can create a rwx------ file with kFileCreate', function() + local err, fp = file_open(filec, m.kFileCreate, 448) + eq(0, err) + local attrs = lfs.attributes(filec) + eq('rwx------', attrs.permissions) + eq(0, m.file_close(fp)) + end) + + it('can create a rw------- file with kFileCreate', function() + local err, fp = file_open(filec, m.kFileCreate, 384) + eq(0, err) + local attrs = lfs.attributes(filec) + eq('rw-------', attrs.permissions) + eq(0, m.file_close(fp)) + end) + + it('can create a rwx------ file with kFileCreateOnly', function() + local err, fp = file_open(filec, m.kFileCreateOnly, 448) + eq(0, err) + local attrs = lfs.attributes(filec) + eq('rwx------', attrs.permissions) + eq(0, m.file_close(fp)) + end) + + it('can create a rw------- file with kFileCreateOnly', function() + local err, fp = file_open(filec, m.kFileCreateOnly, 384) + eq(0, err) + local attrs = lfs.attributes(filec) + eq('rw-------', attrs.permissions) + eq(0, m.file_close(fp)) + end) + + it('fails to open an existing file with kFileCreateOnly', function() + local err, fp = file_open(file1, m.kFileCreateOnly, 384) + eq(m.UV_EEXIST, err) + end) + + it('fails to open an symlink with kFileNoSymlink', function() + local err, fp = file_open(linkf, m.kFileNoSymlink, 384) + eq(m.UV_ELOOP, err) + end) + + it('can open an existing file write-only with kFileCreate', function() + local err, fp = file_open(file1, m.kFileCreate, 384) + eq(0, err) + eq(true, fp.wr) + eq(0, m.file_close(fp)) + end) + + it('can open an existing file read-only with zero', function() + local err, fp = file_open(file1, 0, 384) + eq(0, err) + eq(false, fp.wr) + eq(0, m.file_close(fp)) + end) + + it('can open an existing file read-only with kFileReadOnly', function() + local err, fp = file_open(file1, m.kFileReadOnly, 384) + eq(0, err) + eq(false, fp.wr) + eq(0, m.file_close(fp)) + end) + + it('can open an existing file read-only with kFileNoSymlink', function() + local err, fp = file_open(file1, m.kFileNoSymlink, 384) + eq(0, err) + eq(false, fp.wr) + eq(0, m.file_close(fp)) + end) + + it('can truncate an existing file with kFileTruncate', function() + local err, fp = file_open(file1, m.kFileTruncate, 384) + eq(0, err) + eq(true, fp.wr) + eq(0, m.file_close(fp)) + local attrs = lfs.attributes(file1) + eq(0, attrs.size) + end) + + it('can open an existing file write-only with kFileWriteOnly', function() + local err, fp = file_open(file1, m.kFileWriteOnly, 384) + eq(0, err) + eq(true, fp.wr) + eq(0, m.file_close(fp)) + local attrs = lfs.attributes(file1) + eq(4096, attrs.size) + end) + + it('fails to create a file with just kFileWriteOnly', function() + local err, fp = file_open(filec, m.kFileWriteOnly, 384) + eq(m.UV_ENOENT, err) + local attrs = lfs.attributes(filec) + eq(nil, attrs) + end) + + it('can truncate an existing file with kFileTruncate when opening a symlink', + function() + local err, fp = file_open(linkf, m.kFileTruncate, 384) + eq(0, err) + eq(true, fp.wr) + eq(0, m.file_close(fp)) + local attrs = lfs.attributes(file1) + eq(0, attrs.size) + end) + + it('fails to open a directory write-only', function() + local err, fp = file_open(dir, m.kFileWriteOnly, 384) + eq(m.UV_EISDIR, err) + end) + + it('fails to open a broken symbolic link write-only', function() + local err, fp = file_open(linkb, m.kFileWriteOnly, 384) + eq(m.UV_ENOENT, err) + end) + + it('fails to open a broken symbolic link read-only', function() + local err, fp = file_open(linkb, m.kFileReadOnly, 384) + eq(m.UV_ENOENT, err) + end) +end) + +describe('file_open_new', function() + it('can open a file read-only', function() + local err, fp = file_open_new(file1, 0, 384) + eq(0, err) + eq(false, fp.wr) + eq(0, m.file_free(fp)) + end) + + it('fails to open an existing file with kFileCreateOnly', function() + local err, fp = file_open_new(file1, m.kFileCreateOnly, 384) + eq(m.UV_EEXIST, err) + eq(nil, fp) + end) +end) + +-- file_close is called above, so it is not tested directly + +describe('file_fsync', function() + it('can flush writes to disk', function() + local err, fp = file_open(filec, m.kFileCreateOnly, 384) + eq(0, file_fsync(fp)) + eq(0, err) + eq(0, lfs.attributes(filec).size) + local wsize = file_write(fp, 'test') + eq(4, wsize) + eq(0, lfs.attributes(filec).size) + eq(0, file_fsync(fp)) + eq(wsize, lfs.attributes(filec).size) + eq(0, m.file_close(fp)) + end) +end) + +describe('file_read', function() + it('can read small chunks of input until eof', function() + local err, fp = file_open(file1, 0, 384) + eq(0, err) + eq(false, fp.wr) + local shift = 0 + while shift < #fcontents do + local size = 3 + local exp_err = size + local exp_s = fcontents:sub(shift + 1, shift + size) + if shift + size >= #fcontents then + exp_err = #fcontents - shift + exp_s = (fcontents:sub(shift + 1, shift + size) + .. (('\0'):rep(size - exp_err))) + end + eq({exp_err, exp_s}, {file_read(fp, size)}) + shift = shift + size + end + eq(0, m.file_close(fp)) + end) + + it('can read the whole file at once', function() + local err, fp = file_open(file1, 0, 384) + eq(0, err) + eq(false, fp.wr) + eq({#fcontents, fcontents}, {file_read(fp, #fcontents)}) + eq({0, ('\0'):rep(#fcontents)}, {file_read(fp, #fcontents)}) + eq(0, m.file_close(fp)) + end) + + it('can read more then 1024 bytes after reading a small chunk', function() + local err, fp = file_open(file1, 0, 384) + eq(0, err) + eq(false, fp.wr) + eq({5, fcontents:sub(1, 5)}, {file_read(fp, 5)}) + eq({#fcontents - 5, fcontents:sub(6) .. (('\0'):rep(5))}, + {file_read(fp, #fcontents)}) + eq(0, m.file_close(fp)) + end) + + it('can read file by 768-byte-chunks', function() + local err, fp = file_open(file1, 0, 384) + eq(0, err) + eq(false, fp.wr) + local shift = 0 + while shift < #fcontents do + local size = 768 + local exp_err = size + local exp_s = fcontents:sub(shift + 1, shift + size) + if shift + size >= #fcontents then + exp_err = #fcontents - shift + exp_s = (fcontents:sub(shift + 1, shift + size) + .. (('\0'):rep(size - exp_err))) + end + eq({exp_err, exp_s}, {file_read(fp, size)}) + shift = shift + size + end + eq(0, m.file_close(fp)) + end) +end) + +describe('file_write', function() + it('can write the whole file at once', function() + local err, fp = file_open(filec, m.kFileCreateOnly, 384) + eq(0, err) + eq(true, fp.wr) + local err = file_write(fp, fcontents) + eq(#fcontents, err) + eq(0, m.file_close(fp)) + eq(err, lfs.attributes(filec).size) + eq(fcontents, io.open(filec):read('*a')) + end) + + it('can write the whole file by small chunks', function() + local err, fp = file_open(filec, m.kFileCreateOnly, 384) + eq(0, err) + eq(true, fp.wr) + local shift = 0 + while shift < #fcontents do + local size = 3 + local s = fcontents:sub(shift + 1, shift + size) + local err = file_write(fp, s) + eq(err, #s) + shift = shift + size + end + eq(0, m.file_close(fp)) + eq(#fcontents, lfs.attributes(filec).size) + eq(fcontents, io.open(filec):read('*a')) + end) + + it('can write the whole file by 768-byte-chunks', function() + local err, fp = file_open(filec, m.kFileCreateOnly, 384) + eq(0, err) + eq(true, fp.wr) + local shift = 0 + while shift < #fcontents do + local size = 768 + local s = fcontents:sub(shift + 1, shift + size) + local err = file_write(fp, s) + eq(err, #s) + shift = shift + size + end + eq(0, m.file_close(fp)) + eq(#fcontents, lfs.attributes(filec).size) + eq(fcontents, io.open(filec):read('*a')) + end) +end) + +describe('file_skip', function() + it('can skip 3 bytes', function() + local err, fp = file_open(file1, 0, 384) + eq(0, err) + eq(false, fp.wr) + eq(3, file_skip(fp, 3)) + local err, s = file_read(fp, 3) + eq(3, err) + eq(fcontents:sub(4, 6), s) + eq(0, m.file_close(fp)) + end) +end) diff --git a/test/unit/helpers.lua b/test/unit/helpers.lua index f0c193884e..91da459393 100644 --- a/test/unit/helpers.lua +++ b/test/unit/helpers.lua @@ -7,6 +7,7 @@ local global_helpers = require('test.helpers') local neq = global_helpers.neq local eq = global_helpers.eq +local ok = global_helpers.ok -- add some standard header locations for _, p in ipairs(Paths.include_paths) do @@ -34,7 +35,8 @@ local function filter_complex_blocks(body) if not (string.find(line, "(^)", 1, true) ~= nil or string.find(line, "_ISwupper", 1, true) or string.find(line, "msgpack_zone_push_finalizer") - or string.find(line, "msgpack_unpacker_reserve_buffer")) then + or string.find(line, "msgpack_unpacker_reserve_buffer") + or string.find(line, "inline _Bool")) then result[#result + 1] = line end end @@ -156,6 +158,7 @@ return { cimport = cimport, cppimport = cppimport, internalize = internalize, + ok = ok, eq = eq, neq = neq, ffi = ffi, diff --git a/test/unit/os/fs_spec.lua b/test/unit/os/fs_spec.lua index d7e98b43ce..1f7f6fb791 100644 --- a/test/unit/os/fs_spec.lua +++ b/test/unit/os/fs_spec.lua @@ -6,6 +6,7 @@ local helpers = require('test.unit.helpers') local cimport = helpers.cimport local cppimport = helpers.cppimport local internalize = helpers.internalize +local ok = helpers.ok local eq = helpers.eq local neq = helpers.neq local ffi = helpers.ffi @@ -17,10 +18,6 @@ local NULL = helpers.NULL local NODE_NORMAL = 0 local NODE_WRITABLE = 1 -local function ok(expr) - assert.is_true(expr) -end - cimport('unistd.h') cimport('./src/nvim/os/shell.h') cimport('./src/nvim/option_defs.h') @@ -31,6 +28,12 @@ cppimport('sys/stat.h') cppimport('fcntl.h') cppimport('uv-errno.h') +local s = '' +for i = 0, 255 do + s = s .. (i == 0 and '\0' or ('%c'):format(i)) +end +local fcontents = s:rep(16) + local buffer = "" local directory = nil local absolute_executable = nil @@ -547,12 +550,6 @@ describe('fs function', function() describe('os_read', function() local file = 'test-unit-os-fs_spec-os_read.dat' - local s = '' - for i = 0, 255 do - s = s .. (i == 0 and '\0' or ('%c'):format(i)) - end - local fcontents = s:rep(16) - before_each(function() local f = io.open(file, 'w') f:write(fcontents) @@ -607,12 +604,6 @@ describe('fs function', function() end local file = 'test-unit-os-fs_spec-os_readv.dat' - local s = '' - for i = 0, 255 do - s = s .. (i == 0 and '\0' or ('%c'):format(i)) - end - local fcontents = s:rep(16) - before_each(function() local f = io.open(file, 'w') f:write(fcontents) @@ -673,12 +664,6 @@ describe('fs function', function() -- Function may be absent local file = 'test-unit-os-fs_spec-os_write.dat' - local s = '' - for i = 0, 255 do - s = s .. (i == 0 and '\0' or ('%c'):format(i)) - end - local fcontents = s:rep(16) - before_each(function() local f = io.open(file, 'w') f:write(fcontents) |