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