garray_spec.lua 10 KB

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