aboutsummaryrefslogtreecommitdiff
path: root/test/unit
diff options
context:
space:
mode:
Diffstat (limited to 'test/unit')
-rw-r--r--test/unit/api/helpers.lua91
-rw-r--r--test/unit/api/private_helpers_spec.lua26
-rw-r--r--test/unit/buffer_spec.lua72
-rw-r--r--test/unit/charset/vim_str2nr_spec.lua320
-rw-r--r--test/unit/eval/decode_spec.lua39
-rw-r--r--test/unit/eval/encode_spec.lua25
-rw-r--r--test/unit/eval/helpers.lua549
-rw-r--r--test/unit/eval/tricks_spec.lua29
-rw-r--r--test/unit/eval/tv_clear_spec.lua128
-rw-r--r--test/unit/eval/typval_spec.lua3137
-rw-r--r--test/unit/fileio_spec.lua33
-rw-r--r--test/unit/fixtures/multiqueue.c19
-rw-r--r--test/unit/fixtures/multiqueue.h4
-rw-r--r--test/unit/fixtures/posix.h11
-rw-r--r--test/unit/fixtures/queue.c16
-rw-r--r--test/unit/fixtures/queue.h4
-rw-r--r--test/unit/fixtures/rbuffer.c3
-rw-r--r--test/unit/formatc.lua15
-rw-r--r--test/unit/garray_spec.lua43
-rw-r--r--test/unit/helpers.lua859
-rw-r--r--test/unit/keymap_spec.lua71
-rw-r--r--test/unit/mbyte_spec.lua27
-rw-r--r--test/unit/memory_spec.lua52
-rw-r--r--test/unit/message_spec.lua60
-rw-r--r--test/unit/multiqueue_spec.lua149
-rw-r--r--test/unit/option_spec.lua52
-rw-r--r--test/unit/os/env_spec.lua163
-rw-r--r--test/unit/os/fileio_spec.lua216
-rw-r--r--test/unit/os/fs_spec.lua292
-rw-r--r--test/unit/os/shell_spec.lua139
-rw-r--r--test/unit/os/users_spec.lua19
-rw-r--r--test/unit/path_spec.lua389
-rw-r--r--test/unit/preload.lua2
-rw-r--r--test/unit/preprocess.lua165
-rw-r--r--test/unit/profile_spec.lua43
-rw-r--r--test/unit/queue_spec.lua123
-rw-r--r--test/unit/rbuffer_spec.lua100
-rw-r--r--test/unit/set.lua16
-rw-r--r--test/unit/strings_spec.lua78
-rw-r--r--test/unit/tempfile_spec.lua47
-rw-r--r--test/unit/testtest_spec.lua19
-rw-r--r--test/unit/undo_spec.lua215
-rw-r--r--test/unit/viml/expressions/lexer_spec.lua428
-rw-r--r--test/unit/viml/expressions/parser_spec.lua540
-rw-r--r--test/unit/viml/expressions/parser_tests.lua8317
-rw-r--r--test/unit/viml/helpers.lua130
46 files changed, 16152 insertions, 1123 deletions
diff --git a/test/unit/api/helpers.lua b/test/unit/api/helpers.lua
index 883e1c6c19..4fb1cee4b3 100644
--- a/test/unit/api/helpers.lua
+++ b/test/unit/api/helpers.lua
@@ -1,4 +1,4 @@
-local helpers = require('test.unit.helpers')
+local helpers = require('test.unit.helpers')(nil)
local eval_helpers = require('test.unit.eval.helpers')
local cimport = helpers.cimport
@@ -19,47 +19,55 @@ local api = cimport('./src/nvim/api/private/defs.h',
local obj2lua
-local obj2lua_tab = {
- [tonumber(api.kObjectTypeArray)] = function(obj)
- local ret = {[type_key]=list_type}
- for i = 1,tonumber(obj.data.array.size) do
- ret[i] = obj2lua(obj.data.array.items[i - 1])
- end
- if ret[1] then
- ret[type_key] = nil
- end
- return ret
- end,
- [tonumber(api.kObjectTypeDictionary)] = function(obj)
- local ret = {}
- for i = 1,tonumber(obj.data.dictionary.size) do
- local kv_pair = obj.data.dictionary.items[i - 1]
- ret[ffi.string(kv_pair.key.data, kv_pair.key.size)] = obj2lua(kv_pair.value)
- end
- return ret
- end,
- [tonumber(api.kObjectTypeBoolean)] = function(obj)
- if obj.data.boolean == false then
- return false
- else
- return true
- end
- end,
- [tonumber(api.kObjectTypeNil)] = function(_)
- return nil_value
- end,
- [tonumber(api.kObjectTypeFloat)] = function(obj)
- return tonumber(obj.data.floating)
- end,
- [tonumber(api.kObjectTypeInteger)] = function(obj)
- return {[type_key]=int_type, value=tonumber(obj.data.integer)}
- end,
- [tonumber(api.kObjectTypeString)] = function(obj)
- return ffi.string(obj.data.string.data, obj.data.string.size)
- end,
-}
+local obj2lua_tab = nil
+
+local function init_obj2lua_tab()
+ if obj2lua_tab then
+ return
+ end
+ obj2lua_tab = {
+ [tonumber(api.kObjectTypeArray)] = function(obj)
+ local ret = {[type_key]=list_type}
+ for i = 1,tonumber(obj.data.array.size) do
+ ret[i] = obj2lua(obj.data.array.items[i - 1])
+ end
+ if ret[1] then
+ ret[type_key] = nil
+ end
+ return ret
+ end,
+ [tonumber(api.kObjectTypeDictionary)] = function(obj)
+ local ret = {}
+ for i = 1,tonumber(obj.data.dictionary.size) do
+ local kv_pair = obj.data.dictionary.items[i - 1]
+ ret[ffi.string(kv_pair.key.data, kv_pair.key.size)] = obj2lua(kv_pair.value)
+ end
+ return ret
+ end,
+ [tonumber(api.kObjectTypeBoolean)] = function(obj)
+ if obj.data.boolean == false then
+ return false
+ else
+ return true
+ end
+ end,
+ [tonumber(api.kObjectTypeNil)] = function(_)
+ return nil_value
+ end,
+ [tonumber(api.kObjectTypeFloat)] = function(obj)
+ return tonumber(obj.data.floating)
+ end,
+ [tonumber(api.kObjectTypeInteger)] = function(obj)
+ return {[type_key]=int_type, value=tonumber(obj.data.integer)}
+ end,
+ [tonumber(api.kObjectTypeString)] = function(obj)
+ return ffi.string(obj.data.string.data, obj.data.string.size)
+ end,
+ }
+end
obj2lua = function(obj)
+ init_obj2lua_tab()
return ((obj2lua_tab[tonumber(obj['type'])] or function(obj_inner)
assert(false, 'Converting ' .. tostring(tonumber(obj_inner['type'])) .. ' is not implementing yet')
end)(obj))
@@ -106,7 +114,8 @@ local lua2obj_type_tab = {
api.xmalloc(len * ffi.sizeof('KeyValuePair'))),
}})
for i = 1, len do
- local key, val = table.unpack(kvs[i])
+ local table_unpack = table.unpack or unpack -- luacheck: compat
+ local key, val = table_unpack(kvs[i])
dct.data.dictionary.items[i - 1] = ffi.new(
'KeyValuePair', {key=ffi.gc(lua2obj(key), nil).data.string,
value=ffi.gc(lua2obj(val), nil)})
diff --git a/test/unit/api/private_helpers_spec.lua b/test/unit/api/private_helpers_spec.lua
index 1d7c03787b..a534d83165 100644
--- a/test/unit/api/private_helpers_spec.lua
+++ b/test/unit/api/private_helpers_spec.lua
@@ -1,4 +1,5 @@
-local helpers = require('test.unit.helpers')
+local helpers = require('test.unit.helpers')(after_each)
+local itp = helpers.gen_itp(it)
local eval_helpers = require('test.unit.eval.helpers')
local api_helpers = require('test.unit.api.helpers')
@@ -7,6 +8,7 @@ local NULL = helpers.NULL
local eq = helpers.eq
local lua2typvalt = eval_helpers.lua2typvalt
+local typvalt2lua = eval_helpers.typvalt2lua
local typvalt = eval_helpers.typvalt
local nil_value = api_helpers.nil_value
@@ -14,6 +16,7 @@ local list_type = api_helpers.list_type
local int_type = api_helpers.int_type
local type_key = api_helpers.type_key
local obj2lua = api_helpers.obj2lua
+local func_type = api_helpers.func_type
local api = cimport('./src/nvim/api/private/helpers.h')
@@ -23,7 +26,7 @@ describe('vim_to_object', function()
end
local different_output_test = function(name, input, output)
- it(name, function()
+ itp(name, function()
eq(output, vim_to_object(input))
end)
end
@@ -74,15 +77,30 @@ describe('vim_to_object', function()
different_output_test('outputs nil for nested lists (2 level, in dict)',
lst3, {{lst=nil_value}, true, false, 'ttest'})
- it('outputs empty list for NULL list', function()
+ itp('outputs empty list for NULL list', function()
local tt = typvalt('VAR_LIST', {v_list=NULL})
eq(nil, tt.vval.v_list)
eq({[type_key]=list_type}, obj2lua(api.vim_to_object(tt)))
end)
- it('outputs empty dict for NULL dict', function()
+ itp('outputs empty dict for NULL dict', function()
local tt = typvalt('VAR_DICT', {v_dict=NULL})
eq(nil, tt.vval.v_dict)
eq({}, obj2lua(api.vim_to_object(tt)))
end)
+
+ itp('regression: partials in a list', function()
+ local llist = {
+ {
+ [type_key]=func_type,
+ value='printf',
+ args={'%s'},
+ dict={v=1},
+ },
+ {},
+ }
+ local list = lua2typvalt(llist)
+ eq(llist, typvalt2lua(list))
+ eq({nil_value, {}}, obj2lua(api.vim_to_object(list)))
+ end)
end)
diff --git a/test/unit/buffer_spec.lua b/test/unit/buffer_spec.lua
index 317c9be6e7..bf4e5a0e6d 100644
--- a/test/unit/buffer_spec.lua
+++ b/test/unit/buffer_spec.lua
@@ -1,5 +1,6 @@
-local helpers = require("test.unit.helpers")
+local helpers = require("test.unit.helpers")(after_each)
+local itp = helpers.gen_itp(it)
local to_cstr = helpers.to_cstr
local get_str = helpers.ffi.string
@@ -39,17 +40,17 @@ describe('buffer functions', function()
describe('buf_valid', function()
- it('should view NULL as an invalid buffer', function()
+ itp('should view NULL as an invalid buffer', function()
eq(false, buffer.buf_valid(NULL))
end)
- it('should view an open buffer as valid', function()
+ itp('should view an open buffer as valid', function()
local buf = buflist_new(path1, buffer.BLN_LISTED)
eq(true, buffer.buf_valid(buf))
end)
- it('should view a closed and hidden buffer as valid', function()
+ itp('should view a closed and hidden buffer as valid', function()
local buf = buflist_new(path1, buffer.BLN_LISTED)
close_buffer(NULL, buf, 0, 0)
@@ -57,7 +58,7 @@ describe('buffer functions', function()
eq(true, buffer.buf_valid(buf))
end)
- it('should view a closed and unloaded buffer as valid', function()
+ itp('should view a closed and unloaded buffer as valid', function()
local buf = buflist_new(path1, buffer.BLN_LISTED)
close_buffer(NULL, buf, buffer.DOBUF_UNLOAD, 0)
@@ -65,7 +66,7 @@ describe('buffer functions', function()
eq(true, buffer.buf_valid(buf))
end)
- it('should view a closed and wiped buffer as invalid', function()
+ itp('should view a closed and wiped buffer as invalid', function()
local buf = buflist_new(path1, buffer.BLN_LISTED)
close_buffer(NULL, buf, buffer.DOBUF_WIPE, 0)
@@ -84,36 +85,36 @@ describe('buffer functions', function()
return buffer.buflist_findpat(to_cstr(pat), NULL, allow_unlisted, 0, 0)
end
- it('should find exact matches', function()
+ itp('should find exact matches', function()
local buf = buflist_new(path1, buffer.BLN_LISTED)
- eq(buf.b_fnum, buflist_findpat(path1, ONLY_LISTED))
+ eq(buf.handle, buflist_findpat(path1, ONLY_LISTED))
close_buffer(NULL, buf, buffer.DOBUF_WIPE, 0)
end)
- it('should prefer to match the start of a file path', function()
+ itp('should prefer to match the start of a file path', function()
local buf1 = buflist_new(path1, buffer.BLN_LISTED)
local buf2 = buflist_new(path2, buffer.BLN_LISTED)
local buf3 = buflist_new(path3, buffer.BLN_LISTED)
- eq(buf1.b_fnum, buflist_findpat("test", ONLY_LISTED))
- eq(buf2.b_fnum, buflist_findpat("file", ONLY_LISTED))
- eq(buf3.b_fnum, buflist_findpat("path", ONLY_LISTED))
+ eq(buf1.handle, buflist_findpat("test", ONLY_LISTED))
+ eq(buf2.handle, buflist_findpat("file", ONLY_LISTED))
+ eq(buf3.handle, buflist_findpat("path", ONLY_LISTED))
close_buffer(NULL, buf1, buffer.DOBUF_WIPE, 0)
close_buffer(NULL, buf2, buffer.DOBUF_WIPE, 0)
close_buffer(NULL, buf3, buffer.DOBUF_WIPE, 0)
end)
- it('should prefer to match the end of a file over the middle', function()
+ itp('should prefer to match the end of a file over the middle', function()
--{ Given: Two buffers, where 'test' appears in both
-- And: 'test' appears at the end of buf3 but in the middle of buf2
local buf2 = buflist_new(path2, buffer.BLN_LISTED)
local buf3 = buflist_new(path3, buffer.BLN_LISTED)
-- Then: buf2 is the buffer that is found
- eq(buf2.b_fnum, buflist_findpat("test", ONLY_LISTED))
+ eq(buf2.handle, buflist_findpat("test", ONLY_LISTED))
--}
--{ When: We close buf2
@@ -123,31 +124,31 @@ describe('buffer functions', function()
local buf1 = buflist_new(path1, buffer.BLN_LISTED)
-- Then: buf3 is found since 'file' appears at the end of the name
- eq(buf3.b_fnum, buflist_findpat("file", ONLY_LISTED))
+ eq(buf3.handle, buflist_findpat("file", ONLY_LISTED))
--}
close_buffer(NULL, buf1, buffer.DOBUF_WIPE, 0)
close_buffer(NULL, buf3, buffer.DOBUF_WIPE, 0)
end)
- it('should match a unique fragment of a file path', function()
+ itp('should match a unique fragment of a file path', function()
local buf1 = buflist_new(path1, buffer.BLN_LISTED)
local buf2 = buflist_new(path2, buffer.BLN_LISTED)
local buf3 = buflist_new(path3, buffer.BLN_LISTED)
- eq(buf3.b_fnum, buflist_findpat("_test_", ONLY_LISTED))
+ eq(buf3.handle, buflist_findpat("_test_", ONLY_LISTED))
close_buffer(NULL, buf1, buffer.DOBUF_WIPE, 0)
close_buffer(NULL, buf2, buffer.DOBUF_WIPE, 0)
close_buffer(NULL, buf3, buffer.DOBUF_WIPE, 0)
end)
- it('should include / ignore unlisted buffers based on the flag.', function()
+ itp('should include / ignore unlisted buffers based on the flag.', function()
--{ Given: A buffer
local buf3 = buflist_new(path3, buffer.BLN_LISTED)
-- Then: We should find the buffer when it is given a unique pattern
- eq(buf3.b_fnum, buflist_findpat("_test_", ONLY_LISTED))
+ eq(buf3.handle, buflist_findpat("_test_", ONLY_LISTED))
--}
--{ When: We unlist the buffer
@@ -157,7 +158,7 @@ describe('buffer functions', function()
eq(-1, buflist_findpat("_test_", ONLY_LISTED))
-- And: It should find the buffer when including unlisted buffers
- eq(buf3.b_fnum, buflist_findpat("_test_", ALLOW_UNLISTED))
+ eq(buf3.handle, buflist_findpat("_test_", ALLOW_UNLISTED))
--}
--{ When: We wipe the buffer
@@ -169,13 +170,13 @@ describe('buffer functions', function()
--}
end)
- it('should prefer listed buffers to unlisted buffers.', function()
+ itp('should prefer listed buffers to unlisted buffers.', function()
--{ Given: Two buffers that match a pattern
local buf1 = buflist_new(path1, buffer.BLN_LISTED)
local buf2 = buflist_new(path2, buffer.BLN_LISTED)
-- Then: The first buffer is preferred when both are listed
- eq(buf1.b_fnum, buflist_findpat("test", ONLY_LISTED))
+ eq(buf1.handle, buflist_findpat("test", ONLY_LISTED))
--}
--{ When: The first buffer is unlisted
@@ -183,13 +184,13 @@ describe('buffer functions', function()
-- Then: The second buffer is preferred because
-- unlisted buffers are not allowed
- eq(buf2.b_fnum, buflist_findpat("test", ONLY_LISTED))
+ eq(buf2.handle, buflist_findpat("test", ONLY_LISTED))
--}
--{ When: We allow unlisted buffers
-- Then: The second buffer is still preferred
-- because listed buffers are preferred to unlisted
- eq(buf2.b_fnum, buflist_findpat("test", ALLOW_UNLISTED))
+ eq(buf2.handle, buflist_findpat("test", ALLOW_UNLISTED))
--}
--{ When: We unlist the second buffer
@@ -198,7 +199,7 @@ describe('buffer functions', function()
-- Then: The first buffer is preferred again
-- because buf1 matches better which takes precedence
-- when both buffers have the same listing status.
- eq(buf1.b_fnum, buflist_findpat("test", ALLOW_UNLISTED))
+ eq(buf1.handle, buflist_findpat("test", ALLOW_UNLISTED))
-- And: Neither buffer is returned when ignoring unlisted
eq(-1, buflist_findpat("test", ONLY_LISTED))
@@ -265,7 +266,7 @@ describe('buffer functions', function()
local expected_cell_count = option.expected_cell_count or statusline_cell_count
local expected_byte_length = option.expected_byte_length or expected_cell_count
- it(description, function()
+ itp(description, function()
if option.file_name then
buffer.setfname(globals.curbuf, to_cstr(option.file_name), NULL, 1)
else
@@ -281,6 +282,12 @@ describe('buffer functions', function()
end)
end
+ -- expression testing
+ statusline_test('Should expand expression', 2,
+ '%!expand(20+1)', '21')
+ statusline_test('Should expand broken expression to itself', 11,
+ '%!expand(20+1', 'expand(20+1')
+
-- file name testing
statusline_test('should print no file name', 10,
'%f', '[No Name]',
@@ -305,6 +312,12 @@ describe('buffer functions', function()
statusline_test('should put fillchar `~` in between text', 10,
'abc%=def', 'abc~~~~def',
{fillchar=('~'):byte()})
+ statusline_test('should handle zero-fillchar as a space', 10,
+ 'abcde%=', 'abcde ',
+ {fillchar=0})
+ statusline_test('should handle multibyte-fillchar as a dash', 10,
+ 'abcde%=', 'abcde-----',
+ {fillchar=0x80})
statusline_test('should print the tail file name', 80,
'%t', 'buffer_spec.lua',
{file_name='test/unit/buffer_spec.lua', expected_cell_count=15})
@@ -351,6 +364,8 @@ describe('buffer functions', function()
statusline_test('should truncate at the first `<`', 10,
'abc%<def%<ghijklm', 'abc<hijklm')
+ statusline_test('should ignore trailing %', 3, 'abc%', 'abc')
+
-- alignment testing
statusline_test('should right align when using =', 20,
'neo%=vim', 'neo vim')
@@ -451,5 +466,10 @@ describe('buffer functions', function()
'Ą%=mid%=end', 'Ą@mid@@end',
{fillchar=('@'):byte(), expected_byte_length=11})
+ -- escaping % testing
+ statusline_test('should handle escape of %', 4, 'abc%%', 'abc%')
+ statusline_test('case where escaped % does not fit', 3, 'abc%%abcabc', '<bc')
+ statusline_test('escaped % is first', 1, '%%', '%')
+
end)
end)
diff --git a/test/unit/charset/vim_str2nr_spec.lua b/test/unit/charset/vim_str2nr_spec.lua
new file mode 100644
index 0000000000..891e6def09
--- /dev/null
+++ b/test/unit/charset/vim_str2nr_spec.lua
@@ -0,0 +1,320 @@
+local helpers = require("test.unit.helpers")(after_each)
+local bit = require('bit')
+
+local itp = helpers.gen_itp(it)
+
+local child_call_once = helpers.child_call_once
+local cimport = helpers.cimport
+local ffi = helpers.ffi
+
+local lib = cimport('./src/nvim/charset.h')
+
+local ARGTYPES
+
+child_call_once(function()
+ ARGTYPES = {
+ num = ffi.typeof('varnumber_T[1]'),
+ unum = ffi.typeof('uvarnumber_T[1]'),
+ pre = ffi.typeof('int[1]'),
+ len = ffi.typeof('int[1]'),
+ }
+end)
+
+local icnt = -42
+local ucnt = 4242
+
+local function arginit(arg)
+ if arg == 'unum' then
+ ucnt = ucnt + 1
+ return ARGTYPES[arg]({ucnt})
+ else
+ icnt = icnt - 1
+ return ARGTYPES[arg]({icnt})
+ end
+end
+
+local function argreset(arg, args)
+ if arg == 'unum' then
+ ucnt = ucnt + 1
+ args[arg][0] = ucnt
+ else
+ icnt = icnt - 1
+ args[arg][0] = icnt
+ end
+end
+
+local function test_vim_str2nr(s, what, exp, maxlen)
+ local bits = {}
+ for k, _ in pairs(exp) do
+ bits[#bits + 1] = k
+ end
+ maxlen = maxlen or #s
+ local args = {}
+ for k, _ in pairs(ARGTYPES) do
+ args[k] = arginit(k)
+ end
+ for case = 0, ((2 ^ (#bits)) - 1) do
+ local cv = {}
+ for b = 0, (#bits - 1) do
+ if bit.band(case, (2 ^ b)) == 0 then
+ local k = bits[b + 1]
+ argreset(k, args)
+ cv[k] = args[k]
+ end
+ end
+ lib.vim_str2nr(s, cv.pre, cv.len, what, cv.num, cv.unum, maxlen)
+ for cck, ccv in pairs(cv) do
+ if exp[cck] ~= tonumber(ccv[0]) then
+ error(('Failed check (%s = %d) in test (s=%s, w=%u, m=%d): %d'):format(
+ cck, exp[cck], s, tonumber(what), maxlen, tonumber(ccv[0])
+ ))
+ end
+ end
+ end
+end
+
+local _itp = itp
+itp = function(...)
+ collectgarbage('restart')
+ _itp(...)
+end
+
+describe('vim_str2nr()', function()
+ itp('works fine when it has nothing to do', function()
+ test_vim_str2nr('', 0, {len = 0, num = 0, unum = 0, pre = 0}, 0)
+ test_vim_str2nr('', lib.STR2NR_ALL, {len = 0, num = 0, unum = 0, pre = 0}, 0)
+ test_vim_str2nr('', lib.STR2NR_BIN, {len = 0, num = 0, unum = 0, pre = 0}, 0)
+ test_vim_str2nr('', lib.STR2NR_OCT, {len = 0, num = 0, unum = 0, pre = 0}, 0)
+ test_vim_str2nr('', lib.STR2NR_HEX, {len = 0, num = 0, unum = 0, pre = 0}, 0)
+ test_vim_str2nr('', lib.STR2NR_FORCE + lib.STR2NR_DEC, {len = 0, num = 0, unum = 0, pre = 0}, 0)
+ test_vim_str2nr('', lib.STR2NR_FORCE + lib.STR2NR_BIN, {len = 0, num = 0, unum = 0, pre = 0}, 0)
+ test_vim_str2nr('', lib.STR2NR_FORCE + lib.STR2NR_OCT, {len = 0, num = 0, unum = 0, pre = 0}, 0)
+ test_vim_str2nr('', lib.STR2NR_FORCE + lib.STR2NR_HEX, {len = 0, num = 0, unum = 0, pre = 0}, 0)
+ end)
+ itp('works with decimal numbers', function()
+ for _, flags in ipairs({
+ 0,
+ lib.STR2NR_BIN,
+ lib.STR2NR_OCT,
+ lib.STR2NR_HEX,
+ lib.STR2NR_BIN + lib.STR2NR_OCT,
+ lib.STR2NR_BIN + lib.STR2NR_HEX,
+ lib.STR2NR_OCT + lib.STR2NR_HEX,
+ lib.STR2NR_ALL,
+ lib.STR2NR_FORCE + lib.STR2NR_DEC,
+ }) do
+ -- Check that all digits are recognized
+ test_vim_str2nr( '12345', flags, {len = 5, num = 12345, unum = 12345, pre = 0}, 0)
+ test_vim_str2nr( '67890', flags, {len = 5, num = 67890, unum = 67890, pre = 0}, 0)
+ test_vim_str2nr( '12345A', flags, {len = 5, num = 12345, unum = 12345, pre = 0}, 0)
+ test_vim_str2nr( '67890A', flags, {len = 5, num = 67890, unum = 67890, pre = 0}, 0)
+
+ test_vim_str2nr( '42', flags, {len = 2, num = 42, unum = 42, pre = 0}, 0)
+ test_vim_str2nr( '42', flags, {len = 1, num = 4, unum = 4, pre = 0}, 1)
+ test_vim_str2nr( '42', flags, {len = 2, num = 42, unum = 42, pre = 0}, 2)
+ test_vim_str2nr( '42', flags, {len = 2, num = 42, unum = 42, pre = 0}, 3) -- includes NUL byte in maxlen
+
+ test_vim_str2nr( '42x', flags, {len = 2, num = 42, unum = 42, pre = 0}, 0)
+ test_vim_str2nr( '42x', flags, {len = 2, num = 42, unum = 42, pre = 0}, 3)
+
+ test_vim_str2nr('-42', flags, {len = 3, num = -42, unum = 42, pre = 0}, 3)
+ test_vim_str2nr('-42', flags, {len = 1, num = 0, unum = 0, pre = 0}, 1)
+
+ test_vim_str2nr('-42x', flags, {len = 3, num = -42, unum = 42, pre = 0}, 0)
+ test_vim_str2nr('-42x', flags, {len = 3, num = -42, unum = 42, pre = 0}, 4)
+ end
+ end)
+ itp('works with binary numbers', function()
+ for _, flags in ipairs({
+ lib.STR2NR_BIN,
+ lib.STR2NR_BIN + lib.STR2NR_OCT,
+ lib.STR2NR_BIN + lib.STR2NR_HEX,
+ lib.STR2NR_ALL,
+ lib.STR2NR_FORCE + lib.STR2NR_BIN,
+ }) do
+ local bin
+ local BIN
+ if flags > lib.STR2NR_FORCE then
+ bin = 0
+ BIN = 0
+ else
+ bin = ('b'):byte()
+ BIN = ('B'):byte()
+ end
+
+ test_vim_str2nr( '0b101', flags, {len = 5, num = 5, unum = 5, pre = bin}, 0)
+ test_vim_str2nr( '0b101', flags, {len = 1, num = 0, unum = 0, pre = 0 }, 1)
+ test_vim_str2nr( '0b101', flags, {len = 1, num = 0, unum = 0, pre = 0 }, 2)
+ test_vim_str2nr( '0b101', flags, {len = 3, num = 1, unum = 1, pre = bin}, 3)
+ test_vim_str2nr( '0b101', flags, {len = 4, num = 2, unum = 2, pre = bin}, 4)
+ test_vim_str2nr( '0b101', flags, {len = 5, num = 5, unum = 5, pre = bin}, 5)
+ test_vim_str2nr( '0b101', flags, {len = 5, num = 5, unum = 5, pre = bin}, 6)
+
+ test_vim_str2nr( '0b1012', flags, {len = 5, num = 5, unum = 5, pre = bin}, 0)
+ test_vim_str2nr( '0b1012', flags, {len = 5, num = 5, unum = 5, pre = bin}, 6)
+
+ test_vim_str2nr('-0b101', flags, {len = 6, num = -5, unum = 5, pre = bin}, 0)
+ test_vim_str2nr('-0b101', flags, {len = 1, num = 0, unum = 0, pre = 0 }, 1)
+ test_vim_str2nr('-0b101', flags, {len = 2, num = 0, unum = 0, pre = 0 }, 2)
+ test_vim_str2nr('-0b101', flags, {len = 2, num = 0, unum = 0, pre = 0 }, 3)
+ test_vim_str2nr('-0b101', flags, {len = 4, num = -1, unum = 1, pre = bin}, 4)
+ test_vim_str2nr('-0b101', flags, {len = 5, num = -2, unum = 2, pre = bin}, 5)
+ test_vim_str2nr('-0b101', flags, {len = 6, num = -5, unum = 5, pre = bin}, 6)
+ test_vim_str2nr('-0b101', flags, {len = 6, num = -5, unum = 5, pre = bin}, 7)
+
+ test_vim_str2nr('-0b1012', flags, {len = 6, num = -5, unum = 5, pre = bin}, 0)
+ test_vim_str2nr('-0b1012', flags, {len = 6, num = -5, unum = 5, pre = bin}, 7)
+
+ test_vim_str2nr( '0B101', flags, {len = 5, num = 5, unum = 5, pre = BIN}, 0)
+ test_vim_str2nr( '0B101', flags, {len = 1, num = 0, unum = 0, pre = 0 }, 1)
+ test_vim_str2nr( '0B101', flags, {len = 1, num = 0, unum = 0, pre = 0 }, 2)
+ test_vim_str2nr( '0B101', flags, {len = 3, num = 1, unum = 1, pre = BIN}, 3)
+ test_vim_str2nr( '0B101', flags, {len = 4, num = 2, unum = 2, pre = BIN}, 4)
+ test_vim_str2nr( '0B101', flags, {len = 5, num = 5, unum = 5, pre = BIN}, 5)
+ test_vim_str2nr( '0B101', flags, {len = 5, num = 5, unum = 5, pre = BIN}, 6)
+
+ test_vim_str2nr( '0B1012', flags, {len = 5, num = 5, unum = 5, pre = BIN}, 0)
+ test_vim_str2nr( '0B1012', flags, {len = 5, num = 5, unum = 5, pre = BIN}, 6)
+
+ test_vim_str2nr('-0B101', flags, {len = 6, num = -5, unum = 5, pre = BIN}, 0)
+ test_vim_str2nr('-0B101', flags, {len = 1, num = 0, unum = 0, pre = 0 }, 1)
+ test_vim_str2nr('-0B101', flags, {len = 2, num = 0, unum = 0, pre = 0 }, 2)
+ test_vim_str2nr('-0B101', flags, {len = 2, num = 0, unum = 0, pre = 0 }, 3)
+ test_vim_str2nr('-0B101', flags, {len = 4, num = -1, unum = 1, pre = BIN}, 4)
+ test_vim_str2nr('-0B101', flags, {len = 5, num = -2, unum = 2, pre = BIN}, 5)
+ test_vim_str2nr('-0B101', flags, {len = 6, num = -5, unum = 5, pre = BIN}, 6)
+ test_vim_str2nr('-0B101', flags, {len = 6, num = -5, unum = 5, pre = BIN}, 7)
+
+ test_vim_str2nr('-0B1012', flags, {len = 6, num = -5, unum = 5, pre = BIN}, 0)
+ test_vim_str2nr('-0B1012', flags, {len = 6, num = -5, unum = 5, pre = BIN}, 7)
+
+ if flags > lib.STR2NR_FORCE then
+ test_vim_str2nr('-101', flags, {len = 4, num = -5, unum = 5, pre = 0}, 0)
+ end
+ end
+ end)
+ itp('works with octal numbers', function()
+ for _, flags in ipairs({
+ lib.STR2NR_OCT,
+ lib.STR2NR_OCT + lib.STR2NR_BIN,
+ lib.STR2NR_OCT + lib.STR2NR_HEX,
+ lib.STR2NR_ALL,
+ lib.STR2NR_FORCE + lib.STR2NR_OCT,
+ }) do
+ local oct
+ if flags > lib.STR2NR_FORCE then
+ oct = 0
+ else
+ oct = ('0'):byte()
+ end
+
+ -- Check that all digits are recognized
+ test_vim_str2nr( '012345670', flags, {len = 9, num = 2739128, unum = 2739128, pre = oct}, 0)
+
+ test_vim_str2nr( '054', flags, {len = 3, num = 44, unum = 44, pre = oct}, 0)
+ test_vim_str2nr( '054', flags, {len = 1, num = 0, unum = 0, pre = 0 }, 1)
+ test_vim_str2nr( '054', flags, {len = 2, num = 5, unum = 5, pre = oct}, 2)
+ test_vim_str2nr( '054', flags, {len = 3, num = 44, unum = 44, pre = oct}, 3)
+ test_vim_str2nr( '0548', flags, {len = 3, num = 44, unum = 44, pre = oct}, 3)
+ test_vim_str2nr( '054', flags, {len = 3, num = 44, unum = 44, pre = oct}, 4)
+
+ test_vim_str2nr( '054x', flags, {len = 3, num = 44, unum = 44, pre = oct}, 4)
+ test_vim_str2nr( '054x', flags, {len = 3, num = 44, unum = 44, pre = oct}, 0)
+
+ test_vim_str2nr('-054', flags, {len = 4, num = -44, unum = 44, pre = oct}, 0)
+ test_vim_str2nr('-054', flags, {len = 1, num = 0, unum = 0, pre = 0 }, 1)
+ test_vim_str2nr('-054', flags, {len = 2, num = 0, unum = 0, pre = 0 }, 2)
+ test_vim_str2nr('-054', flags, {len = 3, num = -5, unum = 5, pre = oct}, 3)
+ test_vim_str2nr('-054', flags, {len = 4, num = -44, unum = 44, pre = oct}, 4)
+ test_vim_str2nr('-0548', flags, {len = 4, num = -44, unum = 44, pre = oct}, 4)
+ test_vim_str2nr('-054', flags, {len = 4, num = -44, unum = 44, pre = oct}, 5)
+
+ test_vim_str2nr('-054x', flags, {len = 4, num = -44, unum = 44, pre = oct}, 5)
+ test_vim_str2nr('-054x', flags, {len = 4, num = -44, unum = 44, pre = oct}, 0)
+
+ if flags > lib.STR2NR_FORCE then
+ test_vim_str2nr('-54', flags, {len = 3, num = -44, unum = 44, pre = 0}, 0)
+ test_vim_str2nr('-0548', flags, {len = 4, num = -44, unum = 44, pre = 0}, 5)
+ test_vim_str2nr('-0548', flags, {len = 4, num = -44, unum = 44, pre = 0}, 0)
+ else
+ test_vim_str2nr('-0548', flags, {len = 5, num = -548, unum = 548, pre = 0}, 5)
+ test_vim_str2nr('-0548', flags, {len = 5, num = -548, unum = 548, pre = 0}, 0)
+ end
+ end
+ end)
+ itp('works with hexadecimal numbers', function()
+ for _, flags in ipairs({
+ lib.STR2NR_HEX,
+ lib.STR2NR_HEX + lib.STR2NR_BIN,
+ lib.STR2NR_HEX + lib.STR2NR_OCT,
+ lib.STR2NR_ALL,
+ lib.STR2NR_FORCE + lib.STR2NR_HEX,
+ }) do
+ local hex
+ local HEX
+ if flags > lib.STR2NR_FORCE then
+ hex = 0
+ HEX = 0
+ else
+ hex = ('x'):byte()
+ HEX = ('X'):byte()
+ end
+
+ -- Check that all digits are recognized
+ test_vim_str2nr('0x12345', flags, {len = 7, num = 74565, unum = 74565, pre = hex}, 0)
+ test_vim_str2nr('0x67890', flags, {len = 7, num = 424080, unum = 424080, pre = hex}, 0)
+ test_vim_str2nr('0xABCDEF', flags, {len = 8, num = 11259375, unum = 11259375, pre = hex}, 0)
+ test_vim_str2nr('0xabcdef', flags, {len = 8, num = 11259375, unum = 11259375, pre = hex}, 0)
+
+ test_vim_str2nr( '0x101', flags, {len = 5, num = 257, unum =257, pre = hex}, 0)
+ test_vim_str2nr( '0x101', flags, {len = 1, num = 0, unum = 0, pre = 0 }, 1)
+ test_vim_str2nr( '0x101', flags, {len = 1, num = 0, unum = 0, pre = 0 }, 2)
+ test_vim_str2nr( '0x101', flags, {len = 3, num = 1, unum = 1, pre = hex}, 3)
+ test_vim_str2nr( '0x101', flags, {len = 4, num = 16, unum = 16, pre = hex}, 4)
+ test_vim_str2nr( '0x101', flags, {len = 5, num = 257, unum =257, pre = hex}, 5)
+ test_vim_str2nr( '0x101', flags, {len = 5, num = 257, unum =257, pre = hex}, 6)
+
+ test_vim_str2nr( '0x101G', flags, {len = 5, num = 257, unum =257, pre = hex}, 0)
+ test_vim_str2nr( '0x101G', flags, {len = 5, num = 257, unum =257, pre = hex}, 6)
+
+ test_vim_str2nr('-0x101', flags, {len = 6, num =-257, unum =257, pre = hex}, 0)
+ test_vim_str2nr('-0x101', flags, {len = 1, num = 0, unum = 0, pre = 0 }, 1)
+ test_vim_str2nr('-0x101', flags, {len = 2, num = 0, unum = 0, pre = 0 }, 2)
+ test_vim_str2nr('-0x101', flags, {len = 2, num = 0, unum = 0, pre = 0 }, 3)
+ test_vim_str2nr('-0x101', flags, {len = 4, num = -1, unum = 1, pre = hex}, 4)
+ test_vim_str2nr('-0x101', flags, {len = 5, num = -16, unum = 16, pre = hex}, 5)
+ test_vim_str2nr('-0x101', flags, {len = 6, num =-257, unum =257, pre = hex}, 6)
+ test_vim_str2nr('-0x101', flags, {len = 6, num =-257, unum =257, pre = hex}, 7)
+
+ test_vim_str2nr('-0x101G', flags, {len = 6, num =-257, unum =257, pre = hex}, 0)
+ test_vim_str2nr('-0x101G', flags, {len = 6, num =-257, unum =257, pre = hex}, 7)
+
+ test_vim_str2nr( '0X101', flags, {len = 5, num = 257, unum =257, pre = HEX}, 0)
+ test_vim_str2nr( '0X101', flags, {len = 1, num = 0, unum = 0, pre = 0 }, 1)
+ test_vim_str2nr( '0X101', flags, {len = 1, num = 0, unum = 0, pre = 0 }, 2)
+ test_vim_str2nr( '0X101', flags, {len = 3, num = 1, unum = 1, pre = HEX}, 3)
+ test_vim_str2nr( '0X101', flags, {len = 4, num = 16, unum = 16, pre = HEX}, 4)
+ test_vim_str2nr( '0X101', flags, {len = 5, num = 257, unum =257, pre = HEX}, 5)
+ test_vim_str2nr( '0X101', flags, {len = 5, num = 257, unum =257, pre = HEX}, 6)
+
+ test_vim_str2nr( '0X101G', flags, {len = 5, num = 257, unum =257, pre = HEX}, 0)
+ test_vim_str2nr( '0X101G', flags, {len = 5, num = 257, unum =257, pre = HEX}, 6)
+
+ test_vim_str2nr('-0X101', flags, {len = 6, num =-257, unum =257, pre = HEX}, 0)
+ test_vim_str2nr('-0X101', flags, {len = 1, num = 0, unum = 0, pre = 0 }, 1)
+ test_vim_str2nr('-0X101', flags, {len = 2, num = 0, unum = 0, pre = 0 }, 2)
+ test_vim_str2nr('-0X101', flags, {len = 2, num = 0, unum = 0, pre = 0 }, 3)
+ test_vim_str2nr('-0X101', flags, {len = 4, num = -1, unum = 1, pre = HEX}, 4)
+ test_vim_str2nr('-0X101', flags, {len = 5, num = -16, unum = 16, pre = HEX}, 5)
+ test_vim_str2nr('-0X101', flags, {len = 6, num =-257, unum =257, pre = HEX}, 6)
+ test_vim_str2nr('-0X101', flags, {len = 6, num =-257, unum =257, pre = HEX}, 7)
+
+ test_vim_str2nr('-0X101G', flags, {len = 6, num =-257, unum =257, pre = HEX}, 0)
+ test_vim_str2nr('-0X101G', flags, {len = 6, num =-257, unum =257, pre = HEX}, 7)
+
+ if flags > lib.STR2NR_FORCE then
+ test_vim_str2nr('-101', flags, {len = 4, num = -257, unum = 257, pre = 0}, 0)
+ end
+ end
+ end)
+end)
diff --git a/test/unit/eval/decode_spec.lua b/test/unit/eval/decode_spec.lua
index d94d809c14..0c444b33f2 100644
--- a/test/unit/eval/decode_spec.lua
+++ b/test/unit/eval/decode_spec.lua
@@ -1,38 +1,24 @@
-local helpers = require('test.unit.helpers')
+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')
describe('json_decode_string()', function()
- local saved_p_enc = nil
-
- before_each(function()
- saved_p_enc = decode.p_enc
- end)
-
- after_each(function()
- decode.emsg_silent = 0
- decode.p_enc = saved_p_enc
- while decode.delete_first_msg() == 1 do
- -- Delete all messages
- end
- end)
-
local char = function(c)
return ffi.gc(decode.xmemdup(c, 1), decode.xfree)
end
- it('does not overflow when running with `n…`, `t…`, `f…`', function()
+ itp('does not overflow when running with `n…`, `t…`, `f…`', function()
local rettv = ffi.new('typval_T', {v_type=decode.VAR_UNKNOWN})
decode.emsg_silent = 1
- -- This will not crash, but if `len` argument will be ignored it will parse
+ -- This will not crash, but if `len` argument will be ignored it will parse
-- `null` as `null` and if not it will parse `null` as `n`.
eq(0, decode.json_decode_string('null', 1, rettv))
eq(decode.VAR_UNKNOWN, rettv.v_type)
@@ -56,7 +42,7 @@ describe('json_decode_string()', function()
eq(decode.VAR_UNKNOWN, rettv.v_type)
end)
- it('does not overflow and crash when running with `n`, `t`, `f`', function()
+ itp('does not overflow and crash when running with `n`, `t`, `f`', function()
local rettv = ffi.new('typval_T', {v_type=decode.VAR_UNKNOWN})
decode.emsg_silent = 1
eq(0, decode.json_decode_string(char('n'), 1, rettv))
@@ -67,7 +53,7 @@ describe('json_decode_string()', function()
eq(decode.VAR_UNKNOWN, rettv.v_type)
end)
- it('does not overflow when running with `"…`', function()
+ itp('does not overflow when running with `"…`', function()
local rettv = ffi.new('typval_T', {v_type=decode.VAR_UNKNOWN})
decode.emsg_silent = 1
eq(0, decode.json_decode_string('"t"', 2, rettv))
@@ -84,7 +70,8 @@ describe('json_decode_string()', function()
eq(msg, ffi.string(decode.last_msg_hist.msg))
end
- it('does not overflow in error messages', function()
+ itp('does not overflow in error messages', function()
+ 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,
@@ -117,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')
@@ -129,11 +112,11 @@ describe('json_decode_string()', function()
check_failure('[1test', 2, 'E474: Unexpected end of input: [1')
end)
- it('does not overflow with `-`', function()
+ itp('does not overflow with `-`', function()
check_failure('-0', 1, 'E474: Missing number after minus sign: -')
end)
- it('does not overflow and crash when running with `"`', function()
+ itp('does not overflow and crash when running with `"`', function()
local rettv = ffi.new('typval_T', {v_type=decode.VAR_UNKNOWN})
decode.emsg_silent = 1
eq(0, decode.json_decode_string(char('"'), 1, rettv))
diff --git a/test/unit/eval/encode_spec.lua b/test/unit/eval/encode_spec.lua
index 98fc8305e0..058c55093e 100644
--- a/test/unit/eval/encode_spec.lua
+++ b/test/unit/eval/encode_spec.lua
@@ -1,4 +1,5 @@
-local helpers = require('test.unit.helpers')
+local helpers = require('test.unit.helpers')(after_each)
+local itp = helpers.gen_itp(it)
local eval_helpers = require('test.unit.eval.helpers')
local cimport = helpers.cimport
@@ -18,25 +19,25 @@ describe('encode_list_write()', function()
return encode.encode_list_write(l, to_cstr(s), #s)
end
- it('writes empty string', function()
+ itp('writes empty string', function()
local l = list()
eq(0, encode_list_write(l, ''))
eq({[type_key]=list_type}, lst2tbl(l))
end)
- it('writes ASCII string literal with printable characters', function()
+ itp('writes ASCII string literal with printable characters', function()
local l = list()
eq(0, encode_list_write(l, 'abc'))
eq({'abc'}, lst2tbl(l))
end)
- it('writes string starting with NL', function()
+ itp('writes string starting with NL', function()
local l = list()
eq(0, encode_list_write(l, '\nabc'))
eq({null_string, 'abc'}, lst2tbl(l))
end)
- it('writes string starting with NL twice', function()
+ itp('writes string starting with NL twice', function()
local l = list()
eq(0, encode_list_write(l, '\nabc'))
eq({null_string, 'abc'}, lst2tbl(l))
@@ -44,13 +45,13 @@ describe('encode_list_write()', function()
eq({null_string, 'abc', 'abc'}, lst2tbl(l))
end)
- it('writes string ending with NL', function()
+ itp('writes string ending with NL', function()
local l = list()
eq(0, encode_list_write(l, 'abc\n'))
eq({'abc', null_string}, lst2tbl(l))
end)
- it('writes string ending with NL twice', function()
+ itp('writes string ending with NL twice', function()
local l = list()
eq(0, encode_list_write(l, 'abc\n'))
eq({'abc', null_string}, lst2tbl(l))
@@ -58,7 +59,7 @@ describe('encode_list_write()', function()
eq({'abc', 'abc', null_string}, lst2tbl(l))
end)
- it('writes string starting, ending and containing NL twice', function()
+ itp('writes string starting, ending and containing NL twice', function()
local l = list()
eq(0, encode_list_write(l, '\na\nb\n'))
eq({null_string, 'a', 'b', null_string}, lst2tbl(l))
@@ -66,7 +67,7 @@ describe('encode_list_write()', function()
eq({null_string, 'a', 'b', null_string, 'a', 'b', null_string}, lst2tbl(l))
end)
- it('writes string starting, ending and containing NUL with NL between twice', function()
+ itp('writes string starting, ending and containing NUL with NL between twice', function()
local l = list()
eq(0, encode_list_write(l, '\0\n\0\n\0'))
eq({'\n', '\n', '\n'}, lst2tbl(l))
@@ -74,7 +75,7 @@ describe('encode_list_write()', function()
eq({'\n', '\n', '\n\n', '\n', '\n'}, lst2tbl(l))
end)
- it('writes string starting, ending and containing NL with NUL between twice', function()
+ itp('writes string starting, ending and containing NL with NUL between twice', function()
local l = list()
eq(0, encode_list_write(l, '\n\0\n\0\n'))
eq({null_string, '\n', '\n', null_string}, lst2tbl(l))
@@ -82,7 +83,7 @@ describe('encode_list_write()', function()
eq({null_string, '\n', '\n', null_string, '\n', '\n', null_string}, lst2tbl(l))
end)
- it('writes string containing a single NL twice', function()
+ itp('writes string containing a single NL twice', function()
local l = list()
eq(0, encode_list_write(l, '\n'))
eq({null_string, null_string}, lst2tbl(l))
@@ -90,7 +91,7 @@ describe('encode_list_write()', function()
eq({null_string, null_string, null_string}, lst2tbl(l))
end)
- it('writes string containing a few NLs twice', function()
+ itp('writes string containing a few NLs twice', function()
local l = list()
eq(0, encode_list_write(l, '\n\n\n'))
eq({null_string, null_string, null_string, null_string}, lst2tbl(l))
diff --git a/test/unit/eval/helpers.lua b/test/unit/eval/helpers.lua
index 45fbf8da5c..3d1c42c3a0 100644
--- a/test/unit/eval/helpers.lua
+++ b/test/unit/eval/helpers.lua
@@ -1,15 +1,19 @@
-local helpers = require('test.unit.helpers')
+local helpers = require('test.unit.helpers')(nil)
+local ptr2key = helpers.ptr2key
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/eval_defs.h')
+local eval = cimport('./src/nvim/eval.h', './src/nvim/eval/typval.h',
+ './src/nvim/hashtab.h', './src/nvim/memory.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'}
@@ -18,87 +22,206 @@ local flt_type = {[true]='flt type'}
local nil_value = {[true]='nil'}
-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 typ = type(val)
- if typ == 'string' then
- eval.list_append_string(ret, to_cstr(val))
- elseif typ == 'table' and val == null_string then
- eval.list_append_string(ret, nil)
- elseif typ == 'table' and val == null_list then
- eval.list_append_list(ret, nil)
- elseif typ == 'table' and val[type_key] == list_type then
- local itemlist = ffi.gc(list(table.unpack(val)), nil)
- eq(1, itemlist.lv_refcount)
- itemlist.lv_refcount = 0
- eval.list_append_list(ret, itemlist)
- else
- assert(false, 'Not implemented yet')
+local lua2typvalt
+
+local function tv_list_item_alloc()
+ return ffi.cast('listitem_T*', eval.xmalloc(ffi.sizeof('listitem_T')))
+end
+
+local function tv_list_item_free(li)
+ eval.tv_clear(li.li_tv)
+ eval.xfree(li)
+end
+
+local function li_alloc(nogc)
+ local gcfunc = tv_list_item_free
+ if nogc then gcfunc = nil end
+ local li = ffi.gc(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 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 = tv_list_item_alloc()
+ item_li.li_tv = item_tv
+ eval.tv_list_append(l, item_li)
+ end
+ 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 ret
+ return d
end
-local special_tab = {
- [eval.kSpecialVarFalse] = false,
- [eval.kSpecialVarNull] = nil_value,
- [eval.kSpecialVarTrue] = true,
-}
+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 lst2tbl
local dct2tbl
-local typvalt2lua_tab
+local typvalt2lua
-typvalt2lua_tab = {
- [tonumber(eval.VAR_SPECIAL)] = function(t)
- return special_tab[t.vval.v_special]
- end,
- [tonumber(eval.VAR_NUMBER)] = function(t)
- return {[type_key]=int_type, value=tonumber(t.vval.v_number)}
- end,
- [tonumber(eval.VAR_FLOAT)] = function(t)
- return tonumber(t.vval.v_float)
- end,
- [tonumber(eval.VAR_STRING)] = function(t)
- local str = t.vval.v_string
- if str == nil then
- return null_string
- else
- return ffi.string(str)
+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
- end,
- [tonumber(eval.VAR_LIST)] = function(t)
- return lst2tbl(t.vval.v_list)
- end,
- [tonumber(eval.VAR_DICT)] = function(t)
- return dct2tbl(t.vval.v_dict)
- end,
- [tonumber(eval.VAR_FUNC)] = function(t)
- return {[type_key]=func_type, value=typvalt2lua_tab[eval.VAR_STRING](t)}
- 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()
+ if typvalt2lua_tab then
+ return
+ end
+ typvalt2lua_tab = {
+ [tonumber(eval.VAR_SPECIAL)] = function(t)
+ return ({
+ [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)}
+ end,
+ [tonumber(eval.VAR_FLOAT)] = function(t)
+ return tonumber(t.vval.v_float)
+ end,
+ [tonumber(eval.VAR_STRING)] = function(t)
+ local str = t.vval.v_string
+ if str == nil then
+ return null_string
+ else
+ return ffi.string(str)
+ end
+ end,
+ [tonumber(eval.VAR_LIST)] = function(t, processed)
+ return lst2tbl(t.vval.v_list, processed)
+ end,
+ [tonumber(eval.VAR_DICT)] = function(t, processed)
+ return dct2tbl(t.vval.v_dict, processed)
+ end,
+ [tonumber(eval.VAR_FUNC)] = function(t, processed)
+ return {[type_key]=func_type, value=typvalt2lua_tab[eval.VAR_STRING](t, processed or {})}
+ end,
+ [tonumber(eval.VAR_PARTIAL)] = function(t, processed)
+ local p_key = ptr2key(t)
+ if processed[p_key] then
+ return processed[p_key]
+ end
+ return partial2lua(t.vval.v_partial, processed)
+ end,
+ }
+end
-local typvalt2lua = function(t)
+typvalt2lua = function(t, processed)
+ typvalt2lua_tab_init()
return ((typvalt2lua_tab[tonumber(t.v_type)] or function(t_inner)
assert(false, 'Converting ' .. tonumber(t_inner.v_type) .. ' was not implemented yet')
- end)(t))
+ end)(t, processed or {}))
end
-lst2tbl = function(l)
- local ret = {[type_key]=list_type}
+local function list_iter(l)
+ local init_s = {
+ idx=0,
+ li=l.lv_first,
+ }
+ local function f(s, _)
+ -- (listitem_T *) NULL is equal to nil, but yet it is not false.
+ if s.li == nil then
+ return nil
+ end
+ local ret_li = s.li
+ s.li = s.li.li_next
+ s.idx = s.idx + 1
+ return s.idx, ret_li
+ end
+ return f, init_s, nil
+end
+
+local function list_items(l)
+ local ret = {}
+ for i, li in list_iter(l) do
+ ret[i] = li
+ end
+ return ret
+end
+
+lst2tbl = function(l, processed)
if l == nil then
- return ret
+ return null_list
+ end
+ processed = processed or {}
+ local p_key = ptr2key(l)
+ if processed[p_key] then
+ return processed[p_key]
end
- local li = l.lv_first
- -- (listitem_T *) NULL is equal to nil, but yet it is not false.
- while li ~= nil do
- ret[#ret + 1] = typvalt2lua(li.li_tv)
- li = li.li_next
+ local ret = {[type_key]=list_type}
+ processed[p_key] = ret
+ for i, li in list_iter(l) do
+ ret[i] = typvalt2lua(li.li_tv, processed)
end
if ret[1] then
ret[type_key] = nil
@@ -106,19 +229,75 @@ lst2tbl = function(l)
return ret
end
-dct2tbl = function(d)
- local ret = {d=d}
- assert(false, 'Converting dictionaries is not implemented yet')
+local hi_key_removed = nil
+
+local function dict_iter(d, return_hi)
+ hi_key_removed = hi_key_removed or eval._hash_key_removed()
+ local init_s = {
+ todo=d.dv_hashtab.ht_used,
+ hi=d.dv_hashtab.ht_array,
+ }
+ local function f(s, _)
+ if s.todo == 0 then return nil end
+ while s.todo > 0 do
+ if s.hi.hi_key ~= nil and s.hi.hi_key ~= hi_key_removed then
+ local key = ffi.string(s.hi.hi_key)
+ local ret
+ if return_hi then
+ ret = s.hi
+ else
+ ret = ffi.cast('dictitem_T*',
+ s.hi.hi_key - ffi.offsetof('dictitem_T', 'di_key'))
+ end
+ s.todo = s.todo - 1
+ s.hi = s.hi + 1
+ return key, ret
+ end
+ s.hi = s.hi + 1
+ end
+ end
+ return f, init_s, nil
+end
+
+local function first_di(d)
+ local f, init_s, v = dict_iter(d)
+ return select(2, f(init_s, v))
+end
+
+local function dict_items(d)
+ local ret = {[0]=0}
+ for k, hi in dict_iter(d) do
+ ret[k] = hi
+ ret[0] = ret[0] + 1
+ ret[ret[0]] = hi
+ end
return ret
end
-local lua2typvalt
+dct2tbl = function(d, processed)
+ if d == nil then
+ return null_dict
+ end
+ processed = processed or {}
+ local p_key = ptr2key(d)
+ if processed[p_key] then
+ return processed[p_key]
+ end
+ local ret = {}
+ processed[p_key] = ret
+ for k, di in dict_iter(d) do
+ ret[k] = typvalt2lua(di.di_tv, processed)
+ end
+ return ret
+end
local typvalt = function(typ, vval)
- if type(typ) == 'string' then
+ if typ == nil then
+ typ = eval.VAR_UNKNOWN
+ elseif type(typ) == 'string' then
typ = eval[typ]
end
- return ffi.gc(ffi.new('typval_T', {v_type=typ, vval=vval}), eval.clear_tv)
+ return ffi.gc(ffi.new('typval_T', {v_type=typ, vval=vval}), eval.tv_clear)
end
local lua2typvalt_type_tab = {
@@ -133,43 +312,61 @@ 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), 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
+ 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
+ processed[l].pt_refcount = processed[l].pt_refcount + 1
+ return typvalt(eval.VAR_PARTIAL, {v_partial=processed[l]})
+ end
+ if l.args or l.dict then
+ local pt = 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, {
+ v_string=eval.xmemdupz(to_cstr(l.value), #l.value)
+ })
end
- return ret
end,
}
+local special_vals = nil
+
lua2typvalt = function(l, processed)
+ if not special_vals then
+ special_vals = {
+ [null_string] = {'VAR_STRING', {v_string=ffi.cast('char_u*', nil)}},
+ [null_list] = {'VAR_LIST', {v_list=ffi.cast('list_T*', nil)}},
+ [null_dict] = {'VAR_DICT', {v_dict=ffi.cast('dict_T*', nil)}},
+ [nil_value] = {'VAR_SPECIAL', {v_special=eval.kSpecialVarNull}},
+ [true] = {'VAR_SPECIAL', {v_special=eval.kSpecialVarTrue}},
+ [false] = {'VAR_SPECIAL', {v_special=eval.kSpecialVarFalse}},
+ }
+
+ for k, v in pairs(special_vals) do
+ local tmp = function(typ, vval)
+ special_vals[k] = function()
+ return typvalt(eval[typ], vval)
+ end
+ end
+ tmp(v[1], v[2])
+ end
+ end
processed = processed or {}
if l == nil or l == nil_value then
- return typvalt(eval.VAR_SPECIAL, {v_special=eval.kSpecialVarNull})
+ return special_vals[nil_value]()
+ elseif special_vals[l] then
+ return special_vals[l]()
elseif type(l) == 'table' then
if l[type_key] then
return lua2typvalt_type_tab[l[type_key]](l, processed)
@@ -182,18 +379,148 @@ lua2typvalt = function(l, processed)
end
elseif type(l) == 'number' then
return typvalt(eval.VAR_FLOAT, {v_float=l})
- elseif type(l) == 'boolean' then
- return typvalt(eval.VAR_SPECIAL, {
- v_special=(l and eval.kSpecialVarTrue or eval.kSpecialVarFalse)
- })
elseif type(l) == 'string' then
return typvalt(eval.VAR_STRING, {v_string=eval.xmemdupz(to_cstr(l), #l)})
+ elseif type(l) == 'cdata' then
+ local tv = typvalt(eval.VAR_UNKNOWN)
+ eval.tv_copy(l, tv)
+ return tv
+ end
+end
+
+local void_ptr = ffi.typeof('void *')
+local function 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 = {
+ list = function(l) return {func='calloc', args={1, ffi.sizeof('list_T')}, ret=void(l)} end,
+ li = function(li) return {func='malloc', args={ffi.sizeof('listitem_T')}, ret=void(li)} end,
+ dict = function(d) return {func='malloc', args={ffi.sizeof('dict_T')}, ret=void(d)} end,
+ di = function(di, size)
+ 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)
+ 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={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(select('#', ...)),
+ 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,
list_type=list_type,
dict_type=dict_type,
func_type=func_type,
@@ -203,8 +530,10 @@ return {
nil_value=nil_value,
type_key=type_key,
+ locks_key=locks_key,
list=list,
+ dict=dict,
lst2tbl=lst2tbl,
dct2tbl=dct2tbl,
@@ -212,4 +541,24 @@ return {
typvalt2lua=typvalt2lua,
typvalt=typvalt,
+
+ li_alloc=li_alloc,
+ tv_list_item_free=tv_list_item_free,
+
+ dict_iter=dict_iter,
+ list_iter=list_iter,
+ first_di=first_di,
+
+ alloc_logging_helpers=alloc_logging_helpers,
+
+ list_items=list_items,
+ dict_items=dict_items,
+
+ 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 4c5184995c..7aa0f0f6e6 100644
--- a/test/unit/eval/tricks_spec.lua
+++ b/test/unit/eval/tricks_spec.lua
@@ -1,21 +1,18 @@
-local helpers = require('test.unit.helpers')
+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()
- it('is produced by $XXX_UNEXISTENT_VAR_XXX', function()
+ itp('is produced by $XXX_UNEXISTENT_VAR_XXX', function()
-- Required for various tests which need to check whether typval_T with NULL
-- string works correctly. This test checks that unexistent environment
-- variable produces NULL string, not that some specific environment
@@ -24,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)
- it('is produced by v:_null_list', function()
- local rettv = eval_expr('v:_null_list')
+ itp('is produced by v:_null_list', function()
+ local rettv = eval0('v:_null_list')
eq(eval.VAR_LIST, rettv.v_type)
eq(nil, rettv.vval.v_list)
end)
- it('is produced by v:_null_dict', function()
- local rettv = eval_expr('v:_null_dict')
+ itp('is produced by v:_null_dict', function()
+ 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
new file mode 100644
index 0000000000..ca37301b32
--- /dev/null
+++ b/test/unit/eval/tv_clear_spec.lua
@@ -0,0 +1,128 @@
+local helpers = require('test.unit.helpers')(after_each)
+local itp = helpers.gen_itp(it)
+local eval_helpers = require('test.unit.eval.helpers')
+
+local alloc_log_new = helpers.alloc_log_new
+local cimport = helpers.cimport
+local ffi = helpers.ffi
+local eq = helpers.eq
+
+local a = eval_helpers.alloc_logging_helpers
+local type_key = eval_helpers.type_key
+local list_type = eval_helpers.list_type
+local list_items = eval_helpers.list_items
+local dict_items = eval_helpers.dict_items
+local lua2typvalt = eval_helpers.lua2typvalt
+
+local lib = cimport('./src/nvim/eval/typval.h', './src/nvim/eval.h')
+
+local alloc_log = alloc_log_new()
+
+before_each(function()
+ alloc_log:before_each()
+end)
+
+after_each(function()
+ alloc_log:after_each()
+end)
+
+describe('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}
+ local list_tv = ffi.gc(lua2typvalt(list), nil)
+ local list_p = list_tv.vval.v_list
+ local lis = list_items(list_p)
+ local list_inner_p = lis[1].li_tv.vval.v_list
+ local lis_inner = list_items(list_inner_p)
+ alloc_log:check({
+ a.list(list_p),
+ a.list(list_inner_p),
+ a.li(lis_inner[1]),
+ a.li(lis[1]),
+ a.li(lis[2]),
+ a.li(lis[3]),
+ })
+ eq(3, list_inner_p.lv_refcount)
+ lib.tv_clear(list_tv)
+ alloc_log:check({
+ a.freed(lis_inner[1]),
+ a.freed(list_inner_p),
+ a.freed(lis[1]),
+ a.freed(lis[2]),
+ a.freed(lis[3]),
+ a.freed(list_p),
+ })
+ end)
+ itp('successfully frees all lists in [&l [], *l, *l]', function()
+ local l_inner = {[type_key]=list_type}
+ local list = {l_inner, l_inner, l_inner}
+ local list_tv = ffi.gc(lua2typvalt(list), nil)
+ local list_p = list_tv.vval.v_list
+ local lis = list_items(list_p)
+ local list_inner_p = lis[1].li_tv.vval.v_list
+ alloc_log:check({
+ a.list(list_p),
+ a.list(list_inner_p),
+ a.li(lis[1]),
+ a.li(lis[2]),
+ a.li(lis[3]),
+ })
+ eq(3, list_inner_p.lv_refcount)
+ lib.tv_clear(list_tv)
+ alloc_log:check({
+ a.freed(list_inner_p),
+ a.freed(lis[1]),
+ a.freed(lis[2]),
+ a.freed(lis[3]),
+ a.freed(list_p),
+ })
+ end)
+ itp('successfully frees all dictionaries in [&d {}, *d]', function()
+ local d_inner = {}
+ local list = {d_inner, d_inner}
+ local list_tv = ffi.gc(lua2typvalt(list), nil)
+ local list_p = list_tv.vval.v_list
+ local lis = list_items(list_p)
+ local dict_inner_p = lis[1].li_tv.vval.v_dict
+ alloc_log:check({
+ a.list(list_p),
+ a.dict(dict_inner_p),
+ a.li(lis[1]),
+ a.li(lis[2]),
+ })
+ eq(2, dict_inner_p.dv_refcount)
+ lib.tv_clear(list_tv)
+ alloc_log:check({
+ a.freed(dict_inner_p),
+ a.freed(lis[1]),
+ a.freed(lis[2]),
+ a.freed(list_p),
+ })
+ end)
+ itp('successfully frees all dictionaries in [&d {a: 1}, *d]', function()
+ local d_inner = {a=1}
+ local list = {d_inner, d_inner}
+ local list_tv = ffi.gc(lua2typvalt(list), nil)
+ local list_p = list_tv.vval.v_list
+ local lis = list_items(list_p)
+ local dict_inner_p = lis[1].li_tv.vval.v_dict
+ local dis = dict_items(dict_inner_p)
+ alloc_log:check({
+ a.list(list_p),
+ a.dict(dict_inner_p),
+ a.di(dis.a, 1),
+ a.li(lis[1]),
+ a.li(lis[2]),
+ })
+ eq(2, dict_inner_p.dv_refcount)
+ lib.tv_clear(list_tv)
+ alloc_log:check({
+ a.freed(dis.a),
+ a.freed(dict_inner_p),
+ a.freed(lis[1]),
+ a.freed(lis[2]),
+ a.freed(list_p),
+ })
+ end)
+end)
diff --git a/test/unit/eval/typval_spec.lua b/test/unit/eval/typval_spec.lua
new file mode 100644
index 0000000000..919a42fbb9
--- /dev/null
+++ b/test/unit/eval/typval_spec.lua
@@ -0,0 +1,3137 @@
+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 map = global_helpers.map
+
+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 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('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]),
+ })
+
+ eq(lis[2], lib.tv_list_item_remove(l, lis[1]))
+ alloc_log:check({
+ a.freed(table.remove(lis, 1)),
+ })
+ eq(lis, list_items(l))
+
+ eq(lis[7], lib.tv_list_item_remove(l, lis[6]))
+ alloc_log:check({
+ a.freed(table.remove(lis)),
+ })
+ eq(lis, list_items(l))
+
+ eq(lis[4], lib.tv_list_item_remove(l, lis[3]))
+ alloc_log:check({
+ a.freed(table.remove(lis, 3)),
+ })
+ eq(lis, list_items(l))
+ end)
+ itp('also frees the value', function()
+ local l = list('a', 'b', 'c', 'd')
+ neq(nil, l)
+ local lis = list_items(l)
+ alloc_log:check({
+ a.list(l),
+ a.str(lis[1].li_tv.vval.v_string, 1),
+ a.li(lis[1]),
+ a.str(lis[2].li_tv.vval.v_string, 1),
+ a.li(lis[2]),
+ a.str(lis[3].li_tv.vval.v_string, 1),
+ a.li(lis[3]),
+ a.str(lis[4].li_tv.vval.v_string, 1),
+ a.li(lis[4]),
+ })
+ local strings = map(function(li) return li.li_tv.vval.v_string end,
+ lis)
+
+ eq(lis[2], lib.tv_list_item_remove(l, lis[1]))
+ alloc_log:check({
+ a.freed(table.remove(strings, 1)),
+ a.freed(table.remove(lis, 1)),
+ })
+ eq(lis, list_items(l))
+
+ eq(lis[3], lib.tv_list_item_remove(l, lis[2]))
+ alloc_log:check({
+ a.freed(table.remove(strings, 2)),
+ a.freed(table.remove(lis, 2)),
+ })
+ eq(lis, list_items(l))
+
+ eq(nil, lib.tv_list_item_remove(l, lis[2]))
+ alloc_log:check({
+ a.freed(table.remove(strings, 2)),
+ a.freed(table.remove(lis, 2)),
+ })
+ 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]),
+ })
+
+ eq(lis[5], 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})
+
+ eq(lis[3], 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})
+
+ eq(nil, 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})
+
+ eq(lis[3], 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('drop_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_drop_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_drop_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_drop_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_drop_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('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)
+ local strings = map(function(li) return li.li_tv.vval.v_string end, lis)
+ -- 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})
+ alloc_log:check({
+ a.freed(strings[1]),
+ a.freed(lis[1]),
+ a.freed(strings[2]),
+ a.freed(lis[2]),
+ a.freed(strings[3]),
+ a.freed(lis[3]),
+ })
+
+ 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})
+ alloc_log:check({
+ a.freed(strings[11]),
+ a.freed(lis[11]),
+ a.freed(strings[12]),
+ a.freed(lis[12]),
+ a.freed(strings[13]),
+ a.freed(lis[13]),
+ })
+
+ 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})
+ alloc_log:check({
+ a.freed(strings[6]),
+ a.freed(lis[6]),
+ a.freed(strings[7]),
+ a.freed(lis[7]),
+ a.freed(strings[8]),
+ a.freed(lis[8]),
+ })
+
+ 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})
+ alloc_log:check({
+ a.freed(strings[4]),
+ a.freed(lis[4]),
+ a.freed(strings[5]),
+ a.freed(lis[5]),
+ a.freed(strings[9]),
+ a.freed(lis[9]),
+ a.freed(strings[10]),
+ a.freed(lis[10]),
+ })
+
+ 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)
+ 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_append_tv(l, l_l_tv)
+ 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_append_tv(l, l_s_tv)
+ alloc_log:check({
+ a.li(l.lv_last),
+ a.str(l.lv_last.li_tv.vval.v_string, 'test'),
+ })
+
+ eq({empty_list, 'test'}, typvalt2lua(l_tv))
+ end)
+ end)
+ describe('owned 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_append_owned_tv(l, l_l_tv)
+ eq(1, l_l.lv_refcount)
+ l_l.lv_refcount = l_l.lv_refcount + 1
+ eq(l_l, l.lv_first.li_tv.vval.v_list)
+ alloc_log:check({
+ a.li(l.lv_first),
+ })
+
+ local l_s_tv = ffi.gc(lua2typvalt('test'), nil)
+ alloc_log:check({
+ a.str(l_s_tv.vval.v_string, 'test'),
+ })
+ lib.tv_list_append_owned_tv(l, l_s_tv)
+ eq(l_s_tv.vval.v_string, l.lv_last.li_tv.vval.v_string)
+ l_s_tv.vval.v_string = nil
+ alloc_log:check({
+ a.li(l.lv_last),
+ })
+
+ eq({empty_list, 'test'}, 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, 0)
+ 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/fileio_spec.lua b/test/unit/fileio_spec.lua
index 3e3c36617d..066d013b19 100644
--- a/test/unit/fileio_spec.lua
+++ b/test/unit/fileio_spec.lua
@@ -1,4 +1,5 @@
-local helpers = require("test.unit.helpers")
+local helpers = require("test.unit.helpers")(after_each)
+local itp = helpers.gen_itp(it)
--{:cimport, :internalize, :eq, :neq, :ffi, :lib, :cstr, :to_cstr} = require 'test.unit.helpers'
local eq = helpers.eq
@@ -16,67 +17,67 @@ describe('file_pat functions', function()
return ffi.string(res)
end
- it('returns ^path$ regex for literal path input', function()
+ itp('returns ^path$ regex for literal path input', function()
eq( '^path$', file_pat_to_reg_pat('path'))
end)
- it('does not prepend ^ when there is a starting glob (*)', function()
+ itp('does not prepend ^ when there is a starting glob (*)', function()
eq('path$', file_pat_to_reg_pat('*path'))
end)
- it('does not append $ when there is an ending glob (*)', function()
+ itp('does not append $ when there is an ending glob (*)', function()
eq('^path', file_pat_to_reg_pat('path*'))
end)
- it('does not include ^ or $ when surrounded by globs (*)', function()
+ itp('does not include ^ or $ when surrounded by globs (*)', function()
eq('path', file_pat_to_reg_pat('*path*'))
end)
- it('replaces the bash any character (?) with the regex any character (.)', function()
+ itp('replaces the bash any character (?) with the regex any character (.)', function()
eq('^foo.bar$', file_pat_to_reg_pat('foo?bar'))
end)
- it('replaces a glob (*) in the middle of a path with regex multiple any character (.*)',
+ itp('replaces a glob (*) in the middle of a path with regex multiple any character (.*)',
function()
eq('^foo.*bar$', file_pat_to_reg_pat('foo*bar'))
end)
- it([[unescapes \? to ?]], function()
+ itp([[unescapes \? to ?]], function()
eq('^foo?bar$', file_pat_to_reg_pat([[foo\?bar]]))
end)
- it([[unescapes \% to %]], function()
+ itp([[unescapes \% to %]], function()
eq('^foo%bar$', file_pat_to_reg_pat([[foo\%bar]]))
end)
- it([[unescapes \, to ,]], function()
+ itp([[unescapes \, to ,]], function()
eq('^foo,bar$', file_pat_to_reg_pat([[foo\,bar]]))
end)
- it([[unescapes '\ ' to ' ']], function()
+ itp([[unescapes '\ ' to ' ']], function()
eq('^foo bar$', file_pat_to_reg_pat([[foo\ bar]]))
end)
- it([[escapes . to \.]], function()
+ itp([[escapes . to \.]], function()
eq([[^foo\.bar$]], file_pat_to_reg_pat('foo.bar'))
end)
- it('Converts bash brace expansion {a,b} to regex options (a|b)', function()
+ itp('Converts bash brace expansion {a,b} to regex options (a|b)', function()
eq([[^foo\(bar\|baz\)$]], file_pat_to_reg_pat('foo{bar,baz}'))
end)
- it('Collapses multiple consecutive * into a single character', function()
+ itp('Collapses multiple consecutive * into a single character', function()
eq([[^foo.*bar$]], file_pat_to_reg_pat('foo*******bar'))
eq([[foobar$]], file_pat_to_reg_pat('********foobar'))
eq([[^foobar]], file_pat_to_reg_pat('foobar********'))
end)
- it('Does not escape ^', function()
+ itp('Does not escape ^', function()
eq([[^^blah$]], file_pat_to_reg_pat('^blah'))
eq([[^foo^bar$]], file_pat_to_reg_pat('foo^bar'))
end)
- it('Does not escape $', function()
+ itp('Does not escape $', function()
eq([[^blah$$]], file_pat_to_reg_pat('blah$'))
eq([[^foo$bar$]], file_pat_to_reg_pat('foo$bar'))
end)
diff --git a/test/unit/fixtures/multiqueue.c b/test/unit/fixtures/multiqueue.c
new file mode 100644
index 0000000000..a8dca0a844
--- /dev/null
+++ b/test/unit/fixtures/multiqueue.c
@@ -0,0 +1,19 @@
+// 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"
+#include "multiqueue.h"
+
+
+void ut_multiqueue_put(MultiQueue *this, const char *str)
+{
+ multiqueue_put(this, NULL, 1, str);
+}
+
+const char *ut_multiqueue_get(MultiQueue *this)
+{
+ Event event = multiqueue_get(this);
+ return event.argv[0];
+}
diff --git a/test/unit/fixtures/multiqueue.h b/test/unit/fixtures/multiqueue.h
new file mode 100644
index 0000000000..78a3a89063
--- /dev/null
+++ b/test/unit/fixtures/multiqueue.h
@@ -0,0 +1,4 @@
+#include "nvim/event/multiqueue.h"
+
+void ut_multiqueue_put(MultiQueue *queue, const char *str);
+const char *ut_multiqueue_get(MultiQueue *queue);
diff --git a/test/unit/fixtures/posix.h b/test/unit/fixtures/posix.h
new file mode 100644
index 0000000000..f6f24cd9dc
--- /dev/null
+++ b/test/unit/fixtures/posix.h
@@ -0,0 +1,11 @@
+#include <unistd.h>
+#include <string.h>
+#include <errno.h>
+#include <sys/wait.h>
+#include <stdlib.h>
+
+enum {
+ kPOSIXErrnoEINTR = EINTR,
+ kPOSIXErrnoECHILD = ECHILD,
+ kPOSIXWaitWUNTRACED = WUNTRACED,
+};
diff --git a/test/unit/fixtures/queue.c b/test/unit/fixtures/queue.c
deleted file mode 100644
index bbb6274b21..0000000000
--- a/test/unit/fixtures/queue.c
+++ /dev/null
@@ -1,16 +0,0 @@
-#include <string.h>
-#include <stdlib.h>
-#include "nvim/event/queue.h"
-#include "queue.h"
-
-
-void ut_queue_put(Queue *queue, const char *str)
-{
- queue_put(queue, NULL, 1, str);
-}
-
-const char *ut_queue_get(Queue *queue)
-{
- Event event = queue_get(queue);
- return event.argv[0];
-}
diff --git a/test/unit/fixtures/queue.h b/test/unit/fixtures/queue.h
deleted file mode 100644
index ae949c9f29..0000000000
--- a/test/unit/fixtures/queue.h
+++ /dev/null
@@ -1,4 +0,0 @@
-#include "nvim/event/queue.h"
-
-void ut_queue_put(Queue *queue, const char *str);
-const char *ut_queue_get(Queue *queue);
diff --git a/test/unit/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/formatc.lua b/test/unit/formatc.lua
index 00637e0b8d..2fd37c599a 100644
--- a/test/unit/formatc.lua
+++ b/test/unit/formatc.lua
@@ -65,11 +65,12 @@ local tokens = P { "tokens";
identifier = Ct(C(R("az","AZ","__") * R("09","az","AZ","__")^0) * Cc"identifier"),
-- Single character in a string
- string_char = R("az","AZ","09") + S"$%^&*()_-+={[}]:;@~#<,>.!?/ \t" + (P"\\" * S[[ntvbrfa\?'"0x]]),
+ sstring_char = R("\001&","([","]\255") + (P"\\" * S[[ntvbrfa\?'"0x]]),
+ dstring_char = R("\001!","#[","]\255") + (P"\\" * S[[ntvbrfa\?'"0x]]),
-- String literal
- string = Ct(C(P"'" * (V"string_char" + P'"')^0 * P"'" +
- P'"' * (V"string_char" + P"'")^0 * P'"') * Cc"string"),
+ string = Ct(C(P"'" * (V"sstring_char" + P'"')^0 * P"'" +
+ P'"' * (V"dstring_char" + P"'")^0 * P'"') * Cc"string"),
-- Operator
operator = Ct(C(P">>=" + P"<<=" + P"..." +
@@ -219,13 +220,7 @@ local function standalone(...) -- luacheck: ignore
Preprocess.add_to_include_path('./../../build/include')
Preprocess.add_to_include_path('./../../.deps/usr/include')
- local input = Preprocess.preprocess_stream(arg[1])
- local raw = input:read('*all')
- input:close()
-
- if raw == nil then
- print("ERROR: Preprocess.preprocess_stream():read() returned empty")
- end
+ local raw = Preprocess.preprocess('', arg[1])
local formatted
if #arg == 2 and arg[2] == 'no' then
diff --git a/test/unit/garray_spec.lua b/test/unit/garray_spec.lua
index 9694e3c427..28df8a6e3f 100644
--- a/test/unit/garray_spec.lua
+++ b/test/unit/garray_spec.lua
@@ -1,4 +1,5 @@
-local helpers = require("test.unit.helpers")
+local helpers = require("test.unit.helpers")(after_each)
+local itp = helpers.gen_itp(it)
local cimport = helpers.cimport
local internalize = helpers.internalize
@@ -8,7 +9,7 @@ local ffi = helpers.ffi
local to_cstr = helpers.to_cstr
local NULL = helpers.NULL
-local garray = cimport('stdlib.h', './src/nvim/garray.h')
+local garray = cimport('./src/nvim/garray.h')
local itemsize = 14
local growsize = 95
@@ -156,7 +157,7 @@ local ga_append_ints = function(garr, ...)
end
-- enhanced constructors
-local garray_ctype = ffi.typeof('garray_T[1]')
+local garray_ctype = function(...) return ffi.typeof('garray_T[1]')(...) end
local new_garray = function()
local garr = garray_ctype()
return ffi.gc(garr, ga_clear)
@@ -183,7 +184,7 @@ end
describe('garray', function()
describe('ga_init', function()
- it('initializes the values of the garray', function()
+ itp('initializes the values of the garray', function()
local garr = new_garray()
ga_init(garr, itemsize, growsize)
eq(0, ga_len(garr))
@@ -198,31 +199,31 @@ describe('garray', function()
local function new_and_grow(itemsize_, growsize_, req)
local garr = new_garray()
ga_init(garr, itemsize_, growsize_)
- eq(0, ga_size(garr)) -- should be 0 at first
- eq(NULL, ga_data(garr)) -- should be NULL
+ eq(0, ga_size(garr)) -- should be 0 at first
+ eq(NULL, ga_data(garr)) -- should be NULL
ga_grow(garr, req) -- add space for `req` items
return garr
end
- it('grows by growsize items if num < growsize', function()
+ itp('grows by growsize items if num < growsize', function()
itemsize = 16
growsize = 4
local grow_by = growsize - 1
local garr = new_and_grow(itemsize, growsize, grow_by)
- neq(NULL, ga_data(garr)) -- data should be a ptr to memory
+ neq(NULL, ga_data(garr)) -- data should be a ptr to memory
eq(growsize, ga_maxlen(garr)) -- we requested LESS than growsize, so...
end)
- it('grows by num items if num > growsize', function()
+ itp('grows by num items if num > growsize', function()
itemsize = 16
growsize = 4
local grow_by = growsize + 1
local garr = new_and_grow(itemsize, growsize, grow_by)
- neq(NULL, ga_data(garr)) -- data should be a ptr to memory
+ neq(NULL, ga_data(garr)) -- data should be a ptr to memory
eq(grow_by, ga_maxlen(garr)) -- we requested MORE than growsize, so...
end)
- it('does not grow when nothing is requested', function()
+ itp('does not grow when nothing is requested', function()
local garr = new_and_grow(16, 4, 0)
eq(NULL, ga_data(garr))
eq(0, ga_maxlen(garr))
@@ -230,7 +231,7 @@ describe('garray', function()
end)
describe('ga_clear', function()
- it('clears an already allocated array', function()
+ itp('clears an already allocated array', function()
-- allocate and scramble an array
local garr = garray_ctype()
ga_init(garr, itemsize, growsize)
@@ -247,7 +248,7 @@ describe('garray', function()
end)
describe('ga_append', function()
- it('can append bytes', function()
+ itp('can append bytes', function()
-- this is the actual ga_append, the others are just emulated lua
-- versions
local garr = new_garray()
@@ -262,7 +263,7 @@ describe('garray', function()
eq('hello', ffi.string(bytes))
end)
- it('can append integers', function()
+ itp('can append integers', function()
local garr = new_garray()
ga_init(garr, ffi.sizeof("int"), 1)
local input = {
@@ -279,7 +280,7 @@ describe('garray', function()
end
end)
- it('can append strings to a growing array of strings', function()
+ itp('can append strings to a growing array of strings', function()
local garr = new_string_garray()
local input = {
"some",
@@ -298,7 +299,7 @@ describe('garray', function()
end)
describe('ga_concat', function()
- it('concatenates the parameter to the growing byte array', function()
+ itp('concatenates the parameter to the growing byte array', function()
local garr = new_garray()
ga_init(garr, ffi.sizeof("char"), 1)
local str = "ohwell●●"
@@ -329,11 +330,11 @@ describe('garray', function()
end
describe('ga_concat_strings', function()
- it('returns an empty string when concatenating an empty array', function()
+ itp('returns an empty string when concatenating an empty array', function()
test_concat_fn({ }, ga_concat_strings)
end)
- it('can concatenate a non-empty array', function()
+ itp('can concatenate a non-empty array', function()
test_concat_fn({
'oh',
'my',
@@ -343,11 +344,11 @@ describe('garray', function()
end)
describe('ga_concat_strings_sep', function()
- it('returns an empty string when concatenating an empty array', function()
+ itp('returns an empty string when concatenating an empty array', function()
test_concat_fn({ }, ga_concat_strings_sep, '---')
end)
- it('can concatenate a non-empty array', function()
+ itp('can concatenate a non-empty array', function()
local sep = '-●●-'
test_concat_fn({
'oh',
@@ -358,7 +359,7 @@ describe('garray', function()
end)
describe('ga_remove_duplicate_strings', function()
- it('sorts and removes duplicate strings', function()
+ itp('sorts and removes duplicate strings', function()
local garr = new_string_garray()
local input = {
'ccc',
diff --git a/test/unit/helpers.lua b/test/unit/helpers.lua
index 91da459393..f8143a0125 100644
--- a/test/unit/helpers.lua
+++ b/test/unit/helpers.lua
@@ -4,25 +4,129 @@ local Set = require('test.unit.set')
local Preprocess = require('test.unit.preprocess')
local Paths = require('test.config.paths')
local global_helpers = require('test.helpers')
+local assert = require('luassert')
+local say = require('say')
+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
local ok = global_helpers.ok
+-- C constants.
+local NULL = ffi.cast('void*', 0)
+
+local OK = 1
+local FAIL = 0
+
+local cimport
+
-- add some standard header locations
for _, p in ipairs(Paths.include_paths) do
Preprocess.add_to_include_path(p)
end
--- load neovim shared library
-local libnvim = ffi.load(Paths.test_libnvim_path)
+local child_pid = nil
+local function only_separate(func)
+ return function(...)
+ if child_pid ~= 0 then
+ error('This function must be run in a separate process only')
+ end
+ return func(...)
+ end
+end
+local child_calls_init = {}
+local child_calls_mod = nil
+local child_calls_mod_once = nil
+local function child_call(func, ret)
+ return function(...)
+ local child_calls = child_calls_mod or child_calls_init
+ if child_pid ~= 0 then
+ child_calls[#child_calls + 1] = {func=func, args={...}}
+ return ret
+ else
+ return func(...)
+ end
+ end
+end
+
+-- Run some code at the start of the child process, before running the test
+-- itself. Is supposed to be run in `before_each`.
+local function child_call_once(func, ...)
+ if child_pid ~= 0 then
+ child_calls_mod_once[#child_calls_mod_once + 1] = {
+ func=func, args={...}}
+ else
+ func(...)
+ end
+end
+
+local child_cleanups_mod_once = nil
+
+-- Run some code at the end of the child process, before exiting. Is supposed to
+-- be run in `before_each` because `after_each` is run after child has exited.
+local function child_cleanup_once(func, ...)
+ local child_cleanups = child_cleanups_mod_once
+ if child_pid ~= 0 then
+ child_cleanups[#child_cleanups + 1] = {func=func, args={...}}
+ else
+ func(...)
+ end
+end
+
+local libnvim = nil
+
+local lib = setmetatable({}, {
+ __index = only_separate(function(_, idx)
+ return libnvim[idx]
+ end),
+ __newindex = child_call(function(_, idx, val)
+ libnvim[idx] = val
+ end),
+})
+
+local init = only_separate(function()
+ -- load neovim shared library
+ libnvim = ffi.load(Paths.test_libnvim_path)
+ for _, c in ipairs(child_calls_init) do
+ c.func(unpack(c.args))
+ end
+ libnvim.time_init()
+ libnvim.early_init()
+ libnvim.event_init()
+ if child_calls_mod then
+ for _, c in ipairs(child_calls_mod) do
+ c.func(unpack(c.args))
+ end
+ end
+ if child_calls_mod_once then
+ for _, c in ipairs(child_calls_mod_once) do
+ c.func(unpack(c.args))
+ end
+ child_calls_mod_once = nil
+ end
+end)
+
+local deinit = only_separate(function()
+ if child_cleanups_mod_once then
+ for _, c in ipairs(child_cleanups_mod_once) do
+ c.func(unpack(c.args))
+ end
+ child_cleanups_mod_once = nil
+ end
+end)
local function trim(s)
return s:match('^%s*(.*%S)') or ''
end
-- a Set that keeps around the lines we've already seen
-local cdefs = Set:new()
+local cdefs_init = Set:new()
+local cdefs_mod = nil
local imported = Set:new()
local pragma_pack_id = 1
@@ -34,8 +138,10 @@ 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, "_Float")
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 = {...}
or string.find(line, "inline _Bool")) then
result[#result + 1] = line
end
@@ -44,117 +150,695 @@ local function filter_complex_blocks(body)
return table.concat(result, "\n")
end
+
+local cdef = ffi.cdef
+
+local cimportstr
+
+local previous_defines_init = ''
+local preprocess_cache_init = {}
+local previous_defines_mod = ''
+local preprocess_cache_mod = nil
+
+local function is_child_cdefs()
+ return (os.getenv('NVIM_TEST_MAIN_CDEFS') ~= '1')
+end
+
-- use this helper to import C files, you can pass multiple paths at once,
-- this helper will return the C namespace of the nvim library.
-local function cimport(...)
- local paths = {}
- local args = {...}
+cimport = function(...)
+ local previous_defines, preprocess_cache, cdefs
+ if is_child_cdefs() and preprocess_cache_mod then
+ preprocess_cache = preprocess_cache_mod
+ previous_defines = previous_defines_mod
+ cdefs = cdefs_mod
+ else
+ preprocess_cache = preprocess_cache_init
+ previous_defines = previous_defines_init
+ cdefs = cdefs_init
+ end
+ for _, path in ipairs({...}) do
+ if not (path:sub(1, 1) == '/' or path:sub(1, 1) == '.'
+ or path:sub(2, 2) == ':') then
+ path = './' .. path
+ end
+ if not preprocess_cache[path] then
+ local body
+ body, previous_defines = Preprocess.preprocess(previous_defines, path)
+ -- format it (so that the lines are "unique" statements), also filter out
+ -- Objective-C blocks
+ if os.getenv('NVIM_TEST_PRINT_I') == '1' then
+ local lnum = 0
+ for line in body:gmatch('[^\n]+') do
+ lnum = lnum + 1
+ print(lnum, line)
+ end
+ end
+ body = formatc(body)
+ body = filter_complex_blocks(body)
+ -- add the formatted lines to a set
+ local new_cdefs = Set:new()
+ for line in body:gmatch("[^\r\n]+") do
+ line = trim(line)
+ -- give each #pragma pack an unique id, so that they don't get removed
+ -- if they are inserted into the set
+ -- (they are needed in the right order with the struct definitions,
+ -- otherwise luajit has wrong memory layouts for the sturcts)
+ if line:match("#pragma%s+pack") then
+ line = line .. " // " .. pragma_pack_id
+ pragma_pack_id = pragma_pack_id + 1
+ end
+ new_cdefs:add(line)
+ end
+
+ -- subtract the lines we've already imported from the new lines, then add
+ -- the new unique lines to the old lines (so they won't be imported again)
+ new_cdefs:diff(cdefs)
+ cdefs:union(new_cdefs)
+ -- request a sorted version of the new lines (same relative order as the
+ -- original preprocessed file) and feed that to the LuaJIT ffi
+ local new_lines = new_cdefs:to_table()
+ if os.getenv('NVIM_TEST_PRINT_CDEF') == '1' then
+ for lnum, line in ipairs(new_lines) do
+ print(lnum, line)
+ end
+ end
+ body = table.concat(new_lines, '\n')
- -- filter out paths we've already imported
- for _,path in pairs(args) do
- if path ~= nil and not imported:contains(path) then
- paths[#paths + 1] = path
+ preprocess_cache[path] = body
end
+ cimportstr(preprocess_cache, path)
end
+ return lib
+end
- for _,path in pairs(paths) do
- imported:add(path)
+local cimport_immediate = function(...)
+ local saved_pid = child_pid
+ child_pid = 0
+ local err, emsg = pcall(cimport, ...)
+ child_pid = saved_pid
+ if not err then
+ emsg = tostring(emsg)
+ io.stderr:write(emsg .. '\n')
+ assert(false)
+ else
+ return lib
end
+end
- if #paths == 0 then
- return libnvim
+local function _cimportstr(preprocess_cache, path)
+ if imported:contains(path) then
+ return lib
end
+ local body = preprocess_cache[path]
+ if body == '' then
+ return lib
+ end
+ cdef(body)
+ imported:add(path)
+
+ return lib
+end
+
+if is_child_cdefs() then
+ cimportstr = child_call(_cimportstr, lib)
+else
+ cimportstr = _cimportstr
+end
- local body = nil
- for _ = 1, 10 do
- local stream = Preprocess.preprocess_stream(unpack(paths))
- body = stream:read("*a")
- stream:close()
- if body ~= nil then break end
+local function alloc_log_new()
+ local log = {
+ log={},
+ lib=cimport('./src/nvim/memory.h'),
+ original_functions={},
+ null={['\0:is_null']=true},
+ }
+ local allocator_functions = {'malloc', 'free', 'calloc', 'realloc'}
+ function log:save_original_functions()
+ for _, funcname in ipairs(allocator_functions) do
+ if not self.original_functions[funcname] then
+ self.original_functions[funcname] = self.lib['mem_' .. funcname]
+ end
+ end
end
+ log.save_original_functions = child_call(log.save_original_functions)
+ function log:set_mocks()
+ for _, k in ipairs(allocator_functions) do
+ do
+ local kk = k
+ self.lib['mem_' .. k] = function(...)
+ local log_entry = {func=kk, args={...}}
+ self.log[#self.log + 1] = log_entry
+ if kk == 'free' then
+ self.original_functions[kk](...)
+ else
+ log_entry.ret = self.original_functions[kk](...)
+ end
+ for i, v in ipairs(log_entry.args) do
+ if v == nil then
+ -- XXX This thing thinks that {NULL} ~= {NULL}.
+ log_entry.args[i] = self.null
+ end
+ end
+ if self.hook then self:hook(log_entry) end
+ if log_entry.ret then
+ return log_entry.ret
+ end
+ end
+ end
+ end
+ end
+ log.set_mocks = child_call(log.set_mocks)
+ function log:clear()
+ self.log = {}
+ end
+ function log:check(exp)
+ eq(exp, self.log)
+ self:clear()
+ end
+ function log:clear_tmp_allocs(clear_null_frees)
+ 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
+ elseif clear_null_frees and v.args[1] == self.null then
+ toremove[#toremove + 1] = i
+ 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
+ --[[
+ [ for k, v in pairs(self.original_functions) do
+ [ self.lib['mem_' .. k] = v
+ [ end
+ ]]
+ end
+ function log:setup()
+ log:save_original_functions()
+ log:set_mocks()
+ end
+ function log:before_each()
+ return
+ end
+ function log:after_each()
+ log:restore_original_functions()
+ end
+ log:setup()
+ return log
+end
- if body == nil then
- print("ERROR: helpers.lua: Preprocess.preprocess_stream():read() returned empty")
+-- take a pointer to a C-allocated string and return an interned
+-- version while also freeing the memory
+local function internalize(cdata, len)
+ ffi.gc(cdata, ffi.C.free)
+ return ffi.string(cdata, len)
+end
+
+local cstr = ffi.typeof('char[?]')
+local function to_cstr(string)
+ return cstr(#string + 1, string)
+end
+
+local sc
+
+if posix ~= nil then
+ sc = {
+ fork = posix.fork,
+ pipe = posix.pipe,
+ read = posix.read,
+ write = posix.write,
+ close = posix.close,
+ wait = posix.wait,
+ exit = posix._exit,
+ }
+elseif syscall ~= nil then
+ sc = {
+ fork = syscall.fork,
+ pipe = function()
+ local ret = {syscall.pipe()}
+ return ret[3], ret[4]
+ end,
+ read = function(rd, len)
+ return rd:read(nil, len)
+ end,
+ write = function(wr, s)
+ return wr:write(s)
+ end,
+ close = function(p)
+ return p:close()
+ end,
+ wait = syscall.wait,
+ exit = syscall.exit,
+ }
+else
+ cimport_immediate('./test/unit/fixtures/posix.h')
+ sc = {
+ fork = function()
+ return tonumber(ffi.C.fork())
+ end,
+ pipe = function()
+ local ret = ffi.new('int[2]', {-1, -1})
+ ffi.errno(0)
+ local res = ffi.C.pipe(ret)
+ if (res ~= 0) then
+ local err = ffi.errno(0)
+ assert(res == 0, ("pipe() error: %u: %s"):format(
+ err, ffi.string(ffi.C.strerror(err))))
+ end
+ assert(ret[0] ~= -1 and ret[1] ~= -1)
+ return ret[0], ret[1]
+ end,
+ read = function(rd, len)
+ local ret = ffi.new('char[?]', len, {0})
+ local total_bytes_read = 0
+ ffi.errno(0)
+ while total_bytes_read < len do
+ local bytes_read = tonumber(ffi.C.read(
+ rd,
+ ffi.cast('void*', ret + total_bytes_read),
+ len - total_bytes_read))
+ if bytes_read == -1 then
+ local err = ffi.errno(0)
+ if err ~= ffi.C.kPOSIXErrnoEINTR then
+ assert(false, ("read() error: %u: %s"):format(
+ err, ffi.string(ffi.C.strerror(err))))
+ end
+ elseif bytes_read == 0 then
+ break
+ else
+ total_bytes_read = total_bytes_read + bytes_read
+ end
+ end
+ return ffi.string(ret, total_bytes_read)
+ end,
+ write = function(wr, s)
+ local wbuf = to_cstr(s)
+ local total_bytes_written = 0
+ ffi.errno(0)
+ while total_bytes_written < #s do
+ local bytes_written = tonumber(ffi.C.write(
+ wr,
+ ffi.cast('void*', wbuf + total_bytes_written),
+ #s - total_bytes_written))
+ if bytes_written == -1 then
+ local err = ffi.errno(0)
+ if err ~= ffi.C.kPOSIXErrnoEINTR then
+ assert(false, ("write() error: %u: %s"):format(
+ err, ffi.string(ffi.C.strerror(err))))
+ end
+ elseif bytes_written == 0 then
+ break
+ else
+ total_bytes_written = total_bytes_written + bytes_written
+ end
+ end
+ return total_bytes_written
+ end,
+ close = ffi.C.close,
+ wait = function(pid)
+ ffi.errno(0)
+ while true do
+ local r = ffi.C.waitpid(pid, nil, ffi.C.kPOSIXWaitWUNTRACED)
+ if r == -1 then
+ local err = ffi.errno(0)
+ if err == ffi.C.kPOSIXErrnoECHILD then
+ break
+ elseif err ~= ffi.C.kPOSIXErrnoEINTR then
+ assert(false, ("waitpid() error: %u: %s"):format(
+ err, ffi.string(ffi.C.strerror(err))))
+ end
+ else
+ assert(r == pid)
+ end
+ end
+ end,
+ exit = ffi.C._exit,
+ }
+end
+
+local function format_list(lst)
+ local ret = ''
+ for _, v in ipairs(lst) do
+ if ret ~= '' then ret = ret .. ', ' end
+ ret = ret .. assert:format({v, n=1})[1]
end
+ return ret
+end
- -- format it (so that the lines are "unique" statements), also filter out
- -- Objective-C blocks
- body = formatc(body)
- body = filter_complex_blocks(body)
+if os.getenv('NVIM_TEST_PRINT_SYSCALLS') == '1' then
+ for k_, v_ in pairs(sc) do
+ (function(k, v)
+ sc[k] = function(...)
+ local rets = {v(...)}
+ io.stderr:write(('%s(%s) = %s\n'):format(k, format_list({...}),
+ format_list(rets)))
+ return unpack(rets)
+ end
+ end)(k_, v_)
+ end
+end
- -- add the formatted lines to a set
- local new_cdefs = Set:new()
- for line in body:gmatch("[^\r\n]+") do
- line = trim(line)
- -- give each #pragma pack an unique id, so that they don't get removed
- -- if they are inserted into the set
- -- (they are needed in the right order with the struct definitions,
- -- otherwise luajit has wrong memory layouts for the sturcts)
- if line:match("#pragma%s+pack") then
- line = line .. " // " .. pragma_pack_id
- pragma_pack_id = pragma_pack_id + 1
+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: either in the format described below or custom debug output starting
+ with `>`. Latter lines still have the same width in byte.
+
+ ┌ 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, _>_ for custom debug
+ │ output.
+ │┏ 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
- new_cdefs:add(line)
+
+ 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))
- -- subtract the lines we've already imported from the new lines, then add
- -- the new unique lines to the old lines (so they won't be imported again)
- new_cdefs:diff(cdefs)
- cdefs:union(new_cdefs)
+local _debug_log
- if new_cdefs:size() == 0 then
- -- if there's no new lines, just return
- return libnvim
+local debug_log = only_separate(function(...)
+ return _debug_log(...)
+end)
+
+local function itp_child(wr, func)
+ _debug_log = function(s)
+ s = s:sub(1, hook_msglen - 2)
+ sc.write(wr, '>' .. s .. (' '):rep(hook_msglen - 2 - #s) .. '\n')
+ end
+ local err, emsg = pcall(init)
+ if err then
+ collectgarbage('stop')
+ child_sethook(wr)
+ err, emsg = pcall(func)
+ debug.sethook()
+ end
+ 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()
+ else
+ sc.write(wr, '+\n')
+ deinit()
+ end
+ collectgarbage('restart')
+ collectgarbage()
+ sc.write(wr, '$\n')
+ sc.close(wr)
+ sc.exit(err and 0 or 1)
+end
+
+local function check_child_err(rd)
+ local trace = {}
+ local did_traceline = false
+ local maxtrace = tonumber(os.getenv('NVIM_TEST_MAXTRACE')) or 1024
+ 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
+ if #trace > maxtrace then
+ table.remove(trace, 1)
+ end
end
+ local res = sc.read(rd, 2)
+ if #res == 2 then
+ local err = ''
+ if res ~= '+\n' then
+ eq('-\n', res)
+ local len_s = sc.read(rd, 5)
+ local len = tonumber(len_s)
+ neq(0, len)
+ if os.getenv('NVIM_TEST_TRACE_ON_ERROR') == '1' and #trace ~= 0 then
+ err = '\nTest failed, trace:\n' .. tracehelp
+ for _, traceline in ipairs(trace) do
+ err = err .. traceline
+ end
+ end
+ err = err .. sc.read(rd, len + 1)
+ end
+ local eres = sc.read(rd, 2)
+ if eres ~= '$\n' then
+ if #trace == 0 then
+ err = '\nTest crashed, no trace available\n'
+ else
+ err = '\nTest crashed, trace:\n' .. tracehelp
+ for i = 1, #trace do
+ err = err .. trace[i]
+ end
+ end
+ if not did_traceline then
+ err = err .. '\nNo end of trace occurred'
+ end
+ local cc_err, cc_emsg = pcall(check_cores, Paths.test_luajit_prg, true)
+ if not cc_err then
+ err = err .. '\ncheck_cores failed: ' .. cc_emsg
+ end
+ end
+ if err ~= '' then
+ assert.just_fail(err)
+ end
+ end
+end
- -- request a sorted version of the new lines (same relative order as the
- -- original preprocessed file) and feed that to the LuaJIT ffi
- local new_lines = new_cdefs:to_table()
- if os.getenv('NVIM_TEST_PRINT_CDEF') == '1' then
- for lnum, line in ipairs(new_lines) do
- print(lnum, line)
+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
- ffi.cdef(table.concat(new_lines, "\n"))
+end
- return libnvim
+local function gen_itp(it)
+ child_calls_mod = {}
+ child_calls_mod_once = {}
+ child_cleanups_mod_once = {}
+ 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 itp(name, func, allow_failure)
+ if allow_failure and os.getenv('NVIM_TEST_RUN_FAILING_TESTS') ~= '1' then
+ -- FIXME Fix tests with this true
+ return
+ end
+ it(name, function()
+ local rd, wr = sc.pipe()
+ child_pid = sc.fork()
+ if child_pid == 0 then
+ sc.close(rd)
+ itp_child(wr, func)
+ else
+ sc.close(wr)
+ local saved_child_pid = child_pid
+ child_pid = nil
+ itp_parent(rd, saved_child_pid, allow_failure)
+ end
+ end)
+ end
+ return itp
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')
+cimport('./src/nvim/types.h', './src/nvim/main.h', './src/nvim/os/time.h')
--- take a pointer to a C-allocated string and return an interned
--- version while also freeing the memory
-local function internalize(cdata, len)
- ffi.gc(cdata, ffi.C.free)
- return ffi.string(cdata, len)
+local function conv_enum(etab, eval)
+ local n = tonumber(eval)
+ return etab[n] or n
end
-local cstr = ffi.typeof('char[?]')
-local function to_cstr(string)
- return cstr((string.len(string)) + 1, string)
+local function array_size(arr)
+ return ffi.sizeof(arr) / ffi.sizeof(arr[0])
end
--- initialize some global variables, this is still necessary to unit test
--- functions that rely on global state.
-do
- local main = cimport('./src/nvim/main.h')
- local time = cimport('./src/nvim/os/time.h')
- time.time_init()
- main.early_init()
- main.event_init()
+local function kvi_size(kvi)
+ return array_size(kvi.init_array)
end
--- C constants.
-local NULL = ffi.cast('void*', 0)
+local function kvi_init(kvi)
+ kvi.capacity = kvi_size(kvi)
+ kvi.items = kvi.init_array
+ return kvi
+end
-local OK = 1
-local FAIL = 0
+local function kvi_destroy(kvi)
+ if kvi.items ~= kvi.init_array then
+ lib.xfree(kvi.items)
+ end
+end
+
+local function kvi_new(ct)
+ return kvi_init(ffi.new(ct))
+end
-return {
+local function make_enum_conv_tab(m, values, skip_pref, set_cb)
+ child_call_once(function()
+ local ret = {}
+ for _, v in ipairs(values) do
+ local str_v = v
+ if v:sub(1, #skip_pref) == skip_pref then
+ str_v = v:sub(#skip_pref + 1)
+ end
+ ret[tonumber(m[v])] = str_v
+ end
+ set_cb(ret)
+ end)
+end
+
+local function ptr2addr(ptr)
+ return tonumber(ffi.cast('intptr_t', ffi.cast('void *', ptr)))
+end
+
+local s = ffi.new('char[64]', {0})
+
+local function ptr2key(ptr)
+ ffi.C.snprintf(s, ffi.sizeof(s), '%p', ffi.cast('void *', ptr))
+ return ffi.string(s)
+end
+
+local module = {
cimport = cimport,
cppimport = cppimport,
internalize = internalize,
@@ -162,10 +846,29 @@ return {
eq = eq,
neq = neq,
ffi = ffi,
- lib = libnvim,
+ lib = lib,
cstr = cstr,
to_cstr = to_cstr,
NULL = NULL,
OK = OK,
- FAIL = FAIL
+ FAIL = FAIL,
+ alloc_log_new = alloc_log_new,
+ gen_itp = gen_itp,
+ only_separate = only_separate,
+ child_call_once = child_call_once,
+ child_cleanup_once = child_cleanup_once,
+ sc = sc,
+ conv_enum = conv_enum,
+ array_size = array_size,
+ kvi_destroy = kvi_destroy,
+ kvi_size = kvi_size,
+ kvi_init = kvi_init,
+ kvi_new = kvi_new,
+ make_enum_conv_tab = make_enum_conv_tab,
+ ptr2addr = ptr2addr,
+ ptr2key = ptr2key,
+ debug_log = debug_log,
}
+return function()
+ return module
+end
diff --git a/test/unit/keymap_spec.lua b/test/unit/keymap_spec.lua
new file mode 100644
index 0000000000..595a19eb17
--- /dev/null
+++ b/test/unit/keymap_spec.lua
@@ -0,0 +1,71 @@
+local helpers = require("test.unit.helpers")(after_each)
+local itp = helpers.gen_itp(it)
+
+local ffi = helpers.ffi
+local eq = helpers.eq
+local neq = helpers.neq
+
+local keymap = helpers.cimport("./src/nvim/keymap.h")
+
+describe('keymap.c', function()
+
+ describe('find_special_key()', function()
+ local srcp = ffi.new('const unsigned char *[1]')
+ local modp = ffi.new('int[1]')
+
+ itp('no keycode', function()
+ srcp[0] = 'abc'
+ eq(0, keymap.find_special_key(srcp, 3, modp, false, false, false))
+ end)
+
+ itp('keycode with multiple modifiers', function()
+ srcp[0] = '<C-M-S-A>'
+ neq(0, keymap.find_special_key(srcp, 9, modp, false, false, false))
+ neq(0, modp[0])
+ end)
+
+ itp('case-insensitive', function()
+ -- Compare other capitalizations to this.
+ srcp[0] = '<C-A>'
+ local all_caps_key =
+ keymap.find_special_key(srcp, 5, modp, false, false, false)
+ local all_caps_mod = modp[0]
+
+ srcp[0] = '<C-a>'
+ eq(all_caps_key,
+ keymap.find_special_key(srcp, 5, modp, false, false, false))
+ eq(all_caps_mod, modp[0])
+
+ srcp[0] = '<c-A>'
+ eq(all_caps_key,
+ keymap.find_special_key(srcp, 5, modp, false, false, false))
+ eq(all_caps_mod, modp[0])
+
+ srcp[0] = '<c-a>'
+ eq(all_caps_key,
+ keymap.find_special_key(srcp, 5, modp, false, false, false))
+ eq(all_caps_mod, modp[0])
+ end)
+
+ itp('double-quote in keycode #7411', function()
+ -- Unescaped with in_string=false
+ srcp[0] = '<C-">'
+ eq(string.byte('"'),
+ keymap.find_special_key(srcp, 5, modp, false, false, false))
+
+ -- Unescaped with in_string=true
+ eq(0, keymap.find_special_key(srcp, 5, modp, false, false, true))
+
+ -- Escaped with in_string=false
+ srcp[0] = '<C-\\">'
+ -- Should fail because the key is invalid
+ -- (more than 1 non-modifier character).
+ eq(0, keymap.find_special_key(srcp, 6, modp, false, false, false))
+
+ -- Escaped with in_string=true
+ eq(string.byte('"'),
+ keymap.find_special_key(srcp, 6, modp, false, false, true))
+ end)
+ end)
+
+end)
diff --git a/test/unit/mbyte_spec.lua b/test/unit/mbyte_spec.lua
index 9b2415a93f..3e65537270 100644
--- a/test/unit/mbyte_spec.lua
+++ b/test/unit/mbyte_spec.lua
@@ -1,9 +1,11 @@
-local helpers = require("test.unit.helpers")
+local helpers = require("test.unit.helpers")(after_each)
+local itp = helpers.gen_itp(it)
local ffi = helpers.ffi
local eq = helpers.eq
local mbyte = helpers.cimport("./src/nvim/mbyte.h")
+local charset = helpers.cimport('./src/nvim/charset.h')
describe('mbyte', function()
@@ -26,7 +28,7 @@ describe('mbyte', function()
before_each(function()
end)
- it('utf_ptr2char', function()
+ itp('utf_ptr2char', function()
-- For strings with length 1 the first byte is returned.
for c = 0, 255 do
eq(c, mbyte.utf_ptr2char(to_string({c, 0})))
@@ -41,10 +43,21 @@ describe('mbyte', function()
-- Sequences with more than four bytes
end)
+ for n = 0, 0xF do
+ itp(('utf_char2bytes for chars 0x%x - 0x%x'):format(n * 0x1000, n * 0x1000 + 0xFFF), function()
+ local char_p = ffi.typeof('char[?]')
+ for c = n * 0x1000, n * 0x1000 + 0xFFF do
+ local p = char_p(4, 0)
+ mbyte.utf_char2bytes(c, p)
+ eq(c, mbyte.utf_ptr2char(p))
+ eq(charset.vim_iswordc(c), charset.vim_iswordp(p))
+ end
+ end)
+ end
describe('utfc_ptr2char_len', function()
- it('1-byte sequences', function()
+ itp('1-byte sequences', function()
local pcc = to_intp()
for c = 0, 255 do
eq(c, mbyte.utfc_ptr2char_len(to_string({c}), pcc, 1))
@@ -52,7 +65,7 @@ describe('mbyte', function()
end
end)
- it('2-byte sequences', function()
+ itp('2-byte sequences', function()
local pcc = to_intp()
-- No combining characters
eq(0x007f, mbyte.utfc_ptr2char_len(to_string({0x7f, 0x7f}), pcc, 2))
@@ -76,7 +89,7 @@ describe('mbyte', function()
eq(0, pcc[0])
end)
- it('3-byte sequences', function()
+ itp('3-byte sequences', function()
local pcc = to_intp()
-- No second UTF-8 character
@@ -108,7 +121,7 @@ describe('mbyte', function()
eq(0, pcc[0])
end)
- it('4-byte sequences', function()
+ itp('4-byte sequences', function()
local pcc = to_intp()
-- No following combining character
@@ -145,7 +158,7 @@ describe('mbyte', function()
eq(0, pcc[0])
end)
- it('5+-byte sequences', function()
+ itp('5+-byte sequences', function()
local pcc = to_intp()
-- No following combining character
diff --git a/test/unit/memory_spec.lua b/test/unit/memory_spec.lua
new file mode 100644
index 0000000000..bd72c8bf47
--- /dev/null
+++ b/test/unit/memory_spec.lua
@@ -0,0 +1,52 @@
+local helpers = require("test.unit.helpers")(after_each)
+local itp = helpers.gen_itp(it)
+
+local cimport = helpers.cimport
+local cstr = helpers.cstr
+local eq = helpers.eq
+local ffi = helpers.ffi
+local to_cstr = helpers.to_cstr
+
+local cimp = cimport('stdlib.h', './src/nvim/memory.h')
+
+describe('xstrlcat()', function()
+ local function test_xstrlcat(dst, src, dsize)
+ assert.is_true(dsize >= 1 + string.len(dst)) -- sanity check for tests
+ local dst_cstr = cstr(dsize, dst)
+ local src_cstr = to_cstr(src)
+ eq(string.len(dst .. src), cimp.xstrlcat(dst_cstr, src_cstr, dsize))
+ return ffi.string(dst_cstr)
+ end
+
+ local function test_xstrlcat_overlap(dst, src_idx, dsize)
+ assert.is_true(dsize >= 1 + string.len(dst)) -- sanity check for tests
+ local dst_cstr = cstr(dsize, dst)
+ local src_cstr = dst_cstr + src_idx -- pointer into `dst` (overlaps)
+ eq(string.len(dst) + string.len(dst) - src_idx,
+ cimp.xstrlcat(dst_cstr, src_cstr, dsize))
+ return ffi.string(dst_cstr)
+ end
+
+ itp('concatenates strings', function()
+ eq('ab', test_xstrlcat('a', 'b', 3))
+ eq('ab', test_xstrlcat('a', 'b', 4096))
+ eq('ABCיהZdefgiיהZ', test_xstrlcat('ABCיהZ', 'defgiיהZ', 4096))
+ eq('b', test_xstrlcat('', 'b', 4096))
+ eq('a', test_xstrlcat('a', '', 4096))
+ end)
+
+ itp('concatenates overlapping strings', function()
+ eq('abcabc', test_xstrlcat_overlap('abc', 0, 7))
+ eq('abca', test_xstrlcat_overlap('abc', 0, 5))
+ eq('abcb', test_xstrlcat_overlap('abc', 1, 5))
+ eq('abcc', test_xstrlcat_overlap('abc', 2, 10))
+ eq('abcabc', test_xstrlcat_overlap('abc', 0, 2343))
+ end)
+
+ itp('truncates if `dsize` is too small', function()
+ eq('a', test_xstrlcat('a', 'b', 2))
+ eq('', test_xstrlcat('', 'b', 1))
+ eq('ABCיהZd', test_xstrlcat('ABCיהZ', 'defgiיהZ', 10))
+ end)
+
+end)
diff --git a/test/unit/message_spec.lua b/test/unit/message_spec.lua
new file mode 100644
index 0000000000..7e92b5c857
--- /dev/null
+++ b/test/unit/message_spec.lua
@@ -0,0 +1,60 @@
+local helpers = require("test.unit.helpers")(after_each)
+local itp = helpers.gen_itp(it)
+
+local ffi = helpers.ffi
+local eq = helpers.eq
+local to_cstr = helpers.to_cstr
+
+local cimp = helpers.cimport('./src/nvim/message.h', './src/nvim/memory.h',
+ './src/nvim/strings.h')
+
+describe('trunc_string', function()
+ local buflen = 40
+ local function test_inplace(s, expected, room)
+ room = room and room or 20
+ local buf = cimp.xmalloc(ffi.sizeof('char_u') * buflen)
+ ffi.C.strcpy(buf, s)
+ cimp.trunc_string(buf, buf, room, buflen)
+ eq(expected, ffi.string(buf))
+ cimp.xfree(buf)
+ end
+
+ local function test_copy(s, expected, room)
+ room = room and room or 20
+ local buf = cimp.xmalloc(ffi.sizeof('char_u') * buflen)
+ local str = cimp.vim_strsave(to_cstr(s))
+ cimp.trunc_string(str, buf, room, buflen)
+ eq(expected, ffi.string(buf))
+ cimp.xfree(buf)
+ cimp.xfree(str)
+ end
+
+ local permutations = {
+ { ['desc'] = 'in-place', ['func'] = test_inplace },
+ { ['desc'] = 'by copy', ['func'] = test_copy },
+ }
+
+ for _,t in ipairs(permutations) do
+ describe('populates buf '..t.desc, function()
+ itp('with a small string', function()
+ t.func('text', 'text')
+ end)
+
+ itp('with a medium string', function()
+ t.func('a short text', 'a short text')
+ end)
+
+ itp('with a string of length == 1/2 room', function()
+ t.func('a text that fits', 'a text that fits', 34)
+ end)
+
+ itp('with a string exactly the truncate size', function()
+ t.func('a text tha just fits', 'a text tha just fits')
+ end)
+
+ itp('with a string that must be truncated', function()
+ t.func('a text that nott fits', 'a text t...nott fits')
+ end)
+ end)
+ end
+end)
diff --git a/test/unit/multiqueue_spec.lua b/test/unit/multiqueue_spec.lua
new file mode 100644
index 0000000000..bb08a8386f
--- /dev/null
+++ b/test/unit/multiqueue_spec.lua
@@ -0,0 +1,149 @@
+local helpers = require("test.unit.helpers")(after_each)
+local itp = helpers.gen_itp(it)
+
+local child_call_once = helpers.child_call_once
+local cimport = helpers.cimport
+local ffi = helpers.ffi
+local eq = helpers.eq
+
+local multiqueue = cimport("./test/unit/fixtures/multiqueue.h")
+
+describe("multiqueue (multi-level event-queue)", function()
+ local parent, child1, child2, child3
+
+ local function put(q, str)
+ multiqueue.ut_multiqueue_put(q, str)
+ end
+
+ local function get(q)
+ return ffi.string(multiqueue.ut_multiqueue_get(q))
+ end
+
+ local function free(q)
+ multiqueue.multiqueue_free(q)
+ end
+
+ before_each(function()
+ child_call_once(function()
+ parent = multiqueue.multiqueue_new_parent(ffi.NULL, ffi.NULL)
+ child1 = multiqueue.multiqueue_new_child(parent)
+ child2 = multiqueue.multiqueue_new_child(parent)
+ child3 = multiqueue.multiqueue_new_child(parent)
+ put(child1, 'c1i1')
+ put(child1, 'c1i2')
+ put(child2, 'c2i1')
+ put(child1, 'c1i3')
+ put(child2, 'c2i2')
+ put(child2, 'c2i3')
+ put(child2, 'c2i4')
+ put(child3, 'c3i1')
+ put(child3, 'c3i2')
+ end)
+ end)
+
+ itp('keeps count of added events', function()
+ eq(3, multiqueue.multiqueue_size(child1))
+ eq(4, multiqueue.multiqueue_size(child2))
+ eq(2, multiqueue.multiqueue_size(child3))
+ end)
+
+ itp('keeps count of removed events', function()
+ multiqueue.multiqueue_get(child1)
+ eq(2, multiqueue.multiqueue_size(child1))
+ multiqueue.multiqueue_get(child1)
+ eq(1, multiqueue.multiqueue_size(child1))
+ multiqueue.multiqueue_get(child1)
+ eq(0, multiqueue.multiqueue_size(child1))
+ put(child1, 'c2ixx')
+ eq(1, multiqueue.multiqueue_size(child1))
+ multiqueue.multiqueue_get(child1)
+ eq(0, multiqueue.multiqueue_size(child1))
+ multiqueue.multiqueue_get(child1)
+ eq(0, multiqueue.multiqueue_size(child1))
+ end)
+
+ itp('removing from parent removes from child', function()
+ eq('c1i1', get(parent))
+ eq('c1i2', get(parent))
+ eq('c2i1', get(parent))
+ eq('c1i3', get(parent))
+ eq('c2i2', get(parent))
+ eq('c2i3', get(parent))
+ eq('c2i4', get(parent))
+ end)
+
+ itp('removing from child removes from parent', function()
+ eq('c2i1', get(child2))
+ eq('c2i2', get(child2))
+ eq('c1i1', get(child1))
+ eq('c1i2', get(parent))
+ eq('c1i3', get(parent))
+ eq('c2i3', get(parent))
+ eq('c2i4', get(parent))
+ end)
+
+ itp('removing from child at the beginning of parent', function()
+ eq('c1i1', get(child1))
+ eq('c1i2', get(child1))
+ eq('c2i1', get(parent))
+ end)
+
+ itp('removing from parent after get from parent and put to child', function()
+ eq('c1i1', get(parent))
+ eq('c1i2', get(parent))
+ eq('c2i1', get(parent))
+ eq('c1i3', get(parent))
+ eq('c2i2', get(parent))
+ eq('c2i3', get(parent))
+ eq('c2i4', get(parent))
+ eq('c3i1', get(parent))
+ put(child1, 'c1i11')
+ put(child1, 'c1i22')
+ eq('c3i2', get(parent))
+ eq('c1i11', get(parent))
+ eq('c1i22', get(parent))
+ end)
+
+ itp('removing from parent after get and put to child', function()
+ eq('c1i1', get(child1))
+ eq('c1i2', get(child1))
+ eq('c2i1', get(child2))
+ eq('c1i3', get(child1))
+ eq('c2i2', get(child2))
+ eq('c2i3', get(child2))
+ eq('c2i4', get(child2))
+ eq('c3i1', get(child3))
+ eq('c3i2', get(parent))
+ put(child1, 'c1i11')
+ put(child2, 'c2i11')
+ put(child1, 'c1i12')
+ eq('c2i11', get(child2))
+ eq('c1i11', get(parent))
+ eq('c1i12', get(parent))
+ end)
+
+ itp('put after removing from child at the end of parent', function()
+ eq('c3i1', get(child3))
+ eq('c3i2', get(child3))
+ put(child1, 'c1i11')
+ put(child2, 'c2i11')
+ eq('c1i1', get(parent))
+ eq('c1i2', get(parent))
+ eq('c2i1', get(parent))
+ eq('c1i3', get(parent))
+ eq('c2i2', get(parent))
+ eq('c2i3', get(parent))
+ eq('c2i4', get(parent))
+ eq('c1i11', get(parent))
+ eq('c2i11', get(parent))
+ end)
+
+ itp('removes from parent queue when child is freed', function()
+ free(child2)
+ eq('c1i1', get(parent))
+ eq('c1i2', get(parent))
+ eq('c1i3', get(parent))
+ eq('c3i1', get(child3))
+ eq('c3i2', get(child3))
+ end)
+end)
diff --git a/test/unit/option_spec.lua b/test/unit/option_spec.lua
new file mode 100644
index 0000000000..b8b8a435bc
--- /dev/null
+++ b/test/unit/option_spec.lua
@@ -0,0 +1,52 @@
+local helpers = require("test.unit.helpers")(after_each)
+local itp = helpers.gen_itp(it)
+
+local to_cstr = helpers.to_cstr
+local eq = helpers.eq
+
+local option = helpers.cimport("./src/nvim/option.h")
+local globals = helpers.cimport("./src/nvim/globals.h")
+
+local check_ff_value = function(ff)
+ return option.check_ff_value(to_cstr(ff))
+end
+
+describe('check_ff_value', function()
+
+ itp('views empty string as valid', function()
+ eq(1, check_ff_value(""))
+ end)
+
+ itp('views "unix", "dos" and "mac" as valid', function()
+ eq(1, check_ff_value("unix"))
+ eq(1, check_ff_value("dos"))
+ eq(1, check_ff_value("mac"))
+ end)
+
+ itp('views "foo" as invalid', function()
+ eq(0, check_ff_value("foo"))
+ end)
+end)
+
+describe('get_sts_value', function()
+ itp([[returns 'softtabstop' when it is non-negative]], function()
+ globals.curbuf.b_p_sts = 5
+ eq(5, option.get_sts_value())
+
+ globals.curbuf.b_p_sts = 0
+ eq(0, option.get_sts_value())
+ end)
+
+ itp([[returns "effective shiftwidth" when 'softtabstop' is negative]], function()
+ local shiftwidth = 2
+ globals.curbuf.b_p_sw = shiftwidth
+ local tabstop = 5
+ globals.curbuf.b_p_ts = tabstop
+ globals.curbuf.b_p_sts = -2
+ eq(shiftwidth, option.get_sts_value())
+
+ shiftwidth = 0
+ globals.curbuf.b_p_sw = shiftwidth
+ eq(tabstop, option.get_sts_value())
+ end)
+end)
diff --git a/test/unit/os/env_spec.lua b/test/unit/os/env_spec.lua
index 9e00a3e8f8..c54d5a9b77 100644
--- a/test/unit/os/env_spec.lua
+++ b/test/unit/os/env_spec.lua
@@ -1,4 +1,5 @@
-local helpers = require('test.unit.helpers')
+local helpers = require('test.unit.helpers')(after_each)
+local itp = helpers.gen_itp(it)
local cimport = helpers.cimport
local eq = helpers.eq
@@ -10,19 +11,19 @@ local NULL = helpers.NULL
require('lfs')
-local env = cimport('./src/nvim/os/os.h')
+local cimp = cimport('./src/nvim/os/os.h')
-describe('env function', function()
+describe('env.c', function()
local function os_setenv(name, value, override)
- return env.os_setenv((to_cstr(name)), (to_cstr(value)), override)
+ return cimp.os_setenv(to_cstr(name), to_cstr(value), override)
end
local function os_unsetenv(name, _, _)
- return env.os_unsetenv((to_cstr(name)))
+ return cimp.os_unsetenv(to_cstr(name))
end
local function os_getenv(name)
- local rval = env.os_getenv((to_cstr(name)))
+ local rval = cimp.os_getenv(to_cstr(name))
if rval ~= NULL then
return ffi.string(rval)
else
@@ -33,18 +34,18 @@ describe('env function', function()
describe('os_setenv', function()
local OK = 0
- it('sets an env variable and returns OK', function()
- local name = 'NEOVIM_UNIT_TEST_SETENV_1N'
- local value = 'NEOVIM_UNIT_TEST_SETENV_1V'
+ itp('sets an env variable and returns OK', function()
+ 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)
- it("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'
+ itp("dosn't overwrite an env variable if overwrite is 0", function()
+ 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)))
@@ -52,24 +53,63 @@ describe('env function', function()
end)
end)
+ describe('os_setenv_append_path', function()
+ itp('appends /foo/bar to $PATH', function()
+ local original_path = os.getenv('PATH')
+ eq(true, cimp.os_setenv_append_path(to_cstr('/foo/bar/baz')))
+ eq(original_path..':/foo/bar', os.getenv('PATH'))
+ end)
+
+ itp('returns false if `fname` is not absolute', function()
+ local original_path = os.getenv('PATH')
+ eq(false, cimp.os_setenv_append_path(to_cstr('foo/bar/baz')))
+ eq(original_path, os.getenv('PATH'))
+ 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()
- it('reads an env variable', function()
- local name = 'NEOVIM_UNIT_TEST_GETENV_1N'
- local value = 'NEOVIM_UNIT_TEST_GETENV_1V'
+ itp('reads an env variable', function()
+ 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)
- it('returns NULL if the env variable is not found', function()
- local name = 'NEOVIM_UNIT_TEST_GETENV_NOTFOUND'
+ itp('returns NULL if the env variable is not found', function()
+ local name = 'NVIM_UNIT_TEST_GETENV_NOTFOUND'
return eq(NULL, os_getenv(name))
end)
end)
describe('os_unsetenv', function()
- it('unsets environment variable', function()
+ itp('unsets environment variable', function()
local name = 'TEST_UNSETENV'
local value = 'TESTVALUE'
os_setenv(name, value, 1)
@@ -81,113 +121,126 @@ describe('env function', function()
end)
describe('os_getenvname_at_index', function()
- it('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'
+ itp('returns names of environment variables', function()
+ 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 = { }
local found_name = false
- local name = env.os_getenvname_at_index(i)
+ local name = cimp.os_getenvname_at_index(i)
while name ~= NULL do
table.insert(names, ffi.string(name))
if (ffi.string(name)) == test_name then
found_name = true
end
i = i + 1
- name = env.os_getenvname_at_index(i)
+ name = cimp.os_getenvname_at_index(i)
end
eq(true, (table.getn(names)) > 0)
eq(true, found_name)
end)
- it('returns NULL if the index is out of bounds', function()
+ itp('returns NULL if the index is out of bounds', function()
local huge = ffi.new('size_t', 10000)
local maxuint32 = ffi.new('size_t', 4294967295)
- eq(NULL, env.os_getenvname_at_index(huge))
- eq(NULL, env.os_getenvname_at_index(maxuint32))
+ eq(NULL, cimp.os_getenvname_at_index(huge))
+ eq(NULL, cimp.os_getenvname_at_index(maxuint32))
if ffi.abi('64bit') then
-- couldn't use a bigger number because it gets converted to
-- double somewere, should be big enough anyway
-- maxuint64 = ffi.new 'size_t', 18446744073709551615
local maxuint64 = ffi.new('size_t', 18446744073709000000)
- eq(NULL, env.os_getenvname_at_index(maxuint64))
+ eq(NULL, cimp.os_getenvname_at_index(maxuint64))
end
end)
end)
describe('os_get_pid', function()
- it('returns the process ID', function()
+ itp('returns the process ID', function()
local stat_file = io.open('/proc/self/stat')
if stat_file then
local stat_str = stat_file:read('*l')
stat_file:close()
local pid = tonumber((stat_str:match('%d+')))
- eq(pid, tonumber(env.os_get_pid()))
+ eq(pid, tonumber(cimp.os_get_pid()))
else
-- /proc is not available on all systems, test if pid is nonzero.
- eq(true, (env.os_get_pid() > 0))
+ eq(true, (cimp.os_get_pid() > 0))
end
end)
end)
describe('os_get_hostname', function()
- it('returns the hostname', function()
+ itp('returns the hostname', function()
local handle = io.popen('hostname')
local hostname = handle:read('*l')
handle:close()
local hostname_buf = cstr(255, '')
- env.os_get_hostname(hostname_buf, 255)
+ cimp.os_get_hostname(hostname_buf, 255)
eq(hostname, (ffi.string(hostname_buf)))
end)
end)
describe('expand_env_esc', function()
- it('expands environment variables', function()
- local name = 'NEOVIM_UNIT_TEST_EXPAND_ENV_ESCN'
- local value = 'NEOVIM_UNIT_TEST_EXPAND_ENV_ESCV'
+ itp('expands environment variables', function()
+ 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'
- env.expand_env_esc(input1, output_buff1, 255, false, true, NULL)
- env.expand_env_esc(input2, output_buff2, 255, false, true, NULL)
+ 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))
eq(output_expected, ffi.string(output_buff2))
end)
- it('expands ~ once when one is true', function()
+ itp('expands ~ once when `one` is true', function()
local input = '~/foo ~ foo'
local homedir = cstr(255, '')
- env.expand_env_esc(to_cstr('~'), homedir, 255, false, true, NULL)
+ cimp.expand_env_esc(to_cstr('~'), homedir, 255, false, true, NULL)
local output_expected = ffi.string(homedir) .. "/foo ~ foo"
local output = cstr(255, '')
- env.expand_env_esc(to_cstr(input), output, 255, false, true, NULL)
+ cimp.expand_env_esc(to_cstr(input), output, 255, false, true, NULL)
eq(ffi.string(output), ffi.string(output_expected))
end)
- it('expands ~ every time when one is false', function()
+ itp('expands ~ every time when `one` is false', function()
local input = to_cstr('~/foo ~ foo')
- local homedir = cstr(255, '')
- env.expand_env_esc(to_cstr('~'), homedir, 255, false, true, NULL)
- homedir = ffi.string(homedir)
+ local dst = cstr(255, '')
+ cimp.expand_env_esc(to_cstr('~'), dst, 255, false, true, NULL)
+ local homedir = ffi.string(dst)
local output_expected = homedir .. "/foo " .. homedir .. " foo"
local output = cstr(255, '')
- env.expand_env_esc(input, output, 255, false, false, NULL)
+ cimp.expand_env_esc(input, output, 255, false, false, NULL)
eq(output_expected, ffi.string(output))
end)
- it('respects the dstlen parameter without expansion', function()
+ itp('does not crash #3725', function()
+ local name_out = ffi.new('char[100]')
+ cimp.os_get_user_name(name_out, 100)
+ local curuser = ffi.string(name_out)
+
+ local src = to_cstr("~"..curuser.."/Vcs/django-rest-framework/rest_framework/renderers.py")
+ local dst = cstr(256, "~"..curuser)
+ cimp.expand_env_esc(src, dst, 256, false, false, NULL)
+ local len = string.len(ffi.string(dst))
+ assert.True(len > 56)
+ assert.True(len < 256)
+ end)
+
+ itp('respects `dstlen` without expansion', function()
local input = to_cstr('this is a very long thing that will not fit')
-- The buffer is long enough to actually contain the full input in case the
-- test fails, but we don't tell expand_env_esc that
local output = cstr(255, '')
- env.expand_env_esc(input, output, 5, false, true, NULL)
+ cimp.expand_env_esc(input, output, 5, false, true, NULL)
-- Make sure the first few characters are copied properly and that there is a
-- terminating null character
for i=0,3 do
@@ -196,17 +249,17 @@ describe('env function', function()
eq(0, output[4])
end)
- it('respects the dstlen parameter with expansion', function()
+ itp('respects `dstlen` with expansion', function()
local varname = to_cstr('NVIM_UNIT_TEST_EXPAND_ENV_ESC_DSTLENN')
local varval = to_cstr('NVIM_UNIT_TEST_EXPAND_ENV_ESC_DSTLENV')
- env.os_setenv(varname, varval, 1)
+ cimp.os_setenv(varname, varval, 1)
-- TODO(bobtwinkles) This test uses unix-specific environment variable accessing,
-- should have some alternative for windows
local input = to_cstr('$NVIM_UNIT_TEST_EXPAND_ENV_ESC_DSTLENN/even more stuff')
-- The buffer is long enough to actually contain the full input in case the
-- test fails, but we don't tell expand_env_esc that
local output = cstr(255, '')
- env.expand_env_esc(input, output, 5, false, true, NULL)
+ cimp.expand_env_esc(input, output, 5, false, true, NULL)
-- Make sure the first few characters are copied properly and that there is a
-- terminating null character
-- expand_env_esc SHOULD NOT expand the variable if there is not enough space to
diff --git a/test/unit/os/fileio_spec.lua b/test/unit/os/fileio_spec.lua
index 5358022422..4d58a8934e 100644
--- a/test/unit/os/fileio_spec.lua
+++ b/test/unit/os/fileio_spec.lua
@@ -1,12 +1,15 @@
local lfs = require('lfs')
-local helpers = require('test.unit.helpers')
+local helpers = require('test.unit.helpers')(after_each)
+local itp = helpers.gen_itp(it)
local eq = helpers.eq
local ffi = helpers.ffi
local cimport = helpers.cimport
+local cppimport = helpers.cppimport
-local m = cimport('./src/nvim/os/fileio.h')
+local m = cimport('./src/nvim/os/os.h', './src/nvim/os/fileio.h')
+cppimport('fcntl.h')
local fcontents = ''
for i = 0, 255 do
@@ -57,10 +60,26 @@ local function file_open_new(fname, flags, mode)
return ret1[0], ret2
end
+local function file_open_fd(fd, flags)
+ local ret2 = ffi.new('FileDescriptor')
+ local ret1 = m.file_open_fd(ret2, fd, flags)
+ return ret1, ret2
+end
+
+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), nil)
+ return ret1[0], ret2
+end
+
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
@@ -79,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
@@ -87,149 +110,211 @@ local function file_skip(fp, size)
return m.file_skip(fp, size)
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)
+ eq(0, err)
+ eq({#fcontents, fcontents}, {file_read(fp, #fcontents)})
+ 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)
+ eq(0, err)
+ eq(4, file_write(fp, 'test'))
+ eq(0, m.file_close(fp, false))
+ eq(4, lfs.attributes(filec).size)
+ eq('test', io.open(filec):read('*a'))
+ end)
+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)
+ eq(0, err)
+ eq({#fcontents, fcontents}, {file_read(fp, #fcontents)})
+ 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)
+ eq(0, err)
+ eq(4, file_write(fp, 'test'))
+ eq(0, m.file_free(fp, false))
+ eq(4, lfs.attributes(filec).size)
+ eq('test', io.open(filec):read('*a'))
+ end)
+end)
+
describe('file_open', function()
- it('can create a rwx------ file with kFileCreate', function()
+ itp('can create a rwx------ file with kFileCreate', function()
local err, fp = file_open(filec, m.kFileCreate, 448)
eq(0, err)
local attrs = lfs.attributes(filec)
eq('rwx------', attrs.permissions)
- eq(0, m.file_close(fp))
+ eq(0, m.file_close(fp, false))
end)
- it('can create a rw------- file with kFileCreate', function()
+ itp('can create a rw------- file with kFileCreate', function()
local err, fp = file_open(filec, m.kFileCreate, 384)
eq(0, err)
local attrs = lfs.attributes(filec)
eq('rw-------', attrs.permissions)
- eq(0, m.file_close(fp))
+ eq(0, m.file_close(fp, false))
end)
- it('can create a rwx------ file with kFileCreateOnly', function()
+ itp('can create a rwx------ file with kFileCreateOnly', function()
local err, fp = file_open(filec, m.kFileCreateOnly, 448)
eq(0, err)
local attrs = lfs.attributes(filec)
eq('rwx------', attrs.permissions)
- eq(0, m.file_close(fp))
+ eq(0, m.file_close(fp, false))
end)
- it('can create a rw------- file with kFileCreateOnly', function()
+ itp('can create a rw------- file with kFileCreateOnly', function()
local err, fp = file_open(filec, m.kFileCreateOnly, 384)
eq(0, err)
local attrs = lfs.attributes(filec)
eq('rw-------', attrs.permissions)
- eq(0, m.file_close(fp))
+ eq(0, m.file_close(fp, false))
end)
- it('fails to open an existing file with kFileCreateOnly', function()
+ itp('fails to open an existing file with kFileCreateOnly', function()
local err, _ = file_open(file1, m.kFileCreateOnly, 384)
eq(m.UV_EEXIST, err)
end)
- it('fails to open an symlink with kFileNoSymlink', function()
+ itp('fails to open an symlink with kFileNoSymlink', function()
local err, _ = file_open(linkf, m.kFileNoSymlink, 384)
-- err is UV_EMLINK in FreeBSD, but if I use `ok(err == m.UV_ELOOP or err ==
-- m.UV_EMLINK)`, then I loose the ability to see actual `err` value.
if err ~= m.UV_ELOOP then eq(m.UV_EMLINK, err) end
end)
- it('can open an existing file write-only with kFileCreate', function()
+ itp('can open an existing file write-only with kFileCreate', function()
local err, fp = file_open(file1, m.kFileCreate, 384)
eq(0, err)
eq(true, fp.wr)
- eq(0, m.file_close(fp))
+ eq(0, m.file_close(fp, false))
end)
- it('can open an existing file read-only with zero', function()
+ 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)
- it('can open an existing file read-only with kFileReadOnly', function()
+ 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)
- it('can open an existing file read-only with kFileNoSymlink', function()
+ 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)
- it('can truncate an existing file with kFileTruncate', function()
+ 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)
- it('can open an existing file write-only with kFileWriteOnly', function()
+ itp('can open an existing file write-only with kFileWriteOnly', function()
local err, fp = file_open(file1, m.kFileWriteOnly, 384)
eq(0, err)
eq(true, fp.wr)
- eq(0, m.file_close(fp))
+ eq(0, m.file_close(fp, false))
local attrs = lfs.attributes(file1)
eq(4096, attrs.size)
end)
- it('fails to create a file with just kFileWriteOnly', function()
+ itp('fails to create a file with just kFileWriteOnly', function()
local err, _ = file_open(filec, m.kFileWriteOnly, 384)
eq(m.UV_ENOENT, err)
local attrs = lfs.attributes(filec)
eq(nil, attrs)
end)
- it('can truncate an existing file with kFileTruncate when opening a symlink',
+ itp('can truncate an existing file with kFileTruncate when opening a symlink',
function()
local err, fp = file_open(linkf, m.kFileTruncate, 384)
eq(0, err)
eq(true, fp.wr)
- eq(0, m.file_close(fp))
+ eq(0, m.file_close(fp, false))
local attrs = lfs.attributes(file1)
eq(0, attrs.size)
end)
- it('fails to open a directory write-only', function()
+ itp('fails to open a directory write-only', function()
local err, _ = file_open(dir, m.kFileWriteOnly, 384)
eq(m.UV_EISDIR, err)
end)
- it('fails to open a broken symbolic link write-only', function()
+ itp('fails to open a broken symbolic link write-only', function()
local err, _ = file_open(linkb, m.kFileWriteOnly, 384)
eq(m.UV_ENOENT, err)
end)
- it('fails to open a broken symbolic link read-only', function()
+ itp('fails to open a broken symbolic link read-only', function()
local err, _ = file_open(linkb, m.kFileReadOnly, 384)
eq(m.UV_ENOENT, err)
end)
end)
describe('file_open_new', function()
- it('can open a file read-only', function()
+ itp('can open a file read-only', function()
local err, fp = file_open_new(file1, 0, 384)
eq(0, err)
eq(false, fp.wr)
- eq(0, m.file_free(fp))
+ eq(0, m.file_free(fp, false))
end)
- it('fails to open an existing file with kFileCreateOnly', function()
+ itp('fails to open an existing file with kFileCreateOnly', function()
local err, fp = file_open_new(file1, m.kFileCreateOnly, 384)
eq(m.UV_EEXIST, err)
eq(nil, fp)
end)
end)
--- file_close is called above, so it is not tested directly
+describe('file_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()
- it('can flush writes to disk', function()
+ itp('can flush writes to disk', function()
local err, fp = file_open(filec, m.kFileCreateOnly, 384)
eq(0, file_fsync(fp))
eq(0, err)
@@ -239,12 +324,27 @@ 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)
describe('file_read', function()
- it('can read small chunks of input until eof', function()
+ itp('can read small chunks of input until eof', function()
local err, fp = file_open(file1, 0, 384)
eq(0, err)
eq(false, fp.wr)
@@ -261,29 +361,29 @@ 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)
- it('can read the whole file at once', function()
+ itp('can read the whole file at once', function()
local err, fp = file_open(file1, 0, 384)
eq(0, err)
eq(false, fp.wr)
eq({#fcontents, fcontents}, {file_read(fp, #fcontents)})
eq({0, ('\0'):rep(#fcontents)}, {file_read(fp, #fcontents)})
- eq(0, m.file_close(fp))
+ eq(0, m.file_close(fp, false))
end)
- it('can read more then 1024 bytes after reading a small chunk', function()
+ itp('can read more then 1024 bytes after reading a small chunk', function()
local err, fp = file_open(file1, 0, 384)
eq(0, err)
eq(false, fp.wr)
eq({5, fcontents:sub(1, 5)}, {file_read(fp, 5)})
eq({#fcontents - 5, fcontents:sub(6) .. (('\0'):rep(5))},
{file_read(fp, #fcontents)})
- eq(0, m.file_close(fp))
+ eq(0, m.file_close(fp, false))
end)
- it('can read file by 768-byte-chunks', function()
+ itp('can read file by 768-byte-chunks', function()
local err, fp = file_open(file1, 0, 384)
eq(0, err)
eq(false, fp.wr)
@@ -300,23 +400,23 @@ 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)
describe('file_write', function()
- it('can write the whole file at once', 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 = 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)
- it('can write the whole file by small chunks', function()
+ itp('can write the whole file by small chunks', function()
local err, fp = file_open(filec, m.kFileCreateOnly, 384)
eq(0, err)
eq(true, fp.wr)
@@ -328,12 +428,12 @@ 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)
- it('can write the whole file by 768-byte-chunks', function()
+ itp('can write the whole file by 768-byte-chunks', function()
local err, fp = file_open(filec, m.kFileCreateOnly, 384)
eq(0, err)
eq(true, fp.wr)
@@ -345,14 +445,26 @@ 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()
- it('can skip 3 bytes', function()
+ itp('can skip 3 bytes', function()
local err, fp = file_open(file1, 0, 384)
eq(0, err)
eq(false, fp.wr)
@@ -360,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 7e7eddb6fc..ddb438eb3d 100644
--- a/test/unit/os/fs_spec.lua
+++ b/test/unit/os/fs_spec.lua
@@ -1,7 +1,8 @@
local lfs = require('lfs')
local bit = require('bit')
-local helpers = require('test.unit.helpers')
+local helpers = require('test.unit.helpers')(after_each)
+local itp = helpers.gen_itp(it)
local cimport = helpers.cimport
local cppimport = helpers.cppimport
@@ -15,18 +16,18 @@ local to_cstr = helpers.to_cstr
local OK = helpers.OK
local FAIL = helpers.FAIL
local NULL = helpers.NULL
+
local NODE_NORMAL = 0
local NODE_WRITABLE = 1
-cimport('unistd.h')
cimport('./src/nvim/os/shell.h')
cimport('./src/nvim/option_defs.h')
cimport('./src/nvim/main.h')
cimport('./src/nvim/fileio.h')
-local fs = cimport('./src/nvim/os/os.h')
+local fs = cimport('./src/nvim/os/os.h', './src/nvim/path.h')
cppimport('sys/stat.h')
cppimport('fcntl.h')
-cppimport('uv-errno.h')
+cppimport('uv.h')
local s = ''
for i = 0, 255 do
@@ -34,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
@@ -64,28 +64,27 @@ local function os_getperm(filename)
return tonumber(perm)
end
-describe('fs function', function()
- local orig_test_file_perm
+describe('fs.c', function()
+ local function os_isdir(name)
+ return fs.os_isdir(to_cstr(name))
+ end
- setup(function()
+ before_each(function()
lfs.mkdir('unit-test-directory');
io.open('unit-test-directory/test.file', 'w').close()
- orig_test_file_perm = os_getperm('unit-test-directory/test.file')
io.open('unit-test-directory/test_2.file', 'w').close()
lfs.link('test.file', 'unit-test-directory/test_link.file', true)
lfs.link('non_existing_file.file', 'unit-test-directory/test_broken_link.file', true)
- -- Since the tests are executed, they are called by an executable. We use
- -- that executable for several asserts.
+ -- The tests are invoked with an absolute path to `busted` executable.
absolute_executable = arg[0]
- -- Split absolute_executable into a directory and the actual file name for
- -- later usage.
+ -- Split the absolute_executable path into a directory and filename.
directory, executable_name = string.match(absolute_executable, '^(.*)/(.*)$')
end)
- teardown(function()
+ after_each(function()
os.remove('unit-test-directory/test.file')
os.remove('unit-test-directory/test_2.file')
os.remove('unit-test-directory/test_link.file')
@@ -95,63 +94,68 @@ 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, '')
+ 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)
- it('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 FAIL if the buffer is too small', function()
+ local length = string.len(lfs.currentdir()) + 1
+ local buf = cstr(length - 1, '')
+ eq(FAIL, fs.os_dirname(buf, length - 1))
end)
+ end)
- -- What kind of other failing cases are possible?
- it('returns FAIL if the buffer is too small', function()
- local buf = cstr((length - 1), '')
- eq(FAIL, (os_dirname(buf, (length - 1))))
+ 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)
- local function os_isdir(name)
- return fs.os_isdir((to_cstr(name)))
- end
-
describe('os_isdir', function()
- it('returns false if an empty string is given', function()
+ itp('returns false if an empty string is given', function()
eq(false, (os_isdir('')))
end)
- it('returns false if a nonexisting directory is given', function()
+ itp('returns false if a nonexisting directory is given', function()
eq(false, (os_isdir('non-existing-directory')))
end)
- it('returns false if a nonexisting absolute directory is given', function()
+ itp('returns false if a nonexisting absolute directory is given', function()
eq(false, (os_isdir('/non-existing-directory')))
end)
- it('returns false if an existing file is given', function()
+ itp('returns false if an existing file is given', function()
eq(false, (os_isdir('unit-test-directory/test.file')))
end)
- it('returns true if the current directory is given', function()
+ itp('returns true if the current directory is given', function()
eq(true, (os_isdir('.')))
end)
- it('returns true if the parent directory is given', function()
+ itp('returns true if the parent directory is given', function()
eq(true, (os_isdir('..')))
end)
- it('returns true if an arbitrary directory is given', function()
+ itp('returns true if an arbitrary directory is given', function()
eq(true, (os_isdir('unit-test-directory')))
end)
- it('returns true if an absolute directory is given', function()
+ itp('returns true if an absolute directory is given', function()
eq(true, (os_isdir(directory)))
end)
end)
@@ -181,33 +185,30 @@ describe('fs function', function()
return os_can_exe(name)
end
- it('returns false when given a directory', function()
+ itp('returns false when given a directory', function()
cant_exe('./unit-test-directory')
end)
- it('returns false when given a regular file without executable bit set', function()
+ itp('returns false when given a regular file without executable bit set', function()
cant_exe('unit-test-directory/test.file')
end)
- it('returns false when the given file does not exists', function()
+ itp('returns false when the given file does not exists', function()
cant_exe('does-not-exist.file')
end)
- it('returns the absolute path when given an executable inside $PATH', function()
- -- Since executable_name does not start with "./", the path will be
- -- selected from $PATH. Make sure the ends match, ignore the directories.
- local _, busted = string.match(absolute_executable, '^(.*)/(.*)$')
- local _, name = string.match(exe(executable_name), '^(.*)/(.*)$')
- eq(busted, name)
+ itp('returns the absolute path when given an executable inside $PATH', function()
+ local fullpath = exe('ls')
+ eq(1, fs.path_is_absolute(to_cstr(fullpath)))
end)
- it('returns the absolute path when given an executable relative to the current dir', function()
+ itp('returns the absolute path when given an executable relative to the current dir', function()
local old_dir = lfs.currentdir()
lfs.chdir(directory)
- -- Rely on currentdir to resolve symlinks, if any. Testing against
- -- the absolute path taken from arg[0] may result in failure where
+ -- Rely on currentdir to resolve symlinks, if any. Testing against
+ -- the absolute path taken from arg[0] may result in failure where
-- the path has a symlink in it.
local canonical = lfs.currentdir() .. '/' .. executable_name
local expected = exe(canonical)
@@ -221,10 +222,6 @@ describe('fs function', function()
end)
describe('file permissions', function()
- before_each(function()
- os_setperm('unit-test-directory/test.file', orig_test_file_perm)
- end)
-
local function os_fchown(filename, user_id, group_id)
local fd = ffi.C.open(filename, 0)
local res = fs.os_fchown(fd, user_id, group_id)
@@ -245,22 +242,22 @@ describe('fs function', function()
end
describe('os_getperm', function()
- it('returns UV_ENOENT when the given file does not exist', function()
+ itp('returns UV_ENOENT when the given file does not exist', function()
eq(ffi.C.UV_ENOENT, (os_getperm('non-existing-file')))
end)
- it('returns a perm > 0 when given an existing file', function()
+ itp('returns a perm > 0 when given an existing file', function()
assert.is_true((os_getperm('unit-test-directory')) > 0)
end)
- it('returns S_IRUSR when the file is readable', function()
+ itp('returns S_IRUSR when the file is readable', function()
local perm = os_getperm('unit-test-directory')
assert.is_true((bit_set(perm, ffi.C.kS_IRUSR)))
end)
end)
describe('os_setperm', function()
- it('can set and unset the executable bit of a file', function()
+ itp('can set and unset the executable bit of a file', function()
local perm = os_getperm('unit-test-directory/test.file')
perm = unset_bit(perm, ffi.C.kS_IXUSR)
eq(OK, (os_setperm('unit-test-directory/test.file', perm)))
@@ -272,7 +269,7 @@ describe('fs function', function()
assert.is_true((bit_set(perm, ffi.C.kS_IXUSR)))
end)
- it('fails if given file does not exist', function()
+ itp('fails if given file does not exist', function()
local perm = ffi.C.kS_IXUSR
eq(FAIL, (os_setperm('non-existing-file', perm)))
end)
@@ -280,7 +277,7 @@ describe('fs function', function()
describe('os_fchown', function()
local filename = 'unit-test-directory/test.file'
- it('does not change owner and group if respective IDs are equal to -1', function()
+ itp('does not change owner and group if respective IDs are equal to -1', function()
local uid = lfs.attributes(filename, 'uid')
local gid = lfs.attributes(filename, 'gid')
eq(0, os_fchown(filename, -1, -1))
@@ -292,7 +289,7 @@ describe('fs function', function()
if (os.execute('id -G > /dev/null 2>&1') ~= 0) then
pending('skipped (missing `id` utility)', function() end)
else
- it('owner of a file may change the group of the file to any group of which that owner is a member', function()
+ itp('owner of a file may change the group of the file to any group of which that owner is a member', function()
local file_gid = lfs.attributes(filename, 'gid')
-- Gets ID of any group of which current user is a member except the
@@ -316,7 +313,7 @@ describe('fs function', function()
if (ffi.os == 'Windows' or ffi.C.geteuid() == 0) then
pending('skipped (uv_fs_chown is no-op on Windows)', function() end)
else
- it('returns nonzero if process has not enough permissions', function()
+ itp('returns nonzero if process has not enough permissions', function()
-- chown to root
neq(0, os_fchown(filename, 0, 0))
end)
@@ -325,7 +322,7 @@ describe('fs function', function()
describe('os_file_is_readable', function()
- it('returns false if the file is not readable', function()
+ itp('returns false if the file is not readable', function()
local perm = os_getperm('unit-test-directory/test.file')
perm = unset_bit(perm, ffi.C.kS_IRUSR)
perm = unset_bit(perm, ffi.C.kS_IRGRP)
@@ -334,19 +331,19 @@ describe('fs function', function()
eq(false, os_file_is_readable('unit-test-directory/test.file'))
end)
- it('returns false if the file does not exist', function()
+ itp('returns false if the file does not exist', function()
eq(false, os_file_is_readable(
'unit-test-directory/what_are_you_smoking.gif'))
end)
- it('returns true if the file is readable', function()
+ itp('returns true if the file is readable', function()
eq(true, os_file_is_readable(
'unit-test-directory/test.file'))
end)
end)
describe('os_file_is_writable', function()
- it('returns 0 if the file is readonly', function()
+ itp('returns 0 if the file is readonly', function()
local perm = os_getperm('unit-test-directory/test.file')
perm = unset_bit(perm, ffi.C.kS_IWUSR)
perm = unset_bit(perm, ffi.C.kS_IWGRP)
@@ -355,11 +352,11 @@ describe('fs function', function()
eq(0, os_file_is_writable('unit-test-directory/test.file'))
end)
- it('returns 1 if the file is writable', function()
+ itp('returns 1 if the file is writable', function()
eq(1, os_file_is_writable('unit-test-directory/test.file'))
end)
- it('returns 2 when given a folder with rights to write into', function()
+ itp('returns 2 when given a folder with rights to write into', function()
eq(2, os_file_is_writable('unit-test-directory'))
end)
end)
@@ -393,7 +390,7 @@ describe('fs function', function()
buf = ffi.new('char[?]', size + 1, ('\0'):rep(size))
end
local eof = ffi.new('bool[?]', 1, {true})
- local ret2 = fs.os_read(fd, eof, buf, size)
+ local ret2 = fs.os_read(fd, eof, buf, size, false)
local ret1 = eof[0]
local ret3 = ''
if buf ~= nil then
@@ -411,7 +408,7 @@ describe('fs function', function()
end
local iov = ffi.new('struct iovec[?]', #sizes, bufs)
local eof = ffi.new('bool[?]', 1, {true})
- local ret2 = fs.os_readv(fd, eof, iov, #sizes)
+ local ret2 = fs.os_readv(fd, eof, iov, #sizes, false)
local ret1 = eof[0]
local ret3 = {}
for i = 1,#sizes do
@@ -421,23 +418,23 @@ describe('fs function', function()
return ret1, ret2, ret3
end
local function os_write(fd, data)
- return fs.os_write(fd, data, data and #data or 0)
+ return fs.os_write(fd, data, data and #data or 0, false)
end
describe('os_path_exists', function()
- it('returns false when given a non-existing file', function()
+ itp('returns false when given a non-existing file', function()
eq(false, (os_path_exists('non-existing-file')))
end)
- it('returns true when given an existing file', function()
+ itp('returns true when given an existing file', function()
eq(true, (os_path_exists('unit-test-directory/test.file')))
end)
- it('returns false when given a broken symlink', function()
+ itp('returns false when given a broken symlink', function()
eq(false, (os_path_exists('unit-test-directory/test_broken_link.file')))
end)
- it('returns true when given a directory', function()
+ itp('returns true when given a directory', function()
eq(true, (os_path_exists('unit-test-directory')))
end)
end)
@@ -446,18 +443,18 @@ describe('fs function', function()
local test = 'unit-test-directory/test.file'
local not_exist = 'unit-test-directory/not_exist.file'
- it('can rename file if destination file does not exist', function()
+ itp('can rename file if destination file does not exist', function()
eq(OK, (os_rename(test, not_exist)))
eq(false, (os_path_exists(test)))
eq(true, (os_path_exists(not_exist)))
eq(OK, (os_rename(not_exist, test))) -- restore test file
end)
- it('fail if source file does not exist', function()
+ itp('fail if source file does not exist', function()
eq(FAIL, (os_rename(not_exist, test)))
end)
- it('can overwrite destination file if it exists', function()
+ itp('can overwrite destination file if it exists', function()
local other = 'unit-test-directory/other.file'
local file = io.open(other, 'w')
file:write('other')
@@ -482,11 +479,11 @@ describe('fs function', function()
os.remove('unit-test-directory/test_remove.file')
end)
- it('returns non-zero when given a non-existing file', function()
+ itp('returns non-zero when given a non-existing file', function()
neq(0, (os_remove('non-existing-file')))
end)
- it('removes the given file and returns 0', function()
+ itp('removes the given file and returns 0', function()
local f = 'unit-test-directory/test_remove.file'
assert_file_exists(f)
eq(0, (os_remove(f)))
@@ -494,6 +491,22 @@ describe('fs function', function()
end)
end)
+ describe('os_dup', function()
+ itp('returns new file descriptor', function()
+ local dup0 = fs.os_dup(0)
+ local dup1 = fs.os_dup(1)
+ local dup2 = fs.os_dup(2)
+ local tbl = {[0]=true, [1]=true, [2]=true,
+ [tonumber(dup0)]=true, [tonumber(dup1)]=true,
+ [tonumber(dup2)]=true}
+ local i = 0
+ for _, _ in pairs(tbl) do
+ i = i + 1
+ end
+ eq(i, 6) -- All fds must be unique
+ end)
+ end)
+
describe('os_open', function()
local new_file = 'test_new_file'
local existing_file = 'unit-test-directory/test_existing.file'
@@ -507,30 +520,30 @@ describe('fs function', function()
os.remove(new_file)
end)
- it('returns UV_ENOENT for O_RDWR on a non-existing file', function()
+ itp('returns UV_ENOENT for O_RDWR on a non-existing file', function()
eq(ffi.C.UV_ENOENT, (os_open('non-existing-file', ffi.C.kO_RDWR, 0)))
end)
- it('returns non-negative for O_CREAT on a non-existing file which then can be closed', function()
+ itp('returns non-negative for O_CREAT on a non-existing file which then can be closed', function()
assert_file_does_not_exist(new_file)
local fd = os_open(new_file, ffi.C.kO_CREAT, 0)
assert.is_true(0 <= fd)
eq(0, os_close(fd))
end)
- it('returns non-negative for O_CREAT on a existing file which then can be closed', function()
+ itp('returns non-negative for O_CREAT on a existing file which then can be closed', function()
assert_file_exists(existing_file)
local fd = os_open(existing_file, ffi.C.kO_CREAT, 0)
assert.is_true(0 <= fd)
eq(0, os_close(fd))
end)
- it('returns UV_EEXIST for O_CREAT|O_EXCL on a existing file', function()
+ itp('returns UV_EEXIST for O_CREAT|O_EXCL on a existing file', function()
assert_file_exists(existing_file)
eq(ffi.C.kUV_EEXIST, (os_open(existing_file, (bit.bor(ffi.C.kO_CREAT, ffi.C.kO_EXCL)), 0)))
end)
- it('sets `rwx` permissions for O_CREAT 700 which then can be closed', function()
+ itp('sets `rwx` permissions for O_CREAT 700 which then can be closed', function()
assert_file_does_not_exist(new_file)
--create the file
local fd = os_open(new_file, ffi.C.kO_CREAT, tonumber("700", 8))
@@ -539,7 +552,7 @@ describe('fs function', function()
eq(0, os_close(fd))
end)
- it('sets `rw` permissions for O_CREAT 600 which then can be closed', function()
+ itp('sets `rw` permissions for O_CREAT 600 which then can be closed', function()
assert_file_does_not_exist(new_file)
--create the file
local fd = os_open(new_file, ffi.C.kO_CREAT, tonumber("600", 8))
@@ -548,7 +561,7 @@ describe('fs function', function()
eq(0, os_close(fd))
end)
- it('returns a non-negative file descriptor for an existing file which then can be closed', function()
+ itp('returns a non-negative file descriptor for an existing file which then can be closed', function()
local fd = os_open(existing_file, ffi.C.kO_RDWR, 0)
assert.is_true(0 <= fd)
eq(0, os_close(fd))
@@ -556,7 +569,7 @@ describe('fs function', function()
end)
describe('os_close', function()
- it('returns EBADF for negative file descriptors', function()
+ itp('returns EBADF for negative file descriptors', function()
eq(ffi.C.UV_EBADF, os_close(-1))
eq(ffi.C.UV_EBADF, os_close(-1000))
end)
@@ -575,7 +588,7 @@ describe('fs function', function()
os.remove(file)
end)
- it('can read zero bytes from a file', function()
+ itp('can read zero bytes from a file', function()
local fd = os_open(file, ffi.C.kO_RDONLY, 0)
ok(fd >= 0)
eq({false, 0, ''}, {os_read(fd, nil)})
@@ -583,7 +596,7 @@ describe('fs function', function()
eq(0, os_close(fd))
end)
- it('can read from a file multiple times', function()
+ itp('can read from a file multiple times', function()
local fd = os_open(file, ffi.C.kO_RDONLY, 0)
ok(fd >= 0)
eq({false, 2, '\000\001'}, {os_read(fd, 2)})
@@ -591,7 +604,7 @@ describe('fs function', function()
eq(0, os_close(fd))
end)
- it('can read the whole file at once and then report eof', function()
+ itp('can read the whole file at once and then report eof', function()
local fd = os_open(file, ffi.C.kO_RDONLY, 0)
ok(fd >= 0)
eq({false, #fcontents, fcontents}, {os_read(fd, #fcontents)})
@@ -599,7 +612,7 @@ describe('fs function', function()
eq(0, os_close(fd))
end)
- it('can read the whole file in two calls, one partially', function()
+ itp('can read the whole file in two calls, one partially', function()
local fd = os_open(file, ffi.C.kO_RDONLY, 0)
ok(fd >= 0)
eq({false, #fcontents * 3/4, fcontents:sub(1, #fcontents * 3/4)},
@@ -629,7 +642,7 @@ describe('fs function', function()
os.remove(file)
end)
- it('can read zero bytes from a file', function()
+ itp('can read zero bytes from a file', function()
local fd = os_open(file, ffi.C.kO_RDONLY, 0)
ok(fd >= 0)
eq({false, 0, {}}, {os_readv(fd, {})})
@@ -637,7 +650,7 @@ describe('fs function', function()
eq(0, os_close(fd))
end)
- it('can read from a file multiple times to a differently-sized buffers', function()
+ itp('can read from a file multiple times to a differently-sized buffers', function()
local fd = os_open(file, ffi.C.kO_RDONLY, 0)
ok(fd >= 0)
eq({false, 2, {'\000\001'}}, {os_readv(fd, {2})})
@@ -645,7 +658,7 @@ describe('fs function', function()
eq(0, os_close(fd))
end)
- it('can read the whole file at once and then report eof', function()
+ itp('can read the whole file at once and then report eof', function()
local fd = os_open(file, ffi.C.kO_RDONLY, 0)
ok(fd >= 0)
eq({false,
@@ -662,7 +675,7 @@ describe('fs function', function()
eq(0, os_close(fd))
end)
- it('can read the whole file in two calls, one partially', function()
+ itp('can read the whole file in two calls, one partially', function()
local fd = os_open(file, ffi.C.kO_RDONLY, 0)
ok(fd >= 0)
eq({false, #fcontents * 3/4, {fcontents:sub(1, #fcontents * 3/4)}},
@@ -689,7 +702,7 @@ describe('fs function', function()
os.remove(file)
end)
- it('can write zero bytes to a file', function()
+ itp('can write zero bytes to a file', function()
local fd = os_open(file, ffi.C.kO_WRONLY, 0)
ok(fd >= 0)
eq(0, os_write(fd, ''))
@@ -698,7 +711,7 @@ describe('fs function', function()
eq(0, os_close(fd))
end)
- it('can write some data to a file', function()
+ itp('can write some data to a file', function()
local fd = os_open(file, ffi.C.kO_WRONLY, 0)
ok(fd >= 0)
eq(3, os_write(fd, 'abc'))
@@ -713,11 +726,11 @@ describe('fs function', function()
os.remove('non-existing-file')
end)
- it('returns NODE_NORMAL for non-existing file', function()
+ itp('returns NODE_NORMAL for non-existing file', function()
eq(NODE_NORMAL, fs.os_nodetype(to_cstr('non-existing-file')))
end)
- it('returns NODE_WRITABLE for /dev/stderr', function()
+ itp('returns NODE_WRITABLE for /dev/stderr', function()
eq(NODE_WRITABLE, fs.os_nodetype(to_cstr('/dev/stderr')))
end)
end)
@@ -743,12 +756,12 @@ describe('fs function', function()
end
describe('os_mkdir', function()
- it('returns non-zero when given an already existing directory', function()
+ itp('returns non-zero when given an already existing directory', function()
local mode = ffi.C.kS_IRUSR + ffi.C.kS_IWUSR + ffi.C.kS_IXUSR
neq(0, (os_mkdir('unit-test-directory', mode)))
end)
- it('creates a directory and returns 0', function()
+ itp('creates a directory and returns 0', function()
local mode = ffi.C.kS_IRUSR + ffi.C.kS_IWUSR + ffi.C.kS_IXUSR
eq(false, (os_isdir('unit-test-directory/new-dir')))
eq(0, (os_mkdir('unit-test-directory/new-dir', mode)))
@@ -758,14 +771,14 @@ describe('fs function', function()
end)
describe('os_mkdir_recurse', function()
- it('returns zero when given an already existing directory', function()
+ itp('returns zero when given an already existing directory', function()
local mode = ffi.C.kS_IRUSR + ffi.C.kS_IWUSR + ffi.C.kS_IXUSR
local ret, failed_str = os_mkdir_recurse('unit-test-directory', mode)
eq(0, ret)
eq(nil, failed_str)
end)
- it('fails to create a directory where there is a file', function()
+ itp('fails to create a directory where there is a file', function()
local mode = ffi.C.kS_IRUSR + ffi.C.kS_IWUSR + ffi.C.kS_IXUSR
local ret, failed_str = os_mkdir_recurse(
'unit-test-directory/test.file', mode)
@@ -773,7 +786,7 @@ describe('fs function', function()
eq('unit-test-directory/test.file', failed_str)
end)
- it('fails to create a directory where there is a file in path', function()
+ itp('fails to create a directory where there is a file in path', function()
local mode = ffi.C.kS_IRUSR + ffi.C.kS_IWUSR + ffi.C.kS_IXUSR
local ret, failed_str = os_mkdir_recurse(
'unit-test-directory/test.file/test', mode)
@@ -781,7 +794,7 @@ describe('fs function', function()
eq('unit-test-directory/test.file', failed_str)
end)
- it('succeeds to create a directory', function()
+ itp('succeeds to create a directory', function()
local mode = ffi.C.kS_IRUSR + ffi.C.kS_IWUSR + ffi.C.kS_IXUSR
local ret, failed_str = os_mkdir_recurse(
'unit-test-directory/new-dir-recurse', mode)
@@ -792,7 +805,7 @@ describe('fs function', function()
eq(false, os_isdir('unit-test-directory/new-dir-recurse'))
end)
- it('succeeds to create a directory ending with ///', function()
+ itp('succeeds to create a directory ending with ///', function()
local mode = ffi.C.kS_IRUSR + ffi.C.kS_IWUSR + ffi.C.kS_IXUSR
local ret, failed_str = os_mkdir_recurse(
'unit-test-directory/new-dir-recurse///', mode)
@@ -803,7 +816,7 @@ describe('fs function', function()
eq(false, os_isdir('unit-test-directory/new-dir-recurse'))
end)
- it('succeeds to create a directory ending with /', function()
+ itp('succeeds to create a directory ending with /', function()
local mode = ffi.C.kS_IRUSR + ffi.C.kS_IWUSR + ffi.C.kS_IXUSR
local ret, failed_str = os_mkdir_recurse(
'unit-test-directory/new-dir-recurse/', mode)
@@ -814,7 +827,7 @@ describe('fs function', function()
eq(false, os_isdir('unit-test-directory/new-dir-recurse'))
end)
- it('succeeds to create a directory tree', function()
+ itp('succeeds to create a directory tree', function()
local mode = ffi.C.kS_IRUSR + ffi.C.kS_IWUSR + ffi.C.kS_IXUSR
local ret, failed_str = os_mkdir_recurse(
'unit-test-directory/new-dir-recurse/1/2/3', mode)
@@ -833,11 +846,11 @@ describe('fs function', function()
end)
describe('os_rmdir', function()
- it('returns non_zero when given a non-existing directory', function()
+ itp('returns non_zero when given a non-existing directory', function()
neq(0, (os_rmdir('non-existing-directory')))
end)
- it('removes the given directory and returns 0', function()
+ itp('removes the given directory and returns 0', function()
lfs.mkdir('unit-test-directory/new-dir')
eq(0, os_rmdir('unit-test-directory/new-dir'))
eq(false, (os_isdir('unit-test-directory/new-dir')))
@@ -865,19 +878,24 @@ describe('fs function', function()
end
describe('os_fileinfo', function()
- it('returns false if given a non-existing file', 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)))
end)
- it('returns true if given an existing file and fills file_info', function()
+ itp('returns true if given an existing file and fills file_info', function()
local file_info = file_info_new()
local path = 'unit-test-directory/test.file'
assert.is_true((fs.os_fileinfo(path, file_info)))
assert.is_true((is_file_info_filled(file_info)))
end)
- it('returns the file info of the linked file, not the link', function()
+ itp('returns the file info of the linked file, not the link', function()
local file_info = file_info_new()
local path = 'unit-test-directory/test_link.file'
assert.is_true((fs.os_fileinfo(path, file_info)))
@@ -888,19 +906,19 @@ describe('fs function', function()
end)
describe('os_fileinfo_link', function()
- it('returns false if given a non-existing file', function()
+ itp('returns false if given a non-existing file', function()
local file_info = file_info_new()
assert.is_false((fs.os_fileinfo_link('/non-existent', file_info)))
end)
- it('returns true if given an existing file and fills file_info', function()
+ itp('returns true if given an existing file and fills file_info', function()
local file_info = file_info_new()
local path = 'unit-test-directory/test.file'
assert.is_true((fs.os_fileinfo_link(path, file_info)))
assert.is_true((is_file_info_filled(file_info)))
end)
- it('returns the file info of the link, not the linked file', function()
+ itp('returns the file info of the link, not the linked file', function()
local file_info = file_info_new()
local path = 'unit-test-directory/test_link.file'
assert.is_true((fs.os_fileinfo_link(path, file_info)))
@@ -911,12 +929,12 @@ describe('fs function', function()
end)
describe('os_fileinfo_fd', function()
- it('returns false if given an invalid file descriptor', function()
+ itp('returns false if given an invalid file descriptor', function()
local file_info = file_info_new()
assert.is_false((fs.os_fileinfo_fd(-1, file_info)))
end)
- it('returns true if given a file descriptor and fills file_info', function()
+ itp('returns true if given a file descriptor and fills file_info', function()
local file_info = file_info_new()
local path = 'unit-test-directory/test.file'
local fd = ffi.C.open(path, 0)
@@ -927,7 +945,7 @@ describe('fs function', function()
end)
describe('os_fileinfo_id_equal', function()
- it('returns false if file infos represent different files', function()
+ itp('returns false if file infos represent different files', function()
local file_info_1 = file_info_new()
local file_info_2 = file_info_new()
local path_1 = 'unit-test-directory/test.file'
@@ -937,7 +955,7 @@ describe('fs function', function()
assert.is_false((fs.os_fileinfo_id_equal(file_info_1, file_info_2)))
end)
- it('returns true if file infos represent the same file', function()
+ itp('returns true if file infos represent the same file', function()
local file_info_1 = file_info_new()
local file_info_2 = file_info_new()
local path = 'unit-test-directory/test.file'
@@ -946,7 +964,7 @@ describe('fs function', function()
assert.is_true((fs.os_fileinfo_id_equal(file_info_1, file_info_2)))
end)
- it('returns true if file infos represent the same file (symlink)', function()
+ itp('returns true if file infos represent the same file (symlink)', function()
local file_info_1 = file_info_new()
local file_info_2 = file_info_new()
local path_1 = 'unit-test-directory/test.file'
@@ -958,7 +976,7 @@ describe('fs function', function()
end)
describe('os_fileinfo_id', function()
- it('extracts ino/dev from file_info into file_id', function()
+ itp('extracts ino/dev from file_info into file_id', function()
local file_info = file_info_new()
local file_id = file_id_new()
local path = 'unit-test-directory/test.file'
@@ -970,7 +988,7 @@ describe('fs function', function()
end)
describe('os_fileinfo_inode', function()
- it('returns the inode from file_info', function()
+ itp('returns the inode from file_info', function()
local file_info = file_info_new()
local path = 'unit-test-directory/test.file'
assert.is_true((fs.os_fileinfo(path, file_info)))
@@ -980,7 +998,7 @@ describe('fs function', function()
end)
describe('os_fileinfo_size', function()
- it('returns the correct size of a file', function()
+ itp('returns the correct size of a file', function()
local path = 'unit-test-directory/test.file'
local file = io.open(path, 'w')
file:write('some bytes to get filesize != 0')
@@ -994,7 +1012,7 @@ describe('fs function', function()
end)
describe('os_fileinfo_hardlinks', function()
- it('returns the correct number of hardlinks', function()
+ itp('returns the correct number of hardlinks', function()
local path = 'unit-test-directory/test.file'
local path_link = 'unit-test-directory/test_hlink.file'
local file_info = file_info_new()
@@ -1007,7 +1025,7 @@ describe('fs function', function()
end)
describe('os_fileinfo_blocksize', function()
- it('returns the correct blocksize of a file', function()
+ itp('returns the correct blocksize of a file', function()
local path = 'unit-test-directory/test.file'
-- there is a bug in luafilesystem where
-- `lfs.attributes path, 'blksize'` returns the worng value:
@@ -1028,12 +1046,12 @@ describe('fs function', function()
end)
describe('os_fileid', function()
- it('returns false if given an non-existing file', function()
+ itp('returns false if given an non-existing file', function()
local file_id = file_id_new()
assert.is_false((fs.os_fileid('/non-existent', file_id)))
end)
- it('returns true if given an existing file and fills file_id', function()
+ itp('returns true if given an existing file and fills file_id', function()
local file_id = file_id_new()
local path = 'unit-test-directory/test.file'
assert.is_true((fs.os_fileid(path, file_id)))
@@ -1043,14 +1061,14 @@ describe('fs function', function()
end)
describe('os_fileid_equal', function()
- it('returns true if two FileIDs are equal', function()
+ itp('returns true if two FileIDs are equal', function()
local file_id = file_id_new()
local path = 'unit-test-directory/test.file'
assert.is_true((fs.os_fileid(path, file_id)))
assert.is_true((fs.os_fileid_equal(file_id, file_id)))
end)
- it('returns false if two FileIDs are not equal', function()
+ itp('returns false if two FileIDs are not equal', function()
local file_id_1 = file_id_new()
local file_id_2 = file_id_new()
local path_1 = 'unit-test-directory/test.file'
@@ -1062,7 +1080,7 @@ describe('fs function', function()
end)
describe('os_fileid_equal_fileinfo', function()
- it('returns true if file_id and file_info represent the same file', function()
+ itp('returns true if file_id and file_info represent the same file', function()
local file_id = file_id_new()
local file_info = file_info_new()
local path = 'unit-test-directory/test.file'
@@ -1071,7 +1089,7 @@ describe('fs function', function()
assert.is_true((fs.os_fileid_equal_fileinfo(file_id, file_info)))
end)
- it('returns false if file_id and file_info represent different files', function()
+ itp('returns false if file_id and file_info represent different files', function()
local file_id = file_id_new()
local file_info = file_info_new()
local path_1 = 'unit-test-directory/test.file'
diff --git a/test/unit/os/shell_spec.lua b/test/unit/os/shell_spec.lua
index 906f950308..a73fc8e47e 100644
--- a/test/unit/os/shell_spec.lua
+++ b/test/unit/os/shell_spec.lua
@@ -1,16 +1,5 @@
--- not all operating systems support the system()-tests, as of yet.
-local allowed_os = {
- Linux = true,
- OSX = true,
- BSD = true,
- POSIX = true
-}
-
-if allowed_os[jit.os] ~= true then
- return
-end
-
-local helpers = require('test.unit.helpers')
+local helpers = require('test.unit.helpers')(after_each)
+local itp = helpers.gen_itp(it)
local cimported = helpers.cimport(
'./src/nvim/os/shell.h',
'./src/nvim/option_defs.h',
@@ -24,16 +13,19 @@ local to_cstr = helpers.to_cstr
local NULL = ffi.cast('void *', 0)
describe('shell functions', function()
- setup(function()
+ before_each(function()
-- os_system() can't work when the p_sh and p_shcf variables are unset
- cimported.p_sh = to_cstr('/bin/bash')
+ cimported.p_sh = to_cstr('/bin/sh')
cimported.p_shcf = to_cstr('-c')
+ cimported.p_sxq = to_cstr('')
+ cimported.p_sxe = to_cstr('')
end)
local function shell_build_argv(cmd, extra_args)
local res = cimported.shell_build_argv(
cmd and to_cstr(cmd),
extra_args and to_cstr(extra_args))
+ -- `res` is zero-indexed (C pointer, not Lua table)!
local argc = 0
local ret = {}
-- Explicitly free everything, so if it is not in allocated memory it will
@@ -47,6 +39,26 @@ describe('shell functions', function()
return ret
end
+ local function shell_argv_to_str(argv_table)
+ -- C string array (char **).
+ local argv = (argv_table
+ and ffi.new("char*[?]", #argv_table+1)
+ or NULL)
+
+ local argc = 1
+ while argv_table ~= nil and argv_table[argc] ~= nil do
+ -- `argv` is zero-indexed (C pointer, not Lua table)!
+ argv[argc - 1] = to_cstr(argv_table[argc])
+ argc = argc + 1
+ end
+ if argv_table ~= nil then
+ argv[argc - 1] = NULL
+ end
+
+ local res = cimported.shell_argv_to_str(argv)
+ return ffi.string(res)
+ end
+
local function os_system(cmd, input)
local input_or = input and to_cstr(input) or NULL
local input_len = (input ~= nil) and string.len(input) or 0
@@ -61,63 +73,51 @@ describe('shell functions', function()
end
describe('os_system', function()
- it('can echo some output (shell builtin)', function()
- local cmd, text = 'echo -n', 'some text'
+ itp('can echo some output (shell builtin)', function()
+ local cmd, text = 'printf "%s "', 'some text '
local status, output = os_system(cmd .. ' ' .. text)
eq(text, output)
eq(0, status)
end)
- it('can deal with empty output', function()
- local cmd = 'echo -n'
+ itp('can deal with empty output', function()
+ local cmd = 'printf ""'
local status, output = os_system(cmd)
eq('', output)
eq(0, status)
end)
- it('can pass input on stdin', function()
+ itp('can pass input on stdin', function()
local cmd, input = 'cat -', 'some text\nsome other text'
local status, output = os_system(cmd, input)
eq(input, output)
eq(0, status)
end)
- it ('returns non-zero exit code', function()
+ itp('returns non-zero exit code', function()
local status = os_system('exit 2')
eq(2, status)
end)
end)
describe('shell_build_argv', function()
- local saved_opts = {}
-
- setup(function()
- saved_opts.p_sh = cimported.p_sh
- saved_opts.p_shcf = cimported.p_shcf
- end)
-
- teardown(function()
- cimported.p_sh = saved_opts.p_sh
- cimported.p_shcf = saved_opts.p_shcf
+ itp('works with NULL arguments', function()
+ eq({'/bin/sh'}, shell_build_argv(nil, nil))
end)
- it('works with NULL arguments', function()
- eq({'/bin/bash'}, shell_build_argv(nil, nil))
+ itp('works with cmd', function()
+ eq({'/bin/sh', '-c', 'abc def'}, shell_build_argv('abc def', nil))
end)
- it('works with cmd', function()
- eq({'/bin/bash', '-c', 'abc def'}, shell_build_argv('abc def', nil))
+ itp('works with extra_args', function()
+ eq({'/bin/sh', 'ghi jkl'}, shell_build_argv(nil, 'ghi jkl'))
end)
- it('works with extra_args', function()
- eq({'/bin/bash', 'ghi jkl'}, shell_build_argv(nil, 'ghi jkl'))
+ itp('works with cmd and extra_args', function()
+ eq({'/bin/sh', 'ghi jkl', '-c', 'abc def'}, shell_build_argv('abc def', 'ghi jkl'))
end)
- it('works with cmd and extra_args', function()
- eq({'/bin/bash', 'ghi jkl', '-c', 'abc def'}, shell_build_argv('abc def', 'ghi jkl'))
- end)
-
- it('splits and unquotes &shell and &shellcmdflag', function()
+ itp('splits and unquotes &shell and &shellcmdflag', function()
cimported.p_sh = to_cstr('/Program" "Files/zsh -f')
cimported.p_shcf = to_cstr('-x -o "sh word split" "-"c')
eq({'/Program Files/zsh', '-f',
@@ -126,5 +126,60 @@ describe('shell functions', function()
'-c', 'abc def'},
shell_build_argv('abc def', 'ghi jkl'))
end)
+
+ itp('applies shellxescape (p_sxe) and shellxquote (p_sxq)', function()
+ cimported.p_sxq = to_cstr('(')
+ cimported.p_sxe = to_cstr('"&|<>()@^')
+
+ local argv = ffi.cast('char**',
+ cimported.shell_build_argv(to_cstr('echo &|<>()@^'), nil))
+ eq(ffi.string(argv[0]), '/bin/sh')
+ eq(ffi.string(argv[1]), '-c')
+ eq(ffi.string(argv[2]), '(echo ^&^|^<^>^(^)^@^^)')
+ eq(nil, argv[3])
+ end)
+
+ itp('applies shellxquote="(', function()
+ cimported.p_sxq = to_cstr('"(')
+ cimported.p_sxe = to_cstr('"&|<>()@^')
+
+ local argv = ffi.cast('char**', cimported.shell_build_argv(
+ to_cstr('echo -n some text'), nil))
+ eq(ffi.string(argv[0]), '/bin/sh')
+ eq(ffi.string(argv[1]), '-c')
+ eq(ffi.string(argv[2]), '"(echo -n some text)"')
+ eq(nil, argv[3])
+ end)
+
+ itp('applies shellxquote="', function()
+ cimported.p_sxq = to_cstr('"')
+ cimported.p_sxe = to_cstr('')
+
+ local argv = ffi.cast('char**', cimported.shell_build_argv(
+ to_cstr('echo -n some text'), nil))
+ eq(ffi.string(argv[0]), '/bin/sh')
+ eq(ffi.string(argv[1]), '-c')
+ eq(ffi.string(argv[2]), '"echo -n some text"')
+ eq(nil, argv[3])
+ end)
+
+ 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/sh')
+ eq(ffi.string(argv[1]), '-c')
+ eq(ffi.string(argv[2]), 'echo -n some text')
+ eq(nil, argv[3])
+ end)
+ end)
+
+ itp('shell_argv_to_str', function()
+ eq('', shell_argv_to_str({ nil }))
+ eq("''", shell_argv_to_str({ '' }))
+ eq("'foo' '' 'bar'", shell_argv_to_str({ 'foo', '', 'bar' }))
+ eq("'/bin/sh' '-c' 'abc def'", shell_argv_to_str({'/bin/sh', '-c', 'abc def'}))
+ eq("'abc def' 'ghi jkl'", shell_argv_to_str({'abc def', 'ghi jkl'}))
+ eq("'/bin/sh' '-c' 'abc def' '"..('x'):rep(225).."...",
+ shell_argv_to_str({'/bin/sh', '-c', 'abc def', ('x'):rep(999)}))
end)
end)
diff --git a/test/unit/os/users_spec.lua b/test/unit/os/users_spec.lua
index 236481e9e7..f92413c7de 100644
--- a/test/unit/os/users_spec.lua
+++ b/test/unit/os/users_spec.lua
@@ -1,4 +1,5 @@
-local helpers = require('test.unit.helpers')
+local helpers = require('test.unit.helpers')(after_each)
+local itp = helpers.gen_itp(it)
local cimport = helpers.cimport
local eq = helpers.eq
@@ -27,11 +28,11 @@ describe('users function', function()
local current_username = os.getenv('USER')
describe('os_get_usernames', function()
- it('returns FAIL if called with NULL', function()
+ itp('returns FAIL if called with NULL', function()
eq(FAIL, users.os_get_usernames(NULL))
end)
- it('fills the names garray with os usernames and returns OK', function()
+ itp('fills the names garray with os usernames and returns OK', function()
local ga_users = garray_new()
eq(OK, users.os_get_usernames(ga_users))
local user_count = garray_get_len(ga_users)
@@ -48,7 +49,7 @@ describe('users function', function()
end)
describe('os_get_user_name', function()
- it('should write the username into the buffer and return OK', function()
+ itp('should write the username into the buffer and return OK', function()
local name_out = ffi.new('char[100]')
eq(OK, users.os_get_user_name(name_out, 100))
eq(current_username, ffi.string(name_out))
@@ -56,14 +57,14 @@ describe('users function', function()
end)
describe('os_get_uname', function()
- it('should write the username into the buffer and return OK', function()
+ itp('should write the username into the buffer and return OK', function()
local name_out = ffi.new('char[100]')
local user_id = lib.getuid()
eq(OK, users.os_get_uname(user_id, name_out, 100))
eq(current_username, ffi.string(name_out))
end)
- it('should FAIL if the userid is not found', function()
+ itp('should FAIL if the userid is not found', function()
local name_out = ffi.new('char[100]')
-- hoping nobody has this uid
local user_id = 2342
@@ -73,16 +74,16 @@ describe('users function', function()
end)
describe('os_get_user_directory', function()
- it('should return NULL if called with NULL', function()
+ itp('should return NULL if called with NULL', function()
eq(NULL, users.os_get_user_directory(NULL))
end)
- it('should return $HOME for the current user', function()
+ itp('should return $HOME for the current user', function()
local home = os.getenv('HOME')
eq(home, ffi.string((users.os_get_user_directory(current_username))))
end)
- it('should return NULL if the user is not found', function()
+ itp('should return NULL if the user is not found', function()
eq(NULL, users.os_get_user_directory('neovim_user_not_found_test'))
end)
end)
diff --git a/test/unit/path_spec.lua b/test/unit/path_spec.lua
index 9b76834383..e8ce660ce1 100644
--- a/test/unit/path_spec.lua
+++ b/test/unit/path_spec.lua
@@ -1,5 +1,6 @@
local lfs = require('lfs')
-local helpers = require('test.unit.helpers')
+local helpers = require('test.unit.helpers')(after_each)
+local itp = helpers.gen_itp(it)
local cimport = helpers.cimport
local eq = helpers.eq
@@ -12,19 +13,12 @@ local OK = helpers.OK
local FAIL = helpers.FAIL
cimport('string.h')
-local path = cimport('./src/nvim/path.h')
-
--- import constants parsed by ffi
-local kEqualFiles = path.kEqualFiles
-local kDifferentFiles = path.kDifferentFiles
-local kBothFilesMissing = path.kBothFilesMissing
-local kOneFileMissing = path.kOneFileMissing
-local kEqualFileNames = path.kEqualFileNames
+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')
@@ -36,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()
@@ -45,7 +39,7 @@ describe('path function', function()
buffer = cstr(length, '')
end)
- it('returns the absolute directory name of a given relative one', function()
+ itp('returns the absolute directory name of a given relative one', function()
local result = path_full_dir_name('..', buffer, length)
eq(OK, result)
local old_dir = lfs.currentdir()
@@ -55,16 +49,16 @@ describe('path function', function()
eq(expected, (ffi.string(buffer)))
end)
- it('returns the current directory name if the given string is empty', function()
+ itp('returns the current directory name if the given string is empty', function()
eq(OK, (path_full_dir_name('', buffer, length)))
eq(lfs.currentdir(), (ffi.string(buffer)))
end)
- it('fails if the given directory does not exist', function()
+ itp('fails if the given directory does not exist', function()
eq(FAIL, path_full_dir_name('does_not_exist', buffer, length))
end)
- it('works with a normal relative dir', function()
+ itp('works with a normal relative dir', function()
local result = path_full_dir_name('unit-test-directory', buffer, length)
eq(lfs.currentdir() .. '/unit-test-directory', (ffi.string(buffer)))
eq(OK, result)
@@ -75,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'
@@ -91,70 +85,70 @@ describe('path function', function()
os.remove(f2)
end)
- it('returns kEqualFiles when passed the same file', function()
- eq(kEqualFiles, (path_full_compare(f1, f1)))
+ itp('returns kEqualFiles when passed the same file', function()
+ eq(cimp.kEqualFiles, (path_full_compare(f1, f1)))
end)
- it('returns kEqualFileNames when files that dont exist and have same name', function()
- eq(kEqualFileNames, (path_full_compare('null.txt', 'null.txt', true)))
+ itp('returns kEqualFileNames when files that dont exist and have same name', function()
+ eq(cimp.kEqualFileNames, (path_full_compare('null.txt', 'null.txt', true)))
end)
- it('returns kBothFilesMissing when files that dont exist', function()
- eq(kBothFilesMissing, (path_full_compare('null.txt', 'null.txt')))
+ itp('returns kBothFilesMissing when files that dont exist', function()
+ eq(cimp.kBothFilesMissing, (path_full_compare('null.txt', 'null.txt')))
end)
- it('returns kDifferentFiles when passed different files', function()
- eq(kDifferentFiles, (path_full_compare(f1, f2)))
- eq(kDifferentFiles, (path_full_compare(f2, f1)))
+ itp('returns kDifferentFiles when passed different files', function()
+ eq(cimp.kDifferentFiles, (path_full_compare(f1, f2)))
+ eq(cimp.kDifferentFiles, (path_full_compare(f2, f1)))
end)
- it('returns kOneFileMissing if only one does not exist', function()
- eq(kOneFileMissing, (path_full_compare(f1, 'null.txt')))
- eq(kOneFileMissing, (path_full_compare('null.txt', f1)))
+ itp('returns kOneFileMissing if only one does not exist', function()
+ 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
- it('returns the tail of a given file path', function()
+ itp('returns the tail of a given file path', function()
eq('file.txt', path_tail('directory/file.txt'))
end)
- it('returns an empty string if file ends in a slash', function()
+ itp('returns an empty string if file ends in a slash', function()
eq('', path_tail('directory/'))
end)
end)
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
- it('returns the tail of a file together with its separator', function()
+ itp('returns the tail of a file together with its separator', function()
eq('///file.txt', path_tail_with_sep('directory///file.txt'))
end)
- it('returns an empty string when given an empty file name', function()
+ itp('returns an empty string when given an empty file name', function()
eq('', path_tail_with_sep(''))
end)
- it('returns only the separator if there is a trailing separator', function()
+ itp('returns only the separator if there is a trailing separator', function()
eq('/', path_tail_with_sep('some/directory/'))
end)
- it('cuts a leading separator', function()
+ itp('cuts a leading separator', function()
eq('file.txt', path_tail_with_sep('/file.txt'))
eq('', path_tail_with_sep('/'))
end)
- it('returns the whole file name if there is no separator', function()
+ itp('returns the whole file name if there is no separator', function()
eq('file.txt', path_tail_with_sep('file.txt'))
end)
end)
@@ -165,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]
@@ -180,13 +174,13 @@ describe('path function', function()
return eq(0, (ffi.C.strncmp((to_cstr(base)), pinvk, len)))
end
- it('returns the executable name of an invocation given a relative invocation', function()
+ itp('returns the executable name of an invocation given a relative invocation', function()
local invk, len = invocation_path_tail('directory/exe a b c')
compare("exe a b c", invk, len)
eq(3, len)
end)
- it('returns the executable name of an invocation given an absolute invocation', function()
+ itp('returns the executable name of an invocation given an absolute invocation', function()
if ffi.os == 'Windows' then
local invk, len = invocation_path_tail('C:\\Users\\anyone\\Program Files\\z a b')
compare('z a b', invk, len)
@@ -198,27 +192,27 @@ describe('path function', function()
end
end)
- it('does not count arguments to the executable as part of its path', function()
+ itp('does not count arguments to the executable as part of its path', function()
local invk, len = invocation_path_tail('exe a/b\\c')
compare("exe a/b\\c", invk, len)
eq(3, len)
end)
- it('only accepts whitespace as a terminator for the executable name', function()
+ itp('only accepts whitespace as a terminator for the executable name', function()
local invk, _ = invocation_path_tail('exe-a+b_c[]()|#!@$%^&*')
eq('exe-a+b_c[]()|#!@$%^&*', (ffi.string(invk)))
end)
- it('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"))
+ itp('is equivalent to path_tail when args do not contain a path separator', function()
+ 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")
eq(tail, ffi.string(invk))
end)
- it('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"))
+ itp('is not equivalent to path_tail when args contain a path separator', function()
+ 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)))
@@ -227,47 +221,47 @@ 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
- it('returns', function()
+ itp('returns', function()
eq('directory/file.txt', path_next_component('some/directory/file.txt'))
end)
- it('returns empty string if given file contains no separator', function()
+ itp('returns empty string if given file contains no separator', function()
eq('', path_next_component('file.txt'))
end)
end)
describe('path_shorten_fname', function()
- it('returns NULL if `full_path` is NULL', 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)
- it('returns NULL if the path and dir does not match', function()
+ 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)
- it('returns NULL if the path is not separated properly', function()
+ 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)
- it('shortens the filename if `dir_name` is the start of `full_path`', function()
+ 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)
-describe('path_shorten_fname_if_possible', function()
+describe('path_try_shorten_fname', function()
local cwd = lfs.currentdir()
before_each(function()
@@ -279,27 +273,80 @@ describe('path_shorten_fname_if_possible', function()
lfs.rmdir('ut_directory')
end)
- describe('path_shorten_fname_if_possible', function()
- it('returns shortened path if possible', function()
+ describe('path_try_shorten_fname', 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_try_shorten_fname(full))))
end)
- it('returns `full_path` if a shorter version is not possible', function()
+ 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_try_shorten_fname(to_cstr(full)))))
end)
- it('returns NULL if `full_path` is NULL', function()
- eq(NULL, (path.path_shorten_fname_if_possible(NULL)))
+ itp('returns NULL if `full_path` is NULL', function()
+ eq(NULL, (cimp.path_try_shorten_fname(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()
@@ -319,115 +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, '')
+ 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 do_expand = 1
+ local buflen = 10
+ local buf = cstr(buflen, '')
+ local result = cimp.vim_FullName(NULL, buf, buflen, do_expand)
+ eq(FAIL, result)
end)
- it('fails if given filename is NULL', function()
- local force_expansion = 1
- local result = path.vim_FullName(NULL, buffer, length, force_expansion)
+ itp('fails safely if given length is wrong #5737', function()
+ local filename = 'foo/bar/bazzzzzzz/buz/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/a'
+ local too_short_len = 8
+ local buf = cstr(too_short_len, '')
+ 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(FAIL, result)
end)
- it('uses the filename if the filename is a URL', function()
- local force_expansion = 1
+ itp('uses the filename if the filename is a URL', function()
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)
- it('fails and uses filename if given filename contains non-existing directory', function()
- local force_expansion = 1
+ itp('fails and uses filename if given filename contains non-existing directory', function()
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)
- it('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)
- it('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?
- it('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)
+ itp('enters given directory (instead of just concatenating the strings) if possible and if path contains a slash', function()
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)
- it('just copies the path if it is already absolute and force=0', function()
- local force_expansion = 0
+ itp('just copies the path if it is already absolute and force=0', function()
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)
- it('fails and uses filename when the path is relative to HOME', function()
- local force_expansion = 1
+ itp('fails and uses filename when the path is relative to HOME', function()
+ 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)
- it('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)
+ itp('works with some "normal" relative path with directories', 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(expected, ffi.string(buf))
eq(OK, result)
- eq(lfs.currentdir() .. '/unit-test-directory/test.file', (ffi.string(buffer)))
end)
- it('does not modify the given filename', function()
- local force_expansion = 1
+ itp('does not modify the given filename', function()
+ 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 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)
- it('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))
+ 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
@@ -435,12 +530,12 @@ describe('more path function', function()
after_each(function() lfs.rmdir('CamelCase') end)
if ffi.os == 'Windows' or ffi.os == 'OSX' then
- it('Corrects the case of file names in Mac and Windows', function()
+ itp('Corrects the case of file names in Mac and Windows', function()
eq('CamelCase', fix_case('camelcase'))
eq('CamelCase', fix_case('cAMELcASE'))
end)
else
- it('does nothing on Linux', function()
+ itp('does nothing on Linux', function()
eq('camelcase', fix_case('camelcase'))
eq('cAMELcASE', fix_case('cAMELcASE'))
end)
@@ -448,64 +543,64 @@ describe('more path function', function()
end)
describe('append_path', function()
- it('joins given paths with a slash', 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)
- it('joins given paths without adding an unnecessary slash', function()
+ 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)
- it('fails and uses filename if there is not enough space left for to_append', function()
+ 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)
- it('does not append a slash if to_append is empty', function()
+ 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)
- it('does not append unnecessary dots', function()
+ 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)
- it('copies to_append to path, if path is empty', function()
+ 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)
- describe('path_is_absolute_path', function()
- local function path_is_absolute_path(filename)
+ describe('path_is_absolute', function()
+ local function path_is_absolute(filename)
filename = to_cstr(filename)
- return path.path_is_absolute_path(filename)
+ return cimp.path_is_absolute(filename)
end
- it('returns true if filename starts with a slash', function()
- eq(OK, path_is_absolute_path('/some/directory/'))
+ itp('returns true if filename starts with a slash', function()
+ eq(OK, path_is_absolute('/some/directory/'))
end)
- it('returns true if filename starts with a tilde', function()
- eq(OK, path_is_absolute_path('~/in/my/home~/directory'))
+ itp('returns true if filename starts with a tilde', function()
+ eq(OK, path_is_absolute('~/in/my/home~/directory'))
end)
- it('returns false if filename starts not with slash nor tilde', function()
- eq(FAIL, path_is_absolute_path('not/in/my/home~/directory'))
+ itp('returns false if filename starts not with slash nor tilde', function()
+ eq(FAIL, path_is_absolute('not/in/my/home~/directory'))
end)
end)
end)
diff --git a/test/unit/preload.lua b/test/unit/preload.lua
index d8ec2c3943..841e19b878 100644
--- a/test/unit/preload.lua
+++ b/test/unit/preload.lua
@@ -2,6 +2,6 @@
-- Busted started doing this to help provide more isolation. See issue #62
-- for more information about this.
local ffi = require('ffi')
-local helpers = require('test.unit.helpers')
+local helpers = require('test.unit.helpers')(nil)
local lfs = require('lfs')
local preprocess = require('test.unit.preprocess')
diff --git a/test/unit/preprocess.lua b/test/unit/preprocess.lua
index e5c838b13b..1073855a7d 100644
--- a/test/unit/preprocess.lua
+++ b/test/unit/preprocess.lua
@@ -2,34 +2,29 @@
-- windows, will probably need quite a bit of adjustment to run there.
local ffi = require("ffi")
+local global_helpers = require('test.helpers')
+
+local argss_to_cmd = global_helpers.argss_to_cmd
+local repeated_read_cmd = global_helpers.repeated_read_cmd
local ccs = {}
local env_cc = os.getenv("CC")
if env_cc then
- table.insert(ccs, {path = "/usr/bin/env " .. tostring(env_cc), type = "gcc"})
+ table.insert(ccs, {path = {"/usr/bin/env", env_cc}, type = "gcc"})
end
if ffi.os == "Windows" then
- table.insert(ccs, {path = "cl", type = "msvc"})
+ table.insert(ccs, {path = {"cl"}, type = "msvc"})
end
-table.insert(ccs, {path = "/usr/bin/env cc", type = "gcc"})
-table.insert(ccs, {path = "/usr/bin/env gcc", type = "gcc"})
-table.insert(ccs, {path = "/usr/bin/env gcc-4.9", type = "gcc"})
-table.insert(ccs, {path = "/usr/bin/env gcc-4.8", type = "gcc"})
-table.insert(ccs, {path = "/usr/bin/env gcc-4.7", type = "gcc"})
-table.insert(ccs, {path = "/usr/bin/env clang", type = "clang"})
-table.insert(ccs, {path = "/usr/bin/env icc", type = "gcc"})
-
-local quote_me = '[^%w%+%-%=%@%_%/]' -- complement (needn't quote)
-local function shell_quote(str)
- if string.find(str, quote_me) or str == '' then
- return "'" .. string.gsub(str, "'", [['"'"']]) .. "'"
- else
- return str
- end
-end
+table.insert(ccs, {path = {"/usr/bin/env", "cc"}, type = "gcc"})
+table.insert(ccs, {path = {"/usr/bin/env", "gcc"}, type = "gcc"})
+table.insert(ccs, {path = {"/usr/bin/env", "gcc-4.9"}, type = "gcc"})
+table.insert(ccs, {path = {"/usr/bin/env", "gcc-4.8"}, type = "gcc"})
+table.insert(ccs, {path = {"/usr/bin/env", "gcc-4.7"}, type = "gcc"})
+table.insert(ccs, {path = {"/usr/bin/env", "clang"}, type = "clang"})
+table.insert(ccs, {path = {"/usr/bin/env", "icc"}, type = "gcc"})
-- parse Makefile format dependencies into a Lua table
local function parse_make_deps(deps)
@@ -61,12 +56,12 @@ end
-- will produce a string that represents a meta C header file that includes
-- all the passed in headers. I.e.:
--
--- headerize({"stdio.h", "math.h", true}
+-- headerize({"stdio.h", "math.h"}, true)
-- produces:
-- #include <stdio.h>
-- #include <math.h>
--
--- headerize({"vim.h", "memory.h", false}
+-- headerize({"vim.h", "memory.h"}, false)
-- produces:
-- #include "vim.h"
-- #include "memory.h"
@@ -79,8 +74,7 @@ local function headerize(headers, global)
end
local formatted = {}
- for i = 1, #headers do
- local hdr = headers[i]
+ for _, hdr in ipairs(headers) do
formatted[#formatted + 1] = "#include " ..
tostring(pre) ..
tostring(hdr) ..
@@ -91,44 +85,69 @@ local function headerize(headers, global)
end
local Gcc = {
+ preprocessor_extra_flags = {},
+ get_defines_extra_flags = {'-std=c99', '-dM', '-E'},
+ get_declarations_extra_flags = {'-std=c99', '-P', '-E'},
+}
+
+function Gcc:define(name, args, val)
+ local define = '-D' .. name
+ if args ~= nil then
+ define = define .. '(' .. table.concat(args, ',') .. ')'
+ end
+ if val ~= nil then
+ define = define .. '=' .. val
+ end
+ self.preprocessor_extra_flags[#self.preprocessor_extra_flags + 1] = define
+end
+
+function Gcc:undefine(name)
+ self.preprocessor_extra_flags[#self.preprocessor_extra_flags + 1] = (
+ '-U' .. name)
+end
+
+function Gcc:init_defines()
-- preprocessor flags that will hopefully make the compiler produce C
-- declarations that the LuaJIT ffi understands.
- preprocessor_extra_flags = {
- '-D "aligned(ARGS)="',
- '-D "__attribute__(ARGS)="',
- '-D "__asm(ARGS)="',
- '-D "__asm__(ARGS)="',
- '-D "__inline__="',
- '-D "EXTERN=extern"',
- '-D "INIT(...)="',
- '-D_GNU_SOURCE',
- '-DINCLUDE_GENERATED_DECLARATIONS',
-
- -- Needed for FreeBSD
- '-D "_Thread_local="'
- }
-}
+ self:define('aligned', {'ARGS'}, '')
+ self:define('__attribute__', {'ARGS'}, '')
+ self:define('__asm', {'ARGS'}, '')
+ self:define('__asm__', {'ARGS'}, '')
+ self:define('__inline__', nil, '')
+ self:define('EXTERN', nil, 'extern')
+ self:define('INIT', {'...'}, '')
+ self:define('_GNU_SOURCE')
+ self:define('INCLUDE_GENERATED_DECLARATIONS')
+ self:define('UNIT_TESTING')
+ self:define('UNIT_TESTING_LUA_PREPROCESSING')
+ -- Needed for FreeBSD
+ self:define('_Thread_local', nil, '')
+ -- Needed for macOS Sierra
+ self:define('_Nullable', nil, '')
+ self:define('_Nonnull', nil, '')
+ self:undefine('__BLOCKS__')
+end
function Gcc:new(obj)
obj = obj or {}
setmetatable(obj, self)
self.__index = self
+ self:init_defines()
return obj
end
function Gcc:add_to_include_path(...)
- local paths = {...}
- for i = 1, #paths do
- local path = paths[i]
- local directive = '-I ' .. '"' .. path .. '"'
+ for i = 1, select('#', ...) do
+ local path = select(i, ...)
local ef = self.preprocessor_extra_flags
- ef[#ef + 1] = directive
+ ef[#ef + 1] = '-I' .. path
end
end
-- returns a list of the headers files upon which this file relies
function Gcc:dependencies(hdr)
- local out = io.popen(tostring(self.path) .. " -M " .. tostring(hdr) .. " 2>&1")
+ local cmd = argss_to_cmd(self.path, {'-M', hdr}) .. ' 2>&1'
+ local out = io.popen(cmd)
local deps = out:read("*a")
out:close()
if deps then
@@ -138,23 +157,63 @@ function Gcc:dependencies(hdr)
end
end
+function Gcc:filter_standard_defines(defines)
+ if not self.standard_defines then
+ local pseudoheader_fname = 'tmp_empty_pseudoheader.h'
+ local pseudoheader_file = io.open(pseudoheader_fname, 'w')
+ pseudoheader_file:close()
+ local standard_defines = repeated_read_cmd(self.path,
+ self.preprocessor_extra_flags,
+ self.get_defines_extra_flags,
+ {pseudoheader_fname})
+ os.remove(pseudoheader_fname)
+ self.standard_defines = {}
+ for line in standard_defines:gmatch('[^\n]+') do
+ self.standard_defines[line] = true
+ end
+ end
+ local ret = {}
+ for line in defines:gmatch('[^\n]+') do
+ if not self.standard_defines[line] then
+ ret[#ret + 1] = line
+ end
+ end
+ return table.concat(ret, "\n")
+end
+
-- returns a stream representing a preprocessed form of the passed-in headers.
-- Don't forget to close the stream by calling the close() method on it.
-function Gcc:preprocess_stream(...)
+function Gcc:preprocess(previous_defines, ...)
-- create pseudo-header
local pseudoheader = headerize({...}, false)
- local defines = table.concat(self.preprocessor_extra_flags, ' ')
- local cmd = ("echo $hdr | " ..
- tostring(self.path) ..
- " " ..
- tostring(defines) ..
- " -std=c99 -P -E -"):gsub('$hdr', shell_quote(pseudoheader))
+ local pseudoheader_fname = 'tmp_pseudoheader.h'
+ local pseudoheader_file = io.open(pseudoheader_fname, 'w')
+ pseudoheader_file:write(previous_defines)
+ pseudoheader_file:write("\n")
+ pseudoheader_file:write(pseudoheader)
+ pseudoheader_file:flush()
+ pseudoheader_file:close()
+
+ local defines = repeated_read_cmd(self.path, self.preprocessor_extra_flags,
+ self.get_defines_extra_flags,
+ {pseudoheader_fname})
+ defines = self:filter_standard_defines(defines)
+
-- lfs = require("lfs")
-- print("CWD: #{lfs.currentdir!}")
-- print("CMD: #{cmd}")
-- io.stderr\write("CWD: #{lfs.currentdir!}\n")
-- io.stderr\write("CMD: #{cmd}\n")
- return io.popen(cmd)
+
+ local declarations = repeated_read_cmd(self.path,
+ self.preprocessor_extra_flags,
+ self.get_declarations_extra_flags,
+ {pseudoheader_fname})
+
+ os.remove(pseudoheader_fname)
+
+ assert(declarations and defines)
+ return declarations, defines
end
local Clang = Gcc:new()
@@ -192,8 +251,8 @@ return {
includes = function(hdr)
return cc:dependencies(hdr)
end,
- preprocess_stream = function(...)
- return cc:preprocess_stream(...)
+ preprocess = function(...)
+ return cc:preprocess(...)
end,
add_to_include_path = function(...)
return cc:add_to_include_path(...)
diff --git a/test/unit/profile_spec.lua b/test/unit/profile_spec.lua
index 852475fe2c..08e5cedbab 100644
--- a/test/unit/profile_spec.lua
+++ b/test/unit/profile_spec.lua
@@ -1,10 +1,13 @@
-local helpers = require 'test.unit.helpers'
+local helpers = require('test.unit.helpers')(after_each)
+local itp = helpers.gen_itp(it)
-local prof = helpers.cimport './src/nvim/profile.h'
+local cimport = helpers.cimport
local ffi = helpers.ffi
local eq = helpers.eq
local neq = helpers.neq
+local prof = cimport('./src/nvim/profile.h')
+
local function split(inputstr, sep)
if sep == nil then
sep = "%s"
@@ -78,7 +81,7 @@ describe('profiling related functions', function()
end
describe('profile_equal', function()
- it('times are equal to themselves', function()
+ itp('times are equal to themselves', function()
local start = profile_start()
assert.is_true(profile_equal(start, start))
@@ -86,7 +89,7 @@ describe('profiling related functions', function()
assert.is_true(profile_equal(e, e))
end)
- it('times are unequal to others', function()
+ itp('times are unequal to others', function()
assert.is_false(profile_equal(profile_start(), profile_start()))
end)
end)
@@ -95,24 +98,24 @@ describe('profiling related functions', function()
-- the profiling package. Those functions in turn will probably be tested
-- using profile_cmp... circular reasoning.
describe('profile_cmp', function()
- it('can compare subsequent starts', function()
+ itp('can compare subsequent starts', function()
local s1, s2 = profile_start(), profile_start()
assert.is_true(profile_cmp(s1, s2) > 0)
assert.is_true(profile_cmp(s2, s1) < 0)
end)
- it('can compare the zero element', function()
+ itp('can compare the zero element', function()
assert.is_true(profile_cmp(profile_zero(), profile_zero()) == 0)
end)
- it('correctly orders divisions', function()
+ itp('correctly orders divisions', function()
local start = profile_start()
assert.is_true(profile_cmp(start, profile_divide(start, 10)) <= 0)
end)
end)
describe('profile_divide', function()
- it('actually performs division', function()
+ itp('actually performs division', function()
-- note: the routine actually performs floating-point division to get
-- better rounding behaviour, we have to take that into account when
-- checking. (check range, not exact number).
@@ -134,14 +137,14 @@ describe('profiling related functions', function()
end)
describe('profile_zero', function()
- it('returns the same value on each call', function()
+ itp('returns the same value on each call', function()
eq(0, profile_zero())
assert.is_true(profile_equal(profile_zero(), profile_zero()))
end)
end)
describe('profile_start', function()
- it('increases', function()
+ itp('increases', function()
local last = profile_start()
for _ = 1, 100 do
local curr = profile_start()
@@ -152,11 +155,11 @@ describe('profiling related functions', function()
end)
describe('profile_end', function()
- it('the elapsed time cannot be zero', function()
+ itp('the elapsed time cannot be zero', function()
neq(profile_zero(), profile_end(profile_start()))
end)
- it('outer elapsed >= inner elapsed', function()
+ itp('outer elapsed >= inner elapsed', function()
for _ = 1, 100 do
local start_outer = profile_start()
local start_inner = profile_start()
@@ -169,11 +172,11 @@ describe('profiling related functions', function()
end)
describe('profile_setlimit', function()
- it('sets no limit when 0 is passed', function()
+ itp('sets no limit when 0 is passed', function()
eq(true, profile_equal(profile_setlimit(0), profile_zero()))
end)
- it('sets a limit in the future otherwise', function()
+ itp('sets a limit in the future otherwise', function()
local future = profile_setlimit(1000)
local now = profile_start()
assert.is_true(profile_cmp(future, now) < 0)
@@ -181,12 +184,12 @@ describe('profiling related functions', function()
end)
describe('profile_passed_limit', function()
- it('start is in the past', function()
+ itp('start is in the past', function()
local start = profile_start()
eq(true, profile_passed_limit(start))
end)
- it('start + start is in the future', function()
+ itp('start + start is in the future', function()
local start = profile_start()
local future = profile_add(start, start)
eq(false, profile_passed_limit(future))
@@ -194,12 +197,12 @@ describe('profiling related functions', function()
end)
describe('profile_msg', function()
- it('prints the zero time as 0.00000', function()
+ itp('prints the zero time as 0.00000', function()
local str = trim(profile_msg(profile_zero()))
eq(str, "0.000000")
end)
- it('prints the time passed, in seconds.microsends', function()
+ itp('prints the time passed, in seconds.microsends', function()
local start = profile_start()
local endt = profile_end(start)
local str = trim(profile_msg(endt))
@@ -221,14 +224,14 @@ describe('profiling related functions', function()
end)
describe('profile_add', function()
- it('adds profiling times', function()
+ itp('adds profiling times', function()
local start = profile_start()
assert.equals(start, profile_add(profile_zero(), start))
end)
end)
describe('profile_sub', function()
- it('subtracts profiling times', function()
+ itp('subtracts profiling times', function()
-- subtracting zero does nothing
local start = profile_start()
assert.equals(start, profile_sub(start, profile_zero()))
diff --git a/test/unit/queue_spec.lua b/test/unit/queue_spec.lua
deleted file mode 100644
index 9326c1cad6..0000000000
--- a/test/unit/queue_spec.lua
+++ /dev/null
@@ -1,123 +0,0 @@
-local helpers = require("test.unit.helpers")
-
-local ffi = helpers.ffi
-local eq = helpers.eq
-
-local queue = helpers.cimport("./test/unit/fixtures/queue.h")
-
-describe('queue', function()
- local parent, child1, child2, child3
-
- local function put(q, str)
- queue.ut_queue_put(q, str)
- end
-
- local function get(q)
- return ffi.string(queue.ut_queue_get(q))
- end
-
- local function free(q)
- queue.queue_free(q)
- end
-
- before_each(function()
- parent = queue.queue_new_parent(ffi.NULL, ffi.NULL)
- child1 = queue.queue_new_child(parent)
- child2 = queue.queue_new_child(parent)
- child3 = queue.queue_new_child(parent)
- put(child1, 'c1i1')
- put(child1, 'c1i2')
- put(child2, 'c2i1')
- put(child1, 'c1i3')
- put(child2, 'c2i2')
- put(child2, 'c2i3')
- put(child2, 'c2i4')
- put(child3, 'c3i1')
- put(child3, 'c3i2')
- end)
-
- it('removing from parent removes from child', function()
- eq('c1i1', get(parent))
- eq('c1i2', get(parent))
- eq('c2i1', get(parent))
- eq('c1i3', get(parent))
- eq('c2i2', get(parent))
- eq('c2i3', get(parent))
- eq('c2i4', get(parent))
- end)
-
- it('removing from child removes from parent', function()
- eq('c2i1', get(child2))
- eq('c2i2', get(child2))
- eq('c1i1', get(child1))
- eq('c1i2', get(parent))
- eq('c1i3', get(parent))
- eq('c2i3', get(parent))
- eq('c2i4', get(parent))
- end)
-
- it('removing from child at the beginning of parent', function()
- eq('c1i1', get(child1))
- eq('c1i2', get(child1))
- eq('c2i1', get(parent))
- end)
-
- it('removing from parent after get from parent and put to child', function()
- eq('c1i1', get(parent))
- eq('c1i2', get(parent))
- eq('c2i1', get(parent))
- eq('c1i3', get(parent))
- eq('c2i2', get(parent))
- eq('c2i3', get(parent))
- eq('c2i4', get(parent))
- eq('c3i1', get(parent))
- put(child1, 'c1i11')
- put(child1, 'c1i22')
- eq('c3i2', get(parent))
- eq('c1i11', get(parent))
- eq('c1i22', get(parent))
- end)
-
- it('removing from parent after get and put to child', function()
- eq('c1i1', get(child1))
- eq('c1i2', get(child1))
- eq('c2i1', get(child2))
- eq('c1i3', get(child1))
- eq('c2i2', get(child2))
- eq('c2i3', get(child2))
- eq('c2i4', get(child2))
- eq('c3i1', get(child3))
- eq('c3i2', get(parent))
- put(child1, 'c1i11')
- put(child2, 'c2i11')
- put(child1, 'c1i12')
- eq('c2i11', get(child2))
- eq('c1i11', get(parent))
- eq('c1i12', get(parent))
- end)
-
- it('put after removing from child at the end of parent', function()
- eq('c3i1', get(child3))
- eq('c3i2', get(child3))
- put(child1, 'c1i11')
- put(child2, 'c2i11')
- eq('c1i1', get(parent))
- eq('c1i2', get(parent))
- eq('c2i1', get(parent))
- eq('c1i3', get(parent))
- eq('c2i2', get(parent))
- eq('c2i3', get(parent))
- eq('c2i4', get(parent))
- eq('c1i11', get(parent))
- eq('c2i11', get(parent))
- end)
-
- it('removes from parent queue when child is freed', function()
- free(child2)
- eq('c1i1', get(parent))
- eq('c1i2', get(parent))
- eq('c1i3', get(parent))
- eq('c3i1', get(child3))
- eq('c3i2', get(child3))
- end)
-end)
diff --git a/test/unit/rbuffer_spec.lua b/test/unit/rbuffer_spec.lua
index 89136410d3..e9104dd5c4 100644
--- a/test/unit/rbuffer_spec.lua
+++ b/test/unit/rbuffer_spec.lua
@@ -1,9 +1,11 @@
-local helpers = require("test.unit.helpers")
+local helpers = require("test.unit.helpers")(after_each)
+local itp = helpers.gen_itp(it)
-local ffi = helpers.ffi
-local eq = helpers.eq
-local cstr = helpers.cstr
+local eq = helpers.eq
+local ffi = helpers.ffi
+local cstr = helpers.cstr
local to_cstr = helpers.to_cstr
+local child_call_once = helpers.child_call_once
local rbuffer = helpers.cimport("./test/unit/fixtures/rbuffer.h")
@@ -31,9 +33,11 @@ describe('rbuffer functions', function()
end
before_each(function()
- rbuf = ffi.gc(rbuffer.rbuffer_new(capacity), rbuffer.rbuffer_free)
- -- fill the internal buffer with the character '0' to simplify inspecting
- ffi.C.memset(rbuf.start_ptr, string.byte('0'), capacity)
+ child_call_once(function()
+ rbuf = ffi.gc(rbuffer.rbuffer_new(capacity), rbuffer.rbuffer_free)
+ -- fill the internal buffer with the character '0' to simplify inspecting
+ ffi.C.memset(rbuf.start_ptr, string.byte('0'), capacity)
+ end)
end)
describe('RBUFFER_UNTIL_FULL', function()
@@ -50,66 +54,51 @@ describe('rbuffer functions', function()
end)
describe('with empty buffer in one contiguous chunk', function()
- it('is called once with the empty chunk', function()
+ itp('is called once with the empty chunk', function()
collect_write_chunks()
eq({'0000000000000000'}, chunks)
end)
end)
describe('with partially empty buffer in one contiguous chunk', function()
- before_each(function()
+ itp('is called once with the empty chunk', function()
write('string')
- end)
-
- it('is called once with the empty chunk', function()
collect_write_chunks()
eq({'0000000000'}, chunks)
end)
end)
describe('with filled buffer in one contiguous chunk', function()
- before_each(function()
+ itp('is not called', function()
write('abcdefghijklmnopq')
- end)
-
- it('is not called', function()
collect_write_chunks()
eq({}, chunks)
end)
end)
describe('with buffer partially empty in two contiguous chunks', function()
- before_each(function()
+ itp('is called twice with each filled chunk', function()
write('1234567890')
read(8)
- end)
-
- it('is called twice with each filled chunk', function()
collect_write_chunks()
eq({'000000', '12345678'}, chunks)
end)
end)
describe('with buffer empty in two contiguous chunks', function()
- before_each(function()
+ itp('is called twice with each filled chunk', function()
write('12345678')
read(8)
- end)
-
- it('is called twice with each filled chunk', function()
collect_write_chunks()
eq({'00000000', '12345678'}, chunks)
end)
end)
describe('with buffer filled in two contiguous chunks', function()
- before_each(function()
+ itp('is not called', function()
write('12345678')
read(8)
write('abcdefghijklmnopq')
- end)
-
- it('is not called', function()
collect_write_chunks()
eq({}, chunks)
end)
@@ -130,55 +119,43 @@ describe('rbuffer functions', function()
end)
describe('with empty buffer', function()
- it('is not called', function()
+ itp('is not called', function()
collect_read_chunks()
eq({}, chunks)
end)
end)
describe('with partially filled buffer in one contiguous chunk', function()
- before_each(function()
+ itp('is called once with the filled chunk', function()
write('string')
- end)
-
- it('is called once with the filled chunk', function()
collect_read_chunks()
eq({'string'}, chunks)
end)
end)
describe('with filled buffer in one contiguous chunk', function()
- before_each(function()
+ itp('is called once with the filled chunk', function()
write('abcdefghijklmnopq')
- end)
-
- it('is called once with the filled chunk', function()
collect_read_chunks()
eq({'abcdefghijklmnop'}, chunks)
end)
end)
describe('with buffer partially filled in two contiguous chunks', function()
- before_each(function()
+ itp('is called twice with each filled chunk', function()
write('1234567890')
read(10)
write('long string')
- end)
-
- it('is called twice with each filled chunk', function()
collect_read_chunks()
eq({'long s', 'tring'}, chunks)
end)
end)
describe('with buffer filled in two contiguous chunks', function()
- before_each(function()
+ itp('is called twice with each filled chunk', function()
write('12345678')
read(8)
write('abcdefghijklmnopq')
- end)
-
- it('is called twice with each filled chunk', function()
collect_read_chunks()
eq({'abcdefgh', 'ijklmnop'}, chunks)
end)
@@ -198,20 +175,17 @@ describe('rbuffer functions', function()
end)
describe('with empty buffer', function()
- it('is not called', function()
+ itp('is not called', function()
collect_chars()
eq({}, chars)
end)
end)
describe('with buffer filled in two contiguous chunks', function()
- before_each(function()
+ itp('collects each character and index', function()
write('1234567890')
read(10)
write('long string')
- end)
-
- it('collects each character and index', function()
collect_chars()
eq({{'l', 0}, {'o', 1}, {'n', 2}, {'g', 3}, {' ', 4}, {'s', 5},
{'t', 6}, {'r', 7}, {'i', 8}, {'n', 9}, {'g', 10}}, chars)
@@ -232,20 +206,17 @@ describe('rbuffer functions', function()
end)
describe('with empty buffer', function()
- it('is not called', function()
+ itp('is not called', function()
collect_chars()
eq({}, chars)
end)
end)
describe('with buffer filled in two contiguous chunks', function()
- before_each(function()
+ itp('collects each character and index', function()
write('1234567890')
read(10)
write('long string')
- end)
-
- it('collects each character and index', function()
collect_chars()
eq({{'g', 10}, {'n', 9}, {'i', 8}, {'r', 7}, {'t', 6}, {'s', 5},
{' ', 4}, {'g', 3}, {'n', 2}, {'o', 1}, {'l', 0}}, chars)
@@ -264,13 +235,10 @@ describe('rbuffer functions', function()
end
describe('with buffer filled in two contiguous chunks', function()
- before_each(function()
+ itp('compares the common longest sequence', function()
write('1234567890')
read(10)
write('long string')
- end)
-
- it('compares the common longest sequence', function()
eq(0, cmp('long string'))
eq(0, cmp('long strin'))
eq(-1, cmp('long striM'))
@@ -282,31 +250,31 @@ describe('rbuffer functions', function()
end)
describe('with empty buffer', function()
- it('returns 0 since no characters are compared', function()
+ itp('returns 0 since no characters are compared', function()
eq(0, cmp(''))
end)
end)
end)
describe('rbuffer_write', function()
- it('fills the internal buffer and returns the write count', function()
+ itp('fills the internal buffer and returns the write count', function()
eq(12, write('short string'))
eq('short string0000', inspect())
end)
- it('wont write beyond capacity', function()
+ itp('wont write beyond capacity', function()
eq(16, write('very very long string'))
eq('very very long s', inspect())
end)
end)
describe('rbuffer_read', function()
- it('reads what was previously written', function()
+ itp('reads what was previously written', function()
write('to read')
eq('to read', read(20))
end)
- it('reads nothing if the buffer is empty', function()
+ itp('reads nothing if the buffer is empty', function()
eq('', read(20))
write('empty')
eq('empty', read(20))
@@ -315,7 +283,7 @@ describe('rbuffer functions', function()
end)
describe('rbuffer_get', function()
- it('fetch the pointer at offset, wrapping if required', function()
+ itp('fetch the pointer at offset, wrapping if required', function()
write('1234567890')
read(10)
write('long string')
@@ -334,7 +302,7 @@ describe('rbuffer functions', function()
end)
describe('wrapping behavior', function()
- it('writing/reading wraps across the end of the internal buffer', function()
+ itp('writing/reading wraps across the end of the internal buffer', function()
write('1234567890')
eq('1234', read(4))
eq('5678', read(4))
diff --git a/test/unit/set.lua b/test/unit/set.lua
index 4e66546f32..f3d68c3042 100644
--- a/test/unit/set.lua
+++ b/test/unit/set.lua
@@ -26,6 +26,22 @@ function Set:new(items)
return obj
end
+function Set:copy()
+ local obj = {}
+ obj.nelem = self.nelem
+ obj.tbl = {}
+ obj.items = {}
+ for k, v in pairs(self.tbl) do
+ obj.tbl[k] = v
+ end
+ for k, v in pairs(self.items) do
+ obj.items[k] = v
+ end
+ setmetatable(obj, Set)
+ obj.__index = Set
+ return obj
+end
+
-- adds the argument Set to this Set
function Set:union(other)
for e in other:iterator() do
diff --git a/test/unit/strings_spec.lua b/test/unit/strings_spec.lua
index 0034670ee8..e54c82b26a 100644
--- a/test/unit/strings_spec.lua
+++ b/test/unit/strings_spec.lua
@@ -1,4 +1,5 @@
-local helpers = require("test.unit.helpers")
+local helpers = require("test.unit.helpers")(after_each)
+local itp = helpers.gen_itp(it)
local cimport = helpers.cimport
local eq = helpers.eq
@@ -13,29 +14,29 @@ describe('vim_strsave_escaped()', function()
local res = strings.vim_strsave_escaped(to_cstr(s), to_cstr(chars))
local ret = ffi.string(res)
- -- Explicitly free memory so we are sure it is allocated: if it was not it
+ -- Explicitly free memory so we are sure it is allocated: if it was not it
-- will crash.
strings.xfree(res)
return ret
end
- it('precedes by a backslash all chars from second argument', function()
+ itp('precedes by a backslash all chars from second argument', function()
eq([[\a\b\c\d]], vim_strsave_escaped('abcd','abcd'))
end)
- it('precedes by a backslash chars only from second argument', function()
+ itp('precedes by a backslash chars only from second argument', function()
eq([[\a\bcd]], vim_strsave_escaped('abcd','ab'))
end)
- it('returns a copy of passed string if second argument is empty', function()
+ itp('returns a copy of passed string if second argument is empty', function()
eq('text \n text', vim_strsave_escaped('text \n text',''))
end)
- it('returns an empty string if first argument is empty string', function()
+ itp('returns an empty string if first argument is empty string', function()
eq('', vim_strsave_escaped('','\r'))
end)
- it('returns a copy of passed string if it does not contain chars from 2nd argument', function()
+ itp('returns a copy of passed string if it does not contain chars from 2nd argument', function()
eq('some text', vim_strsave_escaped('some text', 'a'))
end)
end)
@@ -44,57 +45,96 @@ describe('vim_strnsave_unquoted()', function()
local vim_strnsave_unquoted = function(s, len)
local res = strings.vim_strnsave_unquoted(to_cstr(s), len or #s)
local ret = ffi.string(res)
- -- Explicitly free memory so we are sure it is allocated: if it was not it
+ -- Explicitly free memory so we are sure it is allocated: if it was not it
-- will crash.
strings.xfree(res)
return ret
end
- it('copies unquoted strings as-is', function()
+ itp('copies unquoted strings as-is', function()
eq('-c', vim_strnsave_unquoted('-c'))
eq('', vim_strnsave_unquoted(''))
end)
- it('respects length argument', function()
+ itp('respects length argument', function()
eq('', vim_strnsave_unquoted('-c', 0))
eq('-', vim_strnsave_unquoted('-c', 1))
eq('-', vim_strnsave_unquoted('"-c', 2))
end)
- it('unquotes fully quoted word', function()
+ itp('unquotes fully quoted word', function()
eq('/bin/sh', vim_strnsave_unquoted('"/bin/sh"'))
end)
- it('unquotes partially quoted word', function()
+ itp('unquotes partially quoted word', function()
eq('/Program Files/sh', vim_strnsave_unquoted('/Program" "Files/sh'))
end)
- it('removes ""', function()
+ itp('removes ""', function()
eq('/Program Files/sh', vim_strnsave_unquoted('/""Program" "Files/sh'))
end)
- it('performs unescaping of "', function()
+ itp('performs unescaping of "', function()
eq('/"Program Files"/sh', vim_strnsave_unquoted('/"\\""Program Files"\\""/sh'))
end)
- it('performs unescaping of \\', function()
+ itp('performs unescaping of \\', function()
eq('/\\Program Files\\foo/sh', vim_strnsave_unquoted('/"\\\\"Program Files"\\\\foo"/sh'))
end)
- it('strips quote when there is no pair to it', function()
+ itp('strips quote when there is no pair to it', function()
eq('/Program Files/sh', vim_strnsave_unquoted('/Program" Files/sh'))
eq('', vim_strnsave_unquoted('"'))
end)
- it('allows string to end with one backslash unescaped', function()
+ itp('allows string to end with one backslash unescaped', function()
eq('/Program Files/sh\\', vim_strnsave_unquoted('/Program" Files/sh\\'))
end)
- it('does not perform unescaping out of quotes', function()
+ itp('does not perform unescaping out of quotes', function()
eq('/Program\\ Files/sh\\', vim_strnsave_unquoted('/Program\\ Files/sh\\'))
end)
- it('does not unescape \\n', function()
+ itp('does not unescape \\n', 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/tempfile_spec.lua b/test/unit/tempfile_spec.lua
index cf0d78b7a7..c05abfd640 100644
--- a/test/unit/tempfile_spec.lua
+++ b/test/unit/tempfile_spec.lua
@@ -1,61 +1,68 @@
-local lfs = require 'lfs'
-local helpers = require 'test.unit.helpers'
+local lfs = require('lfs')
+local helpers = require('test.unit.helpers')(after_each)
+local itp = helpers.gen_itp(it)
-local os = helpers.cimport './src/nvim/os/os.h'
-local tempfile = helpers.cimport './src/nvim/fileio.h'
+local eq = helpers.eq
+local neq = helpers.neq
+local cimport = helpers.cimport
+local child_call_once = helpers.child_call_once
+local child_cleanup_once = helpers.child_cleanup_once
+
+local lib = cimport('./src/nvim/os/os.h', './src/nvim/fileio.h')
describe('tempfile related functions', function()
before_each(function()
- tempfile.vim_deltempdir()
- end)
- after_each(function()
- tempfile.vim_deltempdir()
+ local function vim_deltempdir()
+ lib.vim_deltempdir()
+ end
+ child_call_once(vim_deltempdir)
+ child_cleanup_once(vim_deltempdir)
end)
local vim_gettempdir = function()
- return helpers.ffi.string(tempfile.vim_gettempdir())
+ return helpers.ffi.string(lib.vim_gettempdir())
end
describe('vim_gettempdir', function()
- it('returns path to Neovim own temp directory', function()
+ itp('returns path to Neovim own temp directory', function()
local dir = vim_gettempdir()
assert.True(dir ~= nil and dir:len() > 0)
-- os_file_is_writable returns 2 for a directory which we have rights
-- to write into.
- assert.equals(os.os_file_is_writable(helpers.to_cstr(dir)), 2)
+ eq(lib.os_file_is_writable(helpers.to_cstr(dir)), 2)
for entry in lfs.dir(dir) do
assert.True(entry == '.' or entry == '..')
end
end)
- it('returns the same directory on each call', function()
+ itp('returns the same directory on each call', function()
local dir1 = vim_gettempdir()
local dir2 = vim_gettempdir()
- assert.equals(dir1, dir2)
+ eq(dir1, dir2)
end)
end)
describe('vim_tempname', function()
local vim_tempname = function()
- return helpers.ffi.string(tempfile.vim_tempname())
+ return helpers.ffi.string(lib.vim_tempname())
end
- it('generate name of non-existing file', function()
+ itp('generate name of non-existing file', function()
local file = vim_tempname()
assert.truthy(file)
- assert.False(os.os_path_exists(file))
+ assert.False(lib.os_path_exists(file))
end)
- it('generate different names on each call', function()
+ itp('generate different names on each call', function()
local fst = vim_tempname()
local snd = vim_tempname()
- assert.not_equals(fst, snd)
+ neq(fst, snd)
end)
- it('generate file name in Neovim own temp directory', function()
+ itp('generate file name in Neovim own temp directory', function()
local dir = vim_gettempdir()
local file = vim_tempname()
- assert.truthy(file:find('^' .. dir .. '[^/]*$'))
+ eq(string.sub(file, 1, string.len(dir)), dir)
end)
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)
diff --git a/test/unit/undo_spec.lua b/test/unit/undo_spec.lua
new file mode 100644
index 0000000000..f23110b329
--- /dev/null
+++ b/test/unit/undo_spec.lua
@@ -0,0 +1,215 @@
+local helpers = require('test.unit.helpers')(after_each)
+local itp = helpers.gen_itp(it)
+local lfs = require('lfs')
+local child_call_once = helpers.child_call_once
+
+local global_helpers = require('test.helpers')
+local sleep = global_helpers.sleep
+
+local ffi = helpers.ffi
+local cimport = helpers.cimport
+local to_cstr = helpers.to_cstr
+local neq = helpers.neq
+local eq = helpers.eq
+
+cimport('./src/nvim/ex_cmds_defs.h')
+cimport('./src/nvim/buffer_defs.h')
+local options = cimport('./src/nvim/option_defs.h')
+-- TODO: remove: local vim = cimport('./src/nvim/vim.h')
+local undo = cimport('./src/nvim/undo.h')
+local buffer = cimport('./src/nvim/buffer.h')
+
+local old_p_udir = nil
+
+-- Values expected by tests. Set in the setup function and destroyed in teardown
+local file_buffer = nil
+local buffer_hash = nil
+
+child_call_once(function()
+ if old_p_udir == nil then
+ old_p_udir = options.p_udir -- save the old value of p_udir (undodir)
+ end
+
+ -- create a new buffer
+ local c_file = to_cstr('Xtest-unit-undo')
+ file_buffer = buffer.buflist_new(c_file, c_file, 1, buffer.BLN_LISTED)
+ file_buffer.b_u_numhead = 1 -- Pretend that the buffer has been changed
+
+ -- TODO(christopher.waldon.dev@gmail.com): replace the 32 with UNDO_HASH_SIZE
+ -- requires refactor of UNDO_HASH_SIZE into constant/enum for ffi
+ --
+ -- compute a hash for this undofile
+ buffer_hash = ffi.new('char_u[32]')
+ undo.u_compute_hash(buffer_hash)
+end)
+
+
+describe('u_write_undo', function()
+ setup(function()
+ lfs.mkdir('unit-test-directory')
+ lfs.chdir('unit-test-directory')
+ options.p_udir = to_cstr(lfs.currentdir()) -- set p_udir to be the test dir
+ end)
+
+ teardown(function()
+ lfs.chdir('..')
+ local success, err = lfs.rmdir('unit-test-directory')
+ if not success then
+ print(err) -- inform tester if directory fails to delete
+ end
+ options.p_udir = old_p_udir --restore old p_udir
+ end)
+
+ -- Lua wrapper for u_write_undo
+ local function u_write_undo(name, forceit, buf, buf_hash)
+ if name ~= nil then
+ name = to_cstr(name)
+ end
+
+ return undo.u_write_undo(name, forceit, buf, buf_hash)
+ end
+
+ itp('writes an undo file to undodir given a buffer and hash', function()
+ u_write_undo(nil, false, file_buffer, buffer_hash)
+ local correct_name = ffi.string(undo.u_get_undo_file_name(file_buffer.b_ffname, false))
+ local undo_file = io.open(correct_name, "r")
+
+ neq(undo_file, nil)
+ local success, err = os.remove(correct_name) -- delete the file now that we're done with it.
+ if not success then
+ print(err) -- inform tester if undofile fails to delete
+ end
+ end)
+
+ itp('writes a correctly-named undo file to undodir given a name, buffer, and hash', function()
+ local correct_name = "undofile.test"
+ u_write_undo(correct_name, false, file_buffer, buffer_hash)
+ local undo_file = io.open(correct_name, "r")
+
+ neq(undo_file, nil)
+ local success, err = os.remove(correct_name) -- delete the file now that we're done with it.
+ if not success then
+ print(err) -- inform tester if undofile fails to delete
+ end
+ end)
+
+ itp('does not write an undofile when the buffer has no valid undofile name', function()
+ -- TODO(christopher.waldon.dev@gmail.com): Figure out how to test this.
+ -- it's hard because u_get_undo_file_name() would need to return null
+ end)
+
+ itp('writes the undofile with the same permissions as the original file', function()
+ -- Create Test file and set permissions
+ local test_file_name = "./test.file"
+ local test_permission_file = io.open(test_file_name, "w")
+ test_permission_file:write("testing permissions")
+ test_permission_file:close()
+ local test_permissions = lfs.attributes(test_file_name).permissions
+
+ -- Create vim buffer
+ local c_file = to_cstr(test_file_name)
+ file_buffer = buffer.buflist_new(c_file, c_file, 1, buffer.BLN_LISTED)
+ file_buffer.b_u_numhead = 1 -- Pretend that the buffer has been changed
+
+ u_write_undo(nil, false, file_buffer, buffer_hash)
+
+ -- Find out the correct name of the undofile
+ local undo_file_name = ffi.string(undo.u_get_undo_file_name(file_buffer.b_ffname, false))
+
+ -- Find out the permissions of the new file
+ local permissions = lfs.attributes(undo_file_name).permissions
+ eq(test_permissions, permissions)
+
+ -- delete the file now that we're done with it.
+ local success, err = os.remove(test_file_name)
+ if not success then
+ print(err) -- inform tester if undofile fails to delete
+ end
+ success, err = os.remove(undo_file_name)
+ if not success then
+ print(err) -- inform tester if undofile fails to delete
+ end
+ end)
+
+ itp('writes an undofile only readable by the user if the buffer is unnamed', function()
+ local correct_permissions = "rw-------"
+ local undo_file_name = "test.undo"
+
+ -- Create vim buffer
+ file_buffer = buffer.buflist_new(nil, nil, 1, buffer.BLN_LISTED)
+ file_buffer.b_u_numhead = 1 -- Pretend that the buffer has been changed
+
+ u_write_undo(undo_file_name, false, file_buffer, buffer_hash)
+
+ -- Find out the permissions of the new file
+ local permissions = lfs.attributes(undo_file_name).permissions
+ eq(correct_permissions, permissions)
+
+ -- delete the file now that we're done with it.
+ local success, err = os.remove(undo_file_name)
+ if not success then
+ print(err) -- inform tester if undofile fails to delete
+ end
+ end)
+
+ itp('forces writing undo file for :wundo! command', function()
+ local file_contents = "testing permissions"
+ -- Write a text file where the undofile should go
+ local correct_name = ffi.string(undo.u_get_undo_file_name(file_buffer.b_ffname, false))
+ global_helpers.write_file(correct_name, file_contents, true, false)
+
+ -- Call with `forceit`.
+ u_write_undo(correct_name, true, file_buffer, buffer_hash)
+
+ local undo_file_contents = global_helpers.read_file(correct_name)
+
+ neq(file_contents, undo_file_contents)
+ local success, deletion_err = os.remove(correct_name) -- delete the file now that we're done with it.
+ if not success then
+ print(deletion_err) -- inform tester if undofile fails to delete
+ end
+ end)
+
+ itp('overwrites an existing undo file', function()
+ u_write_undo(nil, false, file_buffer, buffer_hash)
+ local correct_name = ffi.string(undo.u_get_undo_file_name(file_buffer.b_ffname, false))
+
+ local file_last_modified = lfs.attributes(correct_name).modification
+
+ sleep(1000) -- Ensure difference in timestamps.
+ file_buffer.b_u_numhead = 1 -- Mark it as if there are changes
+ u_write_undo(nil, false, file_buffer, buffer_hash)
+
+ local file_last_modified_2 = lfs.attributes(correct_name).modification
+
+ -- print(file_last_modified, file_last_modified_2)
+ neq(file_last_modified, file_last_modified_2)
+ local success, err = os.remove(correct_name) -- delete the file now that we're done with it.
+ if not success then
+ print(err) -- inform tester if undofile fails to delete
+ end
+ end)
+
+ itp('does not overwrite an existing file that is not an undo file', function()
+ -- TODO: write test
+ end)
+
+ itp('does not overwrite an existing file that has the wrong permissions', function()
+ -- TODO: write test
+ end)
+
+ itp('does not write an undo file if there is no undo information for the buffer', function()
+ file_buffer.b_u_numhead = 0 -- Mark it as if there is no undo information
+ local correct_name = ffi.string(undo.u_get_undo_file_name(file_buffer.b_ffname, false))
+
+ local existing_file = io.open(correct_name,"r")
+ if existing_file then
+ existing_file:close()
+ os.remove(correct_name)
+ end
+ u_write_undo(nil, false, file_buffer, buffer_hash)
+ local undo_file = io.open(correct_name, "r")
+
+ eq(undo_file, nil)
+ end)
+end)
diff --git a/test/unit/viml/expressions/lexer_spec.lua b/test/unit/viml/expressions/lexer_spec.lua
new file mode 100644
index 0000000000..1b57a24ad5
--- /dev/null
+++ b/test/unit/viml/expressions/lexer_spec.lua
@@ -0,0 +1,428 @@
+local helpers = require('test.unit.helpers')(after_each)
+local global_helpers = require('test.helpers')
+local itp = helpers.gen_itp(it)
+local viml_helpers = require('test.unit.viml.helpers')
+
+local child_call_once = helpers.child_call_once
+local conv_enum = helpers.conv_enum
+local cimport = helpers.cimport
+local ffi = helpers.ffi
+local eq = helpers.eq
+
+local conv_ccs = viml_helpers.conv_ccs
+local new_pstate = viml_helpers.new_pstate
+local conv_cmp_type = viml_helpers.conv_cmp_type
+local pstate_set_str = viml_helpers.pstate_set_str
+local conv_expr_asgn_type = viml_helpers.conv_expr_asgn_type
+
+local shallowcopy = global_helpers.shallowcopy
+local intchar2lua = global_helpers.intchar2lua
+
+local lib = cimport('./src/nvim/viml/parser/expressions.h')
+
+local eltkn_type_tab, eltkn_mul_type_tab, eltkn_opt_scope_tab
+child_call_once(function()
+ eltkn_type_tab = {
+ [tonumber(lib.kExprLexInvalid)] = 'Invalid',
+ [tonumber(lib.kExprLexMissing)] = 'Missing',
+ [tonumber(lib.kExprLexSpacing)] = 'Spacing',
+ [tonumber(lib.kExprLexEOC)] = 'EOC',
+
+ [tonumber(lib.kExprLexQuestion)] = 'Question',
+ [tonumber(lib.kExprLexColon)] = 'Colon',
+ [tonumber(lib.kExprLexOr)] = 'Or',
+ [tonumber(lib.kExprLexAnd)] = 'And',
+ [tonumber(lib.kExprLexComparison)] = 'Comparison',
+ [tonumber(lib.kExprLexPlus)] = 'Plus',
+ [tonumber(lib.kExprLexMinus)] = 'Minus',
+ [tonumber(lib.kExprLexDot)] = 'Dot',
+ [tonumber(lib.kExprLexMultiplication)] = 'Multiplication',
+
+ [tonumber(lib.kExprLexNot)] = 'Not',
+
+ [tonumber(lib.kExprLexNumber)] = 'Number',
+ [tonumber(lib.kExprLexSingleQuotedString)] = 'SingleQuotedString',
+ [tonumber(lib.kExprLexDoubleQuotedString)] = 'DoubleQuotedString',
+ [tonumber(lib.kExprLexOption)] = 'Option',
+ [tonumber(lib.kExprLexRegister)] = 'Register',
+ [tonumber(lib.kExprLexEnv)] = 'Env',
+ [tonumber(lib.kExprLexPlainIdentifier)] = 'PlainIdentifier',
+
+ [tonumber(lib.kExprLexBracket)] = 'Bracket',
+ [tonumber(lib.kExprLexFigureBrace)] = 'FigureBrace',
+ [tonumber(lib.kExprLexParenthesis)] = 'Parenthesis',
+ [tonumber(lib.kExprLexComma)] = 'Comma',
+ [tonumber(lib.kExprLexArrow)] = 'Arrow',
+
+ [tonumber(lib.kExprLexAssignment)] = 'Assignment',
+ }
+
+ eltkn_mul_type_tab = {
+ [tonumber(lib.kExprLexMulMul)] = 'Mul',
+ [tonumber(lib.kExprLexMulDiv)] = 'Div',
+ [tonumber(lib.kExprLexMulMod)] = 'Mod',
+ }
+
+ eltkn_opt_scope_tab = {
+ [tonumber(lib.kExprOptScopeUnspecified)] = 'Unspecified',
+ [tonumber(lib.kExprOptScopeGlobal)] = 'Global',
+ [tonumber(lib.kExprOptScopeLocal)] = 'Local',
+ }
+end)
+
+local function conv_eltkn_type(typ)
+ return conv_enum(eltkn_type_tab, typ)
+end
+
+local bracket_types = {
+ Bracket = true,
+ FigureBrace = true,
+ Parenthesis = true,
+}
+
+local function eltkn2lua(pstate, tkn)
+ local ret = {
+ type = conv_eltkn_type(tkn.type),
+ }
+ pstate_set_str(pstate, tkn.start, tkn.len, ret)
+ if not ret.error and (#(ret.str) ~= ret.len) then
+ ret.error = '#str /= len'
+ end
+ if ret.type == 'Comparison' then
+ ret.data = {
+ type = conv_cmp_type(tkn.data.cmp.type),
+ ccs = conv_ccs(tkn.data.cmp.ccs),
+ inv = (not not tkn.data.cmp.inv),
+ }
+ elseif ret.type == 'Multiplication' then
+ ret.data = { type = conv_enum(eltkn_mul_type_tab, tkn.data.mul.type) }
+ elseif bracket_types[ret.type] then
+ ret.data = { closing = (not not tkn.data.brc.closing) }
+ elseif ret.type == 'Register' then
+ ret.data = { name = intchar2lua(tkn.data.reg.name) }
+ elseif (ret.type == 'SingleQuotedString'
+ or ret.type == 'DoubleQuotedString') then
+ ret.data = { closed = (not not tkn.data.str.closed) }
+ elseif ret.type == 'Option' then
+ ret.data = {
+ scope = conv_enum(eltkn_opt_scope_tab, tkn.data.opt.scope),
+ name = ffi.string(tkn.data.opt.name, tkn.data.opt.len),
+ }
+ elseif ret.type == 'PlainIdentifier' then
+ ret.data = {
+ scope = intchar2lua(tkn.data.var.scope),
+ autoload = (not not tkn.data.var.autoload),
+ }
+ elseif ret.type == 'Number' then
+ ret.data = {
+ is_float = (not not tkn.data.num.is_float),
+ base = tonumber(tkn.data.num.base),
+ }
+ ret.data.val = tonumber(tkn.data.num.is_float
+ and tkn.data.num.val.floating
+ or tkn.data.num.val.integer)
+ elseif ret.type == 'Assignment' then
+ ret.data = { type = conv_expr_asgn_type(tkn.data.ass.type) }
+ elseif ret.type == 'Invalid' then
+ ret.data = { error = ffi.string(tkn.data.err.msg) }
+ end
+ return ret, tkn
+end
+
+local function next_eltkn(pstate, flags)
+ return eltkn2lua(pstate, lib.viml_pexpr_next_token(pstate, flags))
+end
+
+describe('Expressions lexer', function()
+ local flags = 0
+ local should_advance = true
+ local function check_advance(pstate, bytes_to_advance, initial_col)
+ local tgt = initial_col + bytes_to_advance
+ if should_advance then
+ if pstate.reader.lines.items[0].size == tgt then
+ eq(1, pstate.pos.line)
+ eq(0, pstate.pos.col)
+ else
+ eq(0, pstate.pos.line)
+ eq(tgt, pstate.pos.col)
+ end
+ else
+ eq(0, pstate.pos.line)
+ eq(initial_col, pstate.pos.col)
+ end
+ end
+ local function singl_eltkn_test(typ, str, data)
+ local pstate = new_pstate({str})
+ eq({data=data, len=#str, start={col=0, line=0}, str=str, type=typ},
+ next_eltkn(pstate, flags))
+ check_advance(pstate, #str, 0)
+ if not (
+ typ == 'Spacing'
+ or (typ == 'Register' and str == '@')
+ or ((typ == 'SingleQuotedString' or typ == 'DoubleQuotedString')
+ and not data.closed)
+ ) then
+ pstate = new_pstate({str .. ' '})
+ eq({data=data, len=#str, start={col=0, line=0}, str=str, type=typ},
+ next_eltkn(pstate, flags))
+ check_advance(pstate, #str, 0)
+ end
+ pstate = new_pstate({'x' .. str})
+ pstate.pos.col = 1
+ eq({data=data, len=#str, start={col=1, line=0}, str=str, type=typ},
+ next_eltkn(pstate, flags))
+ check_advance(pstate, #str, 1)
+ end
+ local function scope_test(scope)
+ singl_eltkn_test('PlainIdentifier', scope .. ':test#var', {autoload=true, scope=scope})
+ singl_eltkn_test('PlainIdentifier', scope .. ':', {autoload=false, scope=scope})
+ end
+ local function comparison_test(op, inv_op, cmp_type)
+ singl_eltkn_test('Comparison', op, {type=cmp_type, inv=false, ccs='UseOption'})
+ singl_eltkn_test('Comparison', inv_op, {type=cmp_type, inv=true, ccs='UseOption'})
+ singl_eltkn_test('Comparison', op .. '#', {type=cmp_type, inv=false, ccs='MatchCase'})
+ singl_eltkn_test('Comparison', inv_op .. '#', {type=cmp_type, inv=true, ccs='MatchCase'})
+ singl_eltkn_test('Comparison', op .. '?', {type=cmp_type, inv=false, ccs='IgnoreCase'})
+ singl_eltkn_test('Comparison', inv_op .. '?', {type=cmp_type, inv=true, ccs='IgnoreCase'})
+ end
+ local function simple_test(pstate_arg, exp_type, exp_len, exp)
+ local pstate = new_pstate(pstate_arg)
+ exp = shallowcopy(exp)
+ exp.type = exp_type
+ exp.len = exp_len or #(pstate_arg[0])
+ exp.start = { col = 0, line = 0 }
+ eq(exp, next_eltkn(pstate, flags))
+ end
+ local function stable_tests()
+ singl_eltkn_test('Parenthesis', '(', {closing=false})
+ singl_eltkn_test('Parenthesis', ')', {closing=true})
+ singl_eltkn_test('Bracket', '[', {closing=false})
+ singl_eltkn_test('Bracket', ']', {closing=true})
+ singl_eltkn_test('FigureBrace', '{', {closing=false})
+ singl_eltkn_test('FigureBrace', '}', {closing=true})
+ singl_eltkn_test('Question', '?')
+ singl_eltkn_test('Colon', ':')
+ singl_eltkn_test('Dot', '.')
+ singl_eltkn_test('Assignment', '.=', {type='Concat'})
+ singl_eltkn_test('Plus', '+')
+ singl_eltkn_test('Assignment', '+=', {type='Add'})
+ singl_eltkn_test('Comma', ',')
+ singl_eltkn_test('Multiplication', '*', {type='Mul'})
+ singl_eltkn_test('Multiplication', '/', {type='Div'})
+ singl_eltkn_test('Multiplication', '%', {type='Mod'})
+ singl_eltkn_test('Spacing', ' \t\t \t\t')
+ singl_eltkn_test('Spacing', ' ')
+ singl_eltkn_test('Spacing', '\t')
+ singl_eltkn_test('Invalid', '\x01\x02\x03', {error='E15: Invalid control character present in input: %.*s'})
+ singl_eltkn_test('Number', '0123', {is_float=false, base=8, val=83})
+ singl_eltkn_test('Number', '01234567', {is_float=false, base=8, val=342391})
+ singl_eltkn_test('Number', '012345678', {is_float=false, base=10, val=12345678})
+ singl_eltkn_test('Number', '0x123', {is_float=false, base=16, val=291})
+ singl_eltkn_test('Number', '0x56FF', {is_float=false, base=16, val=22271})
+ singl_eltkn_test('Number', '0xabcdef', {is_float=false, base=16, val=11259375})
+ singl_eltkn_test('Number', '0xABCDEF', {is_float=false, base=16, val=11259375})
+ singl_eltkn_test('Number', '0x0', {is_float=false, base=16, val=0})
+ singl_eltkn_test('Number', '00', {is_float=false, base=8, val=0})
+ singl_eltkn_test('Number', '0b0', {is_float=false, base=2, val=0})
+ singl_eltkn_test('Number', '0b010111', {is_float=false, base=2, val=23})
+ singl_eltkn_test('Number', '0b100111', {is_float=false, base=2, val=39})
+ singl_eltkn_test('Number', '0', {is_float=false, base=10, val=0})
+ singl_eltkn_test('Number', '9', {is_float=false, base=10, val=9})
+ singl_eltkn_test('Env', '$abc')
+ singl_eltkn_test('Env', '$')
+ singl_eltkn_test('PlainIdentifier', 'test', {autoload=false, scope=0})
+ singl_eltkn_test('PlainIdentifier', '_test', {autoload=false, scope=0})
+ singl_eltkn_test('PlainIdentifier', '_test_foo', {autoload=false, scope=0})
+ singl_eltkn_test('PlainIdentifier', 't', {autoload=false, scope=0})
+ singl_eltkn_test('PlainIdentifier', 'test5', {autoload=false, scope=0})
+ singl_eltkn_test('PlainIdentifier', 't0', {autoload=false, scope=0})
+ singl_eltkn_test('PlainIdentifier', 'test#var', {autoload=true, scope=0})
+ singl_eltkn_test('PlainIdentifier', 'test#var#val###', {autoload=true, scope=0})
+ singl_eltkn_test('PlainIdentifier', 't#####', {autoload=true, scope=0})
+ singl_eltkn_test('And', '&&')
+ singl_eltkn_test('Or', '||')
+ singl_eltkn_test('Invalid', '&', {error='E112: Option name missing: %.*s'})
+ singl_eltkn_test('Option', '&opt', {scope='Unspecified', name='opt'})
+ singl_eltkn_test('Option', '&t_xx', {scope='Unspecified', name='t_xx'})
+ singl_eltkn_test('Option', '&t_\r\r', {scope='Unspecified', name='t_\r\r'})
+ singl_eltkn_test('Option', '&t_\t\t', {scope='Unspecified', name='t_\t\t'})
+ singl_eltkn_test('Option', '&t_ ', {scope='Unspecified', name='t_ '})
+ singl_eltkn_test('Option', '&g:opt', {scope='Global', name='opt'})
+ singl_eltkn_test('Option', '&l:opt', {scope='Local', name='opt'})
+ singl_eltkn_test('Invalid', '&l:', {error='E112: Option name missing: %.*s'})
+ singl_eltkn_test('Invalid', '&g:', {error='E112: Option name missing: %.*s'})
+ singl_eltkn_test('Register', '@', {name=-1})
+ singl_eltkn_test('Register', '@a', {name='a'})
+ singl_eltkn_test('Register', '@\r', {name=13})
+ singl_eltkn_test('Register', '@ ', {name=' '})
+ singl_eltkn_test('Register', '@\t', {name=9})
+ singl_eltkn_test('SingleQuotedString', '\'test', {closed=false})
+ singl_eltkn_test('SingleQuotedString', '\'test\'', {closed=true})
+ singl_eltkn_test('SingleQuotedString', '\'\'\'\'', {closed=true})
+ singl_eltkn_test('SingleQuotedString', '\'x\'\'\'', {closed=true})
+ singl_eltkn_test('SingleQuotedString', '\'\'\'x\'', {closed=true})
+ singl_eltkn_test('SingleQuotedString', '\'\'\'', {closed=false})
+ singl_eltkn_test('SingleQuotedString', '\'x\'\'', {closed=false})
+ singl_eltkn_test('SingleQuotedString', '\'\'\'x', {closed=false})
+ singl_eltkn_test('DoubleQuotedString', '"test', {closed=false})
+ singl_eltkn_test('DoubleQuotedString', '"test"', {closed=true})
+ singl_eltkn_test('DoubleQuotedString', '"\\""', {closed=true})
+ singl_eltkn_test('DoubleQuotedString', '"x\\""', {closed=true})
+ singl_eltkn_test('DoubleQuotedString', '"\\"x"', {closed=true})
+ singl_eltkn_test('DoubleQuotedString', '"\\"', {closed=false})
+ singl_eltkn_test('DoubleQuotedString', '"x\\"', {closed=false})
+ singl_eltkn_test('DoubleQuotedString', '"\\"x', {closed=false})
+ singl_eltkn_test('Not', '!')
+ singl_eltkn_test('Assignment', '=', {type='Plain'})
+ comparison_test('==', '!=', 'Equal')
+ comparison_test('=~', '!~', 'Matches')
+ comparison_test('>', '<=', 'Greater')
+ comparison_test('>=', '<', 'GreaterOrEqual')
+ singl_eltkn_test('Minus', '-')
+ singl_eltkn_test('Assignment', '-=', {type='Subtract'})
+ singl_eltkn_test('Arrow', '->')
+ singl_eltkn_test('Invalid', '~', {error='E15: Unidentified character: %.*s'})
+ simple_test({{data=nil, size=0}}, 'EOC', 0, {error='start.col >= #pstr'})
+ simple_test({''}, 'EOC', 0, {error='start.col >= #pstr'})
+ simple_test({'2.'}, 'Number', 1, {data={is_float=false, base=10, val=2}, str='2'})
+ simple_test({'2e5'}, 'Number', 1, {data={is_float=false, base=10, val=2}, str='2'})
+ simple_test({'2.x'}, 'Number', 1, {data={is_float=false, base=10, val=2}, str='2'})
+ simple_test({'2.2.'}, 'Number', 1, {data={is_float=false, base=10, val=2}, str='2'})
+ simple_test({'2.0x'}, 'Number', 1, {data={is_float=false, base=10, val=2}, str='2'})
+ simple_test({'2.0e'}, 'Number', 1, {data={is_float=false, base=10, val=2}, str='2'})
+ simple_test({'2.0e+'}, 'Number', 1, {data={is_float=false, base=10, val=2}, str='2'})
+ simple_test({'2.0e-'}, 'Number', 1, {data={is_float=false, base=10, val=2}, str='2'})
+ simple_test({'2.0e+x'}, 'Number', 1, {data={is_float=false, base=10, val=2}, str='2'})
+ simple_test({'2.0e-x'}, 'Number', 1, {data={is_float=false, base=10, val=2}, str='2'})
+ simple_test({'2.0e+1a'}, 'Number', 1, {data={is_float=false, base=10, val=2}, str='2'})
+ simple_test({'2.0e-1a'}, 'Number', 1, {data={is_float=false, base=10, val=2}, str='2'})
+ simple_test({'0b102'}, 'Number', 4, {data={is_float=false, base=2, val=2}, str='0b10'})
+ simple_test({'10F'}, 'Number', 2, {data={is_float=false, base=10, val=10}, str='10'})
+ simple_test({'0x0123456789ABCDEFG'}, 'Number', 18, {data={is_float=false, base=16, val=81985529216486895}, str='0x0123456789ABCDEF'})
+ simple_test({{data='00', size=2}}, 'Number', 2, {data={is_float=false, base=8, val=0}, str='00'})
+ simple_test({{data='009', size=2}}, 'Number', 2, {data={is_float=false, base=8, val=0}, str='00'})
+ simple_test({{data='01', size=1}}, 'Number', 1, {data={is_float=false, base=10, val=0}, str='0'})
+ end
+
+ local function regular_scope_tests()
+ scope_test('s')
+ scope_test('g')
+ scope_test('v')
+ scope_test('b')
+ scope_test('w')
+ scope_test('t')
+ scope_test('l')
+ scope_test('a')
+
+ simple_test({'g:'}, 'PlainIdentifier', 2, {data={scope='g', autoload=false}, str='g:'})
+ simple_test({'g:is#foo'}, 'PlainIdentifier', 8, {data={scope='g', autoload=true}, str='g:is#foo'})
+ simple_test({'g:isnot#foo'}, 'PlainIdentifier', 11, {data={scope='g', autoload=true}, str='g:isnot#foo'})
+ end
+
+ local function regular_is_tests()
+ comparison_test('is', 'isnot', 'Identical')
+
+ simple_test({'is'}, 'Comparison', 2, {data={type='Identical', inv=false, ccs='UseOption'}, str='is'})
+ simple_test({'isnot'}, 'Comparison', 5, {data={type='Identical', inv=true, ccs='UseOption'}, str='isnot'})
+ simple_test({'is?'}, 'Comparison', 3, {data={type='Identical', inv=false, ccs='IgnoreCase'}, str='is?'})
+ simple_test({'isnot?'}, 'Comparison', 6, {data={type='Identical', inv=true, ccs='IgnoreCase'}, str='isnot?'})
+ simple_test({'is#'}, 'Comparison', 3, {data={type='Identical', inv=false, ccs='MatchCase'}, str='is#'})
+ simple_test({'isnot#'}, 'Comparison', 6, {data={type='Identical', inv=true, ccs='MatchCase'}, str='isnot#'})
+ simple_test({'is#foo'}, 'Comparison', 3, {data={type='Identical', inv=false, ccs='MatchCase'}, str='is#'})
+ simple_test({'isnot#foo'}, 'Comparison', 6, {data={type='Identical', inv=true, ccs='MatchCase'}, str='isnot#'})
+ end
+
+ local function regular_number_tests()
+ simple_test({'2.0'}, 'Number', 1, {data={is_float=false, base=10, val=2}, str='2'})
+ simple_test({'2.0e5'}, 'Number', 1, {data={is_float=false, base=10, val=2}, str='2'})
+ simple_test({'2.0e+5'}, 'Number', 1, {data={is_float=false, base=10, val=2}, str='2'})
+ simple_test({'2.0e-5'}, 'Number', 1, {data={is_float=false, base=10, val=2}, str='2'})
+ end
+
+ local function regular_eoc_tests()
+ singl_eltkn_test('EOC', '|')
+ singl_eltkn_test('EOC', '\0')
+ singl_eltkn_test('EOC', '\n')
+ end
+
+ itp('works (single tokens, zero flags)', function()
+ stable_tests()
+
+ regular_eoc_tests()
+ regular_scope_tests()
+ regular_is_tests()
+ regular_number_tests()
+ end)
+ itp('peeks', function()
+ flags = tonumber(lib.kELFlagPeek)
+ should_advance = false
+ stable_tests()
+
+ regular_eoc_tests()
+ regular_scope_tests()
+ regular_is_tests()
+ regular_number_tests()
+ end)
+ itp('forbids scope', function()
+ flags = tonumber(lib.kELFlagForbidScope)
+ stable_tests()
+
+ regular_eoc_tests()
+ regular_is_tests()
+ regular_number_tests()
+
+ simple_test({'g:'}, 'PlainIdentifier', 1, {data={scope=0, autoload=false}, str='g'})
+ end)
+ itp('allows floats', function()
+ flags = tonumber(lib.kELFlagAllowFloat)
+ stable_tests()
+
+ regular_eoc_tests()
+ regular_scope_tests()
+ regular_is_tests()
+
+ simple_test({'2.2'}, 'Number', 3, {data={is_float=true, base=10, val=2.2}, str='2.2'})
+ simple_test({'2.0e5'}, 'Number', 5, {data={is_float=true, base=10, val=2e5}, str='2.0e5'})
+ simple_test({'2.0e+5'}, 'Number', 6, {data={is_float=true, base=10, val=2e5}, str='2.0e+5'})
+ simple_test({'2.0e-5'}, 'Number', 6, {data={is_float=true, base=10, val=2e-5}, str='2.0e-5'})
+ simple_test({'2.500000e-5'}, 'Number', 11, {data={is_float=true, base=10, val=2.5e-5}, str='2.500000e-5'})
+ simple_test({'2.5555e2'}, 'Number', 8, {data={is_float=true, base=10, val=2.5555e2}, str='2.5555e2'})
+ simple_test({'2.5555e+2'}, 'Number', 9, {data={is_float=true, base=10, val=2.5555e2}, str='2.5555e+2'})
+ simple_test({'2.5555e-2'}, 'Number', 9, {data={is_float=true, base=10, val=2.5555e-2}, str='2.5555e-2'})
+ simple_test({{data='2.5e-5', size=3}},
+ 'Number', 3, {data={is_float=true, base=10, val=2.5}, str='2.5'})
+ simple_test({{data='2.5e5', size=4}},
+ 'Number', 1, {data={is_float=false, base=10, val=2}, str='2'})
+ simple_test({{data='2.5e-50', size=6}},
+ 'Number', 6, {data={is_float=true, base=10, val=2.5e-5}, str='2.5e-5'})
+ end)
+ itp('treats `is` as an identifier', function()
+ flags = tonumber(lib.kELFlagIsNotCmp)
+ stable_tests()
+
+ regular_eoc_tests()
+ regular_scope_tests()
+ regular_number_tests()
+
+ simple_test({'is'}, 'PlainIdentifier', 2, {data={scope=0, autoload=false}, str='is'})
+ simple_test({'isnot'}, 'PlainIdentifier', 5, {data={scope=0, autoload=false}, str='isnot'})
+ simple_test({'is?'}, 'PlainIdentifier', 2, {data={scope=0, autoload=false}, str='is'})
+ simple_test({'isnot?'}, 'PlainIdentifier', 5, {data={scope=0, autoload=false}, str='isnot'})
+ simple_test({'is#'}, 'PlainIdentifier', 3, {data={scope=0, autoload=true}, str='is#'})
+ simple_test({'isnot#'}, 'PlainIdentifier', 6, {data={scope=0, autoload=true}, str='isnot#'})
+ simple_test({'is#foo'}, 'PlainIdentifier', 6, {data={scope=0, autoload=true}, str='is#foo'})
+ simple_test({'isnot#foo'}, 'PlainIdentifier', 9, {data={scope=0, autoload=true}, str='isnot#foo'})
+ end)
+ itp('forbids EOC', function()
+ flags = tonumber(lib.kELFlagForbidEOC)
+ stable_tests()
+
+ regular_scope_tests()
+ regular_is_tests()
+ regular_number_tests()
+
+ singl_eltkn_test('Invalid', '|', {error='E15: Unexpected EOC character: %.*s'})
+ singl_eltkn_test('Invalid', '\0', {error='E15: Unexpected EOC character: %.*s'})
+ singl_eltkn_test('Invalid', '\n', {error='E15: Unexpected EOC character: %.*s'})
+ end)
+end)
diff --git a/test/unit/viml/expressions/parser_spec.lua b/test/unit/viml/expressions/parser_spec.lua
new file mode 100644
index 0000000000..73388e5dd2
--- /dev/null
+++ b/test/unit/viml/expressions/parser_spec.lua
@@ -0,0 +1,540 @@
+local helpers = require('test.unit.helpers')(after_each)
+local global_helpers = require('test.helpers')
+local itp = helpers.gen_itp(it)
+local viml_helpers = require('test.unit.viml.helpers')
+
+local make_enum_conv_tab = helpers.make_enum_conv_tab
+local child_call_once = helpers.child_call_once
+local alloc_log_new = helpers.alloc_log_new
+local kvi_destroy = helpers.kvi_destroy
+local conv_enum = helpers.conv_enum
+local debug_log = helpers.debug_log
+local ptr2key = helpers.ptr2key
+local cimport = helpers.cimport
+local ffi = helpers.ffi
+local neq = helpers.neq
+local eq = helpers.eq
+
+local conv_ccs = viml_helpers.conv_ccs
+local new_pstate = viml_helpers.new_pstate
+local conv_cmp_type = viml_helpers.conv_cmp_type
+local pstate_set_str = viml_helpers.pstate_set_str
+local conv_expr_asgn_type = viml_helpers.conv_expr_asgn_type
+
+local mergedicts_copy = global_helpers.mergedicts_copy
+local format_string = global_helpers.format_string
+local format_luav = global_helpers.format_luav
+local intchar2lua = global_helpers.intchar2lua
+local dictdiff = global_helpers.dictdiff
+
+local lib = cimport('./src/nvim/viml/parser/expressions.h',
+ './src/nvim/syntax.h')
+
+local alloc_log = alloc_log_new()
+
+local predefined_hl_defs = {
+ -- From highlight_init_both
+ Conceal=true,
+ Cursor=true,
+ lCursor=true,
+ DiffText=true,
+ ErrorMsg=true,
+ IncSearch=true,
+ ModeMsg=true,
+ NonText=true,
+ PmenuSbar=true,
+ StatusLine=true,
+ StatusLineNC=true,
+ TabLineFill=true,
+ TabLineSel=true,
+ TermCursor=true,
+ VertSplit=true,
+ WildMenu=true,
+ EndOfBuffer=true,
+ QuickFixLine=true,
+ Substitute=true,
+ Whitespace=true,
+
+ -- From highlight_init_(dark|light)
+ ColorColumn=true,
+ CursorColumn=true,
+ CursorLine=true,
+ CursorLineNr=true,
+ DiffAdd=true,
+ DiffChange=true,
+ DiffDelete=true,
+ Directory=true,
+ FoldColumn=true,
+ Folded=true,
+ LineNr=true,
+ MatchParen=true,
+ MoreMsg=true,
+ Pmenu=true,
+ PmenuSel=true,
+ PmenuThumb=true,
+ Question=true,
+ Search=true,
+ SignColumn=true,
+ SpecialKey=true,
+ SpellBad=true,
+ SpellCap=true,
+ SpellLocal=true,
+ SpellRare=true,
+ TabLine=true,
+ Title=true,
+ Visual=true,
+ WarningMsg=true,
+ Normal=true,
+
+ -- From syncolor.vim, if &background
+ Comment=true,
+ Constant=true,
+ Special=true,
+ Identifier=true,
+ Statement=true,
+ PreProc=true,
+ Type=true,
+ Underlined=true,
+ Ignore=true,
+
+ -- From syncolor.vim, below if &background
+ Error=true,
+ Todo=true,
+
+ -- From syncolor.vim, links at the bottom
+ String=true,
+ Character=true,
+ Number=true,
+ Boolean=true,
+ Float=true,
+ Function=true,
+ Conditional=true,
+ Repeat=true,
+ Label=true,
+ Operator=true,
+ Keyword=true,
+ Exception=true,
+ Include=true,
+ Define=true,
+ Macro=true,
+ PreCondit=true,
+ StorageClass=true,
+ Structure=true,
+ Typedef=true,
+ Tag=true,
+ SpecialChar=true,
+ Delimiter=true,
+ SpecialComment=true,
+ Debug=true,
+}
+
+local nvim_hl_defs = {}
+
+child_call_once(function()
+ local i = 0
+ while lib.highlight_init_cmdline[i] ~= nil do
+ local hl_args = lib.highlight_init_cmdline[i]
+ local s = ffi.string(hl_args)
+ local err, msg = pcall(function()
+ if s:sub(1, 13) == 'default link ' then
+ local new_grp, grp_link = s:match('^default link (%w+) (%w+)$')
+ neq(nil, new_grp)
+ -- Note: group to link to must be already defined at the time of
+ -- linking, otherwise it will be created as cleared. So existence
+ -- of the group is checked here and not in the next pass over
+ -- nvim_hl_defs.
+ eq(true, not not (nvim_hl_defs[grp_link]
+ or predefined_hl_defs[grp_link]))
+ eq(false, not not (nvim_hl_defs[new_grp]
+ or predefined_hl_defs[new_grp]))
+ nvim_hl_defs[new_grp] = {'link', grp_link}
+ else
+ local new_grp, grp_args = s:match('^(%w+) (.*)')
+ neq(nil, new_grp)
+ eq(false, not not (nvim_hl_defs[new_grp]
+ or predefined_hl_defs[new_grp]))
+ nvim_hl_defs[new_grp] = {'definition', grp_args}
+ end
+ end)
+ if not err then
+ msg = format_string(
+ 'Error while processing string %s at position %u:\n%s', s, i, msg)
+ error(msg)
+ end
+ i = i + 1
+ end
+ for k, _ in ipairs(nvim_hl_defs) do
+ eq('Nvim', k:sub(1, 4))
+ -- NvimInvalid
+ -- 12345678901
+ local err, msg = pcall(function()
+ if k:sub(5, 11) == 'Invalid' then
+ neq(nil, nvim_hl_defs['Nvim' .. k:sub(12)])
+ else
+ neq(nil, nvim_hl_defs['NvimInvalid' .. k:sub(5)])
+ end
+ end)
+ if not err then
+ msg = format_string('Error while processing group %s:\n%s', k, msg)
+ error(msg)
+ end
+ end
+end)
+
+local function hls_to_hl_fs(hls)
+ local ret = {}
+ local next_col = 0
+ for i, v in ipairs(hls) do
+ local group, line, col, str = v:match('^Nvim([a-zA-Z]+):(%d+):(%d+):(.*)$')
+ col = tonumber(col)
+ line = tonumber(line)
+ assert(line == 0)
+ local col_shift = col - next_col
+ assert(col_shift >= 0)
+ next_col = col + #str
+ ret[i] = format_string('hl(%r, %r%s)',
+ group,
+ str,
+ (col_shift == 0
+ and ''
+ or (', %u'):format(col_shift)))
+ end
+ return ret
+end
+
+local function format_check(expr, format_check_data, opts)
+ -- That forces specific order.
+ local zflags = opts.flags[1]
+ local zdata = format_check_data[zflags]
+ local dig_len
+ if opts.funcname then
+ print(format_string('\n%s(%r, {', opts.funcname, expr))
+ dig_len = #opts.funcname + 2
+ else
+ print(format_string('\n_check_parsing(%r, %r, {', opts, expr))
+ dig_len = #('_check_parsing(, \'') + #(format_string('%r', opts))
+ end
+ local digits = ' --' .. (' '):rep(dig_len - #(' --'))
+ local digits2 = digits:sub(1, -10)
+ for i = 0, #expr - 1 do
+ if i % 10 == 0 then
+ digits2 = ('%s%10u'):format(digits2, i / 10)
+ end
+ digits = ('%s%u'):format(digits, i % 10)
+ end
+ print(digits)
+ if #expr > 10 then
+ print(digits2)
+ end
+ if zdata.ast.len then
+ print((' len = %u,'):format(zdata.ast.len))
+ end
+ print(' ast = ' .. format_luav(zdata.ast.ast, ' ') .. ',')
+ if zdata.ast.err then
+ print(' err = {')
+ print(' arg = ' .. format_luav(zdata.ast.err.arg) .. ',')
+ print(' msg = ' .. format_luav(zdata.ast.err.msg) .. ',')
+ print(' },')
+ end
+ print('}, {')
+ for _, v in ipairs(zdata.hl_fs) do
+ print(' ' .. v .. ',')
+ end
+ local diffs = {}
+ local diffs_num = 0
+ for flags, v in pairs(format_check_data) do
+ if flags ~= zflags then
+ diffs[flags] = dictdiff(zdata, v)
+ if diffs[flags] then
+ if flags == 3 + zflags then
+ if (dictdiff(format_check_data[1 + zflags],
+ format_check_data[3 + zflags]) == nil
+ or dictdiff(format_check_data[2 + zflags],
+ format_check_data[3 + zflags]) == nil)
+ then
+ diffs[flags] = nil
+ else
+ diffs_num = diffs_num + 1
+ end
+ else
+ diffs_num = diffs_num + 1
+ end
+ end
+ end
+ end
+ if diffs_num ~= 0 then
+ print('}, {')
+ local flags = 1
+ while diffs_num ~= 0 do
+ if diffs[flags] then
+ diffs_num = diffs_num - 1
+ local diff = diffs[flags]
+ print((' [%u] = {'):format(flags))
+ if diff.ast then
+ print(' ast = ' .. format_luav(diff.ast, ' ') .. ',')
+ end
+ if diff.hl_fs then
+ print(' hl_fs = ' .. format_luav(diff.hl_fs, ' ', {
+ literal_strings=true
+ }) .. ',')
+ end
+ print(' },')
+ end
+ flags = flags + 1
+ end
+ end
+ print('})')
+end
+
+local east_node_type_tab
+make_enum_conv_tab(lib, {
+ 'kExprNodeMissing',
+ 'kExprNodeOpMissing',
+ 'kExprNodeTernary',
+ 'kExprNodeTernaryValue',
+ 'kExprNodeRegister',
+ 'kExprNodeSubscript',
+ 'kExprNodeListLiteral',
+ 'kExprNodeUnaryPlus',
+ 'kExprNodeBinaryPlus',
+ 'kExprNodeNested',
+ 'kExprNodeCall',
+ 'kExprNodePlainIdentifier',
+ 'kExprNodePlainKey',
+ 'kExprNodeComplexIdentifier',
+ 'kExprNodeUnknownFigure',
+ 'kExprNodeLambda',
+ 'kExprNodeDictLiteral',
+ 'kExprNodeCurlyBracesIdentifier',
+ 'kExprNodeComma',
+ 'kExprNodeColon',
+ 'kExprNodeArrow',
+ 'kExprNodeComparison',
+ 'kExprNodeConcat',
+ 'kExprNodeConcatOrSubscript',
+ 'kExprNodeInteger',
+ 'kExprNodeFloat',
+ 'kExprNodeSingleQuotedString',
+ 'kExprNodeDoubleQuotedString',
+ 'kExprNodeOr',
+ 'kExprNodeAnd',
+ 'kExprNodeUnaryMinus',
+ 'kExprNodeBinaryMinus',
+ 'kExprNodeNot',
+ 'kExprNodeMultiplication',
+ 'kExprNodeDivision',
+ 'kExprNodeMod',
+ 'kExprNodeOption',
+ 'kExprNodeEnvironment',
+ 'kExprNodeAssignment',
+}, 'kExprNode', function(ret) east_node_type_tab = ret end)
+
+local function conv_east_node_type(typ)
+ return conv_enum(east_node_type_tab, typ)
+end
+
+local eastnodelist2lua
+
+local function eastnode2lua(pstate, eastnode, checked_nodes)
+ local key = ptr2key(eastnode)
+ if checked_nodes[key] then
+ checked_nodes[key].duplicate_key = key
+ return { duplicate = key }
+ end
+ local typ = conv_east_node_type(eastnode.type)
+ local ret = {}
+ checked_nodes[key] = ret
+ ret.children = eastnodelist2lua(pstate, eastnode.children, checked_nodes)
+ local str = pstate_set_str(pstate, eastnode.start, eastnode.len)
+ local ret_str
+ if str.error then
+ ret_str = 'error:' .. str.error
+ else
+ ret_str = ('%u:%u:%s'):format(str.start.line, str.start.col, str.str)
+ end
+ if typ == 'Register' then
+ typ = typ .. ('(name=%s)'):format(
+ tostring(intchar2lua(eastnode.data.reg.name)))
+ elseif typ == 'PlainIdentifier' then
+ typ = typ .. ('(scope=%s,ident=%s)'):format(
+ tostring(intchar2lua(eastnode.data.var.scope)),
+ ffi.string(eastnode.data.var.ident, eastnode.data.var.ident_len))
+ elseif typ == 'PlainKey' then
+ typ = typ .. ('(key=%s)'):format(
+ ffi.string(eastnode.data.var.ident, eastnode.data.var.ident_len))
+ elseif (typ == 'UnknownFigure' or typ == 'DictLiteral'
+ or typ == 'CurlyBracesIdentifier' or typ == 'Lambda') then
+ typ = typ .. ('(%s)'):format(
+ (eastnode.data.fig.type_guesses.allow_lambda and '\\' or '-')
+ .. (eastnode.data.fig.type_guesses.allow_dict and 'd' or '-')
+ .. (eastnode.data.fig.type_guesses.allow_ident and 'i' or '-'))
+ elseif typ == 'Comparison' then
+ typ = typ .. ('(type=%s,inv=%u,ccs=%s)'):format(
+ conv_cmp_type(eastnode.data.cmp.type), eastnode.data.cmp.inv and 1 or 0,
+ conv_ccs(eastnode.data.cmp.ccs))
+ elseif typ == 'Integer' then
+ typ = typ .. ('(val=%u)'):format(tonumber(eastnode.data.num.value))
+ elseif typ == 'Float' then
+ typ = typ .. format_string('(val=%e)', tonumber(eastnode.data.flt.value))
+ elseif typ == 'SingleQuotedString' or typ == 'DoubleQuotedString' then
+ if eastnode.data.str.value == nil then
+ typ = typ .. '(val=NULL)'
+ else
+ local s = ffi.string(eastnode.data.str.value, eastnode.data.str.size)
+ typ = format_string('%s(val=%q)', typ, s)
+ end
+ elseif typ == 'Option' then
+ typ = ('%s(scope=%s,ident=%s)'):format(
+ typ,
+ tostring(intchar2lua(eastnode.data.opt.scope)),
+ ffi.string(eastnode.data.opt.ident, eastnode.data.opt.ident_len))
+ elseif typ == 'Environment' then
+ typ = ('%s(ident=%s)'):format(
+ typ,
+ ffi.string(eastnode.data.env.ident, eastnode.data.env.ident_len))
+ elseif typ == 'Assignment' then
+ typ = ('%s(%s)'):format(typ, conv_expr_asgn_type(eastnode.data.ass.type))
+ end
+ ret_str = typ .. ':' .. ret_str
+ local can_simplify = not ret.children
+ if can_simplify then
+ ret = ret_str
+ else
+ ret[1] = ret_str
+ end
+ return ret
+end
+
+eastnodelist2lua = function(pstate, eastnode, checked_nodes)
+ local ret = {}
+ while eastnode ~= nil do
+ ret[#ret + 1] = eastnode2lua(pstate, eastnode, checked_nodes)
+ eastnode = eastnode.next
+ end
+ if #ret == 0 then
+ ret = nil
+ end
+ return ret
+end
+
+local function east2lua(str, pstate, east)
+ local checked_nodes = {}
+ local len = tonumber(pstate.pos.col)
+ if pstate.pos.line == 1 then
+ len = tonumber(pstate.reader.lines.items[0].size)
+ end
+ if type(str) == 'string' and len == #str then
+ len = nil
+ end
+ return {
+ err = east.err.msg ~= nil and {
+ msg = ffi.string(east.err.msg),
+ arg = ffi.string(east.err.arg, east.err.arg_len),
+ } or nil,
+ len = len,
+ ast = eastnodelist2lua(pstate, east.root, checked_nodes),
+ }
+end
+
+local function phl2lua(pstate)
+ local ret = {}
+ for i = 0, (tonumber(pstate.colors.size) - 1) do
+ local chunk = pstate.colors.items[i]
+ local chunk_tbl = pstate_set_str(
+ pstate, chunk.start, chunk.end_col - chunk.start.col, {
+ group = ffi.string(chunk.group),
+ })
+ ret[i + 1] = ('%s:%u:%u:%s'):format(
+ chunk_tbl.group,
+ chunk_tbl.start.line,
+ chunk_tbl.start.col,
+ chunk_tbl.str)
+ end
+ return ret
+end
+
+child_call_once(function()
+ assert:set_parameter('TableFormatLevel', 1000000)
+end)
+
+describe('Expressions parser', function()
+ local function _check_parsing(opts, str, exp_ast, exp_highlighting_fs,
+ nz_flags_exps)
+ local zflags = opts.flags[1]
+ nz_flags_exps = nz_flags_exps or {}
+ local format_check_data = {}
+ for _, flags in ipairs(opts.flags) do
+ debug_log(('Running test case (%s, %u)'):format(str, flags))
+ local err, msg = pcall(function()
+ if os.getenv('NVIM_TEST_PARSER_SPEC_PRINT_TEST_CASE') == '1' then
+ print(str, flags)
+ end
+ alloc_log:check({})
+
+ local pstate = new_pstate({str})
+ local east = lib.viml_pexpr_parse(pstate, flags)
+ local ast = east2lua(str, pstate, east)
+ local hls = phl2lua(pstate)
+ if exp_ast == nil then
+ format_check_data[flags] = {ast=ast, hl_fs=hls_to_hl_fs(hls)}
+ else
+ local exps = {
+ ast = exp_ast,
+ hl_fs = exp_highlighting_fs,
+ }
+ local add_exps = nz_flags_exps[flags]
+ if not add_exps and flags == 3 + zflags then
+ add_exps = nz_flags_exps[1 + zflags] or nz_flags_exps[2 + zflags]
+ end
+ if add_exps then
+ if add_exps.ast then
+ exps.ast = mergedicts_copy(exps.ast, add_exps.ast)
+ end
+ if add_exps.hl_fs then
+ exps.hl_fs = mergedicts_copy(exps.hl_fs, add_exps.hl_fs)
+ end
+ end
+ eq(exps.ast, ast)
+ if exp_highlighting_fs then
+ local exp_highlighting = {}
+ local next_col = 0
+ for i, h in ipairs(exps.hl_fs) do
+ exp_highlighting[i], next_col = h(next_col)
+ end
+ eq(exp_highlighting, hls)
+ end
+ end
+ lib.viml_pexpr_free_ast(east)
+ kvi_destroy(pstate.colors)
+ alloc_log:clear_tmp_allocs(true)
+ alloc_log:check({})
+ end)
+ if not err then
+ msg = format_string('Error while processing test (%r, %u):\n%s',
+ str, flags, msg)
+ error(msg)
+ end
+ end
+ if exp_ast == nil then
+ format_check(str, format_check_data, opts)
+ end
+ end
+ local function hl(group, str, shift)
+ return function(next_col)
+ if nvim_hl_defs['Nvim' .. group] == nil then
+ error(('Unknown group: Nvim%s'):format(group))
+ end
+ local col = next_col + (shift or 0)
+ return (('%s:%u:%u:%s'):format(
+ 'Nvim' .. group,
+ 0,
+ col,
+ str)), (col + #str)
+ end
+ end
+ local function fmtn(typ, args, rest)
+ return ('%s(%s)%s'):format(typ, args, rest)
+ end
+ require('test.unit.viml.expressions.parser_tests')(
+ itp, _check_parsing, hl, fmtn)
+end)
diff --git a/test/unit/viml/expressions/parser_tests.lua b/test/unit/viml/expressions/parser_tests.lua
new file mode 100644
index 0000000000..da61672bb1
--- /dev/null
+++ b/test/unit/viml/expressions/parser_tests.lua
@@ -0,0 +1,8317 @@
+local global_helpers = require('test.helpers')
+
+local REMOVE_THIS = global_helpers.REMOVE_THIS
+
+return function(itp, _check_parsing, hl, fmtn)
+ local function check_parsing(...)
+ return _check_parsing({flags={0, 1, 2, 3}, funcname='check_parsing'}, ...)
+ end
+ local function check_asgn_parsing(...)
+ return _check_parsing({
+ flags={4, 5, 6, 7},
+ funcname='check_asgn_parsing',
+ }, ...)
+ end
+ itp('works with + and @a', function()
+ check_parsing('@a', {
+ ast = {
+ 'Register(name=a):0:0:@a',
+ },
+ }, {
+ hl('Register', '@a'),
+ })
+ check_parsing('+@a', {
+ ast = {
+ {
+ 'UnaryPlus:0:0:+',
+ children = {
+ 'Register(name=a):0:1:@a',
+ },
+ },
+ },
+ }, {
+ hl('UnaryPlus', '+'),
+ hl('Register', '@a'),
+ })
+ check_parsing('@a+@b', {
+ ast = {
+ {
+ 'BinaryPlus:0:2:+',
+ children = {
+ 'Register(name=a):0:0:@a',
+ 'Register(name=b):0:3:@b',
+ },
+ },
+ },
+ }, {
+ hl('Register', '@a'),
+ hl('BinaryPlus', '+'),
+ hl('Register', '@b'),
+ })
+ check_parsing('@a+@b+@c', {
+ ast = {
+ {
+ 'BinaryPlus:0:5:+',
+ children = {
+ {
+ 'BinaryPlus:0:2:+',
+ children = {
+ 'Register(name=a):0:0:@a',
+ 'Register(name=b):0:3:@b',
+ },
+ },
+ 'Register(name=c):0:6:@c',
+ },
+ },
+ },
+ }, {
+ hl('Register', '@a'),
+ hl('BinaryPlus', '+'),
+ hl('Register', '@b'),
+ hl('BinaryPlus', '+'),
+ hl('Register', '@c'),
+ })
+ check_parsing('+@a+@b', {
+ ast = {
+ {
+ 'BinaryPlus:0:3:+',
+ children = {
+ {
+ 'UnaryPlus:0:0:+',
+ children = {
+ 'Register(name=a):0:1:@a',
+ },
+ },
+ 'Register(name=b):0:4:@b',
+ },
+ },
+ },
+ }, {
+ hl('UnaryPlus', '+'),
+ hl('Register', '@a'),
+ hl('BinaryPlus', '+'),
+ hl('Register', '@b'),
+ })
+ check_parsing('+@a++@b', {
+ ast = {
+ {
+ 'BinaryPlus:0:3:+',
+ children = {
+ {
+ 'UnaryPlus:0:0:+',
+ children = {
+ 'Register(name=a):0:1:@a',
+ },
+ },
+ {
+ 'UnaryPlus:0:4:+',
+ children = {
+ 'Register(name=b):0:5:@b',
+ },
+ },
+ },
+ },
+ },
+ }, {
+ hl('UnaryPlus', '+'),
+ hl('Register', '@a'),
+ hl('BinaryPlus', '+'),
+ hl('UnaryPlus', '+'),
+ hl('Register', '@b'),
+ })
+ check_parsing('@a@b', {
+ ast = {
+ {
+ 'OpMissing:0:2:',
+ children = {
+ 'Register(name=a):0:0:@a',
+ 'Register(name=b):0:2:@b',
+ },
+ },
+ },
+ err = {
+ arg = '@b',
+ msg = 'E15: Missing operator: %.*s',
+ },
+ }, {
+ hl('Register', '@a'),
+ hl('InvalidRegister', '@b'),
+ }, {
+ [1] = {
+ ast = {
+ len = 2,
+ err = REMOVE_THIS,
+ ast = {
+ 'Register(name=a):0:0:@a'
+ },
+ },
+ hl_fs = {
+ [2] = REMOVE_THIS,
+ },
+ },
+ })
+ check_parsing(' @a \t @b', {
+ ast = {
+ {
+ 'OpMissing:0:3:',
+ children = {
+ 'Register(name=a):0:0: @a',
+ 'Register(name=b):0:3: \t @b',
+ },
+ },
+ },
+ err = {
+ arg = '@b',
+ msg = 'E15: Missing operator: %.*s',
+ },
+ }, {
+ hl('Register', '@a', 1),
+ hl('InvalidSpacing', ' \t '),
+ hl('Register', '@b'),
+ }, {
+ [1] = {
+ ast = {
+ len = 6,
+ err = REMOVE_THIS,
+ ast = {
+ 'Register(name=a):0:0: @a'
+ },
+ },
+ hl_fs = {
+ [2] = REMOVE_THIS,
+ [3] = REMOVE_THIS,
+ },
+ },
+ })
+ check_parsing('+', {
+ ast = {
+ 'UnaryPlus:0:0:+',
+ },
+ err = {
+ arg = '',
+ msg = 'E15: Expected value, got EOC: %.*s',
+ },
+ }, {
+ hl('UnaryPlus', '+'),
+ })
+ check_parsing(' +', {
+ ast = {
+ 'UnaryPlus:0:0: +',
+ },
+ err = {
+ arg = '',
+ msg = 'E15: Expected value, got EOC: %.*s',
+ },
+ }, {
+ hl('UnaryPlus', '+', 1),
+ })
+ check_parsing('@a+ ', {
+ ast = {
+ {
+ 'BinaryPlus:0:2:+',
+ children = {
+ 'Register(name=a):0:0:@a',
+ },
+ },
+ },
+ err = {
+ arg = '',
+ msg = 'E15: Expected value, got EOC: %.*s',
+ },
+ }, {
+ hl('Register', '@a'),
+ hl('BinaryPlus', '+'),
+ })
+ end)
+ itp('works with @a, + and parenthesis', function()
+ check_parsing('(@a)', {
+ ast = {
+ {
+ 'Nested:0:0:(',
+ children = {
+ 'Register(name=a):0:1:@a',
+ },
+ },
+ },
+ }, {
+ hl('NestingParenthesis', '('),
+ hl('Register', '@a'),
+ hl('NestingParenthesis', ')'),
+ })
+ check_parsing('()', {
+ ast = {
+ {
+ 'Nested:0:0:(',
+ children = {
+ 'Missing:0:1:',
+ },
+ },
+ },
+ err = {
+ arg = ')',
+ msg = 'E15: Expected value, got parenthesis: %.*s',
+ },
+ }, {
+ hl('NestingParenthesis', '('),
+ hl('InvalidNestingParenthesis', ')'),
+ })
+ check_parsing(')', {
+ ast = {
+ {
+ 'Nested:0:0:',
+ children = {
+ 'Missing:0:0:',
+ },
+ },
+ },
+ err = {
+ arg = ')',
+ msg = 'E15: Expected value, got parenthesis: %.*s',
+ },
+ }, {
+ hl('InvalidNestingParenthesis', ')'),
+ })
+ check_parsing('+)', {
+ ast = {
+ {
+ 'Nested:0:1:',
+ children = {
+ {
+ 'UnaryPlus:0:0:+',
+ children = {
+ 'Missing:0:1:',
+ },
+ },
+ },
+ },
+ },
+ err = {
+ arg = ')',
+ msg = 'E15: Expected value, got parenthesis: %.*s',
+ },
+ }, {
+ hl('UnaryPlus', '+'),
+ hl('InvalidNestingParenthesis', ')'),
+ })
+ check_parsing('+@a(@b)', {
+ ast = {
+ {
+ 'UnaryPlus:0:0:+',
+ children = {
+ {
+ 'Call:0:3:(',
+ children = {
+ 'Register(name=a):0:1:@a',
+ 'Register(name=b):0:4:@b',
+ },
+ },
+ },
+ },
+ },
+ }, {
+ hl('UnaryPlus', '+'),
+ hl('Register', '@a'),
+ hl('CallingParenthesis', '('),
+ hl('Register', '@b'),
+ hl('CallingParenthesis', ')'),
+ })
+ check_parsing('@a+@b(@c)', {
+ ast = {
+ {
+ 'BinaryPlus:0:2:+',
+ children = {
+ 'Register(name=a):0:0:@a',
+ {
+ 'Call:0:5:(',
+ children = {
+ 'Register(name=b):0:3:@b',
+ 'Register(name=c):0:6:@c',
+ },
+ },
+ },
+ },
+ },
+ }, {
+ hl('Register', '@a'),
+ hl('BinaryPlus', '+'),
+ hl('Register', '@b'),
+ hl('CallingParenthesis', '('),
+ hl('Register', '@c'),
+ hl('CallingParenthesis', ')'),
+ })
+ check_parsing('@a()', {
+ ast = {
+ {
+ 'Call:0:2:(',
+ children = {
+ 'Register(name=a):0:0:@a',
+ },
+ },
+ },
+ }, {
+ hl('Register', '@a'),
+ hl('CallingParenthesis', '('),
+ hl('CallingParenthesis', ')'),
+ })
+ check_parsing('@a ()', {
+ ast = {
+ {
+ 'OpMissing:0:2:',
+ children = {
+ 'Register(name=a):0:0:@a',
+ {
+ 'Nested:0:2: (',
+ children = {
+ 'Missing:0:4:',
+ },
+ },
+ },
+ },
+ },
+ err = {
+ arg = '()',
+ msg = 'E15: Missing operator: %.*s',
+ },
+ }, {
+ hl('Register', '@a'),
+ hl('InvalidSpacing', ' '),
+ hl('NestingParenthesis', '('),
+ hl('InvalidNestingParenthesis', ')'),
+ }, {
+ [1] = {
+ ast = {
+ len = 3,
+ err = REMOVE_THIS,
+ ast = {
+ 'Register(name=a):0:0:@a',
+ },
+ },
+ hl_fs = {
+ [2] = REMOVE_THIS,
+ [3] = REMOVE_THIS,
+ [4] = REMOVE_THIS,
+ },
+ },
+ })
+ check_parsing('@a + (@b)', {
+ ast = {
+ {
+ 'BinaryPlus:0:2: +',
+ children = {
+ 'Register(name=a):0:0:@a',
+ {
+ 'Nested:0:4: (',
+ children = {
+ 'Register(name=b):0:6:@b',
+ },
+ },
+ },
+ },
+ },
+ }, {
+ hl('Register', '@a'),
+ hl('BinaryPlus', '+', 1),
+ hl('NestingParenthesis', '(', 1),
+ hl('Register', '@b'),
+ hl('NestingParenthesis', ')'),
+ })
+ check_parsing('@a + (+@b)', {
+ ast = {
+ {
+ 'BinaryPlus:0:2: +',
+ children = {
+ 'Register(name=a):0:0:@a',
+ {
+ 'Nested:0:4: (',
+ children = {
+ {
+ 'UnaryPlus:0:6:+',
+ children = {
+ 'Register(name=b):0:7:@b',
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ }, {
+ hl('Register', '@a'),
+ hl('BinaryPlus', '+', 1),
+ hl('NestingParenthesis', '(', 1),
+ hl('UnaryPlus', '+'),
+ hl('Register', '@b'),
+ hl('NestingParenthesis', ')'),
+ })
+ check_parsing('@a + (@b + @c)', {
+ ast = {
+ {
+ 'BinaryPlus:0:2: +',
+ children = {
+ 'Register(name=a):0:0:@a',
+ {
+ 'Nested:0:4: (',
+ children = {
+ {
+ 'BinaryPlus:0:8: +',
+ children = {
+ 'Register(name=b):0:6:@b',
+ 'Register(name=c):0:10: @c',
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ }, {
+ hl('Register', '@a'),
+ hl('BinaryPlus', '+', 1),
+ hl('NestingParenthesis', '(', 1),
+ hl('Register', '@b'),
+ hl('BinaryPlus', '+', 1),
+ hl('Register', '@c', 1),
+ hl('NestingParenthesis', ')'),
+ })
+ check_parsing('(@a)+@b', {
+ ast = {
+ {
+ 'BinaryPlus:0:4:+',
+ children = {
+ {
+ 'Nested:0:0:(',
+ children = {
+ 'Register(name=a):0:1:@a',
+ },
+ },
+ 'Register(name=b):0:5:@b',
+ },
+ },
+ },
+ }, {
+ hl('NestingParenthesis', '('),
+ hl('Register', '@a'),
+ hl('NestingParenthesis', ')'),
+ hl('BinaryPlus', '+'),
+ hl('Register', '@b'),
+ })
+ check_parsing('@a+(@b)(@c)', {
+ -- 01234567890
+ ast = {
+ {
+ 'BinaryPlus:0:2:+',
+ children = {
+ 'Register(name=a):0:0:@a',
+ {
+ 'Call:0:7:(',
+ children = {
+ {
+ 'Nested:0:3:(',
+ children = { 'Register(name=b):0:4:@b' },
+ },
+ 'Register(name=c):0:8:@c',
+ },
+ },
+ },
+ },
+ },
+ }, {
+ hl('Register', '@a'),
+ hl('BinaryPlus', '+'),
+ hl('NestingParenthesis', '('),
+ hl('Register', '@b'),
+ hl('NestingParenthesis', ')'),
+ hl('CallingParenthesis', '('),
+ hl('Register', '@c'),
+ hl('CallingParenthesis', ')'),
+ })
+ check_parsing('@a+((@b))(@c)', {
+ -- 01234567890123456890123456789
+ -- 0 1 2
+ ast = {
+ {
+ 'BinaryPlus:0:2:+',
+ children = {
+ 'Register(name=a):0:0:@a',
+ {
+ 'Call:0:9:(',
+ children = {
+ {
+ 'Nested:0:3:(',
+ children = {
+ {
+ 'Nested:0:4:(',
+ children = { 'Register(name=b):0:5:@b' }
+ },
+ },
+ },
+ 'Register(name=c):0:10:@c',
+ },
+ },
+ },
+ },
+ },
+ }, {
+ hl('Register', '@a'),
+ hl('BinaryPlus', '+'),
+ hl('NestingParenthesis', '('),
+ hl('NestingParenthesis', '('),
+ hl('Register', '@b'),
+ hl('NestingParenthesis', ')'),
+ hl('NestingParenthesis', ')'),
+ hl('CallingParenthesis', '('),
+ hl('Register', '@c'),
+ hl('CallingParenthesis', ')'),
+ })
+ check_parsing('@a+((@b))+@c', {
+ -- 01234567890123456890123456789
+ -- 0 1 2
+ ast = {
+ {
+ 'BinaryPlus:0:9:+',
+ children = {
+ {
+ 'BinaryPlus:0:2:+',
+ children = {
+ 'Register(name=a):0:0:@a',
+ {
+ 'Nested:0:3:(',
+ children = {
+ {
+ 'Nested:0:4:(',
+ children = { 'Register(name=b):0:5:@b' }
+ },
+ },
+ },
+ },
+ },
+ 'Register(name=c):0:10:@c',
+ },
+ },
+ },
+ }, {
+ hl('Register', '@a'),
+ hl('BinaryPlus', '+'),
+ hl('NestingParenthesis', '('),
+ hl('NestingParenthesis', '('),
+ hl('Register', '@b'),
+ hl('NestingParenthesis', ')'),
+ hl('NestingParenthesis', ')'),
+ hl('BinaryPlus', '+'),
+ hl('Register', '@c'),
+ })
+ check_parsing(
+ '@a + (@b + @c) + @d(@e) + (+@f) + ((+@g(@h))(@j)(@k))(@l)', {--[[
+ | | | | | | | | || | | || | | ||| || || || ||
+ 000000000011111111112222222222333333333344444444445555555
+ 012345678901234567890123456789012345678901234567890123456
+ ]]
+ ast = {{
+ 'BinaryPlus:0:31: +',
+ children = {
+ {
+ 'BinaryPlus:0:23: +',
+ children = {
+ {
+ 'BinaryPlus:0:14: +',
+ children = {
+ {
+ 'BinaryPlus:0:2: +',
+ children = {
+ 'Register(name=a):0:0:@a',
+ {
+ 'Nested:0:4: (',
+ children = {
+ {
+ 'BinaryPlus:0:8: +',
+ children = {
+ 'Register(name=b):0:6:@b',
+ 'Register(name=c):0:10: @c',
+ },
+ },
+ },
+ },
+ },
+ },
+ {
+ 'Call:0:19:(',
+ children = {
+ 'Register(name=d):0:16: @d',
+ 'Register(name=e):0:20:@e',
+ },
+ },
+ },
+ },
+ {
+ 'Nested:0:25: (',
+ children = {
+ {
+ 'UnaryPlus:0:27:+',
+ children = {
+ 'Register(name=f):0:28:@f',
+ },
+ },
+ },
+ },
+ },
+ },
+ {
+ 'Call:0:53:(',
+ children = {
+ {
+ 'Nested:0:33: (',
+ children = {
+ {
+ 'Call:0:48:(',
+ children = {
+ {
+ 'Call:0:44:(',
+ children = {
+ {
+ 'Nested:0:35:(',
+ children = {
+ {
+ 'UnaryPlus:0:36:+',
+ children = {
+ {
+ 'Call:0:39:(',
+ children = {
+ 'Register(name=g):0:37:@g',
+ 'Register(name=h):0:40:@h',
+ },
+ },
+ },
+ },
+ },
+ },
+ 'Register(name=j):0:45:@j',
+ },
+ },
+ 'Register(name=k):0:49:@k',
+ },
+ },
+ },
+ },
+ 'Register(name=l):0:54:@l',
+ },
+ },
+ },
+ }},
+ }, {
+ hl('Register', '@a'),
+ hl('BinaryPlus', '+', 1),
+ hl('NestingParenthesis', '(', 1),
+ hl('Register', '@b'),
+ hl('BinaryPlus', '+', 1),
+ hl('Register', '@c', 1),
+ hl('NestingParenthesis', ')'),
+ hl('BinaryPlus', '+', 1),
+ hl('Register', '@d', 1),
+ hl('CallingParenthesis', '('),
+ hl('Register', '@e'),
+ hl('CallingParenthesis', ')'),
+ hl('BinaryPlus', '+', 1),
+ hl('NestingParenthesis', '(', 1),
+ hl('UnaryPlus', '+'),
+ hl('Register', '@f'),
+ hl('NestingParenthesis', ')'),
+ hl('BinaryPlus', '+', 1),
+ hl('NestingParenthesis', '(', 1),
+ hl('NestingParenthesis', '('),
+ hl('UnaryPlus', '+'),
+ hl('Register', '@g'),
+ hl('CallingParenthesis', '('),
+ hl('Register', '@h'),
+ hl('CallingParenthesis', ')'),
+ hl('NestingParenthesis', ')'),
+ hl('CallingParenthesis', '('),
+ hl('Register', '@j'),
+ hl('CallingParenthesis', ')'),
+ hl('CallingParenthesis', '('),
+ hl('Register', '@k'),
+ hl('CallingParenthesis', ')'),
+ hl('NestingParenthesis', ')'),
+ hl('CallingParenthesis', '('),
+ hl('Register', '@l'),
+ hl('CallingParenthesis', ')'),
+ })
+ check_parsing('@a)', {
+ -- 012
+ ast = {
+ {
+ 'Nested:0:2:',
+ children = {
+ 'Register(name=a):0:0:@a',
+ },
+ },
+ },
+ err = {
+ arg = ')',
+ msg = 'E15: Unexpected closing parenthesis: %.*s',
+ },
+ }, {
+ hl('Register', '@a'),
+ hl('InvalidNestingParenthesis', ')'),
+ })
+ check_parsing('(@a', {
+ -- 012
+ ast = {
+ {
+ 'Nested:0:0:(',
+ children = {
+ 'Register(name=a):0:1:@a',
+ },
+ },
+ },
+ err = {
+ arg = '(@a',
+ msg = 'E110: Missing closing parenthesis for nested expression: %.*s',
+ },
+ }, {
+ hl('NestingParenthesis', '('),
+ hl('Register', '@a'),
+ })
+ check_parsing('@a(@b', {
+ -- 01234
+ ast = {
+ {
+ 'Call:0:2:(',
+ children = {
+ 'Register(name=a):0:0:@a',
+ 'Register(name=b):0:3:@b',
+ },
+ },
+ },
+ err = {
+ arg = '(@b',
+ msg = 'E116: Missing closing parenthesis for function call: %.*s',
+ },
+ }, {
+ hl('Register', '@a'),
+ hl('CallingParenthesis', '('),
+ hl('Register', '@b'),
+ })
+ check_parsing('@a(@b, @c, @d, @e)', {
+ -- 012345678901234567
+ -- 0 1
+ ast = {
+ {
+ 'Call:0:2:(',
+ children = {
+ 'Register(name=a):0:0:@a',
+ {
+ 'Comma:0:5:,',
+ children = {
+ 'Register(name=b):0:3:@b',
+ {
+ 'Comma:0:9:,',
+ children = {
+ 'Register(name=c):0:6: @c',
+ {
+ 'Comma:0:13:,',
+ children = {
+ 'Register(name=d):0:10: @d',
+ 'Register(name=e):0:14: @e',
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ }, {
+ hl('Register', '@a'),
+ hl('CallingParenthesis', '('),
+ hl('Register', '@b'),
+ hl('Comma', ','),
+ hl('Register', '@c', 1),
+ hl('Comma', ','),
+ hl('Register', '@d', 1),
+ hl('Comma', ','),
+ hl('Register', '@e', 1),
+ hl('CallingParenthesis', ')'),
+ })
+ check_parsing('@a(@b(@c))', {
+ -- 01234567890123456789012345678901234567
+ -- 0 1 2 3
+ ast = {
+ {
+ 'Call:0:2:(',
+ children = {
+ 'Register(name=a):0:0:@a',
+ {
+ 'Call:0:5:(',
+ children = {
+ 'Register(name=b):0:3:@b',
+ 'Register(name=c):0:6:@c',
+ },
+ },
+ },
+ },
+ },
+ }, {
+ hl('Register', '@a'),
+ hl('CallingParenthesis', '('),
+ hl('Register', '@b'),
+ hl('CallingParenthesis', '('),
+ hl('Register', '@c'),
+ hl('CallingParenthesis', ')'),
+ hl('CallingParenthesis', ')'),
+ })
+ check_parsing('@a(@b(@c(@d(@e), @f(@g(@h), @i(@j)))))', {
+ -- 01234567890123456789012345678901234567
+ -- 0 1 2 3
+ ast = {
+ {
+ 'Call:0:2:(',
+ children = {
+ 'Register(name=a):0:0:@a',
+ {
+ 'Call:0:5:(',
+ children = {
+ 'Register(name=b):0:3:@b',
+ {
+ 'Call:0:8:(',
+ children = {
+ 'Register(name=c):0:6:@c',
+ {
+ 'Comma:0:15:,',
+ children = {
+ {
+ 'Call:0:11:(',
+ children = {
+ 'Register(name=d):0:9:@d',
+ 'Register(name=e):0:12:@e',
+ },
+ },
+ {
+ 'Call:0:19:(',
+ children = {
+ 'Register(name=f):0:16: @f',
+ {
+ 'Comma:0:26:,',
+ children = {
+ {
+ 'Call:0:22:(',
+ children = {
+ 'Register(name=g):0:20:@g',
+ 'Register(name=h):0:23:@h',
+ },
+ },
+ {
+ 'Call:0:30:(',
+ children = {
+ 'Register(name=i):0:27: @i',
+ 'Register(name=j):0:31:@j',
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ }, {
+ hl('Register', '@a'),
+ hl('CallingParenthesis', '('),
+ hl('Register', '@b'),
+ hl('CallingParenthesis', '('),
+ hl('Register', '@c'),
+ hl('CallingParenthesis', '('),
+ hl('Register', '@d'),
+ hl('CallingParenthesis', '('),
+ hl('Register', '@e'),
+ hl('CallingParenthesis', ')'),
+ hl('Comma', ','),
+ hl('Register', '@f', 1),
+ hl('CallingParenthesis', '('),
+ hl('Register', '@g'),
+ hl('CallingParenthesis', '('),
+ hl('Register', '@h'),
+ hl('CallingParenthesis', ')'),
+ hl('Comma', ','),
+ hl('Register', '@i', 1),
+ hl('CallingParenthesis', '('),
+ hl('Register', '@j'),
+ hl('CallingParenthesis', ')'),
+ hl('CallingParenthesis', ')'),
+ hl('CallingParenthesis', ')'),
+ hl('CallingParenthesis', ')'),
+ hl('CallingParenthesis', ')'),
+ })
+ check_parsing('()()', {
+ -- 0123
+ ast = {
+ {
+ 'Call:0:2:(',
+ children = {
+ {
+ 'Nested:0:0:(',
+ children = {
+ 'Missing:0:1:',
+ },
+ },
+ },
+ },
+ },
+ err = {
+ arg = ')()',
+ msg = 'E15: Expected value, got parenthesis: %.*s',
+ },
+ }, {
+ hl('NestingParenthesis', '('),
+ hl('InvalidNestingParenthesis', ')'),
+ hl('CallingParenthesis', '('),
+ hl('CallingParenthesis', ')'),
+ })
+ check_parsing('(@a)()', {
+ -- 012345
+ ast = {
+ {
+ 'Call:0:4:(',
+ children = {
+ {
+ 'Nested:0:0:(',
+ children = {
+ 'Register(name=a):0:1:@a',
+ },
+ },
+ },
+ },
+ },
+ }, {
+ hl('NestingParenthesis', '('),
+ hl('Register', '@a'),
+ hl('NestingParenthesis', ')'),
+ hl('CallingParenthesis', '('),
+ hl('CallingParenthesis', ')'),
+ })
+ check_parsing('(@a)(@b)', {
+ -- 01234567
+ ast = {
+ {
+ 'Call:0:4:(',
+ children = {
+ {
+ 'Nested:0:0:(',
+ children = {
+ 'Register(name=a):0:1:@a',
+ },
+ },
+ 'Register(name=b):0:5:@b',
+ },
+ },
+ },
+ }, {
+ hl('NestingParenthesis', '('),
+ hl('Register', '@a'),
+ hl('NestingParenthesis', ')'),
+ hl('CallingParenthesis', '('),
+ hl('Register', '@b'),
+ hl('CallingParenthesis', ')'),
+ })
+ check_parsing('(@a) (@b)', {
+ -- 012345678
+ ast = {
+ {
+ 'OpMissing:0:4:',
+ children = {
+ {
+ 'Nested:0:0:(',
+ children = {
+ 'Register(name=a):0:1:@a',
+ },
+ },
+ {
+ 'Nested:0:4: (',
+ children = {
+ 'Register(name=b):0:6:@b',
+ },
+ },
+ },
+ },
+ },
+ err = {
+ arg = '(@b)',
+ msg = 'E15: Missing operator: %.*s',
+ },
+ }, {
+ hl('NestingParenthesis', '('),
+ hl('Register', '@a'),
+ hl('NestingParenthesis', ')'),
+ hl('InvalidSpacing', ' '),
+ hl('NestingParenthesis', '('),
+ hl('Register', '@b'),
+ hl('NestingParenthesis', ')'),
+ }, {
+ [1] = {
+ ast = {
+ len = 5,
+ ast = {
+ {
+ 'Nested:0:0:(',
+ children = {
+ 'Register(name=a):0:1:@a',
+ REMOVE_THIS,
+ },
+ },
+ },
+ err = REMOVE_THIS,
+ },
+ hl_fs = {
+ [4] = REMOVE_THIS,
+ [5] = REMOVE_THIS,
+ [6] = REMOVE_THIS,
+ [7] = REMOVE_THIS,
+ },
+ },
+ })
+ end)
+ itp('works with variable names, including curly braces ones', function()
+ check_parsing('var', {
+ ast = {
+ 'PlainIdentifier(scope=0,ident=var):0:0:var',
+ },
+ }, {
+ hl('IdentifierName', 'var'),
+ })
+ check_parsing('g:var', {
+ ast = {
+ 'PlainIdentifier(scope=g,ident=var):0:0:g:var',
+ },
+ }, {
+ hl('IdentifierScope', 'g'),
+ hl('IdentifierScopeDelimiter', ':'),
+ hl('IdentifierName', 'var'),
+ })
+ check_parsing('g:', {
+ ast = {
+ 'PlainIdentifier(scope=g,ident=):0:0:g:',
+ },
+ }, {
+ hl('IdentifierScope', 'g'),
+ hl('IdentifierScopeDelimiter', ':'),
+ })
+ check_parsing('{a}', {
+ -- 012
+ ast = {
+ {
+ fmtn('CurlyBracesIdentifier', '-di', ':0:0:{'),
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:1:a',
+ },
+ },
+ },
+ }, {
+ hl('Curly', '{'),
+ hl('IdentifierName', 'a'),
+ hl('Curly', '}'),
+ })
+ check_parsing('{a:b}', {
+ -- 012
+ ast = {
+ {
+ fmtn('CurlyBracesIdentifier', '-di', ':0:0:{'),
+ children = {
+ 'PlainIdentifier(scope=a,ident=b):0:1:a:b',
+ },
+ },
+ },
+ }, {
+ hl('Curly', '{'),
+ hl('IdentifierScope', 'a'),
+ hl('IdentifierScopeDelimiter', ':'),
+ hl('IdentifierName', 'b'),
+ hl('Curly', '}'),
+ })
+ check_parsing('{a:@b}', {
+ -- 012345
+ ast = {
+ {
+ fmtn('CurlyBracesIdentifier', '-di', ':0:0:{'),
+ children = {
+ {
+ 'OpMissing:0:3:',
+ children={
+ 'PlainIdentifier(scope=a,ident=):0:1:a:',
+ 'Register(name=b):0:3:@b',
+ },
+ },
+ },
+ },
+ },
+ err = {
+ arg = '@b}',
+ msg = 'E15: Missing operator: %.*s',
+ },
+ }, {
+ hl('Curly', '{'),
+ hl('IdentifierScope', 'a'),
+ hl('IdentifierScopeDelimiter', ':'),
+ hl('InvalidRegister', '@b'),
+ hl('Curly', '}'),
+ })
+ check_parsing('{@a}', {
+ ast = {
+ {
+ fmtn('CurlyBracesIdentifier', '-di', ':0:0:{'),
+ children = {
+ 'Register(name=a):0:1:@a',
+ },
+ },
+ },
+ }, {
+ hl('Curly', '{'),
+ hl('Register', '@a'),
+ hl('Curly', '}'),
+ })
+ check_parsing('{@a}{@b}', {
+ -- 01234567
+ ast = {
+ {
+ 'ComplexIdentifier:0:4:',
+ children = {
+ {
+ fmtn('CurlyBracesIdentifier', '-di', ':0:0:{'),
+ children = {
+ 'Register(name=a):0:1:@a',
+ },
+ },
+ {
+ fmtn('CurlyBracesIdentifier', '--i', ':0:4:{'),
+ children = {
+ 'Register(name=b):0:5:@b',
+ },
+ },
+ },
+ },
+ },
+ }, {
+ hl('Curly', '{'),
+ hl('Register', '@a'),
+ hl('Curly', '}'),
+ hl('Curly', '{'),
+ hl('Register', '@b'),
+ hl('Curly', '}'),
+ })
+ check_parsing('g:{@a}', {
+ -- 01234567
+ ast = {
+ {
+ 'ComplexIdentifier:0:2:',
+ children = {
+ 'PlainIdentifier(scope=g,ident=):0:0:g:',
+ {
+ fmtn('CurlyBracesIdentifier', '--i', ':0:2:{'),
+ children = {
+ 'Register(name=a):0:3:@a',
+ },
+ },
+ },
+ },
+ },
+ }, {
+ hl('IdentifierScope', 'g'),
+ hl('IdentifierScopeDelimiter', ':'),
+ hl('Curly', '{'),
+ hl('Register', '@a'),
+ hl('Curly', '}'),
+ })
+ check_parsing('{@a}_test', {
+ -- 012345678
+ ast = {
+ {
+ 'ComplexIdentifier:0:4:',
+ children = {
+ {
+ fmtn('CurlyBracesIdentifier', '-di', ':0:0:{'),
+ children = {
+ 'Register(name=a):0:1:@a',
+ },
+ },
+ 'PlainIdentifier(scope=0,ident=_test):0:4:_test',
+ },
+ },
+ },
+ }, {
+ hl('Curly', '{'),
+ hl('Register', '@a'),
+ hl('Curly', '}'),
+ hl('IdentifierName', '_test'),
+ })
+ check_parsing('g:{@a}_test', {
+ -- 01234567890
+ ast = {
+ {
+ 'ComplexIdentifier:0:2:',
+ children = {
+ 'PlainIdentifier(scope=g,ident=):0:0:g:',
+ {
+ 'ComplexIdentifier:0:6:',
+ children = {
+ {
+ fmtn('CurlyBracesIdentifier', '--i', ':0:2:{'),
+ children = {
+ 'Register(name=a):0:3:@a',
+ },
+ },
+ 'PlainIdentifier(scope=0,ident=_test):0:6:_test',
+ },
+ },
+ },
+ },
+ },
+ }, {
+ hl('IdentifierScope', 'g'),
+ hl('IdentifierScopeDelimiter', ':'),
+ hl('Curly', '{'),
+ hl('Register', '@a'),
+ hl('Curly', '}'),
+ hl('IdentifierName', '_test'),
+ })
+ check_parsing('g:{@a}_test()', {
+ -- 0123456789012
+ ast = {
+ {
+ 'Call:0:11:(',
+ children = {
+ {
+ 'ComplexIdentifier:0:2:',
+ children = {
+ 'PlainIdentifier(scope=g,ident=):0:0:g:',
+ {
+ 'ComplexIdentifier:0:6:',
+ children = {
+ {
+ fmtn('CurlyBracesIdentifier', '--i', ':0:2:{'),
+ children = {
+ 'Register(name=a):0:3:@a',
+ },
+ },
+ 'PlainIdentifier(scope=0,ident=_test):0:6:_test',
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ }, {
+ hl('IdentifierScope', 'g'),
+ hl('IdentifierScopeDelimiter', ':'),
+ hl('Curly', '{'),
+ hl('Register', '@a'),
+ hl('Curly', '}'),
+ hl('IdentifierName', '_test'),
+ hl('CallingParenthesis', '('),
+ hl('CallingParenthesis', ')'),
+ })
+ check_parsing('{@a} ()', {
+ -- 0123456789012
+ ast = {
+ {
+ 'Call:0:4: (',
+ children = {
+ {
+ fmtn('CurlyBracesIdentifier', '-di', ':0:0:{'),
+ children = {
+ 'Register(name=a):0:1:@a',
+ },
+ },
+ },
+ },
+ },
+ }, {
+ hl('Curly', '{'),
+ hl('Register', '@a'),
+ hl('Curly', '}'),
+ hl('CallingParenthesis', '(', 1),
+ hl('CallingParenthesis', ')'),
+ })
+ check_parsing('g:{@a} ()', {
+ -- 0123456789012
+ ast = {
+ {
+ 'Call:0:6: (',
+ children = {
+ {
+ 'ComplexIdentifier:0:2:',
+ children = {
+ 'PlainIdentifier(scope=g,ident=):0:0:g:',
+ {
+ fmtn('CurlyBracesIdentifier', '--i', ':0:2:{'),
+ children = {
+ 'Register(name=a):0:3:@a',
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ }, {
+ hl('IdentifierScope', 'g'),
+ hl('IdentifierScopeDelimiter', ':'),
+ hl('Curly', '{'),
+ hl('Register', '@a'),
+ hl('Curly', '}'),
+ hl('CallingParenthesis', '(', 1),
+ hl('CallingParenthesis', ')'),
+ })
+ check_parsing('{@a', {
+ -- 012
+ ast = {
+ {
+ fmtn('UnknownFigure', '-di', ':0:0:{'),
+ children = {
+ 'Register(name=a):0:1:@a',
+ },
+ },
+ },
+ err = {
+ arg = '{@a',
+ msg = 'E15: Missing closing figure brace: %.*s',
+ },
+ }, {
+ hl('FigureBrace', '{'),
+ hl('Register', '@a'),
+ })
+ check_parsing('a ()', {
+ -- 0123
+ ast = {
+ {
+ 'Call:0:1: (',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:0:a',
+ },
+ },
+ },
+ }, {
+ hl('IdentifierName', 'a'),
+ hl('CallingParenthesis', '(', 1),
+ hl('CallingParenthesis', ')'),
+ })
+ end)
+ itp('works with lambdas and dictionaries', function()
+ check_parsing('{}', {
+ ast = {
+ fmtn('DictLiteral', '-di', ':0:0:{'),
+ },
+ }, {
+ hl('Dict', '{'),
+ hl('Dict', '}'),
+ })
+ check_parsing('{->@a}', {
+ ast = {
+ {
+ fmtn('Lambda', '\\di', ':0:0:{'),
+ children = {
+ {
+ 'Arrow:0:1:->',
+ children = {
+ 'Register(name=a):0:3:@a',
+ },
+ },
+ },
+ },
+ },
+ }, {
+ hl('Lambda', '{'),
+ hl('Arrow', '->'),
+ hl('Register', '@a'),
+ hl('Lambda', '}'),
+ })
+ check_parsing('{->@a+@b}', {
+ -- 012345678
+ ast = {
+ {
+ fmtn('Lambda', '\\di', ':0:0:{'),
+ children = {
+ {
+ 'Arrow:0:1:->',
+ children = {
+ {
+ 'BinaryPlus:0:5:+',
+ children = {
+ 'Register(name=a):0:3:@a',
+ 'Register(name=b):0:6:@b',
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ }, {
+ hl('Lambda', '{'),
+ hl('Arrow', '->'),
+ hl('Register', '@a'),
+ hl('BinaryPlus', '+'),
+ hl('Register', '@b'),
+ hl('Lambda', '}'),
+ })
+ check_parsing('{a->@a}', {
+ -- 012345678
+ ast = {
+ {
+ fmtn('Lambda', '\\di', ':0:0:{'),
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:1:a',
+ {
+ 'Arrow:0:2:->',
+ children = {
+ 'Register(name=a):0:4:@a',
+ },
+ },
+ },
+ },
+ },
+ }, {
+ hl('Lambda', '{'),
+ hl('IdentifierName', 'a'),
+ hl('Arrow', '->'),
+ hl('Register', '@a'),
+ hl('Lambda', '}'),
+ })
+ check_parsing('{a,b->@a}', {
+ -- 012345678
+ ast = {
+ {
+ fmtn('Lambda', '\\di', ':0:0:{'),
+ children = {
+ {
+ 'Comma:0:2:,',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:1:a',
+ 'PlainIdentifier(scope=0,ident=b):0:3:b',
+ },
+ },
+ {
+ 'Arrow:0:4:->',
+ children = {
+ 'Register(name=a):0:6:@a',
+ },
+ },
+ },
+ },
+ },
+ }, {
+ hl('Lambda', '{'),
+ hl('IdentifierName', 'a'),
+ hl('Comma', ','),
+ hl('IdentifierName', 'b'),
+ hl('Arrow', '->'),
+ hl('Register', '@a'),
+ hl('Lambda', '}'),
+ })
+ check_parsing('{a,b,c->@a}', {
+ -- 01234567890
+ ast = {
+ {
+ fmtn('Lambda', '\\di', ':0:0:{'),
+ children = {
+ {
+ 'Comma:0:2:,',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:1:a',
+ {
+ 'Comma:0:4:,',
+ children = {
+ 'PlainIdentifier(scope=0,ident=b):0:3:b',
+ 'PlainIdentifier(scope=0,ident=c):0:5:c',
+ },
+ },
+ },
+ },
+ {
+ 'Arrow:0:6:->',
+ children = {
+ 'Register(name=a):0:8:@a',
+ },
+ },
+ },
+ },
+ },
+ }, {
+ hl('Lambda', '{'),
+ hl('IdentifierName', 'a'),
+ hl('Comma', ','),
+ hl('IdentifierName', 'b'),
+ hl('Comma', ','),
+ hl('IdentifierName', 'c'),
+ hl('Arrow', '->'),
+ hl('Register', '@a'),
+ hl('Lambda', '}'),
+ })
+ check_parsing('{a,b,c,d->@a}', {
+ -- 0123456789012
+ ast = {
+ {
+ fmtn('Lambda', '\\di', ':0:0:{'),
+ children = {
+ {
+ 'Comma:0:2:,',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:1:a',
+ {
+ 'Comma:0:4:,',
+ children = {
+ 'PlainIdentifier(scope=0,ident=b):0:3:b',
+ {
+ 'Comma:0:6:,',
+ children = {
+ 'PlainIdentifier(scope=0,ident=c):0:5:c',
+ 'PlainIdentifier(scope=0,ident=d):0:7:d',
+ },
+ },
+ },
+ },
+ },
+ },
+ {
+ 'Arrow:0:8:->',
+ children = {
+ 'Register(name=a):0:10:@a',
+ },
+ },
+ },
+ },
+ },
+ }, {
+ hl('Lambda', '{'),
+ hl('IdentifierName', 'a'),
+ hl('Comma', ','),
+ hl('IdentifierName', 'b'),
+ hl('Comma', ','),
+ hl('IdentifierName', 'c'),
+ hl('Comma', ','),
+ hl('IdentifierName', 'd'),
+ hl('Arrow', '->'),
+ hl('Register', '@a'),
+ hl('Lambda', '}'),
+ })
+ check_parsing('{a,b,c,d,->@a}', {
+ -- 01234567890123
+ ast = {
+ {
+ fmtn('Lambda', '\\di', ':0:0:{'),
+ children = {
+ {
+ 'Comma:0:2:,',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:1:a',
+ {
+ 'Comma:0:4:,',
+ children = {
+ 'PlainIdentifier(scope=0,ident=b):0:3:b',
+ {
+ 'Comma:0:6:,',
+ children = {
+ 'PlainIdentifier(scope=0,ident=c):0:5:c',
+ {
+ 'Comma:0:8:,',
+ children = {
+ 'PlainIdentifier(scope=0,ident=d):0:7:d',
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ {
+ 'Arrow:0:9:->',
+ children = {
+ 'Register(name=a):0:11:@a',
+ },
+ },
+ },
+ },
+ },
+ }, {
+ hl('Lambda', '{'),
+ hl('IdentifierName', 'a'),
+ hl('Comma', ','),
+ hl('IdentifierName', 'b'),
+ hl('Comma', ','),
+ hl('IdentifierName', 'c'),
+ hl('Comma', ','),
+ hl('IdentifierName', 'd'),
+ hl('Comma', ','),
+ hl('Arrow', '->'),
+ hl('Register', '@a'),
+ hl('Lambda', '}'),
+ })
+ check_parsing('{a,b->{c,d->{e,f->@a}}}', {
+ -- 01234567890123456789012
+ -- 0 1 2
+ ast = {
+ {
+ fmtn('Lambda', '\\di', ':0:0:{'),
+ children = {
+ {
+ 'Comma:0:2:,',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:1:a',
+ 'PlainIdentifier(scope=0,ident=b):0:3:b',
+ },
+ },
+ {
+ 'Arrow:0:4:->',
+ children = {
+ {
+ fmtn('Lambda', '\\di', ':0:6:{'),
+ children = {
+ {
+ 'Comma:0:8:,',
+ children = {
+ 'PlainIdentifier(scope=0,ident=c):0:7:c',
+ 'PlainIdentifier(scope=0,ident=d):0:9:d',
+ },
+ },
+ {
+ 'Arrow:0:10:->',
+ children = {
+ {
+ fmtn('Lambda', '\\di', ':0:12:{'),
+ children = {
+ {
+ 'Comma:0:14:,',
+ children = {
+ 'PlainIdentifier(scope=0,ident=e):0:13:e',
+ 'PlainIdentifier(scope=0,ident=f):0:15:f',
+ },
+ },
+ {
+ 'Arrow:0:16:->',
+ children = {
+ 'Register(name=a):0:18:@a',
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ }, {
+ hl('Lambda', '{'),
+ hl('IdentifierName', 'a'),
+ hl('Comma', ','),
+ hl('IdentifierName', 'b'),
+ hl('Arrow', '->'),
+ hl('Lambda', '{'),
+ hl('IdentifierName', 'c'),
+ hl('Comma', ','),
+ hl('IdentifierName', 'd'),
+ hl('Arrow', '->'),
+ hl('Lambda', '{'),
+ hl('IdentifierName', 'e'),
+ hl('Comma', ','),
+ hl('IdentifierName', 'f'),
+ hl('Arrow', '->'),
+ hl('Register', '@a'),
+ hl('Lambda', '}'),
+ hl('Lambda', '}'),
+ hl('Lambda', '}'),
+ })
+ check_parsing('{a,b->c,d}', {
+ -- 0123456789
+ ast = {
+ {
+ fmtn('Lambda', '\\di', ':0:0:{'),
+ children = {
+ {
+ 'Comma:0:2:,',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:1:a',
+ 'PlainIdentifier(scope=0,ident=b):0:3:b',
+ },
+ },
+ {
+ 'Arrow:0:4:->',
+ children = {
+ {
+ 'Comma:0:7:,',
+ children = {
+ 'PlainIdentifier(scope=0,ident=c):0:6:c',
+ 'PlainIdentifier(scope=0,ident=d):0:8:d',
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ err = {
+ arg = ',d}',
+ msg = 'E15: Comma outside of call, lambda or literal: %.*s',
+ },
+ }, {
+ hl('Lambda', '{'),
+ hl('IdentifierName', 'a'),
+ hl('Comma', ','),
+ hl('IdentifierName', 'b'),
+ hl('Arrow', '->'),
+ hl('IdentifierName', 'c'),
+ hl('InvalidComma', ','),
+ hl('IdentifierName', 'd'),
+ hl('Lambda', '}'),
+ })
+ check_parsing('a,b,c,d', {
+ -- 0123456789
+ ast = {
+ {
+ 'Comma:0:1:,',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:0:a',
+ {
+ 'Comma:0:3:,',
+ children = {
+ 'PlainIdentifier(scope=0,ident=b):0:2:b',
+ {
+ 'Comma:0:5:,',
+ children = {
+ 'PlainIdentifier(scope=0,ident=c):0:4:c',
+ 'PlainIdentifier(scope=0,ident=d):0:6:d',
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ err = {
+ arg = ',b,c,d',
+ msg = 'E15: Comma outside of call, lambda or literal: %.*s',
+ },
+ }, {
+ hl('IdentifierName', 'a'),
+ hl('InvalidComma', ','),
+ hl('IdentifierName', 'b'),
+ hl('InvalidComma', ','),
+ hl('IdentifierName', 'c'),
+ hl('InvalidComma', ','),
+ hl('IdentifierName', 'd'),
+ })
+ check_parsing('a,b,c,d,', {
+ -- 0123456789
+ ast = {
+ {
+ 'Comma:0:1:,',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:0:a',
+ {
+ 'Comma:0:3:,',
+ children = {
+ 'PlainIdentifier(scope=0,ident=b):0:2:b',
+ {
+ 'Comma:0:5:,',
+ children = {
+ 'PlainIdentifier(scope=0,ident=c):0:4:c',
+ {
+ 'Comma:0:7:,',
+ children = {
+ 'PlainIdentifier(scope=0,ident=d):0:6:d',
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ err = {
+ arg = ',b,c,d,',
+ msg = 'E15: Comma outside of call, lambda or literal: %.*s',
+ },
+ }, {
+ hl('IdentifierName', 'a'),
+ hl('InvalidComma', ','),
+ hl('IdentifierName', 'b'),
+ hl('InvalidComma', ','),
+ hl('IdentifierName', 'c'),
+ hl('InvalidComma', ','),
+ hl('IdentifierName', 'd'),
+ hl('InvalidComma', ','),
+ })
+ check_parsing(',', {
+ -- 0123456789
+ ast = {
+ {
+ 'Comma:0:0:,',
+ children = {
+ 'Missing:0:0:',
+ },
+ },
+ },
+ err = {
+ arg = ',',
+ msg = 'E15: Expected value, got comma: %.*s',
+ },
+ }, {
+ hl('InvalidComma', ','),
+ })
+ check_parsing('{,a->@a}', {
+ -- 0123456789
+ ast = {
+ {
+ fmtn('CurlyBracesIdentifier', '-di', ':0:0:{'),
+ children = {
+ {
+ 'Arrow:0:3:->',
+ children = {
+ {
+ 'Comma:0:1:,',
+ children = {
+ 'Missing:0:1:',
+ 'PlainIdentifier(scope=0,ident=a):0:2:a',
+ },
+ },
+ 'Register(name=a):0:5:@a',
+ },
+ },
+ },
+ },
+ },
+ err = {
+ arg = ',a->@a}',
+ msg = 'E15: Expected value, got comma: %.*s',
+ },
+ }, {
+ hl('Curly', '{'),
+ hl('InvalidComma', ','),
+ hl('IdentifierName', 'a'),
+ hl('InvalidArrow', '->'),
+ hl('Register', '@a'),
+ hl('Curly', '}'),
+ })
+ check_parsing('}', {
+ -- 0123456789
+ ast = {
+ fmtn('UnknownFigure', '---', ':0:0:'),
+ },
+ err = {
+ arg = '}',
+ msg = 'E15: Unexpected closing figure brace: %.*s',
+ },
+ }, {
+ hl('InvalidFigureBrace', '}'),
+ })
+ check_parsing('{->}', {
+ -- 0123456789
+ ast = {
+ {
+ fmtn('Lambda', '\\di', ':0:0:{'),
+ children = {
+ 'Arrow:0:1:->',
+ },
+ },
+ },
+ err = {
+ arg = '}',
+ msg = 'E15: Expected value, got closing figure brace: %.*s',
+ },
+ }, {
+ hl('Lambda', '{'),
+ hl('Arrow', '->'),
+ hl('InvalidLambda', '}'),
+ })
+ check_parsing('{a,b}', {
+ -- 0123456789
+ ast = {
+ {
+ fmtn('Lambda', '-di', ':0:0:{'),
+ children = {
+ {
+ 'Comma:0:2:,',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:1:a',
+ 'PlainIdentifier(scope=0,ident=b):0:3:b',
+ },
+ },
+ },
+ },
+ },
+ err = {
+ arg = '}',
+ msg = 'E15: Expected lambda arguments list or arrow: %.*s',
+ },
+ }, {
+ hl('Lambda', '{'),
+ hl('IdentifierName', 'a'),
+ hl('Comma', ','),
+ hl('IdentifierName', 'b'),
+ hl('InvalidLambda', '}'),
+ })
+ check_parsing('{a,}', {
+ -- 0123456789
+ ast = {
+ {
+ fmtn('Lambda', '-di', ':0:0:{'),
+ children = {
+ {
+ 'Comma:0:2:,',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:1:a',
+ },
+ },
+ },
+ },
+ },
+ err = {
+ arg = '}',
+ msg = 'E15: Expected lambda arguments list or arrow: %.*s',
+ },
+ }, {
+ hl('Lambda', '{'),
+ hl('IdentifierName', 'a'),
+ hl('Comma', ','),
+ hl('InvalidLambda', '}'),
+ })
+ check_parsing('{@a:@b}', {
+ -- 0123456789
+ ast = {
+ {
+ fmtn('DictLiteral', '-di', ':0:0:{'),
+ children = {
+ {
+ 'Colon:0:3::',
+ children = {
+ 'Register(name=a):0:1:@a',
+ 'Register(name=b):0:4:@b',
+ },
+ },
+ },
+ },
+ },
+ }, {
+ hl('Dict', '{'),
+ hl('Register', '@a'),
+ hl('Colon', ':'),
+ hl('Register', '@b'),
+ hl('Dict', '}'),
+ })
+ check_parsing('{@a:@b,@c:@d}', {
+ -- 0123456789012
+ -- 0 1
+ ast = {
+ {
+ fmtn('DictLiteral', '-di', ':0:0:{'),
+ children = {
+ {
+ 'Comma:0:6:,',
+ children = {
+ {
+ 'Colon:0:3::',
+ children = {
+ 'Register(name=a):0:1:@a',
+ 'Register(name=b):0:4:@b',
+ },
+ },
+ {
+ 'Colon:0:9::',
+ children = {
+ 'Register(name=c):0:7:@c',
+ 'Register(name=d):0:10:@d',
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ }, {
+ hl('Dict', '{'),
+ hl('Register', '@a'),
+ hl('Colon', ':'),
+ hl('Register', '@b'),
+ hl('Comma', ','),
+ hl('Register', '@c'),
+ hl('Colon', ':'),
+ hl('Register', '@d'),
+ hl('Dict', '}'),
+ })
+ check_parsing('{@a:@b,@c:@d,@e:@f,}', {
+ -- 01234567890123456789
+ -- 0 1
+ ast = {
+ {
+ fmtn('DictLiteral', '-di', ':0:0:{'),
+ children = {
+ {
+ 'Comma:0:6:,',
+ children = {
+ {
+ 'Colon:0:3::',
+ children = {
+ 'Register(name=a):0:1:@a',
+ 'Register(name=b):0:4:@b',
+ },
+ },
+ {
+ 'Comma:0:12:,',
+ children = {
+ {
+ 'Colon:0:9::',
+ children = {
+ 'Register(name=c):0:7:@c',
+ 'Register(name=d):0:10:@d',
+ },
+ },
+ {
+ 'Comma:0:18:,',
+ children = {
+ {
+ 'Colon:0:15::',
+ children = {
+ 'Register(name=e):0:13:@e',
+ 'Register(name=f):0:16:@f',
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ }, {
+ hl('Dict', '{'),
+ hl('Register', '@a'),
+ hl('Colon', ':'),
+ hl('Register', '@b'),
+ hl('Comma', ','),
+ hl('Register', '@c'),
+ hl('Colon', ':'),
+ hl('Register', '@d'),
+ hl('Comma', ','),
+ hl('Register', '@e'),
+ hl('Colon', ':'),
+ hl('Register', '@f'),
+ hl('Comma', ','),
+ hl('Dict', '}'),
+ })
+ check_parsing('{@a:@b,@c:@d,@e:@f,@g:}', {
+ -- 01234567890123456789012
+ -- 0 1 2
+ ast = {
+ {
+ fmtn('DictLiteral', '-di', ':0:0:{'),
+ children = {
+ {
+ 'Comma:0:6:,',
+ children = {
+ {
+ 'Colon:0:3::',
+ children = {
+ 'Register(name=a):0:1:@a',
+ 'Register(name=b):0:4:@b',
+ },
+ },
+ {
+ 'Comma:0:12:,',
+ children = {
+ {
+ 'Colon:0:9::',
+ children = {
+ 'Register(name=c):0:7:@c',
+ 'Register(name=d):0:10:@d',
+ },
+ },
+ {
+ 'Comma:0:18:,',
+ children = {
+ {
+ 'Colon:0:15::',
+ children = {
+ 'Register(name=e):0:13:@e',
+ 'Register(name=f):0:16:@f',
+ },
+ },
+ {
+ 'Colon:0:21::',
+ children = {
+ 'Register(name=g):0:19:@g',
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ err = {
+ arg = '}',
+ msg = 'E15: Expected value, got closing figure brace: %.*s',
+ },
+ }, {
+ hl('Dict', '{'),
+ hl('Register', '@a'),
+ hl('Colon', ':'),
+ hl('Register', '@b'),
+ hl('Comma', ','),
+ hl('Register', '@c'),
+ hl('Colon', ':'),
+ hl('Register', '@d'),
+ hl('Comma', ','),
+ hl('Register', '@e'),
+ hl('Colon', ':'),
+ hl('Register', '@f'),
+ hl('Comma', ','),
+ hl('Register', '@g'),
+ hl('Colon', ':'),
+ hl('InvalidDict', '}'),
+ })
+ check_parsing('{@a:@b,}', {
+ -- 01234567890123
+ -- 0 1
+ ast = {
+ {
+ fmtn('DictLiteral', '-di', ':0:0:{'),
+ children = {
+ {
+ 'Comma:0:6:,',
+ children = {
+ {
+ 'Colon:0:3::',
+ children = {
+ 'Register(name=a):0:1:@a',
+ 'Register(name=b):0:4:@b',
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ }, {
+ hl('Dict', '{'),
+ hl('Register', '@a'),
+ hl('Colon', ':'),
+ hl('Register', '@b'),
+ hl('Comma', ','),
+ hl('Dict', '}'),
+ })
+ check_parsing('{({f -> g})(@h)(@i)}', {
+ -- 01234567890123456789
+ -- 0 1
+ ast = {
+ {
+ fmtn('CurlyBracesIdentifier', '-di', ':0:0:{'),
+ children = {
+ {
+ 'Call:0:15:(',
+ children = {
+ {
+ 'Call:0:11:(',
+ children = {
+ {
+ 'Nested:0:1:(',
+ children = {
+ {
+ fmtn('Lambda', '\\di', ':0:2:{'),
+ children = {
+ 'PlainIdentifier(scope=0,ident=f):0:3:f',
+ {
+ 'Arrow:0:4: ->',
+ children = {
+ 'PlainIdentifier(scope=0,ident=g):0:7: g',
+ },
+ },
+ },
+ },
+ },
+ },
+ 'Register(name=h):0:12:@h',
+ },
+ },
+ 'Register(name=i):0:16:@i',
+ },
+ },
+ },
+ },
+ },
+ }, {
+ hl('Curly', '{'),
+ hl('NestingParenthesis', '('),
+ hl('Lambda', '{'),
+ hl('IdentifierName', 'f'),
+ hl('Arrow', '->', 1),
+ hl('IdentifierName', 'g', 1),
+ hl('Lambda', '}'),
+ hl('NestingParenthesis', ')'),
+ hl('CallingParenthesis', '('),
+ hl('Register', '@h'),
+ hl('CallingParenthesis', ')'),
+ hl('CallingParenthesis', '('),
+ hl('Register', '@i'),
+ hl('CallingParenthesis', ')'),
+ hl('Curly', '}'),
+ })
+ check_parsing('a:{b()}c', {
+ -- 01234567
+ ast = {
+ {
+ 'ComplexIdentifier:0:2:',
+ children = {
+ 'PlainIdentifier(scope=a,ident=):0:0:a:',
+ {
+ 'ComplexIdentifier:0:7:',
+ children = {
+ {
+ fmtn('CurlyBracesIdentifier', '--i', ':0:2:{'),
+ children = {
+ {
+ 'Call:0:4:(',
+ children = {
+ 'PlainIdentifier(scope=0,ident=b):0:3:b',
+ },
+ },
+ },
+ },
+ 'PlainIdentifier(scope=0,ident=c):0:7:c',
+ },
+ },
+ },
+ },
+ },
+ }, {
+ hl('IdentifierScope', 'a'),
+ hl('IdentifierScopeDelimiter', ':'),
+ hl('Curly', '{'),
+ hl('IdentifierName', 'b'),
+ hl('CallingParenthesis', '('),
+ hl('CallingParenthesis', ')'),
+ hl('Curly', '}'),
+ hl('IdentifierName', 'c'),
+ })
+ check_parsing('a:{{b, c -> @d + @e + ({f -> g})(@h)}(@i)}j', {
+ -- 01234567890123456789012345678901234567890123456
+ -- 0 1 2 3 4
+ ast = {
+ {
+ 'ComplexIdentifier:0:2:',
+ children = {
+ 'PlainIdentifier(scope=a,ident=):0:0:a:',
+ {
+ 'ComplexIdentifier:0:42:',
+ children = {
+ {
+ fmtn('CurlyBracesIdentifier', '--i', ':0:2:{'),
+ children = {
+ {
+ 'Call:0:37:(',
+ children = {
+ {
+ fmtn('Lambda', '\\di', ':0:3:{'),
+ children = {
+ {
+ 'Comma:0:5:,',
+ children = {
+ 'PlainIdentifier(scope=0,ident=b):0:4:b',
+ 'PlainIdentifier(scope=0,ident=c):0:6: c',
+ },
+ },
+ {
+ 'Arrow:0:8: ->',
+ children = {
+ {
+ 'BinaryPlus:0:19: +',
+ children = {
+ {
+ 'BinaryPlus:0:14: +',
+ children = {
+ 'Register(name=d):0:11: @d',
+ 'Register(name=e):0:16: @e',
+ },
+ },
+ {
+ 'Call:0:32:(',
+ children = {
+ {
+ 'Nested:0:21: (',
+ children = {
+ {
+ fmtn('Lambda', '\\di', ':0:23:{'),
+ children = {
+ 'PlainIdentifier(scope=0,ident=f):0:24:f',
+ {
+ 'Arrow:0:25: ->',
+ children = {
+ 'PlainIdentifier(scope=0,ident=g):0:28: g',
+ },
+ },
+ },
+ },
+ },
+ },
+ 'Register(name=h):0:33:@h',
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ 'Register(name=i):0:38:@i',
+ },
+ },
+ },
+ },
+ 'PlainIdentifier(scope=0,ident=j):0:42:j',
+ },
+ },
+ },
+ },
+ },
+ }, {
+ hl('IdentifierScope', 'a'),
+ hl('IdentifierScopeDelimiter', ':'),
+ hl('Curly', '{'),
+ hl('Lambda', '{'),
+ hl('IdentifierName', 'b'),
+ hl('Comma', ','),
+ hl('IdentifierName', 'c', 1),
+ hl('Arrow', '->', 1),
+ hl('Register', '@d', 1),
+ hl('BinaryPlus', '+', 1),
+ hl('Register', '@e', 1),
+ hl('BinaryPlus', '+', 1),
+ hl('NestingParenthesis', '(', 1),
+ hl('Lambda', '{'),
+ hl('IdentifierName', 'f'),
+ hl('Arrow', '->', 1),
+ hl('IdentifierName', 'g', 1),
+ hl('Lambda', '}'),
+ hl('NestingParenthesis', ')'),
+ hl('CallingParenthesis', '('),
+ hl('Register', '@h'),
+ hl('CallingParenthesis', ')'),
+ hl('Lambda', '}'),
+ hl('CallingParenthesis', '('),
+ hl('Register', '@i'),
+ hl('CallingParenthesis', ')'),
+ hl('Curly', '}'),
+ hl('IdentifierName', 'j'),
+ })
+ check_parsing('{@a + @b : @c + @d, @e + @f : @g + @i}', {
+ -- 01234567890123456789012345678901234567
+ -- 0 1 2 3
+ ast = {
+ {
+ fmtn('DictLiteral', '-di', ':0:0:{'),
+ children = {
+ {
+ 'Comma:0:18:,',
+ children = {
+ {
+ 'Colon:0:8: :',
+ children = {
+ {
+ 'BinaryPlus:0:3: +',
+ children = {
+ 'Register(name=a):0:1:@a',
+ 'Register(name=b):0:5: @b',
+ },
+ },
+ {
+ 'BinaryPlus:0:13: +',
+ children = {
+ 'Register(name=c):0:10: @c',
+ 'Register(name=d):0:15: @d',
+ },
+ },
+ },
+ },
+ {
+ 'Colon:0:27: :',
+ children = {
+ {
+ 'BinaryPlus:0:22: +',
+ children = {
+ 'Register(name=e):0:19: @e',
+ 'Register(name=f):0:24: @f',
+ },
+ },
+ {
+ 'BinaryPlus:0:32: +',
+ children = {
+ 'Register(name=g):0:29: @g',
+ 'Register(name=i):0:34: @i',
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ }, {
+ hl('Dict', '{'),
+ hl('Register', '@a'),
+ hl('BinaryPlus', '+', 1),
+ hl('Register', '@b', 1),
+ hl('Colon', ':', 1),
+ hl('Register', '@c', 1),
+ hl('BinaryPlus', '+', 1),
+ hl('Register', '@d', 1),
+ hl('Comma', ','),
+ hl('Register', '@e', 1),
+ hl('BinaryPlus', '+', 1),
+ hl('Register', '@f', 1),
+ hl('Colon', ':', 1),
+ hl('Register', '@g', 1),
+ hl('BinaryPlus', '+', 1),
+ hl('Register', '@i', 1),
+ hl('Dict', '}'),
+ })
+ check_parsing('-> -> ->', {
+ -- 01234567
+ ast = {
+ {
+ 'Arrow:0:0:->',
+ children = {
+ 'Missing:0:0:',
+ {
+ 'Arrow:0:2: ->',
+ children = {
+ 'Missing:0:2:',
+ {
+ 'Arrow:0:5: ->',
+ children = {
+ 'Missing:0:5:',
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ err = {
+ arg = '-> -> ->',
+ msg = 'E15: Unexpected arrow: %.*s',
+ },
+ }, {
+ hl('InvalidArrow', '->'),
+ hl('InvalidArrow', '->', 1),
+ hl('InvalidArrow', '->', 1),
+ })
+ check_parsing('a -> b -> c -> d', {
+ -- 0123456789012345
+ -- 0 1
+ ast = {
+ {
+ 'Arrow:0:1: ->',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:0:a',
+ {
+ 'Arrow:0:6: ->',
+ children = {
+ 'PlainIdentifier(scope=0,ident=b):0:4: b',
+ {
+ 'Arrow:0:11: ->',
+ children = {
+ 'PlainIdentifier(scope=0,ident=c):0:9: c',
+ 'PlainIdentifier(scope=0,ident=d):0:14: d',
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ err = {
+ arg = '-> b -> c -> d',
+ msg = 'E15: Arrow outside of lambda: %.*s',
+ },
+ }, {
+ hl('IdentifierName', 'a'),
+ hl('InvalidArrow', '->', 1),
+ hl('IdentifierName', 'b', 1),
+ hl('InvalidArrow', '->', 1),
+ hl('IdentifierName', 'c', 1),
+ hl('InvalidArrow', '->', 1),
+ hl('IdentifierName', 'd', 1),
+ })
+ check_parsing('{a -> b -> c}', {
+ -- 0123456789012
+ -- 0 1
+ ast = {
+ {
+ fmtn('Lambda', '\\di', ':0:0:{'),
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:1:a',
+ {
+ 'Arrow:0:2: ->',
+ children = {
+ {
+ 'Arrow:0:7: ->',
+ children = {
+ 'PlainIdentifier(scope=0,ident=b):0:5: b',
+ 'PlainIdentifier(scope=0,ident=c):0:10: c',
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ err = {
+ arg = '-> c}',
+ msg = 'E15: Arrow outside of lambda: %.*s',
+ },
+ }, {
+ hl('Lambda', '{'),
+ hl('IdentifierName', 'a'),
+ hl('Arrow', '->', 1),
+ hl('IdentifierName', 'b', 1),
+ hl('InvalidArrow', '->', 1),
+ hl('IdentifierName', 'c', 1),
+ hl('Lambda', '}'),
+ })
+ check_parsing('{a: -> b}', {
+ -- 012345678
+ ast = {
+ {
+ fmtn('CurlyBracesIdentifier', '-di', ':0:0:{'),
+ children = {
+ {
+ 'Arrow:0:3: ->',
+ children = {
+ 'PlainIdentifier(scope=a,ident=):0:1:a:',
+ 'PlainIdentifier(scope=0,ident=b):0:6: b',
+ },
+ },
+ },
+ },
+ },
+ err = {
+ arg = '-> b}',
+ msg = 'E15: Arrow outside of lambda: %.*s',
+ },
+ }, {
+ hl('Curly', '{'),
+ hl('IdentifierScope', 'a'),
+ hl('IdentifierScopeDelimiter', ':'),
+ hl('InvalidArrow', '->', 1),
+ hl('IdentifierName', 'b', 1),
+ hl('Curly', '}'),
+ })
+
+ check_parsing('{a:b -> b}', {
+ -- 0123456789
+ ast = {
+ {
+ fmtn('CurlyBracesIdentifier', '-di', ':0:0:{'),
+ children = {
+ {
+ 'Arrow:0:4: ->',
+ children = {
+ 'PlainIdentifier(scope=a,ident=b):0:1:a:b',
+ 'PlainIdentifier(scope=0,ident=b):0:7: b',
+ },
+ },
+ },
+ },
+ },
+ err = {
+ arg = '-> b}',
+ msg = 'E15: Arrow outside of lambda: %.*s',
+ },
+ }, {
+ hl('Curly', '{'),
+ hl('IdentifierScope', 'a'),
+ hl('IdentifierScopeDelimiter', ':'),
+ hl('IdentifierName', 'b'),
+ hl('InvalidArrow', '->', 1),
+ hl('IdentifierName', 'b', 1),
+ hl('Curly', '}'),
+ })
+
+ check_parsing('{a#b -> b}', {
+ -- 0123456789
+ ast = {
+ {
+ fmtn('CurlyBracesIdentifier', '-di', ':0:0:{'),
+ children = {
+ {
+ 'Arrow:0:4: ->',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a#b):0:1:a#b',
+ 'PlainIdentifier(scope=0,ident=b):0:7: b',
+ },
+ },
+ },
+ },
+ },
+ err = {
+ arg = '-> b}',
+ msg = 'E15: Arrow outside of lambda: %.*s',
+ },
+ }, {
+ hl('Curly', '{'),
+ hl('IdentifierName', 'a#b'),
+ hl('InvalidArrow', '->', 1),
+ hl('IdentifierName', 'b', 1),
+ hl('Curly', '}'),
+ })
+ check_parsing('{a : b : c}', {
+ -- 01234567890
+ -- 0 1
+ ast = {
+ {
+ fmtn('DictLiteral', '-di', ':0:0:{'),
+ children = {
+ {
+ 'Colon:0:2: :',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:1:a',
+ {
+ 'Colon:0:6: :',
+ children = {
+ 'PlainIdentifier(scope=0,ident=b):0:4: b',
+ 'PlainIdentifier(scope=0,ident=c):0:8: c',
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ err = {
+ arg = ': c}',
+ msg = 'E15: Colon outside of dictionary or ternary operator: %.*s',
+ },
+ }, {
+ hl('Dict', '{'),
+ hl('IdentifierName', 'a'),
+ hl('Colon', ':', 1),
+ hl('IdentifierName', 'b', 1),
+ hl('InvalidColon', ':', 1),
+ hl('IdentifierName', 'c', 1),
+ hl('Dict', '}'),
+ })
+ check_parsing('{', {
+ -- 0
+ ast = {
+ fmtn('UnknownFigure', '\\di', ':0:0:{'),
+ },
+ err = {
+ arg = '{',
+ msg = 'E15: Missing closing figure brace: %.*s',
+ },
+ }, {
+ hl('FigureBrace', '{'),
+ })
+ check_parsing('{a', {
+ -- 01
+ ast = {
+ {
+ fmtn('UnknownFigure', '\\di', ':0:0:{'),
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:1:a',
+ },
+ },
+ },
+ err = {
+ arg = '{a',
+ msg = 'E15: Missing closing figure brace: %.*s',
+ },
+ }, {
+ hl('FigureBrace', '{'),
+ hl('IdentifierName', 'a'),
+ })
+ check_parsing('{a,b', {
+ -- 0123
+ ast = {
+ {
+ fmtn('Lambda', '\\di', ':0:0:{'),
+ children = {
+ {
+ 'Comma:0:2:,',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:1:a',
+ 'PlainIdentifier(scope=0,ident=b):0:3:b',
+ },
+ },
+ },
+ },
+ },
+ err = {
+ arg = '{a,b',
+ msg = 'E15: Missing closing figure brace for lambda: %.*s',
+ },
+ }, {
+ hl('Lambda', '{'),
+ hl('IdentifierName', 'a'),
+ hl('Comma', ','),
+ hl('IdentifierName', 'b'),
+ })
+ check_parsing('{a,b->', {
+ -- 012345
+ ast = {
+ {
+ fmtn('Lambda', '\\di', ':0:0:{'),
+ children = {
+ {
+ 'Comma:0:2:,',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:1:a',
+ 'PlainIdentifier(scope=0,ident=b):0:3:b',
+ },
+ },
+ 'Arrow:0:4:->',
+ },
+ },
+ },
+ err = {
+ arg = '',
+ msg = 'E15: Expected value, got EOC: %.*s',
+ },
+ }, {
+ hl('Lambda', '{'),
+ hl('IdentifierName', 'a'),
+ hl('Comma', ','),
+ hl('IdentifierName', 'b'),
+ hl('Arrow', '->'),
+ })
+ check_parsing('{a,b->c', {
+ -- 0123456
+ ast = {
+ {
+ fmtn('Lambda', '\\di', ':0:0:{'),
+ children = {
+ {
+ 'Comma:0:2:,',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:1:a',
+ 'PlainIdentifier(scope=0,ident=b):0:3:b',
+ },
+ },
+ {
+ 'Arrow:0:4:->',
+ children = {
+ 'PlainIdentifier(scope=0,ident=c):0:6:c',
+ },
+ },
+ },
+ },
+ },
+ err = {
+ arg = '{a,b->c',
+ msg = 'E15: Missing closing figure brace for lambda: %.*s',
+ },
+ }, {
+ hl('Lambda', '{'),
+ hl('IdentifierName', 'a'),
+ hl('Comma', ','),
+ hl('IdentifierName', 'b'),
+ hl('Arrow', '->'),
+ hl('IdentifierName', 'c'),
+ })
+ check_parsing('{a : b', {
+ -- 012345
+ ast = {
+ {
+ fmtn('DictLiteral', '-di', ':0:0:{'),
+ children = {
+ {
+ 'Colon:0:2: :',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:1:a',
+ 'PlainIdentifier(scope=0,ident=b):0:4: b',
+ },
+ },
+ },
+ },
+ },
+ err = {
+ arg = '{a : b',
+ msg = 'E723: Missing end of Dictionary \'}\': %.*s',
+ },
+ }, {
+ hl('Dict', '{'),
+ hl('IdentifierName', 'a'),
+ hl('Colon', ':', 1),
+ hl('IdentifierName', 'b', 1),
+ })
+ check_parsing('{a : b,', {
+ -- 0123456
+ ast = {
+ {
+ fmtn('DictLiteral', '-di', ':0:0:{'),
+ children = {
+ {
+ 'Comma:0:6:,',
+ children = {
+ {
+ 'Colon:0:2: :',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:1:a',
+ 'PlainIdentifier(scope=0,ident=b):0:4: b',
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ err = {
+ arg = '',
+ msg = 'E15: Expected value, got EOC: %.*s',
+ },
+ }, {
+ hl('Dict', '{'),
+ hl('IdentifierName', 'a'),
+ hl('Colon', ':', 1),
+ hl('IdentifierName', 'b', 1),
+ hl('Comma', ','),
+ })
+ end)
+ itp('works with ternary operator', function()
+ check_parsing('a ? b : c', {
+ -- 012345678
+ ast = {
+ {
+ 'Ternary:0:1: ?',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:0:a',
+ {
+ 'TernaryValue:0:5: :',
+ children = {
+ 'PlainIdentifier(scope=0,ident=b):0:3: b',
+ 'PlainIdentifier(scope=0,ident=c):0:7: c',
+ },
+ },
+ },
+ },
+ },
+ }, {
+ hl('IdentifierName', 'a'),
+ hl('Ternary', '?', 1),
+ hl('IdentifierName', 'b', 1),
+ hl('TernaryColon', ':', 1),
+ hl('IdentifierName', 'c', 1),
+ })
+ check_parsing('@a?@b?@c:@d:@e', {
+ -- 01234567890123
+ -- 0 1
+ ast = {
+ {
+ 'Ternary:0:2:?',
+ children = {
+ 'Register(name=a):0:0:@a',
+ {
+ 'TernaryValue:0:11::',
+ children = {
+ {
+ 'Ternary:0:5:?',
+ children = {
+ 'Register(name=b):0:3:@b',
+ {
+ 'TernaryValue:0:8::',
+ children = {
+ 'Register(name=c):0:6:@c',
+ 'Register(name=d):0:9:@d',
+ },
+ },
+ },
+ },
+ 'Register(name=e):0:12:@e',
+ },
+ },
+ },
+ },
+ },
+ }, {
+ hl('Register', '@a'),
+ hl('Ternary', '?'),
+ hl('Register', '@b'),
+ hl('Ternary', '?'),
+ hl('Register', '@c'),
+ hl('TernaryColon', ':'),
+ hl('Register', '@d'),
+ hl('TernaryColon', ':'),
+ hl('Register', '@e'),
+ })
+ check_parsing('@a?@b:@c?@d:@e', {
+ -- 01234567890123
+ -- 0 1
+ ast = {
+ {
+ 'Ternary:0:2:?',
+ children = {
+ 'Register(name=a):0:0:@a',
+ {
+ 'TernaryValue:0:5::',
+ children = {
+ 'Register(name=b):0:3:@b',
+ {
+ 'Ternary:0:8:?',
+ children = {
+ 'Register(name=c):0:6:@c',
+ {
+ 'TernaryValue:0:11::',
+ children = {
+ 'Register(name=d):0:9:@d',
+ 'Register(name=e):0:12:@e',
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ }, {
+ hl('Register', '@a'),
+ hl('Ternary', '?'),
+ hl('Register', '@b'),
+ hl('TernaryColon', ':'),
+ hl('Register', '@c'),
+ hl('Ternary', '?'),
+ hl('Register', '@d'),
+ hl('TernaryColon', ':'),
+ hl('Register', '@e'),
+ })
+ check_parsing('@a?@b?@c?@d:@e?@f:@g:@h?@i:@j:@k', {
+ -- 01234567890123456789012345678901
+ -- 0 1 2 3
+ ast = {
+ {
+ 'Ternary:0:2:?',
+ children = {
+ 'Register(name=a):0:0:@a',
+ {
+ 'TernaryValue:0:29::',
+ children = {
+ {
+ 'Ternary:0:5:?',
+ children = {
+ 'Register(name=b):0:3:@b',
+ {
+ 'TernaryValue:0:20::',
+ children = {
+ {
+ 'Ternary:0:8:?',
+ children = {
+ 'Register(name=c):0:6:@c',
+ {
+ 'TernaryValue:0:11::',
+ children = {
+ 'Register(name=d):0:9:@d',
+ {
+ 'Ternary:0:14:?',
+ children = {
+ 'Register(name=e):0:12:@e',
+ {
+ 'TernaryValue:0:17::',
+ children = {
+ 'Register(name=f):0:15:@f',
+ 'Register(name=g):0:18:@g',
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ {
+ 'Ternary:0:23:?',
+ children = {
+ 'Register(name=h):0:21:@h',
+ {
+ 'TernaryValue:0:26::',
+ children = {
+ 'Register(name=i):0:24:@i',
+ 'Register(name=j):0:27:@j',
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ 'Register(name=k):0:30:@k',
+ },
+ },
+ },
+ },
+ },
+ }, {
+ hl('Register', '@a'),
+ hl('Ternary', '?'),
+ hl('Register', '@b'),
+ hl('Ternary', '?'),
+ hl('Register', '@c'),
+ hl('Ternary', '?'),
+ hl('Register', '@d'),
+ hl('TernaryColon', ':'),
+ hl('Register', '@e'),
+ hl('Ternary', '?'),
+ hl('Register', '@f'),
+ hl('TernaryColon', ':'),
+ hl('Register', '@g'),
+ hl('TernaryColon', ':'),
+ hl('Register', '@h'),
+ hl('Ternary', '?'),
+ hl('Register', '@i'),
+ hl('TernaryColon', ':'),
+ hl('Register', '@j'),
+ hl('TernaryColon', ':'),
+ hl('Register', '@k'),
+ })
+ check_parsing('?', {
+ -- 0
+ ast = {
+ {
+ 'Ternary:0:0:?',
+ children = {
+ 'Missing:0:0:',
+ 'TernaryValue:0:0:?',
+ },
+ },
+ },
+ err = {
+ arg = '?',
+ msg = 'E15: Expected value, got question mark: %.*s',
+ },
+ }, {
+ hl('InvalidTernary', '?'),
+ })
+
+ check_parsing('?:', {
+ -- 01
+ ast = {
+ {
+ 'Ternary:0:0:?',
+ children = {
+ 'Missing:0:0:',
+ {
+ 'TernaryValue:0:1::',
+ children = {
+ 'Missing:0:1:',
+ },
+ },
+ },
+ },
+ },
+ err = {
+ arg = '?:',
+ msg = 'E15: Expected value, got question mark: %.*s',
+ },
+ }, {
+ hl('InvalidTernary', '?'),
+ hl('InvalidTernaryColon', ':'),
+ })
+
+ check_parsing('?::', {
+ -- 012
+ ast = {
+ {
+ 'Colon:0:2::',
+ children = {
+ {
+ 'Ternary:0:0:?',
+ children = {
+ 'Missing:0:0:',
+ {
+ 'TernaryValue:0:1::',
+ children = {
+ 'Missing:0:1:',
+ 'Missing:0:2:',
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ err = {
+ arg = '?::',
+ msg = 'E15: Expected value, got question mark: %.*s',
+ },
+ }, {
+ hl('InvalidTernary', '?'),
+ hl('InvalidTernaryColon', ':'),
+ hl('InvalidColon', ':'),
+ })
+
+ check_parsing('a?b', {
+ -- 012
+ ast = {
+ {
+ 'Ternary:0:1:?',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:0:a',
+ {
+ 'TernaryValue:0:1:?',
+ children = {
+ 'PlainIdentifier(scope=0,ident=b):0:2:b',
+ },
+ },
+ },
+ },
+ },
+ err = {
+ arg = '?b',
+ msg = 'E109: Missing \':\' after \'?\': %.*s',
+ },
+ }, {
+ hl('IdentifierName', 'a'),
+ hl('Ternary', '?'),
+ hl('IdentifierName', 'b'),
+ })
+ check_parsing('a?b:', {
+ -- 0123
+ ast = {
+ {
+ 'Ternary:0:1:?',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:0:a',
+ {
+ 'TernaryValue:0:1:?',
+ children = {
+ 'PlainIdentifier(scope=b,ident=):0:2:b:',
+ },
+ },
+ },
+ },
+ },
+ err = {
+ arg = '?b:',
+ msg = 'E109: Missing \':\' after \'?\': %.*s',
+ },
+ }, {
+ hl('IdentifierName', 'a'),
+ hl('Ternary', '?'),
+ hl('IdentifierScope', 'b'),
+ hl('IdentifierScopeDelimiter', ':'),
+ })
+
+ check_parsing('a?b::c', {
+ -- 012345
+ ast = {
+ {
+ 'Ternary:0:1:?',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:0:a',
+ {
+ 'TernaryValue:0:4::',
+ children = {
+ 'PlainIdentifier(scope=b,ident=):0:2:b:',
+ 'PlainIdentifier(scope=0,ident=c):0:5:c',
+ },
+ },
+ },
+ },
+ },
+ }, {
+ hl('IdentifierName', 'a'),
+ hl('Ternary', '?'),
+ hl('IdentifierScope', 'b'),
+ hl('IdentifierScopeDelimiter', ':'),
+ hl('TernaryColon', ':'),
+ hl('IdentifierName', 'c'),
+ })
+
+ check_parsing('a?b :', {
+ -- 01234
+ ast = {
+ {
+ 'Ternary:0:1:?',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:0:a',
+ {
+ 'TernaryValue:0:3: :',
+ children = {
+ 'PlainIdentifier(scope=0,ident=b):0:2:b',
+ },
+ },
+ },
+ },
+ },
+ err = {
+ arg = '',
+ msg = 'E15: Expected value, got EOC: %.*s',
+ },
+ }, {
+ hl('IdentifierName', 'a'),
+ hl('Ternary', '?'),
+ hl('IdentifierName', 'b'),
+ hl('TernaryColon', ':', 1),
+ })
+
+ check_parsing('(@a?@b:@c)?@d:@e', {
+ -- 0123456789012345
+ -- 0 1
+ ast = {
+ {
+ 'Ternary:0:10:?',
+ children = {
+ {
+ 'Nested:0:0:(',
+ children = {
+ {
+ 'Ternary:0:3:?',
+ children = {
+ 'Register(name=a):0:1:@a',
+ {
+ 'TernaryValue:0:6::',
+ children = {
+ 'Register(name=b):0:4:@b',
+ 'Register(name=c):0:7:@c',
+ },
+ },
+ },
+ },
+ },
+ },
+ {
+ 'TernaryValue:0:13::',
+ children = {
+ 'Register(name=d):0:11:@d',
+ 'Register(name=e):0:14:@e',
+ },
+ },
+ },
+ },
+ },
+ }, {
+ hl('NestingParenthesis', '('),
+ hl('Register', '@a'),
+ hl('Ternary', '?'),
+ hl('Register', '@b'),
+ hl('TernaryColon', ':'),
+ hl('Register', '@c'),
+ hl('NestingParenthesis', ')'),
+ hl('Ternary', '?'),
+ hl('Register', '@d'),
+ hl('TernaryColon', ':'),
+ hl('Register', '@e'),
+ })
+
+ check_parsing('(@a?@b:@c)?(@d?@e:@f):(@g?@h:@i)', {
+ -- 01234567890123456789012345678901
+ -- 0 1 2 3
+ ast = {
+ {
+ 'Ternary:0:10:?',
+ children = {
+ {
+ 'Nested:0:0:(',
+ children = {
+ {
+ 'Ternary:0:3:?',
+ children = {
+ 'Register(name=a):0:1:@a',
+ {
+ 'TernaryValue:0:6::',
+ children = {
+ 'Register(name=b):0:4:@b',
+ 'Register(name=c):0:7:@c',
+ },
+ },
+ },
+ },
+ },
+ },
+ {
+ 'TernaryValue:0:21::',
+ children = {
+ {
+ 'Nested:0:11:(',
+ children = {
+ {
+ 'Ternary:0:14:?',
+ children = {
+ 'Register(name=d):0:12:@d',
+ {
+ 'TernaryValue:0:17::',
+ children = {
+ 'Register(name=e):0:15:@e',
+ 'Register(name=f):0:18:@f',
+ },
+ },
+ },
+ },
+ },
+ },
+ {
+ 'Nested:0:22:(',
+ children = {
+ {
+ 'Ternary:0:25:?',
+ children = {
+ 'Register(name=g):0:23:@g',
+ {
+ 'TernaryValue:0:28::',
+ children = {
+ 'Register(name=h):0:26:@h',
+ 'Register(name=i):0:29:@i',
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ }, {
+ hl('NestingParenthesis', '('),
+ hl('Register', '@a'),
+ hl('Ternary', '?'),
+ hl('Register', '@b'),
+ hl('TernaryColon', ':'),
+ hl('Register', '@c'),
+ hl('NestingParenthesis', ')'),
+ hl('Ternary', '?'),
+ hl('NestingParenthesis', '('),
+ hl('Register', '@d'),
+ hl('Ternary', '?'),
+ hl('Register', '@e'),
+ hl('TernaryColon', ':'),
+ hl('Register', '@f'),
+ hl('NestingParenthesis', ')'),
+ hl('TernaryColon', ':'),
+ hl('NestingParenthesis', '('),
+ hl('Register', '@g'),
+ hl('Ternary', '?'),
+ hl('Register', '@h'),
+ hl('TernaryColon', ':'),
+ hl('Register', '@i'),
+ hl('NestingParenthesis', ')'),
+ })
+
+ check_parsing('(@a?@b:@c)?@d?@e:@f:@g?@h:@i', {
+ -- 0123456789012345678901234567
+ -- 0 1 2
+ ast = {
+ {
+ 'Ternary:0:10:?',
+ children = {
+ {
+ 'Nested:0:0:(',
+ children = {
+ {
+ 'Ternary:0:3:?',
+ children = {
+ 'Register(name=a):0:1:@a',
+ {
+ 'TernaryValue:0:6::',
+ children = {
+ 'Register(name=b):0:4:@b',
+ 'Register(name=c):0:7:@c',
+ },
+ },
+ },
+ },
+ },
+ },
+ {
+ 'TernaryValue:0:19::',
+ children = {
+ {
+ 'Ternary:0:13:?',
+ children = {
+ 'Register(name=d):0:11:@d',
+ {
+ 'TernaryValue:0:16::',
+ children = {
+ 'Register(name=e):0:14:@e',
+ 'Register(name=f):0:17:@f',
+ },
+ },
+ },
+ },
+ {
+ 'Ternary:0:22:?',
+ children = {
+ 'Register(name=g):0:20:@g',
+ {
+ 'TernaryValue:0:25::',
+ children = {
+ 'Register(name=h):0:23:@h',
+ 'Register(name=i):0:26:@i',
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ }, {
+ hl('NestingParenthesis', '('),
+ hl('Register', '@a'),
+ hl('Ternary', '?'),
+ hl('Register', '@b'),
+ hl('TernaryColon', ':'),
+ hl('Register', '@c'),
+ hl('NestingParenthesis', ')'),
+ hl('Ternary', '?'),
+ hl('Register', '@d'),
+ hl('Ternary', '?'),
+ hl('Register', '@e'),
+ hl('TernaryColon', ':'),
+ hl('Register', '@f'),
+ hl('TernaryColon', ':'),
+ hl('Register', '@g'),
+ hl('Ternary', '?'),
+ hl('Register', '@h'),
+ hl('TernaryColon', ':'),
+ hl('Register', '@i'),
+ })
+ check_parsing('a?b{cdef}g:h', {
+ -- 012345678901
+ -- 0 1
+ ast = {
+ {
+ 'Ternary:0:1:?',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:0:a',
+ {
+ 'TernaryValue:0:10::',
+ children = {
+ {
+ 'ComplexIdentifier:0:3:',
+ children = {
+ 'PlainIdentifier(scope=0,ident=b):0:2:b',
+ {
+ 'ComplexIdentifier:0:9:',
+ children = {
+ {
+ fmtn('CurlyBracesIdentifier', '--i', ':0:3:{'),
+ children = {
+ 'PlainIdentifier(scope=0,ident=cdef):0:4:cdef',
+ },
+ },
+ 'PlainIdentifier(scope=0,ident=g):0:9:g',
+ },
+ },
+ },
+ },
+ 'PlainIdentifier(scope=0,ident=h):0:11:h',
+ },
+ },
+ },
+ },
+ },
+ }, {
+ hl('IdentifierName', 'a'),
+ hl('Ternary', '?'),
+ hl('IdentifierName', 'b'),
+ hl('Curly', '{'),
+ hl('IdentifierName', 'cdef'),
+ hl('Curly', '}'),
+ hl('IdentifierName', 'g'),
+ hl('TernaryColon', ':'),
+ hl('IdentifierName', 'h'),
+ })
+ check_parsing('a ? b : c : d', {
+ -- 0123456789012
+ -- 0 1
+ ast = {
+ {
+ 'Colon:0:9: :',
+ children = {
+ {
+ 'Ternary:0:1: ?',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:0:a',
+ {
+ 'TernaryValue:0:5: :',
+ children = {
+ 'PlainIdentifier(scope=0,ident=b):0:3: b',
+ 'PlainIdentifier(scope=0,ident=c):0:7: c',
+ },
+ },
+ },
+ },
+ 'PlainIdentifier(scope=0,ident=d):0:11: d',
+ },
+ },
+ },
+ err = {
+ arg = ': d',
+ msg = 'E15: Colon outside of dictionary or ternary operator: %.*s',
+ },
+ }, {
+ hl('IdentifierName', 'a'),
+ hl('Ternary', '?', 1),
+ hl('IdentifierName', 'b', 1),
+ hl('TernaryColon', ':', 1),
+ hl('IdentifierName', 'c', 1),
+ hl('InvalidColon', ':', 1),
+ hl('IdentifierName', 'd', 1),
+ })
+ end)
+ itp('works with comparison operators', function()
+ check_parsing('a == b', {
+ -- 012345
+ ast = {
+ {
+ 'Comparison(type=Equal,inv=0,ccs=UseOption):0:1: ==',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:0:a',
+ 'PlainIdentifier(scope=0,ident=b):0:4: b',
+ },
+ },
+ },
+ }, {
+ hl('IdentifierName', 'a'),
+ hl('Comparison', '==', 1),
+ hl('IdentifierName', 'b', 1),
+ })
+
+ check_parsing('a ==? b', {
+ -- 0123456
+ ast = {
+ {
+ 'Comparison(type=Equal,inv=0,ccs=IgnoreCase):0:1: ==?',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:0:a',
+ 'PlainIdentifier(scope=0,ident=b):0:5: b',
+ },
+ },
+ },
+ }, {
+ hl('IdentifierName', 'a'),
+ hl('Comparison', '==', 1),
+ hl('ComparisonModifier', '?'),
+ hl('IdentifierName', 'b', 1),
+ })
+
+ check_parsing('a ==# b', {
+ -- 0123456
+ ast = {
+ {
+ 'Comparison(type=Equal,inv=0,ccs=MatchCase):0:1: ==#',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:0:a',
+ 'PlainIdentifier(scope=0,ident=b):0:5: b',
+ },
+ },
+ },
+ }, {
+ hl('IdentifierName', 'a'),
+ hl('Comparison', '==', 1),
+ hl('ComparisonModifier', '#'),
+ hl('IdentifierName', 'b', 1),
+ })
+
+ check_parsing('a !=# b', {
+ -- 0123456
+ ast = {
+ {
+ 'Comparison(type=Equal,inv=1,ccs=MatchCase):0:1: !=#',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:0:a',
+ 'PlainIdentifier(scope=0,ident=b):0:5: b',
+ },
+ },
+ },
+ }, {
+ hl('IdentifierName', 'a'),
+ hl('Comparison', '!=', 1),
+ hl('ComparisonModifier', '#'),
+ hl('IdentifierName', 'b', 1),
+ })
+
+ check_parsing('a <=# b', {
+ -- 0123456
+ ast = {
+ {
+ 'Comparison(type=Greater,inv=1,ccs=MatchCase):0:1: <=#',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:0:a',
+ 'PlainIdentifier(scope=0,ident=b):0:5: b',
+ },
+ },
+ },
+ }, {
+ hl('IdentifierName', 'a'),
+ hl('Comparison', '<=', 1),
+ hl('ComparisonModifier', '#'),
+ hl('IdentifierName', 'b', 1),
+ })
+
+ check_parsing('a >=# b', {
+ -- 0123456
+ ast = {
+ {
+ 'Comparison(type=GreaterOrEqual,inv=0,ccs=MatchCase):0:1: >=#',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:0:a',
+ 'PlainIdentifier(scope=0,ident=b):0:5: b',
+ },
+ },
+ },
+ }, {
+ hl('IdentifierName', 'a'),
+ hl('Comparison', '>=', 1),
+ hl('ComparisonModifier', '#'),
+ hl('IdentifierName', 'b', 1),
+ })
+
+ check_parsing('a ># b', {
+ -- 012345
+ ast = {
+ {
+ 'Comparison(type=Greater,inv=0,ccs=MatchCase):0:1: >#',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:0:a',
+ 'PlainIdentifier(scope=0,ident=b):0:4: b',
+ },
+ },
+ },
+ }, {
+ hl('IdentifierName', 'a'),
+ hl('Comparison', '>', 1),
+ hl('ComparisonModifier', '#'),
+ hl('IdentifierName', 'b', 1),
+ })
+
+ check_parsing('a <# b', {
+ -- 012345
+ ast = {
+ {
+ 'Comparison(type=GreaterOrEqual,inv=1,ccs=MatchCase):0:1: <#',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:0:a',
+ 'PlainIdentifier(scope=0,ident=b):0:4: b',
+ },
+ },
+ },
+ }, {
+ hl('IdentifierName', 'a'),
+ hl('Comparison', '<', 1),
+ hl('ComparisonModifier', '#'),
+ hl('IdentifierName', 'b', 1),
+ })
+
+ check_parsing('a is#b', {
+ -- 012345
+ ast = {
+ {
+ 'Comparison(type=Identical,inv=0,ccs=MatchCase):0:1: is#',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:0:a',
+ 'PlainIdentifier(scope=0,ident=b):0:5:b',
+ },
+ },
+ },
+ }, {
+ hl('IdentifierName', 'a'),
+ hl('Comparison', 'is', 1),
+ hl('ComparisonModifier', '#'),
+ hl('IdentifierName', 'b'),
+ })
+
+ check_parsing('a is?b', {
+ -- 012345
+ ast = {
+ {
+ 'Comparison(type=Identical,inv=0,ccs=IgnoreCase):0:1: is?',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:0:a',
+ 'PlainIdentifier(scope=0,ident=b):0:5:b',
+ },
+ },
+ },
+ }, {
+ hl('IdentifierName', 'a'),
+ hl('Comparison', 'is', 1),
+ hl('ComparisonModifier', '?'),
+ hl('IdentifierName', 'b'),
+ })
+
+ check_parsing('a isnot b', {
+ -- 012345678
+ ast = {
+ {
+ 'Comparison(type=Identical,inv=1,ccs=UseOption):0:1: isnot',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:0:a',
+ 'PlainIdentifier(scope=0,ident=b):0:7: b',
+ },
+ },
+ },
+ }, {
+ hl('IdentifierName', 'a'),
+ hl('Comparison', 'isnot', 1),
+ hl('IdentifierName', 'b', 1),
+ })
+
+ check_parsing('a < b < c', {
+ -- 012345678
+ ast = {
+ {
+ 'Comparison(type=GreaterOrEqual,inv=1,ccs=UseOption):0:1: <',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:0:a',
+ {
+ 'Comparison(type=GreaterOrEqual,inv=1,ccs=UseOption):0:5: <',
+ children = {
+ 'PlainIdentifier(scope=0,ident=b):0:3: b',
+ 'PlainIdentifier(scope=0,ident=c):0:7: c',
+ },
+ },
+ },
+ },
+ },
+ err = {
+ arg = ' < c',
+ msg = 'E15: Operator is not associative: %.*s',
+ },
+ }, {
+ hl('IdentifierName', 'a'),
+ hl('Comparison', '<', 1),
+ hl('IdentifierName', 'b', 1),
+ hl('InvalidComparison', '<', 1),
+ hl('IdentifierName', 'c', 1),
+ })
+
+ check_parsing('a < b <# c', {
+ -- 012345678
+ ast = {
+ {
+ 'Comparison(type=GreaterOrEqual,inv=1,ccs=UseOption):0:1: <',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:0:a',
+ {
+ 'Comparison(type=GreaterOrEqual,inv=1,ccs=MatchCase):0:5: <#',
+ children = {
+ 'PlainIdentifier(scope=0,ident=b):0:3: b',
+ 'PlainIdentifier(scope=0,ident=c):0:8: c',
+ },
+ },
+ },
+ },
+ },
+ err = {
+ arg = ' <# c',
+ msg = 'E15: Operator is not associative: %.*s',
+ },
+ }, {
+ hl('IdentifierName', 'a'),
+ hl('Comparison', '<', 1),
+ hl('IdentifierName', 'b', 1),
+ hl('InvalidComparison', '<', 1),
+ hl('InvalidComparisonModifier', '#'),
+ hl('IdentifierName', 'c', 1),
+ })
+
+ check_parsing('a += b', {
+ -- 012345
+ ast = {
+ {
+ 'Assignment(Add):0:1: +=',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:0:a',
+ 'PlainIdentifier(scope=0,ident=b):0:4: b',
+ },
+ },
+ },
+ err = {
+ arg = '+= b',
+ msg = 'E15: Misplaced assignment: %.*s',
+ },
+ }, {
+ hl('IdentifierName', 'a'),
+ hl('InvalidAssignmentWithAddition', '+=', 1),
+ hl('IdentifierName', 'b', 1),
+ })
+ check_parsing('a + b == c + d', {
+ -- 01234567890123
+ -- 0 1
+ ast = {
+ {
+ 'Comparison(type=Equal,inv=0,ccs=UseOption):0:5: ==',
+ children = {
+ {
+ 'BinaryPlus:0:1: +',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:0:a',
+ 'PlainIdentifier(scope=0,ident=b):0:3: b',
+ },
+ },
+ {
+ 'BinaryPlus:0:10: +',
+ children = {
+ 'PlainIdentifier(scope=0,ident=c):0:8: c',
+ 'PlainIdentifier(scope=0,ident=d):0:12: d',
+ },
+ },
+ },
+ },
+ },
+ }, {
+ hl('IdentifierName', 'a'),
+ hl('BinaryPlus', '+', 1),
+ hl('IdentifierName', 'b', 1),
+ hl('Comparison', '==', 1),
+ hl('IdentifierName', 'c', 1),
+ hl('BinaryPlus', '+', 1),
+ hl('IdentifierName', 'd', 1),
+ })
+ check_parsing('+ a == + b', {
+ -- 0123456789
+ ast = {
+ {
+ 'Comparison(type=Equal,inv=0,ccs=UseOption):0:3: ==',
+ children = {
+ {
+ 'UnaryPlus:0:0:+',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:1: a',
+ },
+ },
+ {
+ 'UnaryPlus:0:6: +',
+ children = {
+ 'PlainIdentifier(scope=0,ident=b):0:8: b',
+ },
+ },
+ },
+ },
+ },
+ }, {
+ hl('UnaryPlus', '+'),
+ hl('IdentifierName', 'a', 1),
+ hl('Comparison', '==', 1),
+ hl('UnaryPlus', '+', 1),
+ hl('IdentifierName', 'b', 1),
+ })
+ end)
+ itp('works with concat/subscript', function()
+ check_parsing('.', {
+ -- 0
+ ast = {
+ {
+ 'ConcatOrSubscript:0:0:.',
+ children = {
+ 'Missing:0:0:',
+ },
+ },
+ },
+ err = {
+ arg = '.',
+ msg = 'E15: Unexpected dot: %.*s',
+ },
+ }, {
+ hl('InvalidConcatOrSubscript', '.'),
+ })
+
+ check_parsing('a.', {
+ -- 01
+ ast = {
+ {
+ 'ConcatOrSubscript:0:1:.',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:0:a',
+ },
+ },
+ },
+ err = {
+ arg = '',
+ msg = 'E15: Expected value, got EOC: %.*s',
+ },
+ }, {
+ hl('IdentifierName', 'a'),
+ hl('ConcatOrSubscript', '.'),
+ })
+
+ check_parsing('a.b', {
+ -- 012
+ ast = {
+ {
+ 'ConcatOrSubscript:0:1:.',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:0:a',
+ 'PlainKey(key=b):0:2:b',
+ },
+ },
+ },
+ }, {
+ hl('IdentifierName', 'a'),
+ hl('ConcatOrSubscript', '.'),
+ hl('IdentifierKey', 'b'),
+ })
+
+ check_parsing('1.2', {
+ -- 012
+ ast = {
+ 'Float(val=1.200000e+00):0:0:1.2',
+ },
+ }, {
+ hl('Float', '1.2'),
+ })
+
+ check_parsing('1.2 + 1.3e-5', {
+ -- 012345678901
+ -- 0 1
+ ast = {
+ {
+ 'BinaryPlus:0:3: +',
+ children = {
+ 'Float(val=1.200000e+00):0:0:1.2',
+ 'Float(val=1.300000e-05):0:5: 1.3e-5',
+ },
+ },
+ },
+ }, {
+ hl('Float', '1.2'),
+ hl('BinaryPlus', '+', 1),
+ hl('Float', '1.3e-5', 1),
+ })
+
+ check_parsing('a . 1.2 + 1.3e-5', {
+ -- 0123456789012345
+ -- 0 1
+ ast = {
+ {
+ 'BinaryPlus:0:7: +',
+ children = {
+ {
+ 'Concat:0:1: .',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:0:a',
+ {
+ 'ConcatOrSubscript:0:5:.',
+ children = {
+ 'Integer(val=1):0:3: 1',
+ 'PlainKey(key=2):0:6:2',
+ },
+ },
+ },
+ },
+ 'Float(val=1.300000e-05):0:9: 1.3e-5',
+ },
+ },
+ },
+ }, {
+ hl('IdentifierName', 'a'),
+ hl('Concat', '.', 1),
+ hl('Number', '1', 1),
+ hl('ConcatOrSubscript', '.'),
+ hl('IdentifierKey', '2'),
+ hl('BinaryPlus', '+', 1),
+ hl('Float', '1.3e-5', 1),
+ })
+
+ check_parsing('1.3e-5 + 1.2 . a', {
+ -- 0123456789012345
+ -- 0 1
+ ast = {
+ {
+ 'Concat:0:12: .',
+ children = {
+ {
+ 'BinaryPlus:0:6: +',
+ children = {
+ 'Float(val=1.300000e-05):0:0:1.3e-5',
+ 'Float(val=1.200000e+00):0:8: 1.2',
+ },
+ },
+ 'PlainIdentifier(scope=0,ident=a):0:14: a',
+ },
+ },
+ },
+ }, {
+ hl('Float', '1.3e-5'),
+ hl('BinaryPlus', '+', 1),
+ hl('Float', '1.2', 1),
+ hl('Concat', '.', 1),
+ hl('IdentifierName', 'a', 1),
+ })
+
+ check_parsing('1.3e-5 + a . 1.2', {
+ -- 0123456789012345
+ -- 0 1
+ ast = {
+ {
+ 'Concat:0:10: .',
+ children = {
+ {
+ 'BinaryPlus:0:6: +',
+ children = {
+ 'Float(val=1.300000e-05):0:0:1.3e-5',
+ 'PlainIdentifier(scope=0,ident=a):0:8: a',
+ },
+ },
+ {
+ 'ConcatOrSubscript:0:14:.',
+ children = {
+ 'Integer(val=1):0:12: 1',
+ 'PlainKey(key=2):0:15:2',
+ },
+ },
+ },
+ },
+ },
+ }, {
+ hl('Float', '1.3e-5'),
+ hl('BinaryPlus', '+', 1),
+ hl('IdentifierName', 'a', 1),
+ hl('Concat', '.', 1),
+ hl('Number', '1', 1),
+ hl('ConcatOrSubscript', '.'),
+ hl('IdentifierKey', '2'),
+ })
+
+ check_parsing('1.2.3', {
+ -- 01234
+ ast = {
+ {
+ 'ConcatOrSubscript:0:3:.',
+ children = {
+ {
+ 'ConcatOrSubscript:0:1:.',
+ children = {
+ 'Integer(val=1):0:0:1',
+ 'PlainKey(key=2):0:2:2',
+ },
+ },
+ 'PlainKey(key=3):0:4:3',
+ },
+ },
+ },
+ }, {
+ hl('Number', '1'),
+ hl('ConcatOrSubscript', '.'),
+ hl('IdentifierKey', '2'),
+ hl('ConcatOrSubscript', '.'),
+ hl('IdentifierKey', '3'),
+ })
+
+ check_parsing('a.1.2', {
+ -- 01234
+ ast = {
+ {
+ 'ConcatOrSubscript:0:3:.',
+ children = {
+ {
+ 'ConcatOrSubscript:0:1:.',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:0:a',
+ 'PlainKey(key=1):0:2:1',
+ },
+ },
+ 'PlainKey(key=2):0:4:2',
+ },
+ },
+ },
+ }, {
+ hl('IdentifierName', 'a'),
+ hl('ConcatOrSubscript', '.'),
+ hl('IdentifierKey', '1'),
+ hl('ConcatOrSubscript', '.'),
+ hl('IdentifierKey', '2'),
+ })
+
+ check_parsing('a . 1.2', {
+ -- 0123456
+ ast = {
+ {
+ 'Concat:0:1: .',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:0:a',
+ {
+ 'ConcatOrSubscript:0:5:.',
+ children = {
+ 'Integer(val=1):0:3: 1',
+ 'PlainKey(key=2):0:6:2',
+ },
+ },
+ },
+ },
+ },
+ }, {
+ hl('IdentifierName', 'a'),
+ hl('Concat', '.', 1),
+ hl('Number', '1', 1),
+ hl('ConcatOrSubscript', '.'),
+ hl('IdentifierKey', '2'),
+ })
+
+ check_parsing('+a . +b', {
+ -- 0123456
+ ast = {
+ {
+ 'Concat:0:2: .',
+ children = {
+ {
+ 'UnaryPlus:0:0:+',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:1:a',
+ },
+ },
+ {
+ 'UnaryPlus:0:4: +',
+ children = {
+ 'PlainIdentifier(scope=0,ident=b):0:6:b',
+ },
+ },
+ },
+ },
+ },
+ }, {
+ hl('UnaryPlus', '+'),
+ hl('IdentifierName', 'a'),
+ hl('Concat', '.', 1),
+ hl('UnaryPlus', '+', 1),
+ hl('IdentifierName', 'b'),
+ })
+
+ check_parsing('a. b', {
+ -- 0123
+ ast = {
+ {
+ 'Concat:0:1:.',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:0:a',
+ 'PlainIdentifier(scope=0,ident=b):0:2: b',
+ },
+ },
+ },
+ }, {
+ hl('IdentifierName', 'a'),
+ hl('ConcatOrSubscript', '.'),
+ hl('IdentifierName', 'b', 1),
+ })
+
+ check_parsing('a. 1', {
+ -- 0123
+ ast = {
+ {
+ 'Concat:0:1:.',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:0:a',
+ 'Integer(val=1):0:2: 1',
+ },
+ },
+ },
+ }, {
+ hl('IdentifierName', 'a'),
+ hl('ConcatOrSubscript', '.'),
+ hl('Number', '1', 1),
+ })
+
+ check_parsing('a[1][2][3[4', {
+ -- 01234567890
+ -- 0 1
+ ast = {
+ {
+ 'Subscript:0:7:[',
+ children = {
+ {
+ 'Subscript:0:4:[',
+ children = {
+ {
+ 'Subscript:0:1:[',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:0:a',
+ 'Integer(val=1):0:2:1',
+ },
+ },
+ 'Integer(val=2):0:5:2',
+ },
+ },
+ {
+ 'Subscript:0:9:[',
+ children = {
+ 'Integer(val=3):0:8:3',
+ 'Integer(val=4):0:10:4',
+ },
+ },
+ },
+ },
+ },
+ }, {
+ hl('IdentifierName', 'a'),
+ hl('SubscriptBracket', '['),
+ hl('Number', '1'),
+ hl('SubscriptBracket', ']'),
+ hl('SubscriptBracket', '['),
+ hl('Number', '2'),
+ hl('SubscriptBracket', ']'),
+ hl('SubscriptBracket', '['),
+ hl('Number', '3'),
+ hl('SubscriptBracket', '['),
+ hl('Number', '4'),
+ })
+ end)
+ itp('works with bracket subscripts', function()
+ check_parsing(':', {
+ -- 0
+ ast = {
+ {
+ 'Colon:0:0::',
+ children = {
+ 'Missing:0:0:',
+ },
+ },
+ },
+ err = {
+ arg = ':',
+ msg = 'E15: Colon outside of dictionary or ternary operator: %.*s',
+ },
+ }, {
+ hl('InvalidColon', ':'),
+ })
+ check_parsing('a[]', {
+ -- 012
+ ast = {
+ {
+ 'Subscript:0:1:[',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:0:a',
+ },
+ },
+ },
+ err = {
+ arg = ']',
+ msg = 'E15: Expected value, got closing bracket: %.*s',
+ },
+ }, {
+ hl('IdentifierName', 'a'),
+ hl('SubscriptBracket', '['),
+ hl('InvalidSubscriptBracket', ']'),
+ })
+ check_parsing('a[b:]', {
+ -- 01234
+ ast = {
+ {
+ 'Subscript:0:1:[',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:0:a',
+ 'PlainIdentifier(scope=b,ident=):0:2:b:',
+ },
+ },
+ },
+ }, {
+ hl('IdentifierName', 'a'),
+ hl('SubscriptBracket', '['),
+ hl('IdentifierScope', 'b'),
+ hl('IdentifierScopeDelimiter', ':'),
+ hl('SubscriptBracket', ']'),
+ })
+
+ check_parsing('a[b:c]', {
+ -- 012345
+ ast = {
+ {
+ 'Subscript:0:1:[',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:0:a',
+ 'PlainIdentifier(scope=b,ident=c):0:2:b:c',
+ },
+ },
+ },
+ }, {
+ hl('IdentifierName', 'a'),
+ hl('SubscriptBracket', '['),
+ hl('IdentifierScope', 'b'),
+ hl('IdentifierScopeDelimiter', ':'),
+ hl('IdentifierName', 'c'),
+ hl('SubscriptBracket', ']'),
+ })
+ check_parsing('a[b : c]', {
+ -- 01234567
+ ast = {
+ {
+ 'Subscript:0:1:[',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:0:a',
+ {
+ 'Colon:0:3: :',
+ children = {
+ 'PlainIdentifier(scope=0,ident=b):0:2:b',
+ 'PlainIdentifier(scope=0,ident=c):0:5: c',
+ },
+ },
+ },
+ },
+ },
+ }, {
+ hl('IdentifierName', 'a'),
+ hl('SubscriptBracket', '['),
+ hl('IdentifierName', 'b'),
+ hl('SubscriptColon', ':', 1),
+ hl('IdentifierName', 'c', 1),
+ hl('SubscriptBracket', ']'),
+ })
+
+ check_parsing('a[: b]', {
+ -- 012345
+ ast = {
+ {
+ 'Subscript:0:1:[',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:0:a',
+ {
+ 'Colon:0:2::',
+ children = {
+ 'Missing:0:2:',
+ 'PlainIdentifier(scope=0,ident=b):0:3: b',
+ },
+ },
+ },
+ },
+ },
+ }, {
+ hl('IdentifierName', 'a'),
+ hl('SubscriptBracket', '['),
+ hl('SubscriptColon', ':'),
+ hl('IdentifierName', 'b', 1),
+ hl('SubscriptBracket', ']'),
+ })
+
+ check_parsing('a[b :]', {
+ -- 012345
+ ast = {
+ {
+ 'Subscript:0:1:[',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:0:a',
+ {
+ 'Colon:0:3: :',
+ children = {
+ 'PlainIdentifier(scope=0,ident=b):0:2:b',
+ },
+ },
+ },
+ },
+ },
+ }, {
+ hl('IdentifierName', 'a'),
+ hl('SubscriptBracket', '['),
+ hl('IdentifierName', 'b'),
+ hl('SubscriptColon', ':', 1),
+ hl('SubscriptBracket', ']'),
+ })
+ check_parsing('a[b][c][d](e)(f)(g)', {
+ -- 0123456789012345678
+ -- 0 1
+ ast = {
+ {
+ 'Call:0:16:(',
+ children = {
+ {
+ 'Call:0:13:(',
+ children = {
+ {
+ 'Call:0:10:(',
+ children = {
+ {
+ 'Subscript:0:7:[',
+ children = {
+ {
+ 'Subscript:0:4:[',
+ children = {
+ {
+ 'Subscript:0:1:[',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:0:a',
+ 'PlainIdentifier(scope=0,ident=b):0:2:b',
+ },
+ },
+ 'PlainIdentifier(scope=0,ident=c):0:5:c',
+ },
+ },
+ 'PlainIdentifier(scope=0,ident=d):0:8:d',
+ },
+ },
+ 'PlainIdentifier(scope=0,ident=e):0:11:e',
+ },
+ },
+ 'PlainIdentifier(scope=0,ident=f):0:14:f',
+ },
+ },
+ 'PlainIdentifier(scope=0,ident=g):0:17:g',
+ },
+ },
+ },
+ }, {
+ hl('IdentifierName', 'a'),
+ hl('SubscriptBracket', '['),
+ hl('IdentifierName', 'b'),
+ hl('SubscriptBracket', ']'),
+ hl('SubscriptBracket', '['),
+ hl('IdentifierName', 'c'),
+ hl('SubscriptBracket', ']'),
+ hl('SubscriptBracket', '['),
+ hl('IdentifierName', 'd'),
+ hl('SubscriptBracket', ']'),
+ hl('CallingParenthesis', '('),
+ hl('IdentifierName', 'e'),
+ hl('CallingParenthesis', ')'),
+ hl('CallingParenthesis', '('),
+ hl('IdentifierName', 'f'),
+ hl('CallingParenthesis', ')'),
+ hl('CallingParenthesis', '('),
+ hl('IdentifierName', 'g'),
+ hl('CallingParenthesis', ')'),
+ })
+ check_parsing('{a}{b}{c}[d][e][f]', {
+ -- 012345678901234567
+ -- 0 1
+ ast = {
+ {
+ 'Subscript:0:15:[',
+ children = {
+ {
+ 'Subscript:0:12:[',
+ children = {
+ {
+ 'Subscript:0:9:[',
+ children = {
+ {
+ 'ComplexIdentifier:0:3:',
+ children = {
+ {
+ fmtn('CurlyBracesIdentifier', '-di', ':0:0:{'),
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:1:a',
+ },
+ },
+ {
+ 'ComplexIdentifier:0:6:',
+ children = {
+ {
+ fmtn('CurlyBracesIdentifier', '--i', ':0:3:{'),
+ children = {
+ 'PlainIdentifier(scope=0,ident=b):0:4:b',
+ },
+ },
+ {
+ fmtn('CurlyBracesIdentifier', '--i', ':0:6:{'),
+ children = {
+ 'PlainIdentifier(scope=0,ident=c):0:7:c',
+ },
+ },
+ },
+ },
+ },
+ },
+ 'PlainIdentifier(scope=0,ident=d):0:10:d',
+ },
+ },
+ 'PlainIdentifier(scope=0,ident=e):0:13:e',
+ },
+ },
+ 'PlainIdentifier(scope=0,ident=f):0:16:f',
+ },
+ },
+ },
+ }, {
+ hl('Curly', '{'),
+ hl('IdentifierName', 'a'),
+ hl('Curly', '}'),
+ hl('Curly', '{'),
+ hl('IdentifierName', 'b'),
+ hl('Curly', '}'),
+ hl('Curly', '{'),
+ hl('IdentifierName', 'c'),
+ hl('Curly', '}'),
+ hl('SubscriptBracket', '['),
+ hl('IdentifierName', 'd'),
+ hl('SubscriptBracket', ']'),
+ hl('SubscriptBracket', '['),
+ hl('IdentifierName', 'e'),
+ hl('SubscriptBracket', ']'),
+ hl('SubscriptBracket', '['),
+ hl('IdentifierName', 'f'),
+ hl('SubscriptBracket', ']'),
+ })
+ end)
+ itp('supports list literals', function()
+ check_parsing('[]', {
+ -- 01
+ ast = {
+ 'ListLiteral:0:0:[',
+ },
+ }, {
+ hl('List', '['),
+ hl('List', ']'),
+ })
+
+ check_parsing('[a]', {
+ -- 012
+ ast = {
+ {
+ 'ListLiteral:0:0:[',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:1:a',
+ },
+ },
+ },
+ }, {
+ hl('List', '['),
+ hl('IdentifierName', 'a'),
+ hl('List', ']'),
+ })
+
+ check_parsing('[a, b]', {
+ -- 012345
+ ast = {
+ {
+ 'ListLiteral:0:0:[',
+ children = {
+ {
+ 'Comma:0:2:,',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:1:a',
+ 'PlainIdentifier(scope=0,ident=b):0:3: b',
+ },
+ },
+ },
+ },
+ },
+ }, {
+ hl('List', '['),
+ hl('IdentifierName', 'a'),
+ hl('Comma', ','),
+ hl('IdentifierName', 'b', 1),
+ hl('List', ']'),
+ })
+
+ check_parsing('[a, b, c]', {
+ -- 012345678
+ ast = {
+ {
+ 'ListLiteral:0:0:[',
+ children = {
+ {
+ 'Comma:0:2:,',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:1:a',
+ {
+ 'Comma:0:5:,',
+ children = {
+ 'PlainIdentifier(scope=0,ident=b):0:3: b',
+ 'PlainIdentifier(scope=0,ident=c):0:6: c',
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ }, {
+ hl('List', '['),
+ hl('IdentifierName', 'a'),
+ hl('Comma', ','),
+ hl('IdentifierName', 'b', 1),
+ hl('Comma', ','),
+ hl('IdentifierName', 'c', 1),
+ hl('List', ']'),
+ })
+
+ check_parsing('[a, b, c, ]', {
+ -- 01234567890
+ -- 0 1
+ ast = {
+ {
+ 'ListLiteral:0:0:[',
+ children = {
+ {
+ 'Comma:0:2:,',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:1:a',
+ {
+ 'Comma:0:5:,',
+ children = {
+ 'PlainIdentifier(scope=0,ident=b):0:3: b',
+ {
+ 'Comma:0:8:,',
+ children = {
+ 'PlainIdentifier(scope=0,ident=c):0:6: c',
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ }, {
+ hl('List', '['),
+ hl('IdentifierName', 'a'),
+ hl('Comma', ','),
+ hl('IdentifierName', 'b', 1),
+ hl('Comma', ','),
+ hl('IdentifierName', 'c', 1),
+ hl('Comma', ','),
+ hl('List', ']', 1),
+ })
+
+ check_parsing('[a : b, c : d]', {
+ -- 01234567890123
+ -- 0 1
+ ast = {
+ {
+ 'ListLiteral:0:0:[',
+ children = {
+ {
+ 'Comma:0:6:,',
+ children = {
+ {
+ 'Colon:0:2: :',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:1:a',
+ 'PlainIdentifier(scope=0,ident=b):0:4: b',
+ },
+ },
+ {
+ 'Colon:0:9: :',
+ children = {
+ 'PlainIdentifier(scope=0,ident=c):0:7: c',
+ 'PlainIdentifier(scope=0,ident=d):0:11: d',
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ err = {
+ arg = ': b, c : d]',
+ msg = 'E15: Colon outside of dictionary or ternary operator: %.*s',
+ },
+ }, {
+ hl('List', '['),
+ hl('IdentifierName', 'a'),
+ hl('InvalidColon', ':', 1),
+ hl('IdentifierName', 'b', 1),
+ hl('Comma', ','),
+ hl('IdentifierName', 'c', 1),
+ hl('InvalidColon', ':', 1),
+ hl('IdentifierName', 'd', 1),
+ hl('List', ']'),
+ })
+
+ check_parsing(']', {
+ -- 0
+ ast = {
+ 'ListLiteral:0:0:',
+ },
+ err = {
+ arg = ']',
+ msg = 'E15: Unexpected closing figure brace: %.*s',
+ },
+ }, {
+ hl('InvalidList', ']'),
+ })
+
+ check_parsing('a]', {
+ -- 01
+ ast = {
+ {
+ 'ListLiteral:0:1:',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:0:a',
+ },
+ },
+ },
+ err = {
+ arg = ']',
+ msg = 'E15: Unexpected closing figure brace: %.*s',
+ },
+ }, {
+ hl('IdentifierName', 'a'),
+ hl('InvalidList', ']'),
+ })
+
+ check_parsing('[] []', {
+ -- 01234
+ ast = {
+ {
+ 'OpMissing:0:2:',
+ children = {
+ 'ListLiteral:0:0:[',
+ 'ListLiteral:0:2: [',
+ },
+ },
+ },
+ err = {
+ arg = '[]',
+ msg = 'E15: Missing operator: %.*s',
+ },
+ }, {
+ hl('List', '['),
+ hl('List', ']'),
+ hl('InvalidSpacing', ' '),
+ hl('List', '['),
+ hl('List', ']'),
+ }, {
+ [1] = {
+ ast = {
+ len = 3,
+ err = REMOVE_THIS,
+ ast = {
+ 'ListLiteral:0:0:[',
+ },
+ },
+ hl_fs = {
+ [3] = REMOVE_THIS,
+ [4] = REMOVE_THIS,
+ [5] = REMOVE_THIS,
+ },
+ },
+ })
+
+ check_parsing('[][]', {
+ -- 0123
+ ast = {
+ {
+ 'Subscript:0:2:[',
+ children = {
+ 'ListLiteral:0:0:[',
+ },
+ },
+ },
+ err = {
+ arg = ']',
+ msg = 'E15: Expected value, got closing bracket: %.*s',
+ },
+ }, {
+ hl('List', '['),
+ hl('List', ']'),
+ hl('SubscriptBracket', '['),
+ hl('InvalidSubscriptBracket', ']'),
+ })
+
+ check_parsing('[', {
+ -- 0
+ ast = {
+ 'ListLiteral:0:0:[',
+ },
+ err = {
+ arg = '',
+ msg = 'E15: Expected value, got EOC: %.*s',
+ },
+ }, {
+ hl('List', '['),
+ })
+
+ check_parsing('[1', {
+ -- 01
+ ast = {
+ {
+ 'ListLiteral:0:0:[',
+ children = {
+ 'Integer(val=1):0:1:1',
+ },
+ },
+ },
+ err = {
+ arg = '[1',
+ msg = 'E697: Missing end of List \']\': %.*s',
+ },
+ }, {
+ hl('List', '['),
+ hl('Number', '1'),
+ })
+ end)
+ itp('works with strings', function()
+ check_parsing('\'abc\'', {
+ -- 01234
+ ast = {
+ fmtn('SingleQuotedString', 'val="abc"', ':0:0:\'abc\''),
+ },
+ }, {
+ hl('SingleQuote', '\''),
+ hl('SingleQuotedBody', 'abc'),
+ hl('SingleQuote', '\''),
+ })
+ check_parsing('"abc"', {
+ -- 01234
+ ast = {
+ fmtn('DoubleQuotedString', 'val="abc"', ':0:0:"abc"'),
+ },
+ }, {
+ hl('DoubleQuote', '"'),
+ hl('DoubleQuotedBody', 'abc'),
+ hl('DoubleQuote', '"'),
+ })
+ check_parsing('\'\'', {
+ -- 01
+ ast = {
+ fmtn('SingleQuotedString', 'val=NULL', ':0:0:\'\''),
+ },
+ }, {
+ hl('SingleQuote', '\''),
+ hl('SingleQuote', '\''),
+ })
+ check_parsing('""', {
+ -- 01
+ ast = {
+ fmtn('DoubleQuotedString', 'val=NULL', ':0:0:""'),
+ },
+ }, {
+ hl('DoubleQuote', '"'),
+ hl('DoubleQuote', '"'),
+ })
+ check_parsing('"', {
+ -- 0
+ ast = {
+ fmtn('DoubleQuotedString', 'val=NULL', ':0:0:"'),
+ },
+ err = {
+ arg = '"',
+ msg = 'E114: Missing double quote: %.*s',
+ },
+ }, {
+ hl('InvalidDoubleQuote', '"'),
+ })
+ check_parsing('\'', {
+ -- 0
+ ast = {
+ fmtn('SingleQuotedString', 'val=NULL', ':0:0:\''),
+ },
+ err = {
+ arg = '\'',
+ msg = 'E115: Missing single quote: %.*s',
+ },
+ }, {
+ hl('InvalidSingleQuote', '\''),
+ })
+ check_parsing('"a', {
+ -- 01
+ ast = {
+ fmtn('DoubleQuotedString', 'val="a"', ':0:0:"a'),
+ },
+ err = {
+ arg = '"a',
+ msg = 'E114: Missing double quote: %.*s',
+ },
+ }, {
+ hl('InvalidDoubleQuote', '"'),
+ hl('InvalidDoubleQuotedBody', 'a'),
+ })
+ check_parsing('\'a', {
+ -- 01
+ ast = {
+ fmtn('SingleQuotedString', 'val="a"', ':0:0:\'a'),
+ },
+ err = {
+ arg = '\'a',
+ msg = 'E115: Missing single quote: %.*s',
+ },
+ }, {
+ hl('InvalidSingleQuote', '\''),
+ hl('InvalidSingleQuotedBody', 'a'),
+ })
+ check_parsing('\'abc\'\'def\'', {
+ -- 0123456789
+ ast = {
+ fmtn('SingleQuotedString', 'val="abc\'def"', ':0:0:\'abc\'\'def\''),
+ },
+ }, {
+ hl('SingleQuote', '\''),
+ hl('SingleQuotedBody', 'abc'),
+ hl('SingleQuotedQuote', '\'\''),
+ hl('SingleQuotedBody', 'def'),
+ hl('SingleQuote', '\''),
+ })
+ check_parsing('\'abc\'\'', {
+ -- 012345
+ ast = {
+ fmtn('SingleQuotedString', 'val="abc\'"', ':0:0:\'abc\'\''),
+ },
+ err = {
+ arg = '\'abc\'\'',
+ msg = 'E115: Missing single quote: %.*s',
+ },
+ }, {
+ hl('InvalidSingleQuote', '\''),
+ hl('InvalidSingleQuotedBody', 'abc'),
+ hl('InvalidSingleQuotedQuote', '\'\''),
+ })
+ check_parsing('\'\'\'\'\'\'\'\'', {
+ -- 01234567
+ ast = {
+ fmtn('SingleQuotedString', 'val="\'\'\'"', ':0:0:\'\'\'\'\'\'\'\''),
+ },
+ }, {
+ hl('SingleQuote', '\''),
+ hl('SingleQuotedQuote', '\'\''),
+ hl('SingleQuotedQuote', '\'\''),
+ hl('SingleQuotedQuote', '\'\''),
+ hl('SingleQuote', '\''),
+ })
+ check_parsing('\'\'\'a\'\'\'\'bc\'', {
+ -- 01234567890
+ -- 0 1
+ ast = {
+ fmtn('SingleQuotedString', 'val="\'a\'\'bc"', ':0:0:\'\'\'a\'\'\'\'bc\''),
+ },
+ }, {
+ hl('SingleQuote', '\''),
+ hl('SingleQuotedQuote', '\'\''),
+ hl('SingleQuotedBody', 'a'),
+ hl('SingleQuotedQuote', '\'\''),
+ hl('SingleQuotedQuote', '\'\''),
+ hl('SingleQuotedBody', 'bc'),
+ hl('SingleQuote', '\''),
+ })
+ check_parsing('"\\"\\"\\"\\""', {
+ -- 0123456789
+ ast = {
+ fmtn('DoubleQuotedString', 'val="\\"\\"\\"\\""', ':0:0:"\\"\\"\\"\\""'),
+ },
+ }, {
+ hl('DoubleQuote', '"'),
+ hl('DoubleQuotedEscape', '\\"'),
+ hl('DoubleQuotedEscape', '\\"'),
+ hl('DoubleQuotedEscape', '\\"'),
+ hl('DoubleQuotedEscape', '\\"'),
+ hl('DoubleQuote', '"'),
+ })
+ check_parsing('"abc\\"def\\"ghi\\"jkl\\"mno"', {
+ -- 0123456789012345678901234
+ -- 0 1 2
+ ast = {
+ fmtn('DoubleQuotedString', 'val="abc\\"def\\"ghi\\"jkl\\"mno"', ':0:0:"abc\\"def\\"ghi\\"jkl\\"mno"'),
+ },
+ }, {
+ hl('DoubleQuote', '"'),
+ hl('DoubleQuotedBody', 'abc'),
+ hl('DoubleQuotedEscape', '\\"'),
+ hl('DoubleQuotedBody', 'def'),
+ hl('DoubleQuotedEscape', '\\"'),
+ hl('DoubleQuotedBody', 'ghi'),
+ hl('DoubleQuotedEscape', '\\"'),
+ hl('DoubleQuotedBody', 'jkl'),
+ hl('DoubleQuotedEscape', '\\"'),
+ hl('DoubleQuotedBody', 'mno'),
+ hl('DoubleQuote', '"'),
+ })
+ check_parsing('"\\b\\e\\f\\r\\t\\\\"', {
+ -- 0123456789012345
+ -- 0 1
+ ast = {
+ [[DoubleQuotedString(val="\008\027\012\r\t\\"):0:0:"\b\e\f\r\t\\"]],
+ },
+ }, {
+ hl('DoubleQuote', '"'),
+ hl('DoubleQuotedEscape', '\\b'),
+ hl('DoubleQuotedEscape', '\\e'),
+ hl('DoubleQuotedEscape', '\\f'),
+ hl('DoubleQuotedEscape', '\\r'),
+ hl('DoubleQuotedEscape', '\\t'),
+ hl('DoubleQuotedEscape', '\\\\'),
+ hl('DoubleQuote', '"'),
+ })
+ check_parsing('"\\n\n"', {
+ -- 01234
+ ast = {
+ fmtn('DoubleQuotedString', 'val="\\n\\n"', ':0:0:"\\n\n"'),
+ },
+ }, {
+ hl('DoubleQuote', '"'),
+ hl('DoubleQuotedEscape', '\\n'),
+ hl('DoubleQuotedBody', '\n'),
+ hl('DoubleQuote', '"'),
+ })
+ check_parsing('"\\x00"', {
+ -- 012345
+ ast = {
+ fmtn('DoubleQuotedString', 'val="\\000"', ':0:0:"\\x00"'),
+ },
+ }, {
+ hl('DoubleQuote', '"'),
+ hl('DoubleQuotedEscape', '\\x00'),
+ hl('DoubleQuote', '"'),
+ })
+ check_parsing('"\\xFF"', {
+ -- 012345
+ ast = {
+ fmtn('DoubleQuotedString', 'val="\255"', ':0:0:"\\xFF"'),
+ },
+ }, {
+ hl('DoubleQuote', '"'),
+ hl('DoubleQuotedEscape', '\\xFF'),
+ hl('DoubleQuote', '"'),
+ })
+ check_parsing('"\\xF"', {
+ -- 012345
+ ast = {
+ fmtn('DoubleQuotedString', 'val="\\015"', ':0:0:"\\xF"'),
+ },
+ }, {
+ hl('DoubleQuote', '"'),
+ hl('DoubleQuotedEscape', '\\xF'),
+ hl('DoubleQuote', '"'),
+ })
+ check_parsing('"\\u00AB"', {
+ -- 01234567
+ ast = {
+ fmtn('DoubleQuotedString', 'val="«"', ':0:0:"\\u00AB"'),
+ },
+ }, {
+ hl('DoubleQuote', '"'),
+ hl('DoubleQuotedEscape', '\\u00AB'),
+ hl('DoubleQuote', '"'),
+ })
+ check_parsing('"\\U000000AB"', {
+ -- 01234567
+ ast = {
+ fmtn('DoubleQuotedString', 'val="«"', ':0:0:"\\U000000AB"'),
+ },
+ }, {
+ hl('DoubleQuote', '"'),
+ hl('DoubleQuotedEscape', '\\U000000AB'),
+ hl('DoubleQuote', '"'),
+ })
+ check_parsing('"\\x"', {
+ -- 0123
+ ast = {
+ fmtn('DoubleQuotedString', 'val="x"', ':0:0:"\\x"'),
+ },
+ }, {
+ hl('DoubleQuote', '"'),
+ hl('DoubleQuotedUnknownEscape', '\\x'),
+ hl('DoubleQuote', '"'),
+ })
+
+ check_parsing('"\\x', {
+ -- 012
+ ast = {
+ fmtn('DoubleQuotedString', 'val="x"', ':0:0:"\\x'),
+ },
+ err = {
+ arg = '"\\x',
+ msg = 'E114: Missing double quote: %.*s',
+ },
+ }, {
+ hl('InvalidDoubleQuote', '"'),
+ hl('InvalidDoubleQuotedUnknownEscape', '\\x'),
+ })
+
+ check_parsing('"\\xF', {
+ -- 0123
+ ast = {
+ fmtn('DoubleQuotedString', 'val="\\015"', ':0:0:"\\xF'),
+ },
+ err = {
+ arg = '"\\xF',
+ msg = 'E114: Missing double quote: %.*s',
+ },
+ }, {
+ hl('InvalidDoubleQuote', '"'),
+ hl('InvalidDoubleQuotedEscape', '\\xF'),
+ })
+
+ check_parsing('"\\u"', {
+ -- 0123
+ ast = {
+ fmtn('DoubleQuotedString', 'val="u"', ':0:0:"\\u"'),
+ },
+ }, {
+ hl('DoubleQuote', '"'),
+ hl('DoubleQuotedUnknownEscape', '\\u'),
+ hl('DoubleQuote', '"'),
+ })
+
+ check_parsing('"\\u', {
+ -- 012
+ ast = {
+ fmtn('DoubleQuotedString', 'val="u"', ':0:0:"\\u'),
+ },
+ err = {
+ arg = '"\\u',
+ msg = 'E114: Missing double quote: %.*s',
+ },
+ }, {
+ hl('InvalidDoubleQuote', '"'),
+ hl('InvalidDoubleQuotedUnknownEscape', '\\u'),
+ })
+
+ check_parsing('"\\U', {
+ -- 012
+ ast = {
+ fmtn('DoubleQuotedString', 'val="U"', ':0:0:"\\U'),
+ },
+ err = {
+ arg = '"\\U',
+ msg = 'E114: Missing double quote: %.*s',
+ },
+ }, {
+ hl('InvalidDoubleQuote', '"'),
+ hl('InvalidDoubleQuotedUnknownEscape', '\\U'),
+ })
+
+ check_parsing('"\\U"', {
+ -- 0123
+ ast = {
+ fmtn('DoubleQuotedString', 'val="U"', ':0:0:"\\U"'),
+ },
+ }, {
+ hl('DoubleQuote', '"'),
+ hl('DoubleQuotedUnknownEscape', '\\U'),
+ hl('DoubleQuote', '"'),
+ })
+
+ check_parsing('"\\xFX"', {
+ -- 012345
+ ast = {
+ fmtn('DoubleQuotedString', 'val="\\015X"', ':0:0:"\\xFX"'),
+ },
+ }, {
+ hl('DoubleQuote', '"'),
+ hl('DoubleQuotedEscape', '\\xF'),
+ hl('DoubleQuotedBody', 'X'),
+ hl('DoubleQuote', '"'),
+ })
+
+ check_parsing('"\\XFX"', {
+ -- 012345
+ ast = {
+ fmtn('DoubleQuotedString', 'val="\\015X"', ':0:0:"\\XFX"'),
+ },
+ }, {
+ hl('DoubleQuote', '"'),
+ hl('DoubleQuotedEscape', '\\XF'),
+ hl('DoubleQuotedBody', 'X'),
+ hl('DoubleQuote', '"'),
+ })
+
+ check_parsing('"\\xX"', {
+ -- 01234
+ ast = {
+ fmtn('DoubleQuotedString', 'val="xX"', ':0:0:"\\xX"'),
+ },
+ }, {
+ hl('DoubleQuote', '"'),
+ hl('DoubleQuotedUnknownEscape', '\\x'),
+ hl('DoubleQuotedBody', 'X'),
+ hl('DoubleQuote', '"'),
+ })
+
+ check_parsing('"\\XX"', {
+ -- 01234
+ ast = {
+ fmtn('DoubleQuotedString', 'val="XX"', ':0:0:"\\XX"'),
+ },
+ }, {
+ hl('DoubleQuote', '"'),
+ hl('DoubleQuotedUnknownEscape', '\\X'),
+ hl('DoubleQuotedBody', 'X'),
+ hl('DoubleQuote', '"'),
+ })
+
+ check_parsing('"\\uX"', {
+ -- 01234
+ ast = {
+ fmtn('DoubleQuotedString', 'val="uX"', ':0:0:"\\uX"'),
+ },
+ }, {
+ hl('DoubleQuote', '"'),
+ hl('DoubleQuotedUnknownEscape', '\\u'),
+ hl('DoubleQuotedBody', 'X'),
+ hl('DoubleQuote', '"'),
+ })
+
+ check_parsing('"\\UX"', {
+ -- 01234
+ ast = {
+ fmtn('DoubleQuotedString', 'val="UX"', ':0:0:"\\UX"'),
+ },
+ }, {
+ hl('DoubleQuote', '"'),
+ hl('DoubleQuotedUnknownEscape', '\\U'),
+ hl('DoubleQuotedBody', 'X'),
+ hl('DoubleQuote', '"'),
+ })
+
+ check_parsing('"\\x0X"', {
+ -- 012345
+ ast = {
+ fmtn('DoubleQuotedString', 'val="\\000X"', ':0:0:"\\x0X"'),
+ },
+ }, {
+ hl('DoubleQuote', '"'),
+ hl('DoubleQuotedEscape', '\\x0'),
+ hl('DoubleQuotedBody', 'X'),
+ hl('DoubleQuote', '"'),
+ })
+
+ check_parsing('"\\X0X"', {
+ -- 012345
+ ast = {
+ fmtn('DoubleQuotedString', 'val="\\000X"', ':0:0:"\\X0X"'),
+ },
+ }, {
+ hl('DoubleQuote', '"'),
+ hl('DoubleQuotedEscape', '\\X0'),
+ hl('DoubleQuotedBody', 'X'),
+ hl('DoubleQuote', '"'),
+ })
+
+ check_parsing('"\\u0X"', {
+ -- 012345
+ ast = {
+ fmtn('DoubleQuotedString', 'val="\\000X"', ':0:0:"\\u0X"'),
+ },
+ }, {
+ hl('DoubleQuote', '"'),
+ hl('DoubleQuotedEscape', '\\u0'),
+ hl('DoubleQuotedBody', 'X'),
+ hl('DoubleQuote', '"'),
+ })
+
+ check_parsing('"\\U0X"', {
+ -- 012345
+ ast = {
+ fmtn('DoubleQuotedString', 'val="\\000X"', ':0:0:"\\U0X"'),
+ },
+ }, {
+ hl('DoubleQuote', '"'),
+ hl('DoubleQuotedEscape', '\\U0'),
+ hl('DoubleQuotedBody', 'X'),
+ hl('DoubleQuote', '"'),
+ })
+
+ check_parsing('"\\x00X"', {
+ -- 0123456
+ ast = {
+ fmtn('DoubleQuotedString', 'val="\\000X"', ':0:0:"\\x00X"'),
+ },
+ }, {
+ hl('DoubleQuote', '"'),
+ hl('DoubleQuotedEscape', '\\x00'),
+ hl('DoubleQuotedBody', 'X'),
+ hl('DoubleQuote', '"'),
+ })
+
+ check_parsing('"\\X00X"', {
+ -- 0123456
+ ast = {
+ fmtn('DoubleQuotedString', 'val="\\000X"', ':0:0:"\\X00X"'),
+ },
+ }, {
+ hl('DoubleQuote', '"'),
+ hl('DoubleQuotedEscape', '\\X00'),
+ hl('DoubleQuotedBody', 'X'),
+ hl('DoubleQuote', '"'),
+ })
+
+ check_parsing('"\\u00X"', {
+ -- 0123456
+ ast = {
+ fmtn('DoubleQuotedString', 'val="\\000X"', ':0:0:"\\u00X"'),
+ },
+ }, {
+ hl('DoubleQuote', '"'),
+ hl('DoubleQuotedEscape', '\\u00'),
+ hl('DoubleQuotedBody', 'X'),
+ hl('DoubleQuote', '"'),
+ })
+
+ check_parsing('"\\U00X"', {
+ -- 0123456
+ ast = {
+ fmtn('DoubleQuotedString', 'val="\\000X"', ':0:0:"\\U00X"'),
+ },
+ }, {
+ hl('DoubleQuote', '"'),
+ hl('DoubleQuotedEscape', '\\U00'),
+ hl('DoubleQuotedBody', 'X'),
+ hl('DoubleQuote', '"'),
+ })
+
+ check_parsing('"\\u000X"', {
+ -- 01234567
+ ast = {
+ fmtn('DoubleQuotedString', 'val="\\000X"', ':0:0:"\\u000X"'),
+ },
+ }, {
+ hl('DoubleQuote', '"'),
+ hl('DoubleQuotedEscape', '\\u000'),
+ hl('DoubleQuotedBody', 'X'),
+ hl('DoubleQuote', '"'),
+ })
+
+ check_parsing('"\\U000X"', {
+ -- 01234567
+ ast = {
+ fmtn('DoubleQuotedString', 'val="\\000X"', ':0:0:"\\U000X"'),
+ },
+ }, {
+ hl('DoubleQuote', '"'),
+ hl('DoubleQuotedEscape', '\\U000'),
+ hl('DoubleQuotedBody', 'X'),
+ hl('DoubleQuote', '"'),
+ })
+
+ check_parsing('"\\u0000X"', {
+ -- 012345678
+ ast = {
+ fmtn('DoubleQuotedString', 'val="\\000X"', ':0:0:"\\u0000X"'),
+ },
+ }, {
+ hl('DoubleQuote', '"'),
+ hl('DoubleQuotedEscape', '\\u0000'),
+ hl('DoubleQuotedBody', 'X'),
+ hl('DoubleQuote', '"'),
+ })
+
+ check_parsing('"\\U0000X"', {
+ -- 012345678
+ ast = {
+ fmtn('DoubleQuotedString', 'val="\\000X"', ':0:0:"\\U0000X"'),
+ },
+ }, {
+ hl('DoubleQuote', '"'),
+ hl('DoubleQuotedEscape', '\\U0000'),
+ hl('DoubleQuotedBody', 'X'),
+ hl('DoubleQuote', '"'),
+ })
+
+ check_parsing('"\\U00000X"', {
+ -- 0123456789
+ ast = {
+ fmtn('DoubleQuotedString', 'val="\\000X"', ':0:0:"\\U00000X"'),
+ },
+ }, {
+ hl('DoubleQuote', '"'),
+ hl('DoubleQuotedEscape', '\\U00000'),
+ hl('DoubleQuotedBody', 'X'),
+ hl('DoubleQuote', '"'),
+ })
+
+ check_parsing('"\\U000000X"', {
+ -- 01234567890
+ -- 0 1
+ ast = {
+ fmtn('DoubleQuotedString', 'val="\\000X"', ':0:0:"\\U000000X"'),
+ },
+ }, {
+ hl('DoubleQuote', '"'),
+ hl('DoubleQuotedEscape', '\\U000000'),
+ hl('DoubleQuotedBody', 'X'),
+ hl('DoubleQuote', '"'),
+ })
+
+ check_parsing('"\\U0000000X"', {
+ -- 012345678901
+ -- 0 1
+ ast = {
+ fmtn('DoubleQuotedString', 'val="\\000X"', ':0:0:"\\U0000000X"'),
+ },
+ }, {
+ hl('DoubleQuote', '"'),
+ hl('DoubleQuotedEscape', '\\U0000000'),
+ hl('DoubleQuotedBody', 'X'),
+ hl('DoubleQuote', '"'),
+ })
+
+ check_parsing('"\\U00000000X"', {
+ -- 0123456789012
+ -- 0 1
+ ast = {
+ fmtn('DoubleQuotedString', 'val="\\000X"', ':0:0:"\\U00000000X"'),
+ },
+ }, {
+ hl('DoubleQuote', '"'),
+ hl('DoubleQuotedEscape', '\\U00000000'),
+ hl('DoubleQuotedBody', 'X'),
+ hl('DoubleQuote', '"'),
+ })
+
+ check_parsing('"\\x000X"', {
+ -- 01234567
+ ast = {
+ fmtn('DoubleQuotedString', 'val="\\0000X"', ':0:0:"\\x000X"'),
+ },
+ }, {
+ hl('DoubleQuote', '"'),
+ hl('DoubleQuotedEscape', '\\x00'),
+ hl('DoubleQuotedBody', '0X'),
+ hl('DoubleQuote', '"'),
+ })
+
+ check_parsing('"\\X000X"', {
+ -- 01234567
+ ast = {
+ fmtn('DoubleQuotedString', 'val="\\0000X"', ':0:0:"\\X000X"'),
+ },
+ }, {
+ hl('DoubleQuote', '"'),
+ hl('DoubleQuotedEscape', '\\X00'),
+ hl('DoubleQuotedBody', '0X'),
+ hl('DoubleQuote', '"'),
+ })
+
+ check_parsing('"\\u00000X"', {
+ -- 0123456789
+ ast = {
+ fmtn('DoubleQuotedString', 'val="\\0000X"', ':0:0:"\\u00000X"'),
+ },
+ }, {
+ hl('DoubleQuote', '"'),
+ hl('DoubleQuotedEscape', '\\u0000'),
+ hl('DoubleQuotedBody', '0X'),
+ hl('DoubleQuote', '"'),
+ })
+
+ check_parsing('"\\U000000000X"', {
+ -- 01234567890123
+ -- 0 1
+ ast = {
+ fmtn('DoubleQuotedString', 'val="\\0000X"', ':0:0:"\\U000000000X"'),
+ },
+ }, {
+ hl('DoubleQuote', '"'),
+ hl('DoubleQuotedEscape', '\\U00000000'),
+ hl('DoubleQuotedBody', '0X'),
+ hl('DoubleQuote', '"'),
+ })
+
+ check_parsing('"\\0"', {
+ -- 0123
+ ast = {
+ fmtn('DoubleQuotedString', 'val="\\000"', ':0:0:"\\0"'),
+ },
+ }, {
+ hl('DoubleQuote', '"'),
+ hl('DoubleQuotedEscape', '\\0'),
+ hl('DoubleQuote', '"'),
+ })
+
+ check_parsing('"\\00"', {
+ -- 01234
+ ast = {
+ fmtn('DoubleQuotedString', 'val="\\000"', ':0:0:"\\00"'),
+ },
+ }, {
+ hl('DoubleQuote', '"'),
+ hl('DoubleQuotedEscape', '\\00'),
+ hl('DoubleQuote', '"'),
+ })
+
+ check_parsing('"\\000"', {
+ -- 012345
+ ast = {
+ fmtn('DoubleQuotedString', 'val="\\000"', ':0:0:"\\000"'),
+ },
+ }, {
+ hl('DoubleQuote', '"'),
+ hl('DoubleQuotedEscape', '\\000'),
+ hl('DoubleQuote', '"'),
+ })
+
+ check_parsing('"\\0000"', {
+ -- 0123456
+ ast = {
+ fmtn('DoubleQuotedString', 'val="\\0000"', ':0:0:"\\0000"'),
+ },
+ }, {
+ hl('DoubleQuote', '"'),
+ hl('DoubleQuotedEscape', '\\000'),
+ hl('DoubleQuotedBody', '0'),
+ hl('DoubleQuote', '"'),
+ })
+
+ check_parsing('"\\8"', {
+ -- 0123
+ ast = {
+ fmtn('DoubleQuotedString', 'val="8"', ':0:0:"\\8"'),
+ },
+ }, {
+ hl('DoubleQuote', '"'),
+ hl('DoubleQuotedUnknownEscape', '\\8'),
+ hl('DoubleQuote', '"'),
+ })
+
+ check_parsing('"\\08"', {
+ -- 01234
+ ast = {
+ fmtn('DoubleQuotedString', 'val="\\0008"', ':0:0:"\\08"'),
+ },
+ }, {
+ hl('DoubleQuote', '"'),
+ hl('DoubleQuotedEscape', '\\0'),
+ hl('DoubleQuotedBody', '8'),
+ hl('DoubleQuote', '"'),
+ })
+
+ check_parsing('"\\008"', {
+ -- 012345
+ ast = {
+ fmtn('DoubleQuotedString', 'val="\\0008"', ':0:0:"\\008"'),
+ },
+ }, {
+ hl('DoubleQuote', '"'),
+ hl('DoubleQuotedEscape', '\\00'),
+ hl('DoubleQuotedBody', '8'),
+ hl('DoubleQuote', '"'),
+ })
+
+ check_parsing('"\\0008"', {
+ -- 0123456
+ ast = {
+ fmtn('DoubleQuotedString', 'val="\\0008"', ':0:0:"\\0008"'),
+ },
+ }, {
+ hl('DoubleQuote', '"'),
+ hl('DoubleQuotedEscape', '\\000'),
+ hl('DoubleQuotedBody', '8'),
+ hl('DoubleQuote', '"'),
+ })
+
+ check_parsing('"\\777"', {
+ -- 012345
+ ast = {
+ fmtn('DoubleQuotedString', 'val="\255"', ':0:0:"\\777"'),
+ },
+ }, {
+ hl('DoubleQuote', '"'),
+ hl('DoubleQuotedEscape', '\\777'),
+ hl('DoubleQuote', '"'),
+ })
+
+ check_parsing('"\\050"', {
+ -- 012345
+ ast = {
+ fmtn('DoubleQuotedString', 'val="\40"', ':0:0:"\\050"'),
+ },
+ }, {
+ hl('DoubleQuote', '"'),
+ hl('DoubleQuotedEscape', '\\050'),
+ hl('DoubleQuote', '"'),
+ })
+
+ check_parsing('"\\<C-u>"', {
+ -- 012345
+ ast = {
+ fmtn('DoubleQuotedString', 'val="\\021"', ':0:0:"\\<C-u>"'),
+ },
+ }, {
+ hl('DoubleQuote', '"'),
+ hl('DoubleQuotedEscape', '\\<C-u>'),
+ hl('DoubleQuote', '"'),
+ })
+
+ check_parsing('"\\<', {
+ -- 012
+ ast = {
+ fmtn('DoubleQuotedString', 'val="<"', ':0:0:"\\<'),
+ },
+ err = {
+ arg = '"\\<',
+ msg = 'E114: Missing double quote: %.*s',
+ },
+ }, {
+ hl('InvalidDoubleQuote', '"'),
+ hl('InvalidDoubleQuotedUnknownEscape', '\\<'),
+ })
+
+ check_parsing('"\\<"', {
+ -- 0123
+ ast = {
+ fmtn('DoubleQuotedString', 'val="<"', ':0:0:"\\<"'),
+ },
+ }, {
+ hl('DoubleQuote', '"'),
+ hl('DoubleQuotedUnknownEscape', '\\<'),
+ hl('DoubleQuote', '"'),
+ })
+
+ check_parsing('"\\<C-u"', {
+ -- 0123456
+ ast = {
+ fmtn('DoubleQuotedString', 'val="<C-u"', ':0:0:"\\<C-u"'),
+ },
+ }, {
+ hl('DoubleQuote', '"'),
+ hl('DoubleQuotedUnknownEscape', '\\<'),
+ hl('DoubleQuotedBody', 'C-u'),
+ hl('DoubleQuote', '"'),
+ })
+ end)
+ itp('works with multiplication-like operators', function()
+ check_parsing('2+2*2', {
+ -- 01234
+ ast = {
+ {
+ 'BinaryPlus:0:1:+',
+ children = {
+ 'Integer(val=2):0:0:2',
+ {
+ 'Multiplication:0:3:*',
+ children = {
+ 'Integer(val=2):0:2:2',
+ 'Integer(val=2):0:4:2',
+ },
+ },
+ },
+ },
+ },
+ }, {
+ hl('Number', '2'),
+ hl('BinaryPlus', '+'),
+ hl('Number', '2'),
+ hl('Multiplication', '*'),
+ hl('Number', '2'),
+ })
+
+ check_parsing('2+2*', {
+ -- 0123
+ ast = {
+ {
+ 'BinaryPlus:0:1:+',
+ children = {
+ 'Integer(val=2):0:0:2',
+ {
+ 'Multiplication:0:3:*',
+ children = {
+ 'Integer(val=2):0:2:2',
+ },
+ },
+ },
+ },
+ },
+ err = {
+ arg = '',
+ msg = 'E15: Expected value, got EOC: %.*s',
+ },
+ }, {
+ hl('Number', '2'),
+ hl('BinaryPlus', '+'),
+ hl('Number', '2'),
+ hl('Multiplication', '*'),
+ })
+
+ check_parsing('2+*2', {
+ -- 0123
+ ast = {
+ {
+ 'BinaryPlus:0:1:+',
+ children = {
+ 'Integer(val=2):0:0:2',
+ {
+ 'Multiplication:0:2:*',
+ children = {
+ 'Missing:0:2:',
+ 'Integer(val=2):0:3:2',
+ },
+ },
+ },
+ },
+ },
+ err = {
+ arg = '*2',
+ msg = 'E15: Unexpected multiplication-like operator: %.*s',
+ },
+ }, {
+ hl('Number', '2'),
+ hl('BinaryPlus', '+'),
+ hl('InvalidMultiplication', '*'),
+ hl('Number', '2'),
+ })
+
+ check_parsing('2+2/2', {
+ -- 01234
+ ast = {
+ {
+ 'BinaryPlus:0:1:+',
+ children = {
+ 'Integer(val=2):0:0:2',
+ {
+ 'Division:0:3:/',
+ children = {
+ 'Integer(val=2):0:2:2',
+ 'Integer(val=2):0:4:2',
+ },
+ },
+ },
+ },
+ },
+ }, {
+ hl('Number', '2'),
+ hl('BinaryPlus', '+'),
+ hl('Number', '2'),
+ hl('Division', '/'),
+ hl('Number', '2'),
+ })
+
+ check_parsing('2+2/', {
+ -- 0123
+ ast = {
+ {
+ 'BinaryPlus:0:1:+',
+ children = {
+ 'Integer(val=2):0:0:2',
+ {
+ 'Division:0:3:/',
+ children = {
+ 'Integer(val=2):0:2:2',
+ },
+ },
+ },
+ },
+ },
+ err = {
+ arg = '',
+ msg = 'E15: Expected value, got EOC: %.*s',
+ },
+ }, {
+ hl('Number', '2'),
+ hl('BinaryPlus', '+'),
+ hl('Number', '2'),
+ hl('Division', '/'),
+ })
+
+ check_parsing('2+/2', {
+ -- 0123
+ ast = {
+ {
+ 'BinaryPlus:0:1:+',
+ children = {
+ 'Integer(val=2):0:0:2',
+ {
+ 'Division:0:2:/',
+ children = {
+ 'Missing:0:2:',
+ 'Integer(val=2):0:3:2',
+ },
+ },
+ },
+ },
+ },
+ err = {
+ arg = '/2',
+ msg = 'E15: Unexpected multiplication-like operator: %.*s',
+ },
+ }, {
+ hl('Number', '2'),
+ hl('BinaryPlus', '+'),
+ hl('InvalidDivision', '/'),
+ hl('Number', '2'),
+ })
+
+ check_parsing('2+2%2', {
+ -- 01234
+ ast = {
+ {
+ 'BinaryPlus:0:1:+',
+ children = {
+ 'Integer(val=2):0:0:2',
+ {
+ 'Mod:0:3:%',
+ children = {
+ 'Integer(val=2):0:2:2',
+ 'Integer(val=2):0:4:2',
+ },
+ },
+ },
+ },
+ },
+ }, {
+ hl('Number', '2'),
+ hl('BinaryPlus', '+'),
+ hl('Number', '2'),
+ hl('Mod', '%'),
+ hl('Number', '2'),
+ })
+
+ check_parsing('2+2%', {
+ -- 0123
+ ast = {
+ {
+ 'BinaryPlus:0:1:+',
+ children = {
+ 'Integer(val=2):0:0:2',
+ {
+ 'Mod:0:3:%',
+ children = {
+ 'Integer(val=2):0:2:2',
+ },
+ },
+ },
+ },
+ },
+ err = {
+ arg = '',
+ msg = 'E15: Expected value, got EOC: %.*s',
+ },
+ }, {
+ hl('Number', '2'),
+ hl('BinaryPlus', '+'),
+ hl('Number', '2'),
+ hl('Mod', '%'),
+ })
+
+ check_parsing('2+%2', {
+ -- 0123
+ ast = {
+ {
+ 'BinaryPlus:0:1:+',
+ children = {
+ 'Integer(val=2):0:0:2',
+ {
+ 'Mod:0:2:%',
+ children = {
+ 'Missing:0:2:',
+ 'Integer(val=2):0:3:2',
+ },
+ },
+ },
+ },
+ },
+ err = {
+ arg = '%2',
+ msg = 'E15: Unexpected multiplication-like operator: %.*s',
+ },
+ }, {
+ hl('Number', '2'),
+ hl('BinaryPlus', '+'),
+ hl('InvalidMod', '%'),
+ hl('Number', '2'),
+ })
+ end)
+ itp('works with -', function()
+ check_parsing('@a', {
+ ast = {
+ 'Register(name=a):0:0:@a',
+ },
+ }, {
+ hl('Register', '@a'),
+ })
+ check_parsing('-@a', {
+ ast = {
+ {
+ 'UnaryMinus:0:0:-',
+ children = {
+ 'Register(name=a):0:1:@a',
+ },
+ },
+ },
+ }, {
+ hl('UnaryMinus', '-'),
+ hl('Register', '@a'),
+ })
+ check_parsing('@a-@b', {
+ ast = {
+ {
+ 'BinaryMinus:0:2:-',
+ children = {
+ 'Register(name=a):0:0:@a',
+ 'Register(name=b):0:3:@b',
+ },
+ },
+ },
+ }, {
+ hl('Register', '@a'),
+ hl('BinaryMinus', '-'),
+ hl('Register', '@b'),
+ })
+ check_parsing('@a-@b-@c', {
+ ast = {
+ {
+ 'BinaryMinus:0:5:-',
+ children = {
+ {
+ 'BinaryMinus:0:2:-',
+ children = {
+ 'Register(name=a):0:0:@a',
+ 'Register(name=b):0:3:@b',
+ },
+ },
+ 'Register(name=c):0:6:@c',
+ },
+ },
+ },
+ }, {
+ hl('Register', '@a'),
+ hl('BinaryMinus', '-'),
+ hl('Register', '@b'),
+ hl('BinaryMinus', '-'),
+ hl('Register', '@c'),
+ })
+ check_parsing('-@a-@b', {
+ ast = {
+ {
+ 'BinaryMinus:0:3:-',
+ children = {
+ {
+ 'UnaryMinus:0:0:-',
+ children = {
+ 'Register(name=a):0:1:@a',
+ },
+ },
+ 'Register(name=b):0:4:@b',
+ },
+ },
+ },
+ }, {
+ hl('UnaryMinus', '-'),
+ hl('Register', '@a'),
+ hl('BinaryMinus', '-'),
+ hl('Register', '@b'),
+ })
+ check_parsing('-@a--@b', {
+ ast = {
+ {
+ 'BinaryMinus:0:3:-',
+ children = {
+ {
+ 'UnaryMinus:0:0:-',
+ children = {
+ 'Register(name=a):0:1:@a',
+ },
+ },
+ {
+ 'UnaryMinus:0:4:-',
+ children = {
+ 'Register(name=b):0:5:@b',
+ },
+ },
+ },
+ },
+ },
+ }, {
+ hl('UnaryMinus', '-'),
+ hl('Register', '@a'),
+ hl('BinaryMinus', '-'),
+ hl('UnaryMinus', '-'),
+ hl('Register', '@b'),
+ })
+ check_parsing('-', {
+ ast = {
+ 'UnaryMinus:0:0:-',
+ },
+ err = {
+ arg = '',
+ msg = 'E15: Expected value, got EOC: %.*s',
+ },
+ }, {
+ hl('UnaryMinus', '-'),
+ })
+ check_parsing(' -', {
+ ast = {
+ 'UnaryMinus:0:0: -',
+ },
+ err = {
+ arg = '',
+ msg = 'E15: Expected value, got EOC: %.*s',
+ },
+ }, {
+ hl('UnaryMinus', '-', 1),
+ })
+ check_parsing('@a- ', {
+ ast = {
+ {
+ 'BinaryMinus:0:2:-',
+ children = {
+ 'Register(name=a):0:0:@a',
+ },
+ },
+ },
+ err = {
+ arg = '',
+ msg = 'E15: Expected value, got EOC: %.*s',
+ },
+ }, {
+ hl('Register', '@a'),
+ hl('BinaryMinus', '-'),
+ })
+ end)
+ itp('works with logical operators', function()
+ check_parsing('a && b || c && d', {
+ -- 0123456789012345
+ -- 0 1
+ ast = {
+ {
+ 'Or:0:6: ||',
+ children = {
+ {
+ 'And:0:1: &&',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:0:a',
+ 'PlainIdentifier(scope=0,ident=b):0:4: b',
+ },
+ },
+ {
+ 'And:0:11: &&',
+ children = {
+ 'PlainIdentifier(scope=0,ident=c):0:9: c',
+ 'PlainIdentifier(scope=0,ident=d):0:14: d',
+ },
+ },
+ },
+ },
+ },
+ }, {
+ hl('IdentifierName', 'a'),
+ hl('And', '&&', 1),
+ hl('IdentifierName', 'b', 1),
+ hl('Or', '||', 1),
+ hl('IdentifierName', 'c', 1),
+ hl('And', '&&', 1),
+ hl('IdentifierName', 'd', 1),
+ })
+
+ check_parsing('&& a', {
+ -- 0123
+ ast = {
+ {
+ 'And:0:0:&&',
+ children = {
+ 'Missing:0:0:',
+ 'PlainIdentifier(scope=0,ident=a):0:2: a',
+ },
+ },
+ },
+ err = {
+ arg = '&& a',
+ msg = 'E15: Unexpected and operator: %.*s',
+ },
+ }, {
+ hl('InvalidAnd', '&&'),
+ hl('IdentifierName', 'a', 1),
+ })
+
+ check_parsing('|| a', {
+ -- 0123
+ ast = {
+ {
+ 'Or:0:0:||',
+ children = {
+ 'Missing:0:0:',
+ 'PlainIdentifier(scope=0,ident=a):0:2: a',
+ },
+ },
+ },
+ err = {
+ arg = '|| a',
+ msg = 'E15: Unexpected or operator: %.*s',
+ },
+ }, {
+ hl('InvalidOr', '||'),
+ hl('IdentifierName', 'a', 1),
+ })
+
+ check_parsing('a||', {
+ -- 012
+ ast = {
+ {
+ 'Or:0:1:||',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:0:a',
+ },
+ },
+ },
+ err = {
+ arg = '',
+ msg = 'E15: Expected value, got EOC: %.*s',
+ },
+ }, {
+ hl('IdentifierName', 'a'),
+ hl('Or', '||'),
+ })
+
+ check_parsing('a&&', {
+ -- 012
+ ast = {
+ {
+ 'And:0:1:&&',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:0:a',
+ },
+ },
+ },
+ err = {
+ arg = '',
+ msg = 'E15: Expected value, got EOC: %.*s',
+ },
+ }, {
+ hl('IdentifierName', 'a'),
+ hl('And', '&&'),
+ })
+
+ check_parsing('(&&)', {
+ -- 0123
+ ast = {
+ {
+ 'Nested:0:0:(',
+ children = {
+ {
+ 'And:0:1:&&',
+ children = {
+ 'Missing:0:1:',
+ 'Missing:0:3:',
+ },
+ },
+ },
+ },
+ },
+ err = {
+ arg = '&&)',
+ msg = 'E15: Unexpected and operator: %.*s',
+ },
+ }, {
+ hl('NestingParenthesis', '('),
+ hl('InvalidAnd', '&&'),
+ hl('InvalidNestingParenthesis', ')'),
+ })
+
+ check_parsing('(||)', {
+ -- 0123
+ ast = {
+ {
+ 'Nested:0:0:(',
+ children = {
+ {
+ 'Or:0:1:||',
+ children = {
+ 'Missing:0:1:',
+ 'Missing:0:3:',
+ },
+ },
+ },
+ },
+ },
+ err = {
+ arg = '||)',
+ msg = 'E15: Unexpected or operator: %.*s',
+ },
+ }, {
+ hl('NestingParenthesis', '('),
+ hl('InvalidOr', '||'),
+ hl('InvalidNestingParenthesis', ')'),
+ })
+
+ check_parsing('(a||)', {
+ -- 01234
+ ast = {
+ {
+ 'Nested:0:0:(',
+ children = {
+ {
+ 'Or:0:2:||',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:1:a',
+ 'Missing:0:4:',
+ },
+ },
+ },
+ },
+ },
+ err = {
+ arg = ')',
+ msg = 'E15: Expected value, got parenthesis: %.*s',
+ },
+ }, {
+ hl('NestingParenthesis', '('),
+ hl('IdentifierName', 'a'),
+ hl('Or', '||'),
+ hl('InvalidNestingParenthesis', ')'),
+ })
+
+ check_parsing('(a&&)', {
+ -- 01234
+ ast = {
+ {
+ 'Nested:0:0:(',
+ children = {
+ {
+ 'And:0:2:&&',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:1:a',
+ 'Missing:0:4:',
+ },
+ },
+ },
+ },
+ },
+ err = {
+ arg = ')',
+ msg = 'E15: Expected value, got parenthesis: %.*s',
+ },
+ }, {
+ hl('NestingParenthesis', '('),
+ hl('IdentifierName', 'a'),
+ hl('And', '&&'),
+ hl('InvalidNestingParenthesis', ')'),
+ })
+
+ check_parsing('(&&a)', {
+ -- 01234
+ ast = {
+ {
+ 'Nested:0:0:(',
+ children = {
+ {
+ 'And:0:1:&&',
+ children = {
+ 'Missing:0:1:',
+ 'PlainIdentifier(scope=0,ident=a):0:3:a',
+ },
+ },
+ },
+ },
+ },
+ err = {
+ arg = '&&a)',
+ msg = 'E15: Unexpected and operator: %.*s',
+ },
+ }, {
+ hl('NestingParenthesis', '('),
+ hl('InvalidAnd', '&&'),
+ hl('IdentifierName', 'a'),
+ hl('NestingParenthesis', ')'),
+ })
+
+ check_parsing('(||a)', {
+ -- 01234
+ ast = {
+ {
+ 'Nested:0:0:(',
+ children = {
+ {
+ 'Or:0:1:||',
+ children = {
+ 'Missing:0:1:',
+ 'PlainIdentifier(scope=0,ident=a):0:3:a',
+ },
+ },
+ },
+ },
+ },
+ err = {
+ arg = '||a)',
+ msg = 'E15: Unexpected or operator: %.*s',
+ },
+ }, {
+ hl('NestingParenthesis', '('),
+ hl('InvalidOr', '||'),
+ hl('IdentifierName', 'a'),
+ hl('NestingParenthesis', ')'),
+ })
+ end)
+ itp('works with &opt', function()
+ check_parsing('&', {
+ -- 0
+ ast = {
+ 'Option(scope=0,ident=):0:0:&',
+ },
+ err = {
+ arg = '&',
+ msg = 'E112: Option name missing: %.*s',
+ },
+ }, {
+ hl('InvalidOptionSigil', '&'),
+ })
+
+ check_parsing('&opt', {
+ -- 0123
+ ast = {
+ 'Option(scope=0,ident=opt):0:0:&opt',
+ },
+ }, {
+ hl('OptionSigil', '&'),
+ hl('OptionName', 'opt'),
+ })
+
+ check_parsing('&l:opt', {
+ -- 012345
+ ast = {
+ 'Option(scope=l,ident=opt):0:0:&l:opt',
+ },
+ }, {
+ hl('OptionSigil', '&'),
+ hl('OptionScope', 'l'),
+ hl('OptionScopeDelimiter', ':'),
+ hl('OptionName', 'opt'),
+ })
+
+ check_parsing('&g:opt', {
+ -- 012345
+ ast = {
+ 'Option(scope=g,ident=opt):0:0:&g:opt',
+ },
+ }, {
+ hl('OptionSigil', '&'),
+ hl('OptionScope', 'g'),
+ hl('OptionScopeDelimiter', ':'),
+ hl('OptionName', 'opt'),
+ })
+
+ check_parsing('&s:opt', {
+ -- 012345
+ ast = {
+ {
+ 'Colon:0:2::',
+ children = {
+ 'Option(scope=0,ident=s):0:0:&s',
+ 'PlainIdentifier(scope=0,ident=opt):0:3:opt',
+ },
+ },
+ },
+ err = {
+ arg = ':opt',
+ msg = 'E15: Colon outside of dictionary or ternary operator: %.*s',
+ },
+ }, {
+ hl('OptionSigil', '&'),
+ hl('OptionName', 's'),
+ hl('InvalidColon', ':'),
+ hl('IdentifierName', 'opt'),
+ })
+
+ check_parsing('& ', {
+ -- 01
+ ast = {
+ 'Option(scope=0,ident=):0:0:&',
+ },
+ err = {
+ arg = '& ',
+ msg = 'E112: Option name missing: %.*s',
+ },
+ }, {
+ hl('InvalidOptionSigil', '&'),
+ })
+
+ check_parsing('&-', {
+ -- 01
+ ast = {
+ {
+ 'BinaryMinus:0:1:-',
+ children = {
+ 'Option(scope=0,ident=):0:0:&',
+ },
+ },
+ },
+ err = {
+ arg = '&-',
+ msg = 'E112: Option name missing: %.*s',
+ },
+ }, {
+ hl('InvalidOptionSigil', '&'),
+ hl('BinaryMinus', '-'),
+ })
+
+ check_parsing('&A', {
+ -- 01
+ ast = {
+ 'Option(scope=0,ident=A):0:0:&A',
+ },
+ }, {
+ hl('OptionSigil', '&'),
+ hl('OptionName', 'A'),
+ })
+
+ check_parsing('&xxx_yyy', {
+ -- 01234567
+ ast = {
+ {
+ 'OpMissing:0:4:',
+ children = {
+ 'Option(scope=0,ident=xxx):0:0:&xxx',
+ 'PlainIdentifier(scope=0,ident=_yyy):0:4:_yyy',
+ },
+ },
+ },
+ err = {
+ arg = '_yyy',
+ msg = 'E15: Missing operator: %.*s',
+ },
+ }, {
+ hl('OptionSigil', '&'),
+ hl('OptionName', 'xxx'),
+ hl('InvalidIdentifierName', '_yyy'),
+ }, {
+ [1] = {
+ ast = {
+ len = 4,
+ err = REMOVE_THIS,
+ ast = {
+ 'Option(scope=0,ident=xxx):0:0:&xxx',
+ },
+ },
+ hl_fs = {
+ [3] = REMOVE_THIS,
+ },
+ },
+ })
+
+ check_parsing('(1+&)', {
+ -- 01234
+ ast = {
+ {
+ 'Nested:0:0:(',
+ children = {
+ {
+ 'BinaryPlus:0:2:+',
+ children = {
+ 'Integer(val=1):0:1:1',
+ 'Option(scope=0,ident=):0:3:&',
+ },
+ },
+ },
+ },
+ },
+ err = {
+ arg = '&)',
+ msg = 'E112: Option name missing: %.*s',
+ },
+ }, {
+ hl('NestingParenthesis', '('),
+ hl('Number', '1'),
+ hl('BinaryPlus', '+'),
+ hl('InvalidOptionSigil', '&'),
+ hl('NestingParenthesis', ')'),
+ })
+
+ check_parsing('(&+1)', {
+ -- 01234
+ ast = {
+ {
+ 'Nested:0:0:(',
+ children = {
+ {
+ 'BinaryPlus:0:2:+',
+ children = {
+ 'Option(scope=0,ident=):0:1:&',
+ 'Integer(val=1):0:3:1',
+ },
+ },
+ },
+ },
+ },
+ err = {
+ arg = '&+1)',
+ msg = 'E112: Option name missing: %.*s',
+ },
+ }, {
+ hl('NestingParenthesis', '('),
+ hl('InvalidOptionSigil', '&'),
+ hl('BinaryPlus', '+'),
+ hl('Number', '1'),
+ hl('NestingParenthesis', ')'),
+ })
+ end)
+ itp('works with $ENV', function()
+ check_parsing('$', {
+ -- 0
+ ast = {
+ 'Environment(ident=):0:0:$',
+ },
+ err = {
+ arg = '$',
+ msg = 'E15: Environment variable name missing',
+ },
+ }, {
+ hl('InvalidEnvironmentSigil', '$'),
+ })
+
+ check_parsing('$g:A', {
+ -- 0123
+ ast = {
+ {
+ 'Colon:0:2::',
+ children = {
+ 'Environment(ident=g):0:0:$g',
+ 'PlainIdentifier(scope=0,ident=A):0:3:A',
+ },
+ },
+ },
+ err = {
+ arg = ':A',
+ msg = 'E15: Colon outside of dictionary or ternary operator: %.*s',
+ },
+ }, {
+ hl('EnvironmentSigil', '$'),
+ hl('EnvironmentName', 'g'),
+ hl('InvalidColon', ':'),
+ hl('IdentifierName', 'A'),
+ })
+
+ check_parsing('$A', {
+ -- 01
+ ast = {
+ 'Environment(ident=A):0:0:$A',
+ },
+ }, {
+ hl('EnvironmentSigil', '$'),
+ hl('EnvironmentName', 'A'),
+ })
+
+ check_parsing('$ABC', {
+ -- 0123
+ ast = {
+ 'Environment(ident=ABC):0:0:$ABC',
+ },
+ }, {
+ hl('EnvironmentSigil', '$'),
+ hl('EnvironmentName', 'ABC'),
+ })
+
+ check_parsing('(1+$)', {
+ -- 01234
+ ast = {
+ {
+ 'Nested:0:0:(',
+ children = {
+ {
+ 'BinaryPlus:0:2:+',
+ children = {
+ 'Integer(val=1):0:1:1',
+ 'Environment(ident=):0:3:$',
+ },
+ },
+ },
+ },
+ },
+ err = {
+ arg = '$)',
+ msg = 'E15: Environment variable name missing',
+ },
+ }, {
+ hl('NestingParenthesis', '('),
+ hl('Number', '1'),
+ hl('BinaryPlus', '+'),
+ hl('InvalidEnvironmentSigil', '$'),
+ hl('NestingParenthesis', ')'),
+ })
+
+ check_parsing('($+1)', {
+ -- 01234
+ ast = {
+ {
+ 'Nested:0:0:(',
+ children = {
+ {
+ 'BinaryPlus:0:2:+',
+ children = {
+ 'Environment(ident=):0:1:$',
+ 'Integer(val=1):0:3:1',
+ },
+ },
+ },
+ },
+ },
+ err = {
+ arg = '$+1)',
+ msg = 'E15: Environment variable name missing',
+ },
+ }, {
+ hl('NestingParenthesis', '('),
+ hl('InvalidEnvironmentSigil', '$'),
+ hl('BinaryPlus', '+'),
+ hl('Number', '1'),
+ hl('NestingParenthesis', ')'),
+ })
+
+ check_parsing('$_ABC', {
+ -- 01234
+ ast = {
+ 'Environment(ident=_ABC):0:0:$_ABC',
+ },
+ }, {
+ hl('EnvironmentSigil', '$'),
+ hl('EnvironmentName', '_ABC'),
+ })
+
+ check_parsing('$_', {
+ -- 01
+ ast = {
+ 'Environment(ident=_):0:0:$_',
+ },
+ }, {
+ hl('EnvironmentSigil', '$'),
+ hl('EnvironmentName', '_'),
+ })
+
+ check_parsing('$ABC_DEF', {
+ -- 01234567
+ ast = {
+ 'Environment(ident=ABC_DEF):0:0:$ABC_DEF',
+ },
+ }, {
+ hl('EnvironmentSigil', '$'),
+ hl('EnvironmentName', 'ABC_DEF'),
+ })
+ end)
+ itp('works with unary !', function()
+ check_parsing('!', {
+ -- 0
+ ast = {
+ 'Not:0:0:!',
+ },
+ err = {
+ arg = '',
+ msg = 'E15: Expected value, got EOC: %.*s',
+ },
+ }, {
+ hl('Not', '!'),
+ })
+
+ check_parsing('!!', {
+ -- 01
+ ast = {
+ {
+ 'Not:0:0:!',
+ children = {
+ 'Not:0:1:!',
+ },
+ },
+ },
+ err = {
+ arg = '',
+ msg = 'E15: Expected value, got EOC: %.*s',
+ },
+ }, {
+ hl('Not', '!'),
+ hl('Not', '!'),
+ })
+
+ check_parsing('!!1', {
+ -- 012
+ ast = {
+ {
+ 'Not:0:0:!',
+ children = {
+ {
+ 'Not:0:1:!',
+ children = {
+ 'Integer(val=1):0:2:1',
+ },
+ },
+ },
+ },
+ },
+ }, {
+ hl('Not', '!'),
+ hl('Not', '!'),
+ hl('Number', '1'),
+ })
+
+ check_parsing('!1', {
+ -- 01
+ ast = {
+ {
+ 'Not:0:0:!',
+ children = {
+ 'Integer(val=1):0:1:1',
+ },
+ },
+ },
+ }, {
+ hl('Not', '!'),
+ hl('Number', '1'),
+ })
+
+ check_parsing('(!1)', {
+ -- 0123
+ ast = {
+ {
+ 'Nested:0:0:(',
+ children = {
+ {
+ 'Not:0:1:!',
+ children = {
+ 'Integer(val=1):0:2:1',
+ },
+ },
+ },
+ },
+ },
+ }, {
+ hl('NestingParenthesis', '('),
+ hl('Not', '!'),
+ hl('Number', '1'),
+ hl('NestingParenthesis', ')'),
+ })
+
+ check_parsing('(!)', {
+ -- 012
+ ast = {
+ {
+ 'Nested:0:0:(',
+ children = {
+ {
+ 'Not:0:1:!',
+ children = {
+ 'Missing:0:2:',
+ },
+ },
+ },
+ },
+ },
+ err = {
+ arg = ')',
+ msg = 'E15: Expected value, got parenthesis: %.*s',
+ },
+ }, {
+ hl('NestingParenthesis', '('),
+ hl('Not', '!'),
+ hl('InvalidNestingParenthesis', ')'),
+ })
+
+ check_parsing('(1!2)', {
+ -- 01234
+ ast = {
+ {
+ 'Nested:0:0:(',
+ children = {
+ {
+ 'OpMissing:0:2:',
+ children = {
+ 'Integer(val=1):0:1:1',
+ {
+ 'Not:0:2:!',
+ children = {
+ 'Integer(val=2):0:3:2',
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ err = {
+ arg = '!2)',
+ msg = 'E15: Missing operator: %.*s',
+ },
+ }, {
+ hl('NestingParenthesis', '('),
+ hl('Number', '1'),
+ hl('InvalidNot', '!'),
+ hl('Number', '2'),
+ hl('NestingParenthesis', ')'),
+ })
+
+ check_parsing('1!2', {
+ -- 012
+ ast = {
+ {
+ 'OpMissing:0:1:',
+ children = {
+ 'Integer(val=1):0:0:1',
+ {
+ 'Not:0:1:!',
+ children = {
+ 'Integer(val=2):0:2:2',
+ },
+ },
+ },
+ },
+ },
+ err = {
+ arg = '!2',
+ msg = 'E15: Missing operator: %.*s',
+ },
+ }, {
+ hl('Number', '1'),
+ hl('InvalidNot', '!'),
+ hl('Number', '2'),
+ }, {
+ [1] = {
+ ast = {
+ len = 1,
+ err = REMOVE_THIS,
+ ast = {
+ 'Integer(val=1):0:0:1',
+ },
+ },
+ hl_fs = {
+ [2] = REMOVE_THIS,
+ [3] = REMOVE_THIS,
+ },
+ },
+ })
+ end)
+ itp('highlights numbers with prefix', function()
+ check_parsing('0xABCDEF', {
+ -- 01234567
+ ast = {
+ 'Integer(val=11259375):0:0:0xABCDEF',
+ },
+ }, {
+ hl('NumberPrefix', '0x'),
+ hl('Number', 'ABCDEF'),
+ })
+
+ check_parsing('0Xabcdef', {
+ -- 01234567
+ ast = {
+ 'Integer(val=11259375):0:0:0Xabcdef',
+ },
+ }, {
+ hl('NumberPrefix', '0X'),
+ hl('Number', 'abcdef'),
+ })
+
+ check_parsing('0XABCDEF', {
+ -- 01234567
+ ast = {
+ 'Integer(val=11259375):0:0:0XABCDEF',
+ },
+ }, {
+ hl('NumberPrefix', '0X'),
+ hl('Number', 'ABCDEF'),
+ })
+
+ check_parsing('0xabcdef', {
+ -- 01234567
+ ast = {
+ 'Integer(val=11259375):0:0:0xabcdef',
+ },
+ }, {
+ hl('NumberPrefix', '0x'),
+ hl('Number', 'abcdef'),
+ })
+
+ check_parsing('0b001', {
+ -- 01234
+ ast = {
+ 'Integer(val=1):0:0:0b001',
+ },
+ }, {
+ hl('NumberPrefix', '0b'),
+ hl('Number', '001'),
+ })
+
+ check_parsing('0B001', {
+ -- 01234
+ ast = {
+ 'Integer(val=1):0:0:0B001',
+ },
+ }, {
+ hl('NumberPrefix', '0B'),
+ hl('Number', '001'),
+ })
+
+ check_parsing('0B00', {
+ -- 0123
+ ast = {
+ 'Integer(val=0):0:0:0B00',
+ },
+ }, {
+ hl('NumberPrefix', '0B'),
+ hl('Number', '00'),
+ })
+
+ check_parsing('00', {
+ -- 01
+ ast = {
+ 'Integer(val=0):0:0:00',
+ },
+ }, {
+ hl('NumberPrefix', '0'),
+ hl('Number', '0'),
+ })
+
+ check_parsing('001', {
+ -- 012
+ ast = {
+ 'Integer(val=1):0:0:001',
+ },
+ }, {
+ hl('NumberPrefix', '0'),
+ hl('Number', '01'),
+ })
+
+ check_parsing('01', {
+ -- 01
+ ast = {
+ 'Integer(val=1):0:0:01',
+ },
+ }, {
+ hl('NumberPrefix', '0'),
+ hl('Number', '1'),
+ })
+
+ check_parsing('1', {
+ -- 0
+ ast = {
+ 'Integer(val=1):0:0:1',
+ },
+ }, {
+ hl('Number', '1'),
+ })
+ end)
+ itp('works (KLEE tests)', function()
+ check_parsing('\0002&A:\000', {
+ len = 0,
+ ast = nil,
+ err = {
+ arg = '\0002&A:\000',
+ msg = 'E15: Expected value, got EOC: %.*s',
+ },
+ }, {
+ }, {
+ [2] = {
+ ast = {
+ len = REMOVE_THIS,
+ ast = {
+ {
+ 'Colon:0:4::',
+ children = {
+ {
+ 'OpMissing:0:2:',
+ children = {
+ 'Integer(val=2):0:1:2',
+ 'Option(scope=0,ident=A):0:2:&A',
+ },
+ },
+ },
+ },
+ },
+ err = {
+ msg = 'E15: Unexpected EOC character: %.*s',
+ },
+ },
+ hl_fs = {
+ hl('InvalidSpacing', '\0'),
+ hl('Number', '2'),
+ hl('InvalidOptionSigil', '&'),
+ hl('InvalidOptionName', 'A'),
+ hl('InvalidColon', ':'),
+ hl('InvalidSpacing', '\0'),
+ },
+ },
+ [3] = {
+ ast = {
+ len = 2,
+ ast = {
+ 'Integer(val=2):0:1:2',
+ },
+ err = {
+ msg = 'E15: Unexpected EOC character: %.*s',
+ },
+ },
+ hl_fs = {
+ hl('InvalidSpacing', '\0'),
+ hl('Number', '2'),
+ },
+ },
+ })
+ check_parsing({data='01', size=1}, {
+ len = 1,
+ ast = {
+ 'Integer(val=0):0:0:0',
+ },
+ }, {
+ hl('Number', '0'),
+ })
+ check_parsing({data='001', size=2}, {
+ len = 2,
+ ast = {
+ 'Integer(val=0):0:0:00',
+ },
+ }, {
+ hl('NumberPrefix', '0'),
+ hl('Number', '0'),
+ })
+ check_parsing('"\\U\\', {
+ -- 0123
+ ast = {
+ [[DoubleQuotedString(val="U\\"):0:0:"\U\]],
+ },
+ err = {
+ arg = '"\\U\\',
+ msg = 'E114: Missing double quote: %.*s',
+ },
+ }, {
+ hl('InvalidDoubleQuote', '"'),
+ hl('InvalidDoubleQuotedUnknownEscape', '\\U'),
+ hl('InvalidDoubleQuotedBody', '\\'),
+ })
+ check_parsing('"\\U', {
+ -- 012
+ ast = {
+ fmtn('DoubleQuotedString', 'val="U"', ':0:0:"\\U'),
+ },
+ err = {
+ arg = '"\\U',
+ msg = 'E114: Missing double quote: %.*s',
+ },
+ }, {
+ hl('InvalidDoubleQuote', '"'),
+ hl('InvalidDoubleQuotedUnknownEscape', '\\U'),
+ })
+ check_parsing('|"\\U\\', {
+ -- 01234
+ len = 0,
+ err = {
+ arg = '|"\\U\\',
+ msg = 'E15: Expected value, got EOC: %.*s',
+ },
+ }, {
+ }, {
+ [2] = {
+ ast = {
+ len = REMOVE_THIS,
+ ast = {
+ {
+ 'Or:0:0:|',
+ children = {
+ 'Missing:0:0:',
+ fmtn('DoubleQuotedString', 'val="U\\\\"', ':0:1:"\\U\\'),
+ },
+ },
+ },
+ err = {
+ msg = 'E15: Unexpected EOC character: %.*s',
+ },
+ },
+ hl_fs = {
+ hl('InvalidOr', '|'),
+ hl('InvalidDoubleQuote', '"'),
+ hl('InvalidDoubleQuotedUnknownEscape', '\\U'),
+ hl('InvalidDoubleQuotedBody', '\\'),
+ },
+ },
+ })
+ check_parsing('|"\\e"', {
+ -- 01234
+ len = 0,
+ err = {
+ arg = '|"\\e"',
+ msg = 'E15: Expected value, got EOC: %.*s',
+ },
+ }, {
+ }, {
+ [2] = {
+ ast = {
+ len = REMOVE_THIS,
+ ast = {
+ {
+ 'Or:0:0:|',
+ children = {
+ 'Missing:0:0:',
+ fmtn('DoubleQuotedString', 'val="\\027"', ':0:1:"\\e"'),
+ },
+ },
+ },
+ err = {
+ msg = 'E15: Unexpected EOC character: %.*s',
+ },
+ },
+ hl_fs = {
+ hl('InvalidOr', '|'),
+ hl('DoubleQuote', '"'),
+ hl('DoubleQuotedEscape', '\\e'),
+ hl('DoubleQuote', '"'),
+ },
+ },
+ })
+ check_parsing('|\029', {
+ -- 01
+ len = 0,
+ err = {
+ arg = '|\029',
+ msg = 'E15: Expected value, got EOC: %.*s',
+ },
+ }, {
+ }, {
+ [2] = {
+ ast = {
+ len = REMOVE_THIS,
+ ast = {
+ {
+ 'Or:0:0:|',
+ children = {
+ 'Missing:0:0:',
+ 'PlainIdentifier(scope=0,ident=\029):0:1:\029',
+ },
+ },
+ },
+ err = {
+ msg = 'E15: Unexpected EOC character: %.*s',
+ },
+ },
+ hl_fs = {
+ hl('InvalidOr', '|'),
+ hl('InvalidIdentifierName', '\029'),
+ },
+ },
+ })
+ check_parsing('"\\<', {
+ -- 012
+ ast = {
+ fmtn('DoubleQuotedString', 'val="<"', ':0:0:"\\<'),
+ },
+ err = {
+ arg = '"\\<',
+ msg = 'E114: Missing double quote: %.*s',
+ },
+ }, {
+ hl('InvalidDoubleQuote', '"'),
+ hl('InvalidDoubleQuotedUnknownEscape', '\\<'),
+ })
+ check_parsing('"\\1', {
+ -- 01 2
+ ast = {
+ fmtn('DoubleQuotedString', 'val="\\001"', ':0:0:"\\1'),
+ },
+ err = {
+ arg = '"\\1',
+ msg = 'E114: Missing double quote: %.*s',
+ },
+ }, {
+ hl('InvalidDoubleQuote', '"'),
+ hl('InvalidDoubleQuotedEscape', '\\1'),
+ })
+ check_parsing('}l', {
+ -- 01
+ ast = {
+ {
+ 'OpMissing:0:1:',
+ children = {
+ fmtn('UnknownFigure', '---', ':0:0:'),
+ 'PlainIdentifier(scope=0,ident=l):0:1:l',
+ },
+ },
+ },
+ err = {
+ arg = '}l',
+ msg = 'E15: Unexpected closing figure brace: %.*s',
+ },
+ }, {
+ hl('InvalidFigureBrace', '}'),
+ hl('InvalidIdentifierName', 'l'),
+ }, {
+ [1] = {
+ ast = {
+ len = 1,
+ ast = {
+ fmtn('UnknownFigure', '---', ':0:0:'),
+ },
+ },
+ hl_fs = {
+ [2] = REMOVE_THIS,
+ },
+ },
+ })
+ check_parsing(':?\000\000\000\000\000\000\000', {
+ len = 2,
+ ast = {
+ {
+ 'Colon:0:0::',
+ children = {
+ 'Missing:0:0:',
+ {
+ 'Ternary:0:1:?',
+ children = {
+ 'Missing:0:1:',
+ 'TernaryValue:0:1:?',
+ },
+ },
+ },
+ },
+ },
+ err = {
+ arg = ':?\000\000\000\000\000\000\000',
+ msg = 'E15: Colon outside of dictionary or ternary operator: %.*s',
+ },
+ }, {
+ hl('InvalidColon', ':'),
+ hl('InvalidTernary', '?'),
+ }, {
+ [2] = {
+ ast = {
+ len = REMOVE_THIS,
+ },
+ hl_fs = {
+ [3] = hl('InvalidSpacing', '\0'),
+ [4] = hl('InvalidSpacing', '\0'),
+ [5] = hl('InvalidSpacing', '\0'),
+ [6] = hl('InvalidSpacing', '\0'),
+ [7] = hl('InvalidSpacing', '\0'),
+ [8] = hl('InvalidSpacing', '\0'),
+ [9] = hl('InvalidSpacing', '\0'),
+ },
+ },
+ })
+ end)
+ itp('works with assignments', function()
+ check_asgn_parsing('a=b', {
+ -- 012
+ ast = {
+ {
+ 'Assignment(Plain):0:1:=',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:0:a',
+ 'PlainIdentifier(scope=0,ident=b):0:2:b',
+ },
+ },
+ },
+ }, {
+ hl('IdentifierName', 'a'),
+ hl('PlainAssignment', '='),
+ hl('IdentifierName', 'b'),
+ })
+
+ check_asgn_parsing('a+=b', {
+ -- 0123
+ ast = {
+ {
+ 'Assignment(Add):0:1:+=',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:0:a',
+ 'PlainIdentifier(scope=0,ident=b):0:3:b',
+ },
+ },
+ },
+ }, {
+ hl('IdentifierName', 'a'),
+ hl('AssignmentWithAddition', '+='),
+ hl('IdentifierName', 'b'),
+ })
+
+ check_asgn_parsing('a-=b', {
+ -- 0123
+ ast = {
+ {
+ 'Assignment(Subtract):0:1:-=',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:0:a',
+ 'PlainIdentifier(scope=0,ident=b):0:3:b',
+ },
+ },
+ },
+ }, {
+ hl('IdentifierName', 'a'),
+ hl('AssignmentWithSubtraction', '-='),
+ hl('IdentifierName', 'b'),
+ })
+
+ check_asgn_parsing('a.=b', {
+ -- 0123
+ ast = {
+ {
+ 'Assignment(Concat):0:1:.=',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:0:a',
+ 'PlainIdentifier(scope=0,ident=b):0:3:b',
+ },
+ },
+ },
+ }, {
+ hl('IdentifierName', 'a'),
+ hl('AssignmentWithConcatenation', '.='),
+ hl('IdentifierName', 'b'),
+ })
+
+ check_asgn_parsing('a', {
+ -- 0
+ ast = {
+ 'PlainIdentifier(scope=0,ident=a):0:0:a',
+ },
+ }, {
+ hl('IdentifierName', 'a'),
+ })
+
+ check_asgn_parsing('a b', {
+ -- 012
+ ast = {
+ {
+ 'OpMissing:0:1:',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:0:a',
+ 'PlainIdentifier(scope=0,ident=b):0:1: b',
+ },
+ },
+ },
+ err = {
+ arg = 'b',
+ msg = 'E15: Expected assignment operator or subscript: %.*s',
+ },
+ }, {
+ hl('IdentifierName', 'a'),
+ hl('InvalidSpacing', ' '),
+ hl('IdentifierName', 'b'),
+ }, {
+ [5] = {
+ ast = {
+ len = 2,
+ ast = {
+ 'PlainIdentifier(scope=0,ident=a):0:0:a',
+ },
+ err = REMOVE_THIS,
+ },
+ hl_fs = {
+ [2] = REMOVE_THIS,
+ [3] = REMOVE_THIS,
+ }
+ },
+ })
+
+ check_asgn_parsing('[a, b, c]', {
+ -- 012345678
+ ast = {
+ {
+ 'ListLiteral:0:0:[',
+ children = {
+ {
+ 'Comma:0:2:,',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:1:a',
+ {
+ 'Comma:0:5:,',
+ children = {
+ 'PlainIdentifier(scope=0,ident=b):0:3: b',
+ 'PlainIdentifier(scope=0,ident=c):0:6: c',
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ }, {
+ hl('List', '['),
+ hl('IdentifierName', 'a'),
+ hl('Comma', ','),
+ hl('IdentifierName', 'b', 1),
+ hl('Comma', ','),
+ hl('IdentifierName', 'c', 1),
+ hl('List', ']'),
+ })
+
+ check_asgn_parsing('[a, b]', {
+ -- 012345
+ ast = {
+ {
+ 'ListLiteral:0:0:[',
+ children = {
+ {
+ 'Comma:0:2:,',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:1:a',
+ 'PlainIdentifier(scope=0,ident=b):0:3: b',
+ },
+ },
+ },
+ },
+ },
+ }, {
+ hl('List', '['),
+ hl('IdentifierName', 'a'),
+ hl('Comma', ','),
+ hl('IdentifierName', 'b', 1),
+ hl('List', ']'),
+ })
+
+ check_asgn_parsing('[a]', {
+ -- 012
+ ast = {
+ {
+ 'ListLiteral:0:0:[',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:1:a',
+ },
+ },
+ },
+ }, {
+ hl('List', '['),
+ hl('IdentifierName', 'a'),
+ hl('List', ']'),
+ })
+
+ check_asgn_parsing('[]', {
+ -- 01
+ ast = {
+ 'ListLiteral:0:0:[',
+ },
+ err = {
+ arg = ']',
+ msg = 'E475: Unable to assign to empty list: %.*s',
+ },
+ }, {
+ hl('List', '['),
+ hl('InvalidList', ']'),
+ })
+
+ check_asgn_parsing('a[1] += 3', {
+ -- 012345678
+ ast = {
+ {
+ 'Assignment(Add):0:4: +=',
+ children = {
+ {
+ 'Subscript:0:1:[',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:0:a',
+ 'Integer(val=1):0:2:1',
+ },
+ },
+ 'Integer(val=3):0:7: 3',
+ },
+ },
+ },
+ }, {
+ hl('IdentifierName', 'a'),
+ hl('SubscriptBracket', '['),
+ hl('Number', '1'),
+ hl('SubscriptBracket', ']'),
+ hl('AssignmentWithAddition', '+=', 1),
+ hl('Number', '3', 1),
+ })
+
+ check_asgn_parsing('a[1 + 2] += 3', {
+ -- 0123456789012
+ -- 0 1
+ ast = {
+ {
+ 'Assignment(Add):0:8: +=',
+ children = {
+ {
+ 'Subscript:0:1:[',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:0:a',
+ {
+ 'BinaryPlus:0:3: +',
+ children = {
+ 'Integer(val=1):0:2:1',
+ 'Integer(val=2):0:5: 2',
+ },
+ },
+ },
+ },
+ 'Integer(val=3):0:11: 3',
+ },
+ },
+ },
+ }, {
+ hl('IdentifierName', 'a'),
+ hl('SubscriptBracket', '['),
+ hl('Number', '1'),
+ hl('BinaryPlus', '+', 1),
+ hl('Number', '2', 1),
+ hl('SubscriptBracket', ']'),
+ hl('AssignmentWithAddition', '+=', 1),
+ hl('Number', '3', 1),
+ })
+
+ check_asgn_parsing('a[{-> {b{3}: 4}[5]}()] += 6', {
+ -- 012345678901234567890123456
+ -- 0 1 2
+ ast = {
+ {
+ 'Assignment(Add):0:22: +=',
+ children = {
+ {
+ 'Subscript:0:1:[',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:0:a',
+ {
+ 'Call:0:19:(',
+ children = {
+ {
+ fmtn('Lambda', '\\di', ':0:2:{'),
+ children = {
+ {
+ 'Arrow:0:3:->',
+ children = {
+ {
+ 'Subscript:0:15:[',
+ children = {
+ {
+ fmtn('DictLiteral', '-di', ':0:5: {'),
+ children = {
+ {
+ 'Colon:0:11::',
+ children = {
+ {
+ 'ComplexIdentifier:0:8:',
+ children = {
+ 'PlainIdentifier(scope=0,ident=b):0:7:b',
+ {
+ fmtn('CurlyBracesIdentifier', '--i', ':0:8:{'),
+ children = {
+ 'Integer(val=3):0:9:3',
+ },
+ },
+ },
+ },
+ 'Integer(val=4):0:12: 4',
+ },
+ },
+ },
+ },
+ 'Integer(val=5):0:16:5',
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ 'Integer(val=6):0:25: 6',
+ },
+ },
+ },
+ }, {
+ hl('IdentifierName', 'a'),
+ hl('SubscriptBracket', '['),
+ hl('Lambda', '{'),
+ hl('Arrow', '->'),
+ hl('Dict', '{', 1),
+ hl('IdentifierName', 'b'),
+ hl('Curly', '{'),
+ hl('Number', '3'),
+ hl('Curly', '}'),
+ hl('Colon', ':'),
+ hl('Number', '4', 1),
+ hl('Dict', '}'),
+ hl('SubscriptBracket', '['),
+ hl('Number', '5'),
+ hl('SubscriptBracket', ']'),
+ hl('Lambda', '}'),
+ hl('CallingParenthesis', '('),
+ hl('CallingParenthesis', ')'),
+ hl('SubscriptBracket', ']'),
+ hl('AssignmentWithAddition', '+=', 1),
+ hl('Number', '6', 1),
+ })
+
+ check_asgn_parsing('a{1}.2[{-> {b{3}: 4}[5]}()]', {
+ -- 012345678901234567890123456
+ -- 0 1 2
+ ast = {
+ {
+ 'Subscript:0:6:[',
+ children = {
+ {
+ 'ConcatOrSubscript:0:4:.',
+ children = {
+ {
+ 'ComplexIdentifier:0:1:',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:0:a',
+ {
+ fmtn('CurlyBracesIdentifier', '--i', ':0:1:{'),
+ children = {
+ 'Integer(val=1):0:2:1',
+ },
+ },
+ },
+ },
+ 'PlainKey(key=2):0:5:2',
+ },
+ },
+ {
+ 'Call:0:24:(',
+ children = {
+ {
+ fmtn('Lambda', '\\di', ':0:7:{'),
+ children = {
+ {
+ 'Arrow:0:8:->',
+ children = {
+ {
+ 'Subscript:0:20:[',
+ children = {
+ {
+ fmtn('DictLiteral', '-di', ':0:10: {'),
+ children = {
+ {
+ 'Colon:0:16::',
+ children = {
+ {
+ 'ComplexIdentifier:0:13:',
+ children = {
+ 'PlainIdentifier(scope=0,ident=b):0:12:b',
+ {
+ fmtn('CurlyBracesIdentifier', '--i', ':0:13:{'),
+ children = {
+ 'Integer(val=3):0:14:3',
+ },
+ },
+ },
+ },
+ 'Integer(val=4):0:17: 4',
+ },
+ },
+ },
+ },
+ 'Integer(val=5):0:21:5',
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ }, {
+ hl('IdentifierName', 'a'),
+ hl('Curly', '{'),
+ hl('Number', '1'),
+ hl('Curly', '}'),
+ hl('ConcatOrSubscript', '.'),
+ hl('IdentifierKey', '2'),
+ hl('SubscriptBracket', '['),
+ hl('Lambda', '{'),
+ hl('Arrow', '->'),
+ hl('Dict', '{', 1),
+ hl('IdentifierName', 'b'),
+ hl('Curly', '{'),
+ hl('Number', '3'),
+ hl('Curly', '}'),
+ hl('Colon', ':'),
+ hl('Number', '4', 1),
+ hl('Dict', '}'),
+ hl('SubscriptBracket', '['),
+ hl('Number', '5'),
+ hl('SubscriptBracket', ']'),
+ hl('Lambda', '}'),
+ hl('CallingParenthesis', '('),
+ hl('CallingParenthesis', ')'),
+ hl('SubscriptBracket', ']'),
+ })
+
+ check_asgn_parsing('a', {
+ -- 0
+ ast = {
+ 'PlainIdentifier(scope=0,ident=a):0:0:a',
+ },
+ }, {
+ hl('IdentifierName', 'a'),
+ })
+
+ check_asgn_parsing('{a}', {
+ -- 012
+ ast = {
+ {
+ fmtn('CurlyBracesIdentifier', '--i', ':0:0:{'),
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:1:a',
+ },
+ },
+ },
+ }, {
+ hl('FigureBrace', '{'),
+ hl('IdentifierName', 'a'),
+ hl('Curly', '}'),
+ })
+
+ check_asgn_parsing('{a}b', {
+ -- 0123
+ ast = {
+ {
+ 'ComplexIdentifier:0:3:',
+ children = {
+ {
+ fmtn('CurlyBracesIdentifier', '--i', ':0:0:{'),
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:1:a',
+ },
+ },
+ 'PlainIdentifier(scope=0,ident=b):0:3:b',
+ },
+ },
+ },
+ }, {
+ hl('FigureBrace', '{'),
+ hl('IdentifierName', 'a'),
+ hl('Curly', '}'),
+ hl('IdentifierName', 'b'),
+ })
+
+ check_asgn_parsing('a{b}c', {
+ -- 01234
+ ast = {
+ {
+ 'ComplexIdentifier:0:1:',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:0:a',
+ {
+ 'ComplexIdentifier:0:4:',
+ children = {
+ {
+ fmtn('CurlyBracesIdentifier', '--i', ':0:1:{'),
+ children = {
+ 'PlainIdentifier(scope=0,ident=b):0:2:b',
+ },
+ },
+ 'PlainIdentifier(scope=0,ident=c):0:4:c',
+ },
+ },
+ },
+ },
+ },
+ }, {
+ hl('IdentifierName', 'a'),
+ hl('Curly', '{'),
+ hl('IdentifierName', 'b'),
+ hl('Curly', '}'),
+ hl('IdentifierName', 'c'),
+ })
+
+ check_asgn_parsing('a{b}c[0]', {
+ -- 01234567
+ ast = {
+ {
+ 'Subscript:0:5:[',
+ children = {
+ {
+ 'ComplexIdentifier:0:1:',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:0:a',
+ {
+ 'ComplexIdentifier:0:4:',
+ children = {
+ {
+ fmtn('CurlyBracesIdentifier', '--i', ':0:1:{'),
+ children = {
+ 'PlainIdentifier(scope=0,ident=b):0:2:b',
+ },
+ },
+ 'PlainIdentifier(scope=0,ident=c):0:4:c',
+ },
+ },
+ },
+ },
+ 'Integer(val=0):0:6:0',
+ },
+ },
+ },
+ }, {
+ hl('IdentifierName', 'a'),
+ hl('Curly', '{'),
+ hl('IdentifierName', 'b'),
+ hl('Curly', '}'),
+ hl('IdentifierName', 'c'),
+ hl('SubscriptBracket', '['),
+ hl('Number', '0'),
+ hl('SubscriptBracket', ']'),
+ })
+
+ check_asgn_parsing('a{b}c.0', {
+ -- 0123456
+ ast = {
+ {
+ 'ConcatOrSubscript:0:5:.',
+ children = {
+ {
+ 'ComplexIdentifier:0:1:',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:0:a',
+ {
+ 'ComplexIdentifier:0:4:',
+ children = {
+ {
+ fmtn('CurlyBracesIdentifier', '--i', ':0:1:{'),
+ children = {
+ 'PlainIdentifier(scope=0,ident=b):0:2:b',
+ },
+ },
+ 'PlainIdentifier(scope=0,ident=c):0:4:c',
+ },
+ },
+ },
+ },
+ 'PlainKey(key=0):0:6:0',
+ },
+ },
+ },
+ }, {
+ hl('IdentifierName', 'a'),
+ hl('Curly', '{'),
+ hl('IdentifierName', 'b'),
+ hl('Curly', '}'),
+ hl('IdentifierName', 'c'),
+ hl('ConcatOrSubscript', '.'),
+ hl('IdentifierKey', '0'),
+ })
+
+ check_asgn_parsing('[a{b}c[0].0]', {
+ -- 012345678901
+ -- 0 1
+ ast = {
+ {
+ 'ListLiteral:0:0:[',
+ children = {
+ {
+ 'ConcatOrSubscript:0:9:.',
+ children = {
+ {
+ 'Subscript:0:6:[',
+ children = {
+ {
+ 'ComplexIdentifier:0:2:',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:1:a',
+ {
+ 'ComplexIdentifier:0:5:',
+ children = {
+ {
+ fmtn('CurlyBracesIdentifier', '--i', ':0:2:{'),
+ children = {
+ 'PlainIdentifier(scope=0,ident=b):0:3:b',
+ },
+ },
+ 'PlainIdentifier(scope=0,ident=c):0:5:c',
+ },
+ },
+ },
+ },
+ 'Integer(val=0):0:7:0',
+ },
+ },
+ 'PlainKey(key=0):0:10:0',
+ },
+ },
+ },
+ },
+ },
+ }, {
+ hl('List', '['),
+ hl('IdentifierName', 'a'),
+ hl('Curly', '{'),
+ hl('IdentifierName', 'b'),
+ hl('Curly', '}'),
+ hl('IdentifierName', 'c'),
+ hl('SubscriptBracket', '['),
+ hl('Number', '0'),
+ hl('SubscriptBracket', ']'),
+ hl('ConcatOrSubscript', '.'),
+ hl('IdentifierKey', '0'),
+ hl('List', ']'),
+ })
+
+ check_asgn_parsing('{a}{b}', {
+ -- 012345
+ ast = {
+ {
+ 'ComplexIdentifier:0:3:',
+ children = {
+ {
+ fmtn('CurlyBracesIdentifier', '--i', ':0:0:{'),
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:1:a',
+ },
+ },
+ {
+ fmtn('CurlyBracesIdentifier', '--i', ':0:3:{'),
+ children = {
+ 'PlainIdentifier(scope=0,ident=b):0:4:b',
+ },
+ },
+ },
+ },
+ },
+ }, {
+ hl('FigureBrace', '{'),
+ hl('IdentifierName', 'a'),
+ hl('Curly', '}'),
+ hl('Curly', '{'),
+ hl('IdentifierName', 'b'),
+ hl('Curly', '}'),
+ })
+
+ check_asgn_parsing('a.b{c}{d}', {
+ -- 012345678
+ ast = {
+ {
+ 'OpMissing:0:3:',
+ children = {
+ {
+ 'ConcatOrSubscript:0:1:.',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:0:a',
+ 'PlainKey(key=b):0:2:b',
+ },
+ },
+ {
+ 'ComplexIdentifier:0:6:',
+ children = {
+ {
+ fmtn('CurlyBracesIdentifier', '--i', ':0:3:{'),
+ children = {
+ 'PlainIdentifier(scope=0,ident=c):0:4:c',
+ },
+ },
+ {
+ fmtn('CurlyBracesIdentifier', '--i', ':0:6:{'),
+ children = {
+ 'PlainIdentifier(scope=0,ident=d):0:7:d',
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ err = {
+ arg = '{c}{d}',
+ msg = 'E15: Missing operator: %.*s',
+ },
+ }, {
+ hl('IdentifierName', 'a'),
+ hl('ConcatOrSubscript', '.'),
+ hl('IdentifierKey', 'b'),
+ hl('InvalidFigureBrace', '{'),
+ hl('IdentifierName', 'c'),
+ hl('Curly', '}'),
+ hl('Curly', '{'),
+ hl('IdentifierName', 'd'),
+ hl('Curly', '}'),
+ })
+
+ check_asgn_parsing('[a] = 1', {
+ -- 0123456
+ ast = {
+ {
+ 'Assignment(Plain):0:3: =',
+ children = {
+ {
+ 'ListLiteral:0:0:[',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:1:a',
+ },
+ },
+ 'Integer(val=1):0:5: 1',
+ },
+ },
+ },
+ }, {
+ hl('List', '['),
+ hl('IdentifierName', 'a'),
+ hl('List', ']'),
+ hl('PlainAssignment', '=', 1),
+ hl('Number', '1', 1),
+ })
+
+ check_asgn_parsing('[a[b], [c, [d, [e]]]] = 1', {
+ -- 0123456789012345678901234
+ -- 0 1 2
+ ast = {
+ {
+ 'Assignment(Plain):0:21: =',
+ children = {
+ {
+ 'ListLiteral:0:0:[',
+ children = {
+ {
+ 'Comma:0:5:,',
+ children = {
+ {
+ 'Subscript:0:2:[',
+ children = {
+ 'PlainIdentifier(scope=0,ident=a):0:1:a',
+ 'PlainIdentifier(scope=0,ident=b):0:3:b',
+ },
+ },
+ {
+ 'ListLiteral:0:6: [',
+ children = {
+ {
+ 'Comma:0:9:,',
+ children = {
+ 'PlainIdentifier(scope=0,ident=c):0:8:c',
+ {
+ 'ListLiteral:0:10: [',
+ children = {
+ {
+ 'Comma:0:13:,',
+ children = {
+ 'PlainIdentifier(scope=0,ident=d):0:12:d',
+ {
+ 'ListLiteral:0:14: [',
+ children = {
+ 'PlainIdentifier(scope=0,ident=e):0:16:e',
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ 'Integer(val=1):0:23: 1',
+ },
+ },
+ },
+ err = {
+ arg = '[c, [d, [e]]]] = 1',
+ msg = 'E475: Nested lists not allowed when assigning: %.*s',
+ },
+ }, {
+ hl('List', '['),
+ hl('IdentifierName', 'a'),
+ hl('SubscriptBracket', '['),
+ hl('IdentifierName', 'b'),
+ hl('SubscriptBracket', ']'),
+ hl('Comma', ','),
+ hl('InvalidList', '[', 1),
+ hl('IdentifierName', 'c'),
+ hl('Comma', ','),
+ hl('InvalidList', '[', 1),
+ hl('IdentifierName', 'd'),
+ hl('Comma', ','),
+ hl('InvalidList', '[', 1),
+ hl('IdentifierName', 'e'),
+ hl('List', ']'),
+ hl('List', ']'),
+ hl('List', ']'),
+ hl('List', ']'),
+ hl('PlainAssignment', '=', 1),
+ hl('Number', '1', 1),
+ })
+
+ check_asgn_parsing('$X += 1', {
+ -- 0123456
+ ast = {
+ {
+ 'Assignment(Add):0:2: +=',
+ children = {
+ 'Environment(ident=X):0:0:$X',
+ 'Integer(val=1):0:5: 1',
+ },
+ },
+ },
+ }, {
+ hl('EnvironmentSigil', '$'),
+ hl('EnvironmentName', 'X'),
+ hl('AssignmentWithAddition', '+=', 1),
+ hl('Number', '1', 1),
+ })
+
+ check_asgn_parsing('@a .= 1', {
+ -- 0123456
+ ast = {
+ {
+ 'Assignment(Concat):0:2: .=',
+ children = {
+ 'Register(name=a):0:0:@a',
+ 'Integer(val=1):0:5: 1',
+ },
+ },
+ },
+ }, {
+ hl('Register', '@a'),
+ hl('AssignmentWithConcatenation', '.=', 1),
+ hl('Number', '1', 1),
+ })
+
+ check_asgn_parsing('&option -= 1', {
+ -- 012345678901
+ -- 0 1
+ ast = {
+ {
+ 'Assignment(Subtract):0:7: -=',
+ children = {
+ 'Option(scope=0,ident=option):0:0:&option',
+ 'Integer(val=1):0:10: 1',
+ },
+ },
+ },
+ }, {
+ hl('OptionSigil', '&'),
+ hl('OptionName', 'option'),
+ hl('AssignmentWithSubtraction', '-=', 1),
+ hl('Number', '1', 1),
+ })
+
+ check_asgn_parsing('[$X, @a, &l:option] = [1, 2, 3]', {
+ -- 0123456789012345678901234567890
+ -- 0 1 2 3
+ ast = {
+ {
+ 'Assignment(Plain):0:19: =',
+ children = {
+ {
+ 'ListLiteral:0:0:[',
+ children = {
+ {
+ 'Comma:0:3:,',
+ children = {
+ 'Environment(ident=X):0:1:$X',
+ {
+ 'Comma:0:7:,',
+ children = {
+ 'Register(name=a):0:4: @a',
+ 'Option(scope=l,ident=option):0:8: &l:option',
+ },
+ },
+ },
+ },
+ },
+ },
+ {
+ 'ListLiteral:0:21: [',
+ children = {
+ {
+ 'Comma:0:24:,',
+ children = {
+ 'Integer(val=1):0:23:1',
+ {
+ 'Comma:0:27:,',
+ children = {
+ 'Integer(val=2):0:25: 2',
+ 'Integer(val=3):0:28: 3',
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ }, {
+ hl('List', '['),
+ hl('EnvironmentSigil', '$'),
+ hl('EnvironmentName', 'X'),
+ hl('Comma', ','),
+ hl('Register', '@a', 1),
+ hl('Comma', ','),
+ hl('OptionSigil', '&', 1),
+ hl('OptionScope', 'l'),
+ hl('OptionScopeDelimiter', ':'),
+ hl('OptionName', 'option'),
+ hl('List', ']'),
+ hl('PlainAssignment', '=', 1),
+ hl('List', '[', 1),
+ hl('Number', '1'),
+ hl('Comma', ','),
+ hl('Number', '2', 1),
+ hl('Comma', ','),
+ hl('Number', '3', 1),
+ hl('List', ']'),
+ })
+ end)
+ itp('works with non-ASCII characters', function()
+ check_parsing('"«»"«»', {
+ -- 013568
+ ast = {
+ {
+ 'OpMissing:0:6:',
+ children = {
+ 'DoubleQuotedString(val="«»"):0:0:"«»"',
+ {
+ 'ComplexIdentifier:0:8:',
+ children = {
+ 'PlainIdentifier(scope=0,ident=«):0:6:«',
+ 'PlainIdentifier(scope=0,ident=»):0:8:»',
+ },
+ },
+ },
+ },
+ },
+ err = {
+ arg = '«»',
+ msg = 'E15: Unidentified character: %.*s',
+ },
+ }, {
+ hl('DoubleQuote', '"'),
+ hl('DoubleQuotedBody', '«»'),
+ hl('DoubleQuote', '"'),
+ hl('InvalidIdentifierName', '«'),
+ hl('InvalidIdentifierName', '»'),
+ }, {
+ [1] = {
+ ast = {
+ ast = {
+ 'DoubleQuotedString(val="«»"):0:0:"«»"',
+ },
+ len = 6,
+ },
+ hl_fs = {
+ [5] = REMOVE_THIS,
+ [4] = REMOVE_THIS,
+ },
+ },
+ })
+ check_parsing('"\192"\192"foo"', {
+ -- 01 23 45678
+ ast = {
+ {
+ 'OpMissing:0:3:',
+ children = {
+ 'DoubleQuotedString(val="\192"):0:0:"\192"',
+ {
+ 'OpMissing:0:4:',
+ children = {
+ 'PlainIdentifier(scope=0,ident=\192):0:3:\192',
+ 'DoubleQuotedString(val="foo"):0:4:"foo"',
+ },
+ },
+ },
+ },
+ },
+ err = {
+ arg = '\192"foo"',
+ msg = 'E15: Unidentified character: %.*s',
+ },
+ }, {
+ hl('DoubleQuote', '"'),
+ hl('DoubleQuotedBody', '\192'),
+ hl('DoubleQuote', '"'),
+ hl('InvalidIdentifierName', '\192'),
+ hl('InvalidDoubleQuote', '"'),
+ hl('InvalidDoubleQuotedBody', 'foo'),
+ hl('InvalidDoubleQuote', '"'),
+ }, {
+ [1] = {
+ ast = {
+ ast = {
+ 'DoubleQuotedString(val="\192"):0:0:"\192"',
+ },
+ len = 3,
+ },
+ hl_fs = {
+ [4] = REMOVE_THIS,
+ [5] = REMOVE_THIS,
+ [6] = REMOVE_THIS,
+ [7] = REMOVE_THIS,
+ },
+ },
+ })
+ end)
+end
diff --git a/test/unit/viml/helpers.lua b/test/unit/viml/helpers.lua
new file mode 100644
index 0000000000..9d8102e023
--- /dev/null
+++ b/test/unit/viml/helpers.lua
@@ -0,0 +1,130 @@
+local helpers = require('test.unit.helpers')(nil)
+
+local ffi = helpers.ffi
+local cimport = helpers.cimport
+local kvi_new = helpers.kvi_new
+local kvi_init = helpers.kvi_init
+local conv_enum = helpers.conv_enum
+local make_enum_conv_tab = helpers.make_enum_conv_tab
+
+local lib = cimport('./src/nvim/viml/parser/expressions.h')
+
+local function new_pstate(strings)
+ local strings_idx = 0
+ local function get_line(_, ret_pline)
+ strings_idx = strings_idx + 1
+ local str = strings[strings_idx]
+ local data, size
+ if type(str) == 'string' then
+ data = str
+ size = #str
+ elseif type(str) == 'nil' then
+ data = nil
+ size = 0
+ elseif type(str) == 'table' then
+ data = str.data
+ size = str.size
+ elseif type(str) == 'function' then
+ data, size = str()
+ size = size or 0
+ end
+ ret_pline.data = data
+ ret_pline.size = size
+ ret_pline.allocated = false
+ end
+ local state = {
+ reader = {
+ get_line = get_line,
+ cookie = nil,
+ conv = {
+ vc_type = 0,
+ vc_factor = 1,
+ vc_fail = false,
+ },
+ },
+ pos = { line = 0, col = 0 },
+ colors = kvi_new('ParserHighlight'),
+ can_continuate = false,
+ }
+ local ret = ffi.new('ParserState', state)
+ kvi_init(ret.reader.lines)
+ kvi_init(ret.stack)
+ return ret
+end
+
+local function pline2lua(pline)
+ return ffi.string(pline.data, pline.size)
+end
+
+local function pstate_str(pstate, start, len)
+ local str = nil
+ local err = nil
+ if start.line < pstate.reader.lines.size then
+ local pstr = pline2lua(pstate.reader.lines.items[start.line])
+ if start.col >= #pstr then
+ err = 'start.col >= #pstr'
+ else
+ str = pstr:sub(tonumber(start.col) + 1, tonumber(start.col + len))
+ end
+ else
+ err = 'start.line >= pstate.reader.lines.size'
+ end
+ return str, err
+end
+
+local function pstate_set_str(pstate, start, len, ret)
+ ret = ret or {}
+ ret.start = {
+ line = tonumber(start.line),
+ col = tonumber(start.col)
+ }
+ ret.len = tonumber(len)
+ ret.str, ret.error = pstate_str(pstate, start, len)
+ return ret
+end
+
+local eltkn_cmp_type_tab
+make_enum_conv_tab(lib, {
+ 'kExprCmpEqual',
+ 'kExprCmpMatches',
+ 'kExprCmpGreater',
+ 'kExprCmpGreaterOrEqual',
+ 'kExprCmpIdentical',
+}, 'kExprCmp', function(ret) eltkn_cmp_type_tab = ret end)
+
+local function conv_cmp_type(typ)
+ return conv_enum(eltkn_cmp_type_tab, typ)
+end
+
+local ccs_tab
+make_enum_conv_tab(lib, {
+ 'kCCStrategyUseOption',
+ 'kCCStrategyMatchCase',
+ 'kCCStrategyIgnoreCase',
+}, 'kCCStrategy', function(ret) ccs_tab = ret end)
+
+local function conv_ccs(ccs)
+ return conv_enum(ccs_tab, ccs)
+end
+
+local expr_asgn_type_tab
+make_enum_conv_tab(lib, {
+ 'kExprAsgnPlain',
+ 'kExprAsgnAdd',
+ 'kExprAsgnSubtract',
+ 'kExprAsgnConcat',
+}, 'kExprAsgn', function(ret) expr_asgn_type_tab = ret end)
+
+local function conv_expr_asgn_type(expr_asgn_type)
+ return conv_enum(expr_asgn_type_tab, expr_asgn_type)
+end
+
+return {
+ conv_ccs = conv_ccs,
+ pline2lua = pline2lua,
+ pstate_str = pstate_str,
+ new_pstate = new_pstate,
+ conv_cmp_type = conv_cmp_type,
+ pstate_set_str = pstate_set_str,
+ conv_expr_asgn_type = conv_expr_asgn_type,
+}