diff options
Diffstat (limited to 'test/unit')
25 files changed, 2123 insertions, 326 deletions
diff --git a/test/unit/api/helpers.lua b/test/unit/api/helpers.lua new file mode 100644 index 0000000000..883e1c6c19 --- /dev/null +++ b/test/unit/api/helpers.lua @@ -0,0 +1,156 @@ +local helpers = require('test.unit.helpers') +local eval_helpers = require('test.unit.eval.helpers') + +local cimport = helpers.cimport +local to_cstr = helpers.to_cstr +local ffi = helpers.ffi + +local list_type = eval_helpers.list_type +local dict_type = eval_helpers.dict_type +local func_type = eval_helpers.func_type +local nil_value = eval_helpers.nil_value +local int_type = eval_helpers.int_type +local flt_type = eval_helpers.flt_type +local type_key = eval_helpers.type_key + +local api = cimport('./src/nvim/api/private/defs.h', + './src/nvim/api/private/helpers.h', + './src/nvim/memory.h') + +local obj2lua + +local obj2lua_tab = { + [tonumber(api.kObjectTypeArray)] = function(obj) + local ret = {[type_key]=list_type} + for i = 1,tonumber(obj.data.array.size) do + ret[i] = obj2lua(obj.data.array.items[i - 1]) + end + if ret[1] then + ret[type_key] = nil + end + return ret + end, + [tonumber(api.kObjectTypeDictionary)] = function(obj) + local ret = {} + for i = 1,tonumber(obj.data.dictionary.size) do + local kv_pair = obj.data.dictionary.items[i - 1] + ret[ffi.string(kv_pair.key.data, kv_pair.key.size)] = obj2lua(kv_pair.value) + end + return ret + end, + [tonumber(api.kObjectTypeBoolean)] = function(obj) + if obj.data.boolean == false then + return false + else + return true + end + end, + [tonumber(api.kObjectTypeNil)] = function(_) + return nil_value + end, + [tonumber(api.kObjectTypeFloat)] = function(obj) + return tonumber(obj.data.floating) + end, + [tonumber(api.kObjectTypeInteger)] = function(obj) + return {[type_key]=int_type, value=tonumber(obj.data.integer)} + end, + [tonumber(api.kObjectTypeString)] = function(obj) + return ffi.string(obj.data.string.data, obj.data.string.size) + end, +} + +obj2lua = function(obj) + return ((obj2lua_tab[tonumber(obj['type'])] or function(obj_inner) + assert(false, 'Converting ' .. tostring(tonumber(obj_inner['type'])) .. ' is not implementing yet') + end)(obj)) +end + +local obj = function(typ, data) + return ffi.gc(ffi.new('Object', {['type']=typ, data=data}), + api.api_free_object) +end + +local lua2obj + +local lua2obj_type_tab = { + [int_type] = function(l) + return obj(api.kObjectTypeInteger, {integer=l.value}) + end, + [flt_type] = function(l) + return obj(api.kObjectTypeFloat, {floating=l}) + end, + [list_type] = function(l) + local len = #l + local arr = obj(api.kObjectTypeArray, {array={ + size=len, + capacity=len, + items=ffi.cast('Object *', api.xmalloc(len * ffi.sizeof('Object'))), + }}) + for i = 1, len do + arr.data.array.items[i - 1] = ffi.gc(lua2obj(l[i]), nil) + end + return arr + end, + [dict_type] = function(l) + local kvs = {} + for k, v in pairs(l) do + if type(k) == 'string' then + kvs[#kvs + 1] = {k, v} + end + end + local len = #kvs + local dct = obj(api.kObjectTypeDictionary, {dictionary={ + size=len, + capacity=len, + items=ffi.cast('KeyValuePair *', + api.xmalloc(len * ffi.sizeof('KeyValuePair'))), + }}) + for i = 1, len do + local key, val = table.unpack(kvs[i]) + dct.data.dictionary.items[i - 1] = ffi.new( + 'KeyValuePair', {key=ffi.gc(lua2obj(key), nil).data.string, + value=ffi.gc(lua2obj(val), nil)}) + end + return dct + end, +} + +lua2obj = function(l) + if type(l) == 'table' then + if l[type_key] then + return lua2obj_type_tab[l[type_key]](l) + else + if l[1] then + return lua2obj_type_tab[list_type](l) + else + return lua2obj_type_tab[dict_type](l) + end + end + elseif type(l) == 'number' then + return lua2obj_type_tab[flt_type](l) + elseif type(l) == 'boolean' then + return obj(api.kObjectTypeBoolean, {boolean=l}) + elseif type(l) == 'string' then + return obj(api.kObjectTypeString, {string={ + size=#l, + data=api.xmemdupz(to_cstr(l), #l), + }}) + elseif l == nil or l == nil_value then + return obj(api.kObjectTypeNil, {integer=0}) + end +end + +return { + list_type=list_type, + dict_type=dict_type, + func_type=func_type, + int_type=int_type, + flt_type=flt_type, + + nil_value=nil_value, + + type_key=type_key, + + obj2lua=obj2lua, + lua2obj=lua2obj, +} diff --git a/test/unit/api/private_helpers_spec.lua b/test/unit/api/private_helpers_spec.lua new file mode 100644 index 0000000000..8c54ea6a2a --- /dev/null +++ b/test/unit/api/private_helpers_spec.lua @@ -0,0 +1,105 @@ +local helpers = require('test.unit.helpers') +local eval_helpers = require('test.unit.eval.helpers') +local api_helpers = require('test.unit.api.helpers') + +local cimport = helpers.cimport +local NULL = helpers.NULL +local eq = helpers.eq + +local lua2typvalt = eval_helpers.lua2typvalt +local typvalt2lua = eval_helpers.typvalt2lua +local typvalt = eval_helpers.typvalt + +local nil_value = api_helpers.nil_value +local list_type = api_helpers.list_type +local int_type = api_helpers.int_type +local type_key = api_helpers.type_key +local obj2lua = api_helpers.obj2lua +local func_type = api_helpers.func_type + +local api = cimport('./src/nvim/api/private/helpers.h') + +describe('vim_to_object', function() + local vim_to_object = function(l) + return obj2lua(api.vim_to_object(lua2typvalt(l))) + end + + local different_output_test = function(name, input, output) + it(name, function() + eq(output, vim_to_object(input)) + end) + end + + local simple_test = function(name, l) + different_output_test(name, l, l) + end + + simple_test('converts true', true) + simple_test('converts false', false) + simple_test('converts nil', nil_value) + simple_test('converts 1', 1) + simple_test('converts -1.5', -1.5) + simple_test('converts empty string', '') + simple_test('converts non-empty string', 'foobar') + simple_test('converts integer 10', {[type_key]=int_type, value=10}) + simple_test('converts empty dictionary', {}) + simple_test('converts dictionary with scalar values', {test=10, test2=true, test3='test'}) + simple_test('converts dictionary with containers inside', {test={}, test2={1, 2}}) + simple_test('converts empty list', {[type_key]=list_type}) + simple_test('converts list with scalar values', {1, 2, 'test', 'foo'}) + simple_test('converts list with containers inside', {{}, {test={}, test3={test4=true}}}) + + local dct = {} + dct.dct = dct + different_output_test('outputs nil for nested dictionaries (1 level)', dct, {dct=nil_value}) + + local lst = {} + lst[1] = lst + different_output_test('outputs nil for nested lists (1 level)', lst, {nil_value}) + + local dct2 = {test=true, dict=nil_value} + dct2.dct = {dct2} + different_output_test('outputs nil for nested dictionaries (2 level, in list)', + dct2, {dct={nil_value}, test=true, dict=nil_value}) + + local dct3 = {test=true, dict=nil_value} + dct3.dct = {dctin=dct3} + different_output_test('outputs nil for nested dictionaries (2 level, in dict)', + dct3, {dct={dctin=nil_value}, test=true, dict=nil_value}) + + local lst2 = {} + lst2[1] = {lst2} + different_output_test('outputs nil for nested lists (2 level, in list)', lst2, {{nil_value}}) + + local lst3 = {nil, true, false, 'ttest'} + lst3[1] = {lst=lst3} + different_output_test('outputs nil for nested lists (2 level, in dict)', + lst3, {{lst=nil_value}, true, false, 'ttest'}) + + it('outputs empty list for NULL list', function() + local tt = typvalt('VAR_LIST', {v_list=NULL}) + eq(nil, tt.vval.v_list) + eq({[type_key]=list_type}, obj2lua(api.vim_to_object(tt))) + end) + + it('outputs empty dict for NULL dict', function() + local tt = typvalt('VAR_DICT', {v_dict=NULL}) + eq(nil, tt.vval.v_dict) + eq({}, obj2lua(api.vim_to_object(tt))) + end) + + it('regression: partials in a list', function() + local llist = { + { + [type_key]=func_type, + value='printf', + args={'%s'}, + dict={v=1}, + }, + {}, + } + local list = lua2typvalt(llist) + eq(llist, typvalt2lua(list)) + eq({nil_value, {}}, obj2lua(api.vim_to_object(list))) + end) +end) diff --git a/test/unit/buffer_spec.lua b/test/unit/buffer_spec.lua index b7f82064d7..49a4d84279 100644 --- a/test/unit/buffer_spec.lua +++ b/test/unit/buffer_spec.lua @@ -1,10 +1,9 @@ -local assert = require("luassert") local helpers = require("test.unit.helpers") local to_cstr = helpers.to_cstr +local get_str = helpers.ffi.string local eq = helpers.eq -local neq = helpers.neq local NULL = helpers.NULL local globals = helpers.cimport("./src/nvim/globals.h") @@ -88,7 +87,7 @@ describe('buffer functions', function() it('should find exact matches', function() local buf = buflist_new(path1, buffer.BLN_LISTED) - eq(buf.b_fnum, buflist_findpat(path1, ONLY_LISTED)) + eq(buf.handle, buflist_findpat(path1, ONLY_LISTED)) close_buffer(NULL, buf, buffer.DOBUF_WIPE, 0) end) @@ -98,9 +97,9 @@ describe('buffer functions', function() local buf2 = buflist_new(path2, buffer.BLN_LISTED) local buf3 = buflist_new(path3, buffer.BLN_LISTED) - eq(buf1.b_fnum, buflist_findpat("test", ONLY_LISTED)) - eq(buf2.b_fnum, buflist_findpat("file", ONLY_LISTED)) - eq(buf3.b_fnum, buflist_findpat("path", ONLY_LISTED)) + eq(buf1.handle, buflist_findpat("test", ONLY_LISTED)) + eq(buf2.handle, buflist_findpat("file", ONLY_LISTED)) + eq(buf3.handle, buflist_findpat("path", ONLY_LISTED)) close_buffer(NULL, buf1, buffer.DOBUF_WIPE, 0) close_buffer(NULL, buf2, buffer.DOBUF_WIPE, 0) @@ -114,7 +113,7 @@ describe('buffer functions', function() local buf3 = buflist_new(path3, buffer.BLN_LISTED) -- Then: buf2 is the buffer that is found - eq(buf2.b_fnum, buflist_findpat("test", ONLY_LISTED)) + eq(buf2.handle, buflist_findpat("test", ONLY_LISTED)) --} --{ When: We close buf2 @@ -124,7 +123,7 @@ describe('buffer functions', function() local buf1 = buflist_new(path1, buffer.BLN_LISTED) -- Then: buf3 is found since 'file' appears at the end of the name - eq(buf3.b_fnum, buflist_findpat("file", ONLY_LISTED)) + eq(buf3.handle, buflist_findpat("file", ONLY_LISTED)) --} close_buffer(NULL, buf1, buffer.DOBUF_WIPE, 0) @@ -136,7 +135,7 @@ describe('buffer functions', function() local buf2 = buflist_new(path2, buffer.BLN_LISTED) local buf3 = buflist_new(path3, buffer.BLN_LISTED) - eq(buf3.b_fnum, buflist_findpat("_test_", ONLY_LISTED)) + eq(buf3.handle, buflist_findpat("_test_", ONLY_LISTED)) close_buffer(NULL, buf1, buffer.DOBUF_WIPE, 0) close_buffer(NULL, buf2, buffer.DOBUF_WIPE, 0) @@ -148,7 +147,7 @@ describe('buffer functions', function() local buf3 = buflist_new(path3, buffer.BLN_LISTED) -- Then: We should find the buffer when it is given a unique pattern - eq(buf3.b_fnum, buflist_findpat("_test_", ONLY_LISTED)) + eq(buf3.handle, buflist_findpat("_test_", ONLY_LISTED)) --} --{ When: We unlist the buffer @@ -158,7 +157,7 @@ describe('buffer functions', function() eq(-1, buflist_findpat("_test_", ONLY_LISTED)) -- And: It should find the buffer when including unlisted buffers - eq(buf3.b_fnum, buflist_findpat("_test_", ALLOW_UNLISTED)) + eq(buf3.handle, buflist_findpat("_test_", ALLOW_UNLISTED)) --} --{ When: We wipe the buffer @@ -176,7 +175,7 @@ describe('buffer functions', function() local buf2 = buflist_new(path2, buffer.BLN_LISTED) -- Then: The first buffer is preferred when both are listed - eq(buf1.b_fnum, buflist_findpat("test", ONLY_LISTED)) + eq(buf1.handle, buflist_findpat("test", ONLY_LISTED)) --} --{ When: The first buffer is unlisted @@ -184,13 +183,13 @@ describe('buffer functions', function() -- Then: The second buffer is preferred because -- unlisted buffers are not allowed - eq(buf2.b_fnum, buflist_findpat("test", ONLY_LISTED)) + eq(buf2.handle, buflist_findpat("test", ONLY_LISTED)) --} --{ When: We allow unlisted buffers -- Then: The second buffer is still preferred -- because listed buffers are preferred to unlisted - eq(buf2.b_fnum, buflist_findpat("test", ALLOW_UNLISTED)) + eq(buf2.handle, buflist_findpat("test", ALLOW_UNLISTED)) --} --{ When: We unlist the second buffer @@ -199,7 +198,7 @@ describe('buffer functions', function() -- Then: The first buffer is preferred again -- because buf1 matches better which takes precedence -- when both buffers have the same listing status. - eq(buf1.b_fnum, buflist_findpat("test", ALLOW_UNLISTED)) + eq(buf1.handle, buflist_findpat("test", ALLOW_UNLISTED)) -- And: Neither buffer is returned when ignoring unlisted eq(-1, buflist_findpat("test", ONLY_LISTED)) @@ -211,93 +210,246 @@ describe('buffer functions', function() end) describe('build_stl_str_hl', function() + local buffer_byte_size = 100 + local STL_MAX_ITEM = 80 + local output_buffer = '' + + -- This function builds the statusline + -- + -- @param arg Optional arguments are: + -- .pat The statusline format string + -- .fillchar The fill character used in the statusline + -- .maximum_cell_count The number of cells available in the statusline + local function build_stl_str_hl(arg) + output_buffer = to_cstr(string.rep(" ", buffer_byte_size)) + + local pat = arg.pat or '' + local fillchar = arg.fillchar or (' '):byte() + local maximum_cell_count = arg.maximum_cell_count or buffer_byte_size - local output_buffer = to_cstr(string.rep(" ", 100)) - - local build_stl_str_hl = function(pat) return buffer.build_stl_str_hl(globals.curwin, output_buffer, - 100, + buffer_byte_size, to_cstr(pat), false, - 32, - 80, + fillchar, + maximum_cell_count, NULL, NULL) end - it('should copy plain text', function() - local width = build_stl_str_hl("this is a test") - - eq(14, width) - eq("this is a test", helpers.ffi.string(output_buffer, width)) - - end) - - it('should print no file name', function() - local width = build_stl_str_hl("%f") - - eq(9, width) - eq("[No Name]", helpers.ffi.string(output_buffer, width)) - - end) - - it('should print the relative file name', function() - buffer.setfname(globals.curbuf, to_cstr("Makefile"), NULL, 1) - local width = build_stl_str_hl("%f") - - eq(8, width) - eq("Makefile", helpers.ffi.string(output_buffer, width)) - - end) - - it('should print the full file name', function() - buffer.setfname(globals.curbuf, to_cstr("Makefile"), NULL, 1) - - local width = build_stl_str_hl("%F") - - assert.is_true(8 < width) - neq(NULL, string.find(helpers.ffi.string(output_buffer, width), "Makefile")) - - end) - - it('should print the tail file name', function() - buffer.setfname(globals.curbuf, to_cstr("src/nvim/buffer.c"), NULL, 1) - - local width = build_stl_str_hl("%t") - - eq(8, width) - eq("buffer.c", helpers.ffi.string(output_buffer, width)) - - end) - - it('should print the buffer number', function() - buffer.setfname(globals.curbuf, to_cstr("src/nvim/buffer.c"), NULL, 1) - - local width = build_stl_str_hl("%n") - - eq(1, width) - eq("1", helpers.ffi.string(output_buffer, width)) - end) - - it('should print the current line number in the buffer', function() - buffer.setfname(globals.curbuf, to_cstr("test/unit/buffer_spec.lua"), NULL, 1) - - local width = build_stl_str_hl("%l") - - eq(1, width) - eq("0", helpers.ffi.string(output_buffer, width)) - - end) - - it('should print the number of lines in the buffer', function() - buffer.setfname(globals.curbuf, to_cstr("test/unit/buffer_spec.lua"), NULL, 1) - - local width = build_stl_str_hl("%L") + -- Use this function to simplify testing the comparison between + -- the format string and the resulting statusline. + -- + -- @param description The description of what the test should be doing + -- @param statusline_cell_count The number of cells available in the statusline + -- @param input_stl The format string for the statusline + -- @param expected_stl The expected result string for the statusline + -- + -- @param arg Options can be placed in an optional dictionary as the last parameter + -- .expected_cell_count The expected number of cells build_stl_str_hl will return + -- .expected_byte_length The expected byte length of the string + -- .file_name The name of the file to be tested (useful in %f type tests) + -- .fillchar The character that will be used to fill any 'extra' space in the stl + local function statusline_test (description, + statusline_cell_count, + input_stl, + expected_stl, + arg) + + -- arg is the optional parameter + -- so we either fill in option with arg or an empty dictionary + local option = arg or {} + + local fillchar = option.fillchar or (' '):byte() + local expected_cell_count = option.expected_cell_count or statusline_cell_count + local expected_byte_length = option.expected_byte_length or expected_cell_count + + it(description, function() + if option.file_name then + buffer.setfname(globals.curbuf, to_cstr(option.file_name), NULL, 1) + else + buffer.setfname(globals.curbuf, nil, NULL, 1) + end + + local result_cell_count = build_stl_str_hl{pat=input_stl, + maximum_cell_count=statusline_cell_count, + fillchar=fillchar} + + eq(expected_stl, get_str(output_buffer, expected_byte_length)) + eq(expected_cell_count, result_cell_count) + end) + end - eq(1, width) - eq("1", helpers.ffi.string(output_buffer, width)) + -- file name testing + statusline_test('should print no file name', 10, + '%f', '[No Name]', + {expected_cell_count=9}) + statusline_test('should print the relative file name', 30, + '%f', 'test/unit/buffer_spec.lua', + {file_name='test/unit/buffer_spec.lua', expected_cell_count=25}) + statusline_test('should print the full file name', 40, + '%F', '/test/unit/buffer_spec.lua', + {file_name='/test/unit/buffer_spec.lua', expected_cell_count=26}) + + -- fillchar testing + statusline_test('should handle `!` as a fillchar', 10, + 'abcde%=', 'abcde!!!!!', + {fillchar=('!'):byte()}) + statusline_test('should handle `~` as a fillchar', 10, + '%=abcde', '~~~~~abcde', + {fillchar=('~'):byte()}) + statusline_test('should put fillchar `!` in between text', 10, + 'abc%=def', 'abc!!!!def', + {fillchar=('!'):byte()}) + statusline_test('should put fillchar `~` in between text', 10, + 'abc%=def', 'abc~~~~def', + {fillchar=('~'):byte()}) + statusline_test('should print the tail file name', 80, + '%t', 'buffer_spec.lua', + {file_name='test/unit/buffer_spec.lua', expected_cell_count=15}) + + -- standard text testing + statusline_test('should copy plain text', 80, + 'this is a test', 'this is a test', + {expected_cell_count=14}) + + -- line number testing + statusline_test('should print the buffer number', 80, + '%n', '1', + {expected_cell_count=1}) + statusline_test('should print the current line number in the buffer', 80, + '%l', '0', + {expected_cell_count=1}) + statusline_test('should print the number of lines in the buffer', 80, + '%L', '1', + {expected_cell_count=1}) + + -- truncation testing + statusline_test('should truncate when standard text pattern is too long', 10, + '0123456789abcde', '<6789abcde') + statusline_test('should truncate when using =', 10, + 'abcdef%=ghijkl', 'abcdef<jkl') + statusline_test('should truncate centered text when using ==', 10, + 'abcde%=gone%=fghij', 'abcde<ghij') + statusline_test('should respect the `<` marker', 10, + 'abc%<defghijkl', 'abc<ghijkl') + statusline_test('should truncate at `<` with one `=`, test 1', 10, + 'abc%<def%=ghijklmno', 'abc<jklmno') + statusline_test('should truncate at `<` with one `=`, test 2', 10, + 'abcdef%=ghijkl%<mno', 'abcdefghi>') + statusline_test('should truncate at `<` with one `=`, test 3', 10, + 'abc%<def%=ghijklmno', 'abc<jklmno') + statusline_test('should truncate at `<` with one `=`, test 4', 10, + 'abc%<def%=ghij', 'abcdefghij') + statusline_test('should truncate at `<` with one `=`, test 4', 10, + 'abc%<def%=ghijk', 'abc<fghijk') + + statusline_test('should truncate at `<` with many `=`, test 4', 10, + 'ab%<cdef%=g%=h%=ijk', 'ab<efghijk') + + statusline_test('should truncate at the first `<`', 10, + 'abc%<def%<ghijklm', 'abc<hijklm') + + -- alignment testing + statusline_test('should right align when using =', 20, + 'neo%=vim', 'neo vim') + statusline_test('should, when possible, center text when using %=text%=', 20, + 'abc%=neovim%=def', 'abc neovim def') + statusline_test('should handle uneven spacing in the buffer when using %=text%=', 20, + 'abc%=neo_vim%=def', 'abc neo_vim def') + statusline_test('should have equal spaces even with non-equal sides when using =', 20, + 'foobar%=test%=baz', 'foobar test baz') + statusline_test('should have equal spaces even with longer right side when using =', 20, + 'a%=test%=longtext', 'a test longtext') + statusline_test('should handle an empty left side when using ==', 20, + '%=test%=baz', ' test baz') + statusline_test('should handle an empty right side when using ==', 20, + 'foobar%=test%=', 'foobar test ') + statusline_test('should handle consecutive empty ==', 20, + '%=%=test%=', ' test ') + statusline_test('should handle an = alone', 20, + '%=', ' ') + statusline_test('should right align text when it is alone with =', 20, + '%=foo', ' foo') + statusline_test('should left align text when it is alone with =', 20, + 'foo%=', 'foo ') + + statusline_test('should approximately center text when using %=text%=', 21, + 'abc%=neovim%=def', 'abc neovim def') + statusline_test('should completely fill the buffer when using %=text%=', 21, + 'abc%=neo_vim%=def', 'abc neo_vim def') + statusline_test('should have equal spaces even with non-equal sides when using =', 21, + 'foobar%=test%=baz', 'foobar test baz') + statusline_test('should have equal spaces even with longer right side when using =', 21, + 'a%=test%=longtext', 'a test longtext') + statusline_test('should handle an empty left side when using ==', 21, + '%=test%=baz', ' test baz') + statusline_test('should handle an empty right side when using ==', 21, + 'foobar%=test%=', 'foobar test ') + + statusline_test('should quadrant the text when using 3 %=', 40, + 'abcd%=n%=eovim%=ef', 'abcd n eovim ef') + statusline_test('should work well with %t', 40, + '%t%=right_aligned', 'buffer_spec.lua right_aligned', + {file_name='test/unit/buffer_spec.lua'}) + statusline_test('should work well with %t and regular text', 40, + 'l%=m_l %t m_r%=r', 'l m_l buffer_spec.lua m_r r', + {file_name='test/unit/buffer_spec.lua'}) + statusline_test('should work well with %=, %t, %L, and %l', 40, + '%t %= %L %= %l', 'buffer_spec.lua 1 0', + {file_name='test/unit/buffer_spec.lua'}) + + statusline_test('should quadrant the text when using 3 %=', 41, + 'abcd%=n%=eovim%=ef', 'abcd n eovim ef') + statusline_test('should work well with %t', 41, + '%t%=right_aligned', 'buffer_spec.lua right_aligned', + {file_name='test/unit/buffer_spec.lua'}) + statusline_test('should work well with %t and regular text', 41, + 'l%=m_l %t m_r%=r', 'l m_l buffer_spec.lua m_r r', + {file_name='test/unit/buffer_spec.lua'}) + statusline_test('should work well with %=, %t, %L, and %l', 41, + '%t %= %L %= %l', 'buffer_spec.lua 1 0', + {file_name='test/unit/buffer_spec.lua'}) + + statusline_test('should work with 10 %=', 50, + 'aaaa%=b%=c%=d%=e%=fg%=hi%=jk%=lmnop%=qrstuv%=wxyz', + 'aaaa b c d e fg hi jk lmnop qrstuv wxyz') + + -- maximum stl item testing + statusline_test('should handle a much larger amount of = than buffer locations', 20, + ('%='):rep(STL_MAX_ITEM - 1), + ' ') -- Should be fine, because within limit + statusline_test('should handle a much larger amount of = than stl max item', 20, + ('%='):rep(STL_MAX_ITEM + 1), + ' E541') -- Should show the VIM error + statusline_test('should handle many extra characters', 20, + 'a' .. ('a'):rep(STL_MAX_ITEM * 4), + '<aaaaaaaaaaaaaaaaaaa') -- Does not show the error because there are no items + statusline_test('should handle almost maximum of characters and flags', 20, + 'a' .. ('%=a'):rep(STL_MAX_ITEM - 1), + 'a<aaaaaaaaaaaaaaaaaa') -- Should not show the VIM error + statusline_test('should handle many extra characters and flags', 20, + 'a' .. ('%=a'):rep(STL_MAX_ITEM), + 'a<aaaaaaaaaaaaa E541') -- Should show the VIM error + statusline_test('should handle many extra characters and flags', 20, + 'a' .. ('%=a'):rep(STL_MAX_ITEM * 2), + 'a<aaaaaaaaaaaaa E541') -- Should show the VIM error + statusline_test('should handle many extra characters and flags with truncation', 20, + 'aaa%<' .. ('%=a'):rep(STL_MAX_ITEM), + 'aaa<aaaaaaaaaaa E541') -- Should show the VIM error + statusline_test('should handle many characters and flags before and after truncation', 20, + 'a%=a%=a%<' .. ('%=a'):rep(STL_MAX_ITEM), + 'aaa<aaaaaaaaaaa E541') -- Should show the VIM error + + + -- multi-byte testing + statusline_test('should handle multibyte characters', 10, + 'Ĉ%=x', 'Ĉ x', + {expected_byte_length=11}) + statusline_test('should handle multibyte characters and different fillchars', 10, + 'Ą%=mid%=end', 'Ą@mid@@end', + {fillchar=('@'):byte(), expected_byte_length=11}) - end) end) end) diff --git a/test/unit/eval/decode_spec.lua b/test/unit/eval/decode_spec.lua index d94d809c14..742b754d8a 100644 --- a/test/unit/eval/decode_spec.lua +++ b/test/unit/eval/decode_spec.lua @@ -32,7 +32,7 @@ describe('json_decode_string()', function() it('does not overflow when running with `n…`, `t…`, `f…`', function() local rettv = ffi.new('typval_T', {v_type=decode.VAR_UNKNOWN}) decode.emsg_silent = 1 - -- This will not crash, but if `len` argument will be ignored it will parse + -- This will not crash, but if `len` argument will be ignored it will parse -- `null` as `null` and if not it will parse `null` as `n`. eq(0, decode.json_decode_string('null', 1, rettv)) eq(decode.VAR_UNKNOWN, rettv.v_type) diff --git a/test/unit/eval/encode_spec.lua b/test/unit/eval/encode_spec.lua index f151a191fb..98fc8305e0 100644 --- a/test/unit/eval/encode_spec.lua +++ b/test/unit/eval/encode_spec.lua @@ -27,74 +27,74 @@ describe('encode_list_write()', function() it('writes ASCII string literal with printable characters', function() local l = list() eq(0, encode_list_write(l, 'abc')) - eq({[type_key]=list_type, 'abc'}, lst2tbl(l)) + eq({'abc'}, lst2tbl(l)) end) it('writes string starting with NL', function() local l = list() eq(0, encode_list_write(l, '\nabc')) - eq({[type_key]=list_type, null_string, 'abc'}, lst2tbl(l)) + eq({null_string, 'abc'}, lst2tbl(l)) end) it('writes string starting with NL twice', function() local l = list() eq(0, encode_list_write(l, '\nabc')) - eq({[type_key]=list_type, null_string, 'abc'}, lst2tbl(l)) + eq({null_string, 'abc'}, lst2tbl(l)) eq(0, encode_list_write(l, '\nabc')) - eq({[type_key]=list_type, null_string, 'abc', 'abc'}, lst2tbl(l)) + eq({null_string, 'abc', 'abc'}, lst2tbl(l)) end) it('writes string ending with NL', function() local l = list() eq(0, encode_list_write(l, 'abc\n')) - eq({[type_key]=list_type, 'abc', null_string}, lst2tbl(l)) + eq({'abc', null_string}, lst2tbl(l)) end) it('writes string ending with NL twice', function() local l = list() eq(0, encode_list_write(l, 'abc\n')) - eq({[type_key]=list_type, 'abc', null_string}, lst2tbl(l)) + eq({'abc', null_string}, lst2tbl(l)) eq(0, encode_list_write(l, 'abc\n')) - eq({[type_key]=list_type, 'abc', 'abc', null_string}, lst2tbl(l)) + eq({'abc', 'abc', null_string}, lst2tbl(l)) end) it('writes string starting, ending and containing NL twice', function() local l = list() eq(0, encode_list_write(l, '\na\nb\n')) - eq({[type_key]=list_type, null_string, 'a', 'b', null_string}, lst2tbl(l)) + eq({null_string, 'a', 'b', null_string}, lst2tbl(l)) eq(0, encode_list_write(l, '\na\nb\n')) - eq({[type_key]=list_type, null_string, 'a', 'b', null_string, 'a', 'b', null_string}, lst2tbl(l)) + eq({null_string, 'a', 'b', null_string, 'a', 'b', null_string}, lst2tbl(l)) end) it('writes string starting, ending and containing NUL with NL between twice', function() local l = list() eq(0, encode_list_write(l, '\0\n\0\n\0')) - eq({[type_key]=list_type, '\n', '\n', '\n'}, lst2tbl(l)) + eq({'\n', '\n', '\n'}, lst2tbl(l)) eq(0, encode_list_write(l, '\0\n\0\n\0')) - eq({[type_key]=list_type, '\n', '\n', '\n\n', '\n', '\n'}, lst2tbl(l)) + eq({'\n', '\n', '\n\n', '\n', '\n'}, lst2tbl(l)) end) it('writes string starting, ending and containing NL with NUL between twice', function() local l = list() eq(0, encode_list_write(l, '\n\0\n\0\n')) - eq({[type_key]=list_type, null_string, '\n', '\n', null_string}, lst2tbl(l)) + eq({null_string, '\n', '\n', null_string}, lst2tbl(l)) eq(0, encode_list_write(l, '\n\0\n\0\n')) - eq({[type_key]=list_type, null_string, '\n', '\n', null_string, '\n', '\n', null_string}, lst2tbl(l)) + eq({null_string, '\n', '\n', null_string, '\n', '\n', null_string}, lst2tbl(l)) end) it('writes string containing a single NL twice', function() local l = list() eq(0, encode_list_write(l, '\n')) - eq({[type_key]=list_type, null_string, null_string}, lst2tbl(l)) + eq({null_string, null_string}, lst2tbl(l)) eq(0, encode_list_write(l, '\n')) - eq({[type_key]=list_type, null_string, null_string, null_string}, lst2tbl(l)) + eq({null_string, null_string, null_string}, lst2tbl(l)) end) it('writes string containing a few NLs twice', function() local l = list() eq(0, encode_list_write(l, '\n\n\n')) - eq({[type_key]=list_type, null_string, null_string, null_string, null_string}, lst2tbl(l)) + eq({null_string, null_string, null_string, null_string}, lst2tbl(l)) eq(0, encode_list_write(l, '\n\n\n')) - eq({[type_key]=list_type, null_string, null_string, null_string, null_string, null_string, null_string, null_string}, lst2tbl(l)) + eq({null_string, null_string, null_string, null_string, null_string, null_string, null_string}, lst2tbl(l)) end) end) diff --git a/test/unit/eval/helpers.lua b/test/unit/eval/helpers.lua index 2367f03e0d..c3c27e4fed 100644 --- a/test/unit/eval/helpers.lua +++ b/test/unit/eval/helpers.lua @@ -5,12 +5,32 @@ local to_cstr = helpers.to_cstr local ffi = helpers.ffi local eq = helpers.eq -local eval = cimport('./src/nvim/eval.h', './src/nvim/eval_defs.h') +local eval = cimport('./src/nvim/eval.h', './src/nvim/eval_defs.h', + './src/nvim/hashtab.h') local null_string = {[true]='NULL string'} local null_list = {[true]='NULL list'} +local null_dict = {[true]='NULL dict'} local type_key = {[true]='type key'} local list_type = {[true]='list type'} +local dict_type = {[true]='dict type'} +local func_type = {[true]='func type'} +local int_type = {[true]='int type'} +local flt_type = {[true]='flt type'} + +local nil_value = {[true]='nil'} + +local lua2typvalt + +local function li_alloc(nogc) + local gcfunc = eval.listitem_free + if nogc then gcfunc = nil end + local li = ffi.gc(eval.listitem_alloc(), gcfunc) + li.li_next = nil + li.li_prev = nil + li.li_tv = {v_type=eval.VAR_UNKNOWN, v_lock=eval.VAR_UNLOCKED} + return li +end local function list(...) local ret = ffi.gc(eval.list_alloc(), eval.list_unref) @@ -18,55 +38,380 @@ local function list(...) ret.lv_refcount = 1 for i = 1, select('#', ...) do local val = select(i, ...) - local typ = type(val) - if typ == 'string' then - eval.list_append_string(ret, to_cstr(val)) - elseif typ == 'table' and val == null_string then - eval.list_append_string(ret, nil) - elseif typ == 'table' and val == null_list then - eval.list_append_list(ret, nil) - elseif typ == 'table' and val[type_key] == list_type then - local itemlist = ffi.gc(list(table.unpack(val)), nil) - eq(1, itemlist.lv_refcount) - itemlist.lv_refcount = 0 - eval.list_append_list(ret, itemlist) + local li_tv = ffi.gc(lua2typvalt(val), nil) + local li = li_alloc(true) + li.li_tv = li_tv + eval.tv_list_append(ret, li) + end + return ret +end + +local special_tab = { + [eval.kSpecialVarFalse] = false, + [eval.kSpecialVarNull] = nil_value, + [eval.kSpecialVarTrue] = true, +} + +local ptr2key = function(ptr) + return tostring(ptr) +end + +local lst2tbl +local dct2tbl + +local typvalt2lua +local typvalt2lua_tab + +typvalt2lua_tab = { + [tonumber(eval.VAR_SPECIAL)] = function(t) + return special_tab[t.vval.v_special] + end, + [tonumber(eval.VAR_NUMBER)] = function(t) + return {[type_key]=int_type, value=tonumber(t.vval.v_number)} + end, + [tonumber(eval.VAR_FLOAT)] = function(t) + return tonumber(t.vval.v_float) + end, + [tonumber(eval.VAR_STRING)] = function(t) + local str = t.vval.v_string + if str == nil then + return null_string else - assert(false, 'Not implemented yet') + return ffi.string(str) + end + end, + [tonumber(eval.VAR_LIST)] = function(t, processed) + return lst2tbl(t.vval.v_list, processed) + end, + [tonumber(eval.VAR_DICT)] = function(t, processed) + return dct2tbl(t.vval.v_dict, processed) + end, + [tonumber(eval.VAR_FUNC)] = function(t, processed) + return {[type_key]=func_type, value=typvalt2lua_tab[eval.VAR_STRING](t, processed or {})} + end, + [tonumber(eval.VAR_PARTIAL)] = function(t, processed) + local p_key = ptr2key(t) + if processed[p_key] then + return processed[p_key] + end + local pt = t.vval.v_partial + local value, auto, dict, argv = nil, nil, nil, nil + if pt ~= nil then + value = ffi.string(pt.pt_name) + auto = pt.pt_auto and true or nil + argv = {} + for i = 1, pt.pt_argc do + argv[i] = typvalt2lua(pt.pt_argv[i - 1], processed) + end + if pt.pt_dict ~= nil then + dict = dct2tbl(pt.pt_dict) + end + end + return { + [type_key]=func_type, + value=value, + auto=auto, + args=argv, + dict=dict, + } + end, +} + +typvalt2lua = function(t, processed) + return ((typvalt2lua_tab[tonumber(t.v_type)] or function(t_inner) + assert(false, 'Converting ' .. tonumber(t_inner.v_type) .. ' was not implemented yet') + end)(t, processed or {})) +end + +local function list_iter(l) + local init_s = { + idx=0, + li=l.lv_first, + } + local function f(s, _) + -- (listitem_T *) NULL is equal to nil, but yet it is not false. + if s.li == nil then + return nil end + local ret_li = s.li + s.li = s.li.li_next + s.idx = s.idx + 1 + return s.idx, ret_li + end + return f, init_s, nil +end + +local function list_items(l) + local ret = {} + for i, li in list_iter(l) do + ret[i] = li end return ret end -local lst2tbl = function(l) - local ret = {[type_key]=list_type} +lst2tbl = function(l, processed) if l == nil then + return null_list + end + processed = processed or {} + local p_key = ptr2key(l) + if processed[p_key] then + return processed[p_key] + end + local ret = {[type_key]=list_type} + processed[p_key] = ret + for i, li in list_iter(l) do + ret[i] = typvalt2lua(li.li_tv, processed) + end + if ret[1] then + ret[type_key] = nil + end + return ret +end + +local hi_key_removed = eval._hash_key_removed() + +local function dict_iter(d, return_hi) + local init_s = { + todo=d.dv_hashtab.ht_used, + hi=d.dv_hashtab.ht_array, + } + local function f(s, _) + if s.todo == 0 then return nil end + while s.todo > 0 do + if s.hi.hi_key ~= nil and s.hi.hi_key ~= hi_key_removed then + local key = ffi.string(s.hi.hi_key) + local ret + if return_hi then + ret = s.hi + else + ret = ffi.cast('dictitem_T*', + s.hi.hi_key - ffi.offsetof('dictitem_T', 'di_key')) + end + s.todo = s.todo - 1 + s.hi = s.hi + 1 + return key, ret + end + s.hi = s.hi + 1 + end + end + return f, init_s, nil +end + +local function first_di(d) + local f, init_s, v = dict_iter(d) + return select(2, f(init_s, v)) +end + +local function dict_items(d) + local ret = {[0]=0} + for k, hi in dict_iter(d) do + ret[k] = hi + ret[0] = ret[0] + 1 + ret[ret[0]] = hi + end + return ret +end + +dct2tbl = function(d, processed) + if d == nil then + return null_dict + end + processed = processed or {} + local p_key = ptr2key(d) + if processed[p_key] then + return processed[p_key] + end + local ret = {} + processed[p_key] = ret + for k, di in dict_iter(d) do + ret[k] = typvalt2lua(di.di_tv, processed) + end + return ret +end + +local typvalt = function(typ, vval) + if typ == nil then + typ = eval.VAR_UNKNOWN + elseif type(typ) == 'string' then + typ = eval[typ] + end + return ffi.gc(ffi.new('typval_T', {v_type=typ, vval=vval}), eval.clear_tv) +end + +local lua2typvalt_type_tab = { + [int_type] = function(l, _) + return typvalt(eval.VAR_NUMBER, {v_number=l.value}) + end, + [flt_type] = function(l, processed) + return lua2typvalt(l.value, processed) + end, + [list_type] = function(l, processed) + if processed[l] then + processed[l].lv_refcount = processed[l].lv_refcount + 1 + return typvalt(eval.VAR_LIST, {v_list=processed[l]}) + end + local lst = eval.list_alloc() + lst.lv_refcount = 1 + processed[l] = lst + local ret = typvalt(eval.VAR_LIST, {v_list=lst}) + for i = 1, #l do + local item_tv = ffi.gc(lua2typvalt(l[i], processed), nil) + eval.list_append_tv(lst, item_tv) + eval.clear_tv(item_tv) + end + return ret + end, + [dict_type] = function(l, processed) + if processed[l] then + processed[l].dv_refcount = processed[l].dv_refcount + 1 + return typvalt(eval.VAR_DICT, {v_dict=processed[l]}) + end + local dct = eval.dict_alloc() + dct.dv_refcount = 1 + processed[l] = dct + local ret = typvalt(eval.VAR_DICT, {v_dict=dct}) + for k, v in pairs(l) do + if type(k) == 'string' then + local di = eval.dictitem_alloc(to_cstr(k)) + local val_tv = ffi.gc(lua2typvalt(v, processed), nil) + eval.copy_tv(val_tv, di.di_tv) + eval.clear_tv(val_tv) + eval.dict_add(dct, di) + end + end return ret + end, + [func_type] = function(l, processed) + if processed[l] then + processed[l].pt_refcount = processed[l].pt_refcount + 1 + return typvalt(eval.VAR_PARTIAL, {v_partial=processed[l]}) + end + if l.args or l.dict then + local pt = ffi.gc(ffi.cast('partial_T*', eval.xmalloc(ffi.sizeof('partial_T'))), nil) + processed[l] = pt + local argv = nil + if l.args and #l.args > 0 then + argv = ffi.gc(ffi.cast('typval_T*', eval.xmalloc(ffi.sizeof('typval_T') * #l.args)), nil) + for i, arg in ipairs(l.args) do + local arg_tv = ffi.gc(lua2typvalt(arg, processed), nil) + eval.copy_tv(arg_tv, argv[i - 1]) + eval.clear_tv(arg_tv) + end + end + local dict = nil + if l.dict then + local dict_tv = ffi.gc(lua2typvalt(l.dict, processed), nil) + assert(dict_tv.v_type == eval.VAR_DICT) + dict = dict_tv.vval.v_dict + end + pt.pt_refcount = 1 + pt.pt_name = eval.xmemdupz(to_cstr(l.value), #l.value) + pt.pt_auto = not not l.auto + pt.pt_argc = l.args and #l.args or 0 + pt.pt_argv = argv + pt.pt_dict = dict + return typvalt(eval.VAR_PARTIAL, {v_partial=pt}) + else + return typvalt(eval.VAR_FUNC, { + v_string=eval.xmemdupz(to_cstr(l.value), #l.value) + }) + end + end, +} + +local special_vals = { + [null_string] = {eval.VAR_STRING, {v_string=ffi.cast('char_u*', nil)}}, + [null_list] = {eval.VAR_LIST, {v_list=ffi.cast('list_T*', nil)}}, + [null_dict] = {eval.VAR_DICT, {v_dict=ffi.cast('dict_T*', nil)}}, + [nil_value] = {eval.VAR_SPECIAL, {v_special=eval.kSpecialVarNull}}, + [true] = {eval.VAR_SPECIAL, {v_special=eval.kSpecialVarTrue}}, + [false] = {eval.VAR_SPECIAL, {v_special=eval.kSpecialVarFalse}}, +} + +for k, v in pairs(special_vals) do + local tmp = function(typ, vval) + special_vals[k] = function() + return typvalt(typ, vval) + end end - local li = l.lv_first - -- (listitem_T *) NULL is equal to nil, but yet it is not false. - while li ~= nil do - local typ = li.li_tv.v_type - if typ == eval.VAR_STRING then - local str = li.li_tv.vval.v_string - if str == nil then - ret[#ret + 1] = null_string + tmp(v[1], v[2]) +end + +lua2typvalt = function(l, processed) + processed = processed or {} + if l == nil or l == nil_value then + return special_vals[nil_value]() + elseif special_vals[l] then + return special_vals[l]() + elseif type(l) == 'table' then + if l[type_key] then + return lua2typvalt_type_tab[l[type_key]](l, processed) + else + if l[1] then + return lua2typvalt_type_tab[list_type](l, processed) else - ret[#ret + 1] = ffi.string(str) + return lua2typvalt_type_tab[dict_type](l, processed) end - else - assert(false, 'Not implemented yet') end - li = li.li_next + elseif type(l) == 'number' then + return typvalt(eval.VAR_FLOAT, {v_float=l}) + elseif type(l) == 'string' then + return typvalt(eval.VAR_STRING, {v_string=eval.xmemdupz(to_cstr(l), #l)}) + elseif type(l) == 'cdata' then + local tv = typvalt(eval.VAR_UNKNOWN) + eval.tv_copy(l, tv) + return tv end - return ret end +local function void(ptr) + return ffi.cast('void*', ptr) +end + +local alloc_logging_helpers = { + list = function(l) return {func='calloc', args={1, ffi.sizeof('list_T')}, ret=void(l)} end, + li = function(li) return {func='malloc', args={ffi.sizeof('listitem_T')}, ret=void(li)} end, + dict = function(d) return {func='malloc', args={ffi.sizeof('dict_T')}, ret=void(d)} end, + di = function(di, size) + return {func='malloc', args={ffi.offsetof('dictitem_T', 'di_key') + size + 1}, ret=void(di)} + end, + str = function(s, size) return {func='malloc', args={size + 1}, ret=void(s)} end, + + freed = function(p) return {func='free', args={p and void(p)}} end, +} + return { null_string=null_string, null_list=null_list, + null_dict=null_dict, list_type=list_type, + dict_type=dict_type, + func_type=func_type, + int_type=int_type, + flt_type=flt_type, + + nil_value=nil_value, + type_key=type_key, list=list, lst2tbl=lst2tbl, + dct2tbl=dct2tbl, + + lua2typvalt=lua2typvalt, + typvalt2lua=typvalt2lua, + + typvalt=typvalt, + + li_alloc=li_alloc, + + dict_iter=dict_iter, + list_iter=list_iter, + first_di=first_di, + + alloc_logging_helpers=alloc_logging_helpers, + + list_items=list_items, + dict_items=dict_items, } diff --git a/test/unit/eval/tv_clear_spec.lua b/test/unit/eval/tv_clear_spec.lua new file mode 100644 index 0000000000..96eccdbd71 --- /dev/null +++ b/test/unit/eval/tv_clear_spec.lua @@ -0,0 +1,127 @@ +local helpers = require('test.unit.helpers') +local eval_helpers = require('test.unit.eval.helpers') + +local alloc_log_new = helpers.alloc_log_new +local cimport = helpers.cimport +local ffi = helpers.ffi +local eq = helpers.eq + +local a = eval_helpers.alloc_logging_helpers +local type_key = eval_helpers.type_key +local list_type = eval_helpers.list_type +local list_items = eval_helpers.list_items +local dict_items = eval_helpers.dict_items +local lua2typvalt = eval_helpers.lua2typvalt + +local lib = cimport('./src/nvim/eval_defs.h', './src/nvim/eval.h') + +local alloc_log = alloc_log_new() + +before_each(function() + alloc_log:before_each() +end) + +after_each(function() + alloc_log:after_each() +end) + +describe('clear_tv()', function() + it('successfully frees all lists in [&l [1], *l, *l]', function() + local l_inner = {1} + local list = {l_inner, l_inner, l_inner} + local list_tv = ffi.gc(lua2typvalt(list), nil) + local list_p = list_tv.vval.v_list + local lis = list_items(list_p) + local list_inner_p = lis[1].li_tv.vval.v_list + local lis_inner = list_items(list_inner_p) + alloc_log:check({ + a.list(list_p), + a.list(list_inner_p), + a.li(lis_inner[1]), + a.li(lis[1]), + a.li(lis[2]), + a.li(lis[3]), + }) + eq(3, list_inner_p.lv_refcount) + lib.clear_tv(list_tv) + alloc_log:check({ + a.freed(lis_inner[1]), + a.freed(list_inner_p), + a.freed(lis[1]), + a.freed(lis[2]), + a.freed(lis[3]), + a.freed(list_p), + }) + end) + it('successfully frees all lists in [&l [], *l, *l]', function() + local l_inner = {[type_key]=list_type} + local list = {l_inner, l_inner, l_inner} + local list_tv = ffi.gc(lua2typvalt(list), nil) + local list_p = list_tv.vval.v_list + local lis = list_items(list_p) + local list_inner_p = lis[1].li_tv.vval.v_list + alloc_log:check({ + a.list(list_p), + a.list(list_inner_p), + a.li(lis[1]), + a.li(lis[2]), + a.li(lis[3]), + }) + eq(3, list_inner_p.lv_refcount) + lib.clear_tv(list_tv) + alloc_log:check({ + a.freed(list_inner_p), + a.freed(lis[1]), + a.freed(lis[2]), + a.freed(lis[3]), + a.freed(list_p), + }) + end) + it('successfully frees all dictionaries in [&d {}, *d]', function() + local d_inner = {} + local list = {d_inner, d_inner} + local list_tv = ffi.gc(lua2typvalt(list), nil) + local list_p = list_tv.vval.v_list + local lis = list_items(list_p) + local dict_inner_p = lis[1].li_tv.vval.v_dict + alloc_log:check({ + a.list(list_p), + a.dict(dict_inner_p), + a.li(lis[1]), + a.li(lis[2]), + }) + eq(2, dict_inner_p.dv_refcount) + lib.clear_tv(list_tv) + alloc_log:check({ + a.freed(dict_inner_p), + a.freed(lis[1]), + a.freed(lis[2]), + a.freed(list_p), + }) + end) + it('successfully frees all dictionaries in [&d {a: 1}, *d]', function() + local d_inner = {a=1} + local list = {d_inner, d_inner} + local list_tv = ffi.gc(lua2typvalt(list), nil) + local list_p = list_tv.vval.v_list + local lis = list_items(list_p) + local dict_inner_p = lis[1].li_tv.vval.v_dict + local dis = dict_items(dict_inner_p) + alloc_log:check({ + a.list(list_p), + a.dict(dict_inner_p), + a.di(dis.a, 1), + a.li(lis[1]), + a.li(lis[2]), + }) + eq(2, dict_inner_p.dv_refcount) + lib.clear_tv(list_tv) + alloc_log:check({ + a.freed(dis.a), + a.freed(dict_inner_p), + a.freed(lis[1]), + a.freed(lis[2]), + a.freed(list_p), + }) + end) +end) diff --git a/test/unit/fixtures/multiqueue.c b/test/unit/fixtures/multiqueue.c new file mode 100644 index 0000000000..da63e55919 --- /dev/null +++ b/test/unit/fixtures/multiqueue.c @@ -0,0 +1,16 @@ +#include <string.h> +#include <stdlib.h> +#include "nvim/event/multiqueue.h" +#include "multiqueue.h" + + +void ut_multiqueue_put(MultiQueue *this, const char *str) +{ + multiqueue_put(this, NULL, 1, str); +} + +const char *ut_multiqueue_get(MultiQueue *this) +{ + Event event = multiqueue_get(this); + return event.argv[0]; +} diff --git a/test/unit/fixtures/multiqueue.h b/test/unit/fixtures/multiqueue.h new file mode 100644 index 0000000000..78a3a89063 --- /dev/null +++ b/test/unit/fixtures/multiqueue.h @@ -0,0 +1,4 @@ +#include "nvim/event/multiqueue.h" + +void ut_multiqueue_put(MultiQueue *queue, const char *str); +const char *ut_multiqueue_get(MultiQueue *queue); diff --git a/test/unit/fixtures/queue.c b/test/unit/fixtures/queue.c deleted file mode 100644 index bbb6274b21..0000000000 --- a/test/unit/fixtures/queue.c +++ /dev/null @@ -1,16 +0,0 @@ -#include <string.h> -#include <stdlib.h> -#include "nvim/event/queue.h" -#include "queue.h" - - -void ut_queue_put(Queue *queue, const char *str) -{ - queue_put(queue, NULL, 1, str); -} - -const char *ut_queue_get(Queue *queue) -{ - Event event = queue_get(queue); - return event.argv[0]; -} diff --git a/test/unit/fixtures/queue.h b/test/unit/fixtures/queue.h deleted file mode 100644 index ae949c9f29..0000000000 --- a/test/unit/fixtures/queue.h +++ /dev/null @@ -1,4 +0,0 @@ -#include "nvim/event/queue.h" - -void ut_queue_put(Queue *queue, const char *str); -const char *ut_queue_get(Queue *queue); diff --git a/test/unit/formatc.lua b/test/unit/formatc.lua index 00637e0b8d..e288081960 100644 --- a/test/unit/formatc.lua +++ b/test/unit/formatc.lua @@ -219,13 +219,7 @@ local function standalone(...) -- luacheck: ignore Preprocess.add_to_include_path('./../../build/include') Preprocess.add_to_include_path('./../../.deps/usr/include') - local input = Preprocess.preprocess_stream(arg[1]) - local raw = input:read('*all') - input:close() - - if raw == nil then - print("ERROR: Preprocess.preprocess_stream():read() returned empty") - end + local raw = Preprocess.preprocess('', arg[1]) local formatted if #arg == 2 and arg[2] == 'no' then diff --git a/test/unit/garray_spec.lua b/test/unit/garray_spec.lua index 9694e3c427..422ef7b36a 100644 --- a/test/unit/garray_spec.lua +++ b/test/unit/garray_spec.lua @@ -198,8 +198,8 @@ describe('garray', function() local function new_and_grow(itemsize_, growsize_, req) local garr = new_garray() ga_init(garr, itemsize_, growsize_) - eq(0, ga_size(garr)) -- should be 0 at first - eq(NULL, ga_data(garr)) -- should be NULL + eq(0, ga_size(garr)) -- should be 0 at first + eq(NULL, ga_data(garr)) -- should be NULL ga_grow(garr, req) -- add space for `req` items return garr end @@ -209,7 +209,7 @@ describe('garray', function() growsize = 4 local grow_by = growsize - 1 local garr = new_and_grow(itemsize, growsize, grow_by) - neq(NULL, ga_data(garr)) -- data should be a ptr to memory + neq(NULL, ga_data(garr)) -- data should be a ptr to memory eq(growsize, ga_maxlen(garr)) -- we requested LESS than growsize, so... end) @@ -218,7 +218,7 @@ describe('garray', function() growsize = 4 local grow_by = growsize + 1 local garr = new_and_grow(itemsize, growsize, grow_by) - neq(NULL, ga_data(garr)) -- data should be a ptr to memory + neq(NULL, ga_data(garr)) -- data should be a ptr to memory eq(grow_by, ga_maxlen(garr)) -- we requested MORE than growsize, so... end) diff --git a/test/unit/helpers.lua b/test/unit/helpers.lua index 426ae2d9e0..1bfdd32739 100644 --- a/test/unit/helpers.lua +++ b/test/unit/helpers.lua @@ -1,9 +1,19 @@ -local assert = require('luassert') local ffi = require('ffi') local formatc = require('test.unit.formatc') local Set = require('test.unit.set') local Preprocess = require('test.unit.preprocess') local Paths = require('test.config.paths') +local global_helpers = require('test.helpers') + +local neq = global_helpers.neq +local eq = global_helpers.eq +local ok = global_helpers.ok + +-- C constants. +local NULL = ffi.cast('void*', 0) + +local OK = 1 +local FAIL = 0 -- add some standard header locations for _, p in ipairs(Paths.include_paths) do @@ -31,7 +41,9 @@ 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, "UUID_NULL") -- static const uuid_t UUID_NULL = {...} + or string.find(line, "inline _Bool")) then result[#result + 1] = line end end @@ -39,6 +51,8 @@ local function filter_complex_blocks(body) return table.concat(result, "\n") end +local previous_defines = '' + -- use this helper to import C files, you can pass multiple paths at once, -- this helper will return the C namespace of the nvim library. local function cimport(...) @@ -60,17 +74,8 @@ local function cimport(...) return libnvim end - local body = nil - for _ = 1, 10 do - local stream = Preprocess.preprocess_stream(unpack(paths)) - body = stream:read("*a") - stream:close() - if body ~= nil then break end - end - - if body == nil then - print("ERROR: helpers.lua: Preprocess.preprocess_stream():read() returned empty") - end + local body + body, previous_defines = Preprocess.preprocess(previous_defines, unpack(paths)) -- format it (so that the lines are "unique" statements), also filter out -- Objective-C blocks @@ -119,6 +124,67 @@ local function cppimport(path) return cimport(Paths.test_include_path .. '/' .. path) end +local function alloc_log_new() + local log = { + log={}, + lib=cimport('./src/nvim/memory.h'), + original_functions={}, + null={['\0:is_null']=true}, + } + local allocator_functions = {'malloc', 'free', 'calloc', 'realloc'} + function log:save_original_functions() + for _, funcname in ipairs(allocator_functions) do + self.original_functions[funcname] = self.lib['mem_' .. funcname] + end + end + function log:set_mocks() + for _, k in ipairs(allocator_functions) do + do + local kk = k + self.lib['mem_' .. k] = function(...) + local log_entry = {func=kk, args={...}} + self.log[#self.log + 1] = log_entry + if kk == 'free' then + self.original_functions[kk](...) + else + log_entry.ret = self.original_functions[kk](...) + end + for i, v in ipairs(log_entry.args) do + if v == nil then + -- XXX This thing thinks that {NULL} ~= {NULL}. + log_entry.args[i] = self.null + end + end + if self.hook then self:hook(log_entry) end + if log_entry.ret then + return log_entry.ret + end + end + end + end + end + function log:clear() + self.log = {} + end + function log:check(exp) + eq(exp, self.log) + self:clear() + end + function log:restore_original_functions() + for k, v in pairs(self.original_functions) do + self.lib['mem_' .. k] = v + end + end + function log:before_each() + log:save_original_functions() + log:set_mocks() + end + function log:after_each() + log:restore_original_functions() + end + return log +end + cimport('./src/nvim/types.h') -- take a pointer to a C-allocated string and return an interned @@ -130,7 +196,7 @@ end local cstr = ffi.typeof('char[?]') local function to_cstr(string) - return cstr((string.len(string)) + 1, string) + return cstr(#string + 1, string) end -- initialize some global variables, this is still necessary to unit test @@ -143,27 +209,19 @@ do main.event_init() end --- C constants. -local NULL = ffi.cast('void*', 0) - -local OK = 1 -local FAIL = 0 - return { cimport = cimport, cppimport = cppimport, internalize = internalize, - eq = function(expected, actual) - return assert.are.same(expected, actual) - end, - neq = function(expected, actual) - return assert.are_not.same(expected, actual) - end, + ok = ok, + eq = eq, + neq = neq, ffi = ffi, lib = libnvim, cstr = cstr, to_cstr = to_cstr, NULL = NULL, OK = OK, - FAIL = FAIL + FAIL = FAIL, + alloc_log_new = alloc_log_new, } diff --git a/test/unit/memory_spec.lua b/test/unit/memory_spec.lua new file mode 100644 index 0000000000..73a32724ef --- /dev/null +++ b/test/unit/memory_spec.lua @@ -0,0 +1,51 @@ +local helpers = require("test.unit.helpers") + +local cimport = helpers.cimport +local cstr = helpers.cstr +local eq = helpers.eq +local ffi = helpers.ffi +local to_cstr = helpers.to_cstr + +local cimp = cimport('stdlib.h', './src/nvim/memory.h') + +describe('xstrlcat()', function() + local function test_xstrlcat(dst, src, dsize) + assert.is_true(dsize >= 1 + string.len(dst)) -- sanity check for tests + local dst_cstr = cstr(dsize, dst) + local src_cstr = to_cstr(src) + eq(string.len(dst .. src), cimp.xstrlcat(dst_cstr, src_cstr, dsize)) + return ffi.string(dst_cstr) + end + + local function test_xstrlcat_overlap(dst, src_idx, dsize) + assert.is_true(dsize >= 1 + string.len(dst)) -- sanity check for tests + local dst_cstr = cstr(dsize, dst) + local src_cstr = dst_cstr + src_idx -- pointer into `dst` (overlaps) + eq(string.len(dst) + string.len(dst) - src_idx, + cimp.xstrlcat(dst_cstr, src_cstr, dsize)) + return ffi.string(dst_cstr) + end + + it('concatenates strings', function() + eq('ab', test_xstrlcat('a', 'b', 3)) + eq('ab', test_xstrlcat('a', 'b', 4096)) + eq('ABCיהZdefgiיהZ', test_xstrlcat('ABCיהZ', 'defgiיהZ', 4096)) + eq('b', test_xstrlcat('', 'b', 4096)) + eq('a', test_xstrlcat('a', '', 4096)) + end) + + it('concatenates overlapping strings', function() + eq('abcabc', test_xstrlcat_overlap('abc', 0, 7)) + eq('abca', test_xstrlcat_overlap('abc', 0, 5)) + eq('abcb', test_xstrlcat_overlap('abc', 1, 5)) + eq('abcc', test_xstrlcat_overlap('abc', 2, 10)) + eq('abcabc', test_xstrlcat_overlap('abc', 0, 2343)) + end) + + it('truncates if `dsize` is too small', function() + eq('a', test_xstrlcat('a', 'b', 2)) + eq('', test_xstrlcat('', 'b', 1)) + eq('ABCיהZd', test_xstrlcat('ABCיהZ', 'defgiיהZ', 10)) + end) + +end) diff --git a/test/unit/queue_spec.lua b/test/unit/multiqueue_spec.lua index 9326c1cad6..c7f8dd8328 100644 --- a/test/unit/queue_spec.lua +++ b/test/unit/multiqueue_spec.lua @@ -3,28 +3,28 @@ local helpers = require("test.unit.helpers") local ffi = helpers.ffi local eq = helpers.eq -local queue = helpers.cimport("./test/unit/fixtures/queue.h") +local multiqueue = helpers.cimport("./test/unit/fixtures/multiqueue.h") -describe('queue', function() +describe("multiqueue (multi-level event-queue)", function() local parent, child1, child2, child3 local function put(q, str) - queue.ut_queue_put(q, str) + multiqueue.ut_multiqueue_put(q, str) end local function get(q) - return ffi.string(queue.ut_queue_get(q)) + return ffi.string(multiqueue.ut_multiqueue_get(q)) end local function free(q) - queue.queue_free(q) + multiqueue.multiqueue_free(q) end before_each(function() - parent = queue.queue_new_parent(ffi.NULL, ffi.NULL) - child1 = queue.queue_new_child(parent) - child2 = queue.queue_new_child(parent) - child3 = queue.queue_new_child(parent) + parent = multiqueue.multiqueue_new_parent(ffi.NULL, ffi.NULL) + child1 = multiqueue.multiqueue_new_child(parent) + child2 = multiqueue.multiqueue_new_child(parent) + child3 = multiqueue.multiqueue_new_child(parent) put(child1, 'c1i1') put(child1, 'c1i2') put(child2, 'c2i1') @@ -36,6 +36,27 @@ describe('queue', function() put(child3, 'c3i2') end) + it('keeps count of added events', function() + eq(3, multiqueue.multiqueue_size(child1)) + eq(4, multiqueue.multiqueue_size(child2)) + eq(2, multiqueue.multiqueue_size(child3)) + end) + + it('keeps count of removed events', function() + multiqueue.multiqueue_get(child1) + eq(2, multiqueue.multiqueue_size(child1)) + multiqueue.multiqueue_get(child1) + eq(1, multiqueue.multiqueue_size(child1)) + multiqueue.multiqueue_get(child1) + eq(0, multiqueue.multiqueue_size(child1)) + put(child1, 'c2ixx') + eq(1, multiqueue.multiqueue_size(child1)) + multiqueue.multiqueue_get(child1) + eq(0, multiqueue.multiqueue_size(child1)) + multiqueue.multiqueue_get(child1) + eq(0, multiqueue.multiqueue_size(child1)) + end) + it('removing from parent removes from child', function() eq('c1i1', get(parent)) eq('c1i2', get(parent)) diff --git a/test/unit/option_spec.lua b/test/unit/option_spec.lua new file mode 100644 index 0000000000..8bab0194a2 --- /dev/null +++ b/test/unit/option_spec.lua @@ -0,0 +1,51 @@ +local helpers = require("test.unit.helpers") + +local to_cstr = helpers.to_cstr +local eq = helpers.eq + +local option = helpers.cimport("./src/nvim/option.h") +local globals = helpers.cimport("./src/nvim/globals.h") + +local check_ff_value = function(ff) + return option.check_ff_value(to_cstr(ff)) +end + +describe('check_ff_value', function() + + it('views empty string as valid', function() + eq(1, check_ff_value("")) + end) + + it('views "unix", "dos" and "mac" as valid', function() + eq(1, check_ff_value("unix")) + eq(1, check_ff_value("dos")) + eq(1, check_ff_value("mac")) + end) + + it('views "foo" as invalid', function() + eq(0, check_ff_value("foo")) + end) +end) + +describe('get_sts_value', function() + it([[returns 'softtabstop' when it is non-negative]], function() + globals.curbuf.b_p_sts = 5 + eq(5, option.get_sts_value()) + + globals.curbuf.b_p_sts = 0 + eq(0, option.get_sts_value()) + end) + + it([[returns "effective shiftwidth" when 'softtabstop' is negative]], function() + local shiftwidth = 2 + globals.curbuf.b_p_sw = shiftwidth + local tabstop = 5 + globals.curbuf.b_p_ts = tabstop + globals.curbuf.b_p_sts = -2 + eq(shiftwidth, option.get_sts_value()) + + shiftwidth = 0 + globals.curbuf.b_p_sw = shiftwidth + eq(tabstop, option.get_sts_value()) + end) +end) diff --git a/test/unit/os/env_spec.lua b/test/unit/os/env_spec.lua index 9e00a3e8f8..64bbaaa8c2 100644 --- a/test/unit/os/env_spec.lua +++ b/test/unit/os/env_spec.lua @@ -10,19 +10,19 @@ local NULL = helpers.NULL require('lfs') -local env = cimport('./src/nvim/os/os.h') +local cimp = cimport('./src/nvim/os/os.h') describe('env function', function() local function os_setenv(name, value, override) - return env.os_setenv((to_cstr(name)), (to_cstr(value)), override) + return cimp.os_setenv((to_cstr(name)), (to_cstr(value)), override) end local function os_unsetenv(name, _, _) - return env.os_unsetenv((to_cstr(name))) + return cimp.os_unsetenv((to_cstr(name))) end local function os_getenv(name) - local rval = env.os_getenv((to_cstr(name))) + local rval = cimp.os_getenv((to_cstr(name))) if rval ~= NULL then return ffi.string(rval) else @@ -88,14 +88,14 @@ describe('env function', function() local i = 0 local names = { } local found_name = false - local name = env.os_getenvname_at_index(i) + local name = cimp.os_getenvname_at_index(i) while name ~= NULL do table.insert(names, ffi.string(name)) if (ffi.string(name)) == test_name then found_name = true end i = i + 1 - name = env.os_getenvname_at_index(i) + name = cimp.os_getenvname_at_index(i) end eq(true, (table.getn(names)) > 0) eq(true, found_name) @@ -104,15 +104,15 @@ describe('env function', function() it('returns NULL if the index is out of bounds', function() local huge = ffi.new('size_t', 10000) local maxuint32 = ffi.new('size_t', 4294967295) - eq(NULL, env.os_getenvname_at_index(huge)) - eq(NULL, env.os_getenvname_at_index(maxuint32)) + eq(NULL, cimp.os_getenvname_at_index(huge)) + eq(NULL, cimp.os_getenvname_at_index(maxuint32)) if ffi.abi('64bit') then -- couldn't use a bigger number because it gets converted to -- double somewere, should be big enough anyway -- maxuint64 = ffi.new 'size_t', 18446744073709551615 local maxuint64 = ffi.new('size_t', 18446744073709000000) - eq(NULL, env.os_getenvname_at_index(maxuint64)) + eq(NULL, cimp.os_getenvname_at_index(maxuint64)) end end) end) @@ -124,10 +124,10 @@ describe('env function', function() local stat_str = stat_file:read('*l') stat_file:close() local pid = tonumber((stat_str:match('%d+'))) - eq(pid, tonumber(env.os_get_pid())) + eq(pid, tonumber(cimp.os_get_pid())) else -- /proc is not available on all systems, test if pid is nonzero. - eq(true, (env.os_get_pid() > 0)) + eq(true, (cimp.os_get_pid() > 0)) end end) end) @@ -138,7 +138,7 @@ describe('env function', function() local hostname = handle:read('*l') handle:close() local hostname_buf = cstr(255, '') - env.os_get_hostname(hostname_buf, 255) + cimp.os_get_hostname(hostname_buf, 255) eq(hostname, (ffi.string(hostname_buf))) end) end) @@ -155,39 +155,52 @@ describe('env function', function() local output_buff1 = cstr(255, '') local output_buff2 = cstr(255, '') local output_expected = 'NEOVIM_UNIT_TEST_EXPAND_ENV_ESCV/test' - env.expand_env_esc(input1, output_buff1, 255, false, true, NULL) - env.expand_env_esc(input2, output_buff2, 255, false, true, NULL) + cimp.expand_env_esc(input1, output_buff1, 255, false, true, NULL) + cimp.expand_env_esc(input2, output_buff2, 255, false, true, NULL) eq(output_expected, ffi.string(output_buff1)) eq(output_expected, ffi.string(output_buff2)) end) - it('expands ~ once when one is true', function() + it('expands ~ once when `one` is true', function() local input = '~/foo ~ foo' local homedir = cstr(255, '') - env.expand_env_esc(to_cstr('~'), homedir, 255, false, true, NULL) + cimp.expand_env_esc(to_cstr('~'), homedir, 255, false, true, NULL) local output_expected = ffi.string(homedir) .. "/foo ~ foo" local output = cstr(255, '') - env.expand_env_esc(to_cstr(input), output, 255, false, true, NULL) + cimp.expand_env_esc(to_cstr(input), output, 255, false, true, NULL) eq(ffi.string(output), ffi.string(output_expected)) end) - it('expands ~ every time when one is false', function() + it('expands ~ every time when `one` is false', function() local input = to_cstr('~/foo ~ foo') - local homedir = cstr(255, '') - env.expand_env_esc(to_cstr('~'), homedir, 255, false, true, NULL) - homedir = ffi.string(homedir) + local dst = cstr(255, '') + cimp.expand_env_esc(to_cstr('~'), dst, 255, false, true, NULL) + local homedir = ffi.string(dst) local output_expected = homedir .. "/foo " .. homedir .. " foo" local output = cstr(255, '') - env.expand_env_esc(input, output, 255, false, false, NULL) + cimp.expand_env_esc(input, output, 255, false, false, NULL) eq(output_expected, ffi.string(output)) end) - it('respects the dstlen parameter without expansion', function() + it('does not crash #3725', function() + local name_out = ffi.new('char[100]') + cimp.os_get_user_name(name_out, 100) + local curuser = ffi.string(name_out) + + local src = to_cstr("~"..curuser.."/Vcs/django-rest-framework/rest_framework/renderers.py") + local dst = cstr(256, "~"..curuser) + cimp.expand_env_esc(src, dst, 1024, false, false, NULL) + local len = string.len(ffi.string(dst)) + assert.True(len > 56) + assert.True(len < 99) + end) + + it('respects `dstlen` without expansion', function() local input = to_cstr('this is a very long thing that will not fit') -- The buffer is long enough to actually contain the full input in case the -- test fails, but we don't tell expand_env_esc that local output = cstr(255, '') - env.expand_env_esc(input, output, 5, false, true, NULL) + cimp.expand_env_esc(input, output, 5, false, true, NULL) -- Make sure the first few characters are copied properly and that there is a -- terminating null character for i=0,3 do @@ -196,17 +209,17 @@ describe('env function', function() eq(0, output[4]) end) - it('respects the dstlen parameter with expansion', function() + it('respects `dstlen` with expansion', function() local varname = to_cstr('NVIM_UNIT_TEST_EXPAND_ENV_ESC_DSTLENN') local varval = to_cstr('NVIM_UNIT_TEST_EXPAND_ENV_ESC_DSTLENV') - env.os_setenv(varname, varval, 1) + cimp.os_setenv(varname, varval, 1) -- TODO(bobtwinkles) This test uses unix-specific environment variable accessing, -- should have some alternative for windows local input = to_cstr('$NVIM_UNIT_TEST_EXPAND_ENV_ESC_DSTLENN/even more stuff') -- The buffer is long enough to actually contain the full input in case the -- test fails, but we don't tell expand_env_esc that local output = cstr(255, '') - env.expand_env_esc(input, output, 5, false, true, NULL) + cimp.expand_env_esc(input, output, 5, false, true, NULL) -- Make sure the first few characters are copied properly and that there is a -- terminating null character -- expand_env_esc SHOULD NOT expand the variable if there is not enough space to diff --git a/test/unit/os/fileio_spec.lua b/test/unit/os/fileio_spec.lua new file mode 100644 index 0000000000..5358022422 --- /dev/null +++ b/test/unit/os/fileio_spec.lua @@ -0,0 +1,365 @@ +local lfs = require('lfs') + +local helpers = require('test.unit.helpers') + +local eq = helpers.eq +local ffi = helpers.ffi +local cimport = helpers.cimport + +local m = cimport('./src/nvim/os/fileio.h') + +local fcontents = '' +for i = 0, 255 do + fcontents = fcontents .. (i == 0 and '\0' or ('%c'):format(i)) +end +fcontents = fcontents: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 + -- For some reason if length of NUL-bytes-string is the same as `char[?]` + -- size luajit garbage collector crashes. But it does not do so in + -- os_read[v] tests in os/fs_spec.lua. + buf = ffi.new('char[?]', size + 1, ('\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, _ = file_open(file1, m.kFileCreateOnly, 384) + eq(m.UV_EEXIST, err) + end) + + it('fails to open an symlink with kFileNoSymlink', function() + local err, _ = file_open(linkf, m.kFileNoSymlink, 384) + -- err is UV_EMLINK in FreeBSD, but if I use `ok(err == m.UV_ELOOP or err == + -- m.UV_EMLINK)`, then I loose the ability to see actual `err` value. + if err ~= m.UV_ELOOP then eq(m.UV_EMLINK, err) end + 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, _ = 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, _ = 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, _ = 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, _ = 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 wr = file_write(fp, fcontents) + eq(#fcontents, wr) + eq(0, m.file_close(fp)) + eq(wr, 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 wr = file_write(fp, s) + eq(wr, #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 wr = file_write(fp, s) + eq(wr, #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 rd, s = file_read(fp, 3) + eq(3, rd) + eq(fcontents:sub(4, 6), s) + eq(0, m.file_close(fp)) + end) +end) diff --git a/test/unit/os/fs_spec.lua b/test/unit/os/fs_spec.lua index 857a5001f1..5d889d6e33 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 @@ -27,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 @@ -68,6 +75,8 @@ describe('fs function', function() io.open('unit-test-directory/test_2.file', 'w').close() lfs.link('test.file', 'unit-test-directory/test_link.file', true) + + lfs.link('non_existing_file.file', 'unit-test-directory/test_broken_link.file', true) -- Since the tests are executed, they are called by an executable. We use -- that executable for several asserts. absolute_executable = arg[0] @@ -81,6 +90,7 @@ describe('fs function', function() os.remove('unit-test-directory/test_2.file') os.remove('unit-test-directory/test_link.file') os.remove('unit-test-directory/test_hlink.file') + os.remove('unit-test-directory/test_broken_link.file') lfs.rmdir('unit-test-directory') end) @@ -150,11 +160,11 @@ describe('fs function', function() local function os_can_exe(name) local buf = ffi.new('char *[1]') buf[0] = NULL - local ok = fs.os_can_exe(to_cstr(name), buf, true) + local ce_ret = fs.os_can_exe(to_cstr(name), buf, true) -- When os_can_exe returns true, it must set the path. -- When it returns false, the path must be NULL. - if ok then + if ce_ret then neq(NULL, buf[0]) return internalize(buf[0]) else @@ -196,8 +206,8 @@ describe('fs function', function() lfs.chdir(directory) - -- Rely on currentdir to resolve symlinks, if any. Testing against - -- the absolute path taken from arg[0] may result in failure where + -- Rely on currentdir to resolve symlinks, if any. Testing against + -- the absolute path taken from arg[0] may result in failure where -- the path has a symlink in it. local canonical = lfs.currentdir() .. '/' .. executable_name local expected = exe(canonical) @@ -356,8 +366,8 @@ describe('fs function', function() end) describe('file operations', function() - local function os_file_exists(filename) - return fs.os_file_exists((to_cstr(filename))) + local function os_path_exists(filename) + return fs.os_path_exists((to_cstr(filename))) end local function os_rename(path, new_path) return fs.os_rename((to_cstr(path)), (to_cstr(new_path))) @@ -368,14 +378,67 @@ describe('fs function', function() local function os_open(path, flags, mode) return fs.os_open((to_cstr(path)), flags, mode) end + local function os_close(fd) + return fs.os_close(fd) + end + -- For some reason if length of NUL-bytes-string is the same as `char[?]` + -- size luajit crashes. Though it does not do so in this test suite, better + -- be cautios and allocate more elements then needed. I only did this to + -- strings. + local function os_read(fd, size) + local buf = nil + if size == nil then + size = 0 + else + buf = ffi.new('char[?]', size + 1, ('\0'):rep(size)) + end + local eof = ffi.new('bool[?]', 1, {true}) + local ret2 = fs.os_read(fd, eof, buf, size) + local ret1 = eof[0] + local ret3 = '' + if buf ~= nil then + ret3 = ffi.string(buf, size) + end + return ret1, ret2, ret3 + end + local function os_readv(fd, sizes) + local bufs = {} + for i, size in ipairs(sizes) do + bufs[i] = { + iov_base=ffi.new('char[?]', size + 1, ('\0'):rep(size)), + iov_len=size, + } + end + local iov = ffi.new('struct iovec[?]', #sizes, bufs) + local eof = ffi.new('bool[?]', 1, {true}) + local ret2 = fs.os_readv(fd, eof, iov, #sizes) + local ret1 = eof[0] + local ret3 = {} + for i = 1,#sizes do + -- Warning: iov may not be used. + ret3[i] = ffi.string(bufs[i].iov_base, bufs[i].iov_len) + end + return ret1, ret2, ret3 + end + local function os_write(fd, data) + return fs.os_write(fd, data, data and #data or 0) + end - describe('os_file_exists', function() + describe('os_path_exists', function() it('returns false when given a non-existing file', function() - eq(false, (os_file_exists('non-existing-file'))) + eq(false, (os_path_exists('non-existing-file'))) end) it('returns true when given an existing file', function() - eq(true, (os_file_exists('unit-test-directory/test.file'))) + eq(true, (os_path_exists('unit-test-directory/test.file'))) + end) + + it('returns false when given a broken symlink', function() + eq(false, (os_path_exists('unit-test-directory/test_broken_link.file'))) + end) + + it('returns true when given a directory', function() + eq(true, (os_path_exists('unit-test-directory'))) end) end) @@ -385,8 +448,8 @@ describe('fs function', function() it('can rename file if destination file does not exist', function() eq(OK, (os_rename(test, not_exist))) - eq(false, (os_file_exists(test))) - eq(true, (os_file_exists(not_exist))) + eq(false, (os_path_exists(test))) + eq(true, (os_path_exists(not_exist))) eq(OK, (os_rename(not_exist, test))) -- restore test file end) @@ -402,8 +465,8 @@ describe('fs function', function() file:close() eq(OK, (os_rename(other, test))) - eq(false, (os_file_exists(other))) - eq(true, (os_file_exists(test))) + eq(false, (os_path_exists(other))) + eq(true, (os_path_exists(test))) file = io.open(test, 'r') eq('other', (file:read('*all'))) file:close() @@ -432,30 +495,34 @@ describe('fs function', function() end) describe('os_open', function() + local new_file = 'test_new_file' + local existing_file = 'unit-test-directory/test_existing.file' + before_each(function() - io.open('unit-test-directory/test_existing.file', 'w').close() + (io.open(existing_file, 'w')):close() end) after_each(function() - os.remove('unit-test-directory/test_existing.file') - os.remove('test_new_file') + os.remove(existing_file) + os.remove(new_file) end) - local new_file = 'test_new_file' - local existing_file = 'unit-test-directory/test_existing.file' - it('returns UV_ENOENT for O_RDWR on a non-existing file', function() eq(ffi.C.UV_ENOENT, (os_open('non-existing-file', ffi.C.kO_RDWR, 0))) end) - it('returns non-negative for O_CREAT on a non-existing file', function() + it('returns non-negative for O_CREAT on a non-existing file which then can be closed', function() assert_file_does_not_exist(new_file) - assert.is_true(0 <= (os_open(new_file, ffi.C.kO_CREAT, 0))) + local fd = os_open(new_file, ffi.C.kO_CREAT, 0) + assert.is_true(0 <= fd) + eq(0, os_close(fd)) end) - it('returns non-negative for O_CREAT on a existing file', function() + it('returns non-negative for O_CREAT on a existing file which then can be closed', function() assert_file_exists(existing_file) - assert.is_true(0 <= (os_open(existing_file, ffi.C.kO_CREAT, 0))) + local fd = os_open(existing_file, ffi.C.kO_CREAT, 0) + assert.is_true(0 <= fd) + eq(0, os_close(fd)) end) it('returns UV_EEXIST for O_CREAT|O_EXCL on a existing file', function() @@ -463,24 +530,181 @@ describe('fs function', function() eq(ffi.C.kUV_EEXIST, (os_open(existing_file, (bit.bor(ffi.C.kO_CREAT, ffi.C.kO_EXCL)), 0))) end) - it('sets `rwx` permissions for O_CREAT 700', function() + it('sets `rwx` permissions for O_CREAT 700 which then can be closed', function() assert_file_does_not_exist(new_file) --create the file - os_open(new_file, ffi.C.kO_CREAT, tonumber("700", 8)) + local fd = os_open(new_file, ffi.C.kO_CREAT, tonumber("700", 8)) --verify permissions eq('rwx------', lfs.attributes(new_file)['permissions']) + eq(0, os_close(fd)) end) - it('sets `rw` permissions for O_CREAT 600', function() + it('sets `rw` permissions for O_CREAT 600 which then can be closed', function() assert_file_does_not_exist(new_file) --create the file - os_open(new_file, ffi.C.kO_CREAT, tonumber("600", 8)) + local fd = os_open(new_file, ffi.C.kO_CREAT, tonumber("600", 8)) --verify permissions eq('rw-------', lfs.attributes(new_file)['permissions']) + eq(0, os_close(fd)) + end) + + it('returns a non-negative file descriptor for an existing file which then can be closed', function() + local fd = os_open(existing_file, ffi.C.kO_RDWR, 0) + assert.is_true(0 <= fd) + eq(0, os_close(fd)) + end) + end) + + describe('os_close', function() + it('returns EBADF for negative file descriptors', function() + eq(ffi.C.UV_EBADF, os_close(-1)) + eq(ffi.C.UV_EBADF, os_close(-1000)) + end) + end) + + describe('os_read', function() + local file = 'test-unit-os-fs_spec-os_read.dat' + + before_each(function() + local f = io.open(file, 'w') + f:write(fcontents) + f:close() + end) + + after_each(function() + os.remove(file) + end) + + it('can read zero bytes from a file', function() + local fd = os_open(file, ffi.C.kO_RDONLY, 0) + ok(fd >= 0) + eq({false, 0, ''}, {os_read(fd, nil)}) + eq({false, 0, ''}, {os_read(fd, 0)}) + eq(0, os_close(fd)) + end) + + it('can read from a file multiple times', function() + local fd = os_open(file, ffi.C.kO_RDONLY, 0) + ok(fd >= 0) + eq({false, 2, '\000\001'}, {os_read(fd, 2)}) + eq({false, 2, '\002\003'}, {os_read(fd, 2)}) + eq(0, os_close(fd)) + end) + + it('can read the whole file at once and then report eof', function() + local fd = os_open(file, ffi.C.kO_RDONLY, 0) + ok(fd >= 0) + eq({false, #fcontents, fcontents}, {os_read(fd, #fcontents)}) + eq({true, 0, ('\0'):rep(#fcontents)}, {os_read(fd, #fcontents)}) + eq(0, os_close(fd)) + end) + + it('can read the whole file in two calls, one partially', function() + local fd = os_open(file, ffi.C.kO_RDONLY, 0) + ok(fd >= 0) + eq({false, #fcontents * 3/4, fcontents:sub(1, #fcontents * 3/4)}, + {os_read(fd, #fcontents * 3/4)}) + eq({true, + (#fcontents * 1/4), + fcontents:sub(#fcontents * 3/4 + 1) .. ('\0'):rep(#fcontents * 2/4)}, + {os_read(fd, #fcontents * 3/4)}) + eq(0, os_close(fd)) + end) + end) + + describe('os_readv', function() + -- Function may be absent + if not pcall(function() return fs.os_readv end) then + return + end + local file = 'test-unit-os-fs_spec-os_readv.dat' + + before_each(function() + local f = io.open(file, 'w') + f:write(fcontents) + f:close() + end) + + after_each(function() + os.remove(file) + end) + + it('can read zero bytes from a file', function() + local fd = os_open(file, ffi.C.kO_RDONLY, 0) + ok(fd >= 0) + eq({false, 0, {}}, {os_readv(fd, {})}) + eq({false, 0, {'', '', ''}}, {os_readv(fd, {0, 0, 0})}) + eq(0, os_close(fd)) + end) + + it('can read from a file multiple times to a differently-sized buffers', function() + local fd = os_open(file, ffi.C.kO_RDONLY, 0) + ok(fd >= 0) + eq({false, 2, {'\000\001'}}, {os_readv(fd, {2})}) + eq({false, 5, {'\002\003', '\004\005\006'}}, {os_readv(fd, {2, 3})}) + eq(0, os_close(fd)) + end) + + it('can read the whole file at once and then report eof', function() + local fd = os_open(file, ffi.C.kO_RDONLY, 0) + ok(fd >= 0) + eq({false, + #fcontents, + {fcontents:sub(1, #fcontents * 1/4), + fcontents:sub(#fcontents * 1/4 + 1, #fcontents * 3/4), + fcontents:sub(#fcontents * 3/4 + 1, #fcontents * 15/16), + fcontents:sub(#fcontents * 15/16 + 1, #fcontents)}}, + {os_readv(fd, {#fcontents * 1/4, + #fcontents * 2/4, + #fcontents * 3/16, + #fcontents * 1/16})}) + eq({true, 0, {'\0'}}, {os_readv(fd, {1})}) + eq(0, os_close(fd)) + end) + + it('can read the whole file in two calls, one partially', function() + local fd = os_open(file, ffi.C.kO_RDONLY, 0) + ok(fd >= 0) + eq({false, #fcontents * 3/4, {fcontents:sub(1, #fcontents * 3/4)}}, + {os_readv(fd, {#fcontents * 3/4})}) + eq({true, + (#fcontents * 1/4), + {fcontents:sub(#fcontents * 3/4 + 1) .. ('\0'):rep(#fcontents * 2/4)}}, + {os_readv(fd, {#fcontents * 3/4})}) + eq(0, os_close(fd)) + end) + end) + + describe('os_write', function() + -- Function may be absent + local file = 'test-unit-os-fs_spec-os_write.dat' + + before_each(function() + local f = io.open(file, 'w') + f:write(fcontents) + f:close() + end) + + after_each(function() + os.remove(file) + end) + + it('can write zero bytes to a file', function() + local fd = os_open(file, ffi.C.kO_WRONLY, 0) + ok(fd >= 0) + eq(0, os_write(fd, '')) + eq(0, os_write(fd, nil)) + eq(fcontents, io.open(file, 'r'):read('*a')) + eq(0, os_close(fd)) end) - it('returns a non-negative file descriptor for an existing file', function() - assert.is_true(0 <= (os_open(existing_file, ffi.C.kO_RDWR, 0))) + it('can write some data to a file', function() + local fd = os_open(file, ffi.C.kO_WRONLY, 0) + ok(fd >= 0) + eq(3, os_write(fd, 'abc')) + eq(4, os_write(fd, ' def')) + eq('abc def' .. fcontents:sub(8), io.open(file, 'r'):read('*a')) + eq(0, os_close(fd)) end) end) diff --git a/test/unit/os/shell_spec.lua b/test/unit/os/shell_spec.lua index 93103e4e8c..3603403daf 100644 --- a/test/unit/os/shell_spec.lua +++ b/test/unit/os/shell_spec.lua @@ -1,15 +1,3 @@ --- not all operating systems support the system()-tests, as of yet. -local allowed_os = { - Linux = true, - OSX = true, - BSD = true, - POSIX = true -} - -if allowed_os[jit.os] ~= true then - return -end - local helpers = require('test.unit.helpers') local cimported = helpers.cimport( './src/nvim/os/shell.h', @@ -24,14 +12,12 @@ local to_cstr = helpers.to_cstr local NULL = ffi.cast('void *', 0) describe('shell functions', function() - setup(function() + before_each(function() -- os_system() can't work when the p_sh and p_shcf variables are unset cimported.p_sh = to_cstr('/bin/bash') cimported.p_shcf = to_cstr('-c') - end) - - teardown(function() - cimported.event_teardown() + cimported.p_sxq = to_cstr('') + cimported.p_sxe = to_cstr('') end) local function shell_build_argv(cmd, extra_args) @@ -130,5 +116,50 @@ describe('shell functions', function() '-c', 'abc def'}, shell_build_argv('abc def', 'ghi jkl')) end) + + it('applies shellxescape (p_sxe) and shellxquote (p_sxq)', function() + cimported.p_sxq = to_cstr('(') + cimported.p_sxe = to_cstr('"&|<>()@^') + + local argv = ffi.cast('char**', + cimported.shell_build_argv(to_cstr('echo &|<>()@^'), nil)) + eq(ffi.string(argv[0]), '/bin/bash') + eq(ffi.string(argv[1]), '-c') + eq(ffi.string(argv[2]), '(echo ^&^|^<^>^(^)^@^^)') + eq(nil, argv[3]) + end) + + it('applies shellxquote="(', function() + cimported.p_sxq = to_cstr('"(') + cimported.p_sxe = to_cstr('"&|<>()@^') + + local argv = ffi.cast('char**', cimported.shell_build_argv( + to_cstr('echo -n some text'), nil)) + eq(ffi.string(argv[0]), '/bin/bash') + eq(ffi.string(argv[1]), '-c') + eq(ffi.string(argv[2]), '"(echo -n some text)"') + eq(nil, argv[3]) + end) + + it('applies shellxquote="', function() + cimported.p_sxq = to_cstr('"') + cimported.p_sxe = to_cstr('') + + local argv = ffi.cast('char**', cimported.shell_build_argv( + to_cstr('echo -n some text'), nil)) + eq(ffi.string(argv[0]), '/bin/bash') + eq(ffi.string(argv[1]), '-c') + eq(ffi.string(argv[2]), '"echo -n some text"') + eq(nil, argv[3]) + end) + + it('with empty shellxquote/shellxescape', function() + local argv = ffi.cast('char**', cimported.shell_build_argv( + to_cstr('echo -n some text'), nil)) + eq(ffi.string(argv[0]), '/bin/bash') + eq(ffi.string(argv[1]), '-c') + eq(ffi.string(argv[2]), 'echo -n some text') + eq(nil, argv[3]) + end) end) end) diff --git a/test/unit/path_spec.lua b/test/unit/path_spec.lua index 9b76834383..ccaf0228ab 100644 --- a/test/unit/path_spec.lua +++ b/test/unit/path_spec.lua @@ -336,6 +336,17 @@ describe('more path function', function() eq(FAIL, result) end) + it('fails safely if given length is wrong #5737', function() + local force_expansion = 1 + local filename = 'foo/bar/bazzzzzzz/buz/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/a' + local too_short_len = 8 + local buf = cstr(too_short_len, '') + local result = path.vim_FullName(filename, buf, too_short_len, force_expansion) + local expected = string.sub(filename, 1, (too_short_len - 1)) + eq(expected, (ffi.string(buf))) + eq(FAIL, result) + end) + it('uses the filename if the filename is a URL', function() local force_expansion = 1 local filename = 'http://www.neovim.org' diff --git a/test/unit/preprocess.lua b/test/unit/preprocess.lua index e5c838b13b..1c9b290462 100644 --- a/test/unit/preprocess.lua +++ b/test/unit/preprocess.lua @@ -7,22 +7,22 @@ local ccs = {} local env_cc = os.getenv("CC") if env_cc then - table.insert(ccs, {path = "/usr/bin/env " .. tostring(env_cc), type = "gcc"}) + table.insert(ccs, {path = {"/usr/bin/env", env_cc}, type = "gcc"}) end if ffi.os == "Windows" then - table.insert(ccs, {path = "cl", type = "msvc"}) + table.insert(ccs, {path = {"cl"}, type = "msvc"}) end -table.insert(ccs, {path = "/usr/bin/env cc", type = "gcc"}) -table.insert(ccs, {path = "/usr/bin/env gcc", type = "gcc"}) -table.insert(ccs, {path = "/usr/bin/env gcc-4.9", type = "gcc"}) -table.insert(ccs, {path = "/usr/bin/env gcc-4.8", type = "gcc"}) -table.insert(ccs, {path = "/usr/bin/env gcc-4.7", type = "gcc"}) -table.insert(ccs, {path = "/usr/bin/env clang", type = "clang"}) -table.insert(ccs, {path = "/usr/bin/env icc", type = "gcc"}) +table.insert(ccs, {path = {"/usr/bin/env", "cc"}, type = "gcc"}) +table.insert(ccs, {path = {"/usr/bin/env", "gcc"}, type = "gcc"}) +table.insert(ccs, {path = {"/usr/bin/env", "gcc-4.9"}, type = "gcc"}) +table.insert(ccs, {path = {"/usr/bin/env", "gcc-4.8"}, type = "gcc"}) +table.insert(ccs, {path = {"/usr/bin/env", "gcc-4.7"}, type = "gcc"}) +table.insert(ccs, {path = {"/usr/bin/env", "clang"}, type = "clang"}) +table.insert(ccs, {path = {"/usr/bin/env", "icc"}, type = "gcc"}) -local quote_me = '[^%w%+%-%=%@%_%/]' -- complement (needn't quote) +local quote_me = '[^.%w%+%-%@%_%/]' -- complement (needn't quote) local function shell_quote(str) if string.find(str, quote_me) or str == '' then return "'" .. string.gsub(str, "'", [['"'"']]) .. "'" @@ -61,12 +61,12 @@ end -- will produce a string that represents a meta C header file that includes -- all the passed in headers. I.e.: -- --- headerize({"stdio.h", "math.h", true} +-- headerize({"stdio.h", "math.h"}, true) -- produces: -- #include <stdio.h> -- #include <math.h> -- --- headerize({"vim.h", "memory.h", false} +-- headerize({"vim.h", "memory.h"}, false) -- produces: -- #include "vim.h" -- #include "memory.h" @@ -79,8 +79,7 @@ local function headerize(headers, global) end local formatted = {} - for i = 1, #headers do - local hdr = headers[i] + for _, hdr in ipairs(headers) do formatted[#formatted + 1] = "#include " .. tostring(pre) .. tostring(hdr) .. @@ -91,44 +90,78 @@ local function headerize(headers, global) end local Gcc = { + preprocessor_extra_flags = {}, + get_defines_extra_flags = {'-std=c99', '-dM', '-E'}, + get_declarations_extra_flags = {'-std=c99', '-P', '-E'}, +} + +function Gcc:define(name, args, val) + local define = '-D' .. name + if args ~= nil then + define = define .. '(' .. table.concat(args, ',') .. ')' + end + if val ~= nil then + define = define .. '=' .. val + end + self.preprocessor_extra_flags[#self.preprocessor_extra_flags + 1] = define +end + +function Gcc:undefine(name) + self.preprocessor_extra_flags[#self.preprocessor_extra_flags + 1] = ( + '-U' .. name) +end + +function Gcc:init_defines() -- preprocessor flags that will hopefully make the compiler produce C -- declarations that the LuaJIT ffi understands. - preprocessor_extra_flags = { - '-D "aligned(ARGS)="', - '-D "__attribute__(ARGS)="', - '-D "__asm(ARGS)="', - '-D "__asm__(ARGS)="', - '-D "__inline__="', - '-D "EXTERN=extern"', - '-D "INIT(...)="', - '-D_GNU_SOURCE', - '-DINCLUDE_GENERATED_DECLARATIONS', - - -- Needed for FreeBSD - '-D "_Thread_local="' - } -} + self:define('aligned', {'ARGS'}, '') + self:define('__attribute__', {'ARGS'}, '') + self:define('__asm', {'ARGS'}, '') + self:define('__asm__', {'ARGS'}, '') + self:define('__inline__', nil, '') + self:define('EXTERN', nil, 'extern') + self:define('INIT', {'...'}, '') + self:define('_GNU_SOURCE') + self:define('INCLUDE_GENERATED_DECLARATIONS') + self:define('UNIT_TESTING') + -- Needed for FreeBSD + self:define('_Thread_local', nil, '') + -- Needed for macOS Sierra + self:define('_Nullable', nil, '') + self:define('_Nonnull', nil, '') + self:undefine('__BLOCKS__') +end function Gcc:new(obj) obj = obj or {} setmetatable(obj, self) self.__index = self + self:init_defines() return obj end function Gcc:add_to_include_path(...) - local paths = {...} - for i = 1, #paths do - local path = paths[i] - local directive = '-I ' .. '"' .. path .. '"' + for i = 1, select('#', ...) do + local path = select(i, ...) local ef = self.preprocessor_extra_flags - ef[#ef + 1] = directive + ef[#ef + 1] = '-I' .. path + end +end + +local function argss_to_cmd(...) + local cmd = '' + for i = 1, select('#', ...) do + for _, arg in ipairs(select(i, ...)) do + cmd = cmd .. ' ' .. shell_quote(arg) + end end + return cmd end -- returns a list of the headers files upon which this file relies function Gcc:dependencies(hdr) - local out = io.popen(tostring(self.path) .. " -M " .. tostring(hdr) .. " 2>&1") + local cmd = argss_to_cmd(self.path, {'-M', hdr}) .. ' 2>&1' + local out = io.popen(cmd) local deps = out:read("*a") out:close() if deps then @@ -138,23 +171,51 @@ function Gcc:dependencies(hdr) end end +local function repeated_call(...) + local cmd = argss_to_cmd(...) + for _ = 1, 10 do + local stream = io.popen(cmd) + local ret = stream:read('*a') + stream:close() + if ret then + return ret + end + end + print('ERROR: preprocess.lua: Failed to execute ' .. cmd .. ': nil return after 10 attempts') + return nil +end + -- returns a stream representing a preprocessed form of the passed-in headers. -- Don't forget to close the stream by calling the close() method on it. -function Gcc:preprocess_stream(...) +function Gcc:preprocess(previous_defines, ...) -- create pseudo-header local pseudoheader = headerize({...}, false) - local defines = table.concat(self.preprocessor_extra_flags, ' ') - local cmd = ("echo $hdr | " .. - tostring(self.path) .. - " " .. - tostring(defines) .. - " -std=c99 -P -E -"):gsub('$hdr', shell_quote(pseudoheader)) + local pseudoheader_fname = 'tmp_pseudoheader.h' + local pseudoheader_file = io.open(pseudoheader_fname, 'w') + pseudoheader_file:write(previous_defines) + pseudoheader_file:write("\n") + pseudoheader_file:write(pseudoheader) + pseudoheader_file:flush() + pseudoheader_file:close() + + local defines = repeated_call(self.path, self.preprocessor_extra_flags, + self.get_defines_extra_flags, + {pseudoheader_fname}) + -- lfs = require("lfs") -- print("CWD: #{lfs.currentdir!}") -- print("CMD: #{cmd}") -- io.stderr\write("CWD: #{lfs.currentdir!}\n") -- io.stderr\write("CMD: #{cmd}\n") - return io.popen(cmd) + + local declarations = repeated_call(self.path, self.preprocessor_extra_flags, + self.get_declarations_extra_flags, + {pseudoheader_fname}) + + os.remove(pseudoheader_fname) + + assert(declarations and defines) + return declarations, defines end local Clang = Gcc:new() @@ -192,8 +253,8 @@ return { includes = function(hdr) return cc:dependencies(hdr) end, - preprocess_stream = function(...) - return cc:preprocess_stream(...) + preprocess = function(...) + return cc:preprocess(...) end, add_to_include_path = function(...) return cc:add_to_include_path(...) diff --git a/test/unit/strings_spec.lua b/test/unit/strings_spec.lua index e935d2af6a..072701ea78 100644 --- a/test/unit/strings_spec.lua +++ b/test/unit/strings_spec.lua @@ -8,11 +8,43 @@ local to_cstr = helpers.to_cstr local strings = cimport('stdlib.h', './src/nvim/strings.h', './src/nvim/memory.h') +describe('vim_strsave_escaped()', function() + local vim_strsave_escaped = function(s, chars) + local res = strings.vim_strsave_escaped(to_cstr(s), to_cstr(chars)) + local ret = ffi.string(res) + + -- Explicitly free memory so we are sure it is allocated: if it was not it + -- will crash. + strings.xfree(res) + return ret + end + + it('precedes by a backslash all chars from second argument', function() + eq([[\a\b\c\d]], vim_strsave_escaped('abcd','abcd')) + end) + + it('precedes by a backslash chars only from second argument', function() + eq([[\a\bcd]], vim_strsave_escaped('abcd','ab')) + end) + + it('returns a copy of passed string if second argument is empty', function() + eq('text \n text', vim_strsave_escaped('text \n text','')) + end) + + it('returns an empty string if first argument is empty string', function() + eq('', vim_strsave_escaped('','\r')) + end) + + it('returns a copy of passed string if it does not contain chars from 2nd argument', function() + eq('some text', vim_strsave_escaped('some text', 'a')) + end) +end) + describe('vim_strnsave_unquoted()', function() local vim_strnsave_unquoted = function(s, len) local res = strings.vim_strnsave_unquoted(to_cstr(s), len or #s) local ret = ffi.string(res) - -- Explicitly free memory so we are sure it is allocated: if it was not it + -- Explicitly free memory so we are sure it is allocated: if it was not it -- will crash. strings.xfree(res) return ret diff --git a/test/unit/tempfile_spec.lua b/test/unit/tempfile_spec.lua index 7975d11aed..cf0d78b7a7 100644 --- a/test/unit/tempfile_spec.lua +++ b/test/unit/tempfile_spec.lua @@ -43,7 +43,7 @@ describe('tempfile related functions', function() it('generate name of non-existing file', function() local file = vim_tempname() assert.truthy(file) - assert.False(os.os_file_exists(file)) + assert.False(os.os_path_exists(file)) end) it('generate different names on each call', function() |