#!/usr/bin/env python## run-tests.py - Run a set of tests on Mercurial## Copyright 2006 Matt Mackall <mpm@selenic.com>## This software may be used and distributed according to the terms of the# GNU General Public License version 2 or any later version.# Modifying this script is tricky because it has many modes:# - serial (default) vs parallel (-jN, N > 1)# - no coverage (default) vs coverage (-c, -C, -s)# - temp install (default) vs specific hg script (--with-hg, --local)# - tests are a mix of shell scripts and Python scripts## If you change this script, it is recommended that you ensure you# haven't broken it by running it in various modes with a representative# sample of test scripts. For example:## 1) serial, no coverage, temp install:# ./run-tests.py test-s*# 2) serial, no coverage, local hg:# ./run-tests.py --local test-s*# 3) serial, coverage, temp install:# ./run-tests.py -c test-s*# 4) serial, coverage, local hg:# ./run-tests.py -c --local test-s* # unsupported# 5) parallel, no coverage, temp install:# ./run-tests.py -j2 test-s*# 6) parallel, no coverage, local hg:# ./run-tests.py -j2 --local test-s*# 7) parallel, coverage, temp install:# ./run-tests.py -j2 -c test-s* # currently broken# 8) parallel, coverage, local install:# ./run-tests.py -j2 -c --local test-s* # unsupported (and broken)# 9) parallel, custom tmp dir:# ./run-tests.py -j2 --tmpdir /tmp/myhgtests## (You could use any subset of the tests: test-s* happens to match# enough that it's worth doing parallel runs, few enough that it# completes fairly quickly, includes both shell and Python scripts, and# includes some scripts that run daemon processes.)fromdistutilsimportversionimportdifflibimporterrnoimportoptparseimportosimportshutilimportsubprocessimportsignalimportsysimporttempfileimporttimeimportreimportthreadingimportkilldaemonsaskillmodprocesslock=threading.Lock()closefds=os.name=='posix'defPopen4(cmd,wd,timeout):processlock.acquire()p=subprocess.Popen(cmd,shell=True,bufsize=-1,cwd=wd,close_fds=closefds,stdin=subprocess.PIPE,stdout=subprocess.PIPE,stderr=subprocess.STDOUT)processlock.release()p.fromchild=p.stdoutp.tochild=p.stdinp.childerr=p.stderrp.timeout=Falseiftimeout:deft():start=time.time()whiletime.time()-start<timeoutandp.returncodeisNone:time.sleep(.1)p.timeout=Trueifp.returncodeisNone:terminate(p)threading.Thread(target=t).start()returnp# reserved exit code to skip test (used by hghave)SKIPPED_STATUS=80SKIPPED_PREFIX='skipped: 'FAILED_PREFIX='hghave check failed: 'PYTHON=sys.executable.replace('\\','/')IMPL_PATH='PYTHONPATH'if'java'insys.platform:IMPL_PATH='JYTHONPATH'requiredtools=["python","diff","grep","unzip","gunzip","bunzip2","sed"]defaults={'jobs':('HGTEST_JOBS',1),'timeout':('HGTEST_TIMEOUT',180),'port':('HGTEST_PORT',20059),'shell':('HGTEST_SHELL','sh'),}defparselistfiles(files,listtype,warn=True):entries=dict()forfilenameinfiles:try:path=os.path.expanduser(os.path.expandvars(filename))f=open(path,"r")exceptIOError,err:iferr.errno!=errno.ENOENT:raiseifwarn:print"warning: no such %s file: %s"%(listtype,filename)continueforlineinf.readlines():line=line.split('#',1)[0].strip()ifline:entries[line]=filenamef.close()returnentriesdefparseargs():parser=optparse.OptionParser("%prog [options] [tests]")# keep these sortedparser.add_option("--blacklist",action="append",help="skip tests listed in the specified blacklist file")parser.add_option("--whitelist",action="append",help="always run tests listed in the specified whitelist file")parser.add_option("-C","--annotate",action="store_true",help="output files annotated with coverage")parser.add_option("--child",type="int",help="run as child process, summary to given fd")parser.add_option("-c","--cover",action="store_true",help="print a test coverage report")parser.add_option("-d","--debug",action="store_true",help="debug mode: write output of test scripts to console"" rather than capturing and diff'ing it (disables timeout)")parser.add_option("-f","--first",action="store_true",help="exit on the first test failure")parser.add_option("-H","--htmlcov",action="store_true",help="create an HTML report of the coverage of the files")parser.add_option("--inotify",action="store_true",help="enable inotify extension when running tests")parser.add_option("-i","--interactive",action="store_true",help="prompt to accept changed output")parser.add_option("-j","--jobs",type="int",help="number of jobs to run in parallel"" (default: $%s or %d)"%defaults['jobs'])parser.add_option("--keep-tmpdir",action="store_true",help="keep temporary directory after running tests")parser.add_option("-k","--keywords",help="run tests matching keywords")parser.add_option("-l","--local",action="store_true",help="shortcut for --with-hg=<testdir>/../hg")parser.add_option("-n","--nodiff",action="store_true",help="skip showing test changes")parser.add_option("-p","--port",type="int",help="port on which servers should listen"" (default: $%s or %d)"%defaults['port'])parser.add_option("--pure",action="store_true",help="use pure Python code instead of C extensions")parser.add_option("-R","--restart",action="store_true",help="restart at last error")parser.add_option("-r","--retest",action="store_true",help="retest failed tests")parser.add_option("-S","--noskips",action="store_true",help="don't report skip tests verbosely")parser.add_option("--shell",type="string",help="shell to use (default: $%s or %s)"%defaults['shell'])parser.add_option("-t","--timeout",type="int",help="kill errant tests after TIMEOUT seconds"" (default: $%s or %d)"%defaults['timeout'])parser.add_option("--tmpdir",type="string",help="run tests in the given temporary directory"" (implies --keep-tmpdir)")parser.add_option("-v","--verbose",action="store_true",help="output verbose messages")parser.add_option("--view",type="string",help="external diff viewer")parser.add_option("--with-hg",type="string",metavar="HG",help="test using specified hg script rather than a ""temporary installation")parser.add_option("-3","--py3k-warnings",action="store_true",help="enable Py3k warnings on Python 2.6+")parser.add_option('--extra-config-opt',action="append",help='set the given config opt in the test hgrc')foroption,(envvar,default)indefaults.items():defaults[option]=type(default)(os.environ.get(envvar,default))parser.set_defaults(**defaults)(options,args)=parser.parse_args()# jython is always pureif'java'insys.platformor'__pypy__'insys.modules:options.pure=Trueifoptions.with_hg:options.with_hg=os.path.expanduser(options.with_hg)ifnot(os.path.isfile(options.with_hg)andos.access(options.with_hg,os.X_OK)):parser.error('--with-hg must specify an executable hg script')ifnotos.path.basename(options.with_hg)=='hg':sys.stderr.write('warning: --with-hg should specify an hg script\n')ifoptions.local:testdir=os.path.dirname(os.path.realpath(sys.argv[0]))hgbin=os.path.join(os.path.dirname(testdir),'hg')ifos.name!='nt'andnotos.access(hgbin,os.X_OK):parser.error('--local specified, but %r not found or not executable'%hgbin)options.with_hg=hgbinoptions.anycoverage=options.coveroroptions.annotateoroptions.htmlcovifoptions.anycoverage:try:importcoveragecovver=version.StrictVersion(coverage.__version__).versionifcovver<(3,3):parser.error('coverage options require coverage 3.3 or later')exceptImportError:parser.error('coverage options now require the coverage package')ifoptions.anycoverageandoptions.local:# this needs some path mangling somewhere, I guessparser.error("sorry, coverage options do not work when --local ""is specified")globalvlogifoptions.verbose:ifoptions.jobs>1oroptions.childisnotNone:pid="[%d]"%os.getpid()else:pid=Nonedefvlog(*msg):iolock.acquire()ifpid:printpid,forminmsg:printm,printsys.stdout.flush()iolock.release()else:vlog=lambda*msg:Noneifoptions.tmpdir:options.tmpdir=os.path.expanduser(options.tmpdir)ifoptions.jobs<1:parser.error('--jobs must be positive')ifoptions.interactiveandoptions.jobs>1:print'(--interactive overrides --jobs)'options.jobs=1ifoptions.interactiveandoptions.debug:parser.error("-i/--interactive and -d/--debug are incompatible")ifoptions.debug:ifoptions.timeout!=defaults['timeout']:sys.stderr.write('warning: --timeout option ignored with --debug\n')options.timeout=0ifoptions.py3k_warnings:ifsys.version_info[:2]<(2,6)orsys.version_info[:2]>=(3,0):parser.error('--py3k-warnings can only be used on Python 2.6+')ifoptions.blacklist:options.blacklist=parselistfiles(options.blacklist,'blacklist')ifoptions.whitelist:options.whitelisted=parselistfiles(options.whitelist,'whitelist',warn=options.childisNone)else:options.whitelisted={}return(options,args)defrename(src,dst):"""Like os.rename(), trade atomicity and opened files friendliness for existing destination support. """shutil.copy(src,dst)os.remove(src)defsplitnewlines(text):'''like str.splitlines, but only split on newlines. keep line endings.'''i=0lines=[]whileTrue:n=text.find('\n',i)ifn==-1:last=text[i:]iflast:lines.append(last)returnlineslines.append(text[i:n+1])i=n+1defparsehghaveoutput(lines):'''Parse hghave log lines. Return tuple of lists (missing, failed): * the missing/unknown features * the features for which existence check failed'''missing=[]failed=[]forlineinlines:ifline.startswith(SKIPPED_PREFIX):line=line.splitlines()[0]missing.append(line[len(SKIPPED_PREFIX):])elifline.startswith(FAILED_PREFIX):line=line.splitlines()[0]failed.append(line[len(FAILED_PREFIX):])returnmissing,faileddefshowdiff(expected,output,ref,err):printforlineindifflib.unified_diff(expected,output,ref,err):sys.stdout.write(line)deffindprogram(program):"""Search PATH for a executable program"""forpinos.environ.get('PATH',os.defpath).split(os.pathsep):name=os.path.join(p,program)ifos.name=='nt'oros.access(name,os.X_OK):returnnamereturnNonedefchecktools():# Before we go any further, check for pre-requisite tools# stuff from coreutils (cat, rm, etc) are not testedforpinrequiredtools:ifos.name=='nt':p+='.exe'found=findprogram(p)iffound:vlog("# Found prerequisite",p,"at",found)else:print"WARNING: Did not find prerequisite tool: "+pdefterminate(proc):"""Terminate subprocess (with fallback for Python versions < 2.6)"""vlog('# Terminating process %d'%proc.pid)try:getattr(proc,'terminate',lambda:os.kill(proc.pid,signal.SIGTERM))()exceptOSError:passdefkilldaemons():returnkillmod.killdaemons(DAEMON_PIDS,tryhard=False,remove=True,logfn=vlog)defcleanup(options):ifnotoptions.keep_tmpdir:vlog("# Cleaning up HGTMP",HGTMP)shutil.rmtree(HGTMP,True)defusecorrectpython():# some tests run python interpreter. they must use same# interpreter we use or bad things will happen.exedir,exename=os.path.split(sys.executable)ifexenamein('python','python.exe'):path=findprogram(exename)ifos.path.dirname(path)==exedir:returnelse:exename='python'vlog('# Making python executable in test path use correct Python')mypython=os.path.join(BINDIR,exename)try:os.symlink(sys.executable,mypython)exceptAttributeError:# windows fallbackshutil.copyfile(sys.executable,mypython)shutil.copymode(sys.executable,mypython)definstallhg(options):vlog("# Performing temporary installation of HG")installerrs=os.path.join("tests","install.err")pure=options.pureand"--pure"or""# Run installer in hg rootscript=os.path.realpath(sys.argv[0])hgroot=os.path.dirname(os.path.dirname(script))os.chdir(hgroot)nohome='--home=""'ifos.name=='nt':# The --home="" trick works only on OS where os.sep == '/'# because of a distutils convert_path() fast-path. Avoid it at# least on Windows for now, deal with .pydistutils.cfg bugs# when they happen.nohome=''cmd=('%s setup.py %s clean --all'' build --build-base="%s"'' install --force --prefix="%s" --install-lib="%s"'' --install-scripts="%s" %s >%s 2>&1'%(sys.executable,pure,os.path.join(HGTMP,"build"),INST,PYTHONDIR,BINDIR,nohome,installerrs))vlog("# Running",cmd)ifos.system(cmd)==0:ifnotoptions.verbose:os.remove(installerrs)else:f=open(installerrs)forlineinf:printline,f.close()sys.exit(1)os.chdir(TESTDIR)usecorrectpython()vlog("# Installing dummy diffstat")f=open(os.path.join(BINDIR,'diffstat'),'w')f.write('#!'+sys.executable+'\n''import sys\n''files = 0\n''for line in sys.stdin:\n'' if line.startswith("diff "):\n'' files += 1\n''sys.stdout.write("files patched: %d\\n" % files)\n')f.close()os.chmod(os.path.join(BINDIR,'diffstat'),0700)ifoptions.py3k_warningsandnotoptions.anycoverage:vlog("# Updating hg command to enable Py3k Warnings switch")f=open(os.path.join(BINDIR,'hg'),'r')lines=[line.rstrip()forlineinf]lines[0]+=' -3'f.close()f=open(os.path.join(BINDIR,'hg'),'w')forlineinlines:f.write(line+'\n')f.close()hgbat=os.path.join(BINDIR,'hg.bat')ifos.path.isfile(hgbat):# hg.bat expects to be put in bin/scripts while run-tests.py# installation layout put it in bin/ directly. Fix itf=open(hgbat,'rb')data=f.read()f.close()if'"%~dp0..\python" "%~dp0hg" %*'indata:data=data.replace('"%~dp0..\python" "%~dp0hg" %*','"%~dp0python" "%~dp0hg" %*')f=open(hgbat,'wb')f.write(data)f.close()else:print'WARNING: cannot fix hg.bat reference to python.exe'ifoptions.anycoverage:custom=os.path.join(TESTDIR,'sitecustomize.py')target=os.path.join(PYTHONDIR,'sitecustomize.py')vlog('# Installing coverage trigger to %s'%target)shutil.copyfile(custom,target)rc=os.path.join(TESTDIR,'.coveragerc')vlog('# Installing coverage rc to %s'%rc)os.environ['COVERAGE_PROCESS_START']=rcfn=os.path.join(INST,'..','.coverage')os.environ['COVERAGE_FILE']=fndefoutputcoverage(options):vlog('# Producing coverage report')os.chdir(PYTHONDIR)defcovrun(*args):cmd='coverage %s'%' '.join(args)vlog('# Running: %s'%cmd)os.system(cmd)ifoptions.child:returncovrun('-c')omit=','.join(os.path.join(x,'*')forxin[BINDIR,TESTDIR])covrun('-i','-r','"--omit=%s"'%omit)# reportifoptions.htmlcov:htmldir=os.path.join(TESTDIR,'htmlcov')covrun('-i','-b','"--directory=%s"'%htmldir,'"--omit=%s"'%omit)ifoptions.annotate:adir=os.path.join(TESTDIR,'annotated')ifnotos.path.isdir(adir):os.mkdir(adir)covrun('-i','-a','"--directory=%s"'%adir,'"--omit=%s"'%omit)defpytest(test,wd,options,replacements):py3kswitch=options.py3k_warningsand' -3'or''cmd='%s%s "%s"'%(PYTHON,py3kswitch,test)vlog("# Running",cmd)returnrun(cmd,wd,options,replacements)defshtest(test,wd,options,replacements):cmd='%s "%s"'%(options.shell,test)vlog("# Running",cmd)returnrun(cmd,wd,options,replacements)needescape=re.compile(r'[\x00-\x08\x0b-\x1f\x7f-\xff]').searchescapesub=re.compile(r'[\x00-\x08\x0b-\x1f\\\x7f-\xff]').subescapemap=dict((chr(i),r'\x%02x'%i)foriinrange(256))escapemap.update({'\\':'\\\\','\r':r'\r'})defescapef(m):returnescapemap[m.group(0)]defstringescape(s):returnescapesub(escapef,s)defrematch(el,l):try:# ensure that the regex matches to the end of the stringreturnre.match(el+r'\Z',l)exceptre.error:# el is an invalid regexreturnFalsedefglobmatch(el,l):# The only supported special characters are * and ? plus / which also# matches \ on windows. Escaping of these caracters is supported.i,n=0,len(el)res=''whilei<n:c=el[i]i+=1ifc=='\\'andel[i]in'*?\\/':res+=el[i-1:i+1]i+=1elifc=='*':res+='.*'elifc=='?':res+='.'elifc=='/'andos.name=='nt':res+='[/\\\\]'else:res+=re.escape(c)returnrematch(res,l)deflinematch(el,l):ifel==l:# perfect match (fast)returnTrueif(eland(el.endswith(" (re)\n")andrematch(el[:-6]+'\n',l)orel.endswith(" (glob)\n")andglobmatch(el[:-8]+'\n',l)orel.endswith(" (esc)\n")and(el[:-7].decode('string-escape')+'\n'==lorel[:-7].decode('string-escape').replace('\r','')+'\n'==landos.name=='nt'))):returnTruereturnFalsedeftsttest(test,wd,options,replacements):# We generate a shell script which outputs unique markers to line# up script results with our source. These markers include input# line number and the last return codesalt="SALT"+str(time.time())defaddsalt(line,inpython):ifinpython:script.append('%s%d 0\n'%(salt,line))else:script.append('echo %s%s $?\n'%(salt,line))# After we run the shell script, we re-unify the script output# with non-active parts of the source, with synchronization by our# SALT line number markers. The after table contains the# non-active components, ordered by line numberafter={}pos=prepos=-1# Expected shellscript outputexpected={}# We keep track of whether or not we're in a Python block so we# can generate the surrounding doctest magicinpython=False# True or False when in a true or false conditional sectionskipping=Nonedefhghave(reqs):# TODO: do something smarter when all other uses of hghave is gonetdir=TESTDIR.replace('\\','/')proc=Popen4('%s -c "%s/hghave %s"'%(options.shell,tdir,' '.join(reqs)),wd,0)proc.communicate()ret=proc.wait()ifwifexited(ret):ret=os.WEXITSTATUS(ret)returnret==0f=open(test)t=f.readlines()f.close()script=[]ifoptions.debug:script.append('set -x\n')ifos.getenv('MSYSTEM'):script.append('alias pwd="pwd -W"\n')forn,linenumerate(t):ifnotl.endswith('\n'):l+='\n'ifl.startswith('#if'):ifskippingisnotNone:after.setdefault(pos,[]).append(' !!! nested #if\n')skipping=nothghave(l.split()[1:])after.setdefault(pos,[]).append(l)elifl.startswith('#else'):ifskippingisNone:after.setdefault(pos,[]).append(' !!! missing #if\n')skipping=notskippingafter.setdefault(pos,[]).append(l)elifl.startswith('#endif'):ifskippingisNone:after.setdefault(pos,[]).append(' !!! missing #if\n')skipping=Noneafter.setdefault(pos,[]).append(l)elifskipping:after.setdefault(pos,[]).append(l)elifl.startswith(' >>> '):# python inlinesafter.setdefault(pos,[]).append(l)prepos=pospos=nifnotinpython:# we've just entered a Python block, add the headerinpython=Trueaddsalt(prepos,False)# make sure we report the exit codescript.append('%s -m heredoctest <<EOF\n'%PYTHON)addsalt(n,True)script.append(l[2:])elifl.startswith(' ... '):# python inlinesafter.setdefault(prepos,[]).append(l)script.append(l[2:])elifl.startswith(' $ '):# commandsifinpython:script.append("EOF\n")inpython=Falseafter.setdefault(pos,[]).append(l)prepos=pospos=naddsalt(n,False)cmd=l[4:].split()iflen(cmd)==2andcmd[0]=='cd':l=' $ cd %s || exit 1\n'%cmd[1]script.append(l[4:])elifl.startswith(' > '):# continuationsafter.setdefault(prepos,[]).append(l)script.append(l[4:])elifl.startswith(' '):# results# queue up a list of expected resultsexpected.setdefault(pos,[]).append(l[2:])else:ifinpython:script.append("EOF\n")inpython=False# non-command/result - queue up for merged outputafter.setdefault(pos,[]).append(l)ifinpython:script.append("EOF\n")ifskippingisnotNone:after.setdefault(pos,[]).append(' !!! missing #endif\n')addsalt(n+1,False)# Write out the script and execute itfd,name=tempfile.mkstemp(suffix='hg-tst')try:forlinscript:os.write(fd,l)os.close(fd)cmd='%s "%s"'%(options.shell,name)vlog("# Running",cmd)exitcode,output=run(cmd,wd,options,replacements)# do not merge output if skipped, return hghave message instead# similarly, with --debug, output is Noneifexitcode==SKIPPED_STATUSoroutputisNone:returnexitcode,outputfinally:os.remove(name)# Merge the script output back into a unified testpos=-1postout=[]ret=0forn,linenumerate(output):lout,lcmd=l,Noneifsaltinl:lout,lcmd=l.split(salt,1)iflout:iflcmd:# output block had no trailing newline, clean uplout+=' (no-eol)\n'# find the expected output at the current positionel=Noneifposinexpectedandexpected[pos]:el=expected[pos].pop(0)iflinematch(el,lout):postout.append(" "+el)else:ifneedescape(lout):lout=stringescape(lout.rstrip('\n'))+" (esc)\n"postout.append(" "+lout)# let diff deal with itiflcmd:# add on last return coderet=int(lcmd.split()[1])ifret!=0:postout.append(" [%s]\n"%ret)ifposinafter:# merge in non-active test bitspostout+=after.pop(pos)pos=int(lcmd.split()[0])ifposinafter:postout+=after.pop(pos)returnexitcode,postoutwifexited=getattr(os,"WIFEXITED",lambdax:False)defrun(cmd,wd,options,replacements):"""Run command in a sub-process, capturing the output (stdout and stderr). Return a tuple (exitcode, output). output is None in debug mode."""# TODO: Use subprocess.Popen if we're running on Python 2.4ifoptions.debug:proc=subprocess.Popen(cmd,shell=True,cwd=wd)ret=proc.wait()return(ret,None)proc=Popen4(cmd,wd,options.timeout)defcleanup():terminate(proc)ret=proc.wait()ifret==0:ret=signal.SIGTERM<<8killdaemons()returnretoutput=''proc.tochild.close()try:output=proc.fromchild.read()exceptKeyboardInterrupt:vlog('# Handling keyboard interrupt')cleanup()raiseret=proc.wait()ifwifexited(ret):ret=os.WEXITSTATUS(ret)ifproc.timeout:ret='timeout'ifret:killdaemons()fors,rinreplacements:output=re.sub(s,r,output)returnret,splitnewlines(output)defrunone(options,test):'''tristate output: None -> skipped True -> passed False -> failed'''globalresults,resultslock,iolocktestpath=os.path.join(TESTDIR,test)defresult(l,e):resultslock.acquire()results[l].append(e)resultslock.release()defskip(msg):ifnotoptions.verbose:result('s',(test,msg))else:iolock.acquire()print"\nSkipping %s: %s"%(testpath,msg)iolock.release()returnNonedeffail(msg,ret):ifnotoptions.nodiff:iolock.acquire()print"\nERROR: %s%s"%(testpath,msg)iolock.release()if(notretandoptions.interactiveandos.path.exists(testpath+".err")):iolock.acquire()print"Accept this change? [n] ",answer=sys.stdin.readline().strip()iolock.release()ifanswer.lower()in"y yes".split():iftest.endswith(".t"):rename(testpath+".err",testpath)else:rename(testpath+".err",testpath+".out")result('p',test)returnresult('f',(test,msg))defsuccess():result('p',test)defignore(msg):result('i',(test,msg))if(os.path.basename(test).startswith("test-")and'~'notintestand('.'notintestortest.endswith('.py')ortest.endswith('.bat')ortest.endswith('.t'))):ifnotos.path.exists(test):skip("doesn't exist")returnNoneelse:vlog('# Test file',test,'not supported, ignoring')returnNone# not a supported test, don't recordifnot(options.whitelistedandtestinoptions.whitelisted):ifoptions.blacklistandtestinoptions.blacklist:skip("blacklisted")returnNoneifoptions.retestandnotos.path.exists(test+".err"):ignore("not retesting")returnNoneifoptions.keywords:fp=open(test)t=fp.read().lower()+test.lower()fp.close()forkinoptions.keywords.lower().split():ifkint:breakelse:ignore("doesn't match keyword")returnNonevlog("# Test",test)# create a fresh hgrchgrc=open(HGRCPATH,'w+')hgrc.write('[ui]\n')hgrc.write('slash = True\n')hgrc.write('[defaults]\n')hgrc.write('backout = -d "0 0"\n')hgrc.write('commit = -d "0 0"\n')hgrc.write('tag = -d "0 0"\n')ifoptions.inotify:hgrc.write('[extensions]\n')hgrc.write('inotify=\n')hgrc.write('[inotify]\n')hgrc.write('pidfile=%s\n'%DAEMON_PIDS)hgrc.write('appendpid=True\n')ifoptions.extra_config_opt:foroptinoptions.extra_config_opt:section,key=opt.split('.',1)assert'='inkey,('extra config opt %s must ''have an = for assignment'%opt)hgrc.write('[%s]\n%s\n'%(section,key))hgrc.close()ref=os.path.join(TESTDIR,test+".out")err=os.path.join(TESTDIR,test+".err")ifos.path.exists(err):os.remove(err)# Remove any previous output filestry:tf=open(testpath)firstline=tf.readline().rstrip()tf.close()exceptIOError:firstline=''lctest=test.lower()iflctest.endswith('.py')orfirstline=='#!/usr/bin/env python':runner=pytesteliflctest.endswith('.t'):runner=tsttestref=testpathelse:# do not try to run non-executable programsifnotos.access(testpath,os.X_OK):returnskip("not executable")runner=shtest# Make a tmp subdirectory to work intesttmp=os.environ["TESTTMP"]=os.environ["HOME"]= \os.path.join(HGTMP,os.path.basename(test))replacements=[(r':%s\b'%options.port,':$HGPORT'),(r':%s\b'%(options.port+1),':$HGPORT1'),(r':%s\b'%(options.port+2),':$HGPORT2'),]ifos.name=='nt':replacements.append((r'\r\n','\n'))replacements.append((''.join(c.isalpha()and'[%s%s]'%(c.lower(),c.upper())orcin'/\\'andr'[/\\]'orc.isdigit()andcor'\\'+cforcintesttmp),'$TESTTMP'))else:replacements.append((re.escape(testtmp),'$TESTTMP'))os.mkdir(testtmp)ret,out=runner(testpath,testtmp,options,replacements)vlog("# Ret was:",ret)mark='.'skipped=(ret==SKIPPED_STATUS)# If we're not in --debug mode and reference output file exists,# check test output against it.ifoptions.debug:refout=None# to match "out is None"elifos.path.exists(ref):f=open(ref,"r")refout=list(splitnewlines(f.read()))f.close()else:refout=[]if(ret!=0orout!=refout)andnotskippedandnotoptions.debug:# Save errors to a file for diagnosisf=open(err,"wb")forlineinout:f.write(line)f.close()ifskipped:mark='s'ifoutisNone:# debug mode: nothing to parsemissing=['unknown']failed=Noneelse:missing,failed=parsehghaveoutput(out)ifnotmissing:missing=['irrelevant']iffailed:fail("hghave failed checking for %s"%failed[-1],ret)skipped=Falseelse:skip(missing[-1])elifret=='timeout':mark='t'fail("timed out",ret)elifout!=refout:mark='!'ifnotoptions.nodiff:iolock.acquire()ifoptions.view:os.system("%s%s%s"%(options.view,ref,err))else:showdiff(refout,out,ref,err)iolock.release()ifret:fail("output changed and returned error code %d"%ret,ret)else:fail("output changed",ret)ret=1elifret:mark='!'fail("returned error code %d"%ret,ret)else:success()ifnotoptions.verbose:iolock.acquire()sys.stdout.write(mark)sys.stdout.flush()iolock.release()killdaemons()ifnotoptions.keep_tmpdir:shutil.rmtree(testtmp,True)ifskipped:returnNonereturnret==0_hgpath=Nonedef_gethgpath():"""Return the path to the mercurial package that is actually found by the current Python interpreter."""global_hgpathif_hgpathisnotNone:return_hgpathcmd='%s -c "import mercurial; print mercurial.__path__[0]"'pipe=os.popen(cmd%PYTHON)try:_hgpath=pipe.read().strip()finally:pipe.close()return_hgpathdef_checkhglib(verb):"""Ensure that the 'mercurial' package imported by python is the one we expect it to be. If not, print a warning to stderr."""expecthg=os.path.join(PYTHONDIR,'mercurial')actualhg=_gethgpath()ifos.path.abspath(actualhg)!=os.path.abspath(expecthg):sys.stderr.write('warning: %s with unexpected mercurial lib: %s\n'' (expected %s)\n'%(verb,actualhg,expecthg))defrunchildren(options,tests):ifINST:installhg(options)_checkhglib("Testing")optcopy=dict(options.__dict__)optcopy['jobs']=1# Because whitelist has to override keyword matches, we have to# actually load the whitelist in the children as well, so we allow# the list of whitelist files to pass through and be parsed in the# children, but not the dict of whitelisted tests resulting from# the parse, used here to override blacklisted tests.whitelist=optcopy['whitelisted']or[]deloptcopy['whitelisted']blacklist=optcopy['blacklist']or[]deloptcopy['blacklist']blacklisted=[]ifoptcopy['with_hg']isNone:optcopy['with_hg']=os.path.join(BINDIR,"hg")optcopy.pop('anycoverage',None)opts=[]foropt,valueinoptcopy.iteritems():name='--'+opt.replace('_','-')ifvalueisTrue:opts.append(name)elifisinstance(value,list):forvinvalue:opts.append(name+'='+str(v))elifvalueisnotNone:opts.append(name+'='+str(value))tests.reverse()jobs=[[]forjinxrange(options.jobs)]whiletests:forjobinjobs:ifnottests:breaktest=tests.pop()iftestnotinwhitelistandtestinblacklist:blacklisted.append(test)else:job.append(test)fps={}forj,jobinenumerate(jobs):ifnotjob:continuerfd,wfd=os.pipe()childopts=['--child=%d'%wfd,'--port=%d'%(options.port+j*3)]childtmp=os.path.join(HGTMP,'child%d'%j)childopts+=['--tmpdir',childtmp]cmdline=[PYTHON,sys.argv[0]]+opts+childopts+jobvlog(' '.join(cmdline))fps[os.spawnvp(os.P_NOWAIT,cmdline[0],cmdline)]=os.fdopen(rfd,'r')os.close(wfd)signal.signal(signal.SIGINT,signal.SIG_IGN)failures=0tested,skipped,failed=0,0,0skips=[]fails=[]whilefps:pid,status=os.wait()fp=fps.pop(pid)l=fp.read().splitlines()try:test,skip,fail=map(int,l[:3])exceptValueError:test,skip,fail=0,0,0split=-failorlen(l)forsinl[3:split]:skips.append(s.split(" ",1))forsinl[split:]:fails.append(s.split(" ",1))tested+=testskipped+=skipfailed+=failvlog('pid %d exited, status %d'%(pid,status))failures|=statusprintskipped+=len(blacklisted)ifnotoptions.noskips:forsinskips:print"Skipped %s: %s"%(s[0],s[1])forsinblacklisted:print"Skipped %s: blacklisted"%sforsinfails:print"Failed %s: %s"%(s[0],s[1])_checkhglib("Tested")print"# Ran %d tests, %d skipped, %d failed."%(tested,skipped,failed)ifoptions.anycoverage:outputcoverage(options)sys.exit(failures!=0)results=dict(p=[],f=[],s=[],i=[])resultslock=threading.Lock()iolock=threading.Lock()defrunqueue(options,tests,results):fortestintests:ret=runone(options,test)ifoptions.firstandretisnotNoneandnotret:breakdefruntests(options,tests):globalDAEMON_PIDS,HGRCPATHDAEMON_PIDS=os.environ["DAEMON_PIDS"]=os.path.join(HGTMP,'daemon.pids')HGRCPATH=os.environ["HGRCPATH"]=os.path.join(HGTMP,'.hgrc')try:ifINST:installhg(options)_checkhglib("Testing")ifoptions.restart:orig=list(tests)whiletests:ifos.path.exists(tests[0]+".err"):breaktests.pop(0)ifnottests:print"running all tests"tests=origrunqueue(options,tests,results)failed=len(results['f'])tested=len(results['p'])+failedskipped=len(results['s'])ignored=len(results['i'])ifoptions.child:fp=os.fdopen(options.child,'w')fp.write('%d\n%d\n%d\n'%(tested,skipped,failed))forsinresults['s']:fp.write("%s%s\n"%s)forsinresults['f']:fp.write("%s%s\n"%s)fp.close()else:printforsinresults['s']:print"Skipped %s: %s"%sforsinresults['f']:print"Failed %s: %s"%s_checkhglib("Tested")print"# Ran %d tests, %d skipped, %d failed."%(tested,skipped+ignored,failed)ifoptions.anycoverage:outputcoverage(options)exceptKeyboardInterrupt:failed=Trueprint"\ninterrupted!"iffailed:sys.exit(1)defmain():(options,args)=parseargs()ifnotoptions.child:os.umask(022)checktools()iflen(args)==0:args=os.listdir(".")args.sort()tests=args# Reset some environment variables to well-known values so that# the tests produce repeatable output.os.environ['LANG']=os.environ['LC_ALL']=os.environ['LANGUAGE']='C'os.environ['TZ']='GMT'os.environ["EMAIL"]="Foo Bar <foo.bar@example.com>"os.environ['CDPATH']=''os.environ['COLUMNS']='80'os.environ['GREP_OPTIONS']=''os.environ['http_proxy']=''os.environ['no_proxy']=''os.environ['NO_PROXY']=''os.environ['TERM']='xterm'# unset env related to hooksforkinos.environ.keys():ifk.startswith('HG_'):# can't remove on solarisos.environ[k]=''delos.environ[k]if'HG'inos.environ:# can't remove on solarisos.environ['HG']=''delos.environ['HG']globalTESTDIR,HGTMP,INST,BINDIR,PYTHONDIR,COVERAGE_FILETESTDIR=os.environ["TESTDIR"]=os.getcwd()ifoptions.tmpdir:options.keep_tmpdir=Truetmpdir=options.tmpdirifos.path.exists(tmpdir):# Meaning of tmpdir has changed since 1.3: we used to create# HGTMP inside tmpdir; now HGTMP is tmpdir. So fail if# tmpdir already exists.sys.exit("error: temp dir %r already exists"%tmpdir)# Automatically removing tmpdir sounds convenient, but could# really annoy anyone in the habit of using "--tmpdir=/tmp"# or "--tmpdir=$HOME".#vlog("# Removing temp dir", tmpdir)#shutil.rmtree(tmpdir)os.makedirs(tmpdir)else:d=Noneifos.name=='nt':# without this, we get the default temp dir location, but# in all lowercase, which causes troubles with paths (issue3490)d=os.getenv('TMP')tmpdir=tempfile.mkdtemp('','hgtests.',d)HGTMP=os.environ['HGTMP']=os.path.realpath(tmpdir)DAEMON_PIDS=NoneHGRCPATH=Noneos.environ["HGEDITOR"]=sys.executable+' -c "import sys; sys.exit(0)"'os.environ["HGMERGE"]="internal:merge"os.environ["HGUSER"]="test"os.environ["HGENCODING"]="ascii"os.environ["HGENCODINGMODE"]="strict"os.environ["HGPORT"]=str(options.port)os.environ["HGPORT1"]=str(options.port+1)os.environ["HGPORT2"]=str(options.port+2)ifoptions.with_hg:INST=NoneBINDIR=os.path.dirname(os.path.realpath(options.with_hg))# This looks redundant with how Python initializes sys.path from# the location of the script being executed. Needed because the# "hg" specified by --with-hg is not the only Python script# executed in the test suite that needs to import 'mercurial'# ... which means it's not really redundant at all.PYTHONDIR=BINDIRelse:INST=os.path.join(HGTMP,"install")BINDIR=os.environ["BINDIR"]=os.path.join(INST,"bin")PYTHONDIR=os.path.join(INST,"lib","python")os.environ["BINDIR"]=BINDIRos.environ["PYTHON"]=PYTHONifnotoptions.child:path=[BINDIR]+os.environ["PATH"].split(os.pathsep)os.environ["PATH"]=os.pathsep.join(path)# Include TESTDIR in PYTHONPATH so that out-of-tree extensions# can run .../tests/run-tests.py test-foo where test-foo# adds an extension to HGRCpypath=[PYTHONDIR,TESTDIR]# We have to augment PYTHONPATH, rather than simply replacing# it, in case external libraries are only available via current# PYTHONPATH. (In particular, the Subversion bindings on OS X# are in /opt/subversion.)oldpypath=os.environ.get(IMPL_PATH)ifoldpypath:pypath.append(oldpypath)os.environ[IMPL_PATH]=os.pathsep.join(pypath)COVERAGE_FILE=os.path.join(TESTDIR,".coverage")vlog("# Using TESTDIR",TESTDIR)vlog("# Using HGTMP",HGTMP)vlog("# Using PATH",os.environ["PATH"])vlog("# Using",IMPL_PATH,os.environ[IMPL_PATH])try:iflen(tests)>1andoptions.jobs>1:runchildren(options,tests)else:runtests(options,tests)finally:time.sleep(.1)cleanup(options)if__name__=='__main__':main()