"""A library of helper functions for the CherryPy test suite."""importdatetimeimportlogginglog=logging.getLogger(__name__)importosthisdir=os.path.abspath(os.path.dirname(__file__))serverpem=os.path.join(os.getcwd(),thisdir,'test.pem')importreimportsysimporttimeimportwarningsimportcherrypyfromcherrypy._cpcompatimportbasestring,copyitems,HTTPSConnection,ntobfromcherrypy._cpcompatimportspawnfromcherrypy.libimporthttputilfromcherrypy.libimportgctoolsfromcherrypy.lib.reprconfimportunreprfromcherrypy.testimportwebtestimportnose_testconfig=Nonedefget_tst_config(overconf={}):global_testconfigif_testconfigisNone:conf={'scheme':'http','protocol':"HTTP/1.1",'port':54583,'host':'127.0.0.1','validate':False,'conquer':False,'server':'wsgi',}try:importtestconfig_conf=testconfig.config.get('supervisor',None)if_confisnotNone:fork,vin_conf.items():ifisinstance(v,basestring):_conf[k]=unrepr(v)conf.update(_conf)exceptImportError:pass_testconfig=confconf=_testconfig.copy()conf.update(overconf)returnconfclassSupervisor(object):"""Base class for modeling and controlling servers during testing."""def__init__(self,**kwargs):fork,vinkwargs.items():ifk=='port':setattr(self,k,int(v))setattr(self,k,v)log_to_stderr=lambdamsg,level:sys.stderr.write(msg+os.linesep)classLocalSupervisor(Supervisor):"""Base class for modeling/controlling servers which run in the same process. When the server side runs in a different process, start/stop can dump all state between each test module easily. When the server side runs in the same process as the client, however, we have to do a bit more work to ensure config and mounted apps are reset between tests. """using_apache=Falseusing_wsgi=Falsedef__init__(self,**kwargs):fork,vinkwargs.items():setattr(self,k,v)cherrypy.server.httpserver=self.httpserver_class# This is perhaps the wrong place for this call but this is the only# place that i've found so far that I KNOW is early enough to set this.cherrypy.config.update({'log.screen':False})engine=cherrypy.engineifhasattr(engine,"signal_handler"):engine.signal_handler.subscribe()ifhasattr(engine,"console_control_handler"):engine.console_control_handler.subscribe()#engine.subscribe('log', log_to_stderr)defstart(self,modulename=None):"""Load and start the HTTP server."""ifmodulename:# Unhook httpserver so cherrypy.server.start() creates a new# one (with config from setup_server, if declared).cherrypy.server.httpserver=Nonecherrypy.engine.start()self.sync_apps()defsync_apps(self):"""Tell the server about any apps which the setup functions mounted."""passdefstop(self):td=getattr(self,'teardown',None)iftd:td()cherrypy.engine.exit()forname,serverincopyitems(getattr(cherrypy,'servers',{})):server.unsubscribe()delcherrypy.servers[name]classNativeServerSupervisor(LocalSupervisor):"""Server supervisor for the builtin HTTP server."""httpserver_class="cherrypy._cpnative_server.CPHTTPServer"using_apache=Falseusing_wsgi=Falsedef__str__(self):return"Builtin HTTP Server on %s:%s"%(self.host,self.port)classLocalWSGISupervisor(LocalSupervisor):"""Server supervisor for the builtin WSGI server."""httpserver_class="cherrypy._cpwsgi_server.CPWSGIServer"using_apache=Falseusing_wsgi=Truedef__str__(self):return"Builtin WSGI Server on %s:%s"%(self.host,self.port)defsync_apps(self):"""Hook a new WSGI app into the origin server."""cherrypy.server.httpserver.wsgi_app=self.get_app()defget_app(self,app=None):"""Obtain a new (decorated) WSGI app to hook into the origin server."""ifappisNone:app=cherrypy.treeifself.conquer:try:importwsgiconqexceptImportError:warnings.warn("Error importing wsgiconq. pyconquer will not run.")else:app=wsgiconq.WSGILogger(app,c_calls=True)ifself.validate:try:fromwsgirefimportvalidateexceptImportError:warnings.warn("Error importing wsgiref. The validator will not run.")else:#wraps the app in the validatorapp=validate.validator(app)returnappdefget_cpmodpy_supervisor(**options):fromcherrypy.testimportmodpysup=modpy.ModPythonSupervisor(**options)sup.template=modpy.conf_cpmodpyreturnsupdefget_modpygw_supervisor(**options):fromcherrypy.testimportmodpysup=modpy.ModPythonSupervisor(**options)sup.template=modpy.conf_modpython_gatewaysup.using_wsgi=Truereturnsupdefget_modwsgi_supervisor(**options):fromcherrypy.testimportmodwsgireturnmodwsgi.ModWSGISupervisor(**options)defget_modfcgid_supervisor(**options):fromcherrypy.testimportmodfcgidreturnmodfcgid.ModFCGISupervisor(**options)defget_modfastcgi_supervisor(**options):fromcherrypy.testimportmodfastcgireturnmodfastcgi.ModFCGISupervisor(**options)defget_wsgi_u_supervisor(**options):cherrypy.server.wsgi_version=('u',0)returnLocalWSGISupervisor(**options)classCPWebCase(webtest.WebCase):script_name=""scheme="http"available_servers={'wsgi':LocalWSGISupervisor,'wsgi_u':get_wsgi_u_supervisor,'native':NativeServerSupervisor,'cpmodpy':get_cpmodpy_supervisor,'modpygw':get_modpygw_supervisor,'modwsgi':get_modwsgi_supervisor,'modfcgid':get_modfcgid_supervisor,'modfastcgi':get_modfastcgi_supervisor,}default_server="wsgi"def_setup_server(cls,supervisor,conf):v=sys.version.split()[0]log.info("Python version used to run this test script: %s"%v)log.info("CherryPy version: %s"%cherrypy.__version__)ifsupervisor.scheme=="https":ssl=" (ssl)"else:ssl=""log.info("HTTP server version: %s%s"%(supervisor.protocol,ssl))log.info("PID: %s"%os.getpid())cherrypy.server.using_apache=supervisor.using_apachecherrypy.server.using_wsgi=supervisor.using_wsgiifsys.platform[:4]=='java':cherrypy.config.update({'server.nodelay':False})ifisinstance(conf,basestring):parser=cherrypy.lib.reprconf.Parser()conf=parser.dict_from_file(conf).get('global',{})else:conf=confor{}baseconf=conf.copy()baseconf.update({'server.socket_host':supervisor.host,'server.socket_port':supervisor.port,'server.protocol_version':supervisor.protocol,'environment':"test_suite",})ifsupervisor.scheme=="https":#baseconf['server.ssl_module'] = 'builtin'baseconf['server.ssl_certificate']=serverpembaseconf['server.ssl_private_key']=serverpem# helper must be imported lazily so the coverage tool# can run against module-level statements within cherrypy.# Also, we have to do "from cherrypy.test import helper",# exactly like each test module does, because a relative import# would stick a second instance of webtest in sys.modules,# and we wouldn't be able to globally override the port anymore.ifsupervisor.scheme=="https":webtest.WebCase.HTTP_CONN=HTTPSConnectionreturnbaseconf_setup_server=classmethod(_setup_server)defsetup_class(cls):''#Creates a serverconf=get_tst_config()supervisor_factory=cls.available_servers.get(conf.get('server','wsgi'))ifsupervisor_factoryisNone:raiseRuntimeError('Unknown server in config: %s'%conf['server'])supervisor=supervisor_factory(**conf)#Copied from "run_test_suite"cherrypy.config.reset()baseconf=cls._setup_server(supervisor,conf)cherrypy.config.update(baseconf)setup_client()ifhasattr(cls,'setup_server'):# Clear the cherrypy tree and clear the wsgi server so that# it can be updated with the new rootcherrypy.tree=cherrypy._cptree.Tree()cherrypy.server.httpserver=Nonecls.setup_server()# Add a resource for verifying there are no refleaks# to *every* test class.cherrypy.tree.mount(gctools.GCRoot(),'/gc')cls.do_gc_test=Truesupervisor.start(cls.__module__)cls.supervisor=supervisorsetup_class=classmethod(setup_class)defteardown_class(cls):''ifhasattr(cls,'setup_server'):cls.supervisor.stop()teardown_class=classmethod(teardown_class)do_gc_test=Falsedeftest_gc(self):ifself.do_gc_test:self.getPage("/gc/stats")self.assertBody("Statistics:")# Tell nose to run this last in each class.# Prefer sys.maxint for Python 2.3, which didn't have float('inf')test_gc.compat_co_firstlineno=getattr(sys,'maxint',None)orfloat('inf')defprefix(self):returnself.script_name.rstrip("/")defbase(self):if((self.scheme=="http"andself.PORT==80)or(self.scheme=="https"andself.PORT==443)):port=""else:port=":%s"%self.PORTreturn"%s://%s%s%s"%(self.scheme,self.HOST,port,self.script_name.rstrip("/"))defexit(self):sys.exit()defgetPage(self,url,headers=None,method="GET",body=None,protocol=None):"""Open the url. Return status, headers, body."""ifself.script_name:url=httputil.urljoin(self.script_name,url)returnwebtest.WebCase.getPage(self,url,headers,method,body,protocol)defskip(self,msg='skipped '):raisenose.SkipTest(msg)defassertErrorPage(self,status,message=None,pattern=''):"""Compare the response body with a built in error page. The function will optionally look for the regexp pattern, within the exception embedded in the error page."""# This will never contain a tracebackpage=cherrypy._cperror.get_error_page(status,message=message)# First, test the response body without checking the traceback.# Stick a match-all group (.*) in to grab the traceback.esc=re.escapeepage=esc(page)epage=epage.replace(esc('<pre id="traceback"></pre>'),esc('<pre id="traceback">')+'(.*)'+esc('</pre>'))m=re.match(ntob(epage,self.encoding),self.body,re.DOTALL)ifnotm:self._handlewebError('Error page does not match; expected:\n'+page)return# Now test the pattern against the tracebackifpatternisNone:# Special-case None to mean that there should be *no* traceback.ifmandm.group(1):self._handlewebError('Error page contains traceback')else:if(misNone)or(notre.search(ntob(re.escape(pattern),self.encoding),m.group(1))):msg='Error page does not contain %s in traceback'self._handlewebError(msg%repr(pattern))date_tolerance=2defassertEqualDates(self,dt1,dt2,seconds=None):"""Assert abs(dt1 - dt2) is within Y seconds."""ifsecondsisNone:seconds=self.date_toleranceifdt1>dt2:diff=dt1-dt2else:diff=dt2-dt1ifnotdiff<datetime.timedelta(seconds=seconds):raiseAssertionError('%r and %r are not within %r seconds.'%(dt1,dt2,seconds))defsetup_client():"""Set up the WebCase classes to match the server's socket settings."""webtest.WebCase.PORT=cherrypy.server.socket_portwebtest.WebCase.HOST=cherrypy.server.socket_hostifcherrypy.server.ssl_certificate:CPWebCase.scheme='https'# --------------------------- Spawning helpers --------------------------- #classCPProcess(object):pid_file=os.path.join(thisdir,'test.pid')config_file=os.path.join(thisdir,'test.conf')config_template="""[global]server.socket_host: '%(host)s'server.socket_port: %(port)schecker.on: Falselog.screen: Falselog.error_file: r'%(error_log)s'log.access_file: r'%(access_log)s'%(ssl)s%(extra)s"""error_log=os.path.join(thisdir,'test.error.log')access_log=os.path.join(thisdir,'test.access.log')def__init__(self,wait=False,daemonize=False,ssl=False,socket_host=None,socket_port=None):self.wait=waitself.daemonize=daemonizeself.ssl=sslself.host=socket_hostorcherrypy.server.socket_hostself.port=socket_portorcherrypy.server.socket_portdefwrite_conf(self,extra=""):ifself.ssl:serverpem=os.path.join(thisdir,'test.pem')ssl="""server.ssl_certificate: r'%s'server.ssl_private_key: r'%s'"""%(serverpem,serverpem)else:ssl=""conf=self.config_template%{'host':self.host,'port':self.port,'error_log':self.error_log,'access_log':self.access_log,'ssl':ssl,'extra':extra,}f=open(self.config_file,'wb')f.write(ntob(conf,'utf-8'))f.close()defstart(self,imports=None):"""Start cherryd in a subprocess."""cherrypy._cpserver.wait_for_free_port(self.host,self.port)args=[os.path.join(thisdir,'..','cherryd'),'-c',self.config_file,'-p',self.pid_file,]ifnotisinstance(imports,(list,tuple)):imports=[imports]foriinimports:ifi:args.append('-i')args.append(i)ifself.daemonize:args.append('-d')env=os.environ.copy()# Make sure we import the cherrypy package in which this module is defined.grandparentdir=os.path.abspath(os.path.join(thisdir,'..','..'))ifenv.get('PYTHONPATH',''):env['PYTHONPATH']=os.pathsep.join((grandparentdir,env['PYTHONPATH']))else:env['PYTHONPATH']=grandparentdirres=spawn([sys.executable]+args,env,wait=self.wait)ifself.wait:self.exit_code=reselse:cherrypy._cpserver.wait_for_occupied_port(self.host,self.port)# Give the engine a wee bit more time to finish STARTINGifself.daemonize:time.sleep(2)else:time.sleep(1)defget_pid(self):returnint(open(self.pid_file,'rb').read())defjoin(self):"""Wait for the process to exit."""try:try:# Mac, UNIXos.wait()exceptAttributeError:# Windowstry:pid=self.get_pid()exceptIOError:# Assume the subprocess deleted the pidfile on shutdown.passelse:os.waitpid(pid,0)exceptOSError:x=sys.exc_info()[1]ifx.args!=(10,'No child processes'):raise