tests/run-tests.py
changeset 0 bbeef801409c
child 21 aa0870d093b8
equal deleted inserted replaced
-1:000000000000 0:bbeef801409c
       
     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", "unzip", "gunzip", "bunzip2", "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     for line in difflib.unified_diff(expected, output, ref, err):
       
   281         sys.stdout.write(line)
       
   282 
       
   283 def findprogram(program):
       
   284     """Search PATH for a executable program"""
       
   285     for p in os.environ.get('PATH', os.defpath).split(os.pathsep):
       
   286         name = os.path.join(p, program)
       
   287         if os.access(name, os.X_OK):
       
   288             return name
       
   289     return None
       
   290 
       
   291 def checktools():
       
   292     # Before we go any further, check for pre-requisite tools
       
   293     # stuff from coreutils (cat, rm, etc) are not tested
       
   294     for p in requiredtools:
       
   295         if os.name == 'nt':
       
   296             p += '.exe'
       
   297         found = findprogram(p)
       
   298         if found:
       
   299             vlog("# Found prerequisite", p, "at", found)
       
   300         else:
       
   301             print "WARNING: Did not find prerequisite tool: "+p
       
   302 
       
   303 def killdaemons():
       
   304     # Kill off any leftover daemon processes
       
   305     try:
       
   306         fp = open(DAEMON_PIDS)
       
   307         for line in fp:
       
   308             try:
       
   309                 pid = int(line)
       
   310             except ValueError:
       
   311                 continue
       
   312             try:
       
   313                 os.kill(pid, 0)
       
   314                 vlog('# Killing daemon process %d' % pid)
       
   315                 os.kill(pid, signal.SIGTERM)
       
   316                 time.sleep(0.25)
       
   317                 os.kill(pid, 0)
       
   318                 vlog('# Daemon process %d is stuck - really killing it' % pid)
       
   319                 os.kill(pid, signal.SIGKILL)
       
   320             except OSError, err:
       
   321                 if err.errno != errno.ESRCH:
       
   322                     raise
       
   323         fp.close()
       
   324         os.unlink(DAEMON_PIDS)
       
   325     except IOError:
       
   326         pass
       
   327 
       
   328 def cleanup(options):
       
   329     if not options.keep_tmpdir:
       
   330         vlog("# Cleaning up HGTMP", HGTMP)
       
   331         shutil.rmtree(HGTMP, True)
       
   332 
       
   333 def usecorrectpython():
       
   334     # some tests run python interpreter. they must use same
       
   335     # interpreter we use or bad things will happen.
       
   336     exedir, exename = os.path.split(sys.executable)
       
   337     if exename == 'python':
       
   338         path = findprogram('python')
       
   339         if os.path.dirname(path) == exedir:
       
   340             return
       
   341     vlog('# Making python executable in test path use correct Python')
       
   342     mypython = os.path.join(BINDIR, 'python')
       
   343     try:
       
   344         os.symlink(sys.executable, mypython)
       
   345     except AttributeError:
       
   346         # windows fallback
       
   347         shutil.copyfile(sys.executable, mypython)
       
   348         shutil.copymode(sys.executable, mypython)
       
   349 
       
   350 def installhg(options):
       
   351     vlog("# Performing temporary installation of HG")
       
   352     installerrs = os.path.join("tests", "install.err")
       
   353     pure = options.pure and "--pure" or ""
       
   354 
       
   355     # Run installer in hg root
       
   356     script = os.path.realpath(sys.argv[0])
       
   357     hgroot = os.path.dirname(os.path.dirname(script))
       
   358     os.chdir(hgroot)
       
   359     nohome = '--home=""'
       
   360     if os.name == 'nt':
       
   361         # The --home="" trick works only on OS where os.sep == '/'
       
   362         # because of a distutils convert_path() fast-path. Avoid it at
       
   363         # least on Windows for now, deal with .pydistutils.cfg bugs
       
   364         # when they happen.
       
   365         nohome = ''
       
   366     cmd = ('%s setup.py %s clean --all'
       
   367            ' build --build-base="%s"'
       
   368            ' install --force --prefix="%s" --install-lib="%s"'
       
   369            ' --install-scripts="%s" %s >%s 2>&1'
       
   370            % (sys.executable, pure, os.path.join(HGTMP, "build"),
       
   371               INST, PYTHONDIR, BINDIR, nohome, installerrs))
       
   372     vlog("# Running", cmd)
       
   373     if os.system(cmd) == 0:
       
   374         if not options.verbose:
       
   375             os.remove(installerrs)
       
   376     else:
       
   377         f = open(installerrs)
       
   378         for line in f:
       
   379             print line,
       
   380         f.close()
       
   381         sys.exit(1)
       
   382     os.chdir(TESTDIR)
       
   383 
       
   384     usecorrectpython()
       
   385 
       
   386     vlog("# Installing dummy diffstat")
       
   387     f = open(os.path.join(BINDIR, 'diffstat'), 'w')
       
   388     f.write('#!' + sys.executable + '\n'
       
   389             'import sys\n'
       
   390             'files = 0\n'
       
   391             'for line in sys.stdin:\n'
       
   392             '    if line.startswith("diff "):\n'
       
   393             '        files += 1\n'
       
   394             'sys.stdout.write("files patched: %d\\n" % files)\n')
       
   395     f.close()
       
   396     os.chmod(os.path.join(BINDIR, 'diffstat'), 0700)
       
   397 
       
   398     if options.py3k_warnings and not options.anycoverage:
       
   399         vlog("# Updating hg command to enable Py3k Warnings switch")
       
   400         f = open(os.path.join(BINDIR, 'hg'), 'r')
       
   401         lines = [line.rstrip() for line in f]
       
   402         lines[0] += ' -3'
       
   403         f.close()
       
   404         f = open(os.path.join(BINDIR, 'hg'), 'w')
       
   405         for line in lines:
       
   406             f.write(line + '\n')
       
   407         f.close()
       
   408 
       
   409     if options.anycoverage:
       
   410         custom = os.path.join(TESTDIR, 'sitecustomize.py')
       
   411         target = os.path.join(PYTHONDIR, 'sitecustomize.py')
       
   412         vlog('# Installing coverage trigger to %s' % target)
       
   413         shutil.copyfile(custom, target)
       
   414         rc = os.path.join(TESTDIR, '.coveragerc')
       
   415         vlog('# Installing coverage rc to %s' % rc)
       
   416         os.environ['COVERAGE_PROCESS_START'] = rc
       
   417         fn = os.path.join(INST, '..', '.coverage')
       
   418         os.environ['COVERAGE_FILE'] = fn
       
   419 
       
   420 def outputcoverage(options):
       
   421 
       
   422     vlog('# Producing coverage report')
       
   423     os.chdir(PYTHONDIR)
       
   424 
       
   425     def covrun(*args):
       
   426         cmd = 'coverage %s' % ' '.join(args)
       
   427         vlog('# Running: %s' % cmd)
       
   428         os.system(cmd)
       
   429 
       
   430     if options.child:
       
   431         return
       
   432 
       
   433     covrun('-c')
       
   434     omit = ','.join([BINDIR, TESTDIR])
       
   435     covrun('-i', '-r', '"--omit=%s"' % omit) # report
       
   436     if options.annotate:
       
   437         adir = os.path.join(TESTDIR, 'annotated')
       
   438         if not os.path.isdir(adir):
       
   439             os.mkdir(adir)
       
   440         covrun('-i', '-a', '"--directory=%s"' % adir, '"--omit=%s"' % omit)
       
   441 
       
   442 class Timeout(Exception):
       
   443     pass
       
   444 
       
   445 def alarmed(signum, frame):
       
   446     raise Timeout
       
   447 
       
   448 def pytest(test, options, replacements):
       
   449     py3kswitch = options.py3k_warnings and ' -3' or ''
       
   450     cmd = '%s%s "%s"' % (PYTHON, py3kswitch, test)
       
   451     vlog("# Running", cmd)
       
   452     return run(cmd, options, replacements)
       
   453 
       
   454 def shtest(test, options, replacements):
       
   455     cmd = '"%s"' % test
       
   456     vlog("# Running", cmd)
       
   457     return run(cmd, options, replacements)
       
   458 
       
   459 needescape = re.compile(r'[\x00-\x08\x0b-\x1f\x7f-\xff]').search
       
   460 escapesub = re.compile(r'[\x00-\x08\x0b-\x1f\\\x7f-\xff]').sub
       
   461 escapemap = dict((chr(i), r'\x%02x' % i) for i in range(256))
       
   462 escapemap.update({'\\': '\\\\', '\r': r'\r'})
       
   463 def escapef(m):
       
   464     return escapemap[m.group(0)]
       
   465 def stringescape(s):
       
   466     return escapesub(escapef, s)
       
   467 
       
   468 def tsttest(test, options, replacements):
       
   469     t = open(test)
       
   470     out = []
       
   471     script = []
       
   472     salt = "SALT" + str(time.time())
       
   473 
       
   474     pos = prepos = -1
       
   475     after = {}
       
   476     expected = {}
       
   477     for n, l in enumerate(t):
       
   478         if not l.endswith('\n'):
       
   479             l += '\n'
       
   480         if l.startswith('  $ '): # commands
       
   481             after.setdefault(pos, []).append(l)
       
   482             prepos = pos
       
   483             pos = n
       
   484             script.append('echo %s %s $?\n' % (salt, n))
       
   485             script.append(l[4:])
       
   486         elif l.startswith('  > '): # continuations
       
   487             after.setdefault(prepos, []).append(l)
       
   488             script.append(l[4:])
       
   489         elif l.startswith('  '): # results
       
   490             # queue up a list of expected results
       
   491             expected.setdefault(pos, []).append(l[2:])
       
   492         else:
       
   493             # non-command/result - queue up for merged output
       
   494             after.setdefault(pos, []).append(l)
       
   495 
       
   496     t.close()
       
   497 
       
   498     script.append('echo %s %s $?\n' % (salt, n + 1))
       
   499 
       
   500     fd, name = tempfile.mkstemp(suffix='hg-tst')
       
   501 
       
   502     try:
       
   503         for l in script:
       
   504             os.write(fd, l)
       
   505         os.close(fd)
       
   506 
       
   507         cmd = '/bin/sh "%s"' % name
       
   508         vlog("# Running", cmd)
       
   509         exitcode, output = run(cmd, options, replacements)
       
   510         # do not merge output if skipped, return hghave message instead
       
   511         # similarly, with --debug, output is None
       
   512         if exitcode == SKIPPED_STATUS or output is None:
       
   513             return exitcode, output
       
   514     finally:
       
   515         os.remove(name)
       
   516 
       
   517     def rematch(el, l):
       
   518         try:
       
   519             # ensure that the regex matches to the end of the string
       
   520             return re.match(el + r'\Z', l)
       
   521         except re.error:
       
   522             # el is an invalid regex
       
   523             return False
       
   524 
       
   525     def globmatch(el, l):
       
   526         # The only supported special characters are * and ?. Escaping is
       
   527         # supported.
       
   528         i, n = 0, len(el)
       
   529         res = ''
       
   530         while i < n:
       
   531             c = el[i]
       
   532             i += 1
       
   533             if c == '\\' and el[i] in '*?\\':
       
   534                 res += el[i - 1:i + 1]
       
   535                 i += 1
       
   536             elif c == '*':
       
   537                 res += '.*'
       
   538             elif c == '?':
       
   539                 res += '.'
       
   540             else:
       
   541                 res += re.escape(c)
       
   542         return rematch(res, l)
       
   543 
       
   544     pos = -1
       
   545     postout = []
       
   546     ret = 0
       
   547     for n, l in enumerate(output):
       
   548         lout, lcmd = l, None
       
   549         if salt in l:
       
   550             lout, lcmd = l.split(salt, 1)
       
   551 
       
   552         if lout:
       
   553             if lcmd:
       
   554                 lout += ' (no-eol)\n'
       
   555 
       
   556             el = None
       
   557             if pos in expected and expected[pos]:
       
   558                 el = expected[pos].pop(0)
       
   559 
       
   560             if el == lout: # perfect match (fast)
       
   561                 postout.append("  " + lout)
       
   562             elif (el and
       
   563                   (el.endswith(" (re)\n") and rematch(el[:-6] + '\n', lout) or
       
   564                    el.endswith(" (glob)\n") and globmatch(el[:-8] + '\n', lout)
       
   565                    or el.endswith(" (esc)\n") and
       
   566                       el.decode('string-escape') == l)):
       
   567                 postout.append("  " + el) # fallback regex/glob/esc match
       
   568             else:
       
   569                 if needescape(lout):
       
   570                     lout = stringescape(lout.rstrip('\n')) + " (esc)\n"
       
   571                 postout.append("  " + lout) # let diff deal with it
       
   572 
       
   573         if lcmd:
       
   574             # add on last return code
       
   575             ret = int(lcmd.split()[1])
       
   576             if ret != 0:
       
   577                 postout.append("  [%s]\n" % ret)
       
   578             if pos in after:
       
   579                 postout += after.pop(pos)
       
   580             pos = int(lcmd.split()[0])
       
   581 
       
   582     if pos in after:
       
   583         postout += after.pop(pos)
       
   584 
       
   585     return exitcode, postout
       
   586 
       
   587 wifexited = getattr(os, "WIFEXITED", lambda x: False)
       
   588 def run(cmd, options, replacements):
       
   589     """Run command in a sub-process, capturing the output (stdout and stderr).
       
   590     Return a tuple (exitcode, output).  output is None in debug mode."""
       
   591     # TODO: Use subprocess.Popen if we're running on Python 2.4
       
   592     if options.debug:
       
   593         proc = subprocess.Popen(cmd, shell=True)
       
   594         ret = proc.wait()
       
   595         return (ret, None)
       
   596 
       
   597     if os.name == 'nt' or sys.platform.startswith('java'):
       
   598         tochild, fromchild = os.popen4(cmd)
       
   599         tochild.close()
       
   600         output = fromchild.read()
       
   601         ret = fromchild.close()
       
   602         if ret is None:
       
   603             ret = 0
       
   604     else:
       
   605         proc = Popen4(cmd)
       
   606         def cleanup():
       
   607             os.kill(proc.pid, signal.SIGTERM)
       
   608             ret = proc.wait()
       
   609             if ret == 0:
       
   610                 ret = signal.SIGTERM << 8
       
   611             killdaemons()
       
   612             return ret
       
   613 
       
   614         try:
       
   615             output = ''
       
   616             proc.tochild.close()
       
   617             output = proc.fromchild.read()
       
   618             ret = proc.wait()
       
   619             if wifexited(ret):
       
   620                 ret = os.WEXITSTATUS(ret)
       
   621         except Timeout:
       
   622             vlog('# Process %d timed out - killing it' % proc.pid)
       
   623             ret = cleanup()
       
   624             output += ("\n### Abort: timeout after %d seconds.\n"
       
   625                        % options.timeout)
       
   626         except KeyboardInterrupt:
       
   627             vlog('# Handling keyboard interrupt')
       
   628             cleanup()
       
   629             raise
       
   630 
       
   631     for s, r in replacements:
       
   632         output = re.sub(s, r, output)
       
   633     return ret, splitnewlines(output)
       
   634 
       
   635 def runone(options, test, skips, fails):
       
   636     '''tristate output:
       
   637     None -> skipped
       
   638     True -> passed
       
   639     False -> failed'''
       
   640 
       
   641     def skip(msg):
       
   642         if not options.verbose:
       
   643             skips.append((test, msg))
       
   644         else:
       
   645             print "\nSkipping %s: %s" % (testpath, msg)
       
   646         return None
       
   647 
       
   648     def fail(msg):
       
   649         fails.append((test, msg))
       
   650         if not options.nodiff:
       
   651             print "\nERROR: %s %s" % (testpath, msg)
       
   652         return None
       
   653 
       
   654     vlog("# Test", test)
       
   655 
       
   656     # create a fresh hgrc
       
   657     hgrc = open(HGRCPATH, 'w+')
       
   658     hgrc.write('[ui]\n')
       
   659     hgrc.write('slash = True\n')
       
   660     hgrc.write('[defaults]\n')
       
   661     hgrc.write('backout = -d "0 0"\n')
       
   662     hgrc.write('commit = -d "0 0"\n')
       
   663     hgrc.write('tag = -d "0 0"\n')
       
   664     if options.inotify:
       
   665         hgrc.write('[extensions]\n')
       
   666         hgrc.write('inotify=\n')
       
   667         hgrc.write('[inotify]\n')
       
   668         hgrc.write('pidfile=%s\n' % DAEMON_PIDS)
       
   669         hgrc.write('appendpid=True\n')
       
   670     hgrc.close()
       
   671 
       
   672     testpath = os.path.join(TESTDIR, test)
       
   673     ref = os.path.join(TESTDIR, test+".out")
       
   674     err = os.path.join(TESTDIR, test+".err")
       
   675     if os.path.exists(err):
       
   676         os.remove(err)       # Remove any previous output files
       
   677     try:
       
   678         tf = open(testpath)
       
   679         firstline = tf.readline().rstrip()
       
   680         tf.close()
       
   681     except:
       
   682         firstline = ''
       
   683     lctest = test.lower()
       
   684 
       
   685     if lctest.endswith('.py') or firstline == '#!/usr/bin/env python':
       
   686         runner = pytest
       
   687     elif lctest.endswith('.t'):
       
   688         runner = tsttest
       
   689         ref = testpath
       
   690     else:
       
   691         # do not try to run non-executable programs
       
   692         if not os.access(testpath, os.X_OK):
       
   693             return skip("not executable")
       
   694         runner = shtest
       
   695 
       
   696     # Make a tmp subdirectory to work in
       
   697     testtmp = os.environ["TESTTMP"] = os.path.join(HGTMP, test)
       
   698     os.mkdir(testtmp)
       
   699     os.chdir(testtmp)
       
   700 
       
   701     if options.timeout > 0:
       
   702         signal.alarm(options.timeout)
       
   703 
       
   704     ret, out = runner(testpath, options, [
       
   705         (re.escape(testtmp), '$TESTTMP'),
       
   706         (r':%s\b' % options.port, ':$HGPORT'),
       
   707         (r':%s\b' % (options.port + 1), ':$HGPORT1'),
       
   708         (r':%s\b' % (options.port + 2), ':$HGPORT2'),
       
   709         ])
       
   710     vlog("# Ret was:", ret)
       
   711 
       
   712     if options.timeout > 0:
       
   713         signal.alarm(0)
       
   714 
       
   715     mark = '.'
       
   716 
       
   717     skipped = (ret == SKIPPED_STATUS)
       
   718 
       
   719     # If we're not in --debug mode and reference output file exists,
       
   720     # check test output against it.
       
   721     if options.debug:
       
   722         refout = None                   # to match "out is None"
       
   723     elif os.path.exists(ref):
       
   724         f = open(ref, "r")
       
   725         refout = splitnewlines(f.read())
       
   726         f.close()
       
   727     else:
       
   728         refout = []
       
   729 
       
   730     if (ret != 0 or out != refout) and not skipped and not options.debug:
       
   731         # Save errors to a file for diagnosis
       
   732         f = open(err, "wb")
       
   733         for line in out:
       
   734             f.write(line)
       
   735         f.close()
       
   736 
       
   737     if skipped:
       
   738         mark = 's'
       
   739         if out is None:                 # debug mode: nothing to parse
       
   740             missing = ['unknown']
       
   741             failed = None
       
   742         else:
       
   743             missing, failed = parsehghaveoutput(out)
       
   744         if not missing:
       
   745             missing = ['irrelevant']
       
   746         if failed:
       
   747             fail("hghave failed checking for %s" % failed[-1])
       
   748             skipped = False
       
   749         else:
       
   750             skip(missing[-1])
       
   751     elif out != refout:
       
   752         mark = '!'
       
   753         if ret:
       
   754             fail("output changed and returned error code %d" % ret)
       
   755         else:
       
   756             fail("output changed")
       
   757         if not options.nodiff:
       
   758             if options.view:
       
   759                 os.system("%s %s %s" % (options.view, ref, err))
       
   760             else:
       
   761                 showdiff(refout, out, ref, err)
       
   762         ret = 1
       
   763     elif ret:
       
   764         mark = '!'
       
   765         fail("returned error code %d" % ret)
       
   766 
       
   767     if not options.verbose:
       
   768         sys.stdout.write(mark)
       
   769         sys.stdout.flush()
       
   770 
       
   771     killdaemons()
       
   772 
       
   773     os.chdir(TESTDIR)
       
   774     if not options.keep_tmpdir:
       
   775         shutil.rmtree(testtmp, True)
       
   776     if skipped:
       
   777         return None
       
   778     return ret == 0
       
   779 
       
   780 _hgpath = None
       
   781 
       
   782 def _gethgpath():
       
   783     """Return the path to the mercurial package that is actually found by
       
   784     the current Python interpreter."""
       
   785     global _hgpath
       
   786     if _hgpath is not None:
       
   787         return _hgpath
       
   788 
       
   789     cmd = '%s -c "import mercurial; print mercurial.__path__[0]"'
       
   790     pipe = os.popen(cmd % PYTHON)
       
   791     try:
       
   792         _hgpath = pipe.read().strip()
       
   793     finally:
       
   794         pipe.close()
       
   795     return _hgpath
       
   796 
       
   797 def _checkhglib(verb):
       
   798     """Ensure that the 'mercurial' package imported by python is
       
   799     the one we expect it to be.  If not, print a warning to stderr."""
       
   800     expecthg = os.path.join(PYTHONDIR, 'mercurial')
       
   801     actualhg = _gethgpath()
       
   802     if actualhg != expecthg:
       
   803         sys.stderr.write('warning: %s with unexpected mercurial lib: %s\n'
       
   804                          '         (expected %s)\n'
       
   805                          % (verb, actualhg, expecthg))
       
   806 
       
   807 def runchildren(options, tests):
       
   808     if INST:
       
   809         installhg(options)
       
   810         _checkhglib("Testing")
       
   811 
       
   812     optcopy = dict(options.__dict__)
       
   813     optcopy['jobs'] = 1
       
   814     del optcopy['blacklist']
       
   815     if optcopy['with_hg'] is None:
       
   816         optcopy['with_hg'] = os.path.join(BINDIR, "hg")
       
   817     optcopy.pop('anycoverage', None)
       
   818 
       
   819     opts = []
       
   820     for opt, value in optcopy.iteritems():
       
   821         name = '--' + opt.replace('_', '-')
       
   822         if value is True:
       
   823             opts.append(name)
       
   824         elif value is not None:
       
   825             opts.append(name + '=' + str(value))
       
   826 
       
   827     tests.reverse()
       
   828     jobs = [[] for j in xrange(options.jobs)]
       
   829     while tests:
       
   830         for job in jobs:
       
   831             if not tests:
       
   832                 break
       
   833             job.append(tests.pop())
       
   834     fps = {}
       
   835 
       
   836     for j, job in enumerate(jobs):
       
   837         if not job:
       
   838             continue
       
   839         rfd, wfd = os.pipe()
       
   840         childopts = ['--child=%d' % wfd, '--port=%d' % (options.port + j * 3)]
       
   841         childtmp = os.path.join(HGTMP, 'child%d' % j)
       
   842         childopts += ['--tmpdir', childtmp]
       
   843         cmdline = [PYTHON, sys.argv[0]] + opts + childopts + job
       
   844         vlog(' '.join(cmdline))
       
   845         fps[os.spawnvp(os.P_NOWAIT, cmdline[0], cmdline)] = os.fdopen(rfd, 'r')
       
   846         os.close(wfd)
       
   847     signal.signal(signal.SIGINT, signal.SIG_IGN)
       
   848     failures = 0
       
   849     tested, skipped, failed = 0, 0, 0
       
   850     skips = []
       
   851     fails = []
       
   852     while fps:
       
   853         pid, status = os.wait()
       
   854         fp = fps.pop(pid)
       
   855         l = fp.read().splitlines()
       
   856         try:
       
   857             test, skip, fail = map(int, l[:3])
       
   858         except ValueError:
       
   859             test, skip, fail = 0, 0, 0
       
   860         split = -fail or len(l)
       
   861         for s in l[3:split]:
       
   862             skips.append(s.split(" ", 1))
       
   863         for s in l[split:]:
       
   864             fails.append(s.split(" ", 1))
       
   865         tested += test
       
   866         skipped += skip
       
   867         failed += fail
       
   868         vlog('pid %d exited, status %d' % (pid, status))
       
   869         failures |= status
       
   870     print
       
   871     if not options.noskips:
       
   872         for s in skips:
       
   873             print "Skipped %s: %s" % (s[0], s[1])
       
   874     for s in fails:
       
   875         print "Failed %s: %s" % (s[0], s[1])
       
   876 
       
   877     _checkhglib("Tested")
       
   878     print "# Ran %d tests, %d skipped, %d failed." % (
       
   879         tested, skipped, failed)
       
   880 
       
   881     if options.anycoverage:
       
   882         outputcoverage(options)
       
   883     sys.exit(failures != 0)
       
   884 
       
   885 def runtests(options, tests):
       
   886     global DAEMON_PIDS, HGRCPATH
       
   887     DAEMON_PIDS = os.environ["DAEMON_PIDS"] = os.path.join(HGTMP, 'daemon.pids')
       
   888     HGRCPATH = os.environ["HGRCPATH"] = os.path.join(HGTMP, '.hgrc')
       
   889 
       
   890     try:
       
   891         if INST:
       
   892             installhg(options)
       
   893             _checkhglib("Testing")
       
   894 
       
   895         if options.timeout > 0:
       
   896             try:
       
   897                 signal.signal(signal.SIGALRM, alarmed)
       
   898                 vlog('# Running each test with %d second timeout' %
       
   899                      options.timeout)
       
   900             except AttributeError:
       
   901                 print 'WARNING: cannot run tests with timeouts'
       
   902                 options.timeout = 0
       
   903 
       
   904         tested = 0
       
   905         failed = 0
       
   906         skipped = 0
       
   907 
       
   908         if options.restart:
       
   909             orig = list(tests)
       
   910             while tests:
       
   911                 if os.path.exists(tests[0] + ".err"):
       
   912                     break
       
   913                 tests.pop(0)
       
   914             if not tests:
       
   915                 print "running all tests"
       
   916                 tests = orig
       
   917 
       
   918         skips = []
       
   919         fails = []
       
   920 
       
   921         for test in tests:
       
   922             if options.blacklist:
       
   923                 filename = options.blacklist.get(test)
       
   924                 if filename is not None:
       
   925                     skips.append((test, "blacklisted (%s)" % filename))
       
   926                     skipped += 1
       
   927                     continue
       
   928 
       
   929             if options.retest and not os.path.exists(test + ".err"):
       
   930                 skipped += 1
       
   931                 continue
       
   932 
       
   933             if options.keywords:
       
   934                 fp = open(test)
       
   935                 t = fp.read().lower() + test.lower()
       
   936                 fp.close()
       
   937                 for k in options.keywords.lower().split():
       
   938                     if k in t:
       
   939                         break
       
   940                 else:
       
   941                     skipped += 1
       
   942                     continue
       
   943 
       
   944             ret = runone(options, test, skips, fails)
       
   945             if ret is None:
       
   946                 skipped += 1
       
   947             elif not ret:
       
   948                 if options.interactive:
       
   949                     print "Accept this change? [n] ",
       
   950                     answer = sys.stdin.readline().strip()
       
   951                     if answer.lower() in "y yes".split():
       
   952                         if test.endswith(".t"):
       
   953                             rename(test + ".err", test)
       
   954                         else:
       
   955                             rename(test + ".err", test + ".out")
       
   956                         tested += 1
       
   957                         fails.pop()
       
   958                         continue
       
   959                 failed += 1
       
   960                 if options.first:
       
   961                     break
       
   962             tested += 1
       
   963 
       
   964         if options.child:
       
   965             fp = os.fdopen(options.child, 'w')
       
   966             fp.write('%d\n%d\n%d\n' % (tested, skipped, failed))
       
   967             for s in skips:
       
   968                 fp.write("%s %s\n" % s)
       
   969             for s in fails:
       
   970                 fp.write("%s %s\n" % s)
       
   971             fp.close()
       
   972         else:
       
   973             print
       
   974             for s in skips:
       
   975                 print "Skipped %s: %s" % s
       
   976             for s in fails:
       
   977                 print "Failed %s: %s" % s
       
   978             _checkhglib("Tested")
       
   979             print "# Ran %d tests, %d skipped, %d failed." % (
       
   980                 tested, skipped, failed)
       
   981 
       
   982         if options.anycoverage:
       
   983             outputcoverage(options)
       
   984     except KeyboardInterrupt:
       
   985         failed = True
       
   986         print "\ninterrupted!"
       
   987 
       
   988     if failed:
       
   989         sys.exit(1)
       
   990 
       
   991 def main():
       
   992     (options, args) = parseargs()
       
   993     if not options.child:
       
   994         os.umask(022)
       
   995 
       
   996         checktools()
       
   997 
       
   998     if len(args) == 0:
       
   999         args = os.listdir(".")
       
  1000     args.sort()
       
  1001 
       
  1002     tests = []
       
  1003     skipped = []
       
  1004     for test in args:
       
  1005         if (test.startswith("test-") and '~' not in test and
       
  1006             ('.' not in test or test.endswith('.py') or
       
  1007              test.endswith('.bat') or test.endswith('.t'))):
       
  1008             if not os.path.exists(test):
       
  1009                 skipped.append(test)
       
  1010             else:
       
  1011                 tests.append(test)
       
  1012     if not tests:
       
  1013         for test in skipped:
       
  1014             print 'Skipped %s: does not exist' % test
       
  1015         print "# Ran 0 tests, %d skipped, 0 failed." % len(skipped)
       
  1016         return
       
  1017     tests = tests + skipped
       
  1018 
       
  1019     # Reset some environment variables to well-known values so that
       
  1020     # the tests produce repeatable output.
       
  1021     os.environ['LANG'] = os.environ['LC_ALL'] = os.environ['LANGUAGE'] = 'C'
       
  1022     os.environ['TZ'] = 'GMT'
       
  1023     os.environ["EMAIL"] = "Foo Bar <foo.bar@example.com>"
       
  1024     os.environ['CDPATH'] = ''
       
  1025     os.environ['COLUMNS'] = '80'
       
  1026     os.environ['GREP_OPTIONS'] = ''
       
  1027     os.environ['http_proxy'] = ''
       
  1028 
       
  1029     # unset env related to hooks
       
  1030     for k in os.environ.keys():
       
  1031         if k.startswith('HG_'):
       
  1032             # can't remove on solaris
       
  1033             os.environ[k] = ''
       
  1034             del os.environ[k]
       
  1035 
       
  1036     global TESTDIR, HGTMP, INST, BINDIR, PYTHONDIR, COVERAGE_FILE
       
  1037     TESTDIR = os.environ["TESTDIR"] = os.getcwd()
       
  1038     if options.tmpdir:
       
  1039         options.keep_tmpdir = True
       
  1040         tmpdir = options.tmpdir
       
  1041         if os.path.exists(tmpdir):
       
  1042             # Meaning of tmpdir has changed since 1.3: we used to create
       
  1043             # HGTMP inside tmpdir; now HGTMP is tmpdir.  So fail if
       
  1044             # tmpdir already exists.
       
  1045             sys.exit("error: temp dir %r already exists" % tmpdir)
       
  1046 
       
  1047             # Automatically removing tmpdir sounds convenient, but could
       
  1048             # really annoy anyone in the habit of using "--tmpdir=/tmp"
       
  1049             # or "--tmpdir=$HOME".
       
  1050             #vlog("# Removing temp dir", tmpdir)
       
  1051             #shutil.rmtree(tmpdir)
       
  1052         os.makedirs(tmpdir)
       
  1053     else:
       
  1054         tmpdir = tempfile.mkdtemp('', 'hgtests.')
       
  1055     HGTMP = os.environ['HGTMP'] = os.path.realpath(tmpdir)
       
  1056     DAEMON_PIDS = None
       
  1057     HGRCPATH = None
       
  1058 
       
  1059     os.environ["HGEDITOR"] = sys.executable + ' -c "import sys; sys.exit(0)"'
       
  1060     os.environ["HGMERGE"] = "internal:merge"
       
  1061     os.environ["HGUSER"]   = "test"
       
  1062     os.environ["HGENCODING"] = "ascii"
       
  1063     os.environ["HGENCODINGMODE"] = "strict"
       
  1064     os.environ["HGPORT"] = str(options.port)
       
  1065     os.environ["HGPORT1"] = str(options.port + 1)
       
  1066     os.environ["HGPORT2"] = str(options.port + 2)
       
  1067 
       
  1068     if options.with_hg:
       
  1069         INST = None
       
  1070         BINDIR = os.path.dirname(os.path.realpath(options.with_hg))
       
  1071 
       
  1072         # This looks redundant with how Python initializes sys.path from
       
  1073         # the location of the script being executed.  Needed because the
       
  1074         # "hg" specified by --with-hg is not the only Python script
       
  1075         # executed in the test suite that needs to import 'mercurial'
       
  1076         # ... which means it's not really redundant at all.
       
  1077         PYTHONDIR = BINDIR
       
  1078     else:
       
  1079         INST = os.path.join(HGTMP, "install")
       
  1080         BINDIR = os.environ["BINDIR"] = os.path.join(INST, "bin")
       
  1081         PYTHONDIR = os.path.join(INST, "lib", "python")
       
  1082 
       
  1083     os.environ["BINDIR"] = BINDIR
       
  1084     os.environ["PYTHON"] = PYTHON
       
  1085 
       
  1086     if not options.child:
       
  1087         path = [BINDIR] + os.environ["PATH"].split(os.pathsep)
       
  1088         os.environ["PATH"] = os.pathsep.join(path)
       
  1089 
       
  1090         # Include TESTDIR in PYTHONPATH so that out-of-tree extensions
       
  1091         # can run .../tests/run-tests.py test-foo where test-foo
       
  1092         # adds an extension to HGRC
       
  1093         pypath = [PYTHONDIR, TESTDIR]
       
  1094         # We have to augment PYTHONPATH, rather than simply replacing
       
  1095         # it, in case external libraries are only available via current
       
  1096         # PYTHONPATH.  (In particular, the Subversion bindings on OS X
       
  1097         # are in /opt/subversion.)
       
  1098         oldpypath = os.environ.get(IMPL_PATH)
       
  1099         if oldpypath:
       
  1100             pypath.append(oldpypath)
       
  1101         os.environ[IMPL_PATH] = os.pathsep.join(pypath)
       
  1102 
       
  1103     COVERAGE_FILE = os.path.join(TESTDIR, ".coverage")
       
  1104 
       
  1105     vlog("# Using TESTDIR", TESTDIR)
       
  1106     vlog("# Using HGTMP", HGTMP)
       
  1107     vlog("# Using PATH", os.environ["PATH"])
       
  1108     vlog("# Using", IMPL_PATH, os.environ[IMPL_PATH])
       
  1109 
       
  1110     try:
       
  1111         if len(tests) > 1 and options.jobs > 1:
       
  1112             runchildren(options, tests)
       
  1113         else:
       
  1114             runtests(options, tests)
       
  1115     finally:
       
  1116         time.sleep(1)
       
  1117         cleanup(options)
       
  1118 
       
  1119 if __name__ == '__main__':
       
  1120     main()