diff options
author | ZyX <kp-pav@yandex.ru> | 2017-12-03 16:49:30 +0300 |
---|---|---|
committer | ZyX <kp-pav@yandex.ru> | 2017-12-03 16:49:30 +0300 |
commit | c49e22d3964d6c7ae1c24e8ad01b5fec4ca40b57 (patch) | |
tree | b7e59c416d1435725c65f8952b6e55c70544d97e /test/unit | |
parent | 62108c3b0be46936c83f6d4c98b44ceb5e6f77fd (diff) | |
parent | 27a577586eace687c47e7398845178208cae524a (diff) | |
download | rneovim-c49e22d3964d6c7ae1c24e8ad01b5fec4ca40b57.tar.gz rneovim-c49e22d3964d6c7ae1c24e8ad01b5fec4ca40b57.tar.bz2 rneovim-c49e22d3964d6c7ae1c24e8ad01b5fec4ca40b57.zip |
Merge branch 'master' into s-dash-stdin
Diffstat (limited to 'test/unit')
-rw-r--r-- | test/unit/eval/decode_spec.lua | 9 | ||||
-rw-r--r-- | test/unit/eval/helpers.lua | 311 | ||||
-rw-r--r-- | test/unit/eval/tricks_spec.lua | 20 | ||||
-rw-r--r-- | test/unit/eval/tv_clear_spec.lua | 12 | ||||
-rw-r--r-- | test/unit/eval/typval_spec.lua | 3049 | ||||
-rw-r--r-- | test/unit/fixtures/multiqueue.c | 3 | ||||
-rw-r--r-- | test/unit/fixtures/rbuffer.c | 3 | ||||
-rw-r--r-- | test/unit/helpers.lua | 292 | ||||
-rw-r--r-- | test/unit/os/env_spec.lua | 63 | ||||
-rw-r--r-- | test/unit/os/fileio_spec.lua | 125 | ||||
-rw-r--r-- | test/unit/os/fs_spec.lua | 57 | ||||
-rw-r--r-- | test/unit/os/shell_spec.lua | 22 | ||||
-rw-r--r-- | test/unit/path_spec.lua | 260 | ||||
-rw-r--r-- | test/unit/strings_spec.lua | 39 | ||||
-rw-r--r-- | test/unit/testtest_spec.lua | 19 |
15 files changed, 3943 insertions, 341 deletions
diff --git a/test/unit/eval/decode_spec.lua b/test/unit/eval/decode_spec.lua index 2d7597c0f4..0c444b33f2 100644 --- a/test/unit/eval/decode_spec.lua +++ b/test/unit/eval/decode_spec.lua @@ -2,12 +2,11 @@ local helpers = require('test.unit.helpers')(after_each) local itp = helpers.gen_itp(it) local cimport = helpers.cimport -local to_cstr = helpers.to_cstr local eq = helpers.eq local neq = helpers.neq local ffi = helpers.ffi -local decode = cimport('./src/nvim/eval/decode.h', './src/nvim/eval_defs.h', +local decode = cimport('./src/nvim/eval/decode.h', './src/nvim/eval/typval.h', './src/nvim/globals.h', './src/nvim/memory.h', './src/nvim/message.h') @@ -72,7 +71,7 @@ describe('json_decode_string()', function() end itp('does not overflow in error messages', function() - local saved_p_enc = decode.p_enc + collectgarbage('restart') check_failure(']test', 1, 'E474: No container to close: ]') check_failure('[}test', 2, 'E474: Closing list with curly bracket: }') check_failure('{]test', 2, @@ -105,10 +104,6 @@ describe('json_decode_string()', function() check_failure('"\194"test', 3, 'E474: Only UTF-8 strings allowed: \194"') check_failure('"\252\144\128\128\128\128"test', 8, 'E474: Only UTF-8 code points up to U+10FFFF are allowed to appear unescaped: \252\144\128\128\128\128"') check_failure('"test', 1, 'E474: Expected string end: "') - decode.p_enc = to_cstr('latin1') - check_failure('"\\uABCD"test', 8, - 'E474: Failed to convert string "ꯍ" from UTF-8') - decode.p_enc = saved_p_enc check_failure('-test', 1, 'E474: Missing number after minus sign: -') check_failure('-1.test', 3, 'E474: Missing number after decimal dot: -1.') check_failure('-1.0etest', 5, 'E474: Missing exponent: -1.0e') diff --git a/test/unit/eval/helpers.lua b/test/unit/eval/helpers.lua index 1377d5b501..5bc482216e 100644 --- a/test/unit/eval/helpers.lua +++ b/test/unit/eval/helpers.lua @@ -5,13 +5,14 @@ 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/typval.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 locks_key = {[true]='locks key'} local list_type = {[true]='list type'} local dict_type = {[true]='dict type'} local func_type = {[true]='func type'} @@ -23,27 +24,71 @@ local nil_value = {[true]='nil'} local lua2typvalt local function li_alloc(nogc) - local gcfunc = eval.listitem_free + local gcfunc = eval.tv_list_item_free if nogc then gcfunc = nil end - local li = ffi.gc(eval.listitem_alloc(), gcfunc) + local li = ffi.gc(eval.tv_list_item_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) - eq(0, ret.lv_refcount) - ret.lv_refcount = 1 - for i = 1, select('#', ...) do - local val = select(i, ...) - local li_tv = ffi.gc(lua2typvalt(val), nil) - local li = li_alloc(true) - li.li_tv = li_tv - eval.tv_list_append(ret, li) +local function populate_list(l, lua_l, processed) + processed = processed or {} + eq(0, l.lv_refcount) + l.lv_refcount = 1 + processed[lua_l] = l + for i = 1, #lua_l do + local item_tv = ffi.gc(lua2typvalt(lua_l[i], processed), nil) + local item_li = eval.tv_list_item_alloc() + item_li.li_tv = item_tv + eval.tv_list_append(l, item_li) end - return ret + return l +end + +local function populate_dict(d, lua_d, processed) + processed = processed or {} + eq(0, d.dv_refcount) + d.dv_refcount = 1 + processed[lua_d] = d + for k, v in pairs(lua_d) do + if type(k) == 'string' then + local di = eval.tv_dict_item_alloc(to_cstr(k)) + local val_tv = ffi.gc(lua2typvalt(v, processed), nil) + eval.tv_copy(val_tv, di.di_tv) + eval.tv_clear(val_tv) + eval.tv_dict_add(d, di) + end + end + return d +end + +local function populate_partial(pt, lua_pt, processed) + processed = processed or {} + eq(0, pt.pt_refcount) + processed[lua_pt] = pt + local argv = nil + if lua_pt.args and #lua_pt.args > 0 then + argv = ffi.gc(ffi.cast('typval_T*', eval.xmalloc(ffi.sizeof('typval_T') * #lua_pt.args)), nil) + for i, arg in ipairs(lua_pt.args) do + local arg_tv = ffi.gc(lua2typvalt(arg, processed), nil) + argv[i - 1] = arg_tv + end + end + local dict = nil + if lua_pt.dict then + local dict_tv = ffi.gc(lua2typvalt(lua_pt.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(lua_pt.value), #lua_pt.value) + pt.pt_auto = not not lua_pt.auto + pt.pt_argc = lua_pt.args and #lua_pt.args or 0 + pt.pt_argv = argv + pt.pt_dict = dict + return pt end local ptr2key = function(ptr) @@ -54,6 +99,30 @@ local lst2tbl local dct2tbl local typvalt2lua + +local function partial2lua(pt, processed) + processed = processed or {} + 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 + local typvalt2lua_tab = nil local function typvalt2lua_tab_init() @@ -63,10 +132,10 @@ local function typvalt2lua_tab_init() typvalt2lua_tab = { [tonumber(eval.VAR_SPECIAL)] = function(t) return ({ - [eval.kSpecialVarFalse] = false, - [eval.kSpecialVarNull] = nil_value, - [eval.kSpecialVarTrue] = true, - })[t.vval.v_special] + [tonumber(eval.kSpecialVarFalse)] = false, + [tonumber(eval.kSpecialVarNull)] = nil_value, + [tonumber(eval.kSpecialVarTrue)] = true, + })[tonumber(t.vval.v_special)] end, [tonumber(eval.VAR_NUMBER)] = function(t) return {[type_key]=int_type, value=tonumber(t.vval.v_number)} @@ -96,26 +165,7 @@ local function typvalt2lua_tab_init() 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, - } + return partial2lua(t.vval.v_partial, processed) end, } end @@ -241,7 +291,7 @@ local typvalt = function(typ, vval) elseif type(typ) == 'string' then typ = eval[typ] end - return ffi.gc(ffi.new('typval_T', {v_type=typ, vval=vval}), eval.clear_tv) + return ffi.gc(ffi.new('typval_T', {v_type=typ, vval=vval}), eval.tv_clear) end local lua2typvalt_type_tab = { @@ -256,36 +306,16 @@ local lua2typvalt_type_tab = { 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 + local lst = populate_list(eval.tv_list_alloc(), l, processed) + return typvalt(eval.VAR_LIST, {v_list=lst}) 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 + local dct = populate_dict(eval.tv_dict_alloc(), l, processed) + return typvalt(eval.VAR_DICT, {v_dict=dct}) end, [func_type] = function(l, processed) if processed[l] then @@ -293,29 +323,8 @@ local lua2typvalt_type_tab = { 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 + local pt = populate_partial(ffi.gc(ffi.cast('partial_T*', + eval.xcalloc(1, ffi.sizeof('partial_T'))), nil), l, processed) return typvalt(eval.VAR_PARTIAL, {v_partial=pt}) else return typvalt(eval.VAR_FUNC, { @@ -368,13 +377,24 @@ lua2typvalt = function(l, processed) 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.copy_tv(l, tv) + eval.tv_copy(l, tv) return tv end end +local void_ptr = ffi.typeof('void *') local function void(ptr) - return ffi.cast('void*', ptr) + return ffi.cast(void_ptr, ptr) +end + +local function alloc_len(len, get_ptr) + if type(len) == 'string' or type(len) == 'table' then + return #len + elseif len == nil then + return eval.strlen(get_ptr()) + else + return len + end end local alloc_logging_helpers = { @@ -382,14 +402,115 @@ local alloc_logging_helpers = { 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) + size = alloc_len(size, function() return di.di_key end) 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, + str = function(s, size) + size = alloc_len(size, function() return s end) + return {func='malloc', args={size + 1}, ret=void(s)} + end, + + dwatcher = function(w) return {func='malloc', args={ffi.sizeof('DictWatcher')}, ret=void(w)} end, - freed = function(p) return {func='free', args={p and void(p)}} end, + freed = function(p) return {func='free', args={type(p) == 'table' and p or void(p)}} end, + + -- lua_…: allocated by this file, not by some Neovim function + lua_pt = function(pt) return {func='calloc', args={1, ffi.sizeof('partial_T')}, ret=void(pt)} end, + lua_tvs = function(argv, argc) + argc = alloc_len(argc) + return {func='malloc', args={ffi.sizeof('typval_T')*argc}, ret=void(argv)} + end, } +local function int(n) + return {[type_key]=int_type, value=n} +end + +local function list(...) + return populate_list(ffi.gc(eval.tv_list_alloc(), eval.tv_list_unref), + {...}, {}) +end + +local function dict(d) + return populate_dict(ffi.gc(eval.tv_dict_alloc(), eval.tv_dict_free), + d or {}, {}) +end + +local callback2tbl_type_tab = nil + +local function init_callback2tbl_type_tab() + if callback2tbl_type_tab then + return + end + callback2tbl_type_tab = { + [tonumber(eval.kCallbackNone)] = function(_) return {type='none'} end, + [tonumber(eval.kCallbackFuncref)] = function(cb) + return {type='fref', fref=ffi.string(cb.data.funcref)} + end, + [tonumber(eval.kCallbackPartial)] = function(cb) + local lua_pt = partial2lua(cb.data.partial) + return {type='pt', fref=ffi.string(lua_pt.value), pt=lua_pt} + end + } +end + +local function callback2tbl(cb) + init_callback2tbl_type_tab() + return callback2tbl_type_tab[tonumber(cb.type)](cb) +end + +local function tbl2callback(tbl) + local ret = nil + if tbl.type == 'none' then + ret = ffi.new('Callback[1]', {{type=eval.kCallbackNone}}) + elseif tbl.type == 'fref' then + ret = ffi.new('Callback[1]', {{type=eval.kCallbackFuncref, + data={funcref=eval.xstrdup(tbl.fref)}}}) + elseif tbl.type == 'pt' then + local pt = ffi.gc(ffi.cast('partial_T*', + eval.xcalloc(1, ffi.sizeof('partial_T'))), nil) + ret = ffi.new('Callback[1]', {{type=eval.kCallbackPartial, + data={partial=populate_partial(pt, tbl.pt, {})}}}) + else + assert(false) + end + return ffi.gc(ffi.cast('Callback*', ret), helpers.callback_free) +end + +local function dict_watchers(d) + local ret = {} + local h = d.watchers + local q = h.next + local qs = {} + local key_patterns = {} + while q ~= h do + local qitem = ffi.cast('DictWatcher *', + ffi.cast('char *', q) - ffi.offsetof('DictWatcher', 'node')) + ret[#ret + 1] = { + cb=callback2tbl(qitem.callback), + pat=ffi.string(qitem.key_pattern, qitem.key_pattern_len), + busy=qitem.busy, + } + qs[#qs + 1] = qitem + key_patterns[#key_patterns + 1] = {qitem.key_pattern, qitem.key_pattern_len} + q = q.next + end + return ret, qs, key_patterns +end + +local function eval0(expr) + local tv = ffi.gc(ffi.new('typval_T', {v_type=eval.VAR_UNKNOWN}), + eval.tv_clear) + if eval.eval0(to_cstr(expr), tv, nil, true) == 0 then + return nil + else + return tv + end +end + return { + int=int, + null_string=null_string, null_list=null_list, null_dict=null_dict, @@ -402,8 +523,10 @@ return { nil_value=nil_value, type_key=type_key, + locks_key=locks_key, list=list, + dict=dict, lst2tbl=lst2tbl, dct2tbl=dct2tbl, @@ -422,4 +545,12 @@ return { list_items=list_items, dict_items=dict_items, + + dict_watchers=dict_watchers, + tbl2callback=tbl2callback, + callback2tbl=callback2tbl, + + eval0=eval0, + + empty_list = {[type_key]=list_type}, } diff --git a/test/unit/eval/tricks_spec.lua b/test/unit/eval/tricks_spec.lua index ec79a9cad5..7aa0f0f6e6 100644 --- a/test/unit/eval/tricks_spec.lua +++ b/test/unit/eval/tricks_spec.lua @@ -1,19 +1,15 @@ local helpers = require('test.unit.helpers')(after_each) +local eval_helpers = require('test.unit.eval.helpers') + local itp = helpers.gen_itp(it) local cimport = helpers.cimport -local to_cstr = helpers.to_cstr -local ffi = helpers.ffi local eq = helpers.eq -local eval = cimport('./src/nvim/eval.h', './src/nvim/memory.h') +local eval0 = eval_helpers.eval0 -local eval_expr = function(expr) - return ffi.gc(eval.eval_expr(to_cstr(expr), nil), function(tv) - eval.clear_tv(tv) - eval.xfree(tv) - end) -end +local eval = cimport('./src/nvim/eval.h', './src/nvim/eval/typval.h', + './src/nvim/memory.h') describe('NULL typval_T', function() itp('is produced by $XXX_UNEXISTENT_VAR_XXX', function() @@ -25,19 +21,19 @@ describe('NULL typval_T', function() while os.getenv(unexistent_env) ~= nil do unexistent_env = unexistent_env .. '_XXX' end - local rettv = eval_expr('$' .. unexistent_env) + local rettv = eval0('$' .. unexistent_env) eq(eval.VAR_STRING, rettv.v_type) eq(nil, rettv.vval.v_string) end) itp('is produced by v:_null_list', function() - local rettv = eval_expr('v:_null_list') + local rettv = eval0('v:_null_list') eq(eval.VAR_LIST, rettv.v_type) eq(nil, rettv.vval.v_list) end) itp('is produced by v:_null_dict', function() - local rettv = eval_expr('v:_null_dict') + local rettv = eval0('v:_null_dict') eq(eval.VAR_DICT, rettv.v_type) eq(nil, rettv.vval.v_dict) end) diff --git a/test/unit/eval/tv_clear_spec.lua b/test/unit/eval/tv_clear_spec.lua index 47d4661ad8..ca37301b32 100644 --- a/test/unit/eval/tv_clear_spec.lua +++ b/test/unit/eval/tv_clear_spec.lua @@ -14,7 +14,7 @@ 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 lib = cimport('./src/nvim/eval/typval.h', './src/nvim/eval.h') local alloc_log = alloc_log_new() @@ -26,7 +26,7 @@ after_each(function() alloc_log:after_each() end) -describe('clear_tv()', function() +describe('tv_clear()', function() itp('successfully frees all lists in [&l [1], *l, *l]', function() local l_inner = {1} local list = {l_inner, l_inner, l_inner} @@ -44,7 +44,7 @@ describe('clear_tv()', function() a.li(lis[3]), }) eq(3, list_inner_p.lv_refcount) - lib.clear_tv(list_tv) + lib.tv_clear(list_tv) alloc_log:check({ a.freed(lis_inner[1]), a.freed(list_inner_p), @@ -69,7 +69,7 @@ describe('clear_tv()', function() a.li(lis[3]), }) eq(3, list_inner_p.lv_refcount) - lib.clear_tv(list_tv) + lib.tv_clear(list_tv) alloc_log:check({ a.freed(list_inner_p), a.freed(lis[1]), @@ -92,7 +92,7 @@ describe('clear_tv()', function() a.li(lis[2]), }) eq(2, dict_inner_p.dv_refcount) - lib.clear_tv(list_tv) + lib.tv_clear(list_tv) alloc_log:check({ a.freed(dict_inner_p), a.freed(lis[1]), @@ -116,7 +116,7 @@ describe('clear_tv()', function() a.li(lis[2]), }) eq(2, dict_inner_p.dv_refcount) - lib.clear_tv(list_tv) + lib.tv_clear(list_tv) alloc_log:check({ a.freed(dis.a), a.freed(dict_inner_p), diff --git a/test/unit/eval/typval_spec.lua b/test/unit/eval/typval_spec.lua new file mode 100644 index 0000000000..bec74f05fc --- /dev/null +++ b/test/unit/eval/typval_spec.lua @@ -0,0 +1,3049 @@ +local bit = require('bit') +local helpers = require('test.unit.helpers')(after_each) +local eval_helpers = require('test.unit.eval.helpers') +local global_helpers = require('test.helpers') + +local itp = helpers.gen_itp(it) + +local OK = helpers.OK +local eq = helpers.eq +local neq = helpers.neq +local ffi = helpers.ffi +local FAIL = helpers.FAIL +local NULL = helpers.NULL +local cimport = helpers.cimport +local to_cstr = helpers.to_cstr +local alloc_log_new = helpers.alloc_log_new + +local a = eval_helpers.alloc_logging_helpers +local int = eval_helpers.int +local list = eval_helpers.list +local dict = eval_helpers.dict +local eval0 = eval_helpers.eval0 +local lst2tbl = eval_helpers.lst2tbl +local dct2tbl = eval_helpers.dct2tbl +local typvalt = eval_helpers.typvalt +local type_key = eval_helpers.type_key +local li_alloc = eval_helpers.li_alloc +local first_di = eval_helpers.first_di +local nil_value = eval_helpers.nil_value +local func_type = eval_helpers.func_type +local null_list = eval_helpers.null_list +local null_dict = eval_helpers.null_dict +local dict_items = eval_helpers.dict_items +local list_items = eval_helpers.list_items +local empty_list = eval_helpers.empty_list +local lua2typvalt = eval_helpers.lua2typvalt +local typvalt2lua = eval_helpers.typvalt2lua +local null_string = eval_helpers.null_string +local callback2tbl = eval_helpers.callback2tbl +local tbl2callback = eval_helpers.tbl2callback +local dict_watchers = eval_helpers.dict_watchers + +local concat_tables = global_helpers.concat_tables + +local lib = cimport('./src/nvim/eval/typval.h', './src/nvim/memory.h', + './src/nvim/mbyte.h', './src/nvim/garray.h', + './src/nvim/eval.h', './src/nvim/vim.h', + './src/nvim/globals.h') + +local function vimconv_alloc() + return ffi.gc( + ffi.cast('vimconv_T*', lib.xcalloc(1, ffi.sizeof('vimconv_T'))), + function(vc) + lib.convert_setup(vc, nil, nil) + lib.xfree(vc) + end) +end + +local function list_watch_alloc(li) + return ffi.cast('listwatch_T*', ffi.new('listwatch_T[1]', {{lw_item=li}})) +end + +local function list_watch(l, li) + local lw = list_watch_alloc(li or l.lv_first) + lib.tv_list_watch_add(l, lw) + return lw +end + +local function get_alloc_rets(exp_log, res) + setmetatable(res, { + __index={ + freed=function(r, n) return {func='free', args={r[n]}} end + } + }) + for i = 1,#exp_log do + if ({malloc=true, calloc=true})[exp_log[i].func] then + res[#res + 1] = exp_log[i].ret + end + end + return exp_log +end + +local to_cstr_nofree = function(v) return lib.xstrdup(v) end + +local alloc_log = alloc_log_new() + +before_each(function() + alloc_log:before_each() +end) + +after_each(function() + alloc_log:after_each() +end) + +local function ga_alloc(itemsize, growsize) + local ga = ffi.gc(ffi.cast('garray_T*', ffi.new('garray_T[1]', {})), + lib.ga_clear) + lib.ga_init(ga, itemsize or 1, growsize or 80) + return ga +end + +local function check_emsg(f, msg) + local saved_last_msg_hist = lib.last_msg_hist + if saved_last_msg_hist == nil then + saved_last_msg_hist = nil + end + local ret = {f()} + if msg ~= nil then + eq(msg, ffi.string(lib.last_msg_hist.msg)) + neq(saved_last_msg_hist, lib.last_msg_hist) + else + if saved_last_msg_hist ~= lib.last_msg_hist then + eq(nil, ffi.string(lib.last_msg_hist.msg)) + else + eq(saved_last_msg_hist, lib.last_msg_hist) + end + end + return unpack(ret) +end + +describe('typval.c', function() + describe('list', function() + describe('item', function() + describe('alloc()/free()', function() + itp('works', function() + local li = li_alloc(true) + neq(nil, li) + lib.tv_list_item_free(li) + alloc_log:check({ + a.li(li), + a.freed(li), + }) + end) + itp('also frees the value', function() + local li + local s + local l + local tv + li = li_alloc(true) + li.li_tv.v_type = lib.VAR_NUMBER + li.li_tv.vval.v_number = 10 + lib.tv_list_item_free(li) + alloc_log:check({ + a.li(li), + a.freed(li), + }) + + li = li_alloc(true) + li.li_tv.v_type = lib.VAR_FLOAT + li.li_tv.vval.v_float = 10.5 + lib.tv_list_item_free(li) + alloc_log:check({ + a.li(li), + a.freed(li), + }) + + li = li_alloc(true) + li.li_tv.v_type = lib.VAR_STRING + li.li_tv.vval.v_string = nil + lib.tv_list_item_free(li) + alloc_log:check({ + a.li(li), + a.freed(alloc_log.null), + a.freed(li), + }) + + li = li_alloc(true) + li.li_tv.v_type = lib.VAR_STRING + s = to_cstr_nofree('test') + li.li_tv.vval.v_string = s + lib.tv_list_item_free(li) + alloc_log:check({ + a.li(li), + a.str(s, #('test')), + a.freed(s), + a.freed(li), + }) + + li = li_alloc(true) + li.li_tv.v_type = lib.VAR_LIST + l = ffi.gc(list(), nil) + l.lv_refcount = 2 + li.li_tv.vval.v_list = l + lib.tv_list_item_free(li) + alloc_log:check({ + a.li(li), + a.list(l), + a.freed(li), + }) + eq(1, l.lv_refcount) + + li = li_alloc(true) + tv = lua2typvalt({}) + tv.vval.v_dict.dv_refcount = 2 + li.li_tv = tv + lib.tv_list_item_free(li) + alloc_log:check({ + a.li(li), + a.dict(tv.vval.v_dict), + a.freed(li), + }) + eq(1, tv.vval.v_dict.dv_refcount) + end) + end) + describe('remove()', function() + itp('works', function() + local l = list(1, 2, 3, 4, 5, 6, 7) + neq(nil, l) + local lis = list_items(l) + alloc_log:check({ + a.list(l), + a.li(lis[1]), + a.li(lis[2]), + a.li(lis[3]), + a.li(lis[4]), + a.li(lis[5]), + a.li(lis[6]), + a.li(lis[7]), + }) + + lib.tv_list_item_remove(l, lis[1]) + alloc_log:check({ + a.freed(table.remove(lis, 1)), + }) + eq(lis, list_items(l)) + + lib.tv_list_item_remove(l, lis[6]) + alloc_log:check({ + a.freed(table.remove(lis)), + }) + eq(lis, list_items(l)) + + lib.tv_list_item_remove(l, lis[3]) + alloc_log:check({ + a.freed(table.remove(lis, 3)), + }) + eq(lis, list_items(l)) + end) + itp('works and adjusts watchers correctly', function() + local l = ffi.gc(list(1, 2, 3, 4, 5, 6, 7), nil) + neq(nil, l) + local lis = list_items(l) + -- Three watchers: pointing to first, middle and last elements. + local lws = { + list_watch(l, lis[1]), + list_watch(l, lis[4]), + list_watch(l, lis[7]), + } + alloc_log:check({ + a.list(l), + a.li(lis[1]), + a.li(lis[2]), + a.li(lis[3]), + a.li(lis[4]), + a.li(lis[5]), + a.li(lis[6]), + a.li(lis[7]), + }) + + lib.tv_list_item_remove(l, lis[4]) + alloc_log:check({a.freed(lis[4])}) + eq({lis[1], lis[5], lis[7]}, {lws[1].lw_item, lws[2].lw_item, lws[3].lw_item}) + + lib.tv_list_item_remove(l, lis[2]) + alloc_log:check({a.freed(lis[2])}) + eq({lis[1], lis[5], lis[7]}, {lws[1].lw_item, lws[2].lw_item, lws[3].lw_item}) + + lib.tv_list_item_remove(l, lis[7]) + alloc_log:check({a.freed(lis[7])}) + eq({lis[1], lis[5], nil}, {lws[1].lw_item, lws[2].lw_item, lws[3].lw_item == nil and nil}) + + lib.tv_list_item_remove(l, lis[1]) + alloc_log:check({a.freed(lis[1])}) + eq({lis[3], lis[5], nil}, {lws[1].lw_item, lws[2].lw_item, lws[3].lw_item == nil and nil}) + + lib.tv_list_watch_remove(l, lws[2]) + lib.tv_list_watch_remove(l, lws[3]) + lib.tv_list_watch_remove(l, lws[1]) + lib.tv_list_free(l) + alloc_log:check({ + a.freed(lis[3]), + a.freed(lis[5]), + a.freed(lis[6]), + a.freed(l), + }) + end) + end) + end) + describe('watch', function() + describe('remove()', function() + itp('works', function() + local l = ffi.gc(list(1, 2, 3, 4, 5, 6, 7), nil) + eq(nil, l.lv_watch) + local lw = list_watch(l) + neq(nil, l.lv_watch) + alloc_log:clear() + lib.tv_list_watch_remove(l, lw) + eq(nil, l.lv_watch) + alloc_log:check({ + -- Does not free anything. + }) + local lws = { list_watch(l), list_watch(l), list_watch(l) } + alloc_log:clear() + lib.tv_list_watch_remove(l, lws[2]) + eq(lws[3], l.lv_watch) + eq(lws[1], l.lv_watch.lw_next) + lib.tv_list_watch_remove(l, lws[1]) + eq(lws[3], l.lv_watch) + eq(nil, l.lv_watch.lw_next) + lib.tv_list_watch_remove(l, lws[3]) + eq(nil, l.lv_watch) + alloc_log:check({ + -- Does not free anything. + }) + end) + itp('ignores not found watchers', function() + local l = list(1, 2, 3, 4, 5, 6, 7) + local lw = list_watch_alloc() + lib.tv_list_watch_remove(l, lw) + end) + end) + end) + -- add() and fix() were tested when testing tv_list_item_remove() + describe('free()', function() + itp('recursively frees list', function() + local l1 = ffi.gc(list(1, 'abc'), nil) + local l2 = ffi.gc(list({}), nil) + local l3 = ffi.gc(list(empty_list), nil) + local alloc_rets = {} + alloc_log:check(get_alloc_rets({ + a.list(l1), + a.li(l1.lv_first), + a.str(l1.lv_last.li_tv.vval.v_string, #('abc')), + a.li(l1.lv_last), + a.list(l2), + a.dict(l2.lv_first.li_tv.vval.v_dict), + a.li(l2.lv_first), + a.list(l3), + a.list(l3.lv_first.li_tv.vval.v_list), + a.li(l3.lv_first), + }, alloc_rets)) + lib.tv_list_free(l1) + alloc_log:check({ + alloc_rets:freed(2), + alloc_rets:freed(3), + alloc_rets:freed(4), + alloc_rets:freed(1), + }) + lib.tv_list_free(l2) + alloc_log:check({ + alloc_rets:freed(6), + alloc_rets:freed(7), + alloc_rets:freed(5), + }) + lib.tv_list_free(l3) + alloc_log:check({ + alloc_rets:freed(9), + alloc_rets:freed(10), + alloc_rets:freed(8), + }) + end) + end) + describe('free_list()', function() + itp('does not free list contents', function() + local l1 = ffi.gc(list(1, 'abc'), nil) + local l2 = ffi.gc(list({}), nil) + local l3 = ffi.gc(list(empty_list), nil) + local alloc_rets = {} + alloc_log:check(get_alloc_rets({ + a.list(l1), + a.li(l1.lv_first), + a.str(l1.lv_last.li_tv.vval.v_string, #('abc')), + a.li(l1.lv_last), + a.list(l2), + a.dict(l2.lv_first.li_tv.vval.v_dict), + a.li(l2.lv_first), + a.list(l3), + a.list(l3.lv_first.li_tv.vval.v_list), + a.li(l3.lv_first), + }, alloc_rets)) + lib.tv_list_free_list(l1) + alloc_log:check({ + alloc_rets:freed(1), + }) + lib.tv_list_free_list(l2) + alloc_log:check({ + alloc_rets:freed(5), + }) + lib.tv_list_free_list(l3) + alloc_log:check({ + alloc_rets:freed(8), + }) + end) + end) + describe('free_contents()', function() + itp('recursively frees list, except for the list structure itself', + function() + local l1 = ffi.gc(list(1, 'abc'), nil) + local l2 = ffi.gc(list({}), nil) + local l3 = ffi.gc(list(empty_list), nil) + local alloc_rets = {} + alloc_log:check(get_alloc_rets({ + a.list(l1), + a.li(l1.lv_first), + a.str(l1.lv_last.li_tv.vval.v_string, #('abc')), + a.li(l1.lv_last), + a.list(l2), + a.dict(l2.lv_first.li_tv.vval.v_dict), + a.li(l2.lv_first), + a.list(l3), + a.list(l3.lv_first.li_tv.vval.v_list), + a.li(l3.lv_first), + }, alloc_rets)) + lib.tv_list_free_contents(l1) + alloc_log:check({ + alloc_rets:freed(2), + alloc_rets:freed(3), + alloc_rets:freed(4), + }) + lib.tv_list_free_contents(l2) + alloc_log:check({ + alloc_rets:freed(6), + alloc_rets:freed(7), + }) + lib.tv_list_free_contents(l3) + alloc_log:check({ + alloc_rets:freed(9), + alloc_rets:freed(10), + }) + end) + end) + describe('unref()', function() + itp('recursively frees list when reference count goes to 0', function() + local l = ffi.gc(list(empty_list), nil) + local alloc_rets = {} + alloc_log:check(get_alloc_rets({ + a.list(l), + a.list(l.lv_first.li_tv.vval.v_list), + a.li(l.lv_first), + }, alloc_rets)) + l.lv_refcount = 2 + lib.tv_list_unref(l) + alloc_log:check({}) + lib.tv_list_unref(l) + alloc_log:check({ + alloc_rets:freed(2), + alloc_rets:freed(3), + alloc_rets:freed(1), + }) + end) + end) + describe('remove_items()', function() + itp('works', function() + local l_tv = lua2typvalt({1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13}) + local l = l_tv.vval.v_list + local lis = list_items(l) + -- Three watchers: pointing to first, middle and last elements. + local lws = { + list_watch(l, lis[1]), + list_watch(l, lis[7]), + list_watch(l, lis[13]), + } + alloc_log:clear() + + lib.tv_list_remove_items(l, lis[1], lis[3]) + eq({4, 5, 6, 7, 8, 9, 10, 11, 12, 13}, typvalt2lua(l_tv)) + eq({lis[4], lis[7], lis[13]}, {lws[1].lw_item, lws[2].lw_item, lws[3].lw_item}) + + lib.tv_list_remove_items(l, lis[11], lis[13]) + eq({4, 5, 6, 7, 8, 9, 10}, typvalt2lua(l_tv)) + eq({lis[4], lis[7], nil}, {lws[1].lw_item, lws[2].lw_item, lws[3].lw_item == nil and nil}) + + lib.tv_list_remove_items(l, lis[6], lis[8]) + eq({4, 5, 9, 10}, typvalt2lua(l_tv)) + eq({lis[4], lis[9], nil}, {lws[1].lw_item, lws[2].lw_item, lws[3].lw_item == nil and nil}) + + lib.tv_list_remove_items(l, lis[4], lis[10]) + eq(empty_list, typvalt2lua(l_tv)) + eq({true, true, true}, {lws[1].lw_item == nil, lws[2].lw_item == nil, lws[3].lw_item == nil}) + + lib.tv_list_watch_remove(l, lws[1]) + lib.tv_list_watch_remove(l, lws[2]) + lib.tv_list_watch_remove(l, lws[3]) + + alloc_log:check({}) + end) + end) + describe('insert', function() + describe('()', function() + itp('works', function() + local l_tv = lua2typvalt({1, 2, 3, 4, 5, 6, 7}) + local l = l_tv.vval.v_list + local lis = list_items(l) + local li + + li = li_alloc(true) + li.li_tv = {v_type=lib.VAR_FLOAT, vval={v_float=100500}} + lib.tv_list_insert(l, li, nil) + eq(l.lv_last, li) + eq({1, 2, 3, 4, 5, 6, 7, 100500}, typvalt2lua(l_tv)) + + li = li_alloc(true) + li.li_tv = {v_type=lib.VAR_FLOAT, vval={v_float=0}} + lib.tv_list_insert(l, li, lis[1]) + eq(l.lv_first, li) + eq({0, 1, 2, 3, 4, 5, 6, 7, 100500}, typvalt2lua(l_tv)) + + li = li_alloc(true) + li.li_tv = {v_type=lib.VAR_FLOAT, vval={v_float=4.5}} + lib.tv_list_insert(l, li, lis[5]) + eq(list_items(l)[6], li) + eq({0, 1, 2, 3, 4, 4.5, 5, 6, 7, 100500}, typvalt2lua(l_tv)) + end) + itp('works with an empty list', function() + local l_tv = lua2typvalt(empty_list) + local l = l_tv.vval.v_list + + eq(nil, l.lv_first) + eq(nil, l.lv_last) + + local li = li_alloc(true) + li.li_tv = {v_type=lib.VAR_FLOAT, vval={v_float=100500}} + lib.tv_list_insert(l, li, nil) + eq(l.lv_last, li) + eq({100500}, typvalt2lua(l_tv)) + end) + end) + describe('tv()', function() + itp('works', function() + local l_tv = lua2typvalt(empty_list) + local l = l_tv.vval.v_list + + local l_l_tv = lua2typvalt(empty_list) + alloc_log:clear() + local l_l = l_l_tv.vval.v_list + eq(1, l_l.lv_refcount) + lib.tv_list_insert_tv(l, l_l_tv, nil) + eq(2, l_l.lv_refcount) + eq(l_l, l.lv_first.li_tv.vval.v_list) + alloc_log:check({ + a.li(l.lv_first), + }) + + local l_s_tv = lua2typvalt('test') + alloc_log:check({ + a.str(l_s_tv.vval.v_string, 'test'), + }) + lib.tv_list_insert_tv(l, l_s_tv, l.lv_first) + alloc_log:check({ + a.li(l.lv_first), + a.str(l.lv_first.li_tv.vval.v_string, 'test'), + }) + + eq({'test', empty_list}, typvalt2lua(l_tv)) + end) + end) + end) + describe('append', function() + describe('list()', function() + itp('works', function() + local l_tv = lua2typvalt(empty_list) + local l = l_tv.vval.v_list + + local l_l = list(1) + alloc_log:clear() + eq(1, l_l.lv_refcount) + lib.tv_list_append_list(l, l_l) + eq(2, l_l.lv_refcount) + eq(l_l, l.lv_first.li_tv.vval.v_list) + alloc_log:check({ + a.li(l.lv_last), + }) + + lib.tv_list_append_list(l, nil) + alloc_log:check({ + a.li(l.lv_last), + }) + + eq({{1}, null_list}, typvalt2lua(l_tv)) + end) + end) + describe('dict()', function() + itp('works', function() + local l_tv = lua2typvalt(empty_list) + local l = l_tv.vval.v_list + + local l_d_tv = lua2typvalt({test=1}) + local l_d = l_d_tv.vval.v_dict + alloc_log:clear() + eq(1, l_d.dv_refcount) + lib.tv_list_append_dict(l, l_d) + eq(2, l_d.dv_refcount) + eq(l_d, l.lv_first.li_tv.vval.v_list) + alloc_log:check({ + a.li(l.lv_last), + }) + + lib.tv_list_append_dict(l, nil) + alloc_log:check({ + a.li(l.lv_last), + }) + + eq({{test=1}, null_dict}, typvalt2lua(l_tv)) + end) + end) + describe('string()', function() + itp('works', function() + local l_tv = lua2typvalt(empty_list) + local l = l_tv.vval.v_list + + alloc_log:clear() + lib.tv_list_append_string(l, 'test', 3) + alloc_log:check({ + a.str(l.lv_last.li_tv.vval.v_string, 'tes'), + a.li(l.lv_last), + }) + + lib.tv_list_append_string(l, nil, 0) + alloc_log:check({ + a.li(l.lv_last), + }) + + lib.tv_list_append_string(l, nil, -1) + alloc_log:check({ + a.li(l.lv_last), + }) + + lib.tv_list_append_string(l, 'test', -1) + alloc_log:check({ + a.str(l.lv_last.li_tv.vval.v_string, 'test'), + a.li(l.lv_last), + }) + + eq({'tes', null_string, null_string, 'test'}, typvalt2lua(l_tv)) + end) + end) + describe('allocated string()', function() + itp('works', function() + local l_tv = lua2typvalt(empty_list) + local l = l_tv.vval.v_list + + local s = lib.xstrdup('test') + alloc_log:clear() + lib.tv_list_append_allocated_string(l, s) + alloc_log:check({ + a.li(l.lv_last), + }) + + lib.tv_list_append_allocated_string(l, nil) + alloc_log:check({ + a.li(l.lv_last), + }) + + lib.tv_list_append_allocated_string(l, nil) + alloc_log:check({ + a.li(l.lv_last), + }) + + eq({'test', null_string, null_string}, typvalt2lua(l_tv)) + end) + end) + describe('number()', function() + itp('works', function() + local l_tv = lua2typvalt(empty_list) + local l = l_tv.vval.v_list + + alloc_log:clear() + lib.tv_list_append_number(l, -100500) + alloc_log:check({ + a.li(l.lv_last), + }) + + lib.tv_list_append_number(l, 100500) + alloc_log:check({ + a.li(l.lv_last), + }) + + eq({int(-100500), int(100500)}, typvalt2lua(l_tv)) + end) + end) + end) + describe('copy()', function() + local function tv_list_copy(...) + return ffi.gc(lib.tv_list_copy(...), lib.tv_list_unref) + end + itp('copies NULL correctly', function() + eq(nil, lib.tv_list_copy(nil, nil, true, 0)) + eq(nil, lib.tv_list_copy(nil, nil, false, 0)) + eq(nil, lib.tv_list_copy(nil, nil, true, 1)) + eq(nil, lib.tv_list_copy(nil, nil, false, 1)) + end) + itp('copies list correctly without converting items', function() + do + local v = {{['«']='»'}, {'„'}, 1, '“', null_string, null_list, null_dict} + local l_tv = lua2typvalt(v) + local l = l_tv.vval.v_list + local lis = list_items(l) + alloc_log:clear() + + eq(1, lis[1].li_tv.vval.v_dict.dv_refcount) + eq(1, lis[2].li_tv.vval.v_list.lv_refcount) + local l_copy1 = tv_list_copy(nil, l, false, 0) + eq(2, lis[1].li_tv.vval.v_dict.dv_refcount) + eq(2, lis[2].li_tv.vval.v_list.lv_refcount) + local lis_copy1 = list_items(l_copy1) + eq(lis[1].li_tv.vval.v_dict, lis_copy1[1].li_tv.vval.v_dict) + eq(lis[2].li_tv.vval.v_list, lis_copy1[2].li_tv.vval.v_list) + eq(v, lst2tbl(l_copy1)) + alloc_log:check({ + a.list(l_copy1), + a.li(lis_copy1[1]), + a.li(lis_copy1[2]), + a.li(lis_copy1[3]), + a.li(lis_copy1[4]), + a.str(lis_copy1[4].li_tv.vval.v_string, #v[4]), + a.li(lis_copy1[5]), + a.li(lis_copy1[6]), + a.li(lis_copy1[7]), + }) + lib.tv_list_free(ffi.gc(l_copy1, nil)) + alloc_log:clear() + + eq(1, lis[1].li_tv.vval.v_dict.dv_refcount) + eq(1, lis[2].li_tv.vval.v_list.lv_refcount) + local l_deepcopy1 = tv_list_copy(nil, l, true, 0) + neq(nil, l_deepcopy1) + eq(1, lis[1].li_tv.vval.v_dict.dv_refcount) + eq(1, lis[2].li_tv.vval.v_list.lv_refcount) + local lis_deepcopy1 = list_items(l_deepcopy1) + neq(lis[1].li_tv.vval.v_dict, lis_deepcopy1[1].li_tv.vval.v_dict) + neq(lis[2].li_tv.vval.v_list, lis_deepcopy1[2].li_tv.vval.v_list) + eq(v, lst2tbl(l_deepcopy1)) + local di_deepcopy1 = first_di(lis_deepcopy1[1].li_tv.vval.v_dict) + alloc_log:check({ + a.list(l_deepcopy1), + a.li(lis_deepcopy1[1]), + a.dict(lis_deepcopy1[1].li_tv.vval.v_dict), + a.di(di_deepcopy1, #('«')), + a.str(di_deepcopy1.di_tv.vval.v_string, #v[1]['«']), + a.li(lis_deepcopy1[2]), + a.list(lis_deepcopy1[2].li_tv.vval.v_list), + a.li(lis_deepcopy1[2].li_tv.vval.v_list.lv_first), + a.str(lis_deepcopy1[2].li_tv.vval.v_list.lv_first.li_tv.vval.v_string, #v[2][1]), + a.li(lis_deepcopy1[3]), + a.li(lis_deepcopy1[4]), + a.str(lis_deepcopy1[4].li_tv.vval.v_string, #v[4]), + a.li(lis_deepcopy1[5]), + a.li(lis_deepcopy1[6]), + a.li(lis_deepcopy1[7]), + }) + end + collectgarbage() + end) + itp('copies list correctly and converts items', function() + local vc = vimconv_alloc() + -- UTF-8 ↔ latin1 conversions needs no iconv + eq(OK, lib.convert_setup(vc, to_cstr('utf-8'), to_cstr('latin1'))) + + local v = {{['«']='»'}, {'„'}, 1, '“', null_string, null_list, null_dict} + local l_tv = lua2typvalt(v) + local l = l_tv.vval.v_list + local lis = list_items(l) + alloc_log:clear() + + eq(1, lis[1].li_tv.vval.v_dict.dv_refcount) + eq(1, lis[2].li_tv.vval.v_list.lv_refcount) + local l_deepcopy1 = tv_list_copy(vc, l, true, 0) + neq(nil, l_deepcopy1) + eq(1, lis[1].li_tv.vval.v_dict.dv_refcount) + eq(1, lis[2].li_tv.vval.v_list.lv_refcount) + local lis_deepcopy1 = list_items(l_deepcopy1) + neq(lis[1].li_tv.vval.v_dict, lis_deepcopy1[1].li_tv.vval.v_dict) + neq(lis[2].li_tv.vval.v_list, lis_deepcopy1[2].li_tv.vval.v_list) + eq({{['\171']='\187'}, {'\191'}, 1, '\191', null_string, null_list, null_dict}, + lst2tbl(l_deepcopy1)) + local di_deepcopy1 = first_di(lis_deepcopy1[1].li_tv.vval.v_dict) + alloc_log:clear_tmp_allocs() + alloc_log:check({ + a.list(l_deepcopy1), + a.li(lis_deepcopy1[1]), + a.dict(lis_deepcopy1[1].li_tv.vval.v_dict), + a.di(di_deepcopy1, 1), + a.str(di_deepcopy1.di_tv.vval.v_string, 2), + a.li(lis_deepcopy1[2]), + a.list(lis_deepcopy1[2].li_tv.vval.v_list), + a.li(lis_deepcopy1[2].li_tv.vval.v_list.lv_first), + a.str(lis_deepcopy1[2].li_tv.vval.v_list.lv_first.li_tv.vval.v_string, #v[2][1]), + a.li(lis_deepcopy1[3]), + a.li(lis_deepcopy1[4]), + a.str(lis_deepcopy1[4].li_tv.vval.v_string, #v[4]), + a.li(lis_deepcopy1[5]), + a.li(lis_deepcopy1[6]), + a.li(lis_deepcopy1[7]), + }) + end) + itp('returns different/same containers with(out) copyID', function() + local l_inner_tv = lua2typvalt(empty_list) + local l_tv = lua2typvalt({l_inner_tv, l_inner_tv}) + eq(3, l_inner_tv.vval.v_list.lv_refcount) + local l = l_tv.vval.v_list + eq(l.lv_first.li_tv.vval.v_list, l.lv_last.li_tv.vval.v_list) + + local l_copy1 = tv_list_copy(nil, l, true, 0) + neq(l_copy1.lv_first.li_tv.vval.v_list, l_copy1.lv_last.li_tv.vval.v_list) + eq({empty_list, empty_list}, lst2tbl(l_copy1)) + + local l_copy2 = tv_list_copy(nil, l, true, 2) + eq(l_copy2.lv_first.li_tv.vval.v_list, l_copy2.lv_last.li_tv.vval.v_list) + eq({empty_list, empty_list}, lst2tbl(l_copy2)) + + eq(3, l_inner_tv.vval.v_list.lv_refcount) + end) + itp('works with self-referencing list with copyID', function() + local l_tv = lua2typvalt(empty_list) + local l = l_tv.vval.v_list + eq(1, l.lv_refcount) + lib.tv_list_append_list(l, l) + eq(2, l.lv_refcount) + + local l_copy1 = tv_list_copy(nil, l, true, 2) + eq(2, l_copy1.lv_refcount) + local v = {} + v[1] = v + eq(v, lst2tbl(l_copy1)) + + local lis = list_items(l) + lib.tv_list_item_remove(l, lis[1]) + eq(1, l.lv_refcount) + + local lis_copy1 = list_items(l_copy1) + lib.tv_list_item_remove(l_copy1, lis_copy1[1]) + eq(1, l_copy1.lv_refcount) + end) + end) + describe('extend()', function() + itp('can extend list with itself', function() + local l + + l = list(1, {}) + alloc_log:clear() + eq(1, l.lv_refcount) + eq(1, l.lv_last.li_tv.vval.v_dict.dv_refcount) + + lib.tv_list_extend(l, l, nil) + alloc_log:check({ + a.li(l.lv_last.li_prev), + a.li(l.lv_last), + }) + eq(1, l.lv_refcount) + eq(2, l.lv_last.li_tv.vval.v_dict.dv_refcount) + eq({1, {}, 1, {}}, lst2tbl(l)) + + l = list(1, {}) + alloc_log:clear() + eq(1, l.lv_refcount) + eq(1, l.lv_last.li_tv.vval.v_dict.dv_refcount) + + lib.tv_list_extend(l, l, l.lv_last) + alloc_log:check({ + a.li(l.lv_last.li_prev.li_prev), + a.li(l.lv_last.li_prev), + }) + eq({1, 1, {}, {}}, lst2tbl(l)) + eq(1, l.lv_refcount) + eq(2, l.lv_last.li_tv.vval.v_dict.dv_refcount) + + l = list(1, {}) + alloc_log:clear() + eq(1, l.lv_refcount) + eq(1, l.lv_last.li_tv.vval.v_dict.dv_refcount) + + lib.tv_list_extend(l, l, l.lv_first) + alloc_log:check({ + a.li(l.lv_first), + a.li(l.lv_first.li_next), + }) + eq({1, {}, 1, {}}, lst2tbl(l)) + eq(1, l.lv_refcount) + eq(2, l.lv_last.li_tv.vval.v_dict.dv_refcount) + end) + itp('can extend list with an empty list', function() + local l = list(1, {}) + local el = list() + alloc_log:clear() + eq(1, l.lv_refcount) + eq(1, l.lv_last.li_tv.vval.v_dict.dv_refcount) + eq(1, el.lv_refcount) + + lib.tv_list_extend(l, el, nil) + alloc_log:check({ + }) + eq(1, l.lv_refcount) + eq(1, l.lv_last.li_tv.vval.v_dict.dv_refcount) + eq(1, el.lv_refcount) + eq({1, {}}, lst2tbl(l)) + + lib.tv_list_extend(l, el, l.lv_first) + alloc_log:check({ + }) + eq(1, l.lv_refcount) + eq(1, l.lv_last.li_tv.vval.v_dict.dv_refcount) + eq(1, el.lv_refcount) + eq({1, {}}, lst2tbl(l)) + + lib.tv_list_extend(l, el, l.lv_last) + alloc_log:check({ + }) + eq(1, l.lv_refcount) + eq(1, l.lv_last.li_tv.vval.v_dict.dv_refcount) + eq(1, el.lv_refcount) + eq({1, {}}, lst2tbl(l)) + end) + itp('can extend list with another non-empty list', function() + local l + local l2 = list(42, empty_list) + eq(1, l2.lv_refcount) + eq(1, l2.lv_last.li_tv.vval.v_list.lv_refcount) + + l = ffi.gc(list(1, {}), nil) + alloc_log:clear() + eq(1, l.lv_refcount) + eq(1, l.lv_last.li_tv.vval.v_dict.dv_refcount) + + lib.tv_list_extend(l, l2, nil) + alloc_log:check({ + a.li(l.lv_last.li_prev), + a.li(l.lv_last), + }) + eq(1, l2.lv_refcount) + eq(2, l2.lv_last.li_tv.vval.v_list.lv_refcount) + eq({1, {}, 42, empty_list}, lst2tbl(l)) + lib.tv_list_free(l) + eq(1, l2.lv_last.li_tv.vval.v_list.lv_refcount) + + l = ffi.gc(list(1, {}), nil) + alloc_log:clear() + eq(1, l.lv_refcount) + eq(1, l.lv_last.li_tv.vval.v_dict.dv_refcount) + + lib.tv_list_extend(l, l2, l.lv_first) + alloc_log:check({ + a.li(l.lv_first), + a.li(l.lv_first.li_next), + }) + eq(1, l2.lv_refcount) + eq(2, l2.lv_last.li_tv.vval.v_list.lv_refcount) + eq({42, empty_list, 1, {}}, lst2tbl(l)) + lib.tv_list_free(l) + eq(1, l2.lv_last.li_tv.vval.v_list.lv_refcount) + + l = ffi.gc(list(1, {}), nil) + alloc_log:clear() + eq(1, l.lv_refcount) + eq(1, l.lv_last.li_tv.vval.v_dict.dv_refcount) + + lib.tv_list_extend(l, l2, l.lv_last) + alloc_log:check({ + a.li(l.lv_first.li_next), + a.li(l.lv_first.li_next.li_next), + }) + eq(1, l2.lv_refcount) + eq(2, l2.lv_last.li_tv.vval.v_list.lv_refcount) + eq({1, 42, empty_list, {}}, lst2tbl(l)) + lib.tv_list_free(l) + eq(1, l2.lv_last.li_tv.vval.v_list.lv_refcount) + end) + end) + describe('concat()', function() + itp('works with NULL lists', function() + local l = list(1, {}) + alloc_log:clear() + eq(1, l.lv_refcount) + eq(1, l.lv_last.li_tv.vval.v_dict.dv_refcount) + + local rettv1 = typvalt() + eq(OK, lib.tv_list_concat(nil, l, rettv1)) + eq(1, l.lv_refcount) + eq(tonumber(lib.VAR_LIST), tonumber(rettv1.v_type)) + eq({1, {}}, typvalt2lua(rettv1)) + eq(1, rettv1.vval.v_list.lv_refcount) + alloc_log:check({ + a.list(rettv1.vval.v_list), + a.li(rettv1.vval.v_list.lv_first), + a.li(rettv1.vval.v_list.lv_last), + }) + eq(2, l.lv_last.li_tv.vval.v_dict.dv_refcount) + + local rettv2 = typvalt() + eq(OK, lib.tv_list_concat(l, nil, rettv2)) + eq(1, l.lv_refcount) + eq(tonumber(lib.VAR_LIST), tonumber(rettv2.v_type)) + eq({1, {}}, typvalt2lua(rettv2)) + eq(1, rettv2.vval.v_list.lv_refcount) + alloc_log:check({ + a.list(rettv2.vval.v_list), + a.li(rettv2.vval.v_list.lv_first), + a.li(rettv2.vval.v_list.lv_last), + }) + eq(3, l.lv_last.li_tv.vval.v_dict.dv_refcount) + + local rettv3 = typvalt() + eq(OK, lib.tv_list_concat(nil, nil, rettv3)) + eq(tonumber(lib.VAR_LIST), tonumber(rettv3.v_type)) + eq(null_list, typvalt2lua(rettv3)) + alloc_log:check({}) + end) + itp('works with two different lists', function() + local l1 = list(1, {}) + local l2 = list(3, empty_list) + eq(1, l1.lv_refcount) + eq(1, l1.lv_last.li_tv.vval.v_dict.dv_refcount) + eq(1, l2.lv_refcount) + eq(1, l2.lv_last.li_tv.vval.v_list.lv_refcount) + alloc_log:clear() + + local rettv = typvalt() + eq(OK, lib.tv_list_concat(l1, l2, rettv)) + eq(1, l1.lv_refcount) + eq(2, l1.lv_last.li_tv.vval.v_dict.dv_refcount) + eq(1, l2.lv_refcount) + eq(2, l2.lv_last.li_tv.vval.v_list.lv_refcount) + alloc_log:check({ + a.list(rettv.vval.v_list), + a.li(rettv.vval.v_list.lv_first), + a.li(rettv.vval.v_list.lv_first.li_next), + a.li(rettv.vval.v_list.lv_last.li_prev), + a.li(rettv.vval.v_list.lv_last), + }) + eq({1, {}, 3, empty_list}, typvalt2lua(rettv)) + end) + itp('can concatenate list with itself', function() + local l = list(1, {}) + eq(1, l.lv_refcount) + eq(1, l.lv_last.li_tv.vval.v_dict.dv_refcount) + alloc_log:clear() + + local rettv = typvalt() + eq(OK, lib.tv_list_concat(l, l, rettv)) + eq(1, l.lv_refcount) + eq(3, l.lv_last.li_tv.vval.v_dict.dv_refcount) + alloc_log:check({ + a.list(rettv.vval.v_list), + a.li(rettv.vval.v_list.lv_first), + a.li(rettv.vval.v_list.lv_first.li_next), + a.li(rettv.vval.v_list.lv_last.li_prev), + a.li(rettv.vval.v_list.lv_last), + }) + eq({1, {}, 1, {}}, typvalt2lua(rettv)) + end) + itp('can concatenate empty non-NULL lists', function() + local l = list(1, {}) + local le = list() + local le2 = list() + eq(1, l.lv_refcount) + eq(1, l.lv_last.li_tv.vval.v_dict.dv_refcount) + eq(1, le.lv_refcount) + eq(1, le2.lv_refcount) + alloc_log:clear() + + local rettv1 = typvalt() + eq(OK, lib.tv_list_concat(l, le, rettv1)) + eq(1, l.lv_refcount) + eq(2, l.lv_last.li_tv.vval.v_dict.dv_refcount) + eq(1, le.lv_refcount) + eq(1, le2.lv_refcount) + alloc_log:check({ + a.list(rettv1.vval.v_list), + a.li(rettv1.vval.v_list.lv_first), + a.li(rettv1.vval.v_list.lv_last), + }) + eq({1, {}}, typvalt2lua(rettv1)) + + local rettv2 = typvalt() + eq(OK, lib.tv_list_concat(le, l, rettv2)) + eq(1, l.lv_refcount) + eq(3, l.lv_last.li_tv.vval.v_dict.dv_refcount) + eq(1, le.lv_refcount) + eq(1, le2.lv_refcount) + alloc_log:check({ + a.list(rettv2.vval.v_list), + a.li(rettv2.vval.v_list.lv_first), + a.li(rettv2.vval.v_list.lv_last), + }) + eq({1, {}}, typvalt2lua(rettv2)) + + local rettv3 = typvalt() + eq(OK, lib.tv_list_concat(le, le, rettv3)) + eq(1, l.lv_refcount) + eq(3, l.lv_last.li_tv.vval.v_dict.dv_refcount) + eq(1, le.lv_refcount) + eq(1, le2.lv_refcount) + alloc_log:check({ + a.list(rettv3.vval.v_list), + }) + eq(empty_list, typvalt2lua(rettv3)) + + local rettv4 = typvalt() + eq(OK, lib.tv_list_concat(le, le2, rettv4)) + eq(1, l.lv_refcount) + eq(3, l.lv_last.li_tv.vval.v_dict.dv_refcount) + eq(1, le.lv_refcount) + eq(1, le2.lv_refcount) + alloc_log:check({ + a.list(rettv4.vval.v_list), + }) + eq(empty_list, typvalt2lua(rettv4)) + end) + end) + describe('join()', function() + local function list_join(l, sep, join_ret) + local ga = ga_alloc() + eq(join_ret or OK, lib.tv_list_join(ga, l, sep)) + local ret = '' + if ga.ga_data ~= nil then + ret = ffi.string(ga.ga_data) + end + -- For some reason this is not working well in GC + lib.ga_clear(ffi.gc(ga, nil)) + return ret + end + itp('works', function() + local l + l = list('boo', 'far') + eq('boo far', list_join(l, ' ')) + eq('boofar', list_join(l, '')) + + l = list('boo') + eq('boo', list_join(l, ' ')) + + l = list() + eq('', list_join(l, ' ')) + + l = list({}, 'far') + eq('{} far', list_join(l, ' ')) + + local recursive_list = {} + recursive_list[1] = recursive_list + l = ffi.gc(list(recursive_list, 'far'), nil) + eq('[[...@0]] far', list_join(l, ' ')) + + local recursive_l = l.lv_first.li_tv.vval.v_list + local recursive_li = recursive_l.lv_first + lib.tv_list_item_remove(recursive_l, recursive_li) + lib.tv_list_free(l) + end) + end) + describe('equal()', function() + itp('compares empty and NULL lists correctly', function() + local l = list() + local l2 = list() + + -- NULL lists are not equal to empty lists + eq(false, lib.tv_list_equal(l, nil, true, false)) + eq(false, lib.tv_list_equal(nil, l, false, false)) + eq(false, lib.tv_list_equal(nil, l, false, true)) + eq(false, lib.tv_list_equal(l, nil, true, true)) + + -- Yet NULL lists are equal themselves + eq(true, lib.tv_list_equal(nil, nil, true, false)) + eq(true, lib.tv_list_equal(nil, nil, false, false)) + eq(true, lib.tv_list_equal(nil, nil, false, true)) + eq(true, lib.tv_list_equal(nil, nil, true, true)) + + -- As well as empty lists + eq(true, lib.tv_list_equal(l, l, true, false)) + eq(true, lib.tv_list_equal(l, l2, false, false)) + eq(true, lib.tv_list_equal(l2, l, false, true)) + eq(true, lib.tv_list_equal(l2, l2, true, true)) + end) + -- Must not use recursive=true argument in the following tests because it + -- indicates that tv_equal_recurse_limit and recursive_cnt were set which + -- is essential. This argument will be set when comparing inner lists. + itp('compares lists correctly when case is not ignored', function() + local l1 = list('abc', {1, 2, 'Abc'}, 'def') + local l2 = list('abc', {1, 2, 'Abc'}) + local l3 = list('abc', {1, 2, 'Abc'}, 'Def') + local l4 = list('abc', {1, 2, 'Abc', 4}, 'def') + local l5 = list('Abc', {1, 2, 'Abc'}, 'def') + local l6 = list('abc', {1, 2, 'Abc'}, 'def') + local l7 = list('abc', {1, 2, 'abc'}, 'def') + local l8 = list('abc', nil, 'def') + local l9 = list('abc', {1, 2, nil}, 'def') + + eq(true, lib.tv_list_equal(l1, l1, false, false)) + eq(false, lib.tv_list_equal(l1, l2, false, false)) + eq(false, lib.tv_list_equal(l1, l3, false, false)) + eq(false, lib.tv_list_equal(l1, l4, false, false)) + eq(false, lib.tv_list_equal(l1, l5, false, false)) + eq(true, lib.tv_list_equal(l1, l6, false, false)) + eq(false, lib.tv_list_equal(l1, l7, false, false)) + eq(false, lib.tv_list_equal(l1, l8, false, false)) + eq(false, lib.tv_list_equal(l1, l9, false, false)) + end) + itp('compares lists correctly when case is ignored', function() + local l1 = list('abc', {1, 2, 'Abc'}, 'def') + local l2 = list('abc', {1, 2, 'Abc'}) + local l3 = list('abc', {1, 2, 'Abc'}, 'Def') + local l4 = list('abc', {1, 2, 'Abc', 4}, 'def') + local l5 = list('Abc', {1, 2, 'Abc'}, 'def') + local l6 = list('abc', {1, 2, 'Abc'}, 'def') + local l7 = list('abc', {1, 2, 'abc'}, 'def') + local l8 = list('abc', nil, 'def') + local l9 = list('abc', {1, 2, nil}, 'def') + + eq(true, lib.tv_list_equal(l1, l1, true, false)) + eq(false, lib.tv_list_equal(l1, l2, true, false)) + eq(true, lib.tv_list_equal(l1, l3, true, false)) + eq(false, lib.tv_list_equal(l1, l4, true, false)) + eq(true, lib.tv_list_equal(l1, l5, true, false)) + eq(true, lib.tv_list_equal(l1, l6, true, false)) + eq(true, lib.tv_list_equal(l1, l7, true, false)) + eq(false, lib.tv_list_equal(l1, l8, true, false)) + eq(false, lib.tv_list_equal(l1, l9, true, false)) + end) + end) + describe('find', function() + describe('()', function() + itp('correctly indexes list', function() + local l = list(1, 2, 3, 4, 5) + local lis = list_items(l) + alloc_log:clear() + + eq(nil, lib.tv_list_find(nil, -1)) + eq(nil, lib.tv_list_find(nil, 0)) + eq(nil, lib.tv_list_find(nil, 1)) + + eq(nil, lib.tv_list_find(l, 5)) + eq(nil, lib.tv_list_find(l, -6)) + eq(lis[1], lib.tv_list_find(l, -5)) + eq(lis[5], lib.tv_list_find(l, 4)) + eq(lis[3], lib.tv_list_find(l, 2)) + eq(lis[3], lib.tv_list_find(l, -3)) + eq(lis[3], lib.tv_list_find(l, 2)) + eq(lis[3], lib.tv_list_find(l, 2)) + eq(lis[3], lib.tv_list_find(l, -3)) + + l.lv_idx_item = nil + eq(lis[1], lib.tv_list_find(l, -5)) + l.lv_idx_item = nil + eq(lis[5], lib.tv_list_find(l, 4)) + l.lv_idx_item = nil + eq(lis[3], lib.tv_list_find(l, 2)) + l.lv_idx_item = nil + eq(lis[3], lib.tv_list_find(l, -3)) + l.lv_idx_item = nil + eq(lis[3], lib.tv_list_find(l, 2)) + l.lv_idx_item = nil + eq(lis[3], lib.tv_list_find(l, 2)) + l.lv_idx_item = nil + eq(lis[3], lib.tv_list_find(l, -3)) + + l.lv_idx_item = nil + eq(lis[3], lib.tv_list_find(l, 2)) + eq(lis[1], lib.tv_list_find(l, -5)) + eq(lis[3], lib.tv_list_find(l, 2)) + eq(lis[5], lib.tv_list_find(l, 4)) + eq(lis[3], lib.tv_list_find(l, 2)) + eq(lis[3], lib.tv_list_find(l, 2)) + eq(lis[3], lib.tv_list_find(l, 2)) + eq(lis[3], lib.tv_list_find(l, -3)) + eq(lis[3], lib.tv_list_find(l, 2)) + eq(lis[3], lib.tv_list_find(l, 2)) + eq(lis[3], lib.tv_list_find(l, 2)) + eq(lis[3], lib.tv_list_find(l, -3)) + + alloc_log:check({}) + end) + end) + describe('nr()', function() + local function tv_list_find_nr(l, n, msg) + return check_emsg(function() + local err = ffi.new('bool[1]', {false}) + local ret = lib.tv_list_find_nr(l, n, err) + return (err[0] == true), ret + end, msg) + end + itp('returns correct number', function() + local l = list(int(1), int(2), int(3), int(4), int(5)) + alloc_log:clear() + + eq({false, 1}, {tv_list_find_nr(l, -5)}) + eq({false, 5}, {tv_list_find_nr(l, 4)}) + eq({false, 3}, {tv_list_find_nr(l, 2)}) + eq({false, 3}, {tv_list_find_nr(l, -3)}) + + alloc_log:check({}) + end) + itp('returns correct number when given a string', function() + local l = list('1', '2', '3', '4', '5') + alloc_log:clear() + + eq({false, 1}, {tv_list_find_nr(l, -5)}) + eq({false, 5}, {tv_list_find_nr(l, 4)}) + eq({false, 3}, {tv_list_find_nr(l, 2)}) + eq({false, 3}, {tv_list_find_nr(l, -3)}) + + alloc_log:check({}) + end) + itp('returns zero when given a NULL string', function() + local l = list(null_string) + alloc_log:clear() + + eq({false, 0}, {tv_list_find_nr(l, 0)}) + + alloc_log:check({}) + end) + itp('errors out on NULL lists', function() + eq({true, -1}, {tv_list_find_nr(nil, -5)}) + eq({true, -1}, {tv_list_find_nr(nil, 4)}) + eq({true, -1}, {tv_list_find_nr(nil, 2)}) + eq({true, -1}, {tv_list_find_nr(nil, -3)}) + + alloc_log:check({}) + end) + itp('errors out on out-of-range indexes', function() + local l = list(int(1), int(2), int(3), int(4), int(5)) + alloc_log:clear() + + eq({true, -1}, {tv_list_find_nr(l, -6)}) + eq({true, -1}, {tv_list_find_nr(l, 5)}) + + alloc_log:check({}) + end) + itp('errors out on invalid types', function() + local l = list(1, empty_list, {}) + + eq({true, 0}, {tv_list_find_nr(l, 0, 'E805: Using a Float as a Number')}) + eq({true, 0}, {tv_list_find_nr(l, 1, 'E745: Using a List as a Number')}) + eq({true, 0}, {tv_list_find_nr(l, 2, 'E728: Using a Dictionary as a Number')}) + eq({true, 0}, {tv_list_find_nr(l, -1, 'E728: Using a Dictionary as a Number')}) + eq({true, 0}, {tv_list_find_nr(l, -2, 'E745: Using a List as a Number')}) + eq({true, 0}, {tv_list_find_nr(l, -3, 'E805: Using a Float as a Number')}) + end) + end) + local function tv_list_find_str(l, n, msg) + return check_emsg(function() + local ret = lib.tv_list_find_str(l, n) + local s = nil + if ret ~= nil then + s = ffi.string(ret) + end + return s + end, msg) + end + describe('str()', function() + itp('returns correct string', function() + local l = list(int(1), int(2), int(3), int(4), int(5)) + alloc_log:clear() + + eq('1', tv_list_find_str(l, -5)) + eq('5', tv_list_find_str(l, 4)) + eq('3', tv_list_find_str(l, 2)) + eq('3', tv_list_find_str(l, -3)) + + alloc_log:check({}) + end) + itp('returns string when used with VAR_STRING items', function() + local l = list('1', '2', '3', '4', '5') + alloc_log:clear() + + eq('1', tv_list_find_str(l, -5)) + eq('5', tv_list_find_str(l, 4)) + eq('3', tv_list_find_str(l, 2)) + eq('3', tv_list_find_str(l, -3)) + + alloc_log:check({}) + end) + itp('returns empty when used with NULL string', function() + local l = list(null_string) + alloc_log:clear() + + eq('', tv_list_find_str(l, 0)) + + alloc_log:check({}) + end) + itp('fails with error message when index is out of range', function() + local l = list(int(1), int(2), int(3), int(4), int(5)) + + eq(nil, tv_list_find_str(l, -6, 'E684: list index out of range: -6')) + eq(nil, tv_list_find_str(l, 5, 'E684: list index out of range: 5')) + end) + itp('fails with error message on invalid types', function() + local l = list(1, empty_list, {}) + + eq('', tv_list_find_str(l, 0, 'E806: using Float as a String')) + eq('', tv_list_find_str(l, 1, 'E730: using List as a String')) + eq('', tv_list_find_str(l, 2, 'E731: using Dictionary as a String')) + eq('', tv_list_find_str(l, -1, 'E731: using Dictionary as a String')) + eq('', tv_list_find_str(l, -2, 'E730: using List as a String')) + eq('', tv_list_find_str(l, -3, 'E806: using Float as a String')) + end) + end) + end) + describe('idx_of_item()', function() + itp('works', function() + local l = list(1, 2, 3, 4, 5) + local l2 = list(42, empty_list) + local lis = list_items(l) + local lis2 = list_items(l2) + + for i, li in ipairs(lis) do + eq(i - 1, lib.tv_list_idx_of_item(l, li)) + end + eq(-1, lib.tv_list_idx_of_item(l, lis2[1])) + eq(-1, lib.tv_list_idx_of_item(l, nil)) + eq(-1, lib.tv_list_idx_of_item(nil, nil)) + eq(-1, lib.tv_list_idx_of_item(nil, lis[1])) + end) + end) + end) + describe('dict', function() + describe('watcher', function() + describe('add()/remove()', function() + itp('works with an empty key', function() + local d = dict({}) + eq({}, dict_watchers(d)) + local cb = ffi.gc(tbl2callback({type='none'}), nil) + alloc_log:clear() + lib.tv_dict_watcher_add(d, '*', 0, cb[0]) + local ws, qs = dict_watchers(d) + local key_p = qs[1].key_pattern + alloc_log:check({ + a.dwatcher(qs[1]), + a.str(key_p, 0), + }) + eq({{busy=false, cb={type='none'}, pat=''}}, ws) + eq(true, lib.tv_dict_watcher_remove(d, 'x', 0, cb[0])) + alloc_log:check({ + a.freed(key_p), + a.freed(qs[1]), + }) + eq({}, dict_watchers(d)) + end) + itp('works with multiple callbacks', function() + local d = dict({}) + eq({}, dict_watchers(d)) + alloc_log:check({a.dict(d)}) + local cbs = {} + cbs[1] = {'te', ffi.gc(tbl2callback({type='none'}), nil)} + alloc_log:check({}) + cbs[2] = {'foo', ffi.gc(tbl2callback({type='fref', fref='tr'}), nil)} + alloc_log:check({ + a.str(cbs[2][2].data.funcref, #('tr')), + }) + cbs[3] = {'te', ffi.gc(tbl2callback({type='pt', fref='tr', pt={ + value='tr', + args={'test'}, + dict={}, + }}), nil)} + local pt3 = cbs[3][2].data.partial + local pt3_argv = pt3.pt_argv + local pt3_dict = pt3.pt_dict + local pt3_name = pt3.pt_name + local pt3_str_arg = pt3.pt_argv[0].vval.v_string + alloc_log:check({ + a.lua_pt(pt3), + a.lua_tvs(pt3_argv, pt3.pt_argc), + a.str(pt3_str_arg, #('test')), + a.dict(pt3_dict), + a.str(pt3_name, #('tr')), + }) + for _, v in ipairs(cbs) do + lib.tv_dict_watcher_add(d, v[1], #(v[1]), v[2][0]) + end + local ws, qs, kps = dict_watchers(d) + eq({{busy=false, pat=cbs[1][1], cb={type='none'}}, + {busy=false, pat=cbs[2][1], cb={type='fref', fref='tr'}}, + {busy=false, pat=cbs[3][1], cb={type='pt', fref='tr', pt={ + [type_key]=func_type, + value='tr', + args={'test'}, + dict={}, + }}}}, ws) + alloc_log:check({ + a.dwatcher(qs[1]), + a.str(kps[1][1], kps[1][2]), + a.dwatcher(qs[2]), + a.str(kps[2][1], kps[2][2]), + a.dwatcher(qs[3]), + a.str(kps[3][1], kps[3][2]), + }) + eq(true, lib.tv_dict_watcher_remove(d, cbs[2][1], #cbs[2][1], cbs[2][2][0])) + alloc_log:check({ + a.freed(cbs[2][2].data.funcref), + a.freed(kps[2][1]), + a.freed(qs[2]), + }) + eq(false, lib.tv_dict_watcher_remove(d, cbs[2][1], #cbs[2][1], cbs[2][2][0])) + eq({{busy=false, pat=cbs[1][1], cb={type='none'}}, + {busy=false, pat=cbs[3][1], cb={type='pt', fref='tr', pt={ + [type_key]=func_type, + value='tr', + args={'test'}, + dict={}, + }}}}, dict_watchers(d)) + eq(true, lib.tv_dict_watcher_remove(d, cbs[3][1], #cbs[3][1], cbs[3][2][0])) + alloc_log:check({ + a.freed(pt3_str_arg), + a.freed(pt3_argv), + a.freed(pt3_dict), + a.freed(pt3_name), + a.freed(pt3), + a.freed(kps[3][1]), + a.freed(qs[3]), + }) + eq(false, lib.tv_dict_watcher_remove(d, cbs[3][1], #cbs[3][1], cbs[3][2][0])) + eq({{busy=false, pat=cbs[1][1], cb={type='none'}}}, dict_watchers(d)) + eq(true, lib.tv_dict_watcher_remove(d, cbs[1][1], #cbs[1][1], cbs[1][2][0])) + alloc_log:check({ + a.freed(kps[1][1]), + a.freed(qs[1]), + }) + eq(false, lib.tv_dict_watcher_remove(d, cbs[1][1], #cbs[1][1], cbs[1][2][0])) + eq({}, dict_watchers(d)) + end) + end) + describe('notify', function() + -- Way too hard to test it here, functional tests in + -- dict_notifications_spec.lua. + end) + end) + describe('item', function() + describe('alloc()/free()', function() + local function check_tv_dict_item_alloc_len(s, len, tv, more_frees) + local di + if len == nil then + di = ffi.gc(lib.tv_dict_item_alloc(s), nil) + len = #s + else + di = ffi.gc(lib.tv_dict_item_alloc_len(s, len or #s), nil) + end + eq(s:sub(1, len), ffi.string(di.di_key)) + alloc_log:check({a.di(di, len)}) + if tv then + di.di_tv = ffi.gc(tv, nil) + else + di.di_tv.v_type = lib.VAR_UNKNOWN + end + lib.tv_dict_item_free(di) + alloc_log:check(concat_tables(more_frees, {a.freed(di)})) + end + local function check_tv_dict_item_alloc(s, tv, more_frees) + return check_tv_dict_item_alloc_len(s, nil, tv, more_frees) + end + itp('works', function() + check_tv_dict_item_alloc('') + check_tv_dict_item_alloc('t') + check_tv_dict_item_alloc('TEST') + check_tv_dict_item_alloc_len('', 0) + check_tv_dict_item_alloc_len('TEST', 2) + local tv = lua2typvalt('test') + alloc_log:check({a.str(tv.vval.v_string, #('test'))}) + check_tv_dict_item_alloc('', tv, {a.freed(tv.vval.v_string)}) + tv = lua2typvalt('test') + alloc_log:check({a.str(tv.vval.v_string, #('test'))}) + check_tv_dict_item_alloc_len('', 0, tv, {a.freed(tv.vval.v_string)}) + end) + end) + describe('add()/remove()', function() + itp('works', function() + local d = dict() + eq({}, dct2tbl(d)) + alloc_log:check({a.dict(d)}) + local di = ffi.gc(lib.tv_dict_item_alloc(''), nil) + local tv = lua2typvalt('test') + di.di_tv = ffi.gc(tv, nil) + alloc_log:check({a.di(di, ''), a.str(tv.vval.v_string, 'test')}) + eq(OK, lib.tv_dict_add(d, di)) + alloc_log:check({}) + eq(FAIL, check_emsg(function() return lib.tv_dict_add(d, di) end, + 'E685: Internal error: hash_add()')) + alloc_log:clear() + lib.tv_dict_item_remove(d, di) + alloc_log:check({ + a.freed(tv.vval.v_string), + a.freed(di), + }) + end) + end) + end) + describe('indexing', function() + describe('find()', function() + local function tv_dict_find(d, key, key_len) + local di = lib.tv_dict_find(d, key, key_len or #key) + if di == nil then + return nil, nil, nil + end + return typvalt2lua(di.di_tv), ffi.string(di.di_key), di + end + itp('works with NULL dict', function() + eq(nil, lib.tv_dict_find(nil, '', 0)) + eq(nil, lib.tv_dict_find(nil, 'test', -1)) + eq(nil, lib.tv_dict_find(nil, nil, 0)) + end) + itp('works with NULL key', function() + local lua_d = { + ['']=0, + t=1, + te=2, + tes=3, + test=4, + testt=5, + } + local d = dict(lua_d) + alloc_log:clear() + eq(lua_d, dct2tbl(d)) + alloc_log:check({}) + local dis = dict_items(d) + eq({0, '', dis['']}, {tv_dict_find(d, '', 0)}) + eq({0, '', dis['']}, {tv_dict_find(d, nil, 0)}) + end) + itp('works with len properly', function() + local lua_d = { + ['']=0, + t=1, + te=2, + tes=3, + test=4, + testt=5, + } + local d = dict(lua_d) + alloc_log:clear() + eq(lua_d, dct2tbl(d)) + alloc_log:check({}) + for i = 0, 5 do + local v, k = tv_dict_find(d, 'testt', i) + eq({i, ('testt'):sub(1, i)}, {v, k}) + end + eq(nil, tv_dict_find(d, 'testt', 6)) -- Should take NUL byte + eq(5, tv_dict_find(d, 'testt', -1)) + alloc_log:check({}) + end) + end) + describe('get_number()', function() + itp('works with NULL dict', function() + eq(0, check_emsg(function() return lib.tv_dict_get_number(nil, 'test') end, + nil)) + end) + itp('works', function() + local d = ffi.gc(dict({test={}}), nil) + eq(0, check_emsg(function() return lib.tv_dict_get_number(d, 'test') end, + 'E728: Using a Dictionary as a Number')) + d = ffi.gc(dict({tes=int(42), t=44, te='43'}), nil) + alloc_log:clear() + eq(0, check_emsg(function() return lib.tv_dict_get_number(d, 'test') end, + nil)) + eq(42, check_emsg(function() return lib.tv_dict_get_number(d, 'tes') end, + nil)) + eq(43, check_emsg(function() return lib.tv_dict_get_number(d, 'te') end, + nil)) + alloc_log:check({}) + eq(0, check_emsg(function() return lib.tv_dict_get_number(d, 't') end, + 'E805: Using a Float as a Number')) + end) + end) + describe('get_string()', function() + itp('works with NULL dict', function() + eq(nil, check_emsg(function() return lib.tv_dict_get_string(nil, 'test', false) end, + nil)) + end) + itp('works', function() + local d = ffi.gc(dict({test={}}), nil) + eq('', ffi.string(check_emsg(function() return lib.tv_dict_get_string(d, 'test', false) end, + 'E731: using Dictionary as a String'))) + d = ffi.gc(dict({tes=int(42), t=44, te='43', xx=int(45)}), nil) + alloc_log:clear() + local dis = dict_items(d) + eq(nil, check_emsg(function() return lib.tv_dict_get_string(d, 'test', false) end, + nil)) + local s42 = check_emsg(function() return lib.tv_dict_get_string(d, 'tes', false) end, + nil) + eq('42', ffi.string(s42)) + local s45 = check_emsg(function() return lib.tv_dict_get_string(d, 'xx', false) end, + nil) + eq(s42, s45) + eq('45', ffi.string(s45)) + eq('45', ffi.string(s42)) + local s43 = check_emsg(function() return lib.tv_dict_get_string(d, 'te', false) end, + nil) + eq('43', ffi.string(s43)) + neq(s42, s43) + eq(s43, dis.te.di_tv.vval.v_string) + alloc_log:check({}) + eq('', ffi.string(check_emsg(function() return lib.tv_dict_get_string(d, 't', false) end, + 'E806: using Float as a String'))) + end) + itp('allocates a string copy when requested', function() + local function tv_dict_get_string_alloc(d, key, emsg) + alloc_log:clear() + local ret = check_emsg(function() return lib.tv_dict_get_string(d, key, true) end, + emsg) + local s_ret = (ret ~= nil) and ffi.string(ret) or nil + if not emsg then + if s_ret then + alloc_log:check({a.str(ret, s_ret)}) + else + alloc_log:check({}) + end + end + lib.xfree(ret) + return s_ret + end + local d = ffi.gc(dict({test={}}), nil) + eq('', tv_dict_get_string_alloc(d, 'test', 'E731: using Dictionary as a String')) + d = ffi.gc(dict({tes=int(42), t=44, te='43', xx=int(45)}), nil) + alloc_log:clear() + eq(nil, tv_dict_get_string_alloc(d, 'test')) + eq('42', tv_dict_get_string_alloc(d, 'tes')) + eq('45', tv_dict_get_string_alloc(d, 'xx')) + eq('43', tv_dict_get_string_alloc(d, 'te')) + eq('', tv_dict_get_string_alloc(d, 't', 'E806: using Float as a String')) + end) + end) + describe('get_string_buf()', function() + local function tv_dict_get_string_buf(d, key, buf, emsg) + buf = buf or ffi.gc(lib.xmalloc(lib.NUMBUFLEN), lib.xfree) + alloc_log:clear() + local ret = check_emsg(function() return lib.tv_dict_get_string_buf(d, key, buf) end, + emsg) + local s_ret = (ret ~= nil) and ffi.string(ret) or nil + if not emsg then + alloc_log:check({}) + end + return s_ret, ret, buf + end + itp('works with NULL dict', function() + eq(nil, tv_dict_get_string_buf(nil, 'test')) + end) + itp('works', function() + local lua_d = { + ['']={}, + t=1, + te=int(2), + tes=empty_list, + test='tset', + testt=5, + } + local d = dict(lua_d) + alloc_log:clear() + eq(lua_d, dct2tbl(d)) + alloc_log:check({}) + local s, r, b + s, r, b = tv_dict_get_string_buf(d, 'test') + neq(r, b) + eq('tset', s) + s, r, b = tv_dict_get_string_buf(d, 't', nil, 'E806: using Float as a String') + neq(r, b) + eq('', s) + s, r, b = tv_dict_get_string_buf(d, 'te') + eq(r, b) + eq('2', s) + end) + end) + describe('get_string_buf_chk()', function() + local function tv_dict_get_string_buf_chk(d, key, len, buf, def, emsg) + buf = buf or ffi.gc(lib.xmalloc(lib.NUMBUFLEN), lib.xfree) + def = def or ffi.gc(lib.xstrdup('DEFAULT'), lib.xfree) + len = len or #key + alloc_log:clear() + local ret = check_emsg(function() return lib.tv_dict_get_string_buf_chk(d, key, len, buf, def) end, + emsg) + local s_ret = (ret ~= nil) and ffi.string(ret) or nil + if not emsg then + alloc_log:check({}) + end + return s_ret, ret, buf, def + end + itp('works with NULL dict', function() + eq('DEFAULT', tv_dict_get_string_buf_chk(nil, 'test')) + end) + itp('works', function() + local lua_d = { + ['']={}, + t=1, + te=int(2), + tes=empty_list, + test='tset', + testt=5, + } + local d = dict(lua_d) + alloc_log:clear() + eq(lua_d, dct2tbl(d)) + alloc_log:check({}) + local s, r, b, def + s, r, b, def = tv_dict_get_string_buf_chk(d, 'test') + neq(r, b) + neq(r, def) + eq('tset', s) + s, r, b, def = tv_dict_get_string_buf_chk(d, 'test', 1, nil, nil, 'E806: using Float as a String') + neq(r, b) + neq(r, def) + eq(nil, s) + s, r, b, def = tv_dict_get_string_buf_chk(d, 'te') + eq(r, b) + neq(r, def) + eq('2', s) + s, r, b, def = tv_dict_get_string_buf_chk(d, 'TEST') + eq(r, def) + neq(r, b) + eq('DEFAULT', s) + end) + end) + describe('get_callback()', function() + local function tv_dict_get_callback(d, key, key_len, emsg) + key_len = key_len or #key + local cb = ffi.gc(ffi.cast('Callback*', lib.xmalloc(ffi.sizeof('Callback'))), lib.callback_free) + alloc_log:clear() + local ret = check_emsg(function() + return lib.tv_dict_get_callback(d, key, key_len, cb) + end, emsg) + local cb_lua = callback2tbl(cb[0]) + return cb_lua, ret + end + itp('works with NULL dict', function() + eq({{type='none'}, true}, {tv_dict_get_callback(nil, '')}) + end) + itp('works', function() + local lua_d = { + ['']='tr', + t=int(1), + te={[type_key]=func_type, value='tr'}, + tes={[type_key]=func_type, value='tr', args={'a', 'b'}}, + test={[type_key]=func_type, value='Test', dict={test=1}, args={}}, + testt={[type_key]=func_type, value='Test', dict={test=1}, args={1}}, + } + local d = dict(lua_d) + eq(lua_d, dct2tbl(d)) + eq({{type='fref', fref='tr'}, true}, + {tv_dict_get_callback(d, nil, 0)}) + eq({{type='fref', fref='tr'}, true}, + {tv_dict_get_callback(d, '', -1)}) + eq({{type='none'}, true}, + {tv_dict_get_callback(d, 'x', -1)}) + eq({{type='fref', fref='tr'}, true}, + {tv_dict_get_callback(d, 'testt', 0)}) + eq({{type='none'}, false}, + {tv_dict_get_callback(d, 'test', 1, 'E6000: Argument is not a function or function name')}) + eq({{type='fref', fref='tr'}, true}, + {tv_dict_get_callback(d, 'testt', 2)}) + eq({{ type='pt', fref='tr', pt={ [type_key]=func_type, value='tr', args={ 'a', 'b' } } }, true}, + {tv_dict_get_callback(d, 'testt', 3)}) + eq({{ type='pt', fref='Test', pt={ [type_key]=func_type, value='Test', dict={ test=1 }, args={} } }, true}, + {tv_dict_get_callback(d, 'testt', 4)}) + eq({{ type='pt', fref='Test', pt={ [type_key]=func_type, value='Test', dict={ test=1 }, args={1} } }, true}, + {tv_dict_get_callback(d, 'testt', 5)}) + end) + end) + end) + describe('add', function() + describe('()', function() + itp('works', function() + local di = lib.tv_dict_item_alloc_len('t-est', 5) + alloc_log:check({a.di(di, 't-est')}) + di.di_tv.v_type = lib.VAR_NUMBER + di.di_tv.vval.v_number = 42 + local d = dict({test=10}) + local dis = dict_items(d) + alloc_log:check({ + a.dict(d), + a.di(dis.test, 'test') + }) + eq({test=10}, dct2tbl(d)) + alloc_log:clear() + eq(OK, lib.tv_dict_add(d, di)) + alloc_log:check({}) + eq({test=10, ['t-est']=int(42)}, dct2tbl(d)) + eq(FAIL, check_emsg(function() return lib.tv_dict_add(d, di) end, + 'E685: Internal error: hash_add()')) + end) + end) + describe('list()', function() + itp('works', function() + local l = list(1, 2, 3) + alloc_log:clear() + eq(1, l.lv_refcount) + local d = dict({test=10}) + alloc_log:clear() + eq({test=10}, dct2tbl(d)) + eq(OK, lib.tv_dict_add_list(d, 'testt', 3, l)) + local dis = dict_items(d) + alloc_log:check({a.di(dis.tes, 'tes')}) + eq({test=10, tes={1, 2, 3}}, dct2tbl(d)) + eq(2, l.lv_refcount) + eq(FAIL, check_emsg(function() return lib.tv_dict_add_list(d, 'testt', 3, l) end, + 'E685: Internal error: hash_add()')) + eq(2, l.lv_refcount) + alloc_log:clear() + lib.emsg_skip = lib.emsg_skip + 1 + eq(FAIL, check_emsg(function() return lib.tv_dict_add_list(d, 'testt', 3, l) end, + nil)) + eq(2, l.lv_refcount) + lib.emsg_skip = lib.emsg_skip - 1 + alloc_log:clear_tmp_allocs() + alloc_log:check({}) + end) + end) + describe('dict()', function() + itp('works', function() + local d2 = dict({foo=42}) + alloc_log:clear() + eq(1, d2.dv_refcount) + local d = dict({test=10}) + alloc_log:clear() + eq({test=10}, dct2tbl(d)) + eq(OK, lib.tv_dict_add_dict(d, 'testt', 3, d2)) + local dis = dict_items(d) + alloc_log:check({a.di(dis.tes, 'tes')}) + eq({test=10, tes={foo=42}}, dct2tbl(d)) + eq(2, d2.dv_refcount) + eq(FAIL, check_emsg(function() return lib.tv_dict_add_dict(d, 'testt', 3, d2) end, + 'E685: Internal error: hash_add()')) + eq(2, d2.dv_refcount) + alloc_log:clear() + lib.emsg_skip = lib.emsg_skip + 1 + eq(FAIL, check_emsg(function() return lib.tv_dict_add_dict(d, 'testt', 3, d2) end, + nil)) + eq(2, d2.dv_refcount) + lib.emsg_skip = lib.emsg_skip - 1 + alloc_log:clear_tmp_allocs() + alloc_log:check({}) + end) + end) + describe('nr()', function() + itp('works', function() + local d = dict({test=10}) + alloc_log:clear() + eq({test=10}, dct2tbl(d)) + eq(OK, lib.tv_dict_add_nr(d, 'testt', 3, 2)) + local dis = dict_items(d) + alloc_log:check({a.di(dis.tes, 'tes')}) + eq({test=10, tes=int(2)}, dct2tbl(d)) + eq(FAIL, check_emsg(function() return lib.tv_dict_add_nr(d, 'testt', 3, 2) end, + 'E685: Internal error: hash_add()')) + alloc_log:clear() + lib.emsg_skip = lib.emsg_skip + 1 + eq(FAIL, check_emsg(function() return lib.tv_dict_add_nr(d, 'testt', 3, 2) end, + nil)) + lib.emsg_skip = lib.emsg_skip - 1 + alloc_log:clear_tmp_allocs() + alloc_log:check({}) + end) + end) + describe('str()', function() + itp('works', function() + local d = dict({test=10}) + alloc_log:clear() + eq({test=10}, dct2tbl(d)) + eq(OK, lib.tv_dict_add_str(d, 'testt', 3, 'TEST')) + local dis = dict_items(d) + alloc_log:check({ + a.str(dis.tes.di_tv.vval.v_string, 'TEST'), + a.di(dis.tes, 'tes'), + }) + eq({test=10, tes='TEST'}, dct2tbl(d)) + eq(FAIL, check_emsg(function() return lib.tv_dict_add_str(d, 'testt', 3, 'TEST') end, + 'E685: Internal error: hash_add()')) + alloc_log:clear() + lib.emsg_skip = lib.emsg_skip + 1 + eq(FAIL, check_emsg(function() return lib.tv_dict_add_str(d, 'testt', 3, 'TEST') end, + nil)) + lib.emsg_skip = lib.emsg_skip - 1 + alloc_log:clear_tmp_allocs() + alloc_log:check({}) + end) + end) + describe('allocated_str()', function() + itp('works', function() + local d = dict({test=10}) + eq({test=10}, dct2tbl(d)) + alloc_log:clear() + local s1 = lib.xstrdup('TEST') + local s2 = lib.xstrdup('TEST') + local s3 = lib.xstrdup('TEST') + alloc_log:check({ + a.str(s1, 'TEST'), + a.str(s2, 'TEST'), + a.str(s3, 'TEST'), + }) + eq(OK, lib.tv_dict_add_allocated_str(d, 'testt', 3, s1)) + local dis = dict_items(d) + alloc_log:check({ + a.di(dis.tes, 'tes'), + }) + eq({test=10, tes='TEST'}, dct2tbl(d)) + eq(FAIL, check_emsg(function() return lib.tv_dict_add_allocated_str(d, 'testt', 3, s2) end, + 'E685: Internal error: hash_add()')) + alloc_log:clear() + lib.emsg_skip = lib.emsg_skip + 1 + eq(FAIL, check_emsg(function() return lib.tv_dict_add_allocated_str(d, 'testt', 3, s3) end, + nil)) + lib.emsg_skip = lib.emsg_skip - 1 + alloc_log:clear_tmp_allocs() + alloc_log:check({ + a.freed(s3), + }) + end) + end) + end) + describe('clear()', function() + itp('works', function() + local d = dict() + alloc_log:check({a.dict(d)}) + eq({}, dct2tbl(d)) + lib.tv_dict_clear(d) + eq({}, dct2tbl(d)) + lib.tv_dict_add_str(d, 'TEST', 3, 'tEsT') + local dis = dict_items(d) + local di = dis.TES + local di_s = di.di_tv.vval.v_string + alloc_log:check({a.str(di_s), a.di(di)}) + eq({TES='tEsT'}, dct2tbl(d)) + lib.tv_dict_clear(d) + alloc_log:check({a.freed(di_s), a.freed(di)}) + eq({}, dct2tbl(d)) + end) + end) + describe('extend()', function() + local function tv_dict_extend(d1, d2, action, emsg) + action = action or "force" + check_emsg(function() return lib.tv_dict_extend(d1, d2, action) end, emsg) + end + itp('works', function() + local d1 = dict() + alloc_log:check({a.dict(d1)}) + eq({}, dct2tbl(d1)) + local d2 = dict() + alloc_log:check({a.dict(d2)}) + eq({}, dct2tbl(d2)) + tv_dict_extend(d1, d2, 'error') + tv_dict_extend(d1, d2, 'keep') + tv_dict_extend(d1, d2, 'force') + alloc_log:check({}) + + d1 = dict({a='TEST'}) + eq({a='TEST'}, dct2tbl(d1)) + local dis1 = dict_items(d1) + local a1_s = dis1.a.di_tv.vval.v_string + alloc_log:clear_tmp_allocs() + alloc_log:check({ + a.dict(d1), + a.di(dis1.a), + a.str(a1_s), + }) + d2 = dict({a='TSET'}) + eq({a='TSET'}, dct2tbl(d2)) + local dis2 = dict_items(d2) + local a2_s = dis2.a.di_tv.vval.v_string + alloc_log:clear_tmp_allocs() + alloc_log:check({ + a.dict(d2), + a.di(dis2.a), + a.str(a2_s), + }) + + tv_dict_extend(d1, d2, 'error', 'E737: Key already exists: a') + eq({a='TEST'}, dct2tbl(d1)) + eq({a='TSET'}, dct2tbl(d2)) + alloc_log:clear() + + tv_dict_extend(d1, d2, 'keep') + alloc_log:check({}) + eq({a='TEST'}, dct2tbl(d1)) + eq({a='TSET'}, dct2tbl(d2)) + + tv_dict_extend(d1, d2, 'force') + alloc_log:check({ + a.freed(a1_s), + a.str(dis1.a.di_tv.vval.v_string), + }) + eq({a='TSET'}, dct2tbl(d1)) + eq({a='TSET'}, dct2tbl(d2)) + end) + itp('disallows overriding builtin or user functions', function() + local d = dict() + d.dv_scope = lib.VAR_DEF_SCOPE + local f_lua = { + [type_key]=func_type, + value='tr', + } + local f_tv = lua2typvalt(f_lua) + local p_lua = { + [type_key]=func_type, + value='tr', + args={1}, + } + local p_tv = lua2typvalt(p_lua) + eq(lib.VAR_PARTIAL, p_tv.v_type) + local d2 = dict({tr=f_tv}) + local d3 = dict({tr=p_tv}) + local d4 = dict({['TEST:THIS']=p_tv}) + local d5 = dict({Test=f_tv}) + local d6 = dict({Test=p_tv}) + eval0([[execute("function Test()\nendfunction")]]) + tv_dict_extend(d, d2, 'force', + 'E704: Funcref variable name must start with a capital: tr') + tv_dict_extend(d, d3, 'force', + 'E704: Funcref variable name must start with a capital: tr') + tv_dict_extend(d, d4, 'force', + 'E461: Illegal variable name: TEST:THIS') + tv_dict_extend(d, d5, 'force', + 'E705: Variable name conflicts with existing function: Test') + tv_dict_extend(d, d6, 'force', + 'E705: Variable name conflicts with existing function: Test') + eq({}, dct2tbl(d)) + d.dv_scope = lib.VAR_SCOPE + tv_dict_extend(d, d4, 'force', + 'E461: Illegal variable name: TEST:THIS') + eq({}, dct2tbl(d)) + tv_dict_extend(d, d2, 'force') + eq({tr=f_lua}, dct2tbl(d)) + tv_dict_extend(d, d3, 'force') + eq({tr=p_lua}, dct2tbl(d)) + tv_dict_extend(d, d5, 'force') + eq({tr=p_lua, Test=f_lua}, dct2tbl(d)) + tv_dict_extend(d, d6, 'force') + eq({tr=p_lua, Test=p_lua}, dct2tbl(d)) + end) + itp('cares about locks and read-only items', function() + local d_lua = {tv_locked=1, tv_fixed=2, di_ro=3, di_ro_sbx=4} + local d = dict(d_lua) + local dis = dict_items(d) + dis.tv_locked.di_tv.v_lock = lib.VAR_LOCKED + dis.tv_fixed.di_tv.v_lock = lib.VAR_FIXED + dis.di_ro.di_flags = bit.bor(dis.di_ro.di_flags, lib.DI_FLAGS_RO) + dis.di_ro_sbx.di_flags = bit.bor(dis.di_ro_sbx.di_flags, lib.DI_FLAGS_RO_SBX) + lib.sandbox = true + local d1 = dict({tv_locked=41}) + local d2 = dict({tv_fixed=42}) + local d3 = dict({di_ro=43}) + local d4 = dict({di_ro_sbx=44}) + tv_dict_extend(d, d1, 'force', 'E741: Value is locked: extend() argument') + tv_dict_extend(d, d2, 'force', 'E742: Cannot change value of extend() argument') + tv_dict_extend(d, d3, 'force', 'E46: Cannot change read-only variable "extend() argument"') + tv_dict_extend(d, d4, 'force', 'E794: Cannot set variable in the sandbox: "extend() argument"') + eq(d_lua, dct2tbl(d)) + lib.sandbox = false + tv_dict_extend(d, d4, 'force') + d_lua.di_ro_sbx = 44 + eq(d_lua, dct2tbl(d)) + end) + end) + describe('equal()', function() + local function tv_dict_equal(d1, d2, ic, recursive) + return lib.tv_dict_equal(d1, d2, ic or false, recursive or false) + end + itp('works', function() + eq(true, tv_dict_equal(nil, nil)) + local d1 = dict() + alloc_log:check({a.dict(d1)}) + eq(1, d1.dv_refcount) + eq(false, tv_dict_equal(nil, d1)) + eq(false, tv_dict_equal(d1, nil)) + eq(true, tv_dict_equal(d1, d1)) + eq(1, d1.dv_refcount) + alloc_log:check({}) + local d_upper = dict({a='TEST'}) + local dis_upper = dict_items(d_upper) + local d_lower = dict({a='test'}) + local dis_lower = dict_items(d_lower) + local d_kupper_upper = dict({A='TEST'}) + local dis_kupper_upper = dict_items(d_kupper_upper) + local d_kupper_lower = dict({A='test'}) + local dis_kupper_lower = dict_items(d_kupper_lower) + alloc_log:clear_tmp_allocs() + alloc_log:check({ + a.dict(d_upper), + a.di(dis_upper.a), + a.str(dis_upper.a.di_tv.vval.v_string), + + a.dict(d_lower), + a.di(dis_lower.a), + a.str(dis_lower.a.di_tv.vval.v_string), + + a.dict(d_kupper_upper), + a.di(dis_kupper_upper.A), + a.str(dis_kupper_upper.A.di_tv.vval.v_string), + + a.dict(d_kupper_lower), + a.di(dis_kupper_lower.A), + a.str(dis_kupper_lower.A.di_tv.vval.v_string), + }) + eq(true, tv_dict_equal(d_upper, d_upper)) + eq(true, tv_dict_equal(d_upper, d_upper, true)) + eq(false, tv_dict_equal(d_upper, d_lower, false)) + eq(true, tv_dict_equal(d_upper, d_lower, true)) + eq(true, tv_dict_equal(d_kupper_upper, d_kupper_lower, true)) + eq(false, tv_dict_equal(d_kupper_upper, d_lower, true)) + eq(false, tv_dict_equal(d_kupper_upper, d_upper, true)) + eq(true, tv_dict_equal(d_upper, d_upper, true, true)) + alloc_log:check({}) + end) + end) + describe('copy()', function() + local function tv_dict_copy(...) + return ffi.gc(lib.tv_dict_copy(...), lib.tv_dict_unref) + end + itp('copies NULL correctly', function() + eq(nil, lib.tv_dict_copy(nil, nil, true, 0)) + eq(nil, lib.tv_dict_copy(nil, nil, false, 0)) + eq(nil, lib.tv_dict_copy(nil, nil, true, 1)) + eq(nil, lib.tv_dict_copy(nil, nil, false, 1)) + end) + itp('copies dict correctly without converting items', function() + do + local v = {a={['«']='»'}, b={'„'}, ['1']=1, ['«»']='“', ns=null_string, nl=null_list, nd=null_dict} + local d_tv = lua2typvalt(v) + local d = d_tv.vval.v_dict + local dis = dict_items(d) + alloc_log:clear() + + eq(1, dis.a.di_tv.vval.v_dict.dv_refcount) + eq(1, dis.b.di_tv.vval.v_list.lv_refcount) + local d_copy1 = tv_dict_copy(nil, d, false, 0) + eq(2, dis.a.di_tv.vval.v_dict.dv_refcount) + eq(2, dis.b.di_tv.vval.v_list.lv_refcount) + local dis_copy1 = dict_items(d_copy1) + eq(dis.a.di_tv.vval.v_dict, dis_copy1.a.di_tv.vval.v_dict) + eq(dis.b.di_tv.vval.v_list, dis_copy1.b.di_tv.vval.v_list) + eq(v, dct2tbl(d_copy1)) + alloc_log:clear() + lib.tv_dict_free(ffi.gc(d_copy1, nil)) + alloc_log:clear() + + eq(1, dis.a.di_tv.vval.v_dict.dv_refcount) + eq(1, dis.b.di_tv.vval.v_list.lv_refcount) + local d_deepcopy1 = tv_dict_copy(nil, d, true, 0) + neq(nil, d_deepcopy1) + eq(1, dis.a.di_tv.vval.v_dict.dv_refcount) + eq(1, dis.b.di_tv.vval.v_list.lv_refcount) + local dis_deepcopy1 = dict_items(d_deepcopy1) + neq(dis.a.di_tv.vval.v_dict, dis_deepcopy1.a.di_tv.vval.v_dict) + neq(dis.b.di_tv.vval.v_list, dis_deepcopy1.b.di_tv.vval.v_list) + eq(v, dct2tbl(d_deepcopy1)) + alloc_log:clear() + end + collectgarbage() + end) + itp('copies dict correctly and converts items', function() + local vc = vimconv_alloc() + -- UTF-8 ↔ latin1 conversions need no iconv + eq(OK, lib.convert_setup(vc, to_cstr('utf-8'), to_cstr('latin1'))) + + local v = {a={['«']='»'}, b={'„'}, ['1']=1, ['«»']='“', ns=null_string, nl=null_list, nd=null_dict} + local d_tv = lua2typvalt(v) + local d = d_tv.vval.v_dict + local dis = dict_items(d) + alloc_log:clear() + + eq(1, dis.a.di_tv.vval.v_dict.dv_refcount) + eq(1, dis.b.di_tv.vval.v_list.lv_refcount) + local d_deepcopy1 = tv_dict_copy(vc, d, true, 0) + neq(nil, d_deepcopy1) + eq(1, dis.a.di_tv.vval.v_dict.dv_refcount) + eq(1, dis.b.di_tv.vval.v_list.lv_refcount) + local dis_deepcopy1 = dict_items(d_deepcopy1) + neq(dis.a.di_tv.vval.v_dict, dis_deepcopy1.a.di_tv.vval.v_dict) + neq(dis.b.di_tv.vval.v_list, dis_deepcopy1.b.di_tv.vval.v_list) + eq({a={['\171']='\187'}, b={'\191'}, ['1']=1, ['\171\187']='\191', ns=null_string, nl=null_list, nd=null_dict}, + dct2tbl(d_deepcopy1)) + alloc_log:clear_tmp_allocs() + alloc_log:clear() + end) + itp('returns different/same containers with(out) copyID', function() + local d_inner_tv = lua2typvalt({}) + local d_tv = lua2typvalt({a=d_inner_tv, b=d_inner_tv}) + eq(3, d_inner_tv.vval.v_dict.dv_refcount) + local d = d_tv.vval.v_dict + local dis = dict_items(d) + eq(dis.a.di_tv.vval.v_dict, dis.b.di_tv.vval.v_dict) + + local d_copy1 = tv_dict_copy(nil, d, true, 0) + local dis_copy1 = dict_items(d_copy1) + neq(dis_copy1.a.di_tv.vval.v_dict, dis_copy1.b.di_tv.vval.v_dict) + eq({a={}, b={}}, dct2tbl(d_copy1)) + + local d_copy2 = tv_dict_copy(nil, d, true, 2) + local dis_copy2 = dict_items(d_copy2) + eq(dis_copy2.a.di_tv.vval.v_dict, dis_copy2.b.di_tv.vval.v_dict) + eq({a={}, b={}}, dct2tbl(d_copy2)) + + eq(3, d_inner_tv.vval.v_dict.dv_refcount) + end) + itp('works with self-referencing dict with copyID', function() + local d_tv = lua2typvalt({}) + local d = d_tv.vval.v_dict + eq(1, d.dv_refcount) + lib.tv_dict_add_dict(d, 'test', 4, d) + eq(2, d.dv_refcount) + + local d_copy1 = tv_dict_copy(nil, d, true, 2) + eq(2, d_copy1.dv_refcount) + local v = {} + v.test = v + eq(v, dct2tbl(d_copy1)) + + lib.tv_dict_clear(d) + eq(1, d.dv_refcount) + + lib.tv_dict_clear(d_copy1) + eq(1, d_copy1.dv_refcount) + end) + end) + describe('set_keys_readonly()', function() + itp('works', function() + local d = dict({a=true}) + local dis = dict_items(d) + alloc_log:check({a.dict(d), a.di(dis.a)}) + eq(0, bit.band(dis.a.di_flags, lib.DI_FLAGS_RO)) + eq(0, bit.band(dis.a.di_flags, lib.DI_FLAGS_FIX)) + lib.tv_dict_set_keys_readonly(d) + alloc_log:check({}) + eq(lib.DI_FLAGS_RO, bit.band(dis.a.di_flags, lib.DI_FLAGS_RO)) + eq(lib.DI_FLAGS_FIX, bit.band(dis.a.di_flags, lib.DI_FLAGS_FIX)) + end) + end) + end) + describe('tv', function() + describe('alloc', function() + describe('list ret()', function() + itp('works', function() + local rettv = typvalt(lib.VAR_UNKNOWN) + local l = lib.tv_list_alloc_ret(rettv) + eq(empty_list, typvalt2lua(rettv)) + eq(rettv.vval.v_list, l) + end) + end) + describe('dict ret()', function() + itp('works', function() + local rettv = typvalt(lib.VAR_UNKNOWN) + lib.tv_dict_alloc_ret(rettv) + eq({}, typvalt2lua(rettv)) + end) + end) + end) + local function defalloc() + return {} + end + describe('clear()', function() + itp('works', function() + local function deffrees(alloc_rets) + local ret = {} + for i = #alloc_rets, 1, -1 do + ret[#alloc_rets - i + 1] = alloc_rets:freed(i) + end + return ret + end + alloc_log:check({}) + lib.tv_clear(nil) + alloc_log:check({}) + local ll = {} + local ll_l = nil + ll[1] = ll + local dd = {} + local dd_d = nil + dd.dd = dd + for _, v in ipairs({ + {nil_value}, + {null_string, nil, function() return {a.freed(alloc_log.null)} end}, + {0}, + {int(0)}, + {true}, + {false}, + {'true', function(tv) return {a.str(tv.vval.v_string)} end}, + {{}, function(tv) return {a.dict(tv.vval.v_dict)} end}, + {empty_list, function(tv) return {a.list(tv.vval.v_list)} end}, + {ll, function(tv) + ll_l = tv.vval.v_list + return {a.list(tv.vval.v_list), a.li(tv.vval.v_list.lv_first)} + end, defalloc}, + {dd, function(tv) + dd_d = tv.vval.v_dict + return {a.dict(tv.vval.v_dict), a.di(first_di(tv.vval.v_dict))} + end, defalloc}, + }) do + local tv = lua2typvalt(v[1]) + local alloc_rets = {} + alloc_log:check(get_alloc_rets((v[2] or defalloc)(tv), alloc_rets)) + lib.tv_clear(tv) + alloc_log:check((v[3] or deffrees)(alloc_rets)) + end + eq(1, ll_l.lv_refcount) + eq(1, dd_d.dv_refcount) + end) + end) + describe('copy()', function() + itp('works', function() + local function strallocs(tv) + return {a.str(tv.vval.v_string)} + end + for _, v in ipairs({ + {nil_value}, + {null_string}, + {0}, + {int(0)}, + {true}, + {false}, + {{}, function(tv) return {a.dict(tv.vval.v_dict)} end, nil, function(from, to) + eq(2, to.vval.v_dict.dv_refcount) + eq(to.vval.v_dict, from.vval.v_dict) + end}, + {empty_list, function(tv) return {a.list(tv.vval.v_list)} end, nil, function(from, to) + eq(2, to.vval.v_list.lv_refcount) + eq(to.vval.v_list, from.vval.v_list) + end}, + {'test', strallocs, strallocs, function(from, to) + neq(to.vval.v_string, from.vval.v_string) + end}, + }) do + local from = lua2typvalt(v[1]) + alloc_log:check((v[2] or defalloc)(from)) + local to = typvalt(lib.VAR_UNKNOWN) + lib.tv_copy(from, to) + local res = v[1] + eq(res, typvalt2lua(to)) + alloc_log:check((v[3] or defalloc)(to)) + if v[4] then + v[4](from, to) + end + end + end) + end) + describe('item_lock()', function() + itp('does not alter VAR_PARTIAL', function() + local p_tv = lua2typvalt({ + [type_key]=func_type, + value='tr', + dict={}, + }) + lib.tv_item_lock(p_tv, -1, true) + eq(lib.VAR_UNLOCKED, p_tv.vval.v_partial.pt_dict.dv_lock) + end) + itp('does not change VAR_FIXED values', function() + local d_tv = lua2typvalt({}) + local l_tv = lua2typvalt(empty_list) + alloc_log:clear() + d_tv.v_lock = lib.VAR_FIXED + d_tv.vval.v_dict.dv_lock = lib.VAR_FIXED + l_tv.v_lock = lib.VAR_FIXED + l_tv.vval.v_list.lv_lock = lib.VAR_FIXED + lib.tv_item_lock(d_tv, 1, true) + lib.tv_item_lock(l_tv, 1, true) + eq(lib.VAR_FIXED, d_tv.v_lock) + eq(lib.VAR_FIXED, l_tv.v_lock) + eq(lib.VAR_FIXED, d_tv.vval.v_dict.dv_lock) + eq(lib.VAR_FIXED, l_tv.vval.v_list.lv_lock) + lib.tv_item_lock(d_tv, 1, false) + lib.tv_item_lock(l_tv, 1, false) + eq(lib.VAR_FIXED, d_tv.v_lock) + eq(lib.VAR_FIXED, l_tv.v_lock) + eq(lib.VAR_FIXED, d_tv.vval.v_dict.dv_lock) + eq(lib.VAR_FIXED, l_tv.vval.v_list.lv_lock) + alloc_log:check({}) + end) + itp('works with NULL values', function() + local l_tv = lua2typvalt(null_list) + local d_tv = lua2typvalt(null_dict) + local s_tv = lua2typvalt(null_string) + alloc_log:clear() + lib.tv_item_lock(l_tv, 1, true) + lib.tv_item_lock(d_tv, 1, true) + lib.tv_item_lock(s_tv, 1, true) + eq(null_list, typvalt2lua(l_tv)) + eq(null_dict, typvalt2lua(d_tv)) + eq(null_string, typvalt2lua(s_tv)) + eq(lib.VAR_LOCKED, d_tv.v_lock) + eq(lib.VAR_LOCKED, l_tv.v_lock) + eq(lib.VAR_LOCKED, s_tv.v_lock) + alloc_log:check({}) + end) + end) + describe('islocked()', function() + itp('works with NULL values', function() + local l_tv = lua2typvalt(null_list) + local d_tv = lua2typvalt(null_dict) + eq(false, lib.tv_islocked(l_tv)) + eq(false, lib.tv_islocked(d_tv)) + end) + itp('works', function() + local tv = lua2typvalt() + local d_tv = lua2typvalt({}) + local l_tv = lua2typvalt(empty_list) + alloc_log:clear() + eq(false, lib.tv_islocked(tv)) + eq(false, lib.tv_islocked(l_tv)) + eq(false, lib.tv_islocked(d_tv)) + d_tv.vval.v_dict.dv_lock = lib.VAR_LOCKED + l_tv.vval.v_list.lv_lock = lib.VAR_LOCKED + eq(true, lib.tv_islocked(l_tv)) + eq(true, lib.tv_islocked(d_tv)) + tv.v_lock = lib.VAR_LOCKED + d_tv.v_lock = lib.VAR_LOCKED + l_tv.v_lock = lib.VAR_LOCKED + eq(true, lib.tv_islocked(tv)) + eq(true, lib.tv_islocked(l_tv)) + eq(true, lib.tv_islocked(d_tv)) + d_tv.vval.v_dict.dv_lock = lib.VAR_UNLOCKED + l_tv.vval.v_list.lv_lock = lib.VAR_UNLOCKED + eq(true, lib.tv_islocked(tv)) + eq(true, lib.tv_islocked(l_tv)) + eq(true, lib.tv_islocked(d_tv)) + tv.v_lock = lib.VAR_FIXED + d_tv.v_lock = lib.VAR_FIXED + l_tv.v_lock = lib.VAR_FIXED + eq(false, lib.tv_islocked(tv)) + eq(false, lib.tv_islocked(l_tv)) + eq(false, lib.tv_islocked(d_tv)) + d_tv.vval.v_dict.dv_lock = lib.VAR_LOCKED + l_tv.vval.v_list.lv_lock = lib.VAR_LOCKED + eq(true, lib.tv_islocked(l_tv)) + eq(true, lib.tv_islocked(d_tv)) + d_tv.vval.v_dict.dv_lock = lib.VAR_FIXED + l_tv.vval.v_list.lv_lock = lib.VAR_FIXED + eq(false, lib.tv_islocked(l_tv)) + eq(false, lib.tv_islocked(d_tv)) + alloc_log:check({}) + end) + end) + describe('check_lock()', function() + local function tv_check_lock(lock, name, name_len, emsg) + return check_emsg(function() + return lib.tv_check_lock(lock, name, name_len) + end, emsg) + end + itp('works', function() + eq(false, tv_check_lock(lib.VAR_UNLOCKED, 'test', 3)) + eq(true, tv_check_lock(lib.VAR_LOCKED, 'test', 3, + 'E741: Value is locked: tes')) + eq(true, tv_check_lock(lib.VAR_FIXED, 'test', 3, + 'E742: Cannot change value of tes')) + eq(true, tv_check_lock(lib.VAR_LOCKED, nil, 0, + 'E741: Value is locked: Unknown')) + eq(true, tv_check_lock(lib.VAR_FIXED, nil, 0, + 'E742: Cannot change value of Unknown')) + eq(true, tv_check_lock(lib.VAR_LOCKED, nil, lib.kTVCstring, + 'E741: Value is locked: Unknown')) + eq(true, tv_check_lock(lib.VAR_FIXED, 'test', lib.kTVCstring, + 'E742: Cannot change value of test')) + end) + end) + describe('equal()', function() + itp('compares empty and NULL lists correctly', function() + local l = lua2typvalt(empty_list) + local l2 = lua2typvalt(empty_list) + local nl = lua2typvalt(null_list) + + -- NULL lists are not equal to empty lists + eq(false, lib.tv_equal(l, nl, true, false)) + eq(false, lib.tv_equal(nl, l, false, false)) + eq(false, lib.tv_equal(nl, l, false, true)) + eq(false, lib.tv_equal(l, nl, true, true)) + + -- Yet NULL lists are equal themselves + eq(true, lib.tv_equal(nl, nl, true, false)) + eq(true, lib.tv_equal(nl, nl, false, false)) + eq(true, lib.tv_equal(nl, nl, false, true)) + eq(true, lib.tv_equal(nl, nl, true, true)) + + -- As well as empty lists + eq(true, lib.tv_equal(l, l, true, false)) + eq(true, lib.tv_equal(l, l2, false, false)) + eq(true, lib.tv_equal(l2, l, false, true)) + eq(true, lib.tv_equal(l2, l2, true, true)) + end) + -- Must not use recursive=true argument in the following tests because it + -- indicates that tv_equal_recurse_limit and recursive_cnt were set which + -- is essential. This argument will be set when comparing inner lists. + itp('compares lists correctly when case is not ignored', function() + local l1 = lua2typvalt({'abc', {1, 2, 'Abc'}, 'def'}) + local l2 = lua2typvalt({'abc', {1, 2, 'Abc'}}) + local l3 = lua2typvalt({'abc', {1, 2, 'Abc'}, 'Def'}) + local l4 = lua2typvalt({'abc', {1, 2, 'Abc', 4}, 'def'}) + local l5 = lua2typvalt({'Abc', {1, 2, 'Abc'}, 'def'}) + local l6 = lua2typvalt({'abc', {1, 2, 'Abc'}, 'def'}) + local l7 = lua2typvalt({'abc', {1, 2, 'abc'}, 'def'}) + local l8 = lua2typvalt({'abc', nil, 'def'}) + local l9 = lua2typvalt({'abc', {1, 2, nil}, 'def'}) + + eq(true, lib.tv_equal(l1, l1, false, false)) + eq(false, lib.tv_equal(l1, l2, false, false)) + eq(false, lib.tv_equal(l1, l3, false, false)) + eq(false, lib.tv_equal(l1, l4, false, false)) + eq(false, lib.tv_equal(l1, l5, false, false)) + eq(true, lib.tv_equal(l1, l6, false, false)) + eq(false, lib.tv_equal(l1, l7, false, false)) + eq(false, lib.tv_equal(l1, l8, false, false)) + eq(false, lib.tv_equal(l1, l9, false, false)) + end) + itp('compares lists correctly when case is ignored', function() + local l1 = lua2typvalt({'abc', {1, 2, 'Abc'}, 'def'}) + local l2 = lua2typvalt({'abc', {1, 2, 'Abc'}}) + local l3 = lua2typvalt({'abc', {1, 2, 'Abc'}, 'Def'}) + local l4 = lua2typvalt({'abc', {1, 2, 'Abc', 4}, 'def'}) + local l5 = lua2typvalt({'Abc', {1, 2, 'Abc'}, 'def'}) + local l6 = lua2typvalt({'abc', {1, 2, 'Abc'}, 'def'}) + local l7 = lua2typvalt({'abc', {1, 2, 'abc'}, 'def'}) + local l8 = lua2typvalt({'abc', nil, 'def'}) + local l9 = lua2typvalt({'abc', {1, 2, nil}, 'def'}) + + eq(true, lib.tv_equal(l1, l1, true, false)) + eq(false, lib.tv_equal(l1, l2, true, false)) + eq(true, lib.tv_equal(l1, l3, true, false)) + eq(false, lib.tv_equal(l1, l4, true, false)) + eq(true, lib.tv_equal(l1, l5, true, false)) + eq(true, lib.tv_equal(l1, l6, true, false)) + eq(true, lib.tv_equal(l1, l7, true, false)) + eq(false, lib.tv_equal(l1, l8, true, false)) + eq(false, lib.tv_equal(l1, l9, true, false)) + end) + local function tv_equal(d1, d2, ic, recursive) + return lib.tv_equal(d1, d2, ic or false, recursive or false) + end + itp('works with dictionaries', function() + local nd = lua2typvalt(null_dict) + eq(true, tv_equal(nd, nd)) + alloc_log:check({}) + local d1 = lua2typvalt({}) + alloc_log:check({a.dict(d1.vval.v_dict)}) + eq(1, d1.vval.v_dict.dv_refcount) + eq(false, tv_equal(nd, d1)) + eq(false, tv_equal(d1, nd)) + eq(true, tv_equal(d1, d1)) + eq(1, d1.vval.v_dict.dv_refcount) + alloc_log:check({}) + local d_upper = lua2typvalt({a='TEST'}) + local dis_upper = dict_items(d_upper.vval.v_dict) + local d_lower = lua2typvalt({a='test'}) + local dis_lower = dict_items(d_lower.vval.v_dict) + local d_kupper_upper = lua2typvalt({A='TEST'}) + local dis_kupper_upper = dict_items(d_kupper_upper.vval.v_dict) + local d_kupper_lower = lua2typvalt({A='test'}) + local dis_kupper_lower = dict_items(d_kupper_lower.vval.v_dict) + alloc_log:clear_tmp_allocs() + alloc_log:check({ + a.dict(d_upper.vval.v_dict), + a.di(dis_upper.a), + a.str(dis_upper.a.di_tv.vval.v_string), + + a.dict(d_lower.vval.v_dict), + a.di(dis_lower.a), + a.str(dis_lower.a.di_tv.vval.v_string), + + a.dict(d_kupper_upper.vval.v_dict), + a.di(dis_kupper_upper.A), + a.str(dis_kupper_upper.A.di_tv.vval.v_string), + + a.dict(d_kupper_lower.vval.v_dict), + a.di(dis_kupper_lower.A), + a.str(dis_kupper_lower.A.di_tv.vval.v_string), + }) + eq(true, tv_equal(d_upper, d_upper)) + eq(true, tv_equal(d_upper, d_upper, true)) + eq(false, tv_equal(d_upper, d_lower, false)) + eq(true, tv_equal(d_upper, d_lower, true)) + eq(true, tv_equal(d_kupper_upper, d_kupper_lower, true)) + eq(false, tv_equal(d_kupper_upper, d_lower, true)) + eq(false, tv_equal(d_kupper_upper, d_upper, true)) + eq(true, tv_equal(d_upper, d_upper, true, true)) + alloc_log:check({}) + end) + end) + describe('check', function() + describe('str_or_nr()', function() + itp('works', function() + local tv = typvalt() + local mem = lib.xmalloc(1) + tv.vval.v_list = mem -- Should crash when actually accessed + alloc_log:clear() + for _, v in ipairs({ + {lib.VAR_NUMBER, nil}, + {lib.VAR_FLOAT, 'E805: Expected a Number or a String, Float found'}, + {lib.VAR_PARTIAL, 'E703: Expected a Number or a String, Funcref found'}, + {lib.VAR_FUNC, 'E703: Expected a Number or a String, Funcref found'}, + {lib.VAR_LIST, 'E745: Expected a Number or a String, List found'}, + {lib.VAR_DICT, 'E728: Expected a Number or a String, Dictionary found'}, + {lib.VAR_SPECIAL, 'E5300: Expected a Number or a String'}, + {lib.VAR_UNKNOWN, 'E685: Internal error: tv_check_str_or_nr(UNKNOWN)'}, + }) do + local typ = v[1] + local emsg = v[2] + local ret = true + if emsg then ret = false end + tv.v_type = typ + eq(ret, check_emsg(function() return lib.tv_check_str_or_nr(tv) end, emsg)) + if emsg then + alloc_log:clear() + else + alloc_log:check({}) + end + end + end) + end) + describe('num()', function() + itp('works', function() + local tv = typvalt() + local mem = lib.xmalloc(1) + tv.vval.v_list = mem -- Should crash when actually accessed + alloc_log:clear() + for _, v in ipairs({ + {lib.VAR_NUMBER, nil}, + {lib.VAR_FLOAT, 'E805: Using a Float as a Number'}, + {lib.VAR_PARTIAL, 'E703: Using a Funcref as a Number'}, + {lib.VAR_FUNC, 'E703: Using a Funcref as a Number'}, + {lib.VAR_LIST, 'E745: Using a List as a Number'}, + {lib.VAR_DICT, 'E728: Using a Dictionary as a Number'}, + {lib.VAR_SPECIAL, nil}, + {lib.VAR_UNKNOWN, 'E685: using an invalid value as a Number'}, + }) do + local typ = v[1] + local emsg = v[2] + local ret = true + if emsg then ret = false end + tv.v_type = typ + eq(ret, check_emsg(function() return lib.tv_check_num(tv) end, emsg)) + if emsg then + alloc_log:clear() + else + alloc_log:check({}) + end + end + end) + end) + describe('str()', function() + itp('works', function() + local tv = typvalt() + local mem = lib.xmalloc(1) + tv.vval.v_list = mem -- Should crash when actually accessed + alloc_log:clear() + for _, v in ipairs({ + {lib.VAR_NUMBER, nil}, + {lib.VAR_FLOAT, 'E806: using Float as a String'}, + {lib.VAR_PARTIAL, 'E729: using Funcref as a String'}, + {lib.VAR_FUNC, 'E729: using Funcref as a String'}, + {lib.VAR_LIST, 'E730: using List as a String'}, + {lib.VAR_DICT, 'E731: using Dictionary as a String'}, + {lib.VAR_SPECIAL, nil}, + {lib.VAR_UNKNOWN, 'E908: using an invalid value as a String'}, + }) do + local typ = v[1] + local emsg = v[2] + local ret = true + if emsg then ret = false end + tv.v_type = typ + eq(ret, check_emsg(function() return lib.tv_check_str(tv) end, emsg)) + if emsg then + alloc_log:clear() + else + alloc_log:check({}) + end + end + end) + end) + end) + describe('get', function() + describe('number()', function() + itp('works', function() + for _, v in ipairs({ + {lib.VAR_NUMBER, {v_number=42}, nil, 42}, + {lib.VAR_STRING, {v_string=to_cstr('100500')}, nil, 100500}, + {lib.VAR_FLOAT, {v_float=42.53}, 'E805: Using a Float as a Number', 0}, + {lib.VAR_PARTIAL, {v_partial=NULL}, 'E703: Using a Funcref as a Number', 0}, + {lib.VAR_FUNC, {v_string=NULL}, 'E703: Using a Funcref as a Number', 0}, + {lib.VAR_LIST, {v_list=NULL}, 'E745: Using a List as a Number', 0}, + {lib.VAR_DICT, {v_dict=NULL}, 'E728: Using a Dictionary as a Number', 0}, + {lib.VAR_SPECIAL, {v_special=lib.kSpecialVarNull}, nil, 0}, + {lib.VAR_SPECIAL, {v_special=lib.kSpecialVarTrue}, nil, 1}, + {lib.VAR_SPECIAL, {v_special=lib.kSpecialVarFalse}, nil, 0}, + {lib.VAR_UNKNOWN, nil, 'E685: Internal error: tv_get_number(UNKNOWN)', 0}, + }) do + -- Using to_cstr, cannot free with tv_clear + local tv = ffi.gc(typvalt(v[1], v[2]), nil) + alloc_log:check({}) + local emsg = v[3] + local ret = v[4] + eq(ret, check_emsg(function() return lib.tv_get_number(tv) end, emsg)) + if emsg then + alloc_log:clear() + else + alloc_log:check({}) + end + end + end) + end) + describe('number_chk()', function() + itp('works', function() + for _, v in ipairs({ + {lib.VAR_NUMBER, {v_number=42}, nil, 42}, + {lib.VAR_STRING, {v_string=to_cstr('100500')}, nil, 100500}, + {lib.VAR_FLOAT, {v_float=42.53}, 'E805: Using a Float as a Number', 0}, + {lib.VAR_PARTIAL, {v_partial=NULL}, 'E703: Using a Funcref as a Number', 0}, + {lib.VAR_FUNC, {v_string=NULL}, 'E703: Using a Funcref as a Number', 0}, + {lib.VAR_LIST, {v_list=NULL}, 'E745: Using a List as a Number', 0}, + {lib.VAR_DICT, {v_dict=NULL}, 'E728: Using a Dictionary as a Number', 0}, + {lib.VAR_SPECIAL, {v_special=lib.kSpecialVarNull}, nil, 0}, + {lib.VAR_SPECIAL, {v_special=lib.kSpecialVarTrue}, nil, 1}, + {lib.VAR_SPECIAL, {v_special=lib.kSpecialVarFalse}, nil, 0}, + {lib.VAR_UNKNOWN, nil, 'E685: Internal error: tv_get_number(UNKNOWN)', 0}, + }) do + -- Using to_cstr, cannot free with tv_clear + local tv = ffi.gc(typvalt(v[1], v[2]), nil) + alloc_log:check({}) + local emsg = v[3] + local ret = {v[4], not not emsg} + eq(ret, check_emsg(function() + local err = ffi.new('bool[1]', {false}) + local res = lib.tv_get_number_chk(tv, err) + return {res, err[0]} + end, emsg)) + if emsg then + alloc_log:clear() + else + alloc_log:check({}) + end + end + end) + end) + describe('lnum()', function() + itp('works', function() + for _, v in ipairs({ + {lib.VAR_NUMBER, {v_number=42}, nil, 42}, + {lib.VAR_STRING, {v_string=to_cstr('100500')}, nil, 100500}, + {lib.VAR_STRING, {v_string=to_cstr('.')}, nil, 46}, + {lib.VAR_FLOAT, {v_float=42.53}, 'E805: Using a Float as a Number', -1}, + {lib.VAR_PARTIAL, {v_partial=NULL}, 'E703: Using a Funcref as a Number', -1}, + {lib.VAR_FUNC, {v_string=NULL}, 'E703: Using a Funcref as a Number', -1}, + {lib.VAR_LIST, {v_list=NULL}, 'E745: Using a List as a Number', -1}, + {lib.VAR_DICT, {v_dict=NULL}, 'E728: Using a Dictionary as a Number', -1}, + {lib.VAR_SPECIAL, {v_special=lib.kSpecialVarNull}, nil, 0}, + {lib.VAR_SPECIAL, {v_special=lib.kSpecialVarTrue}, nil, 1}, + {lib.VAR_SPECIAL, {v_special=lib.kSpecialVarFalse}, nil, 0}, + {lib.VAR_UNKNOWN, nil, 'E685: Internal error: tv_get_number(UNKNOWN)', -1}, + }) do + lib.curwin.w_cursor.lnum = 46 + -- Using to_cstr, cannot free with tv_clear + local tv = ffi.gc(typvalt(v[1], v[2]), nil) + alloc_log:check({}) + local emsg = v[3] + local ret = v[4] + eq(ret, check_emsg(function() return lib.tv_get_lnum(tv) end, emsg)) + if emsg then + alloc_log:clear() + else + alloc_log:check({}) + end + end + end) + end) + describe('float()', function() + itp('works', function() + for _, v in ipairs({ + {lib.VAR_NUMBER, {v_number=42}, nil, 42}, + {lib.VAR_STRING, {v_string=to_cstr('100500')}, 'E892: Using a String as a Float', 0}, + {lib.VAR_FLOAT, {v_float=42.53}, nil, 42.53}, + {lib.VAR_PARTIAL, {v_partial=NULL}, 'E891: Using a Funcref as a Float', 0}, + {lib.VAR_FUNC, {v_string=NULL}, 'E891: Using a Funcref as a Float', 0}, + {lib.VAR_LIST, {v_list=NULL}, 'E893: Using a List as a Float', 0}, + {lib.VAR_DICT, {v_dict=NULL}, 'E894: Using a Dictionary as a Float', 0}, + {lib.VAR_SPECIAL, {v_special=lib.kSpecialVarNull}, 'E907: Using a special value as a Float', 0}, + {lib.VAR_SPECIAL, {v_special=lib.kSpecialVarTrue}, 'E907: Using a special value as a Float', 0}, + {lib.VAR_SPECIAL, {v_special=lib.kSpecialVarFalse}, 'E907: Using a special value as a Float', 0}, + {lib.VAR_UNKNOWN, nil, 'E685: Internal error: tv_get_float(UNKNOWN)', 0}, + }) do + -- Using to_cstr, cannot free with tv_clear + local tv = ffi.gc(typvalt(v[1], v[2]), nil) + alloc_log:check({}) + local emsg = v[3] + local ret = v[4] + eq(ret, check_emsg(function() return lib.tv_get_float(tv) end, emsg)) + if emsg then + alloc_log:clear() + else + alloc_log:check({}) + end + end + end) + end) + describe('string()', function() + itp('works', function() + local buf = lib.tv_get_string(lua2typvalt(int(1))) + local buf_chk = lib.tv_get_string_chk(lua2typvalt(int(1))) + neq(buf, buf_chk) + for _, v in ipairs({ + {lib.VAR_NUMBER, {v_number=42}, nil, '42'}, + {lib.VAR_STRING, {v_string=to_cstr('100500')}, nil, '100500'}, + {lib.VAR_FLOAT, {v_float=42.53}, 'E806: using Float as a String', ''}, + {lib.VAR_PARTIAL, {v_partial=NULL}, 'E729: using Funcref as a String', ''}, + {lib.VAR_FUNC, {v_string=NULL}, 'E729: using Funcref as a String', ''}, + {lib.VAR_LIST, {v_list=NULL}, 'E730: using List as a String', ''}, + {lib.VAR_DICT, {v_dict=NULL}, 'E731: using Dictionary as a String', ''}, + {lib.VAR_SPECIAL, {v_special=lib.kSpecialVarNull}, nil, 'null'}, + {lib.VAR_SPECIAL, {v_special=lib.kSpecialVarTrue}, nil, 'true'}, + {lib.VAR_SPECIAL, {v_special=lib.kSpecialVarFalse}, nil, 'false'}, + {lib.VAR_UNKNOWN, nil, 'E908: using an invalid value as a String', ''}, + }) do + -- Using to_cstr in place of Neovim allocated string, cannot + -- tv_clear() that. + local tv = ffi.gc(typvalt(v[1], v[2]), nil) + alloc_log:check({}) + local emsg = v[3] + local ret = v[4] + eq(ret, check_emsg(function() + local res = lib.tv_get_string(tv) + if tv.v_type == lib.VAR_NUMBER or tv.v_type == lib.VAR_SPECIAL then + eq(buf, res) + else + neq(buf, res) + end + if res ~= nil then + return ffi.string(res) + else + return nil + end + end, emsg)) + if emsg then + alloc_log:clear() + else + alloc_log:check({}) + end + end + end) + end) + describe('string_chk()', function() + itp('works', function() + local buf = lib.tv_get_string_chk(lua2typvalt(int(1))) + for _, v in ipairs({ + {lib.VAR_NUMBER, {v_number=42}, nil, '42'}, + {lib.VAR_STRING, {v_string=to_cstr('100500')}, nil, '100500'}, + {lib.VAR_FLOAT, {v_float=42.53}, 'E806: using Float as a String', nil}, + {lib.VAR_PARTIAL, {v_partial=NULL}, 'E729: using Funcref as a String', nil}, + {lib.VAR_FUNC, {v_string=NULL}, 'E729: using Funcref as a String', nil}, + {lib.VAR_LIST, {v_list=NULL}, 'E730: using List as a String', nil}, + {lib.VAR_DICT, {v_dict=NULL}, 'E731: using Dictionary as a String', nil}, + {lib.VAR_SPECIAL, {v_special=lib.kSpecialVarNull}, nil, 'null'}, + {lib.VAR_SPECIAL, {v_special=lib.kSpecialVarTrue}, nil, 'true'}, + {lib.VAR_SPECIAL, {v_special=lib.kSpecialVarFalse}, nil, 'false'}, + {lib.VAR_UNKNOWN, nil, 'E908: using an invalid value as a String', nil}, + }) do + -- Using to_cstr, cannot free with tv_clear + local tv = ffi.gc(typvalt(v[1], v[2]), nil) + alloc_log:check({}) + local emsg = v[3] + local ret = v[4] + eq(ret, check_emsg(function() + local res = lib.tv_get_string_chk(tv) + if tv.v_type == lib.VAR_NUMBER or tv.v_type == lib.VAR_SPECIAL then + eq(buf, res) + else + neq(buf, res) + end + if res ~= nil then + return ffi.string(res) + else + return nil + end + end, emsg)) + if emsg then + alloc_log:clear() + else + alloc_log:check({}) + end + end + end) + end) + describe('string_buf()', function() + itp('works', function() + for _, v in ipairs({ + {lib.VAR_NUMBER, {v_number=42}, nil, '42'}, + {lib.VAR_STRING, {v_string=to_cstr('100500')}, nil, '100500'}, + {lib.VAR_FLOAT, {v_float=42.53}, 'E806: using Float as a String', ''}, + {lib.VAR_PARTIAL, {v_partial=NULL}, 'E729: using Funcref as a String', ''}, + {lib.VAR_FUNC, {v_string=NULL}, 'E729: using Funcref as a String', ''}, + {lib.VAR_LIST, {v_list=NULL}, 'E730: using List as a String', ''}, + {lib.VAR_DICT, {v_dict=NULL}, 'E731: using Dictionary as a String', ''}, + {lib.VAR_SPECIAL, {v_special=lib.kSpecialVarNull}, nil, 'null'}, + {lib.VAR_SPECIAL, {v_special=lib.kSpecialVarTrue}, nil, 'true'}, + {lib.VAR_SPECIAL, {v_special=lib.kSpecialVarFalse}, nil, 'false'}, + {lib.VAR_UNKNOWN, nil, 'E908: using an invalid value as a String', ''}, + }) do + -- Using to_cstr, cannot free with tv_clear + local tv = ffi.gc(typvalt(v[1], v[2]), nil) + alloc_log:check({}) + local emsg = v[3] + local ret = v[4] + eq(ret, check_emsg(function() + local buf = ffi.new('char[?]', lib.NUMBUFLEN, {0}) + local res = lib.tv_get_string_buf(tv, buf) + if tv.v_type == lib.VAR_NUMBER or tv.v_type == lib.VAR_SPECIAL then + eq(buf, res) + else + neq(buf, res) + end + if res ~= nil then + return ffi.string(res) + else + return nil + end + end, emsg)) + if emsg then + alloc_log:clear() + else + alloc_log:check({}) + end + end + end) + end) + describe('string_buf_chk()', function() + itp('works', function() + for _, v in ipairs({ + {lib.VAR_NUMBER, {v_number=42}, nil, '42'}, + {lib.VAR_STRING, {v_string=to_cstr('100500')}, nil, '100500'}, + {lib.VAR_FLOAT, {v_float=42.53}, 'E806: using Float as a String', nil}, + {lib.VAR_PARTIAL, {v_partial=NULL}, 'E729: using Funcref as a String', nil}, + {lib.VAR_FUNC, {v_string=NULL}, 'E729: using Funcref as a String', nil}, + {lib.VAR_LIST, {v_list=NULL}, 'E730: using List as a String', nil}, + {lib.VAR_DICT, {v_dict=NULL}, 'E731: using Dictionary as a String', nil}, + {lib.VAR_SPECIAL, {v_special=lib.kSpecialVarNull}, nil, 'null'}, + {lib.VAR_SPECIAL, {v_special=lib.kSpecialVarTrue}, nil, 'true'}, + {lib.VAR_SPECIAL, {v_special=lib.kSpecialVarFalse}, nil, 'false'}, + {lib.VAR_UNKNOWN, nil, 'E908: using an invalid value as a String', nil}, + }) do + -- Using to_cstr, cannot free with tv_clear + local tv = ffi.gc(typvalt(v[1], v[2]), nil) + alloc_log:check({}) + local emsg = v[3] + local ret = v[4] + eq(ret, check_emsg(function() + local buf = ffi.new('char[?]', lib.NUMBUFLEN, {0}) + local res = lib.tv_get_string_buf_chk(tv, buf) + if tv.v_type == lib.VAR_NUMBER or tv.v_type == lib.VAR_SPECIAL then + eq(buf, res) + else + neq(buf, res) + end + if res ~= nil then + return ffi.string(res) + else + return nil + end + end, emsg)) + if emsg then + alloc_log:clear() + else + alloc_log:check({}) + end + end + end) + end) + end) + end) +end) diff --git a/test/unit/fixtures/multiqueue.c b/test/unit/fixtures/multiqueue.c index da63e55919..a8dca0a844 100644 --- a/test/unit/fixtures/multiqueue.c +++ b/test/unit/fixtures/multiqueue.c @@ -1,3 +1,6 @@ +// This is an open source non-commercial project. Dear PVS-Studio, please check +// it. PVS-Studio Static Code Analyzer for C, C++ and C#: http://www.viva64.com + #include <string.h> #include <stdlib.h> #include "nvim/event/multiqueue.h" diff --git a/test/unit/fixtures/rbuffer.c b/test/unit/fixtures/rbuffer.c index d587d6b054..3f4062fa18 100644 --- a/test/unit/fixtures/rbuffer.c +++ b/test/unit/fixtures/rbuffer.c @@ -1,3 +1,6 @@ +// This is an open source non-commercial project. Dear PVS-Studio, please check +// it. PVS-Studio Static Code Analyzer for C, C++ and C#: http://www.viva64.com + #include "nvim/rbuffer.h" #include "rbuffer.h" diff --git a/test/unit/helpers.lua b/test/unit/helpers.lua index 612b337ee7..ef2ee9b8e9 100644 --- a/test/unit/helpers.lua +++ b/test/unit/helpers.lua @@ -11,6 +11,7 @@ local posix = nil local syscall = nil local check_cores = global_helpers.check_cores +local dedent = global_helpers.dedent local neq = global_helpers.neq local map = global_helpers.map local eq = global_helpers.eq @@ -137,6 +138,7 @@ local function filter_complex_blocks(body) for line in body:gmatch("[^\r\n]+") do if not (string.find(line, "(^)", 1, true) ~= nil or string.find(line, "_ISwupper", 1, true) + or string.find(line, "_Float128") or string.find(line, "msgpack_zone_push_finalizer") or string.find(line, "msgpack_unpacker_reserve_buffer") or string.find(line, "UUID_NULL") -- static const uuid_t UUID_NULL = {...} @@ -314,6 +316,29 @@ local function alloc_log_new() eq(exp, self.log) self:clear() end + function log:clear_tmp_allocs() + local toremove = {} + local allocs = {} + for i, v in ipairs(self.log) do + if v.func == 'malloc' or v.func == 'calloc' then + allocs[tostring(v.ret)] = i + elseif v.func == 'realloc' or v.func == 'free' then + if allocs[tostring(v.args[1])] then + toremove[#toremove + 1] = allocs[tostring(v.args[1])] + if v.func == 'free' then + toremove[#toremove + 1] = i + end + end + if v.func == 'realloc' then + allocs[tostring(v.ret)] = i + end + end + end + table.sort(toremove) + for i = #toremove,1,-1 do + table.remove(self.log, toremove[i]) + end + end function log:restore_original_functions() -- Do nothing: set mocks live in a separate process return @@ -488,6 +513,212 @@ if os.getenv('NVIM_TEST_PRINT_SYSCALLS') == '1' then end end +local function just_fail(_) + return false +end +say:set('assertion.just_fail.positive', '%s') +say:set('assertion.just_fail.negative', '%s') +assert:register('assertion', 'just_fail', just_fail, + 'assertion.just_fail.positive', + 'assertion.just_fail.negative') + +local hook_fnamelen = 30 +local hook_sfnamelen = 30 +local hook_numlen = 5 +local hook_msglen = 1 + 1 + 1 + (1 + hook_fnamelen) + (1 + hook_sfnamelen) + (1 + hook_numlen) + 1 + +local tracehelp = dedent([[ + ┌ Trace type: _r_eturn from function , function _c_all, _l_ine executed, + │ _t_ail return, _C_ount (should not actually appear), + │ _s_aved from previous run for reference. + │┏ Function type: _L_ua function, _C_ function, _m_ain part of chunk, + │┃ function that did _t_ail call. + │┃┌ Function name type: _g_lobal, _l_ocal, _m_ethod, _f_ield, _u_pvalue, + │┃│ space for unknown. + │┃│ ┏ Source file name ┌ Function name ┏ Line + │┃│ ┃ (trunc to 30 bytes, no .lua) │ (truncated to last 30 bytes) ┃ number + CWN SSSSSSSSSSSSSSSSSSSSSSSSSSSSSS:FFFFFFFFFFFFFFFFFFFFFFFFFFFFFF:LLLLL\n +]]) + +local function child_sethook(wr) + local trace_level = os.getenv('NVIM_TEST_TRACE_LEVEL') + if not trace_level or trace_level == '' then + trace_level = 1 + else + trace_level = tonumber(trace_level) + end + if trace_level <= 0 then + return + end + local trace_only_c = trace_level <= 1 + local prev_info, prev_reason, prev_lnum + local function hook(reason, lnum, use_prev) + local info = nil + if use_prev then + info = prev_info + elseif reason ~= 'tail return' then -- tail return + info = debug.getinfo(2, 'nSl') + end + + if trace_only_c and (not info or info.what ~= 'C') and not use_prev then + if info.source:sub(-9) == '_spec.lua' then + prev_info = info + prev_reason = 'saved' + prev_lnum = lnum + end + return + end + if trace_only_c and not use_prev and prev_reason then + hook(prev_reason, prev_lnum, true) + prev_reason = nil + end + + local whatchar = ' ' + local namewhatchar = ' ' + local funcname = '' + local source = '' + local msgchar = reason:sub(1, 1) + + if reason == 'count' then + msgchar = 'C' + end + + if info then + funcname = (info.name or ''):sub(1, hook_fnamelen) + whatchar = info.what:sub(1, 1) + namewhatchar = info.namewhat:sub(1, 1) + if namewhatchar == '' then + namewhatchar = ' ' + end + source = info.source + if source:sub(1, 1) == '@' then + if source:sub(-4, -1) == '.lua' then + source = source:sub(1, -5) + end + source = source:sub(-hook_sfnamelen, -1) + end + lnum = lnum or info.currentline + end + + -- assert(-1 <= lnum and lnum <= 99999) + local lnum_s + if lnum == -1 then + lnum_s = 'nknwn' + else + lnum_s = ('%u'):format(lnum) + end + local msg = ( -- lua does not support %* + '' + .. msgchar + .. whatchar + .. namewhatchar + .. ' ' + .. source .. (' '):rep(hook_sfnamelen - #source) + .. ':' + .. funcname .. (' '):rep(hook_fnamelen - #funcname) + .. ':' + .. ('0'):rep(hook_numlen - #lnum_s) .. lnum_s + .. '\n' + ) + -- eq(hook_msglen, #msg) + sc.write(wr, msg) + end + debug.sethook(hook, 'crl') +end + +local trace_end_msg = ('E%s\n'):format((' '):rep(hook_msglen - 2)) + +local function itp_child(wr, func) + init() + collectgarbage('stop') + child_sethook(wr) + local err, emsg = pcall(func) + collectgarbage('restart') + collectgarbage() + debug.sethook() + emsg = tostring(emsg) + sc.write(wr, trace_end_msg) + if not err then + if #emsg > 99999 then + emsg = emsg:sub(1, 99999) + end + sc.write(wr, ('-\n%05u\n%s'):format(#emsg, emsg)) + deinit() + sc.close(wr) + sc.exit(1) + else + sc.write(wr, '+\n') + deinit() + sc.close(wr) + sc.exit(0) + end +end + +local function check_child_err(rd) + local trace = {} + local did_traceline = false + while true do + local traceline = sc.read(rd, hook_msglen) + if #traceline ~= hook_msglen then + if #traceline == 0 then + break + else + trace[#trace + 1] = 'Partial read: <' .. trace .. '>\n' + end + end + if traceline == trace_end_msg then + did_traceline = true + break + end + trace[#trace + 1] = traceline + end + local res = sc.read(rd, 2) + if #res ~= 2 then + local error + if #trace == 0 then + error = '\nTest crashed, no trace available\n' + else + error = '\nTest crashed, trace:\n' .. tracehelp + for i = 1, #trace do + error = error .. trace[i] + end + end + if not did_traceline then + error = error .. '\nNo end of trace occurred' + end + local cc_err, cc_emsg = pcall(check_cores, Paths.test_luajit_prg, true) + if not cc_err then + error = error .. '\ncheck_cores failed: ' .. cc_emsg + end + assert.just_fail(error) + end + if res == '+\n' then + return + end + eq('-\n', res) + local len_s = sc.read(rd, 5) + local len = tonumber(len_s) + neq(0, len) + local err = sc.read(rd, len + 1) + assert.just_fail(err) +end + +local function itp_parent(rd, pid, allow_failure) + local err, emsg = pcall(check_child_err, rd) + sc.wait(pid) + sc.close(rd) + if not err then + if allow_failure then + io.stderr:write('Errorred out:\n' .. tostring(emsg) .. '\n') + os.execute([[ + sh -c "source ci/common/test.sh + check_core_dumps --delete \"]] .. Paths.test_luajit_prg .. [[\""]]) + else + error(emsg) + end + end +end + local function gen_itp(it) child_calls_mod = {} child_calls_mod_once = {} @@ -495,14 +726,6 @@ local function gen_itp(it) preprocess_cache_mod = map(function(v) return v end, preprocess_cache_init) previous_defines_mod = previous_defines_init cdefs_mod = cdefs_init:copy() - local function just_fail(_) - return false - end - say:set('assertion.just_fail.positive', '%s') - say:set('assertion.just_fail.negative', '%s') - assert:register('assertion', 'just_fail', just_fail, - 'assertion.just_fail.positive', - 'assertion.just_fail.negative') local function itp(name, func, allow_failure) if allow_failure and os.getenv('NVIM_TEST_RUN_FAILING_TESTS') ~= '1' then -- FIXME Fix tests with this true @@ -512,50 +735,13 @@ local function gen_itp(it) local rd, wr = sc.pipe() child_pid = sc.fork() if child_pid == 0 then - init() sc.close(rd) - collectgarbage('stop') - local err, emsg = pcall(func) - collectgarbage('restart') - emsg = tostring(emsg) - if not err then - sc.write(wr, ('-\n%05u\n%s'):format(#emsg, emsg)) - deinit() - sc.close(wr) - sc.exit(1) - else - sc.write(wr, '+\n') - deinit() - sc.close(wr) - sc.exit(0) - end + itp_child(wr, func) else sc.close(wr) - sc.wait(child_pid) + local saved_child_pid = child_pid child_pid = nil - local function check() - local res = sc.read(rd, 2) - eq(2, #res) - if res == '+\n' then - return - end - eq('-\n', res) - local len_s = sc.read(rd, 5) - local len = tonumber(len_s) - neq(0, len) - local err = sc.read(rd, len + 1) - assert.just_fail(err) - end - local err, emsg = pcall(check) - sc.close(rd) - if not err then - if allow_failure then - io.stderr:write('Errorred out:\n' .. tostring(emsg) .. '\n') - os.execute([[sh -c "source .ci/common/test.sh ; check_core_dumps --delete \"]] .. Paths.test_luajit_prg .. [[\""]]) - else - error(emsg) - end - end + itp_parent(rd, saved_child_pid, allow_failure) end end) end @@ -563,7 +749,7 @@ local function gen_itp(it) end local function cppimport(path) - return cimport(Paths.test_include_path .. '/' .. path) + return cimport(Paths.test_source_path .. '/test/includes/pre/' .. path) end cimport('./src/nvim/types.h', './src/nvim/main.h', './src/nvim/os/time.h') @@ -587,12 +773,8 @@ local module = { only_separate = only_separate, child_call_once = child_call_once, child_cleanup_once = child_cleanup_once, + sc = sc, } -return function(after_each) - if after_each then - after_each(function() - check_cores(Paths.test_luajit_prg) - end) - end +return function() return module end diff --git a/test/unit/os/env_spec.lua b/test/unit/os/env_spec.lua index 1ffed784ff..c54d5a9b77 100644 --- a/test/unit/os/env_spec.lua +++ b/test/unit/os/env_spec.lua @@ -13,7 +13,7 @@ require('lfs') local cimp = cimport('./src/nvim/os/os.h') -describe('env function', function() +describe('env.c', function() local function os_setenv(name, value, override) return cimp.os_setenv(to_cstr(name), to_cstr(value), override) end @@ -35,17 +35,17 @@ describe('env function', function() local OK = 0 itp('sets an env variable and returns OK', function() - local name = 'NEOVIM_UNIT_TEST_SETENV_1N' - local value = 'NEOVIM_UNIT_TEST_SETENV_1V' + local name = 'NVIM_UNIT_TEST_SETENV_1N' + local value = 'NVIM_UNIT_TEST_SETENV_1V' eq(nil, os.getenv(name)) eq(OK, (os_setenv(name, value, 1))) eq(value, os.getenv(name)) end) itp("dosn't overwrite an env variable if overwrite is 0", function() - local name = 'NEOVIM_UNIT_TEST_SETENV_2N' - local value = 'NEOVIM_UNIT_TEST_SETENV_2V' - local value_updated = 'NEOVIM_UNIT_TEST_SETENV_2V_UPDATED' + local name = 'NVIM_UNIT_TEST_SETENV_2N' + local value = 'NVIM_UNIT_TEST_SETENV_2V' + local value_updated = 'NVIM_UNIT_TEST_SETENV_2V_UPDATED' eq(OK, (os_setenv(name, value, 0))) eq(value, os.getenv(name)) eq(OK, (os_setenv(name, value_updated, 0))) @@ -67,18 +67,43 @@ describe('env function', function() end) end) + describe('os_shell_is_cmdexe', function() + itp('returns true for expected names', function() + eq(true, cimp.os_shell_is_cmdexe(to_cstr('cmd.exe'))) + eq(true, cimp.os_shell_is_cmdexe(to_cstr('cmd'))) + eq(true, cimp.os_shell_is_cmdexe(to_cstr('CMD.EXE'))) + eq(true, cimp.os_shell_is_cmdexe(to_cstr('CMD'))) + + os_setenv('COMSPEC', '/foo/bar/cmd.exe', 0) + eq(true, cimp.os_shell_is_cmdexe(to_cstr('$COMSPEC'))) + os_setenv('COMSPEC', [[C:\system32\cmd.exe]], 0) + eq(true, cimp.os_shell_is_cmdexe(to_cstr('$COMSPEC'))) + end) + itp('returns false for unexpected names', function() + eq(false, cimp.os_shell_is_cmdexe(to_cstr(''))) + eq(false, cimp.os_shell_is_cmdexe(to_cstr('powershell'))) + eq(false, cimp.os_shell_is_cmdexe(to_cstr(' cmd.exe '))) + eq(false, cimp.os_shell_is_cmdexe(to_cstr('cm'))) + eq(false, cimp.os_shell_is_cmdexe(to_cstr('md'))) + eq(false, cimp.os_shell_is_cmdexe(to_cstr('cmd.ex'))) + + os_setenv('COMSPEC', '/foo/bar/cmd', 0) + eq(false, cimp.os_shell_is_cmdexe(to_cstr('$COMSPEC'))) + end) + end) + describe('os_getenv', function() itp('reads an env variable', function() - local name = 'NEOVIM_UNIT_TEST_GETENV_1N' - local value = 'NEOVIM_UNIT_TEST_GETENV_1V' + local name = 'NVIM_UNIT_TEST_GETENV_1N' + local value = 'NVIM_UNIT_TEST_GETENV_1V' eq(NULL, os_getenv(name)) - -- need to use os_setenv, because lua dosn't have a setenv function + -- Use os_setenv because Lua dosen't have setenv. os_setenv(name, value, 1) eq(value, os_getenv(name)) end) itp('returns NULL if the env variable is not found', function() - local name = 'NEOVIM_UNIT_TEST_GETENV_NOTFOUND' + local name = 'NVIM_UNIT_TEST_GETENV_NOTFOUND' return eq(NULL, os_getenv(name)) end) end) @@ -97,8 +122,8 @@ describe('env function', function() describe('os_getenvname_at_index', function() itp('returns names of environment variables', function() - local test_name = 'NEOVIM_UNIT_TEST_GETENVNAME_AT_INDEX_1N' - local test_value = 'NEOVIM_UNIT_TEST_GETENVNAME_AT_INDEX_1V' + local test_name = 'NVIM_UNIT_TEST_GETENVNAME_AT_INDEX_1N' + local test_value = 'NVIM_UNIT_TEST_GETENVNAME_AT_INDEX_1V' os_setenv(test_name, test_value, 1) local i = 0 local names = { } @@ -160,16 +185,16 @@ describe('env function', function() describe('expand_env_esc', function() itp('expands environment variables', function() - local name = 'NEOVIM_UNIT_TEST_EXPAND_ENV_ESCN' - local value = 'NEOVIM_UNIT_TEST_EXPAND_ENV_ESCV' + local name = 'NVIM_UNIT_TEST_EXPAND_ENV_ESCN' + local value = 'NVIM_UNIT_TEST_EXPAND_ENV_ESCV' os_setenv(name, value, 1) -- TODO(bobtwinkles) This only tests Unix expansions. There should be a -- test for Windows as well - local input1 = to_cstr('$NEOVIM_UNIT_TEST_EXPAND_ENV_ESCN/test') - local input2 = to_cstr('${NEOVIM_UNIT_TEST_EXPAND_ENV_ESCN}/test') + local input1 = to_cstr('$NVIM_UNIT_TEST_EXPAND_ENV_ESCN/test') + local input2 = to_cstr('${NVIM_UNIT_TEST_EXPAND_ENV_ESCN}/test') local output_buff1 = cstr(255, '') local output_buff2 = cstr(255, '') - local output_expected = 'NEOVIM_UNIT_TEST_EXPAND_ENV_ESCV/test' + local output_expected = 'NVIM_UNIT_TEST_EXPAND_ENV_ESCV/test' 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)) @@ -204,10 +229,10 @@ describe('env function', function() 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) + cimp.expand_env_esc(src, dst, 256, false, false, NULL) local len = string.len(ffi.string(dst)) assert.True(len > 56) - assert.True(len < 99) + assert.True(len < 256) end) itp('respects `dstlen` without expansion', function() diff --git a/test/unit/os/fileio_spec.lua b/test/unit/os/fileio_spec.lua index eaccdcb2a2..4d58a8934e 100644 --- a/test/unit/os/fileio_spec.lua +++ b/test/unit/os/fileio_spec.lua @@ -60,15 +60,15 @@ local function file_open_new(fname, flags, mode) return ret1[0], ret2 end -local function file_open_fd(fd, flags, mode) +local function file_open_fd(fd, flags) local ret2 = ffi.new('FileDescriptor') - local ret1 = m.file_open_fd(ret2, fd, flags, mode) + local ret1 = m.file_open_fd(ret2, fd, flags) return ret1, ret2 end -local function file_open_fd_new(fd, flags, mode) +local function file_open_fd_new(fd, flags) local ret1 = ffi.new('int[?]', 1, {0}) - local ret2 = ffi.gc(m.file_open_fd_new(ret1, fd, flags, mode), nil) + local ret2 = ffi.gc(m.file_open_fd_new(ret1, fd, flags), nil) return ret1[0], ret2 end @@ -76,6 +76,10 @@ local function file_write(fp, buf) return m.file_write(fp, buf, #buf) end +local function msgpack_file_write(fp, buf) + return m.msgpack_file_write(fp, buf, #buf) +end + local function file_read(fp, size) local buf = nil if size == nil then @@ -94,6 +98,10 @@ local function file_read(fp, size) return ret1, ret2 end +local function file_flush(fp) + return m.file_flush(fp) +end + local function file_fsync(fp) return m.file_fsync(fp) end @@ -105,18 +113,18 @@ end describe('file_open_fd', function() itp('can use file descriptor returned by os_open for reading', function() local fd = m.os_open(file1, m.kO_RDONLY, 0) - local err, fp = file_open_fd(fd, m.kFileReadOnly, 0) + local err, fp = file_open_fd(fd, m.kFileReadOnly) eq(0, err) eq({#fcontents, fcontents}, {file_read(fp, #fcontents)}) - eq(0, m.file_close(fp)) + eq(0, m.file_close(fp, false)) end) itp('can use file descriptor returned by os_open for writing', function() eq(nil, lfs.attributes(filec)) local fd = m.os_open(filec, m.kO_WRONLY + m.kO_CREAT, 384) - local err, fp = file_open_fd(fd, m.kFileWriteOnly, 0) + local err, fp = file_open_fd(fd, m.kFileWriteOnly) eq(0, err) eq(4, file_write(fp, 'test')) - eq(0, m.file_close(fp)) + eq(0, m.file_close(fp, false)) eq(4, lfs.attributes(filec).size) eq('test', io.open(filec):read('*a')) end) @@ -125,18 +133,18 @@ end) describe('file_open_fd_new', function() itp('can use file descriptor returned by os_open for reading', function() local fd = m.os_open(file1, m.kO_RDONLY, 0) - local err, fp = file_open_fd_new(fd, m.kFileReadOnly, 0) + local err, fp = file_open_fd_new(fd, m.kFileReadOnly) eq(0, err) eq({#fcontents, fcontents}, {file_read(fp, #fcontents)}) - eq(0, m.file_free(fp)) + eq(0, m.file_free(fp, false)) end) itp('can use file descriptor returned by os_open for writing', function() eq(nil, lfs.attributes(filec)) local fd = m.os_open(filec, m.kO_WRONLY + m.kO_CREAT, 384) - local err, fp = file_open_fd_new(fd, m.kFileWriteOnly, 0) + local err, fp = file_open_fd_new(fd, m.kFileWriteOnly) eq(0, err) eq(4, file_write(fp, 'test')) - eq(0, m.file_free(fp)) + eq(0, m.file_free(fp, false)) eq(4, lfs.attributes(filec).size) eq('test', io.open(filec):read('*a')) end) @@ -148,7 +156,7 @@ describe('file_open', function() eq(0, err) local attrs = lfs.attributes(filec) eq('rwx------', attrs.permissions) - eq(0, m.file_close(fp)) + eq(0, m.file_close(fp, false)) end) itp('can create a rw------- file with kFileCreate', function() @@ -156,7 +164,7 @@ describe('file_open', function() eq(0, err) local attrs = lfs.attributes(filec) eq('rw-------', attrs.permissions) - eq(0, m.file_close(fp)) + eq(0, m.file_close(fp, false)) end) itp('can create a rwx------ file with kFileCreateOnly', function() @@ -164,7 +172,7 @@ describe('file_open', function() eq(0, err) local attrs = lfs.attributes(filec) eq('rwx------', attrs.permissions) - eq(0, m.file_close(fp)) + eq(0, m.file_close(fp, false)) end) itp('can create a rw------- file with kFileCreateOnly', function() @@ -172,7 +180,7 @@ describe('file_open', function() eq(0, err) local attrs = lfs.attributes(filec) eq('rw-------', attrs.permissions) - eq(0, m.file_close(fp)) + eq(0, m.file_close(fp, false)) end) itp('fails to open an existing file with kFileCreateOnly', function() @@ -191,35 +199,35 @@ describe('file_open', function() local err, fp = file_open(file1, m.kFileCreate, 384) eq(0, err) eq(true, fp.wr) - eq(0, m.file_close(fp)) + eq(0, m.file_close(fp, false)) end) itp('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)) + eq(0, m.file_close(fp, false)) end) itp('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)) + eq(0, m.file_close(fp, false)) end) itp('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)) + eq(0, m.file_close(fp, false)) end) itp('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)) + eq(0, m.file_close(fp, false)) local attrs = lfs.attributes(file1) eq(0, attrs.size) end) @@ -228,7 +236,7 @@ describe('file_open', function() local err, fp = file_open(file1, m.kFileWriteOnly, 384) eq(0, err) eq(true, fp.wr) - eq(0, m.file_close(fp)) + eq(0, m.file_close(fp, false)) local attrs = lfs.attributes(file1) eq(4096, attrs.size) end) @@ -245,7 +253,7 @@ describe('file_open', function() local err, fp = file_open(linkf, m.kFileTruncate, 384) eq(0, err) eq(true, fp.wr) - eq(0, m.file_close(fp)) + eq(0, m.file_close(fp, false)) local attrs = lfs.attributes(file1) eq(0, attrs.size) end) @@ -271,7 +279,7 @@ describe('file_open_new', function() local err, fp = file_open_new(file1, 0, 384) eq(0, err) eq(false, fp.wr) - eq(0, m.file_free(fp)) + eq(0, m.file_free(fp, false)) end) itp('fails to open an existing file with kFileCreateOnly', function() @@ -281,7 +289,29 @@ describe('file_open_new', function() end) end) --- file_close is called above, so it is not tested directly +describe('file_close', function() + itp('can flush writes to disk also with true argument', function() + local err, fp = file_open(filec, m.kFileCreateOnly, 384) + eq(0, err) + local wsize = file_write(fp, 'test') + eq(4, wsize) + eq(0, lfs.attributes(filec).size) + eq(0, m.file_close(fp, true)) + eq(wsize, lfs.attributes(filec).size) + end) +end) + +describe('file_free', function() + itp('can flush writes to disk also with true argument', function() + local err, fp = file_open_new(filec, m.kFileCreateOnly, 384) + eq(0, err) + local wsize = file_write(fp, 'test') + eq(4, wsize) + eq(0, lfs.attributes(filec).size) + eq(0, m.file_free(fp, true)) + eq(wsize, lfs.attributes(filec).size) + end) +end) describe('file_fsync', function() itp('can flush writes to disk', function() @@ -294,7 +324,22 @@ describe('file_fsync', function() eq(0, lfs.attributes(filec).size) eq(0, file_fsync(fp)) eq(wsize, lfs.attributes(filec).size) - eq(0, m.file_close(fp)) + eq(0, m.file_close(fp, false)) + end) +end) + +describe('file_flush', function() + itp('can flush writes to disk', function() + local err, fp = file_open(filec, m.kFileCreateOnly, 384) + eq(0, file_flush(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_flush(fp)) + eq(wsize, lfs.attributes(filec).size) + eq(0, m.file_close(fp, false)) end) end) @@ -316,7 +361,7 @@ describe('file_read', function() eq({exp_err, exp_s}, {file_read(fp, size)}) shift = shift + size end - eq(0, m.file_close(fp)) + eq(0, m.file_close(fp, false)) end) itp('can read the whole file at once', function() @@ -325,7 +370,7 @@ describe('file_read', function() 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)) + eq(0, m.file_close(fp, false)) end) itp('can read more then 1024 bytes after reading a small chunk', function() @@ -335,7 +380,7 @@ describe('file_read', function() 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)) + eq(0, m.file_close(fp, false)) end) itp('can read file by 768-byte-chunks', function() @@ -355,7 +400,7 @@ describe('file_read', function() eq({exp_err, exp_s}, {file_read(fp, size)}) shift = shift + size end - eq(0, m.file_close(fp)) + eq(0, m.file_close(fp, false)) end) end) @@ -366,7 +411,7 @@ describe('file_write', function() eq(true, fp.wr) local wr = file_write(fp, fcontents) eq(#fcontents, wr) - eq(0, m.file_close(fp)) + eq(0, m.file_close(fp, false)) eq(wr, lfs.attributes(filec).size) eq(fcontents, io.open(filec):read('*a')) end) @@ -383,7 +428,7 @@ describe('file_write', function() eq(wr, #s) shift = shift + size end - eq(0, m.file_close(fp)) + eq(0, m.file_close(fp, false)) eq(#fcontents, lfs.attributes(filec).size) eq(fcontents, io.open(filec):read('*a')) end) @@ -400,12 +445,24 @@ describe('file_write', function() eq(wr, #s) shift = shift + size end - eq(0, m.file_close(fp)) + eq(0, m.file_close(fp, false)) eq(#fcontents, lfs.attributes(filec).size) eq(fcontents, io.open(filec):read('*a')) end) end) +describe('msgpack_file_write', function() + itp('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 = msgpack_file_write(fp, fcontents) + eq(0, wr) + eq(0, m.file_close(fp, false)) + eq(fcontents, io.open(filec):read('*a')) + end) +end) + describe('file_skip', function() itp('can skip 3 bytes', function() local err, fp = file_open(file1, 0, 384) @@ -415,6 +472,6 @@ describe('file_skip', function() local rd, s = file_read(fp, 3) eq(3, rd) eq(fcontents:sub(4, 6), s) - eq(0, m.file_close(fp)) + eq(0, m.file_close(fp, false)) end) end) diff --git a/test/unit/os/fs_spec.lua b/test/unit/os/fs_spec.lua index 5f8be93aad..667dbeed77 100644 --- a/test/unit/os/fs_spec.lua +++ b/test/unit/os/fs_spec.lua @@ -35,7 +35,6 @@ for i = 0, 255 do end local fcontents = s:rep(16) -local buffer = "" local directory = nil local absolute_executable = nil local executable_name = nil @@ -65,7 +64,11 @@ local function os_getperm(filename) return tonumber(perm) end -describe('fs function', function() +describe('fs.c', function() + local function os_isdir(name) + return fs.os_isdir(to_cstr(name)) + end + before_each(function() lfs.mkdir('unit-test-directory'); @@ -91,32 +94,37 @@ describe('fs function', function() end) describe('os_dirname', function() - local length - - local function os_dirname(buf, len) - return fs.os_dirname(buf, len) - end - - before_each(function() - length = (string.len(lfs.currentdir())) + 1 - buffer = cstr(length, '') - end) - - itp('returns OK and writes current directory into the buffer if it is large\n enough', function() - eq(OK, (os_dirname(buffer, length))) - eq(lfs.currentdir(), (ffi.string(buffer))) + itp('returns OK and writes current directory to the buffer', function() + local length = string.len(lfs.currentdir()) + 1 + local buf = cstr(length, '') + eq(OK, fs.os_dirname(buf, length)) + eq(lfs.currentdir(), ffi.string(buf)) end) - -- What kind of other failing cases are possible? itp('returns FAIL if the buffer is too small', function() - local buf = cstr((length - 1), '') - eq(FAIL, (os_dirname(buf, (length - 1)))) + local length = string.len(lfs.currentdir()) + 1 + local buf = cstr(length - 1, '') + eq(FAIL, fs.os_dirname(buf, length - 1)) end) end) - local function os_isdir(name) - return fs.os_isdir((to_cstr(name))) - end + describe('os_chdir', function() + itp('fails with path="~"', function() + eq(false, os_isdir('~')) -- sanity check: no literal "~" directory. + local length = 4096 + local expected_cwd = cstr(length, '') + local cwd = cstr(length, '') + eq(OK, fs.os_dirname(expected_cwd, length)) + + -- os_chdir returns 0 for success, not OK (1). + neq(0, fs.os_chdir('~')) -- fail + neq(0, fs.os_chdir('~/')) -- fail + + eq(OK, fs.os_dirname(cwd, length)) + -- CWD did not change. + eq(ffi.string(expected_cwd), ffi.string(cwd)) + end) + end) describe('os_isdir', function() itp('returns false if an empty string is given', function() @@ -870,6 +878,11 @@ describe('fs function', function() end describe('os_fileinfo', function() + itp('returns false if path=NULL', function() + local file_info = file_info_new() + assert.is_false((fs.os_fileinfo(nil, file_info))) + end) + itp('returns false if given a non-existing file', function() local file_info = file_info_new() assert.is_false((fs.os_fileinfo('/non-existent', file_info))) diff --git a/test/unit/os/shell_spec.lua b/test/unit/os/shell_spec.lua index e883301cfb..37274502de 100644 --- a/test/unit/os/shell_spec.lua +++ b/test/unit/os/shell_spec.lua @@ -15,7 +15,7 @@ local NULL = ffi.cast('void *', 0) describe('shell functions', 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_sh = to_cstr('/bin/sh') cimported.p_shcf = to_cstr('-c') cimported.p_sxq = to_cstr('') cimported.p_sxe = to_cstr('') @@ -53,14 +53,14 @@ describe('shell functions', function() describe('os_system', function() itp('can echo some output (shell builtin)', function() - local cmd, text = 'echo -n', 'some text' + local cmd, text = 'printf "%s "', 'some text ' local status, output = os_system(cmd .. ' ' .. text) eq(text, output) eq(0, status) end) itp('can deal with empty output', function() - local cmd = 'echo -n' + local cmd = 'printf ""' local status, output = os_system(cmd) eq('', output) eq(0, status) @@ -81,19 +81,19 @@ describe('shell functions', function() describe('shell_build_argv', function() itp('works with NULL arguments', function() - eq({'/bin/bash'}, shell_build_argv(nil, nil)) + eq({'/bin/sh'}, shell_build_argv(nil, nil)) end) itp('works with cmd', function() - eq({'/bin/bash', '-c', 'abc def'}, shell_build_argv('abc def', nil)) + eq({'/bin/sh', '-c', 'abc def'}, shell_build_argv('abc def', nil)) end) itp('works with extra_args', function() - eq({'/bin/bash', 'ghi jkl'}, shell_build_argv(nil, 'ghi jkl')) + eq({'/bin/sh', 'ghi jkl'}, shell_build_argv(nil, 'ghi jkl')) end) itp('works with cmd and extra_args', function() - eq({'/bin/bash', 'ghi jkl', '-c', 'abc def'}, shell_build_argv('abc def', 'ghi jkl')) + eq({'/bin/sh', 'ghi jkl', '-c', 'abc def'}, shell_build_argv('abc def', 'ghi jkl')) end) itp('splits and unquotes &shell and &shellcmdflag', function() @@ -112,7 +112,7 @@ describe('shell functions', function() local argv = ffi.cast('char**', cimported.shell_build_argv(to_cstr('echo &|<>()@^'), nil)) - eq(ffi.string(argv[0]), '/bin/bash') + eq(ffi.string(argv[0]), '/bin/sh') eq(ffi.string(argv[1]), '-c') eq(ffi.string(argv[2]), '(echo ^&^|^<^>^(^)^@^^)') eq(nil, argv[3]) @@ -124,7 +124,7 @@ describe('shell functions', 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[0]), '/bin/sh') eq(ffi.string(argv[1]), '-c') eq(ffi.string(argv[2]), '"(echo -n some text)"') eq(nil, argv[3]) @@ -136,7 +136,7 @@ describe('shell functions', 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[0]), '/bin/sh') eq(ffi.string(argv[1]), '-c') eq(ffi.string(argv[2]), '"echo -n some text"') eq(nil, argv[3]) @@ -145,7 +145,7 @@ describe('shell functions', function() itp('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[0]), '/bin/sh') eq(ffi.string(argv[1]), '-c') eq(ffi.string(argv[2]), 'echo -n some text') eq(nil, argv[3]) diff --git a/test/unit/path_spec.lua b/test/unit/path_spec.lua index 470f971e68..ed597eaed7 100644 --- a/test/unit/path_spec.lua +++ b/test/unit/path_spec.lua @@ -13,12 +13,12 @@ local OK = helpers.OK local FAIL = helpers.FAIL cimport('string.h') -local path = cimport('./src/nvim/path.h') +local cimp = cimport('./src/nvim/os/os.h', './src/nvim/path.h') local length = 0 local buffer = nil -describe('path function', function() +describe('path.c', function() describe('path_full_dir_name', function() setup(function() lfs.mkdir('unit-test-directory') @@ -30,7 +30,7 @@ describe('path function', function() local function path_full_dir_name(directory, buf, len) directory = to_cstr(directory) - return path.path_full_dir_name(directory, buf, len) + return cimp.path_full_dir_name(directory, buf, len) end before_each(function() @@ -69,7 +69,7 @@ describe('path function', function() local function path_full_compare(s1, s2, cn) s1 = to_cstr(s1) s2 = to_cstr(s2) - return path.path_full_compare(s1, s2, cn or 0) + return cimp.path_full_compare(s1, s2, cn or 0) end local f1 = 'f1.o' @@ -86,31 +86,31 @@ describe('path function', function() end) itp('returns kEqualFiles when passed the same file', function() - eq(path.kEqualFiles, (path_full_compare(f1, f1))) + eq(cimp.kEqualFiles, (path_full_compare(f1, f1))) end) itp('returns kEqualFileNames when files that dont exist and have same name', function() - eq(path.kEqualFileNames, (path_full_compare('null.txt', 'null.txt', true))) + eq(cimp.kEqualFileNames, (path_full_compare('null.txt', 'null.txt', true))) end) itp('returns kBothFilesMissing when files that dont exist', function() - eq(path.kBothFilesMissing, (path_full_compare('null.txt', 'null.txt'))) + eq(cimp.kBothFilesMissing, (path_full_compare('null.txt', 'null.txt'))) end) itp('returns kDifferentFiles when passed different files', function() - eq(path.kDifferentFiles, (path_full_compare(f1, f2))) - eq(path.kDifferentFiles, (path_full_compare(f2, f1))) + eq(cimp.kDifferentFiles, (path_full_compare(f1, f2))) + eq(cimp.kDifferentFiles, (path_full_compare(f2, f1))) end) itp('returns kOneFileMissing if only one does not exist', function() - eq(path.kOneFileMissing, (path_full_compare(f1, 'null.txt'))) - eq(path.kOneFileMissing, (path_full_compare('null.txt', f1))) + eq(cimp.kOneFileMissing, (path_full_compare(f1, 'null.txt'))) + eq(cimp.kOneFileMissing, (path_full_compare('null.txt', f1))) end) end) describe('path_tail', function() local function path_tail(file) - local res = path.path_tail((to_cstr(file))) + local res = cimp.path_tail((to_cstr(file))) neq(NULL, res) return ffi.string(res) end @@ -126,7 +126,7 @@ describe('path function', function() describe('path_tail_with_sep', function() local function path_tail_with_sep(file) - local res = path.path_tail_with_sep((to_cstr(file))) + local res = cimp.path_tail_with_sep((to_cstr(file))) neq(NULL, res) return ffi.string(res) end @@ -159,11 +159,11 @@ describe('path function', function() -- strcmp. local function invocation_path_tail(invk) local plen = ffi.new('size_t[?]', 1) - local ptail = path.invocation_path_tail((to_cstr(invk)), plen) + local ptail = cimp.invocation_path_tail((to_cstr(invk)), plen) neq(NULL, ptail) -- it does not change the output if len==NULL - local tail2 = path.invocation_path_tail((to_cstr(invk)), NULL) + local tail2 = cimp.invocation_path_tail((to_cstr(invk)), NULL) neq(NULL, tail2) eq((ffi.string(ptail)), (ffi.string(tail2))) return ptail, plen[0] @@ -204,7 +204,7 @@ describe('path function', function() end) itp('is equivalent to path_tail when args do not contain a path separator', function() - local ptail = path.path_tail(to_cstr("a/b/c x y z")) + local ptail = cimp.path_tail(to_cstr("a/b/c x y z")) neq(NULL, ptail) local tail = ffi.string(ptail) local invk, _ = invocation_path_tail("a/b/c x y z") @@ -212,7 +212,7 @@ describe('path function', function() end) itp('is not equivalent to path_tail when args contain a path separator', function() - local ptail = path.path_tail(to_cstr("a/b/c x y/z")) + local ptail = cimp.path_tail(to_cstr("a/b/c x y/z")) neq(NULL, ptail) local invk, _ = invocation_path_tail("a/b/c x y/z") neq((ffi.string(ptail)), (ffi.string(invk))) @@ -221,7 +221,7 @@ describe('path function', function() describe('path_next_component', function() local function path_next_component(file) - local res = path.path_next_component((to_cstr(file))) + local res = cimp.path_next_component((to_cstr(file))) neq(NULL, res) return ffi.string(res) end @@ -238,25 +238,25 @@ describe('path function', function() describe('path_shorten_fname', function() itp('returns NULL if `full_path` is NULL', function() local dir = to_cstr('some/directory/file.txt') - eq(NULL, (path.path_shorten_fname(NULL, dir))) + eq(NULL, (cimp.path_shorten_fname(NULL, dir))) end) itp('returns NULL if the path and dir does not match', function() local dir = to_cstr('not/the/same') local full = to_cstr('as/this.txt') - eq(NULL, (path.path_shorten_fname(full, dir))) + eq(NULL, (cimp.path_shorten_fname(full, dir))) end) itp('returns NULL if the path is not separated properly', function() local dir = to_cstr('some/very/long/') local full = to_cstr('some/very/long/directory/file.txt') - eq(NULL, (path.path_shorten_fname(full, dir))) + eq(NULL, (cimp.path_shorten_fname(full, dir))) end) itp('shortens the filename if `dir_name` is the start of `full_path`', function() local full = to_cstr('some/very/long/directory/file.txt') local dir = to_cstr('some/very/long') - eq('directory/file.txt', (ffi.string(path.path_shorten_fname(full, dir)))) + eq('directory/file.txt', (ffi.string(cimp.path_shorten_fname(full, dir)))) end) end) end) @@ -277,23 +277,76 @@ describe('path_shorten_fname_if_possible', function() itp('returns shortened path if possible', function() lfs.chdir('ut_directory') local full = to_cstr(lfs.currentdir() .. '/subdir/file.txt') - eq('subdir/file.txt', (ffi.string(path.path_shorten_fname_if_possible(full)))) + eq('subdir/file.txt', (ffi.string(cimp.path_shorten_fname_if_possible(full)))) end) itp('returns `full_path` if a shorter version is not possible', function() local old = lfs.currentdir() lfs.chdir('ut_directory') local full = old .. '/subdir/file.txt' - eq(full, (ffi.string(path.path_shorten_fname_if_possible(to_cstr(full))))) + eq(full, (ffi.string(cimp.path_shorten_fname_if_possible(to_cstr(full))))) end) itp('returns NULL if `full_path` is NULL', function() - eq(NULL, (path.path_shorten_fname_if_possible(NULL))) + eq(NULL, (cimp.path_shorten_fname_if_possible(NULL))) end) end) end) -describe('more path function', function() +describe('path.c path_guess_exepath', function() + local cwd = lfs.currentdir() + + for _,name in ipairs({'./nvim', '.nvim', 'foo/nvim'}) do + itp('"'..name..'" returns name catenated with CWD', function() + local bufsize = 255 + local buf = cstr(bufsize, '') + cimp.path_guess_exepath(name, buf, bufsize) + eq(cwd..'/'..name, ffi.string(buf)) + end) + end + + itp('absolute path returns the name unmodified', function() + local name = '/foo/bar/baz' + local bufsize = 255 + local buf = cstr(bufsize, '') + cimp.path_guess_exepath(name, buf, bufsize) + eq(name, ffi.string(buf)) + end) + + itp('returns the name unmodified if not found in $PATH', function() + local name = '23u0293_not_in_path' + local bufsize = 255 + local buf = cstr(bufsize, '') + cimp.path_guess_exepath(name, buf, bufsize) + eq(name, ffi.string(buf)) + end) + + itp('does not crash if $PATH item exceeds MAXPATHL', function() + local orig_path_env = os.getenv('PATH') + local name = 'cat' -- Some executable in $PATH. + local bufsize = 255 + local buf = cstr(bufsize, '') + local insane_path = orig_path_env..':'..(("x/"):rep(4097)) + + cimp.os_setenv('PATH', insane_path, true) + cimp.path_guess_exepath(name, buf, bufsize) + eq('bin/' .. name, ffi.string(buf):sub(-#('bin/' .. name), -1)) + + -- Restore $PATH. + cimp.os_setenv('PATH', orig_path_env, true) + end) + + itp('returns full path found in $PATH', function() + local name = 'cat' -- Some executable in $PATH. + local bufsize = 255 + local buf = cstr(bufsize, '') + cimp.path_guess_exepath(name, buf, bufsize) + -- Usually "/bin/cat" on unix, "/path/to/nvim/cat" on Windows. + eq('bin/' .. name, ffi.string(buf):sub(-#('bin/' .. name), -1)) + end) +end) + +describe('path.c', function() setup(function() lfs.mkdir('unit-test-directory'); io.open('unit-test-directory/test.file', 'w').close() @@ -313,126 +366,163 @@ describe('more path function', function() end) describe('vim_FullName', function() - local function vim_FullName(filename, buf, len, force) - filename = to_cstr(filename) - return path.vim_FullName(filename, buf, len, force) + local function vim_FullName(filename, buflen, do_expand) + local buf = cstr(buflen, '') + local result = cimp.vim_FullName(to_cstr(filename), buf, buflen, do_expand) + return buf, result end - before_each(function() - -- Create empty string buffer which will contain the resulting path. - length = (string.len(lfs.currentdir())) + 33 - buffer = cstr(length, '') - end) + local function get_buf_len(s, t) + return math.max(string.len(s), string.len(t)) + 1 + end itp('fails if given filename is NULL', function() - local force_expansion = 1 - local result = path.vim_FullName(NULL, buffer, length, force_expansion) + local do_expand = 1 + local buflen = 10 + local buf = cstr(buflen, '') + local result = cimp.vim_FullName(NULL, buf, buflen, do_expand) eq(FAIL, result) end) itp('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 do_expand = 1 + local result = cimp.vim_FullName(filename, buf, too_short_len, do_expand) local expected = string.sub(filename, 1, (too_short_len - 1)) - eq(expected, (ffi.string(buf))) + eq(expected, ffi.string(buf)) eq(FAIL, result) end) itp('uses the filename if the filename is a URL', function() - local force_expansion = 1 local filename = 'http://www.neovim.org' - local result = vim_FullName(filename, buffer, length, force_expansion) - eq(filename, (ffi.string(buffer))) + local buflen = string.len(filename) + 1 + local do_expand = 1 + local buf, result = vim_FullName(filename, buflen, do_expand) + eq(filename, ffi.string(buf)) eq(OK, result) end) itp('fails and uses filename if given filename contains non-existing directory', function() - local force_expansion = 1 local filename = 'non_existing_dir/test.file' - local result = vim_FullName(filename, buffer, length, force_expansion) - eq(filename, (ffi.string(buffer))) + local buflen = string.len(filename) + 1 + local do_expand = 1 + local buf, result = vim_FullName(filename, buflen, do_expand) + eq(filename, ffi.string(buf)) eq(FAIL, result) end) - itp('concatenates given filename if it does not contain a slash', function() - local force_expansion = 1 - local result = vim_FullName('test.file', buffer, length, force_expansion) + itp('concatenates filename if it does not contain a slash', function() local expected = lfs.currentdir() .. '/test.file' - eq(expected, (ffi.string(buffer))) + local filename = 'test.file' + local buflen = get_buf_len(expected, filename) + local do_expand = 1 + local buf, result = vim_FullName(filename, buflen, do_expand) + eq(expected, ffi.string(buf)) eq(OK, result) end) - itp('concatenates given filename if it is a directory but does not contain a\n slash', function() - local force_expansion = 1 - local result = vim_FullName('..', buffer, length, force_expansion) + itp('concatenates directory name if it does not contain a slash', function() local expected = lfs.currentdir() .. '/..' - eq(expected, (ffi.string(buffer))) + local filename = '..' + local buflen = get_buf_len(expected, filename) + local do_expand = 1 + local buf, result = vim_FullName(filename, buflen, do_expand) + eq(expected, ffi.string(buf)) eq(OK, result) end) - -- Is it possible for every developer to enter '..' directory while running - -- the unit tests? Which other directory would be better? itp('enters given directory (instead of just concatenating the strings) if possible and if path contains a slash', function() - local force_expansion = 1 - local result = vim_FullName('../test.file', buffer, length, force_expansion) local old_dir = lfs.currentdir() lfs.chdir('..') local expected = lfs.currentdir() .. '/test.file' lfs.chdir(old_dir) - eq(expected, (ffi.string(buffer))) + local filename = '../test.file' + local buflen = get_buf_len(expected, filename) + local do_expand = 1 + local buf, result = vim_FullName(filename, buflen, do_expand) + eq(expected, ffi.string(buf)) eq(OK, result) end) itp('just copies the path if it is already absolute and force=0', function() - local force_expansion = 0 local absolute_path = '/absolute/path' - local result = vim_FullName(absolute_path, buffer, length, force_expansion) - eq(absolute_path, (ffi.string(buffer))) + local buflen = string.len(absolute_path) + 1 + local do_expand = 0 + local buf, result = vim_FullName(absolute_path, buflen, do_expand) + eq(absolute_path, ffi.string(buf)) eq(OK, result) end) itp('fails and uses filename when the path is relative to HOME', function() - local force_expansion = 1 + eq(false, cimp.os_isdir('~')) -- sanity check: no literal "~" directory. local absolute_path = '~/home.file' - local result = vim_FullName(absolute_path, buffer, length, force_expansion) - eq(absolute_path, (ffi.string(buffer))) + local buflen = string.len(absolute_path) + 1 + local do_expand = 1 + local buf, result = vim_FullName(absolute_path, buflen, do_expand) + eq(absolute_path, ffi.string(buf)) eq(FAIL, result) end) itp('works with some "normal" relative path with directories', function() - local force_expansion = 1 - local result = vim_FullName('unit-test-directory/test.file', buffer, length, force_expansion) + local expected = lfs.currentdir() .. '/unit-test-directory/test.file' + local filename = 'unit-test-directory/test.file' + local buflen = get_buf_len(expected, filename) + local do_expand = 1 + local buf, result = vim_FullName(filename, buflen, do_expand) + eq(expected, ffi.string(buf)) eq(OK, result) - eq(lfs.currentdir() .. '/unit-test-directory/test.file', (ffi.string(buffer))) end) itp('does not modify the given filename', function() - local force_expansion = 1 + local expected = lfs.currentdir() .. '/unit-test-directory/test.file' local filename = to_cstr('unit-test-directory/test.file') - -- Don't use the wrapper here but pass a cstring directly to the c - -- function. - local result = path.vim_FullName(filename, buffer, length, force_expansion) - eq(lfs.currentdir() .. '/unit-test-directory/test.file', (ffi.string(buffer))) - eq('unit-test-directory/test.file', (ffi.string(filename))) + local buflen = string.len(expected) + 1 + local buf = cstr(buflen, '') + local do_expand = 1 + -- Don't use the wrapper but pass a cstring directly to the c function. + eq('unit-test-directory/test.file', ffi.string(filename)) + local result = cimp.vim_FullName(filename, buf, buflen, do_expand) + eq(expected, ffi.string(buf)) eq(OK, result) end) itp('works with directories that have one path component', function() - local force_expansion = 1 - local filename = to_cstr('/tmp') - local result = path.vim_FullName(filename, buffer, length, force_expansion) - eq('/tmp', ffi.string(buffer)) + local filename = '/tmp' + local expected = filename + local buflen = get_buf_len(expected, filename) + local do_expand = 1 + local buf, result = vim_FullName(filename, buflen, do_expand) + eq('/tmp', ffi.string(buf)) + eq(OK, result) + end) + + itp('expands "./" to the current directory #7117', function() + local expected = lfs.currentdir() .. '/unit-test-directory/test.file' + local filename = './unit-test-directory/test.file' + local buflen = get_buf_len(expected, filename) + local do_expand = 1 + local buf, result = vim_FullName(filename, buflen, do_expand) + eq(OK, result) + eq(expected, ffi.string(buf)) + end) + + itp('collapses "foo/../foo" to "foo" #7117', function() + local expected = lfs.currentdir() .. '/unit-test-directory/test.file' + local filename = 'unit-test-directory/../unit-test-directory/test.file' + local buflen = get_buf_len(expected, filename) + local do_expand = 1 + local buf, result = vim_FullName(filename, buflen, do_expand) eq(OK, result) + eq(expected, ffi.string(buf)) end) end) describe('path_fix_case', function() local function fix_case(file) local c_file = to_cstr(file) - path.path_fix_case(c_file) + cimp.path_fix_case(c_file) return ffi.string(c_file) end @@ -456,41 +546,41 @@ describe('more path function', function() itp('joins given paths with a slash', function() local path1 = cstr(100, 'path1') local to_append = to_cstr('path2') - eq(OK, (path.append_path(path1, to_append, 100))) + eq(OK, (cimp.append_path(path1, to_append, 100))) eq("path1/path2", (ffi.string(path1))) end) itp('joins given paths without adding an unnecessary slash', function() local path1 = cstr(100, 'path1/') local to_append = to_cstr('path2') - eq(OK, path.append_path(path1, to_append, 100)) + eq(OK, cimp.append_path(path1, to_append, 100)) eq("path1/path2", (ffi.string(path1))) end) itp('fails and uses filename if there is not enough space left for to_append', function() local path1 = cstr(11, 'path1/') local to_append = to_cstr('path2') - eq(FAIL, (path.append_path(path1, to_append, 11))) + eq(FAIL, (cimp.append_path(path1, to_append, 11))) end) itp('does not append a slash if to_append is empty', function() local path1 = cstr(6, 'path1') local to_append = to_cstr('') - eq(OK, (path.append_path(path1, to_append, 6))) + eq(OK, (cimp.append_path(path1, to_append, 6))) eq('path1', (ffi.string(path1))) end) itp('does not append unnecessary dots', function() local path1 = cstr(6, 'path1') local to_append = to_cstr('.') - eq(OK, (path.append_path(path1, to_append, 6))) + eq(OK, (cimp.append_path(path1, to_append, 6))) eq('path1', (ffi.string(path1))) end) itp('copies to_append to path, if path is empty', function() local path1 = cstr(7, '') local to_append = to_cstr('/path2') - eq(OK, (path.append_path(path1, to_append, 7))) + eq(OK, (cimp.append_path(path1, to_append, 7))) eq('/path2', (ffi.string(path1))) end) end) @@ -498,7 +588,7 @@ describe('more path function', function() describe('path_is_absolute_path', function() local function path_is_absolute_path(filename) filename = to_cstr(filename) - return path.path_is_absolute_path(filename) + return cimp.path_is_absolute_path(filename) end itp('returns true if filename starts with a slash', function() diff --git a/test/unit/strings_spec.lua b/test/unit/strings_spec.lua index 3bc3dc7130..e54c82b26a 100644 --- a/test/unit/strings_spec.lua +++ b/test/unit/strings_spec.lua @@ -99,3 +99,42 @@ describe('vim_strnsave_unquoted()', function() eq('/Program\\nFiles/sh', vim_strnsave_unquoted('/Program"\\n"Files/sh')) end) end) + +describe('vim_strchr()', function() + local vim_strchr = function(s, c) + local str = to_cstr(s) + local res = strings.vim_strchr(str, c) + if res == nil then + return nil + else + return res - str + end + end + itp('handles NUL and <0 correctly', function() + eq(nil, vim_strchr('abc', 0)) + eq(nil, vim_strchr('abc', -1)) + end) + itp('works', function() + eq(0, vim_strchr('abc', ('a'):byte())) + eq(1, vim_strchr('abc', ('b'):byte())) + eq(2, vim_strchr('abc', ('c'):byte())) + eq(0, vim_strchr('a«b»c', ('a'):byte())) + eq(3, vim_strchr('a«b»c', ('b'):byte())) + eq(6, vim_strchr('a«b»c', ('c'):byte())) + + eq(nil, vim_strchr('«»', ('«'):byte())) + -- 0xAB == 171 == '«' + eq(nil, vim_strchr('\171', 0xAB)) + eq(0, vim_strchr('«»', 0xAB)) + eq(3, vim_strchr('„«»“', 0xAB)) + + eq(7, vim_strchr('„«»“', 0x201C)) + eq(nil, vim_strchr('„«»“', 0x201D)) + eq(0, vim_strchr('„«»“', 0x201E)) + + eq(0, vim_strchr('\244\143\188\128', 0x10FF00)) + eq(2, vim_strchr('«\244\143\188\128»', 0x10FF00)) + -- |0xDBFF |0xDF00 - surrogate pair for 0x10FF00 + eq(nil, vim_strchr('«\237\175\191\237\188\128»', 0x10FF00)) + end) +end) diff --git a/test/unit/testtest_spec.lua b/test/unit/testtest_spec.lua new file mode 100644 index 0000000000..d2f3632b6f --- /dev/null +++ b/test/unit/testtest_spec.lua @@ -0,0 +1,19 @@ +local helpers = require('test.unit.helpers')(after_each) +local assert = require('luassert') + +local itp = helpers.gen_itp(it) + +local sc = helpers.sc + +-- All of the below tests must fail. Check how exactly they fail. +if os.getenv('NVIM_TEST_RUN_TESTTEST') ~= '1' then + return +end +describe('test code', function() + itp('does not hang when working with lengthy errors', function() + assert.just_fail(('x'):rep(65536)) + end) + itp('shows trace after exiting abnormally', function() + sc.exit(0) + end) +end) |