tests/run-tests.py
changeset 1525 25a0c31882df
parent 1524 bfbd99b50f8f
child 1530 cafa9437a537
equal deleted inserted replaced
1524:bfbd99b50f8f 1525:25a0c31882df
     1 #!/usr/bin/env python
       
     2 #
       
     3 # run-tests.py - Run a set of tests on Mercurial
       
     4 #
       
     5 # Copyright 2006 Matt Mackall <mpm@selenic.com>
       
     6 #
       
     7 # This software may be used and distributed according to the terms of the
       
     8 # GNU General Public License version 2 or any later version.
       
     9 
       
    10 # Modifying this script is tricky because it has many modes:
       
    11 #   - serial (default) vs parallel (-jN, N > 1)
       
    12 #   - no coverage (default) vs coverage (-c, -C, -s)
       
    13 #   - temp install (default) vs specific hg script (--with-hg, --local)
       
    14 #   - tests are a mix of shell scripts and Python scripts
       
    15 #
       
    16 # If you change this script, it is recommended that you ensure you
       
    17 # haven't broken it by running it in various modes with a representative
       
    18 # sample of test scripts.  For example:
       
    19 #
       
    20 #  1) serial, no coverage, temp install:
       
    21 #      ./run-tests.py test-s*
       
    22 #  2) serial, no coverage, local hg:
       
    23 #      ./run-tests.py --local test-s*
       
    24 #  3) serial, coverage, temp install:
       
    25 #      ./run-tests.py -c test-s*
       
    26 #  4) serial, coverage, local hg:
       
    27 #      ./run-tests.py -c --local test-s*      # unsupported
       
    28 #  5) parallel, no coverage, temp install:
       
    29 #      ./run-tests.py -j2 test-s*
       
    30 #  6) parallel, no coverage, local hg:
       
    31 #      ./run-tests.py -j2 --local test-s*
       
    32 #  7) parallel, coverage, temp install:
       
    33 #      ./run-tests.py -j2 -c test-s*          # currently broken
       
    34 #  8) parallel, coverage, local install:
       
    35 #      ./run-tests.py -j2 -c --local test-s*  # unsupported (and broken)
       
    36 #  9) parallel, custom tmp dir:
       
    37 #      ./run-tests.py -j2 --tmpdir /tmp/myhgtests
       
    38 #
       
    39 # (You could use any subset of the tests: test-s* happens to match
       
    40 # enough that it's worth doing parallel runs, few enough that it
       
    41 # completes fairly quickly, includes both shell and Python scripts, and
       
    42 # includes some scripts that run daemon processes.)
       
    43 
       
    44 from distutils import version
       
    45 import difflib
       
    46 import errno
       
    47 import optparse
       
    48 import os
       
    49 import shutil
       
    50 import subprocess
       
    51 import signal
       
    52 import sys
       
    53 import tempfile
       
    54 import time
       
    55 import re
       
    56 
       
    57 closefds = os.name == 'posix'
       
    58 def Popen4(cmd, bufsize=-1):
       
    59     p = subprocess.Popen(cmd, shell=True, bufsize=bufsize,
       
    60                          close_fds=closefds,
       
    61                          stdin=subprocess.PIPE, stdout=subprocess.PIPE,
       
    62                          stderr=subprocess.STDOUT)
       
    63     p.fromchild = p.stdout
       
    64     p.tochild = p.stdin
       
    65     p.childerr = p.stderr
       
    66     return p
       
    67 
       
    68 # reserved exit code to skip test (used by hghave)
       
    69 SKIPPED_STATUS = 80
       
    70 SKIPPED_PREFIX = 'skipped: '
       
    71 FAILED_PREFIX  = 'hghave check failed: '
       
    72 PYTHON = sys.executable
       
    73 IMPL_PATH = 'PYTHONPATH'
       
    74 if 'java' in sys.platform:
       
    75     IMPL_PATH = 'JYTHONPATH'
       
    76 
       
    77 requiredtools = ["python", "diff", "grep", "sed"]
       
    78 
       
    79 defaults = {
       
    80     'jobs': ('HGTEST_JOBS', 1),
       
    81     'timeout': ('HGTEST_TIMEOUT', 180),
       
    82     'port': ('HGTEST_PORT', 20059),
       
    83 }
       
    84 
       
    85 def parseargs():
       
    86     parser = optparse.OptionParser("%prog [options] [tests]")
       
    87 
       
    88     # keep these sorted
       
    89     parser.add_option("--blacklist", action="append",
       
    90         help="skip tests listed in the specified blacklist file")
       
    91     parser.add_option("-C", "--annotate", action="store_true",
       
    92         help="output files annotated with coverage")
       
    93     parser.add_option("--child", type="int",
       
    94         help="run as child process, summary to given fd")
       
    95     parser.add_option("-c", "--cover", action="store_true",
       
    96         help="print a test coverage report")
       
    97     parser.add_option("-d", "--debug", action="store_true",
       
    98         help="debug mode: write output of test scripts to console"
       
    99              " rather than capturing and diff'ing it (disables timeout)")
       
   100     parser.add_option("-f", "--first", action="store_true",
       
   101         help="exit on the first test failure")
       
   102     parser.add_option("--inotify", action="store_true",
       
   103         help="enable inotify extension when running tests")
       
   104     parser.add_option("-i", "--interactive", action="store_true",
       
   105         help="prompt to accept changed output")
       
   106     parser.add_option("-j", "--jobs", type="int",
       
   107         help="number of jobs to run in parallel"
       
   108              " (default: $%s or %d)" % defaults['jobs'])
       
   109     parser.add_option("--keep-tmpdir", action="store_true",
       
   110         help="keep temporary directory after running tests")
       
   111     parser.add_option("-k", "--keywords",
       
   112         help="run tests matching keywords")
       
   113     parser.add_option("-l", "--local", action="store_true",
       
   114         help="shortcut for --with-hg=<testdir>/../hg")
       
   115     parser.add_option("-n", "--nodiff", action="store_true",
       
   116         help="skip showing test changes")
       
   117     parser.add_option("-p", "--port", type="int",
       
   118         help="port on which servers should listen"
       
   119              " (default: $%s or %d)" % defaults['port'])
       
   120     parser.add_option("--pure", action="store_true",
       
   121         help="use pure Python code instead of C extensions")
       
   122     parser.add_option("-R", "--restart", action="store_true",
       
   123         help="restart at last error")
       
   124     parser.add_option("-r", "--retest", action="store_true",
       
   125         help="retest failed tests")
       
   126     parser.add_option("-S", "--noskips", action="store_true",
       
   127         help="don't report skip tests verbosely")
       
   128     parser.add_option("-t", "--timeout", type="int",
       
   129         help="kill errant tests after TIMEOUT seconds"
       
   130              " (default: $%s or %d)" % defaults['timeout'])
       
   131     parser.add_option("--tmpdir", type="string",
       
   132         help="run tests in the given temporary directory"
       
   133              " (implies --keep-tmpdir)")
       
   134     parser.add_option("-v", "--verbose", action="store_true",
       
   135         help="output verbose messages")
       
   136     parser.add_option("--view", type="string",
       
   137         help="external diff viewer")
       
   138     parser.add_option("--with-hg", type="string",
       
   139         metavar="HG",
       
   140         help="test using specified hg script rather than a "
       
   141              "temporary installation")
       
   142     parser.add_option("-3", "--py3k-warnings", action="store_true",
       
   143         help="enable Py3k warnings on Python 2.6+")
       
   144 
       
   145     for option, default in defaults.items():
       
   146         defaults[option] = int(os.environ.get(*default))
       
   147     parser.set_defaults(**defaults)
       
   148     (options, args) = parser.parse_args()
       
   149 
       
   150     # jython is always pure
       
   151     if 'java' in sys.platform or '__pypy__' in sys.modules:
       
   152         options.pure = True
       
   153 
       
   154     if options.with_hg:
       
   155         if not (os.path.isfile(options.with_hg) and
       
   156                 os.access(options.with_hg, os.X_OK)):
       
   157             parser.error('--with-hg must specify an executable hg script')
       
   158         if not os.path.basename(options.with_hg) == 'hg':
       
   159             sys.stderr.write('warning: --with-hg should specify an hg script')
       
   160     if options.local:
       
   161         testdir = os.path.dirname(os.path.realpath(sys.argv[0]))
       
   162         hgbin = os.path.join(os.path.dirname(testdir), 'hg')
       
   163         if not os.access(hgbin, os.X_OK):
       
   164             parser.error('--local specified, but %r not found or not executable'
       
   165                          % hgbin)
       
   166         options.with_hg = hgbin
       
   167 
       
   168     options.anycoverage = options.cover or options.annotate
       
   169     if options.anycoverage:
       
   170         try:
       
   171             import coverage
       
   172             covver = version.StrictVersion(coverage.__version__).version
       
   173             if covver < (3, 3):
       
   174                 parser.error('coverage options require coverage 3.3 or later')
       
   175         except ImportError:
       
   176             parser.error('coverage options now require the coverage package')
       
   177 
       
   178     if options.anycoverage and options.local:
       
   179         # this needs some path mangling somewhere, I guess
       
   180         parser.error("sorry, coverage options do not work when --local "
       
   181                      "is specified")
       
   182 
       
   183     global vlog
       
   184     if options.verbose:
       
   185         if options.jobs > 1 or options.child is not None:
       
   186             pid = "[%d]" % os.getpid()
       
   187         else:
       
   188             pid = None
       
   189         def vlog(*msg):
       
   190             if pid:
       
   191                 print pid,
       
   192             for m in msg:
       
   193                 print m,
       
   194             print
       
   195             sys.stdout.flush()
       
   196     else:
       
   197         vlog = lambda *msg: None
       
   198 
       
   199     if options.tmpdir:
       
   200         options.tmpdir = os.path.expanduser(options.tmpdir)
       
   201 
       
   202     if options.jobs < 1:
       
   203         parser.error('--jobs must be positive')
       
   204     if options.interactive and options.jobs > 1:
       
   205         print '(--interactive overrides --jobs)'
       
   206         options.jobs = 1
       
   207     if options.interactive and options.debug:
       
   208         parser.error("-i/--interactive and -d/--debug are incompatible")
       
   209     if options.debug:
       
   210         if options.timeout != defaults['timeout']:
       
   211             sys.stderr.write(
       
   212                 'warning: --timeout option ignored with --debug\n')
       
   213         options.timeout = 0
       
   214     if options.py3k_warnings:
       
   215         if sys.version_info[:2] < (2, 6) or sys.version_info[:2] >= (3, 0):
       
   216             parser.error('--py3k-warnings can only be used on Python 2.6+')
       
   217     if options.blacklist:
       
   218         blacklist = dict()
       
   219         for filename in options.blacklist:
       
   220             try:
       
   221                 path = os.path.expanduser(os.path.expandvars(filename))
       
   222                 f = open(path, "r")
       
   223             except IOError, err:
       
   224                 if err.errno != errno.ENOENT:
       
   225                     raise
       
   226                 print "warning: no such blacklist file: %s" % filename
       
   227                 continue
       
   228 
       
   229             for line in f.readlines():
       
   230                 line = line.strip()
       
   231                 if line and not line.startswith('#'):
       
   232                     blacklist[line] = filename
       
   233 
       
   234             f.close()
       
   235 
       
   236         options.blacklist = blacklist
       
   237 
       
   238     return (options, args)
       
   239 
       
   240 def rename(src, dst):
       
   241     """Like os.rename(), trade atomicity and opened files friendliness
       
   242     for existing destination support.
       
   243     """
       
   244     shutil.copy(src, dst)
       
   245     os.remove(src)
       
   246 
       
   247 def splitnewlines(text):
       
   248     '''like str.splitlines, but only split on newlines.
       
   249     keep line endings.'''
       
   250     i = 0
       
   251     lines = []
       
   252     while True:
       
   253         n = text.find('\n', i)
       
   254         if n == -1:
       
   255             last = text[i:]
       
   256             if last:
       
   257                 lines.append(last)
       
   258             return lines
       
   259         lines.append(text[i:n + 1])
       
   260         i = n + 1
       
   261 
       
   262 def parsehghaveoutput(lines):
       
   263     '''Parse hghave log lines.
       
   264     Return tuple of lists (missing, failed):
       
   265       * the missing/unknown features
       
   266       * the features for which existence check failed'''
       
   267     missing = []
       
   268     failed = []
       
   269     for line in lines:
       
   270         if line.startswith(SKIPPED_PREFIX):
       
   271             line = line.splitlines()[0]
       
   272             missing.append(line[len(SKIPPED_PREFIX):])
       
   273         elif line.startswith(FAILED_PREFIX):
       
   274             line = line.splitlines()[0]
       
   275             failed.append(line[len(FAILED_PREFIX):])
       
   276 
       
   277     return missing, failed
       
   278 
       
   279 def showdiff(expected, output, ref, err):
       
   280     try:
       
   281         for line in difflib.unified_diff(expected, output, ref, err):
       
   282             sys.stdout.write(line)
       
   283     except IOError, ex:
       
   284         print >>sys.stderr, 'BORKEN PIPE', ex.errno
       
   285         pass
       
   286 
       
   287 def findprogram(program):
       
   288     """Search PATH for a executable program"""
       
   289     for p in os.environ.get('PATH', os.defpath).split(os.pathsep):
       
   290         name = os.path.join(p, program)
       
   291         if os.access(name, os.X_OK):
       
   292             return name
       
   293     return None
       
   294 
       
   295 def checktools():
       
   296     # Before we go any further, check for pre-requisite tools
       
   297     # stuff from coreutils (cat, rm, etc) are not tested
       
   298     for p in requiredtools:
       
   299         if os.name == 'nt':
       
   300             p += '.exe'
       
   301         found = findprogram(p)
       
   302         if found:
       
   303             vlog("# Found prerequisite", p, "at", found)
       
   304         else:
       
   305             print "WARNING: Did not find prerequisite tool: "+p
       
   306 
       
   307 def killdaemons():
       
   308     # Kill off any leftover daemon processes
       
   309     try:
       
   310         fp = open(DAEMON_PIDS)
       
   311         for line in fp:
       
   312             try:
       
   313                 pid = int(line)
       
   314             except ValueError:
       
   315                 continue
       
   316             try:
       
   317                 os.kill(pid, 0)
       
   318                 vlog('# Killing daemon process %d' % pid)
       
   319                 os.kill(pid, signal.SIGTERM)
       
   320                 time.sleep(0.25)
       
   321                 os.kill(pid, 0)
       
   322                 vlog('# Daemon process %d is stuck - really killing it' % pid)
       
   323                 os.kill(pid, signal.SIGKILL)
       
   324             except OSError, err:
       
   325                 if err.errno != errno.ESRCH:
       
   326                     raise
       
   327         fp.close()
       
   328         os.unlink(DAEMON_PIDS)
       
   329     except IOError:
       
   330         pass
       
   331 
       
   332 def cleanup(options):
       
   333     if not options.keep_tmpdir:
       
   334         vlog("# Cleaning up HGTMP", HGTMP)
       
   335         shutil.rmtree(HGTMP, True)
       
   336 
       
   337 def usecorrectpython():
       
   338     # some tests run python interpreter. they must use same
       
   339     # interpreter we use or bad things will happen.
       
   340     exedir, exename = os.path.split(sys.executable)
       
   341     if exename == 'python':
       
   342         path = findprogram('python')
       
   343         if os.path.dirname(path) == exedir:
       
   344             return
       
   345     vlog('# Making python executable in test path use correct Python')
       
   346     mypython = os.path.join(BINDIR, 'python')
       
   347     try:
       
   348         os.symlink(sys.executable, mypython)
       
   349     except AttributeError:
       
   350         # windows fallback
       
   351         shutil.copyfile(sys.executable, mypython)
       
   352         shutil.copymode(sys.executable, mypython)
       
   353 
       
   354 def installhg(options):
       
   355     vlog("# Performing temporary installation of HG")
       
   356     installerrs = os.path.join("tests", "install.err")
       
   357     pure = options.pure and "--pure" or ""
       
   358 
       
   359     # Run installer in hg root
       
   360     script = os.path.realpath(sys.argv[0])
       
   361     hgroot = os.path.dirname(os.path.dirname(script))
       
   362     os.chdir(hgroot)
       
   363     nohome = '--home=""'
       
   364     if os.name == 'nt':
       
   365         # The --home="" trick works only on OS where os.sep == '/'
       
   366         # because of a distutils convert_path() fast-path. Avoid it at
       
   367         # least on Windows for now, deal with .pydistutils.cfg bugs
       
   368         # when they happen.
       
   369         nohome = ''
       
   370     cmd = ('%s setup.py %s clean --all'
       
   371            ' build --build-base="%s"'
       
   372            ' install --force --prefix="%s" --install-lib="%s"'
       
   373            ' --install-scripts="%s" %s >%s 2>&1'
       
   374            % (sys.executable, pure, os.path.join(HGTMP, "build"),
       
   375               INST, PYTHONDIR, BINDIR, nohome, installerrs))
       
   376     vlog("# Running", cmd)
       
   377     if os.system(cmd) == 0:
       
   378         if not options.verbose:
       
   379             os.remove(installerrs)
       
   380     else:
       
   381         f = open(installerrs)
       
   382         for line in f:
       
   383             print line,
       
   384         f.close()
       
   385         sys.exit(1)
       
   386     os.chdir(TESTDIR)
       
   387 
       
   388     usecorrectpython()
       
   389 
       
   390     vlog("# Installing dummy diffstat")
       
   391     f = open(os.path.join(BINDIR, 'diffstat'), 'w')
       
   392     f.write('#!' + sys.executable + '\n'
       
   393             'import sys\n'
       
   394             'files = 0\n'
       
   395             'for line in sys.stdin:\n'
       
   396             '    if line.startswith("diff "):\n'
       
   397             '        files += 1\n'
       
   398             'sys.stdout.write("files patched: %d\\n" % files)\n')
       
   399     f.close()
       
   400     os.chmod(os.path.join(BINDIR, 'diffstat'), 0700)
       
   401 
       
   402     if options.py3k_warnings and not options.anycoverage:
       
   403         vlog("# Updating hg command to enable Py3k Warnings switch")
       
   404         f = open(os.path.join(BINDIR, 'hg'), 'r')
       
   405         lines = [line.rstrip() for line in f]
       
   406         lines[0] += ' -3'
       
   407         f.close()
       
   408         f = open(os.path.join(BINDIR, 'hg'), 'w')
       
   409         for line in lines:
       
   410             f.write(line + '\n')
       
   411         f.close()
       
   412 
       
   413     if options.anycoverage:
       
   414         custom = os.path.join(TESTDIR, 'sitecustomize.py')
       
   415         target = os.path.join(PYTHONDIR, 'sitecustomize.py')
       
   416         vlog('# Installing coverage trigger to %s' % target)
       
   417         shutil.copyfile(custom, target)
       
   418         rc = os.path.join(TESTDIR, '.coveragerc')
       
   419         vlog('# Installing coverage rc to %s' % rc)
       
   420         os.environ['COVERAGE_PROCESS_START'] = rc
       
   421         fn = os.path.join(INST, '..', '.coverage')
       
   422         os.environ['COVERAGE_FILE'] = fn
       
   423 
       
   424 def outputcoverage(options):
       
   425 
       
   426     vlog('# Producing coverage report')
       
   427     os.chdir(PYTHONDIR)
       
   428 
       
   429     def covrun(*args):
       
   430         cmd = 'coverage %s' % ' '.join(args)
       
   431         vlog('# Running: %s' % cmd)
       
   432         os.system(cmd)
       
   433 
       
   434     if options.child:
       
   435         return
       
   436 
       
   437     covrun('-c')
       
   438     omit = ','.join([BINDIR, TESTDIR])
       
   439     covrun('-i', '-r', '"--omit=%s"' % omit) # report
       
   440     if options.annotate:
       
   441         adir = os.path.join(TESTDIR, 'annotated')
       
   442         if not os.path.isdir(adir):
       
   443             os.mkdir(adir)
       
   444         covrun('-i', '-a', '"--directory=%s"' % adir, '"--omit=%s"' % omit)
       
   445 
       
   446 class Timeout(Exception):
       
   447     pass
       
   448 
       
   449 def alarmed(signum, frame):
       
   450     raise Timeout
       
   451 
       
   452 def pytest(test, options, replacements):
       
   453     py3kswitch = options.py3k_warnings and ' -3' or ''
       
   454     cmd = '%s%s "%s"' % (PYTHON, py3kswitch, test)
       
   455     vlog("# Running", cmd)
       
   456     return run(cmd, options, replacements)
       
   457 
       
   458 def shtest(test, options, replacements):
       
   459     cmd = '"%s"' % test
       
   460     vlog("# Running", cmd)
       
   461     return run(cmd, options, replacements)
       
   462 
       
   463 needescape = re.compile(r'[\x00-\x08\x0b-\x1f\x7f-\xff]').search
       
   464 escapesub = re.compile(r'[\x00-\x08\x0b-\x1f\\\x7f-\xff]').sub
       
   465 escapemap = dict((chr(i), r'\x%02x' % i) for i in range(256))
       
   466 escapemap.update({'\\': '\\\\', '\r': r'\r'})
       
   467 def escapef(m):
       
   468     return escapemap[m.group(0)]
       
   469 def stringescape(s):
       
   470     return escapesub(escapef, s)
       
   471 
       
   472 def tsttest(test, options, replacements):
       
   473     t = open(test)
       
   474     out = []
       
   475     script = []
       
   476     salt = "SALT" + str(time.time())
       
   477 
       
   478     pos = prepos = -1
       
   479     after = {}
       
   480     expected = {}
       
   481     for n, l in enumerate(t):
       
   482         if not l.endswith('\n'):
       
   483             l += '\n'
       
   484         if l.startswith('  $ '): # commands
       
   485             after.setdefault(pos, []).append(l)
       
   486             prepos = pos
       
   487             pos = n
       
   488             script.append('echo %s %s $?\n' % (salt, n))
       
   489             script.append(l[4:])
       
   490         elif l.startswith('  > '): # continuations
       
   491             after.setdefault(prepos, []).append(l)
       
   492             script.append(l[4:])
       
   493         elif l.startswith('  '): # results
       
   494             # queue up a list of expected results
       
   495             expected.setdefault(pos, []).append(l[2:])
       
   496         else:
       
   497             # non-command/result - queue up for merged output
       
   498             after.setdefault(pos, []).append(l)
       
   499 
       
   500     t.close()
       
   501 
       
   502     script.append('echo %s %s $?\n' % (salt, n + 1))
       
   503 
       
   504     fd, name = tempfile.mkstemp(suffix='hg-tst')
       
   505 
       
   506     try:
       
   507         for l in script:
       
   508             os.write(fd, l)
       
   509         os.close(fd)
       
   510 
       
   511         cmd = '/bin/sh "%s"' % name
       
   512         vlog("# Running", cmd)
       
   513         exitcode, output = run(cmd, options, replacements)
       
   514         # do not merge output if skipped, return hghave message instead
       
   515         # similarly, with --debug, output is None
       
   516         if exitcode == SKIPPED_STATUS or output is None:
       
   517             return exitcode, output
       
   518     finally:
       
   519         os.remove(name)
       
   520 
       
   521     def rematch(el, l):
       
   522         try:
       
   523             # ensure that the regex matches to the end of the string
       
   524             return re.match(el + r'\Z', l)
       
   525         except re.error:
       
   526             # el is an invalid regex
       
   527             return False
       
   528 
       
   529     def globmatch(el, l):
       
   530         # The only supported special characters are * and ?. Escaping is
       
   531         # supported.
       
   532         i, n = 0, len(el)
       
   533         res = ''
       
   534         while i < n:
       
   535             c = el[i]
       
   536             i += 1
       
   537             if c == '\\' and el[i] in '*?\\':
       
   538                 res += el[i - 1:i + 1]
       
   539                 i += 1
       
   540             elif c == '*':
       
   541                 res += '.*'
       
   542             elif c == '?':
       
   543                 res += '.'
       
   544             else:
       
   545                 res += re.escape(c)
       
   546         return rematch(res, l)
       
   547 
       
   548     pos = -1
       
   549     postout = []
       
   550     ret = 0
       
   551     for n, l in enumerate(output):
       
   552         lout, lcmd = l, None
       
   553         if salt in l:
       
   554             lout, lcmd = l.split(salt, 1)
       
   555 
       
   556         if lout:
       
   557             if lcmd:
       
   558                 lout += ' (no-eol)\n'
       
   559 
       
   560             el = None
       
   561             if pos in expected and expected[pos]:
       
   562                 el = expected[pos].pop(0)
       
   563 
       
   564             if el == lout: # perfect match (fast)
       
   565                 postout.append("  " + lout)
       
   566             elif (el and
       
   567                   (el.endswith(" (re)\n") and rematch(el[:-6] + '\n', lout) or
       
   568                    el.endswith(" (glob)\n") and globmatch(el[:-8] + '\n', lout)
       
   569                    or el.endswith(" (esc)\n") and
       
   570                       el.decode('string-escape') == l)):
       
   571                 postout.append("  " + el) # fallback regex/glob/esc match
       
   572             else:
       
   573                 if needescape(lout):
       
   574                     lout = stringescape(lout.rstrip('\n')) + " (esc)\n"
       
   575                 postout.append("  " + lout) # let diff deal with it
       
   576 
       
   577         if lcmd:
       
   578             # add on last return code
       
   579             ret = int(lcmd.split()[1])
       
   580             if ret != 0:
       
   581                 postout.append("  [%s]\n" % ret)
       
   582             if pos in after:
       
   583                 postout += after.pop(pos)
       
   584             pos = int(lcmd.split()[0])
       
   585 
       
   586     if pos in after:
       
   587         postout += after.pop(pos)
       
   588 
       
   589     return exitcode, postout
       
   590 
       
   591 wifexited = getattr(os, "WIFEXITED", lambda x: False)
       
   592 def run(cmd, options, replacements):
       
   593     """Run command in a sub-process, capturing the output (stdout and stderr).
       
   594     Return a tuple (exitcode, output).  output is None in debug mode."""
       
   595     # TODO: Use subprocess.Popen if we're running on Python 2.4
       
   596     if options.debug:
       
   597         proc = subprocess.Popen(cmd, shell=True)
       
   598         ret = proc.wait()
       
   599         return (ret, None)
       
   600 
       
   601     if os.name == 'nt' or sys.platform.startswith('java'):
       
   602         tochild, fromchild = os.popen4(cmd)
       
   603         tochild.close()
       
   604         output = fromchild.read()
       
   605         ret = fromchild.close()
       
   606         if ret is None:
       
   607             ret = 0
       
   608     else:
       
   609         proc = Popen4(cmd)
       
   610         def cleanup():
       
   611             os.kill(proc.pid, signal.SIGTERM)
       
   612             ret = proc.wait()
       
   613             if ret == 0:
       
   614                 ret = signal.SIGTERM << 8
       
   615             killdaemons()
       
   616             return ret
       
   617 
       
   618         try:
       
   619             output = ''
       
   620             proc.tochild.close()
       
   621             output = proc.fromchild.read()
       
   622             ret = proc.wait()
       
   623             if wifexited(ret):
       
   624                 ret = os.WEXITSTATUS(ret)
       
   625         except Timeout:
       
   626             vlog('# Process %d timed out - killing it' % proc.pid)
       
   627             ret = cleanup()
       
   628             output += ("\n### Abort: timeout after %d seconds.\n"
       
   629                        % options.timeout)
       
   630         except KeyboardInterrupt:
       
   631             vlog('# Handling keyboard interrupt')
       
   632             cleanup()
       
   633             raise
       
   634 
       
   635     for s, r in replacements:
       
   636         output = re.sub(s, r, output)
       
   637     return ret, splitnewlines(output)
       
   638 
       
   639 def runone(options, test, skips, fails):
       
   640     '''tristate output:
       
   641     None -> skipped
       
   642     True -> passed
       
   643     False -> failed'''
       
   644 
       
   645     def skip(msg):
       
   646         if not options.verbose:
       
   647             skips.append((test, msg))
       
   648         else:
       
   649             print "\nSkipping %s: %s" % (testpath, msg)
       
   650         return None
       
   651 
       
   652     def fail(msg):
       
   653         fails.append((test, msg))
       
   654         if not options.nodiff:
       
   655             print "\nERROR: %s %s" % (testpath, msg)
       
   656         return None
       
   657 
       
   658     vlog("# Test", test)
       
   659 
       
   660     # create a fresh hgrc
       
   661     hgrc = open(HGRCPATH, 'w+')
       
   662     hgrc.write('[ui]\n')
       
   663     hgrc.write('slash = True\n')
       
   664     hgrc.write('[defaults]\n')
       
   665     hgrc.write('backout = -d "0 0"\n')
       
   666     hgrc.write('commit = -d "0 0"\n')
       
   667     hgrc.write('tag = -d "0 0"\n')
       
   668     if options.inotify:
       
   669         hgrc.write('[extensions]\n')
       
   670         hgrc.write('inotify=\n')
       
   671         hgrc.write('[inotify]\n')
       
   672         hgrc.write('pidfile=%s\n' % DAEMON_PIDS)
       
   673         hgrc.write('appendpid=True\n')
       
   674     hgrc.close()
       
   675 
       
   676     testpath = os.path.join(TESTDIR, test)
       
   677     ref = os.path.join(TESTDIR, test+".out")
       
   678     err = os.path.join(TESTDIR, test+".err")
       
   679     if os.path.exists(err):
       
   680         os.remove(err)       # Remove any previous output files
       
   681     try:
       
   682         tf = open(testpath)
       
   683         firstline = tf.readline().rstrip()
       
   684         tf.close()
       
   685     except:
       
   686         firstline = ''
       
   687     lctest = test.lower()
       
   688 
       
   689     if lctest.endswith('.py') or firstline == '#!/usr/bin/env python':
       
   690         runner = pytest
       
   691     elif lctest.endswith('.t'):
       
   692         runner = tsttest
       
   693         ref = testpath
       
   694     else:
       
   695         # do not try to run non-executable programs
       
   696         if not os.access(testpath, os.X_OK):
       
   697             return skip("not executable")
       
   698         runner = shtest
       
   699 
       
   700     # Make a tmp subdirectory to work in
       
   701     testtmp = os.environ["TESTTMP"] = os.path.join(HGTMP, test)
       
   702     os.mkdir(testtmp)
       
   703     os.chdir(testtmp)
       
   704 
       
   705     if options.timeout > 0:
       
   706         signal.alarm(options.timeout)
       
   707 
       
   708     ret, out = runner(testpath, options, [
       
   709         (re.escape(testtmp), '$TESTTMP'),
       
   710         (r':%s\b' % options.port, ':$HGPORT'),
       
   711         (r':%s\b' % (options.port + 1), ':$HGPORT1'),
       
   712         (r':%s\b' % (options.port + 2), ':$HGPORT2'),
       
   713         ])
       
   714     vlog("# Ret was:", ret)
       
   715 
       
   716     if options.timeout > 0:
       
   717         signal.alarm(0)
       
   718 
       
   719     mark = '.'
       
   720 
       
   721     skipped = (ret == SKIPPED_STATUS)
       
   722 
       
   723     # If we're not in --debug mode and reference output file exists,
       
   724     # check test output against it.
       
   725     if options.debug:
       
   726         refout = None                   # to match "out is None"
       
   727     elif os.path.exists(ref):
       
   728         f = open(ref, "r")
       
   729         refout = splitnewlines(f.read())
       
   730         f.close()
       
   731     else:
       
   732         refout = []
       
   733 
       
   734     if (ret != 0 or out != refout) and not skipped and not options.debug:
       
   735         # Save errors to a file for diagnosis
       
   736         f = open(err, "wb")
       
   737         for line in out:
       
   738             f.write(line)
       
   739         f.close()
       
   740 
       
   741     if skipped:
       
   742         mark = 's'
       
   743         if out is None:                 # debug mode: nothing to parse
       
   744             missing = ['unknown']
       
   745             failed = None
       
   746         else:
       
   747             missing, failed = parsehghaveoutput(out)
       
   748         if not missing:
       
   749             missing = ['irrelevant']
       
   750         if failed:
       
   751             fail("hghave failed checking for %s" % failed[-1])
       
   752             skipped = False
       
   753         else:
       
   754             skip(missing[-1])
       
   755     elif out != refout:
       
   756         mark = '!'
       
   757         if ret:
       
   758             fail("output changed and returned error code %d" % ret)
       
   759         else:
       
   760             fail("output changed")
       
   761         if not options.nodiff:
       
   762             if options.view:
       
   763                 os.system("%s %s %s" % (options.view, ref, err))
       
   764             else:
       
   765                 showdiff(refout, out, ref, err)
       
   766         ret = 1
       
   767     elif ret:
       
   768         mark = '!'
       
   769         fail("returned error code %d" % ret)
       
   770 
       
   771     if not options.verbose:
       
   772         try:
       
   773             sys.stdout.write(mark)
       
   774             sys.stdout.flush()
       
   775         except IOError, ex:
       
   776             print >>sys.stderr, 'BORKEN PIPE', ex.errno
       
   777             pass
       
   778 
       
   779     killdaemons()
       
   780 
       
   781     os.chdir(TESTDIR)
       
   782     if not options.keep_tmpdir:
       
   783         shutil.rmtree(testtmp, True)
       
   784     if skipped:
       
   785         return None
       
   786     return ret == 0
       
   787 
       
   788 _hgpath = None
       
   789 
       
   790 def _gethgpath():
       
   791     """Return the path to the mercurial package that is actually found by
       
   792     the current Python interpreter."""
       
   793     global _hgpath
       
   794     if _hgpath is not None:
       
   795         return _hgpath
       
   796 
       
   797     cmd = '%s -c "import mercurial; print mercurial.__path__[0]"'
       
   798     pipe = os.popen(cmd % PYTHON)
       
   799     try:
       
   800         _hgpath = pipe.read().strip()
       
   801     finally:
       
   802         pipe.close()
       
   803     return _hgpath
       
   804 
       
   805 def _checkhglib(verb):
       
   806     """Ensure that the 'mercurial' package imported by python is
       
   807     the one we expect it to be.  If not, print a warning to stderr."""
       
   808     expecthg = os.path.join(PYTHONDIR, 'mercurial')
       
   809     actualhg = _gethgpath()
       
   810     if actualhg != expecthg:
       
   811         sys.stderr.write('warning: %s with unexpected mercurial lib: %s\n'
       
   812                          '         (expected %s)\n'
       
   813                          % (verb, actualhg, expecthg))
       
   814 
       
   815 def runchildren(options, tests):
       
   816     if INST:
       
   817         installhg(options)
       
   818         _checkhglib("Testing")
       
   819 
       
   820     optcopy = dict(options.__dict__)
       
   821     optcopy['jobs'] = 1
       
   822     del optcopy['blacklist']
       
   823     if optcopy['with_hg'] is None:
       
   824         optcopy['with_hg'] = os.path.join(BINDIR, "hg")
       
   825     optcopy.pop('anycoverage', None)
       
   826 
       
   827     opts = []
       
   828     for opt, value in optcopy.iteritems():
       
   829         name = '--' + opt.replace('_', '-')
       
   830         if value is True:
       
   831             opts.append(name)
       
   832         elif value is not None:
       
   833             opts.append(name + '=' + str(value))
       
   834 
       
   835     tests.reverse()
       
   836     jobs = [[] for j in xrange(options.jobs)]
       
   837     while tests:
       
   838         for job in jobs:
       
   839             if not tests:
       
   840                 break
       
   841             job.append(tests.pop())
       
   842     fps = {}
       
   843 
       
   844     for j, job in enumerate(jobs):
       
   845         if not job:
       
   846             continue
       
   847         rfd, wfd = os.pipe()
       
   848         childopts = ['--child=%d' % wfd, '--port=%d' % (options.port + j * 3)]
       
   849         childtmp = os.path.join(HGTMP, 'child%d' % j)
       
   850         childopts += ['--tmpdir', childtmp]
       
   851         cmdline = [PYTHON, sys.argv[0]] + opts + childopts + job
       
   852         vlog(' '.join(cmdline))
       
   853         fps[os.spawnvp(os.P_NOWAIT, cmdline[0], cmdline)] = os.fdopen(rfd, 'r')
       
   854         os.close(wfd)
       
   855     signal.signal(signal.SIGINT, signal.SIG_IGN)
       
   856     failures = 0
       
   857     tested, skipped, failed = 0, 0, 0
       
   858     skips = []
       
   859     fails = []
       
   860     while fps:
       
   861         pid, status = os.wait()
       
   862         fp = fps.pop(pid)
       
   863         l = fp.read().splitlines()
       
   864         try:
       
   865             test, skip, fail = map(int, l[:3])
       
   866         except ValueError:
       
   867             test, skip, fail = 0, 0, 0
       
   868         split = -fail or len(l)
       
   869         for s in l[3:split]:
       
   870             skips.append(s.split(" ", 1))
       
   871         for s in l[split:]:
       
   872             fails.append(s.split(" ", 1))
       
   873         tested += test
       
   874         skipped += skip
       
   875         failed += fail
       
   876         vlog('pid %d exited, status %d' % (pid, status))
       
   877         failures |= status
       
   878     print
       
   879     if not options.noskips:
       
   880         for s in skips:
       
   881             print "Skipped %s: %s" % (s[0], s[1])
       
   882     for s in fails:
       
   883         print "Failed %s: %s" % (s[0], s[1])
       
   884 
       
   885     _checkhglib("Tested")
       
   886     print "# Ran %d tests, %d skipped, %d failed." % (
       
   887         tested, skipped, failed)
       
   888 
       
   889     if options.anycoverage:
       
   890         outputcoverage(options)
       
   891     sys.exit(failures != 0)
       
   892 
       
   893 def runtests(options, tests):
       
   894     global DAEMON_PIDS, HGRCPATH
       
   895     DAEMON_PIDS = os.environ["DAEMON_PIDS"] = os.path.join(HGTMP, 'daemon.pids')
       
   896     HGRCPATH = os.environ["HGRCPATH"] = os.path.join(HGTMP, '.hgrc')
       
   897 
       
   898     try:
       
   899         if INST:
       
   900             installhg(options)
       
   901             _checkhglib("Testing")
       
   902 
       
   903         if options.timeout > 0:
       
   904             try:
       
   905                 signal.signal(signal.SIGALRM, alarmed)
       
   906                 vlog('# Running each test with %d second timeout' %
       
   907                      options.timeout)
       
   908             except AttributeError:
       
   909                 print 'WARNING: cannot run tests with timeouts'
       
   910                 options.timeout = 0
       
   911 
       
   912         tested = 0
       
   913         failed = 0
       
   914         skipped = 0
       
   915 
       
   916         if options.restart:
       
   917             orig = list(tests)
       
   918             while tests:
       
   919                 if os.path.exists(tests[0] + ".err"):
       
   920                     break
       
   921                 tests.pop(0)
       
   922             if not tests:
       
   923                 print "running all tests"
       
   924                 tests = orig
       
   925 
       
   926         skips = []
       
   927         fails = []
       
   928 
       
   929         for test in tests:
       
   930             if options.blacklist:
       
   931                 filename = options.blacklist.get(test)
       
   932                 if filename is not None:
       
   933                     skips.append((test, "blacklisted (%s)" % filename))
       
   934                     skipped += 1
       
   935                     continue
       
   936 
       
   937             if options.retest and not os.path.exists(test + ".err"):
       
   938                 skipped += 1
       
   939                 continue
       
   940 
       
   941             if options.keywords:
       
   942                 fp = open(test)
       
   943                 t = fp.read().lower() + test.lower()
       
   944                 fp.close()
       
   945                 for k in options.keywords.lower().split():
       
   946                     if k in t:
       
   947                         break
       
   948                 else:
       
   949                     skipped += 1
       
   950                     continue
       
   951 
       
   952             ret = runone(options, test, skips, fails)
       
   953             if ret is None:
       
   954                 skipped += 1
       
   955             elif not ret:
       
   956                 if options.interactive:
       
   957                     print "Accept this change? [n] ",
       
   958                     answer = sys.stdin.readline().strip()
       
   959                     if answer.lower() in "y yes".split():
       
   960                         if test.endswith(".t"):
       
   961                             rename(test + ".err", test)
       
   962                         else:
       
   963                             rename(test + ".err", test + ".out")
       
   964                         tested += 1
       
   965                         fails.pop()
       
   966                         continue
       
   967                 failed += 1
       
   968                 if options.first:
       
   969                     break
       
   970             tested += 1
       
   971 
       
   972         if options.child:
       
   973             fp = os.fdopen(options.child, 'w')
       
   974             fp.write('%d\n%d\n%d\n' % (tested, skipped, failed))
       
   975             for s in skips:
       
   976                 fp.write("%s %s\n" % s)
       
   977             for s in fails:
       
   978                 fp.write("%s %s\n" % s)
       
   979             fp.close()
       
   980         else:
       
   981             print
       
   982             for s in skips:
       
   983                 print "Skipped %s: %s" % s
       
   984             for s in fails:
       
   985                 print "Failed %s: %s" % s
       
   986             _checkhglib("Tested")
       
   987             print "# Ran %d tests, %d skipped, %d failed." % (
       
   988                 tested, skipped, failed)
       
   989 
       
   990         if options.anycoverage:
       
   991             outputcoverage(options)
       
   992     except KeyboardInterrupt:
       
   993         failed = True
       
   994         print "\ninterrupted!"
       
   995 
       
   996     if failed:
       
   997         sys.exit(1)
       
   998 
       
   999 def main():
       
  1000     (options, args) = parseargs()
       
  1001     if not options.child:
       
  1002         os.umask(022)
       
  1003 
       
  1004         checktools()
       
  1005 
       
  1006     if len(args) == 0:
       
  1007         args = os.listdir(".")
       
  1008     args.sort()
       
  1009 
       
  1010     tests = []
       
  1011     skipped = []
       
  1012     for test in args:
       
  1013         if (test.startswith("test-") and '~' not in test and
       
  1014             ('.' not in test or test.endswith('.py') or
       
  1015              test.endswith('.bat') or test.endswith('.t'))):
       
  1016             if not os.path.exists(test):
       
  1017                 skipped.append(test)
       
  1018             else:
       
  1019                 tests.append(test)
       
  1020     if not tests:
       
  1021         for test in skipped:
       
  1022             print 'Skipped %s: does not exist' % test
       
  1023         print "# Ran 0 tests, %d skipped, 0 failed." % len(skipped)
       
  1024         return
       
  1025     tests = tests + skipped
       
  1026 
       
  1027     # Reset some environment variables to well-known values so that
       
  1028     # the tests produce repeatable output.
       
  1029     os.environ['LANG'] = os.environ['LC_ALL'] = os.environ['LANGUAGE'] = 'C'
       
  1030     os.environ['TZ'] = 'GMT'
       
  1031     os.environ["EMAIL"] = "Foo Bar <foo.bar@example.com>"
       
  1032     os.environ['CDPATH'] = ''
       
  1033     os.environ['COLUMNS'] = '80'
       
  1034     os.environ['GREP_OPTIONS'] = ''
       
  1035     os.environ['http_proxy'] = ''
       
  1036 
       
  1037     # unset env related to hooks
       
  1038     for k in os.environ.keys():
       
  1039         if k.startswith('HG_'):
       
  1040             # can't remove on solaris
       
  1041             os.environ[k] = ''
       
  1042             del os.environ[k]
       
  1043 
       
  1044     global TESTDIR, HGTMP, INST, BINDIR, PYTHONDIR, COVERAGE_FILE
       
  1045     TESTDIR = os.environ["TESTDIR"] = os.getcwd()
       
  1046     if options.tmpdir:
       
  1047         options.keep_tmpdir = True
       
  1048         tmpdir = options.tmpdir
       
  1049         if os.path.exists(tmpdir):
       
  1050             # Meaning of tmpdir has changed since 1.3: we used to create
       
  1051             # HGTMP inside tmpdir; now HGTMP is tmpdir.  So fail if
       
  1052             # tmpdir already exists.
       
  1053             sys.exit("error: temp dir %r already exists" % tmpdir)
       
  1054 
       
  1055             # Automatically removing tmpdir sounds convenient, but could
       
  1056             # really annoy anyone in the habit of using "--tmpdir=/tmp"
       
  1057             # or "--tmpdir=$HOME".
       
  1058             #vlog("# Removing temp dir", tmpdir)
       
  1059             #shutil.rmtree(tmpdir)
       
  1060         os.makedirs(tmpdir)
       
  1061     else:
       
  1062         tmpdir = tempfile.mkdtemp('', 'hgtests.')
       
  1063     HGTMP = os.environ['HGTMP'] = os.path.realpath(tmpdir)
       
  1064     DAEMON_PIDS = None
       
  1065     HGRCPATH = None
       
  1066 
       
  1067     os.environ["HGEDITOR"] = sys.executable + ' -c "import sys; sys.exit(0)"'
       
  1068     os.environ["HGMERGE"] = "internal:merge"
       
  1069     os.environ["HGUSER"]   = "test"
       
  1070     os.environ["HGENCODING"] = "ascii"
       
  1071     os.environ["HGENCODINGMODE"] = "strict"
       
  1072     os.environ["HGPORT"] = str(options.port)
       
  1073     os.environ["HGPORT1"] = str(options.port + 1)
       
  1074     os.environ["HGPORT2"] = str(options.port + 2)
       
  1075 
       
  1076     if options.with_hg:
       
  1077         INST = None
       
  1078         BINDIR = os.path.dirname(os.path.realpath(options.with_hg))
       
  1079 
       
  1080         # This looks redundant with how Python initializes sys.path from
       
  1081         # the location of the script being executed.  Needed because the
       
  1082         # "hg" specified by --with-hg is not the only Python script
       
  1083         # executed in the test suite that needs to import 'mercurial'
       
  1084         # ... which means it's not really redundant at all.
       
  1085         PYTHONDIR = BINDIR
       
  1086     else:
       
  1087         INST = os.path.join(HGTMP, "install")
       
  1088         BINDIR = os.environ["BINDIR"] = os.path.join(INST, "bin")
       
  1089         PYTHONDIR = os.path.join(INST, "lib", "python")
       
  1090 
       
  1091     os.environ["BINDIR"] = BINDIR
       
  1092     os.environ["PYTHON"] = PYTHON
       
  1093 
       
  1094     if not options.child:
       
  1095         path = [BINDIR] + os.environ["PATH"].split(os.pathsep)
       
  1096         os.environ["PATH"] = os.pathsep.join(path)
       
  1097 
       
  1098         # Include TESTDIR in PYTHONPATH so that out-of-tree extensions
       
  1099         # can run .../tests/run-tests.py test-foo where test-foo
       
  1100         # adds an extension to HGRC
       
  1101         pypath = [PYTHONDIR, TESTDIR]
       
  1102         # We have to augment PYTHONPATH, rather than simply replacing
       
  1103         # it, in case external libraries are only available via current
       
  1104         # PYTHONPATH.  (In particular, the Subversion bindings on OS X
       
  1105         # are in /opt/subversion.)
       
  1106         oldpypath = os.environ.get(IMPL_PATH)
       
  1107         if oldpypath:
       
  1108             pypath.append(oldpypath)
       
  1109         os.environ[IMPL_PATH] = os.pathsep.join(pypath)
       
  1110 
       
  1111     COVERAGE_FILE = os.path.join(TESTDIR, ".coverage")
       
  1112 
       
  1113     vlog("# Using TESTDIR", TESTDIR)
       
  1114     vlog("# Using HGTMP", HGTMP)
       
  1115     vlog("# Using PATH", os.environ["PATH"])
       
  1116     vlog("# Using", IMPL_PATH, os.environ[IMPL_PATH])
       
  1117 
       
  1118     try:
       
  1119         if len(tests) > 1 and options.jobs > 1:
       
  1120             runchildren(options, tests)
       
  1121         else:
       
  1122             runtests(options, tests)
       
  1123     finally:
       
  1124         time.sleep(1)
       
  1125         cleanup(options)
       
  1126 
       
  1127 if __name__ == '__main__':
       
  1128     main()