1-- Copyright (C) 2006 Timothy Brownawell <tbrownaw@gmail.com>
2--
3-- This program is made available under the GNU GPL version 2.0 or
4-- greater. See the accompanying file COPYING for details.
5--
6-- This program is distributed WITHOUT ANY WARRANTY; without even the
7-- implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
8-- PURPOSE.
9
10-- misc global values
11-- where the main testsuite file is
12srcdir = get_source_dir()
13-- where the individual test dirs are
14-- most paths will be testdir.."/something"
15-- normally reset by the main testsuite file
16testdir = srcdir
17-- was the -d switch given?
18debugging = false
19
20-- combined logfile; tester.cc will reset this to a filename, which is
21-- then opened in run_tests
22logfile = nil
23
24-- This is for redirected output from local implementations
25-- of shellutils type stuff (ie, grep).
26-- Reason: {set,clear}_redirect don't seem to (always?) work
27-- for this (at least on Windows).
28files = {stdout = nil, stdin = nil, stderr = nil}
29
30-- for convenience, this is the first word of what get_ostype() returns.
31ostype = string.sub(get_ostype(), 1, string.find(get_ostype(), " ")-1)
32
33-- Since Lua 5.2, unpack and loadstrings are deprecated and are either moved
34-- to table.unpack() or replaced by load(). If lua was compiled without
35-- LUA_COMPAT_UNPACK and/or LUA_COMPAT_LOADSTRING, these two are not
36-- available and we add a similar compatibility layer, ourselves.
37unpack = unpack or table.unpack
38loadstring = loadstring or load
39
40-- table of per-test values
41test = {}
42-- misc per-test values
43test.root = nil
44test.name = nil
45test.wanted_fail = false
46test.partial_skip = false -- set this to true if you skip part of the test
47
48--probably should put these in the error that gets thrown...
49test.errfile = ""
50test.errline = -1
51
52-- for tracking background processes
53test.bgid = 0
54test.bglist = {}
55
56test.log = nil -- logfile for this test
57
58-- hook to be overridden by the main testsuite file, if necessary;
59-- called after determining the set of tests to run.
60-- P may be used to write messages to the user's tty.
61function prepare_to_run_tests(P)
62   return 0
63end
64
65-- hook to be overridden by the main testsuite file, if necessary;
66-- called after opening the master logfile, but _before_ parsing
67-- arguments or determining the set of tests to run.
68-- P may be used to write messages to the user's tty.
69function prepare_to_enumerate_tests(P)
70   return 0
71end
72
73function L(...)
74  test.log:write(...)
75  test.log:flush()
76end
77
78function getsrcline()
79  local info
80  local depth = 1
81  repeat
82    depth = depth + 1
83    info = debug.getinfo(depth)
84  until info == nil
85  while src == nil and depth > 1 do
86    depth = depth - 1
87    info = debug.getinfo(depth)
88    if string.find(info.source, "^@.*__driver__%.lua") then
89      -- return info.source, info.currentline
90      return test.name, info.currentline
91    end
92  end
93end
94
95function locheader()
96  local _,line = getsrcline()
97  if line == nil then line = -1 end
98  if test.name == nil then
99    return "\n<unknown>:" .. line .. ": "
100  else
101    return "\n" .. test.name .. ":" .. line .. ": "
102  end
103end
104
105function err(what, level)
106  if level == nil then level = 2 end
107  test.errfile, test.errline = getsrcline()
108  local e
109  if type(what) == "table" then
110    e = what
111    if e.bt == nil then e.bt = {} end
112    table.insert(e.bt, debug.traceback())
113  else
114    e = {e = what, bt = {debug.traceback()}}
115  end
116  error(e, level)
117end
118
119do -- replace some builtings with logged versions
120  unlogged_mtime = mtime
121  mtime = function(name)
122    local x = unlogged_mtime(name)
123    L(locheader(), "mtime(", name, ") = ", tostring(x), "\n")
124    return x
125  end
126
127  unlogged_mkdir = mkdir
128  mkdir = function(name)
129    L(locheader(), "mkdir ", name, "\n")
130    unlogged_mkdir(name)
131  end
132
133  unlogged_existsonpath = existsonpath
134  existsonpath = function(name)
135    local r = (unlogged_existsonpath(name) == 0)
136    local what
137    if r then
138      what = "exists"
139    else
140      what = "does not exist"
141    end
142    L(locheader(), name, " ", what, " on the path\n")
143    return r
144  end
145end
146
147function numlines(filename)
148  local n = 0
149  for _ in io.lines(filename) do n = n + 1 end
150  L(locheader(), "numlines(", filename, ") = ", n, "\n")
151  return n
152end
153
154-- encodes a query by percent escaping reserved characters (as defined
155-- in RFC 3986) - except for the directory separator ('/').
156function url_encode_path(path)
157  path = string.gsub(path, "([!*'();:@&=+$,?#[%]])",
158    function (x) return string.format("%%%02X", string.byte(x)) end)
159  return string.gsub(path, " ", "+")
160end
161
162-- encodes a query by percent escaping reserved characters (as defined
163-- in RFC 3986) - except for the ampersand and equal sign ('&', '=')
164function url_encode_query(path)
165  path = string.gsub(path, "([!*'();:@+$,/?#[%]])",
166    function (x) return string.format("%%%02X", string.byte(x)) end)
167  return string.gsub(path, " ", "+")
168end
169
170function open_or_err(filename, mode, depth)
171  local file, e = io.open(filename, mode)
172  if file == nil then
173    err("Cannot open file " .. filename .. ": " .. e, depth)
174  end
175  return file
176end
177
178function fsize(filename)
179  local file = open_or_err(filename, "r", 3)
180  local size = file:seek("end")
181  file:close()
182  return size
183end
184
185function readfile_q(filename)
186  local file = open_or_err(filename, "rb", 3)
187  local dat = file:read("*a")
188  file:close()
189  return dat
190end
191
192function readfile(filename)
193  L(locheader(), "readfile ", filename, "\n")
194  return readfile_q(filename)
195end
196
197function readfile_lines(filename)
198   L(locheader(), "readfile_lines ", filename, "\n")
199   local file = open_or_err(filename, "rb", 2)
200   local dat = {}
201   for line in file:lines() do
202      table.insert(dat, line)
203   end
204   return dat
205end
206
207function readstdfile(filename)
208  return readfile(testdir.."/"..filename)
209end
210
211-- Return all but the first N lines of FILENAME.
212-- Note that (unlike readfile()) the result will
213-- end with a \n whether or not the file did.
214function tailfile(filename, n)
215  L(locheader(), "tailfile ", filename, ", ", n, "\n")
216  local i = 1
217  local t = {}
218  for l in io.lines(filename) do
219    if i > n then
220      table.insert(t, l)
221    end
222    i = i + 1
223  end
224  table.insert(t, "")
225  return table.concat(t, "\n")
226end
227
228function writefile_q(filename, dat)
229  local file,e
230  if dat == nil then
231    file,e = open_or_err(filename, "a+b", 3)
232  else
233    file,e = open_or_err(filename, "wb", 3)
234  end
235  if dat ~= nil then
236    file:write(dat)
237  end
238  file:close()
239  return true
240end
241
242function writefile(filename, dat)
243  L(locheader(), "writefile ", filename, "\n")
244  return writefile_q(filename, dat)
245end
246
247function append(filename, dat)
248  L(locheader(), "append to file ", filename, "\n")
249  local file,e = open_or_err(filename, "a+", 3)
250  file:write(dat)
251  file:close()
252  return true
253end
254
255do
256  unlogged_copy = copy_recursive
257  copy_recursive = nil
258  function copy(from, to)
259    L(locheader(), "copy ", from, " -> ", to, "\n")
260    local ok, res = unlogged_copy(from, to)
261    if not ok then
262      L(res, "\n")
263      return false
264    else
265      return true
266    end
267  end
268end
269
270do
271  local os_rename = os.rename
272  os.rename = nil
273  os.remove = nil
274  function rename(from, to)
275    L(locheader(), "rename ", from, " ", to, "\n")
276    if exists(to) and not isdir(to) then
277      L("Destination ", to, " exists; removing...\n")
278      local ok, res = unlogged_remove(to)
279      if not ok then
280        L("Could not remove ", to, ": ", res, "\n")
281        return false
282      end
283    end
284    local ok,res = os_rename(from, to)
285    if not ok then
286      L(res, "\n")
287      return false
288    else
289      return true
290    end
291  end
292  function unlogged_rename(from, to)
293    if exists(to) and not isdir(to) then
294      unlogged_remove(to)
295    end
296    os_rename(from, to)
297  end
298  unlogged_remove = remove_recursive
299  remove_recursive = nil
300  function remove(file)
301    L(locheader(), "remove ", file, "\n")
302    local ok,res = unlogged_remove(file)
303    if not ok then
304      L(res, "\n")
305      return false
306    else
307      return true
308    end
309  end
310end
311
312
313function getstd(name, as)
314  if as == nil then as = name end
315  local ret = copy(testdir .. "/" .. name, as)
316  make_tree_accessible(as)
317  return ret
318end
319
320function getcommon(name, as)
321  if as == nil then as = name end
322  local ret = copy(srcdir .. "/common/" .. name, as)
323  make_tree_accessible(as)
324  return ret
325end
326
327function get(name, as)
328  if as == nil then as = name end
329  return getstd(test.name .. "/" .. name, as)
330end
331
332-- include from the main tests directory; there's no reason
333-- to want to include from the dir for the current test,
334-- since in that case it could just go in the driver file.
335function include(name)
336  dofile(testdir.."/"..name)
337end
338
339function includecommon(name)
340  dofile(srcdir.."/common/"..name)
341end
342
343function trim(str)
344  return string.gsub(str, "^%s*(.-)%s*$", "%1")
345end
346
347function getpathof(exe, ext)
348  local function gotit(now)
349    if test.log == nil then
350      logfile:write(exe, " found at ", now, "\n")
351    else
352      test.log:write(exe, " found at ", now, "\n")
353    end
354    return now
355  end
356  local path = os.getenv("PATH")
357  local char
358  if ostype == "Windows" then
359    char = ';'
360  else
361    char = ':'
362  end
363  if ostype == "Windows" then
364    if ext == nil then ext = ".exe" end
365  else
366    if ext == nil then ext = "" end
367  end
368  local now = initial_dir.."/"..exe..ext
369  if exists(now) then return gotit(now) end
370  for x in string.gmatch(path, "[^"..char.."]*"..char) do
371    local dir = string.sub(x, 0, -2)
372    if string.find(dir, "[\\/]$") then
373      dir = string.sub(dir, 0, -2)
374    end
375    local now = dir.."/"..exe..ext
376    if exists(now) then return gotit(now) end
377  end
378  if test.log == nil then
379    logfile:write("Cannot find ", exe, "\n")
380  else
381    test.log:write("Cannot find ", exe, "\n")
382  end
383  return nil
384end
385
386function prepare_redirect(fin, fout, ferr)
387  local cwd = chdir(".").."/"
388  redir = {fin = cwd..fin, fout = cwd..fout, ferr = cwd..ferr}
389end
390do
391  oldspawn = spawn
392  function spawn(...)
393   if redir == nil then
394     return oldspawn(...)
395   else
396     return spawn_redirected(redir.fin, redir.fout, redir.ferr, ...)
397   end
398  end
399end
400function execute(path, ...)
401   local pid
402   local ret = -1
403   pid = spawn(path, ...)
404   redir = nil
405   if (pid ~= -1) then ret, pid = wait(pid) end
406   return ret
407end
408
409function cmd_as_str(cmd_table)
410  local str = ""
411  for i,x in ipairs(cmd_table) do
412    if str ~= "" then str = str .. " " end
413    if type(x) == "function" then
414      str = str .. "<function>"
415    else
416      local s = tostring(x)
417      if string.find(s, " ") then
418        str = str .. '"'..s..'"'
419      else
420        str = str .. s
421      end
422    end
423  end
424  return str
425end
426
427function runcmd(cmd, prefix, bgnd)
428  if prefix == nil then prefix = "ts-" end
429  if type(cmd) ~= "table" then err("runcmd called with bad argument") end
430  local local_redir = cmd.local_redirect
431  if cmd.local_redirect == nil then
432    if type(cmd[1]) == "function" then
433      local_redir = true
434    else
435      local_redir = false
436    end
437  end
438  if bgnd == true and type(cmd[1]) == "string" then local_redir = false end
439  L("\nruncmd: ", tostring(cmd[1]), ", local_redir = ", tostring(local_redir), ", requested = ", tostring(cmd.local_redirect))
440  local redir
441  if local_redir then
442    files.stdin = open_or_err(prefix.."stdin", nil, 2)
443    files.stdout = open_or_err(prefix.."stdout", "w", 2)
444    files.stderr = open_or_err(prefix.."stderr", "w", 2)
445  else
446    prepare_redirect(prefix.."stdin", prefix.."stdout", prefix.."stderr")
447  end
448
449  local result
450  if cmd.logline ~= nil then
451    L(locheader(), cmd.logline, "\n")
452  else
453    L(locheader(), cmd_as_str(cmd), "\n")
454  end
455
456  local oldexec = execute
457  if bgnd then
458     execute = spawn
459  end
460  if type(cmd[1]) == "function" then
461    result = {pcall(unpack(cmd))}
462  elseif type(cmd[1]) == "string" then
463     result = {pcall(execute, unpack(cmd))}
464  else
465     execute = oldexec
466    err("runcmd called with bad command table " ..
467	"(first entry is a " .. type(cmd[1]) ..")")
468 end
469 execute = oldexec
470
471  if local_redir then
472    files.stdin:close()
473    files.stdout:close()
474    files.stderr:close()
475  end
476  return unpack(result)
477end
478
479function samefile(left, right)
480  if left == "-" or right == "-" then
481    err("tests may not rely on standard input")
482  end
483  if fsize(left) ~= fsize(right) then
484    return false
485  else
486    local ldat = readfile(left)
487    local rdat = readfile(right)
488    return ldat == rdat
489 end
490end
491
492function samefilestd(left, right)
493   return samefile(testdir .. "/" .. test.name .. "/" .. left, right)
494end
495
496function samelines(f, t)
497  local fl = {}
498  for l in io.lines(f) do table.insert(fl, l) end
499  if not (#fl == #t) then
500    L(locheader(), string.format("file has %s lines; table has %s\n", #fl, #t))
501    return false
502  end
503  for i=1,#t do
504    if fl[i] ~= t[i] then
505      if fl[i] then
506        L(locheader(), string.format("file[%d] = '%s'; table[%d] = '%s'\n",
507                                     i, fl[i], i, t[i]))
508      else
509        L(locheader(), string.format("file[i] = ''; table[i] = '%s'\n",
510                                     t[i]))
511      end
512      return false
513    end
514  end
515  return true
516end
517
518function greplines(f, t)
519  local fl = {}
520  for l in io.lines(f) do table.insert(fl, l) end
521  if not (#fl == #t) then
522    L(locheader(), string.format("file has %s lines; table has %s\n", #fl, #t))
523    return false
524  end
525  for i=1,#t do
526    if not regex.search(t[i], fl[i]) then
527      L(locheader(), string.format("file[i] = '%s'; table[i] = '%s'\n",
528                                   fl[i], t[i]))
529      return false
530    end
531  end
532  return true
533end
534
535function grep(...)
536  local flags, what, where = ...
537  local dogrep = function ()
538                   if where == nil and string.sub(flags, 1, 1) ~= "-" then
539                     where = what
540                     what = flags
541                     flags = ""
542                   end
543                   local quiet = string.find(flags, "q") ~= nil
544                   local reverse = string.find(flags, "v") ~= nil
545                   if not quiet and files.stdout == nil then err("non-quiet grep not redirected") end
546                   local out = 1
547                   local infile = files.stdin
548                   if where ~= nil then infile = open_or_err(where) end
549                   for line in infile:lines() do
550                     local matched = regex.search(what, line)
551                     if reverse then matched = not matched end
552                     if matched then
553                       if not quiet then files.stdout:write(line, "\n") end
554                       out = 0
555                     end
556                   end
557                   if where ~= nil then infile:close() end
558                   return out
559                 end
560  return {dogrep, logline = "grep "..cmd_as_str({...})}
561end
562
563function cat(...)
564  local arguments = {...}
565  local function docat()
566    local bsize = 8*1024
567    for _,x in ipairs(arguments) do
568      local infile
569      if x == "-" then
570        infile = files.stdin
571      else
572        infile = open_or_err(x, "rb", 3)
573      end
574      local block = infile:read(bsize)
575      while block do
576        files.stdout:write(block)
577        block = infile:read(bsize)
578      end
579      if x ~= "-" then
580        infile:close()
581      end
582    end
583    return 0
584  end
585  return {docat, logline = "cat "..cmd_as_str({...})}
586end
587
588function tail(...)
589  local file, num = ...
590  local function dotail()
591    if num == nil then num = 10 end
592    local mylines = {}
593    for l in io.lines(file) do
594      table.insert(mylines, l)
595      if #mylines > num then
596        table.remove(mylines, 1)
597      end
598    end
599    for _,x in ipairs(mylines) do
600      files.stdout:write(x, "\n")
601    end
602    return 0
603  end
604  return {dotail, logline = "tail "..cmd_as_str({...})}
605end
606
607function sort(file)
608  local function dosort(file)
609    local infile
610    if file == nil then
611      infile = files.stdin
612    else
613      infile = open_or_err(file)
614    end
615    local lines = {}
616    for l in infile:lines() do
617      table.insert(lines, l)
618    end
619    if file ~= nil then infile:close() end
620    table.sort(lines)
621    for _,l in ipairs(lines) do
622      files.stdout:write(l, "\n")
623    end
624    return 0
625  end
626  return {dosort, file, logline = "sort "..file}
627end
628
629function log_file_contents(filename)
630  L(readfile_q(filename), "\n")
631end
632
633function pre_cmd(stdin, ident)
634  if ident == nil then ident = "ts-" end
635  if stdin == true then
636    unlogged_copy("stdin", ident .. "stdin")
637  elseif type(stdin) == "table" then
638    unlogged_copy(stdin[1], ident .. "stdin")
639  else
640    local infile = open_or_err(ident .. "stdin", "w", 3)
641    if stdin ~= nil and stdin ~= false then
642      infile:write(stdin)
643    end
644    infile:close()
645  end
646  L("stdin:\n")
647  log_file_contents(ident .. "stdin")
648end
649
650function post_cmd(result, ret, stdout, stderr, ident)
651  if ret == nil then ret = 0 end
652  if ident == nil then ident = "ts-" end
653  L("stdout:\n")
654  log_file_contents(ident .. "stdout")
655  L("stderr:\n")
656  log_file_contents(ident .. "stderr")
657  L("exit code: " .. result .. "\n")
658  if result ~= ret and ret ~= false then
659    err("Check failed (return value): wanted " .. ret .. " got " .. result, 3)
660  end
661
662  if stdout == nil then
663    if fsize(ident .. "stdout") ~= 0 then
664      err("Check failed (stdout): not empty", 3)
665    end
666  elseif type(stdout) == "string" then
667    local realout = open_or_err(ident .. "stdout", nil, 3)
668    local contents = realout:read("*a")
669    realout:close()
670    if contents ~= stdout then
671      err("Check failed (stdout): doesn't match", 3)
672    end
673  elseif type(stdout) == "table" then
674    if not samefile(ident .. "stdout", stdout[1]) then
675      err("Check failed (stdout): doesn't match", 3)
676    end
677  elseif stdout == true then
678    unlogged_remove("stdout")
679    unlogged_rename(ident .. "stdout", "stdout")
680  end
681
682  if stderr == nil then
683    if fsize(ident .. "stderr") ~= 0 then
684      err("Check failed (stderr): not empty", 3)
685    end
686  elseif type(stderr) == "string" then
687    local realerr = open_or_err(ident .. "stderr", nil, 3)
688    local contents = realerr:read("*a")
689    realerr:close()
690    if contents ~= stderr then
691      err("Check failed (stderr): doesn't match", 3)
692    end
693  elseif type(stderr) == "table" then
694    if not samefile(ident .. "stderr", stderr[1]) then
695      err("Check failed (stderr): doesn't match", 3)
696    end
697  elseif stderr == true then
698    unlogged_remove("stderr")
699    unlogged_rename(ident .. "stderr", "stderr")
700  end
701end
702
703-- std{out,err} can be:
704--   * false: ignore
705--   * true: ignore, copy to stdout
706--   * string: check that it matches the contents
707--   * nil: must be empty
708--   * {string}: check that it matches the named file
709-- stdin can be:
710--   * true: use existing "stdin" file
711--   * nil, false: empty input
712--   * string: contents of string
713--   * {string}: contents of the named file
714
715function bg(torun, ret, stdout, stderr, stdin)
716  test.bgid = test.bgid + 1
717  local out = {}
718  out.prefix = "ts-" .. test.bgid .. "-"
719  pre_cmd(stdin, out.prefix)
720  L("Starting background command...")
721  local ok,pid = runcmd(torun, out.prefix, true)
722  if not ok then err(pid, 2) end
723  if pid == -1 then err("Failed to start background process\n", 2) end
724  out.pid = pid
725  test.bglist[test.bgid] = out
726  out.id = test.bgid
727  out.retval = nil
728  out.locstr = locheader()
729  out.cmd = torun
730  out.expret = ret
731  out.expout = stdout
732  out.experr = stderr
733  local mt = {}
734  mt.__index = mt
735  mt.finish = function(obj, timeout)
736                if obj.retval ~= nil then return end
737
738                if timeout == nil then timeout = 0 end
739                if type(timeout) ~= "number" then
740                  err("Bad timeout of type "..type(timeout))
741                end
742                local res
743                obj.retval, res = timed_wait(obj.pid, timeout)
744                if (res == -1) then
745                  if (obj.retval ~= 0) then
746                    L(locheader(), "error in timed_wait ", obj.retval, "\n")
747                  end
748                  kill(obj.pid, 15) -- TERM
749                  obj.retval, res = timed_wait(obj.pid, 2)
750                  if (res == -1) then
751                    kill(obj.pid, 9) -- KILL
752                    obj.retval, res = timed_wait(obj.pid, 2)
753                  end
754                end
755
756                test.bglist[obj.id] = nil
757                L(locheader(), "checking background command from ", out.locstr,
758		  cmd_as_str(out.cmd), "\n")
759                post_cmd(obj.retval, out.expret, out.expout, out.experr, obj.prefix)
760                return true
761              end
762  mt.wait = function(obj, timeout)
763              if obj.retval ~= nil then return end
764              if timeout == nil then
765                obj.retval = wait(obj.pid)
766              else
767                local res
768                obj.retval, res = timed_wait(obj.pid, timeout)
769                if res == -1 then
770                  obj.retval = nil
771                  return false
772                end
773              end
774              test.bglist[obj.id] = nil
775              L(locheader(), "checking background command from ", out.locstr,
776                table.concat(out.cmd, " "), "\n")
777              post_cmd(obj.retval, out.expret, out.expout, out.experr, obj.prefix)
778              return true
779            end
780  return setmetatable(out, mt)
781end
782
783function runcheck(cmd, ret, stdout, stderr, stdin)
784  if ret == nil then ret = 0 end
785  pre_cmd(stdin)
786  local ok, result = runcmd(cmd)
787  if ok == false then
788    err(result, 2)
789  end
790  post_cmd(result, ret, stdout, stderr)
791  return result
792end
793
794function indir(dir, what)
795  if type(what) ~= "table" then
796    err("bad argument of type "..type(what).." to indir()")
797  end
798  local function do_indir()
799    local savedir = chdir(dir)
800    if savedir == nil then
801      err("Cannot chdir to "..dir)
802    end
803    local ok, res
804    if type(what[1]) == "function" then
805      ok, res = pcall(unpack(what))
806    elseif type(what[1]) == "string" then
807      ok, res = pcall(execute, unpack(what))
808    else
809      err("bad argument to indir(): cannot execute a "..type(what[1]))
810    end
811    chdir(savedir)
812    if not ok then err(res) end
813    return res
814  end
815  local want_local
816  if type(what[1]) == "function" then
817    if type(what.local_redirect) == "nil" then
818      want_local = true
819    else
820      want_local = what.local_redirect
821    end
822  else
823    want_local = false
824  end
825  local ll = "In directory "..dir..": "
826  if what.logline ~= nil then ll = ll .. tostring(what.logline)
827  else
828    ll = ll .. cmd_as_str(what)
829  end
830  return {do_indir, local_redirect = want_local, logline = ll}
831end
832
833function check(first, ...)
834  if type(first) == "table" then
835    return runcheck(first, ...)
836  elseif type(first) == "boolean" then
837    if not first then err("Check failed: false", 2) end
838  elseif type(first) == "number" then
839    if first ~= 0 then
840      err("Check failed: " .. first .. " ~= 0", 2)
841    end
842  else
843    err("Bad argument to check() (" .. type(first) .. ")", 2)
844  end
845  return first
846end
847
848function skip_if(chk)
849  if chk then
850    err(true, 2)
851  end
852end
853
854function xfail_if(chk, ...)
855  local ok,res = pcall(check, ...)
856  if ok == false then
857    if chk then err(false, 2) else err(err, 2) end
858  else
859    if chk then
860      test.wanted_fail = true
861      L("UNEXPECTED SUCCESS\n")
862    end
863  end
864end
865
866function xfail(...)
867   xfail_if(true, ...)
868end
869
870function log_error(e)
871  if type(e) == "table" then
872    L("\n", tostring(e.e), "\n")
873    for i,bt in ipairs(e.bt) do
874      if i ~= 1 then L("Rethrown from:") end
875      L(bt)
876    end
877  else
878    L("\n", tostring(e), "\n")
879  end
880end
881
882function run_tests(debugging, list_only, run_dir, logname, args, progress)
883  local torun = {}
884  local run_all = true
885
886  local function P(...)
887     progress(...)
888     logfile:write(...)
889  end
890
891  -- NLS nuisances.
892  for _,name in pairs({  "LANG",
893			 "LANGUAGE",
894			 "LC_ADDRESS",
895			 "LC_ALL",
896			 "LC_COLLATE",
897			 "LC_CTYPE",
898			 "LC_IDENTIFICATION",
899			 "LC_MEASUREMENT",
900			 "LC_MESSAGES",
901			 "LC_MONETARY",
902			 "LC_NAME",
903			 "LC_NUMERIC",
904			 "LC_PAPER",
905			 "LC_TELEPHONE",
906			 "LC_TIME"  }) do
907     set_env(name,"C")
908  end
909
910  -- no test suite should touch the user's ssh agent or display
911  unset_env("SSH_AUTH_SOCK")
912  unset_env("DISPLAY")
913
914  -- tests do not use (interactive) editors for commits
915  unset_env("EDITOR")
916  unset_env("VISUAL")
917
918  -- tests should not be timezone sensitive
919  set_env("TZ", "UTC")
920
921  logfile = io.open(logname, "w")
922  chdir(run_dir);
923
924  do
925     local s = prepare_to_enumerate_tests(P)
926     if s ~= 0 then
927	P("Enumeration of tests failed.\n")
928	return s
929     end
930  end
931
932  -- testdir is set by the testsuite definition
933  -- any directory in testdir with a __driver__.lua inside is a test case
934  local tests = {}
935  for _,candidate in ipairs(read_directory(testdir)) do
936     -- n.b. it is not necessary to throw out directories before doing
937     -- this check, because exists(nondirectory/__driver__.lua) will
938     -- never be true.
939     if exists(testdir .. "/" .. candidate .. "/__driver__.lua") then
940	table.insert(tests, candidate)
941     end
942  end
943  table.sort(tests)
944
945  for i,a in pairs(args) do
946    local _1,_2,l,r = string.find(a, "^(-?%d+)%.%.(-?%d+)$")
947    if _1 then
948      l = l + 0
949      r = r + 0
950      if l < 1 then l = #tests + l + 1 end
951      if r < 1 then r = #tests + r + 1 end
952      if l > r then l,r = r,l end
953      for j = l,r do
954        torun[j] = tests[j]
955      end
956      run_all = false
957    elseif string.find(a, "^-?%d+$") then
958      r = a + 0
959      if r < 1 then r = #tests + r + 1 end
960      torun[r] = tests[r]
961      run_all = false
962    else
963      -- pattern
964      run_all = false
965      local matched = false
966      for i,t in pairs(tests) do
967        if regex.search(a, t) then
968          torun[i] = t
969          matched = true
970        end
971      end
972      if not matched then
973        print(string.format("Warning: pattern '%s' does not match any tests.", a))
974      end
975    end
976  end
977
978  if run_all then torun = tests end
979
980  if list_only then
981    for i,t in pairs(torun) do
982      if i < 10 then P(" ") end
983      if i < 100 then P(" ") end
984      P(i .. " " .. t .. "\n")
985    end
986    logfile:close()
987    return 0
988  end
989
990  logfile:write("Running on ", get_ostype(), "\n\n")
991  local s = prepare_to_run_tests(P)
992  if s ~= 0 then
993    P("Test suite preparation failed.\n")
994    return s
995  end
996  P("Running tests...\n")
997
998  local counts = {}
999  counts.success = 0
1000  counts.skip = 0
1001  counts.xfail = 0
1002  counts.noxfail = 0
1003  counts.fail = 0
1004  counts.total = 0
1005  counts.of_interest = 0
1006  local of_interest = {}
1007  local failed_testlogs = {}
1008
1009  -- exit codes which indicate failure at a point in the process-spawning
1010  -- code where it is impossible to give more detailed diagnostics
1011  local magic_exit_codes = {
1012     [121] = "error creating test directory",
1013     [122] = "error spawning test process",
1014     [123] = "error entering test directory",
1015     [124] = "unhandled exception in child process"
1016  }
1017
1018  -- callback closure passed to run_tests_in_children
1019  local function report_one_test(tno, tname, status, wall_seconds, cpu_seconds)
1020     local tdir = run_dir .. "/" .. tname
1021     local test_header = string.format("%3d %-45s", tno, tname)
1022     local what
1023     local can_delete
1024     -- the child should always exit successfully, just to avoid
1025     -- headaches.  if we get any other code we report it as a failure.
1026     if status ~= 0 then
1027	if status < 0 then
1028	   what = string.format("FAIL (signal %d)", -status)
1029	elseif magic_exit_codes[status] ~= nil then
1030	   what = string.format("FAIL (%s)", magic_exit_codes[status])
1031	else
1032	   what = string.format("FAIL (exit %d)", status)
1033	end
1034     else
1035	local wfile, err = io.open(tdir .. "/STATUS", "r")
1036	if wfile ~= nil then
1037	   what = string.gsub(wfile:read("*a"), "\n$", "")
1038	   wfile:close()
1039	else
1040	   what = string.format("FAIL (status file: %s)", err)
1041	end
1042     end
1043     if what == "unexpected success" then
1044	counts.noxfail = counts.noxfail + 1
1045	counts.of_interest = counts.of_interest + 1
1046	table.insert(of_interest, test_header .. "unexpected success")
1047	can_delete = false
1048     elseif what == "partial skip" or what == "ok" then
1049	counts.success = counts.success + 1
1050	can_delete = true
1051     elseif string.find(what, "skipped ") == 1 then
1052	counts.skip = counts.skip + 1
1053	can_delete = true
1054     elseif string.find(what, "expected failure ") == 1 then
1055	counts.xfail = counts.xfail + 1
1056	can_delete = false
1057     elseif string.find(what, "FAIL ") == 1 then
1058	counts.fail = counts.fail + 1
1059	table.insert(of_interest, test_header .. what)
1060	table.insert(failed_testlogs, tdir .. "/tester.log")
1061	can_delete = false
1062     else
1063	counts.fail = counts.fail + 1
1064	what = "FAIL (gobbledygook: " .. what .. ")"
1065	table.insert(of_interest, test_header .. what)
1066	table.insert(failed_testlogs, tdir .. "/tester.log")
1067	can_delete = false
1068     end
1069
1070     counts.total = counts.total + 1
1071     local format_seconds = function (seconds)
1072                               return string.format("%d:%02d",
1073                                                    seconds / 60,
1074                                                    seconds % 60)
1075                            end
1076     local times = ""
1077     if wall_seconds > -1 then
1078        times = format_seconds(wall_seconds)
1079        if cpu_seconds > -1 then
1080           times = times .. ", " .. format_seconds(cpu_seconds) .. " on CPU"
1081        end
1082     end
1083     P(string.format("%s %s %s\n", test_header, what, times))
1084     return (can_delete and not debugging)
1085  end
1086
1087  run_tests_in_children(torun, report_one_test)
1088
1089  if counts.of_interest ~= 0 and (counts.total / counts.of_interest) > 4 then
1090   P("\nInteresting tests:\n")
1091   for i,x in ipairs(of_interest) do
1092     P(x, "\n")
1093   end
1094  end
1095  P("\n")
1096
1097  for i,log in pairs(failed_testlogs) do
1098    local tlog = io.open(log, "r")
1099    if tlog ~= nil then
1100      local dat = tlog:read("*a")
1101      tlog:close()
1102      logfile:write("\n", string.rep("*", 50), "\n")
1103      logfile:write(dat)
1104    end
1105  end
1106
1107  -- Write out this summary in one go so that it does not get interrupted
1108  -- by concurrent test suites' summaries.
1109  P(string.format("Of %i tests run:\n"..
1110		  "\t%i succeeded\n"..
1111		  "\t%i failed\n"..
1112		  "\t%i had expected failures\n"..
1113		  "\t%i succeeded unexpectedly\n"..
1114		  "\t%i were skipped\n",
1115		  counts.total, counts.success, counts.fail,
1116		  counts.xfail, counts.noxfail, counts.skip))
1117
1118  logfile:close()
1119  if counts.success + counts.skip + counts.xfail == counts.total then
1120    return 0
1121  else
1122    return 1
1123  end
1124end
1125
1126function run_one_test(tname)
1127   test.bgid = 0
1128   test.name = tname
1129   test.wanted_fail = false
1130   test.partial_skip = false
1131   test.root = chdir(".")
1132   test.errfile = ""
1133   test.errline = -1
1134   test.bglist = {}
1135   test.log = io.open("tester.log", "w")
1136
1137   -- Sanitize $HOME.  This is done here so that each test gets its
1138   -- very own empty directory (in case some test writes stuff inside).
1139   unlogged_mkdir("emptyhomedir")
1140   test.home = test.root .. "/emptyhomedir"
1141   if ostype == "Windows" then
1142      set_env("APPDATA", test.home)
1143   else
1144      set_env("HOME", test.home)
1145   end
1146
1147   L("Test ", test.name, "\n")
1148
1149   local driverfile = testdir .. "/" .. test.name .. "/__driver__.lua"
1150   local driver, e = loadfile(driverfile)
1151   local r
1152   if driver == nil then
1153      r = false
1154      e = "Could not load driver file " .. driverfile .. ".\n" .. e
1155   else
1156      local oldmask = posix_umask(0)
1157      posix_umask(oldmask)
1158      r,e = xpcall(driver, debug.traceback)
1159      local errline = test.errline
1160      for i,b in pairs(test.bglist) do
1161	 local a,x = pcall(function () b:finish(0) end)
1162	 if r and not a then
1163	    r = a
1164	    e = x
1165	 elseif not a then
1166	    L("Error cleaning up background processes: ",
1167	      tostring(b.locstr), "\n")
1168	 end
1169      end
1170      if type(cleanup) == "function" then
1171	 local a,b = pcall(cleanup)
1172	 if r and not a then
1173	    r = a
1174	    e = b
1175	 end
1176      end
1177      test.errline = errline
1178      posix_umask(oldmask)
1179   end
1180
1181   if not r then
1182      if test.errline == nil then test.errline = -1 end
1183      if type(e) ~= "table" then
1184	 local tbl = {e = e, bt = {"no backtrace; type(err) = "..type(e)}}
1185	 e = tbl
1186      end
1187      if type(e.e) ~= "boolean" then
1188	 log_error(e)
1189      end
1190      test.log:write("\n")
1191   end
1192   test.log:close()
1193
1194   -- record the short status where report_one_test can find it
1195   local s = io.open(test.root .. "/STATUS", "w")
1196   if r then
1197      if test.wanted_fail then
1198	 s:write("unexpected success\n")
1199      else
1200	 if test.partial_skip then
1201	    s:write("partial skip\n")
1202	 else
1203	    s:write("ok\n")
1204	 end
1205      end
1206   else
1207      if e.e == true then
1208	 s:write(string.format("skipped (line %i)\n", test.errline))
1209      elseif e.e == false then
1210	 s:write(string.format("expected failure (line %i)\n",
1211			       test.errline))
1212      else
1213	 s:write(string.format("FAIL (line %i)\n", test.errline))
1214      end
1215   end
1216   s:close()
1217   return 0
1218end
1219