diff options
Diffstat (limited to 'test/unit/garray_spec.lua')
-rw-r--r-- | test/unit/garray_spec.lua | 391 |
1 files changed, 391 insertions, 0 deletions
diff --git a/test/unit/garray_spec.lua b/test/unit/garray_spec.lua new file mode 100644 index 0000000000..c0c6d7e9ce --- /dev/null +++ b/test/unit/garray_spec.lua @@ -0,0 +1,391 @@ +local helpers = require("test.unit.helpers") + +local cimport = helpers.cimport +local internalize = helpers.internalize +local eq = helpers.eq +local neq = helpers.neq +local ffi = helpers.ffi +local lib = helpers.lib +local cstr = helpers.cstr +local to_cstr = helpers.to_cstr +local NULL = helpers.NULL + +local garray = cimport('./src/nvim/garray.h') + +-- define a basic interface to garray. We could make it a lot nicer by +-- constructing a class wrapper around garray. It could for example associate +-- ga_clear_strings to the underlying garray cdata if the garray is a string +-- array. But for now I estimate that that kind of magic might make testing +-- less "transparant" (i.e.: the interface would become quite different as to +-- how one would use it from C. + +-- accessors +function ga_len(garr) + return garr[0].ga_len +end + +function ga_maxlen(garr) + return garr[0].ga_maxlen +end + +function ga_itemsize(garr) + return garr[0].ga_itemsize +end + +function ga_growsize(garr) + return garr[0].ga_growsize +end + +function ga_data(garr) + return garr[0].ga_data +end + +-- derived accessors +function ga_size(garr) + return ga_len(garr) * ga_itemsize(garr) +end + +function ga_maxsize(garr) + return ga_maxlen(garr) * ga_itemsize(garr) +end + +function ga_data_as_bytes(garr) + return ffi.cast('uint8_t *', ga_data(garr)) +end + +function ga_data_as_strings(garr) + return ffi.cast('char **', ga_data(garr)) +end + +function ga_data_as_ints(garr) + return ffi.cast('int *', ga_data(garr)) +end + +-- garray manipulation +function ga_init(garr, itemsize, growsize) + return garray.ga_init(garr, itemsize, growsize) +end + +function ga_clear(garr) + return garray.ga_clear(garr) +end + +function ga_clear_strings(garr) + assert.is_true(ga_itemsize(garr) == ffi.sizeof('char *')) + return garray.ga_clear_strings(garr) +end + +function ga_grow(garr, n) + return garray.ga_grow(garr, n) +end + +function ga_concat(garr, str) + return garray.ga_concat(garr, to_cstr(str)) +end + +function ga_append(garr, b) + if type(b) == 'string' then + return garray.ga_append(garr, string.byte(b)) + else + return garray.ga_append(garr, b) + end +end + +function ga_concat_strings(garr) + return internalize(garray.ga_concat_strings(garr)) +end + +function ga_concat_strings_sep(garr, sep) + return internalize(garray.ga_concat_strings_sep(garr, to_cstr(sep))) +end + +function ga_remove_duplicate_strings(garr) + return garray.ga_remove_duplicate_strings(garr) +end + +-- derived manipulators +function ga_set_len(garr, len) + assert.is_true(len <= ga_maxlen(garr)) + garr[0].ga_len = len +end + +function ga_inc_len(garr, by) + return ga_set_len(garr, ga_len(garr) + 1) +end + +-- custom append functions +-- not the C ga_append, which only works for bytes +function ga_append_int(garr, it) + assert.is_true(ga_itemsize(garr) == ffi.sizeof('int')) + ga_grow(garr, 1) + local data = ga_data_as_ints(garr) + data[ga_len(garr)] = it + return ga_inc_len(garr, 1) +end + +function ga_append_string(garr, it) + assert.is_true(ga_itemsize(garr) == ffi.sizeof('char *')) + -- make a non-garbage collected string and copy the lua string into it, + -- TODO(aktau): we should probably call xmalloc here, though as long as + -- xmalloc is based on malloc it should work. + local mem = ffi.C.malloc(string.len(it) + 1) + ffi.copy(mem, it) + ga_grow(garr, 1) + local data = ga_data_as_strings(garr) + data[ga_len(garr)] = mem + return ga_inc_len(garr, 1) +end + +function ga_append_strings(garr, ...) + local prevlen = ga_len(garr) + local len = select('#', ...) + for i = 1, len do + ga_append_string(garr, select(i, ...)) + end + return eq(prevlen + len, ga_len(garr)) +end + +function ga_append_ints(garr, ...) + local prevlen = ga_len(garr) + local len = select('#', ...) + for i = 1, len do + ga_append_int(garr, select(i, ...)) + end + return eq(prevlen + len, ga_len(garr)) +end + +-- enhanced constructors +local garray_ctype = ffi.typeof('garray_T[1]') +function new_garray() + local garr = garray_ctype() + return ffi.gc(garr, ga_clear) +end + +function new_string_garray() + local garr = garray_ctype() + ga_init(garr, ffi.sizeof("char_u *"), 1) + return ffi.gc(garr, ga_clear_strings) +end + +function randomByte() + return ffi.cast('uint8_t', math.random(0, 255)) +end + +-- scramble the data in a garray +function ga_scramble(garr) + local size, bytes = ga_size(garr), ga_data_as_bytes(garr) + for i = 0, size - 1 do + bytes[i] = randomByte() + end +end + +describe('garray', function() + local itemsize = 14 + local growsize = 95 + + describe('ga_init', function() + it('initializes the values of the garray', function() + local garr = new_garray() + ga_init(garr, itemsize, growsize) + eq(0, ga_len(garr)) + eq(0, ga_maxlen(garr)) + eq(growsize, ga_growsize(garr)) + eq(itemsize, ga_itemsize(garr)) + eq(NULL, ga_data(garr)) + end) + end) + + describe('ga_grow', function() + local new_and_grow + 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 + ga_grow(garr, req) -- add space for `req` items + return garr + end + + it('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 + eq(growsize, ga_maxlen(garr)) -- we requested LESS than growsize, so... + end) + + it('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 + eq(grow_by, ga_maxlen(garr)) -- we requested MORE than growsize, so... + end) + + it('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)) + end) + end) + + describe('ga_clear', function() + it('clears an already allocated array', function() + -- allocate and scramble an array + local garr = garray_ctype() + ga_init(garr, itemsize, growsize) + ga_grow(garr, 4) + ga_set_len(garr, 4) + ga_scramble(garr) + + -- clear it and check + ga_clear(garr) + eq(NULL, ga_data(garr)) + eq(0, ga_maxlen(garr)) + eq(0, ga_len(garr)) + end) + end) + + describe('ga_append', function() + it('can append bytes', function() + -- this is the actual ga_append, the others are just emulated lua + -- versions + local garr = new_garray() + ga_init(garr, ffi.sizeof("uint8_t"), 1) + ga_append(garr, 'h') + ga_append(garr, 'e') + ga_append(garr, 'l') + ga_append(garr, 'l') + ga_append(garr, 'o') + ga_append(garr, 0) + local bytes = ga_data_as_bytes(garr) + eq('hello', ffi.string(bytes)) + end) + + it('can append integers', function() + local garr = new_garray() + ga_init(garr, ffi.sizeof("int"), 1) + local input = { + -20, + 94, + 867615, + 90927, + 86 + } + ga_append_ints(garr, unpack(input)) + local ints = ga_data_as_ints(garr) + for i = 0, #input - 1 do + eq(input[i + 1], ints[i]) + end + end) + + it('can append strings to a growing array of strings', function() + local garr = new_string_garray() + local input = { + "some", + "str", + "\r\n\r●●●●●●,,,", + "hmm", + "got it" + } + ga_append_strings(garr, unpack(input)) + -- check that we can get the same strings out of the array + local strings = ga_data_as_strings(garr) + for i = 0, #input - 1 do + eq(input[i + 1], ffi.string(strings[i])) + end + end) + end) + + describe('ga_concat', function() + it('concatenates the parameter to the growing byte array', function() + local garr = new_garray() + ga_init(garr, ffi.sizeof("char"), 1) + local str = "ohwell●●" + local loop = 5 + for i = 1, loop do + ga_concat(garr, str) + end + + -- ga_concat does NOT append the NUL in the src string to the + -- destination, you have to do that manually by calling something like + -- ga_append(gar, '\0'). I'ts always used like that in the vim + -- codebase. I feel that this is a bit of an unnecesesary + -- micro-optimization. + ga_append(garr, 0) + local result = ffi.string(ga_data_as_bytes(garr)) + eq(string.rep(str, loop), result) + end) + end) + + function test_concat_fn(input, fn, sep) + local garr = new_string_garray() + ga_append_strings(garr, unpack(input)) + if sep == nil then + eq(table.concat(input, ','), fn(garr)) + else + eq(table.concat(input, sep), fn(garr, sep)) + end + end + + describe('ga_concat_strings', function() + it('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() + test_concat_fn({ + 'oh', + 'my', + 'neovim' + }, ga_concat_strings) + end) + end) + + describe('ga_concat_strings_sep', function() + it('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() + local sep = '-●●-' + test_concat_fn({ + 'oh', + 'my', + 'neovim' + }, ga_concat_strings_sep, sep) + end) + end) + + describe('ga_remove_duplicate_strings', function() + it('sorts and removes duplicate strings', function() + local garr = new_string_garray() + local input = { + 'ccc', + 'aaa', + 'bbb', + 'ddd●●', + 'aaa', + 'bbb', + 'ccc', + 'ccc', + 'ddd●●' + } + local sorted_dedup_input = { + 'aaa', + 'bbb', + 'ccc', + 'ddd●●' + } + ga_append_strings(garr, unpack(input)) + ga_remove_duplicate_strings(garr) + eq(#sorted_dedup_input, ga_len(garr)) + local strings = ga_data_as_strings(garr) + for i = 0, #sorted_dedup_input - 1 do + eq(sorted_dedup_input[i + 1], ffi.string(strings[i])) + end + end) + end) +end) |