tests/run-tests.py
changeset 562 b1aa7b64890b
child 670 80d0ed025a02
equal deleted inserted replaced
561:efeb09cb3760 562:b1aa7b64890b
       
     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, incorporated herein by reference.
       
     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 #
       
    37 # (You could use any subset of the tests: test-s* happens to match
       
    38 # enough that it's worth doing parallel runs, few enough that it
       
    39 # completes fairly quickly, includes both shell and Python scripts, and
       
    40 # includes some scripts that run daemon processes.)
       
    41 
       
    42 import difflib
       
    43 import errno
       
    44 import optparse
       
    45 import os
       
    46 import subprocess
       
    47 import shutil
       
    48 import signal
       
    49 import sys
       
    50 import tempfile
       
    51 import time
       
    52 
       
    53 closefds = os.name == 'posix'
       
    54 def Popen4(cmd, bufsize=-1):
       
    55     p = subprocess.Popen(cmd, shell=True, bufsize=bufsize,
       
    56                          close_fds=closefds,
       
    57                          stdin=subprocess.PIPE, stdout=subprocess.PIPE,
       
    58                          stderr=subprocess.STDOUT)
       
    59     p.fromchild = p.stdout
       
    60     p.tochild = p.stdin
       
    61     p.childerr = p.stderr
       
    62     return p
       
    63 
       
    64 # reserved exit code to skip test (used by hghave)
       
    65 SKIPPED_STATUS = 80
       
    66 SKIPPED_PREFIX = 'skipped: '
       
    67 FAILED_PREFIX  = 'hghave check failed: '
       
    68 PYTHON = sys.executable
       
    69 
       
    70 requiredtools = ["python", "diff", "grep", "unzip", "gunzip", "bunzip2", "sed"]
       
    71 
       
    72 defaults = {
       
    73     'jobs': ('HGTEST_JOBS', 1),
       
    74     'timeout': ('HGTEST_TIMEOUT', 180),
       
    75     'port': ('HGTEST_PORT', 20059),
       
    76 }
       
    77 
       
    78 def parseargs():
       
    79     parser = optparse.OptionParser("%prog [options] [tests]")
       
    80     parser.add_option("-C", "--annotate", action="store_true",
       
    81         help="output files annotated with coverage")
       
    82     parser.add_option("--child", type="int",
       
    83         help="run as child process, summary to given fd")
       
    84     parser.add_option("-c", "--cover", action="store_true",
       
    85         help="print a test coverage report")
       
    86     parser.add_option("-f", "--first", action="store_true",
       
    87         help="exit on the first test failure")
       
    88     parser.add_option("-i", "--interactive", action="store_true",
       
    89         help="prompt to accept changed output")
       
    90     parser.add_option("-j", "--jobs", type="int",
       
    91         help="number of jobs to run in parallel"
       
    92              " (default: $%s or %d)" % defaults['jobs'])
       
    93     parser.add_option("--keep-tmpdir", action="store_true",
       
    94         help="keep temporary directory after running tests"
       
    95              " (best used with --tmpdir)")
       
    96     parser.add_option("-R", "--restart", action="store_true",
       
    97         help="restart at last error")
       
    98     parser.add_option("-p", "--port", type="int",
       
    99         help="port on which servers should listen"
       
   100              " (default: $%s or %d)" % defaults['port'])
       
   101     parser.add_option("-r", "--retest", action="store_true",
       
   102         help="retest failed tests")
       
   103     parser.add_option("-s", "--cover_stdlib", action="store_true",
       
   104         help="print a test coverage report inc. standard libraries")
       
   105     parser.add_option("-t", "--timeout", type="int",
       
   106         help="kill errant tests after TIMEOUT seconds"
       
   107              " (default: $%s or %d)" % defaults['timeout'])
       
   108     parser.add_option("--tmpdir", type="string",
       
   109         help="run tests in the given temporary directory")
       
   110     parser.add_option("-v", "--verbose", action="store_true",
       
   111         help="output verbose messages")
       
   112     parser.add_option("-n", "--nodiff", action="store_true",
       
   113         help="skip showing test changes")
       
   114     parser.add_option("--with-hg", type="string",
       
   115         metavar="HG",
       
   116         help="test using specified hg script rather than a "
       
   117              "temporary installation")
       
   118     parser.add_option("--local", action="store_true",
       
   119         help="shortcut for --with-hg=<testdir>/../hg")
       
   120     parser.add_option("--pure", action="store_true",
       
   121         help="use pure Python code instead of C extensions")
       
   122 
       
   123     for option, default in defaults.items():
       
   124         defaults[option] = int(os.environ.get(*default))
       
   125     parser.set_defaults(**defaults)
       
   126     (options, args) = parser.parse_args()
       
   127 
       
   128     if options.with_hg:
       
   129         if not (os.path.isfile(options.with_hg) and
       
   130                 os.access(options.with_hg, os.X_OK)):
       
   131             parser.error('--with-hg must specify an executable hg script')
       
   132         if not os.path.basename(options.with_hg) == 'hg':
       
   133             sys.stderr.write('warning: --with-hg should specify an hg script')
       
   134     if options.local:
       
   135         testdir = os.path.dirname(os.path.realpath(sys.argv[0]))
       
   136         hgbin = os.path.join(os.path.dirname(testdir), 'hg')
       
   137         if not os.access(hgbin, os.X_OK):
       
   138             parser.error('--local specified, but %r not found or not executable'
       
   139                          % hgbin)
       
   140         options.with_hg = hgbin
       
   141 
       
   142     options.anycoverage = (options.cover or
       
   143                            options.cover_stdlib or
       
   144                            options.annotate)
       
   145 
       
   146     if options.anycoverage and options.with_hg:
       
   147         # I'm not sure if this is a fundamental limitation or just a
       
   148         # bug.  But I don't want to waste people's time and energy doing
       
   149         # test runs that don't give the results they want.
       
   150         parser.error("sorry, coverage options do not work when --with-hg "
       
   151                      "or --local specified")
       
   152 
       
   153     global vlog
       
   154     if options.verbose:
       
   155         if options.jobs > 1 or options.child is not None:
       
   156             pid = "[%d]" % os.getpid()
       
   157         else:
       
   158             pid = None
       
   159         def vlog(*msg):
       
   160             if pid:
       
   161                 print pid,
       
   162             for m in msg:
       
   163                 print m,
       
   164             print
       
   165     else:
       
   166         vlog = lambda *msg: None
       
   167 
       
   168     if options.jobs < 1:
       
   169         print >> sys.stderr, 'ERROR: -j/--jobs must be positive'
       
   170         sys.exit(1)
       
   171     if options.interactive and options.jobs > 1:
       
   172         print '(--interactive overrides --jobs)'
       
   173         options.jobs = 1
       
   174 
       
   175     return (options, args)
       
   176 
       
   177 def rename(src, dst):
       
   178     """Like os.rename(), trade atomicity and opened files friendliness
       
   179     for existing destination support.
       
   180     """
       
   181     shutil.copy(src, dst)
       
   182     os.remove(src)
       
   183 
       
   184 def splitnewlines(text):
       
   185     '''like str.splitlines, but only split on newlines.
       
   186     keep line endings.'''
       
   187     i = 0
       
   188     lines = []
       
   189     while True:
       
   190         n = text.find('\n', i)
       
   191         if n == -1:
       
   192             last = text[i:]
       
   193             if last:
       
   194                 lines.append(last)
       
   195             return lines
       
   196         lines.append(text[i:n+1])
       
   197         i = n + 1
       
   198 
       
   199 def parsehghaveoutput(lines):
       
   200     '''Parse hghave log lines.
       
   201     Return tuple of lists (missing, failed):
       
   202       * the missing/unknown features
       
   203       * the features for which existence check failed'''
       
   204     missing = []
       
   205     failed = []
       
   206     for line in lines:
       
   207         if line.startswith(SKIPPED_PREFIX):
       
   208             line = line.splitlines()[0]
       
   209             missing.append(line[len(SKIPPED_PREFIX):])
       
   210         elif line.startswith(FAILED_PREFIX):
       
   211             line = line.splitlines()[0]
       
   212             failed.append(line[len(FAILED_PREFIX):])
       
   213 
       
   214     return missing, failed
       
   215 
       
   216 def showdiff(expected, output):
       
   217     for line in difflib.unified_diff(expected, output,
       
   218             "Expected output", "Test output"):
       
   219         sys.stdout.write(line)
       
   220 
       
   221 def findprogram(program):
       
   222     """Search PATH for a executable program"""
       
   223     for p in os.environ.get('PATH', os.defpath).split(os.pathsep):
       
   224         name = os.path.join(p, program)
       
   225         if os.access(name, os.X_OK):
       
   226             return name
       
   227     return None
       
   228 
       
   229 def checktools():
       
   230     # Before we go any further, check for pre-requisite tools
       
   231     # stuff from coreutils (cat, rm, etc) are not tested
       
   232     for p in requiredtools:
       
   233         if os.name == 'nt':
       
   234             p += '.exe'
       
   235         found = findprogram(p)
       
   236         if found:
       
   237             vlog("# Found prerequisite", p, "at", found)
       
   238         else:
       
   239             print "WARNING: Did not find prerequisite tool: "+p
       
   240 
       
   241 def cleanup(options):
       
   242     if not options.keep_tmpdir:
       
   243         vlog("# Cleaning up HGTMP", HGTMP)
       
   244         shutil.rmtree(HGTMP, True)
       
   245 
       
   246 def usecorrectpython():
       
   247     # some tests run python interpreter. they must use same
       
   248     # interpreter we use or bad things will happen.
       
   249     exedir, exename = os.path.split(sys.executable)
       
   250     if exename == 'python':
       
   251         path = findprogram('python')
       
   252         if os.path.dirname(path) == exedir:
       
   253             return
       
   254     vlog('# Making python executable in test path use correct Python')
       
   255     mypython = os.path.join(BINDIR, 'python')
       
   256     try:
       
   257         os.symlink(sys.executable, mypython)
       
   258     except AttributeError:
       
   259         # windows fallback
       
   260         shutil.copyfile(sys.executable, mypython)
       
   261         shutil.copymode(sys.executable, mypython)
       
   262 
       
   263 def installhg(options):
       
   264     vlog("# Performing temporary installation of HG")
       
   265     installerrs = os.path.join("tests", "install.err")
       
   266     pure = options.pure and "--pure" or ""
       
   267 
       
   268     # Run installer in hg root
       
   269     os.chdir(os.path.join(os.path.dirname(sys.argv[0]), '..'))
       
   270     cmd = ('%s setup.py %s clean --all'
       
   271            ' install --force --prefix="%s" --install-lib="%s"'
       
   272            ' --install-scripts="%s" >%s 2>&1'
       
   273            % (sys.executable, pure, INST, PYTHONDIR, BINDIR, installerrs))
       
   274     vlog("# Running", cmd)
       
   275     if os.system(cmd) == 0:
       
   276         if not options.verbose:
       
   277             os.remove(installerrs)
       
   278     else:
       
   279         f = open(installerrs)
       
   280         for line in f:
       
   281             print line,
       
   282         f.close()
       
   283         sys.exit(1)
       
   284     os.chdir(TESTDIR)
       
   285 
       
   286     usecorrectpython()
       
   287 
       
   288     vlog("# Installing dummy diffstat")
       
   289     f = open(os.path.join(BINDIR, 'diffstat'), 'w')
       
   290     f.write('#!' + sys.executable + '\n'
       
   291             'import sys\n'
       
   292             'files = 0\n'
       
   293             'for line in sys.stdin:\n'
       
   294             '    if line.startswith("diff "):\n'
       
   295             '        files += 1\n'
       
   296             'sys.stdout.write("files patched: %d\\n" % files)\n')
       
   297     f.close()
       
   298     os.chmod(os.path.join(BINDIR, 'diffstat'), 0700)
       
   299 
       
   300     if options.anycoverage:
       
   301         vlog("# Installing coverage wrapper")
       
   302         os.environ['COVERAGE_FILE'] = COVERAGE_FILE
       
   303         if os.path.exists(COVERAGE_FILE):
       
   304             os.unlink(COVERAGE_FILE)
       
   305         # Create a wrapper script to invoke hg via coverage.py
       
   306         os.rename(os.path.join(BINDIR, "hg"), os.path.join(BINDIR, "_hg.py"))
       
   307         f = open(os.path.join(BINDIR, 'hg'), 'w')
       
   308         f.write('#!' + sys.executable + '\n')
       
   309         f.write('import sys, os; os.execv(sys.executable, [sys.executable, '
       
   310                 '"%s", "-x", "-p", "%s"] + sys.argv[1:])\n' %
       
   311                 (os.path.join(TESTDIR, 'coverage.py'),
       
   312                  os.path.join(BINDIR, '_hg.py')))
       
   313         f.close()
       
   314         os.chmod(os.path.join(BINDIR, 'hg'), 0700)
       
   315 
       
   316 def outputcoverage(options):
       
   317 
       
   318     vlog('# Producing coverage report')
       
   319     os.chdir(PYTHONDIR)
       
   320 
       
   321     def covrun(*args):
       
   322         start = sys.executable, os.path.join(TESTDIR, 'coverage.py')
       
   323         cmd = '"%s" "%s" %s' % (start[0], start[1], ' '.join(args))
       
   324         vlog('# Running: %s' % cmd)
       
   325         os.system(cmd)
       
   326 
       
   327     omit = [BINDIR, TESTDIR, PYTHONDIR]
       
   328     if not options.cover_stdlib:
       
   329         # Exclude as system paths (ignoring empty strings seen on win)
       
   330         omit += [x for x in sys.path if x != '']
       
   331     omit = ','.join(omit)
       
   332 
       
   333     covrun('-c') # combine from parallel processes
       
   334     for fn in os.listdir(TESTDIR):
       
   335         if fn.startswith('.coverage.'):
       
   336             os.unlink(os.path.join(TESTDIR, fn))
       
   337 
       
   338     covrun('-i', '-r', '"--omit=%s"' % omit) # report
       
   339     if options.annotate:
       
   340         adir = os.path.join(TESTDIR, 'annotated')
       
   341         if not os.path.isdir(adir):
       
   342             os.mkdir(adir)
       
   343         covrun('-i', '-a', '"--directory=%s"' % adir, '"--omit=%s"' % omit)
       
   344 
       
   345 class Timeout(Exception):
       
   346     pass
       
   347 
       
   348 def alarmed(signum, frame):
       
   349     raise Timeout
       
   350 
       
   351 def run(cmd, options):
       
   352     """Run command in a sub-process, capturing the output (stdout and stderr).
       
   353     Return the exist code, and output."""
       
   354     # TODO: Use subprocess.Popen if we're running on Python 2.4
       
   355     if os.name == 'nt' or sys.platform.startswith('java'):
       
   356         tochild, fromchild = os.popen4(cmd)
       
   357         tochild.close()
       
   358         output = fromchild.read()
       
   359         ret = fromchild.close()
       
   360         if ret == None:
       
   361             ret = 0
       
   362     else:
       
   363         proc = Popen4(cmd)
       
   364         try:
       
   365             output = ''
       
   366             proc.tochild.close()
       
   367             output = proc.fromchild.read()
       
   368             ret = proc.wait()
       
   369             if os.WIFEXITED(ret):
       
   370                 ret = os.WEXITSTATUS(ret)
       
   371         except Timeout:
       
   372             vlog('# Process %d timed out - killing it' % proc.pid)
       
   373             os.kill(proc.pid, signal.SIGTERM)
       
   374             ret = proc.wait()
       
   375             if ret == 0:
       
   376                 ret = signal.SIGTERM << 8
       
   377             output += ("\n### Abort: timeout after %d seconds.\n"
       
   378                        % options.timeout)
       
   379     return ret, splitnewlines(output)
       
   380 
       
   381 def runone(options, test, skips, fails):
       
   382     '''tristate output:
       
   383     None -> skipped
       
   384     True -> passed
       
   385     False -> failed'''
       
   386 
       
   387     def skip(msg):
       
   388         if not options.verbose:
       
   389             skips.append((test, msg))
       
   390         else:
       
   391             print "\nSkipping %s: %s" % (test, msg)
       
   392         return None
       
   393 
       
   394     def fail(msg):
       
   395         fails.append((test, msg))
       
   396         if not options.nodiff:
       
   397             print "\nERROR: %s %s" % (test, msg)
       
   398         return None
       
   399 
       
   400     vlog("# Test", test)
       
   401 
       
   402     # create a fresh hgrc
       
   403     hgrc = file(HGRCPATH, 'w+')
       
   404     hgrc.write('[ui]\n')
       
   405     hgrc.write('slash = True\n')
       
   406     hgrc.write('[defaults]\n')
       
   407     hgrc.write('backout = -d "0 0"\n')
       
   408     hgrc.write('commit = -d "0 0"\n')
       
   409     hgrc.write('tag = -d "0 0"\n')
       
   410     hgrc.close()
       
   411 
       
   412     err = os.path.join(TESTDIR, test+".err")
       
   413     ref = os.path.join(TESTDIR, test+".out")
       
   414     testpath = os.path.join(TESTDIR, test)
       
   415 
       
   416     if os.path.exists(err):
       
   417         os.remove(err)       # Remove any previous output files
       
   418 
       
   419     # Make a tmp subdirectory to work in
       
   420     tmpd = os.path.join(HGTMP, test)
       
   421     os.mkdir(tmpd)
       
   422     os.chdir(tmpd)
       
   423 
       
   424     try:
       
   425         tf = open(testpath)
       
   426         firstline = tf.readline().rstrip()
       
   427         tf.close()
       
   428     except:
       
   429         firstline = ''
       
   430     lctest = test.lower()
       
   431 
       
   432     if lctest.endswith('.py') or firstline == '#!/usr/bin/env python':
       
   433         cmd = '%s "%s"' % (PYTHON, testpath)
       
   434     elif lctest.endswith('.bat'):
       
   435         # do not run batch scripts on non-windows
       
   436         if os.name != 'nt':
       
   437             return skip("batch script")
       
   438         # To reliably get the error code from batch files on WinXP,
       
   439         # the "cmd /c call" prefix is needed. Grrr
       
   440         cmd = 'cmd /c call "%s"' % testpath
       
   441     else:
       
   442         # do not run shell scripts on windows
       
   443         if os.name == 'nt':
       
   444             return skip("shell script")
       
   445         # do not try to run non-executable programs
       
   446         if not os.path.exists(testpath):
       
   447             return fail("does not exist")
       
   448         elif not os.access(testpath, os.X_OK):
       
   449             return skip("not executable")
       
   450         cmd = '"%s"' % testpath
       
   451 
       
   452     if options.timeout > 0:
       
   453         signal.alarm(options.timeout)
       
   454 
       
   455     vlog("# Running", cmd)
       
   456     ret, out = run(cmd, options)
       
   457     vlog("# Ret was:", ret)
       
   458 
       
   459     if options.timeout > 0:
       
   460         signal.alarm(0)
       
   461 
       
   462     mark = '.'
       
   463 
       
   464     skipped = (ret == SKIPPED_STATUS)
       
   465     # If reference output file exists, check test output against it
       
   466     if os.path.exists(ref):
       
   467         f = open(ref, "r")
       
   468         refout = splitnewlines(f.read())
       
   469         f.close()
       
   470     else:
       
   471         refout = []
       
   472     if skipped:
       
   473         mark = 's'
       
   474         missing, failed = parsehghaveoutput(out)
       
   475         if not missing:
       
   476             missing = ['irrelevant']
       
   477         if failed:
       
   478             fail("hghave failed checking for %s" % failed[-1])
       
   479             skipped = False
       
   480         else:
       
   481             skip(missing[-1])
       
   482     elif out != refout:
       
   483         mark = '!'
       
   484         if ret:
       
   485             fail("output changed and returned error code %d" % ret)
       
   486         else:
       
   487             fail("output changed")
       
   488         if not options.nodiff:
       
   489             showdiff(refout, out)
       
   490         ret = 1
       
   491     elif ret:
       
   492         mark = '!'
       
   493         fail("returned error code %d" % ret)
       
   494 
       
   495     if not options.verbose:
       
   496         sys.stdout.write(mark)
       
   497         sys.stdout.flush()
       
   498 
       
   499     if ret != 0 and not skipped:
       
   500         # Save errors to a file for diagnosis
       
   501         f = open(err, "wb")
       
   502         for line in out:
       
   503             f.write(line)
       
   504         f.close()
       
   505 
       
   506     # Kill off any leftover daemon processes
       
   507     try:
       
   508         fp = file(DAEMON_PIDS)
       
   509         for line in fp:
       
   510             try:
       
   511                 pid = int(line)
       
   512             except ValueError:
       
   513                 continue
       
   514             try:
       
   515                 os.kill(pid, 0)
       
   516                 vlog('# Killing daemon process %d' % pid)
       
   517                 os.kill(pid, signal.SIGTERM)
       
   518                 time.sleep(0.25)
       
   519                 os.kill(pid, 0)
       
   520                 vlog('# Daemon process %d is stuck - really killing it' % pid)
       
   521                 os.kill(pid, signal.SIGKILL)
       
   522             except OSError, err:
       
   523                 if err.errno != errno.ESRCH:
       
   524                     raise
       
   525         fp.close()
       
   526         os.unlink(DAEMON_PIDS)
       
   527     except IOError:
       
   528         pass
       
   529 
       
   530     os.chdir(TESTDIR)
       
   531     if not options.keep_tmpdir:
       
   532         shutil.rmtree(tmpd, True)
       
   533     if skipped:
       
   534         return None
       
   535     return ret == 0
       
   536 
       
   537 _hgpath = None
       
   538 
       
   539 def _gethgpath():
       
   540     """Return the path to the mercurial package that is actually found by
       
   541     the current Python interpreter."""
       
   542     global _hgpath
       
   543     if _hgpath is not None:
       
   544         return _hgpath
       
   545 
       
   546     cmd = '%s -c "import mercurial; print mercurial.__path__[0]"'
       
   547     pipe = os.popen(cmd % PYTHON)
       
   548     try:
       
   549         _hgpath = pipe.read().strip()
       
   550     finally:
       
   551         pipe.close()
       
   552     return _hgpath
       
   553 
       
   554 def _checkhglib(verb):
       
   555     """Ensure that the 'mercurial' package imported by python is
       
   556     the one we expect it to be.  If not, print a warning to stderr."""
       
   557     expecthg = os.path.join(PYTHONDIR, 'mercurial')
       
   558     actualhg = _gethgpath()
       
   559     if actualhg != expecthg:
       
   560         sys.stderr.write('warning: %s with unexpected mercurial lib: %s\n'
       
   561                          '         (expected %s)\n'
       
   562                          % (verb, actualhg, expecthg))
       
   563 
       
   564 def runchildren(options, tests):
       
   565     if INST:
       
   566         installhg(options)
       
   567         _checkhglib("Testing")
       
   568 
       
   569     optcopy = dict(options.__dict__)
       
   570     optcopy['jobs'] = 1
       
   571     if optcopy['with_hg'] is None:
       
   572         optcopy['with_hg'] = os.path.join(BINDIR, "hg")
       
   573     opts = []
       
   574     for opt, value in optcopy.iteritems():
       
   575         name = '--' + opt.replace('_', '-')
       
   576         if value is True:
       
   577             opts.append(name)
       
   578         elif value is not None:
       
   579             opts.append(name + '=' + str(value))
       
   580 
       
   581     tests.reverse()
       
   582     jobs = [[] for j in xrange(options.jobs)]
       
   583     while tests:
       
   584         for job in jobs:
       
   585             if not tests: break
       
   586             job.append(tests.pop())
       
   587     fps = {}
       
   588     for j, job in enumerate(jobs):
       
   589         if not job:
       
   590             continue
       
   591         rfd, wfd = os.pipe()
       
   592         childopts = ['--child=%d' % wfd, '--port=%d' % (options.port + j * 3)]
       
   593         cmdline = [PYTHON, sys.argv[0]] + opts + childopts + job
       
   594         vlog(' '.join(cmdline))
       
   595         fps[os.spawnvp(os.P_NOWAIT, cmdline[0], cmdline)] = os.fdopen(rfd, 'r')
       
   596         os.close(wfd)
       
   597     failures = 0
       
   598     tested, skipped, failed = 0, 0, 0
       
   599     skips = []
       
   600     fails = []
       
   601     while fps:
       
   602         pid, status = os.wait()
       
   603         fp = fps.pop(pid)
       
   604         l = fp.read().splitlines()
       
   605         test, skip, fail = map(int, l[:3])
       
   606         split = -fail or len(l)
       
   607         for s in l[3:split]:
       
   608             skips.append(s.split(" ", 1))
       
   609         for s in l[split:]:
       
   610             fails.append(s.split(" ", 1))
       
   611         tested += test
       
   612         skipped += skip
       
   613         failed += fail
       
   614         vlog('pid %d exited, status %d' % (pid, status))
       
   615         failures |= status
       
   616     print
       
   617     for s in skips:
       
   618         print "Skipped %s: %s" % (s[0], s[1])
       
   619     for s in fails:
       
   620         print "Failed %s: %s" % (s[0], s[1])
       
   621 
       
   622     _checkhglib("Tested")
       
   623     print "# Ran %d tests, %d skipped, %d failed." % (
       
   624         tested, skipped, failed)
       
   625     sys.exit(failures != 0)
       
   626 
       
   627 def runtests(options, tests):
       
   628     global DAEMON_PIDS, HGRCPATH
       
   629     DAEMON_PIDS = os.environ["DAEMON_PIDS"] = os.path.join(HGTMP, 'daemon.pids')
       
   630     HGRCPATH = os.environ["HGRCPATH"] = os.path.join(HGTMP, '.hgrc')
       
   631 
       
   632     try:
       
   633         if INST:
       
   634             installhg(options)
       
   635             _checkhglib("Testing")
       
   636 
       
   637         if options.timeout > 0:
       
   638             try:
       
   639                 signal.signal(signal.SIGALRM, alarmed)
       
   640                 vlog('# Running each test with %d second timeout' %
       
   641                      options.timeout)
       
   642             except AttributeError:
       
   643                 print 'WARNING: cannot run tests with timeouts'
       
   644                 options.timeout = 0
       
   645 
       
   646         tested = 0
       
   647         failed = 0
       
   648         skipped = 0
       
   649 
       
   650         if options.restart:
       
   651             orig = list(tests)
       
   652             while tests:
       
   653                 if os.path.exists(tests[0] + ".err"):
       
   654                     break
       
   655                 tests.pop(0)
       
   656             if not tests:
       
   657                 print "running all tests"
       
   658                 tests = orig
       
   659 
       
   660         skips = []
       
   661         fails = []
       
   662         for test in tests:
       
   663             if options.retest and not os.path.exists(test + ".err"):
       
   664                 skipped += 1
       
   665                 continue
       
   666             ret = runone(options, test, skips, fails)
       
   667             if ret is None:
       
   668                 skipped += 1
       
   669             elif not ret:
       
   670                 if options.interactive:
       
   671                     print "Accept this change? [n] ",
       
   672                     answer = sys.stdin.readline().strip()
       
   673                     if answer.lower() in "y yes".split():
       
   674                         rename(test + ".err", test + ".out")
       
   675                         tested += 1
       
   676                         fails.pop()
       
   677                         continue
       
   678                 failed += 1
       
   679                 if options.first:
       
   680                     break
       
   681             tested += 1
       
   682 
       
   683         if options.child:
       
   684             fp = os.fdopen(options.child, 'w')
       
   685             fp.write('%d\n%d\n%d\n' % (tested, skipped, failed))
       
   686             for s in skips:
       
   687                 fp.write("%s %s\n" % s)
       
   688             for s in fails:
       
   689                 fp.write("%s %s\n" % s)
       
   690             fp.close()
       
   691         else:
       
   692             print
       
   693             for s in skips:
       
   694                 print "Skipped %s: %s" % s
       
   695             for s in fails:
       
   696                 print "Failed %s: %s" % s
       
   697             _checkhglib("Tested")
       
   698             print "# Ran %d tests, %d skipped, %d failed." % (
       
   699                 tested, skipped, failed)
       
   700 
       
   701         if options.anycoverage:
       
   702             outputcoverage(options)
       
   703     except KeyboardInterrupt:
       
   704         failed = True
       
   705         print "\ninterrupted!"
       
   706 
       
   707     if failed:
       
   708         sys.exit(1)
       
   709 
       
   710 def main():
       
   711     (options, args) = parseargs()
       
   712     if not options.child:
       
   713         os.umask(022)
       
   714 
       
   715         checktools()
       
   716 
       
   717     # Reset some environment variables to well-known values so that
       
   718     # the tests produce repeatable output.
       
   719     os.environ['LANG'] = os.environ['LC_ALL'] = 'C'
       
   720     os.environ['TZ'] = 'GMT'
       
   721     os.environ["EMAIL"] = "Foo Bar <foo.bar@example.com>"
       
   722     os.environ['CDPATH'] = ''
       
   723 
       
   724     global TESTDIR, HGTMP, INST, BINDIR, PYTHONDIR, COVERAGE_FILE
       
   725     TESTDIR = os.environ["TESTDIR"] = os.getcwd()
       
   726     HGTMP = os.environ['HGTMP'] = os.path.realpath(tempfile.mkdtemp('', 'hgtests.',
       
   727                                                    options.tmpdir))
       
   728     DAEMON_PIDS = None
       
   729     HGRCPATH = None
       
   730 
       
   731     os.environ["HGEDITOR"] = sys.executable + ' -c "import sys; sys.exit(0)"'
       
   732     os.environ["HGMERGE"] = "internal:merge"
       
   733     os.environ["HGUSER"]   = "test"
       
   734     os.environ["HGENCODING"] = "ascii"
       
   735     os.environ["HGENCODINGMODE"] = "strict"
       
   736     os.environ["HGPORT"] = str(options.port)
       
   737     os.environ["HGPORT1"] = str(options.port + 1)
       
   738     os.environ["HGPORT2"] = str(options.port + 2)
       
   739 
       
   740     if options.with_hg:
       
   741         INST = None
       
   742         BINDIR = os.path.dirname(os.path.realpath(options.with_hg))
       
   743 
       
   744         # This looks redundant with how Python initializes sys.path from
       
   745         # the location of the script being executed.  Needed because the
       
   746         # "hg" specified by --with-hg is not the only Python script
       
   747         # executed in the test suite that needs to import 'mercurial'
       
   748         # ... which means it's not really redundant at all.
       
   749         PYTHONDIR = BINDIR
       
   750     else:
       
   751         INST = os.path.join(HGTMP, "install")
       
   752         BINDIR = os.environ["BINDIR"] = os.path.join(INST, "bin")
       
   753         PYTHONDIR = os.path.join(INST, "lib", "python")
       
   754 
       
   755     os.environ["BINDIR"] = BINDIR
       
   756     os.environ["PYTHON"] = PYTHON
       
   757 
       
   758     if not options.child:
       
   759         path = [BINDIR] + os.environ["PATH"].split(os.pathsep)
       
   760         os.environ["PATH"] = os.pathsep.join(path)
       
   761 
       
   762         # Include TESTDIR in PYTHONPATH so that out-of-tree extensions
       
   763         # can run .../tests/run-tests.py test-foo where test-foo
       
   764         # adds an extension to HGRC
       
   765         pypath = [PYTHONDIR, TESTDIR]
       
   766         # We have to augment PYTHONPATH, rather than simply replacing
       
   767         # it, in case external libraries are only available via current
       
   768         # PYTHONPATH.  (In particular, the Subversion bindings on OS X
       
   769         # are in /opt/subversion.)
       
   770         oldpypath = os.environ.get('PYTHONPATH')
       
   771         if oldpypath:
       
   772             pypath.append(oldpypath)
       
   773         os.environ['PYTHONPATH'] = os.pathsep.join(pypath)
       
   774 
       
   775     COVERAGE_FILE = os.path.join(TESTDIR, ".coverage")
       
   776 
       
   777     if len(args) == 0:
       
   778         args = os.listdir(".")
       
   779         args.sort()
       
   780 
       
   781     tests = []
       
   782     for test in args:
       
   783         if (test.startswith("test-") and '~' not in test and
       
   784             ('.' not in test or test.endswith('.py') or
       
   785              test.endswith('.bat'))):
       
   786             tests.append(test)
       
   787     if not tests:
       
   788         print "# Ran 0 tests, 0 skipped, 0 failed."
       
   789         return
       
   790 
       
   791     vlog("# Using TESTDIR", TESTDIR)
       
   792     vlog("# Using HGTMP", HGTMP)
       
   793     vlog("# Using PATH", os.environ["PATH"])
       
   794     vlog("# Using PYTHONPATH", os.environ["PYTHONPATH"])
       
   795 
       
   796     try:
       
   797         if len(tests) > 1 and options.jobs > 1:
       
   798             runchildren(options, tests)
       
   799         else:
       
   800             runtests(options, tests)
       
   801     finally:
       
   802         cleanup(options)
       
   803 
       
   804 main()