aboutsummaryrefslogtreecommitdiff
path: root/test/unit/garray_spec.lua
blob: 5d41dd39ec2cbeb8f32b41ef8a3fbe4dde991f35 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
local helpers = require("test.unit.helpers")(after_each)
local itp = helpers.gen_itp(it)

local cimport = helpers.cimport
local internalize = helpers.internalize
local eq = helpers.eq
local neq = helpers.neq
local ffi = helpers.ffi
local to_cstr = helpers.to_cstr
local NULL = helpers.NULL

local garray = cimport('./src/nvim/garray.h')

local itemsize = 14
local growsize = 95

-- 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 "transparent" (i.e.: the interface would become quite different as to
-- how one would use it from C.

-- accessors
local ga_len = function(garr)
  return garr[0].ga_len
end

local ga_maxlen = function(garr)
  return garr[0].ga_maxlen
end

local ga_itemsize = function(garr)
  return garr[0].ga_itemsize
end

local ga_growsize = function(garr)
  return garr[0].ga_growsize
end

local ga_data = function(garr)
  return garr[0].ga_data
end

-- derived accessors
local ga_size = function(garr)
  return ga_len(garr) * ga_itemsize(garr)
end

local ga_maxsize = function(garr)  -- luacheck: ignore
  return ga_maxlen(garr) * ga_itemsize(garr)
end

local ga_data_as_bytes = function(garr)
  return ffi.cast('uint8_t *', ga_data(garr))
end

local ga_data_as_strings = function(garr)
  return ffi.cast('char **', ga_data(garr))
end

local ga_data_as_ints = function(garr)
  return ffi.cast('int *', ga_data(garr))
end

-- garray manipulation
local ga_init = function(garr, itemsize_, growsize_)
  return garray.ga_init(garr, itemsize_, growsize_)
end

local ga_clear = function(garr)
  return garray.ga_clear(garr)
end

local ga_clear_strings = function(garr)
  assert.is_true(ga_itemsize(garr) == ffi.sizeof('char *'))
  return garray.ga_clear_strings(garr)
end

local ga_grow = function(garr, n)
  return garray.ga_grow(garr, n)
end

local ga_concat = function(garr, str)
  return garray.ga_concat(garr, to_cstr(str))
end

local ga_append = function(garr, b)
  if type(b) == 'string' then
    return garray.ga_append(garr, string.byte(b))
  else
    return garray.ga_append(garr, b)
  end
end

local ga_concat_strings = function(garr)
  return internalize(garray.ga_concat_strings(garr))
end

local ga_concat_strings_sep = function(garr, sep)
  return internalize(garray.ga_concat_strings_sep(garr, to_cstr(sep)))
end

local ga_remove_duplicate_strings = function(garr)
  return garray.ga_remove_duplicate_strings(garr)
end

-- derived manipulators
local ga_set_len = function(garr, len)
  assert.is_true(len <= ga_maxlen(garr))
  garr[0].ga_len = len
end

local ga_inc_len = function(garr, by)
  return ga_set_len(garr, ga_len(garr) + by)
end

-- custom append functions
-- not the C ga_append, which only works for bytes
local ga_append_int = function(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

local ga_append_string = function(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

local ga_append_strings = function(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

local ga_append_ints = function(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 = function(...) return ffi.typeof('garray_T[1]')(...) end
local new_garray = function()
  local garr = garray_ctype()
  return ffi.gc(garr, ga_clear)
end

local new_string_garray = function()
  local garr = garray_ctype()
  ga_init(garr, ffi.sizeof("unsigned char *"), 1)
  return ffi.gc(garr, ga_clear_strings)
end

local randomByte = function()
  return ffi.cast('uint8_t', math.random(0, 255))
end

-- scramble the data in a garray
local ga_scramble = function(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()

  describe('ga_init', function()
    itp('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 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

    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
      eq(growsize, ga_maxlen(garr))  -- we requested LESS than growsize, so...
    end)

    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
      eq(grow_by, ga_maxlen(garr))   -- we requested MORE than growsize, so...
    end)

    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))
    end)
  end)

  describe('ga_clear', function()
    itp('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()
    itp('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)

    itp('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)

    itp('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()
    itp('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 _ = 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)

  local 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()
    itp('returns an empty string when concatenating an empty array', function()
      test_concat_fn({ }, ga_concat_strings)
    end)

    itp('can concatenate a non-empty array', function()
      test_concat_fn({
        'oh',
        'my',
        'neovim'
      }, ga_concat_strings)
    end)
  end)

  describe('ga_concat_strings_sep', function()
    itp('returns an empty string when concatenating an empty array', function()
      test_concat_fn({ }, ga_concat_strings_sep, '---')
    end)

    itp('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()
    itp('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)