neovim

Neovim text editor
git clone https://git.dasho.dev/neovim.git
Log | Files | Refs | README

garray_spec.lua (10053B)


      1 local t = require('test.unit.testutil')
      2 local itp = t.gen_itp(it)
      3 
      4 local cimport = t.cimport
      5 local internalize = t.internalize
      6 local eq = t.eq
      7 local neq = t.neq
      8 local ffi = t.ffi
      9 local to_cstr = t.to_cstr
     10 local NULL = t.NULL
     11 
     12 local garray = cimport('./src/nvim/garray.h')
     13 
     14 local itemsize = 14
     15 local growsize = 95
     16 
     17 -- define a basic interface to garray. We could make it a lot nicer by
     18 -- constructing a class wrapper around garray. It could for example associate
     19 -- ga_clear_strings to the underlying garray cdata if the garray is a string
     20 -- array. But for now I estimate that that kind of magic might make testing
     21 -- less "transparent" (i.e.: the interface would become quite different as to
     22 -- how one would use it from C.
     23 
     24 -- accessors
     25 local ga_len = function(garr)
     26  return garr[0].ga_len
     27 end
     28 
     29 local ga_maxlen = function(garr)
     30  return garr[0].ga_maxlen
     31 end
     32 
     33 local ga_itemsize = function(garr)
     34  return garr[0].ga_itemsize
     35 end
     36 
     37 local ga_growsize = function(garr)
     38  return garr[0].ga_growsize
     39 end
     40 
     41 local ga_data = function(garr)
     42  return garr[0].ga_data
     43 end
     44 
     45 -- derived accessors
     46 local ga_size = function(garr)
     47  return ga_len(garr) * ga_itemsize(garr)
     48 end
     49 
     50 local ga_maxsize = function(garr) -- luacheck: ignore
     51  return ga_maxlen(garr) * ga_itemsize(garr)
     52 end
     53 
     54 local ga_data_as_bytes = function(garr)
     55  return ffi.cast('uint8_t *', ga_data(garr))
     56 end
     57 
     58 local ga_data_as_strings = function(garr)
     59  return ffi.cast('char **', ga_data(garr))
     60 end
     61 
     62 local ga_data_as_ints = function(garr)
     63  return ffi.cast('int *', ga_data(garr))
     64 end
     65 
     66 -- garray manipulation
     67 local ga_init = function(garr, itemsize_, growsize_)
     68  return garray.ga_init(garr, itemsize_, growsize_)
     69 end
     70 
     71 local ga_clear = function(garr)
     72  return garray.ga_clear(garr)
     73 end
     74 
     75 local ga_clear_strings = function(garr)
     76  assert.is_true(ga_itemsize(garr) == ffi.sizeof('char *'))
     77  return garray.ga_clear_strings(garr)
     78 end
     79 
     80 local ga_grow = function(garr, n)
     81  return garray.ga_grow(garr, n)
     82 end
     83 
     84 local ga_concat = function(garr, str)
     85  return garray.ga_concat(garr, to_cstr(str))
     86 end
     87 
     88 local ga_append = function(garr, b)
     89  if type(b) == 'string' then
     90    return garray.ga_append(garr, string.byte(b))
     91  else
     92    return garray.ga_append(garr, b)
     93  end
     94 end
     95 
     96 local ga_concat_strings = function(garr, sep)
     97  return internalize(garray.ga_concat_strings(garr, to_cstr(sep)))
     98 end
     99 
    100 local ga_remove_duplicate_strings = function(garr)
    101  return garray.ga_remove_duplicate_strings(garr)
    102 end
    103 
    104 -- derived manipulators
    105 local ga_set_len = function(garr, len)
    106  assert.is_true(len <= ga_maxlen(garr))
    107  garr[0].ga_len = len
    108 end
    109 
    110 local ga_inc_len = function(garr, by)
    111  return ga_set_len(garr, ga_len(garr) + by)
    112 end
    113 
    114 -- custom append functions
    115 -- not the C ga_append, which only works for bytes
    116 local ga_append_int = function(garr, it)
    117  assert.is_true(ga_itemsize(garr) == ffi.sizeof('int'))
    118  ga_grow(garr, 1)
    119  local data = ga_data_as_ints(garr)
    120  data[ga_len(garr)] = it
    121  return ga_inc_len(garr, 1)
    122 end
    123 
    124 local ga_append_string = function(garr, it)
    125  assert.is_true(ga_itemsize(garr) == ffi.sizeof('char *'))
    126  -- make a non-garbage collected string and copy the lua string into it,
    127  -- TODO(aktau): we should probably call xmalloc here, though as long as
    128  -- xmalloc is based on malloc it should work.
    129  local mem = ffi.C.malloc(string.len(it) + 1)
    130  ffi.copy(mem, it)
    131  ga_grow(garr, 1)
    132  local data = ga_data_as_strings(garr)
    133  data[ga_len(garr)] = mem
    134  return ga_inc_len(garr, 1)
    135 end
    136 
    137 local ga_append_strings = function(garr, ...)
    138  local prevlen = ga_len(garr)
    139  local len = select('#', ...)
    140  for i = 1, len do
    141    ga_append_string(garr, select(i, ...))
    142  end
    143  return eq(prevlen + len, ga_len(garr))
    144 end
    145 
    146 local ga_append_ints = function(garr, ...)
    147  local prevlen = ga_len(garr)
    148  local len = select('#', ...)
    149  for i = 1, len do
    150    ga_append_int(garr, select(i, ...))
    151  end
    152  return eq(prevlen + len, ga_len(garr))
    153 end
    154 
    155 -- enhanced constructors
    156 local garray_ctype = function(...)
    157  return ffi.typeof('garray_T[1]')(...)
    158 end
    159 local new_garray = function()
    160  local garr = garray_ctype()
    161  return ffi.gc(garr, ga_clear)
    162 end
    163 
    164 local new_string_garray = function()
    165  local garr = garray_ctype()
    166  ga_init(garr, ffi.sizeof('unsigned char *'), 1)
    167  return ffi.gc(garr, ga_clear_strings)
    168 end
    169 
    170 local randomByte = function()
    171  return ffi.cast('uint8_t', math.random(0, 255))
    172 end
    173 
    174 -- scramble the data in a garray
    175 local ga_scramble = function(garr)
    176  local size, bytes = ga_size(garr), ga_data_as_bytes(garr)
    177  for i = 0, size - 1 do
    178    bytes[i] = randomByte()
    179  end
    180 end
    181 
    182 describe('garray', function()
    183  describe('ga_init', function()
    184    itp('initializes the values of the garray', function()
    185      local garr = new_garray()
    186      ga_init(garr, itemsize, growsize)
    187      eq(0, ga_len(garr))
    188      eq(0, ga_maxlen(garr))
    189      eq(growsize, ga_growsize(garr))
    190      eq(itemsize, ga_itemsize(garr))
    191      eq(NULL, ga_data(garr))
    192    end)
    193  end)
    194 
    195  describe('ga_grow', function()
    196    local function new_and_grow(itemsize_, growsize_, req)
    197      local garr = new_garray()
    198      ga_init(garr, itemsize_, growsize_)
    199      eq(0, ga_size(garr)) -- should be 0 at first
    200      eq(NULL, ga_data(garr)) -- should be NULL
    201      ga_grow(garr, req) -- add space for `req` items
    202      return garr
    203    end
    204 
    205    itp('grows by growsize items if num < growsize', function()
    206      itemsize = 16
    207      growsize = 4
    208      local grow_by = growsize - 1
    209      local garr = new_and_grow(itemsize, growsize, grow_by)
    210      neq(NULL, ga_data(garr)) -- data should be a ptr to memory
    211      eq(growsize, ga_maxlen(garr)) -- we requested LESS than growsize, so...
    212    end)
    213 
    214    itp('grows by num items if num > growsize', function()
    215      itemsize = 16
    216      growsize = 4
    217      local grow_by = growsize + 1
    218      local garr = new_and_grow(itemsize, growsize, grow_by)
    219      neq(NULL, ga_data(garr)) -- data should be a ptr to memory
    220      eq(grow_by, ga_maxlen(garr)) -- we requested MORE than growsize, so...
    221    end)
    222 
    223    itp('does not grow when nothing is requested', function()
    224      local garr = new_and_grow(16, 4, 0)
    225      eq(NULL, ga_data(garr))
    226      eq(0, ga_maxlen(garr))
    227    end)
    228  end)
    229 
    230  describe('ga_clear', function()
    231    itp('clears an already allocated array', function()
    232      -- allocate and scramble an array
    233      local garr = garray_ctype()
    234      ga_init(garr, itemsize, growsize)
    235      ga_grow(garr, 4)
    236      ga_set_len(garr, 4)
    237      ga_scramble(garr)
    238 
    239      -- clear it and check
    240      ga_clear(garr)
    241      eq(NULL, ga_data(garr))
    242      eq(0, ga_maxlen(garr))
    243      eq(0, ga_len(garr))
    244    end)
    245  end)
    246 
    247  describe('ga_append', function()
    248    itp('can append bytes', function()
    249      -- this is the actual ga_append, the others are just emulated lua
    250      -- versions
    251      local garr = new_garray()
    252      ga_init(garr, ffi.sizeof('uint8_t'), 1)
    253      ga_append(garr, 'h')
    254      ga_append(garr, 'e')
    255      ga_append(garr, 'l')
    256      ga_append(garr, 'l')
    257      ga_append(garr, 'o')
    258      ga_append(garr, 0)
    259      local bytes = ga_data_as_bytes(garr)
    260      eq('hello', ffi.string(bytes))
    261    end)
    262 
    263    itp('can append integers', function()
    264      local garr = new_garray()
    265      ga_init(garr, ffi.sizeof('int'), 1)
    266      local input = {
    267        -20,
    268        94,
    269        867615,
    270        90927,
    271        86,
    272      }
    273      ga_append_ints(garr, unpack(input))
    274      local ints = ga_data_as_ints(garr)
    275      for i = 0, #input - 1 do
    276        eq(input[i + 1], ints[i])
    277      end
    278    end)
    279 
    280    itp('can append strings to a growing array of strings', function()
    281      local garr = new_string_garray()
    282      local input = {
    283        'some',
    284        'str',
    285        '\r\n\r●●●●●●,,,',
    286        'hmm',
    287        'got it',
    288      }
    289      ga_append_strings(garr, unpack(input))
    290      -- check that we can get the same strings out of the array
    291      local strings = ga_data_as_strings(garr)
    292      for i = 0, #input - 1 do
    293        eq(input[i + 1], ffi.string(strings[i]))
    294      end
    295    end)
    296  end)
    297 
    298  describe('ga_concat', function()
    299    itp('concatenates the parameter to the growing byte array', function()
    300      local garr = new_garray()
    301      ga_init(garr, ffi.sizeof('char'), 1)
    302      local str = 'ohwell●●'
    303      local loop = 5
    304      for _ = 1, loop do
    305        ga_concat(garr, str)
    306      end
    307 
    308      -- ga_concat does NOT append the NUL in the src string to the
    309      -- destination, you have to do that manually by calling something like
    310      -- ga_append(gar, '\0'). I'ts always used like that in the vim
    311      -- codebase. I feel that this is a bit of an unnecesesary
    312      -- micro-optimization.
    313      ga_append(garr, 0)
    314      local result = ffi.string(ga_data_as_bytes(garr))
    315      eq(string.rep(str, loop), result)
    316    end)
    317  end)
    318 
    319  local function test_concat_fn(input, sep)
    320    local garr = new_string_garray()
    321    ga_append_strings(garr, unpack(input))
    322    eq(table.concat(input, sep), ga_concat_strings(garr, sep))
    323  end
    324 
    325  describe('ga_concat_strings', function()
    326    itp('returns an empty string when concatenating an empty array', function()
    327      test_concat_fn({}, ',')
    328      test_concat_fn({}, '---')
    329    end)
    330 
    331    itp('can concatenate a non-empty array', function()
    332      test_concat_fn({
    333        'oh',
    334        'my',
    335        'neovim',
    336      }, ',')
    337      test_concat_fn({
    338        'oh',
    339        'my',
    340        'neovim',
    341      }, '-●●-')
    342    end)
    343  end)
    344 
    345  describe('ga_remove_duplicate_strings', function()
    346    itp('sorts and removes duplicate strings', function()
    347      local garr = new_string_garray()
    348      local input = {
    349        'ccc',
    350        'aaa',
    351        'bbb',
    352        'ddd●●',
    353        'aaa',
    354        'bbb',
    355        'ccc',
    356        'ccc',
    357        'ddd●●',
    358      }
    359      local sorted_dedup_input = {
    360        'aaa',
    361        'bbb',
    362        'ccc',
    363        'ddd●●',
    364      }
    365      ga_append_strings(garr, unpack(input))
    366      ga_remove_duplicate_strings(garr)
    367      eq(#sorted_dedup_input, ga_len(garr))
    368      local strings = ga_data_as_strings(garr)
    369      for i = 0, #sorted_dedup_input - 1 do
    370        eq(sorted_dedup_input[i + 1], ffi.string(strings[i]))
    371      end
    372    end)
    373  end)
    374 end)