1-- ----------------------------------------------------------------------------
2-- test.lua
3-- Luabins test suite
4-- See copyright notice in luabins.h
5-- ----------------------------------------------------------------------------
6
7package.cpath = "./?.so;"..package.cpath
8
9local randomseed = 1235134892
10--local randomseed = os.time()
11
12print("===== BEGIN LUABINS TEST SUITE (seed " .. randomseed .. ") =====")
13math.randomseed(randomseed)
14
15-- ----------------------------------------------------------------------------
16-- Utility functions
17-- ----------------------------------------------------------------------------
18
19local invariant = function(v)
20  return function()
21    return v
22  end
23end
24
25local escape_string = function(str)
26  return str:gsub(
27      "[^0-9A-Za-z_%- :]",
28      function(c)
29        return ("%%%02X"):format(c:byte())
30      end
31    )
32end
33
34local ensure_equals = function(msg, actual, expected)
35  if actual ~= expected then
36    error(
37        msg..":\n  actual: `"..escape_string(tostring(actual))
38        .."`\nexpected: `"..escape_string(tostring(expected)).."'"
39      )
40  end
41end
42
43local ensure_equals_permute
44do
45  -- Based on MIT-licensed
46  -- http://snippets.luacode.org/sputnik.lua?p=snippets/ \
47  -- Iterator_over_Permutations_of_a_Table_62
48  -- Which is based on PiL
49  local function permgen(a, n, fn)
50    if n == 0 then
51      fn(a)
52    else
53      for i = 1, n do
54        -- put i-th element as the last one
55        a[n], a[i] = a[i], a[n]
56
57        -- generate all permutations of the other elements
58        permgen(a, n - 1, fn)
59
60        -- restore i-th element
61        a[n], a[i] = a[i], a[n]
62      end
63    end
64  end
65
66  --- an iterator over all permutations of the elements of a list.
67  -- Please note that the same list is returned each time,
68  -- so do not keep references!
69  -- @param a list-like table
70  -- @return an iterator which provides the next permutation as a list
71  local function permute_iter(a, n)
72    local n = n or #a
73    local co = coroutine.create(function() permgen(a, n, coroutine.yield) end)
74    return function() -- iterator
75      local code, res = coroutine.resume(co)
76      return res
77    end
78  end
79
80  ensure_equals_permute = function(
81      msg,
82      actual,
83      expected_prefix,
84      expected_body,
85      expected_suffix,
86      expected_body_size
87    )
88    expected_body_size = expected_body_size or #expected_body
89
90    local expected
91    for t in permute_iter(expected_body, expected_body_size) do
92      expected = expected_prefix .. table.concat(t) .. expected_suffix
93      if actual == expected then
94        return actual
95      end
96    end
97
98    error(
99        msg..":\nactual: `"..escape_string(tostring(actual))
100        .."`\nexpected one of permutations: `"
101        ..escape_string(tostring(expected)).."'"
102      )
103  end
104end
105
106local function deepequals(lhs, rhs)
107  if type(lhs) ~= "table" or type(rhs) ~= "table" then
108    return lhs == rhs
109  end
110
111  local checked_keys = {}
112
113  for k, v in pairs(lhs) do
114    checked_keys[k] = true
115    if not deepequals(v, rhs[k]) then
116      return false
117    end
118  end
119
120  for k, v in pairs(rhs) do
121    if not checked_keys[k] then
122      return false -- extra key
123    end
124  end
125
126  return true
127end
128
129local nargs = function(...)
130  return select("#", ...), ...
131end
132
133local pack = function(...)
134  return select("#", ...), { ... }
135end
136
137local eat_true = function(t, ...)
138  if t == nil then
139    error("failed: " .. (...))
140  end
141  return ...
142end
143
144-- ----------------------------------------------------------------------------
145-- Test helper functions
146-- ----------------------------------------------------------------------------
147
148local luabins_local = require 'luabins'
149assert(luabins_local == luabins)
150
151assert(type(luabins.save) == "function")
152assert(type(luabins.load) == "function")
153
154local check_load_fn_ok = function(eq, saved, ...)
155  local expected = { nargs(...) }
156  local loaded = { nargs(eat_true(luabins.load(saved))) }
157
158  ensure_equals("num arguments match", loaded[1], expected[1])
159  for i = 2, expected[1] do
160    assert(eq(loaded[i], expected[i]))
161  end
162
163  return saved
164end
165
166local check_load_ok = function(saved, ...)
167  return check_load_fn_ok(deepequals, saved, ...)
168end
169
170local check_fn_ok = function(eq, ...)
171  local saved = assert(luabins.save(...))
172
173  assert(type(saved) == "string")
174
175  print("saved length", #saved, "(display truncated to 70 chars)")
176  print(escape_string(saved):sub(1, 70))
177
178  return check_load_fn_ok(eq, saved, ...)
179end
180
181local check_ok = function(...)
182  print("check_ok")
183  return check_fn_ok(deepequals, ...)
184end
185
186local check_fail_save = function(msg, ...)
187  print("check_fail_save")
188  local res, err = luabins.save(...)
189  ensure_equals("result", res, nil)
190  ensure_equals("error message", err, msg)
191--  print("/check_fail_save")
192end
193
194local check_fail_load = function(msg, v)
195  print("check_fail_load")
196  local res, err = luabins.load(v)
197  ensure_equals("result", res, nil)
198  ensure_equals("error message", err, msg)
199--  print("/check_fail_load")
200end
201
202print("===== BEGIN LARGE DATA OK =====")
203
204-- Based on actual bug.
205-- This dataset triggered Lua C data stack overflow.
206-- (Note that bug is not triggered if check_ok is used)
207-- Update data with
208-- $ lua etc/toluabins.lua test/large_data.lua>test/large_data.luabins
209-- WARNING: Keep this test above other tests, so Lua stack is small.
210assert(
211    luabins.load(
212        assert(io.open("test/large_data.luabins", "r"):read("*a"))
213      )
214  )
215
216print("===== LARGE DATA OK =====")
217
218-- ----------------------------------------------------------------------------
219-- Basic tests
220-- ----------------------------------------------------------------------------
221
222print("===== BEGIN BASIC TESTS =====")
223
224print("---> basic corrupt data tests")
225
226check_fail_load("can't load: corrupt data", "")
227check_fail_load("can't load: corrupt data", "bad data")
228
229print("---> basic extra data tests")
230do
231  local s
232
233  s = check_ok()
234  check_fail_load("can't load: extra data at end", s .. "-")
235
236  s = check_ok(nil)
237  check_fail_load("can't load: extra data at end", s .. "-")
238
239  s = check_ok(true)
240  check_fail_load("can't load: extra data at end", s .. "-")
241
242  s = check_ok(false)
243  check_fail_load("can't load: extra data at end", s .. "-")
244
245  s = check_ok(42)
246  check_fail_load("can't load: extra data at end", s .. "-")
247
248  s = check_ok(math.pi)
249  check_fail_load("can't load: extra data at end", s .. "-")
250
251  s = check_ok(1/0)
252  check_fail_load("can't load: extra data at end", s .. "-")
253
254  s = check_ok(-1/0)
255  check_fail_load("can't load: extra data at end", s .. "-")
256
257  s = check_ok("Luabins")
258  check_fail_load("can't load: extra data at end", s .. "-")
259
260  s = check_ok({ })
261
262  check_fail_load("can't load: extra data at end", s .. "-")
263
264  s = check_ok({ a = 1, 2 })
265  check_fail_load("can't load: extra data at end", s .. "-")
266end
267
268print("---> basic type tests")
269
270-- This is the way to detect NaN
271check_fn_ok(function(lhs, rhs) return lhs ~= lhs and rhs ~= rhs end, 0/0)
272
273check_ok("")
274
275check_ok("Embedded\0Zero")
276
277check_ok(("longstring"):rep(1024000))
278
279check_fail_save("can't save: unsupported type detected", function() end)
280check_fail_save(
281    "can't save: unsupported type detected",
282    coroutine.create(function() end)
283  )
284check_fail_save("can't save: unsupported type detected", newproxy())
285
286print("---> basic table tests")
287
288check_ok({ 1 })
289check_ok({ a = 1 })
290check_ok({ a = 1, 2, [42] = true, [math.pi] = math.huge })
291check_ok({ { } })
292check_ok({ a = {}, b = { c = 7 } })
293check_ok({ 1, 2, 3 })
294check_ok({ [1] = 1, [1.5] = 2, [2] = 3 })
295check_ok({ 1, nil, 3 })
296check_ok({ 1, nil, 3, [{ 1, nil, 3 }] = { 1, nil, 3 } })
297
298print("---> basic tuple tests")
299
300check_ok(nil, nil)
301
302do
303  local s = check_ok(nil, false, true, 42, "Embedded\0Zero", { { [{3}] = 54 } })
304  check_fail_load("can't load: extra data at end", s .. "-")
305
306  check_ok(check_ok(s)) -- Save data string couple of times more
307end
308
309print("---> basic table tuple tests")
310
311check_ok({ a = {}, b = { c = 7 } }, nil, { { } }, 42)
312
313check_ok({ ["1"] = "str", [1] = "num" })
314
315check_ok({ [true] = true })
316check_ok({ [true] = true, [false] = false, 1 })
317
318print("---> basic fail save tests")
319
320check_fail_save(
321    "can't save: unsupported type detected",
322    { { function() end } }
323  )
324
325check_fail_save(
326    "can't save: unsupported type detected",
327    nil, false, true, 42, "Embedded\0Zero", function() end,
328    { { [{3}] = 54 } }
329  )
330
331print("---> recursive table test")
332
333local t = {}; t[1] = t
334check_fail_save("can't save: nesting is too deep", t)
335
336print("---> metatable test")
337
338check_ok(setmetatable({}, {__index = function(t, k) return k end}))
339
340print("===== BASIC TESTS OK =====")
341
342print("===== BEGIN FORMAT SANITY TESTS =====")
343
344-- Format sanity checks for LJ2 compatibility tests.
345-- These tests are intended to help debugging actual problems
346-- of test suite, and are not feature complete.
347-- What is not checked here, checked in the rest of suite.
348
349do
350  do
351    local saved = check_ok(1)
352    local expected =
353      "\001".."N"
354      .. "\000\000\000\000\000\000\240\063" -- Note number is a double
355
356    ensure_equals(
357        "1 as number",
358        expected,
359        saved
360      )
361  end
362
363  do
364    local saved = check_ok({ [true] = 1 })
365    local expected =
366      "\001".."T"
367      .. "\000\000\000\000".."\001\000\000\000"
368      .. "1"
369      .. "N\000\000\000\000\000\000\240\063" -- Note number is a double
370
371    ensure_equals(
372        "1 as value",
373        expected,
374        saved
375      )
376  end
377
378  do
379    local saved = check_ok({ [1] = true })
380    local expected =
381      "\001".."T"
382      .. "\001\000\000\000".."\000\000\000\000"
383      .. "N\000\000\000\000\000\000\240\063" -- Note number is a double
384      .. "1"
385
386    ensure_equals(
387        "1 as key",
388        expected,
389        saved
390      )
391  end
392end
393
394print("===== FORMAT SANITY TESTS OK =====")
395
396print("===== BEGIN AUTOCOLLAPSE TESTS =====")
397
398-- Note: those are ad-hoc tests, tuned for old implementation
399-- which generated save data on Lua stack.
400-- These tests are kept here for performance comparisons.
401
402local LUABINS_CONCATTHRESHOLD = 1024
403
404local gen_t = function(size)
405  -- two per numeric entry, three per string entry,
406  -- two entries per key-value pair
407  local actual_size = math.ceil(size / (2 + 3))
408  print("generating table of "..actual_size.." pairs")
409  local t = {}
410  for i = 1, actual_size do
411    t[i] = "a"..i
412  end
413  return t
414end
415
416-- Test table value autocollapse
417check_ok(gen_t(LUABINS_CONCATTHRESHOLD - 100)) -- underflow, no autocollapse
418check_ok(gen_t(LUABINS_CONCATTHRESHOLD)) -- autocollapse, no extra elements
419check_ok(gen_t(LUABINS_CONCATTHRESHOLD + 100)) -- autocollapse, extra elements
420
421-- Test table key autocollapse
422check_ok({ [gen_t(LUABINS_CONCATTHRESHOLD - 4)] = true })
423
424-- Test multiarg autocollapse
425check_ok(
426    1,
427    gen_t(LUABINS_CONCATTHRESHOLD - 5),
428    2,
429    gen_t(LUABINS_CONCATTHRESHOLD - 5),
430    3
431  )
432
433print("===== AUTOCOLLAPSE TESTS OK =====")
434
435print("===== BEGIN MIN TABLE SIZE TESTS =====")
436
437do
438  -- one small key
439  do
440    local data = { [true] = true }
441    local saved = check_ok(data)
442    ensure_equals(
443        "format sanity check",
444        "\001".."T".."\000\000\000\000".."\001\000\000\000".."11",
445        saved
446      )
447    check_fail_load(
448        "can't load: corrupt data, bad size",
449        saved:sub(1, #saved - 1)
450      )
451
452    -- As long as array and hash size sum is correct
453    -- (and both are within limits), load is successful.
454    -- If values are swapped, we get some performance hit.
455    check_load_ok(
456        "\001".."T".."\001\000\000\000".."\000\000\000\000".."11",
457        data
458      )
459
460    check_fail_load(
461        "can't load: corrupt data, bad size",
462        "\001".."T".."\001\000\000\000".."\001\000\000\000".."11"
463      )
464
465    check_fail_load(
466        "can't load: corrupt data, bad size",
467        "\001".."T".."\000\000\000\000".."\002\000\000\000".."11"
468      )
469
470    check_fail_load(
471        "can't load: extra data at end",
472        "\001".."T".."\000\000\000\000".."\000\000\000\000".."11"
473      )
474
475    check_fail_load(
476        "can't load: corrupt data, bad size",
477        "\001".."T".."\255\255\255\255".."\255\255\255\255".."11"
478      )
479    check_fail_load(
480        "can't load: corrupt data, bad size",
481        "\001".."T".."\000\255\255\255".."\000\255\255\255".."11"
482      )
483    check_fail_load(
484        "can't load: corrupt data, bad size",
485        "\255".."T".."\000\000\000\000".."\000\000\000\000"
486      )
487  end
488
489  -- two small keys
490  do
491    local data = { [true] = true, [false] = false }
492    local saved = check_ok({ [true] = true, [false] = false })
493    ensure_equals_permute(
494        "format sanity check",
495        saved,
496        "\001" .. "T" .. "\000\000\000\000" .. "\002\000\000\000",
497        {
498          "0" .. "0";
499          "1" .. "1";
500        },
501        ""
502      )
503    check_fail_load(
504        "can't load: corrupt data, bad size",
505        saved:sub(1, #saved - 1)
506      )
507
508    -- See above about swapped array and hash sizes
509    check_load_ok(
510        "\001".."T".."\001\000\000\000".."\001\000\000\000".."1100",
511        data
512      )
513
514    check_fail_load(
515        "can't load: corrupt data, bad size",
516        "\001".."T".."\000\000\000\000".."\003\000\000\000".."0011"
517      )
518  end
519
520  -- two small and one large key
521  do
522    local saved = check_ok({ [true] = true, [false] = false, [1] = true })
523    ensure_equals_permute(
524        "format sanity check",
525        saved,
526        "\001" .. "T" .. "\001\000\000\000" .. "\002\000\000\000",
527        {
528          "0" .. "0";
529          "1" .. "1";
530           -- Note number is a double
531          "N\000\000\000\000\000\000\240\063" .. "1";
532        },
533        ""
534      )
535
536    check_fail_load(
537        "can't load: corrupt data",
538        saved:sub(1, #saved - 1)
539      )
540
541    check_fail_load(
542        "can't load: corrupt data, bad size",
543        "\001".."T"
544        .. "\002\000\000\000".."\002\000\000\000"
545        .. "0011"
546        .. "N\000\000\000\000\000\000\240\063"
547        .. "1"
548      )
549
550    check_fail_load(
551        "can't load: corrupt data, bad size",
552        "\001".."T"
553        .. "\001\000\000\000".."\003\000\000\000"
554        .. "0011"
555        .. "N\000\000\000\000\000\000\240\063"
556        .. "1"
557      )
558  end
559
560  -- two small and two large keys
561  do
562    local saved = check_ok(
563        { [true] = true, [false] = false, [1] = true, [42] = true }
564      )
565    local expected =
566      "\001".."T"
567      .. "\001\000\000\000".."\003\000\000\000"
568      .. "0011"
569    ensure_equals_permute(
570        "format sanity check",
571        saved,
572        "\001" .. "T" .. "\001\000\000\000" .. "\003\000\000\000",
573        {
574          "0" .. "0";
575          "1" .. "1";
576          "N\000\000\000\000\000\000\069\064" .. "1";
577          "N\000\000\000\000\000\000\240\063" .. "1";
578        },
579        ""
580      )
581
582    check_fail_load(
583        "can't load: corrupt data",
584        saved:sub(1, #saved - 1)
585      )
586
587    check_fail_load(
588        "can't load: corrupt data, bad size",
589        "\001".."T"
590        .. "\001\000\000\000".."\005\000\000\000"
591        .. "0011"
592        .. "N\000\000\000\000\000\000\069\064"
593        .. "1"
594        .. "N\000\000\000\000\000\000\240\063"
595        .. "1"
596      )
597
598    check_fail_load(
599        "can't load: corrupt data, bad size",
600        "\001".."T"
601        .. "\003\000\000\000".."\003\000\000\000"
602        .. "0011"
603        .. "N\000\000\000\000\000\000\069\064"
604        .. "1"
605        .. "N\000\000\000\000\000\000\240\063"
606        .. "1"
607      )
608  end
609end
610
611print("===== MIN TABLE SIZE TESTS OK =====")
612
613print("===== BEGIN LOAD TRUNCATION TESTS =====")
614
615local function gen_random_dataset(num, nesting)
616  num = num or math.random(0, 128)
617  nesting = nesting or 1
618
619  local gen_str = function()
620    local t = {}
621    local n = math.random(0, 1024)
622    for i = 1, n do
623      t[i] = string.char(math.random(0, 255))
624    end
625    return table.concat(t)
626  end
627
628  local gen_bool = function() return math.random() >= 0.5 end
629
630  local gen_nil = function() return nil end
631
632  local generators =
633  {
634    gen_nil;
635    gen_nil;
636    gen_nil;
637    gen_bool;
638    gen_bool;
639    gen_bool;
640    function() return math.random() end;
641    function() return math.random(-10000, 10000) end;
642    function() return math.random() * math.random(-10000, 10000) end;
643    gen_str;
644    gen_str;
645    gen_str;
646    function()
647      if nesting >= 24 then
648        return nil
649      end
650
651      local t = {}
652      local n = math.random(0, 24 - nesting)
653      for i = 1, n do
654        local k = gen_random_dataset(1, nesting + 1)
655        if k == nil then
656          k = "(nil)"
657        end
658        t[ k ] = gen_random_dataset(
659            1,
660            nesting + 1
661          )
662      end
663
664      return t
665    end;
666  }
667
668  local t = {}
669  for i = 1, num do
670    local n = math.random(1, #generators)
671    t[i] = generators[n]()
672  end
673  return unpack(t, 0, num)
674end
675
676local random_dataset_num, random_dataset_data = pack(gen_random_dataset())
677local random_dataset_saved = check_ok(
678    unpack(random_dataset_data, 0, random_dataset_num)
679  )
680
681local num_tries = 100
682local errors = {}
683for i = 1, num_tries do
684  local to = math.random(1, #random_dataset_saved - 1)
685  local new_data = random_dataset_saved:sub(1, to)
686
687  local res, err = luabins.load(new_data)
688  ensure_equals("truncated data must not be loaded", res, nil)
689  errors[err] = (errors[err] or 0) + 1
690end
691
692print("truncation errors encountered:")
693for err, n in pairs(errors) do
694  print(err, n)
695end
696
697print("===== BASIC LOAD TRUNCATION OK =====")
698
699print("===== BEGIN LOAD MUTATION TESTS =====")
700
701local function mutate_string(str, num, override)
702  num = num or math.random(1, 8)
703
704  if num < 1 then
705    return str
706  end
707
708  local mutators =
709  {
710    -- truncate at end
711    function(str)
712      local pos = math.random(1, #str)
713      return str:sub(1, pos)
714    end;
715    -- truncate at beginning
716    function(str)
717      local pos = math.random(1, #str)
718      return str:sub(-pos)
719    end;
720    -- cut out the middle
721    function(str)
722      local from = math.random(1, #str)
723      local to = math.random(from, #str)
724      return str:sub(1, from) .. str:sub(to)
725    end;
726    -- swap two halves
727    function(str)
728      local pos = math.random(1, #str)
729      return str:sub(pos + 1, #str) .. str:sub(1, pos)
730    end;
731    -- swap two characters
732    function(str)
733      local pa, pb = math.random(1, #str), math.random(1, #str)
734      local a, b = str:sub(pa, pa), str:sub(pb, pb)
735      return
736        str:sub(1, pa - 1) ..
737        a ..
738        str:sub(pa + 1, pb - 1) ..
739        b ..
740        str:sub(pb + 1, #str)
741    end;
742    -- replace one character
743    function(str)
744      local pos = math.random(1, #str)
745      return
746        str:sub(1, pos - 1) ..
747        string.char(math.random(0, 255)) ..
748        str:sub(pos + 1, #str)
749    end;
750    -- increase one character
751    function(str)
752      local pos = math.random(1, #str)
753      local b = str:byte(pos, pos) + 1
754      if b > 255 then
755        b = 0
756      end
757      return
758        str:sub(1, pos - 1) ..
759        string.char(b) ..
760        str:sub(pos + 1, #str)
761    end;
762    -- decrease one character
763    function(str)
764      local pos = math.random(1, #str)
765      local b = str:byte(pos, pos) - 1
766      if b < 0 then
767        b = 255
768      end
769      return
770        str:sub(1, pos - 1) ..
771        string.char(b) ..
772        str:sub(pos + 1, #str)
773    end;
774  }
775
776  local n = override or math.random(1, #mutators)
777
778  str = mutators[n](str)
779
780  return mutate_string(str, num - 1, override)
781end
782
783local num_tries = 100000
784local num_successes = 0
785local errors = {}
786for i = 1, num_tries do
787  local new_data = mutate_string(random_dataset_saved)
788
789  local res, err = luabins.load(new_data)
790  if res == nil then
791    errors[err] = (errors[err] or 0) + 1
792  else
793     num_successes = num_successes + 1
794  end
795end
796
797if num_successes == 0 then
798  print("no mutated strings loaded successfully")
799else
800  -- This is ok, since we may corrupt data, not format.
801  -- If it is an issue for user, he must append checksum to data,
802  -- as usual.
803  print("mutated strings loaded successfully: "..num_successes)
804end
805
806print("mutation errors encountered:")
807for err, n in pairs(errors) do
808  print(err, n)
809end
810
811print("===== BASIC LOAD MUTATION OK =====")
812
813print("OK")
814