diff options
Diffstat (limited to 'test/helpers.lua')
| -rw-r--r-- | test/helpers.lua | 510 | 
1 files changed, 466 insertions, 44 deletions
| diff --git a/test/helpers.lua b/test/helpers.lua index beef53b5a9..0d3fe1316b 100644 --- a/test/helpers.lua +++ b/test/helpers.lua @@ -1,34 +1,98 @@  local assert = require('luassert') +local luv = require('luv')  local lfs = require('lfs') +local quote_me = '[^.%w%+%-%@%_%/]' -- complement (needn't quote) +local function shell_quote(str) +  if string.find(str, quote_me) or str == '' then +    return '"' .. str:gsub('[$%%"\\]', '\\%0') .. '"' +  else +    return str +  end +end + +local function argss_to_cmd(...) +  local cmd = '' +  for i = 1, select('#', ...) do +    local arg = select(i, ...) +    if type(arg) == 'string' then +      cmd = cmd .. ' ' ..shell_quote(arg) +    else +      for _, subarg in ipairs(arg) do +        cmd = cmd .. ' ' .. shell_quote(subarg) +      end +    end +  end +  return cmd +end + +local function popen_r(...) +  return io.popen(argss_to_cmd(...), 'r') +end + +local function popen_w(...) +  return io.popen(argss_to_cmd(...), 'w') +end + +-- sleeps the test runner (_not_ the nvim instance) +local function sleep(ms) +  luv.sleep(ms) +end +  local check_logs_useless_lines = {    ['Warning: noted but unhandled ioctl']=1,    ['could cause spurious value errors to appear']=2,    ['See README_MISSING_SYSCALL_OR_IOCTL for guidance']=3,  } -local eq = function(exp, act) -  return assert.are.same(exp, act) +local function eq(expected, actual) +  return assert.are.same(expected, actual)  end -local neq = function(exp, act) -  return assert.are_not.same(exp, act) +local function neq(expected, actual) +  return assert.are_not.same(expected, actual)  end -local ok = function(res) +local function ok(res)    return assert.is_true(res)  end +local function matches(pat, actual) +  if nil ~= string.match(actual, pat) then +    return true +  end +  error(string.format('Pattern does not match.\nPattern:\n%s\nActual:\n%s', pat, actual)) +end +-- Expect an error matching pattern `pat`. +local function expect_err(pat, ...) +  local fn = select(1, ...) +  local fn_args = {...} +  table.remove(fn_args, 1) +  assert.error_matches(function() return fn(unpack(fn_args)) end, pat) +end +-- initial_path:  directory to recurse into +-- re:            include pattern (string) +-- exc_re:        exclude pattern(s) (string or table)  local function glob(initial_path, re, exc_re) +  exc_re = type(exc_re) == 'table' and exc_re or { exc_re }    local paths_to_check = {initial_path}    local ret = {}    local checked_files = {} +  local function is_excluded(path) +    for _, pat in pairs(exc_re) do +      if path:match(pat) then return true end +    end +    return false +  end + +  if is_excluded(initial_path) then +    return ret +  end    while #paths_to_check > 0 do      local cur_path = paths_to_check[#paths_to_check]      paths_to_check[#paths_to_check] = nil      for e in lfs.dir(cur_path) do        local full_path = cur_path .. '/' .. e        local checked_path = full_path:sub(#initial_path + 1) -      if ((not exc_re or not checked_path:match(exc_re)) -          and e:sub(1, 1) ~= '.') then +      if (not is_excluded(checked_path)) and e:sub(1, 1) ~= '.' then          local attrs = lfs.attributes(full_path)          if attrs then            local check_key = attrs.dev .. ':' .. tostring(attrs.ino) @@ -95,7 +159,7 @@ local uname = (function()        return platform      end -    local status, f = pcall(io.popen, "uname -s") +    local status, f = pcall(popen_r, 'uname', '-s')      if status then        platform = f:read("*l")        f:close() @@ -106,13 +170,20 @@ local uname = (function()    end)  end)() +local function tmpdir_get() +  return os.getenv('TMPDIR') and os.getenv('TMPDIR') or os.getenv('TEMP') +end + +-- Is temp directory `dir` defined local to the project workspace? +local function tmpdir_is_local(dir) +  return not not (dir and string.find(dir, 'Xtest')) +end +  local tmpname = (function()    local seq = 0 -  local tmpdir = os.getenv('TMPDIR') and os.getenv('TMPDIR') or os.getenv('TEMP') -  -- Is $TMPDIR defined local to the project workspace? -  local in_workspace = not not (tmpdir and string.find(tmpdir, 'Xtest')) +  local tmpdir = tmpdir_get()    return (function() -    if in_workspace then +    if tmpdir_is_local(tmpdir) then        -- Cannot control os.tmpname() dir, so hack our own tmpname() impl.        seq = seq + 1        local fname = tmpdir..'/nvim-test-lua-'..seq @@ -168,22 +239,27 @@ local function check_cores(app, force)    local gdb_db_cmd = 'gdb -n -batch -ex "thread apply all bt full" "$_NVIM_TEST_APP" -c "$_NVIM_TEST_CORE"'    local lldb_db_cmd = 'lldb -Q -o "bt all" -f "$_NVIM_TEST_APP" -c "$_NVIM_TEST_CORE"'    local random_skip = false +  -- Workspace-local $TMPDIR, scrubbed and pattern-escaped. +  -- "./Xtest-tmpdir/" => "Xtest%-tmpdir" +  local local_tmpdir = (tmpdir_is_local(tmpdir_get()) +    and tmpdir_get():gsub('^[ ./]+',''):gsub('%/+$',''):gsub('([^%w])', '%%%1') +    or nil)    local db_cmd    if hasenv('NVIM_TEST_CORE_GLOB_DIRECTORY') then      initial_path = os.getenv('NVIM_TEST_CORE_GLOB_DIRECTORY')      re = os.getenv('NVIM_TEST_CORE_GLOB_RE') -    exc_re = os.getenv('NVIM_TEST_CORE_EXC_RE') +    exc_re = { os.getenv('NVIM_TEST_CORE_EXC_RE'), local_tmpdir }      db_cmd = os.getenv('NVIM_TEST_CORE_DB_CMD') or gdb_db_cmd      random_skip = os.getenv('NVIM_TEST_CORE_RANDOM_SKIP')    elseif os.getenv('TRAVIS_OS_NAME') == 'osx' then      initial_path = '/cores'      re = nil -    exc_re = nil +    exc_re = { local_tmpdir }      db_cmd = lldb_db_cmd    else      initial_path = '.'      re = '/core[^/]*$' -    exc_re = '^/%.deps$' +    exc_re = { '^/%.deps$', local_tmpdir }      db_cmd = gdb_db_cmd      random_skip = true    end @@ -200,21 +276,8 @@ local function check_cores(app, force)      local esigns = ('='):rep(len / 2)      out:write(('\n%s Core file %s %s\n'):format(esigns, core, esigns))      out:flush() -    local pipe = io.popen( -        db_cmd:gsub('%$_NVIM_TEST_APP', app):gsub('%$_NVIM_TEST_CORE', core) -        .. ' 2>&1', 'r') -    if pipe then -      local bt = pipe:read('*a') -      if bt then -        out:write(bt) -        out:write('\n') -      else -        out:write('Failed to read from the pipe\n') -      end -    else -      out:write('Failed to create pipe\n') -    end -    out:flush() +    os.execute(db_cmd:gsub('%$_NVIM_TEST_APP', app):gsub('%$_NVIM_TEST_CORE', core) .. ' 2>&1') +    out:write('\n')      found_cores = found_cores + 1      os.remove(core)    end @@ -228,7 +291,7 @@ local function check_cores(app, force)  end  local function which(exe) -  local pipe = io.popen('which ' .. exe, 'r') +  local pipe = popen_r('which', exe)    local ret = pipe:read('*a')    pipe:close()    if ret == '' then @@ -238,6 +301,116 @@ local function which(exe)    end  end +local function repeated_read_cmd(...) +  for _ = 1, 10 do +    local stream = popen_r(...) +    local ret = stream:read('*a') +    stream:close() +    if ret then +      return ret +    end +  end +  print('ERROR: Failed to execute ' .. argss_to_cmd(...) .. ': nil return after 10 attempts') +  return nil +end + +local function shallowcopy(orig) +  if type(orig) ~= 'table' then +    return orig +  end +  local copy = {} +  for orig_key, orig_value in pairs(orig) do +    copy[orig_key] = orig_value +  end +  return copy +end + +local deepcopy + +local function id(v) +  return v +end + +local deepcopy_funcs = { +  table = function(orig) +    local copy = {} +    for k, v in pairs(orig) do +      copy[deepcopy(k)] = deepcopy(v) +    end +    return copy +  end, +  number = id, +  string = id, +  ['nil'] = id, +  boolean = id, +} + +deepcopy = function(orig) +  return deepcopy_funcs[type(orig)](orig) +end + +local REMOVE_THIS = {} + +local function mergedicts_copy(d1, d2) +  local ret = shallowcopy(d1) +  for k, v in pairs(d2) do +    if d2[k] == REMOVE_THIS then +      ret[k] = nil +    elseif type(d1[k]) == 'table' and type(v) == 'table' then +      ret[k] = mergedicts_copy(d1[k], v) +    else +      ret[k] = v +    end +  end +  return ret +end + +-- dictdiff: find a diff so that mergedicts_copy(d1, diff) is equal to d2 +-- +-- Note: does not do copies of d2 values used. +local function dictdiff(d1, d2) +  local ret = {} +  local hasdiff = false +  for k, v in pairs(d1) do +    if d2[k] == nil then +      hasdiff = true +      ret[k] = REMOVE_THIS +    elseif type(v) == type(d2[k]) then +      if type(v) == 'table' then +        local subdiff = dictdiff(v, d2[k]) +        if subdiff ~= nil then +          hasdiff = true +          ret[k] = subdiff +        end +      elseif v ~= d2[k] then +        ret[k] = d2[k] +        hasdiff = true +      end +    else +      ret[k] = d2[k] +      hasdiff = true +    end +  end +  for k, v in pairs(d2) do +    if d1[k] == nil then +      ret[k] = shallowcopy(v) +      hasdiff = true +    end +  end +  if hasdiff then +    return ret +  else +    return nil +  end +end + +local function updated(d, d2) +  for k, v in pairs(d2) do +    d[k] = v +  end +  return d +end +  local function concat_tables(...)    local ret = {}    for i = 1, select('#', ...) do @@ -251,7 +424,7 @@ local function concat_tables(...)    return ret  end -local function dedent(str) +local function dedent(str, leave_indent)    -- find minimum common indent across lines    local indent = nil    for line in str:gmatch('[^\n]+') do @@ -264,28 +437,277 @@ local function dedent(str)      -- no minimum common indent      return str    end +  local left_indent = (' '):rep(leave_indent or 0)    -- create a pattern for the indent    indent = indent:gsub('%s', '[ \t]')    -- strip it from the first line -  str = str:gsub('^'..indent, '') +  str = str:gsub('^'..indent, left_indent)    -- strip it from the remaining lines -  str = str:gsub('[\n]'..indent, '\n') +  str = str:gsub('[\n]'..indent, '\n' .. left_indent)    return str  end -return { -  eq = eq, -  neq = neq, -  ok = ok, +local function format_float(v) +  -- On windows exponent appears to have three digits and not two +  local ret = ('%.6e'):format(v) +  local l, f, es, e = ret:match('^(%-?%d)%.(%d+)e([+%-])0*(%d%d+)$') +  return l .. '.' .. f .. 'e' .. es .. e +end + +local SUBTBL = { +  '\\000', '\\001', '\\002', '\\003', '\\004', +  '\\005', '\\006', '\\007', '\\008', '\\t', +  '\\n',   '\\011', '\\012', '\\r',   '\\014', +  '\\015', '\\016', '\\017', '\\018', '\\019', +  '\\020', '\\021', '\\022', '\\023', '\\024', +  '\\025', '\\026', '\\027', '\\028', '\\029', +  '\\030', '\\031', +} + +local format_luav + +format_luav = function(v, indent, opts) +  opts = opts or {} +  local linesep = '\n' +  local next_indent_arg = nil +  local indent_shift = opts.indent_shift or '  ' +  local next_indent +  local nl = '\n' +  if indent == nil then +    indent = '' +    linesep = '' +    next_indent = '' +    nl = ' ' +  else +    next_indent_arg = indent .. indent_shift +    next_indent = indent .. indent_shift +  end +  local ret = '' +  if type(v) == 'string' then +    if opts.literal_strings then +      ret = v +    else +      local quote = opts.dquote_strings and '"' or '\'' +      ret = quote .. tostring(v):gsub( +        opts.dquote_strings and '["\\]' or '[\'\\]', +        '\\%0'):gsub( +          '[%z\1-\31]', function(match) +            return SUBTBL[match:byte() + 1] +          end) .. quote +    end +  elseif type(v) == 'table' then +    if v == REMOVE_THIS then +      ret = 'REMOVE_THIS' +    else +      local processed_keys = {} +      ret = '{' .. linesep +      local non_empty = false +      for i, subv in ipairs(v) do +        ret = ('%s%s%s,%s'):format(ret, next_indent, +                                   format_luav(subv, next_indent_arg, opts), nl) +        processed_keys[i] = true +        non_empty = true +      end +      for k, subv in pairs(v) do +        if not processed_keys[k] then +          if type(k) == 'string' and k:match('^[a-zA-Z_][a-zA-Z0-9_]*$') then +            ret = ret .. next_indent .. k .. ' = ' +          else +            ret = ('%s%s[%s] = '):format(ret, next_indent, +                                         format_luav(k, nil, opts)) +          end +          ret = ret .. format_luav(subv, next_indent_arg, opts) .. ',' .. nl +          non_empty = true +        end +      end +      if nl == ' ' and non_empty then +        ret = ret:sub(1, -3) +      end +      ret = ret  .. indent .. '}' +    end +  elseif type(v) == 'number' then +    if v % 1 == 0 then +      ret = ('%d'):format(v) +    else +      ret = format_float(v) +    end +  elseif type(v) == 'nil' then +    ret = 'nil' +  elseif type(v) == 'boolean' then +    ret = (v and 'true' or 'false') +  else +    print(type(v)) +    -- Not implemented yet +    assert(false) +  end +  return ret +end + +local function format_string(fmt, ...) +  local i = 0 +  local args = {...} +  local function getarg() +    i = i + 1 +    return args[i] +  end +  local ret = fmt:gsub('%%[0-9*]*%.?[0-9*]*[cdEefgGiouXxqsr%%]', function(match) +    local subfmt = match:gsub('%*', function() +      return tostring(getarg()) +    end) +    local arg = nil +    if subfmt:sub(-1) ~= '%' then +      arg = getarg() +    end +    if subfmt:sub(-1) == 'r' or subfmt:sub(-1) == 'q' then +      -- %r is like built-in %q, but it is supposed to single-quote strings and +      -- not double-quote them, and also work not only for strings. +      -- Builtin %q is replaced here as it gives invalid and inconsistent with +      -- luajit results for e.g. "\e" on lua: luajit transforms that into `\27`, +      -- lua leaves as-is. +      arg = format_luav(arg, nil, {dquote_strings = (subfmt:sub(-1) == 'q')}) +      subfmt = subfmt:sub(1, -2) .. 's' +    end +    if subfmt == '%e' then +      return format_float(arg) +    else +      return subfmt:format(arg) +    end +  end) +  return ret +end + +local function intchar2lua(ch) +  ch = tonumber(ch) +  return (20 <= ch and ch < 127) and ('%c'):format(ch) or ch +end + +local fixtbl_metatable = { +  __newindex = function() +    assert(false) +  end, +} + +local function fixtbl(tbl) +  return setmetatable(tbl, fixtbl_metatable) +end + +local function fixtbl_rec(tbl) +  for _, v in pairs(tbl) do +    if type(v) == 'table' then +      fixtbl_rec(v) +    end +  end +  return fixtbl(tbl) +end + +-- From https://github.com/premake/premake-core/blob/master/src/base/table.lua +local function table_flatten(arr) +  local result = {} +  local function _table_flatten(_arr) +    local n = #_arr +    for i = 1, n do +      local v = _arr[i] +      if type(v) == "table" then +        _table_flatten(v) +      elseif v then +        table.insert(result, v) +      end +    end +  end +  _table_flatten(arr) +  return result +end + +local function hexdump(str) +  local len = string.len(str) +  local dump = "" +  local hex = "" +  local asc = "" + +  for i = 1, len do +    if 1 == i % 8 then +      dump = dump .. hex .. asc .. "\n" +      hex = string.format("%04x: ", i - 1) +      asc = "" +    end + +    local ord = string.byte(str, i) +    hex = hex .. string.format("%02x ", ord) +    if ord >= 32 and ord <= 126 then +      asc = asc .. string.char(ord) +    else +      asc = asc .. "." +    end +  end + +  return dump .. hex .. string.rep("   ", 8 - len % 8) .. asc +end + +local function read_file(name) +  local file = io.open(name, 'r') +  if not file then +    return nil +  end +  local ret = file:read('*a') +  file:close() +  return ret +end + +-- Dedent the given text and write it to the file name. +local function write_file(name, text, no_dedent, append) +  local file = io.open(name, (append and 'a' or 'w')) +  if type(text) == 'table' then +    -- Byte blob +    local bytes = text +    text = '' +    for _, char in ipairs(bytes) do +      text = ('%s%c'):format(text, char) +    end +  elseif not no_dedent then +    text = dedent(text) +  end +  file:write(text) +  file:flush() +  file:close() +end + +local module = { +  REMOVE_THIS = REMOVE_THIS, +  argss_to_cmd = argss_to_cmd, +  check_cores = check_cores,    check_logs = check_logs, -  uname = uname, -  tmpname = tmpname, -  map = map, +  concat_tables = concat_tables, +  dedent = dedent, +  deepcopy = deepcopy, +  dictdiff = dictdiff, +  eq = eq, +  expect_err = expect_err,    filter = filter, +  fixtbl = fixtbl, +  fixtbl_rec = fixtbl_rec, +  format_luav = format_luav, +  format_string = format_string,    glob = glob, -  check_cores = check_cores,    hasenv = hasenv, +  hexdump = hexdump, +  intchar2lua = intchar2lua, +  map = map, +  matches = matches, +  mergedicts_copy = mergedicts_copy, +  neq = neq, +  ok = ok, +  popen_r = popen_r, +  popen_w = popen_w, +  read_file = read_file, +  repeated_read_cmd = repeated_read_cmd, +  sleep = sleep, +  shallowcopy = shallowcopy, +  table_flatten = table_flatten, +  tmpname = tmpname, +  uname = uname, +  updated = updated,    which = which, -  concat_tables = concat_tables, -  dedent = dedent, +  write_file = write_file,  } + +return module | 
