# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr## This file is part of CubicWeb.## CubicWeb is free software: you can redistribute it and/or modify it under the# terms of the GNU Lesser General Public License as published by the Free# Software Foundation, either version 2.1 of the License, or (at your option)# any later version.## CubicWeb is distributed in the hope that it will be useful, but WITHOUT# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more# details.## You should have received a copy of the GNU Lesser General Public License along# with CubicWeb. If not, see <http://www.gnu.org/licenses/>."""Test tools for cubicweb"""__docformat__="restructuredtext en"importosimportsysimportloggingfromdatetimeimporttimedeltafromos.pathimport(abspath,join,exists,basename,dirname,normpath,split,isfile,isabs)fromlogilab.common.dateimportstrptimefromcubicwebimportCW_SOFTWARE_ROOT,ConfigurationError,schema,cwconfigfromcubicweb.server.serverconfigimportServerConfigurationfromcubicweb.etwist.twconfigimportTwistedConfigurationcwconfig.CubicWebConfiguration.cls_adjust_sys_path()# db auto-population configuration #############################################SYSTEM_ENTITIES=schema.SCHEMA_TYPES|set(('CWGroup','CWUser','CWProperty','Workflow','State','BaseTransition','Transition','WorkflowTransition','TrInfo','SubWorkflowExitPoint',))SYSTEM_RELATIONS=schema.META_RTYPES|set((# workflow related'workflow_of','state_of','transition_of','initial_state','default_workflow','allowed_transition','destination_state','from_state','to_state','condition','subworkflow','subworkflow_state','subworkflow_exit','custom_workflow','in_state','wf_info_for',# cwproperty'for_user',# schema definition'specializes','relation_type','from_entity','to_entity','constrained_by','cstrtype','widget','read_permission','update_permission','delete_permission','add_permission',# permission'in_group','require_group','require_permission',# deducted from other relations'primary_email',))# content validation configuration ############################################## validators are used to validate (XML, DTD, whatever) view's content# validators availables are :# 'dtd' : validates XML + declared DTD# 'xml' : guarantees XML is well formed# None : do not try to validate anything# {'vid': validator}VIEW_VALIDATORS={}# cubicweb test configuration ##################################################BASE_URL='http://testing.fr/cubicweb/'DEFAULT_SOURCES={'system':{'adapter':'native','db-encoding':'UTF-8',#'ISO-8859-1','db-user':u'admin','db-password':'gingkow','db-name':'tmpdb','db-driver':'sqlite','db-host':None,},'admin':{'login':u'admin','password':u'gingkow',},}classTestServerConfiguration(ServerConfiguration):mode='test'set_language=Falseread_instance_schema=Falseinit_repository=Truedb_require_setup=Trueoptions=cwconfig.merge_options(ServerConfiguration.options+(('anonymous-user',{'type':'string','default':None,'help':'login of the CubicWeb user account to use for anonymous user (if you want to allow anonymous)','group':'main','level':1,}),('anonymous-password',{'type':'string','default':None,'help':'password of the CubicWeb user account matching login','group':'main','level':1,}),))def__init__(self,appid,apphome=None,log_threshold=logging.CRITICAL+10):# must be set before calling parent __init__ifapphomeisNone:ifexists(appid):apphome=abspath(appid)else:# cube testapphome=abspath('..')self._apphome=apphomeServerConfiguration.__init__(self,appid)self.init_log(log_threshold,force=True)# need this, usually triggered by cubicweb-ctlself.load_cwctl_plugins()anonymous_user=TwistedConfiguration.anonymous_user.im_func@propertydefapphome(self):returnself._apphomeappdatahome=apphomedefload_configuration(self):super(TestServerConfiguration,self).load_configuration()self.global_set_option('anonymous-user','anon')self.global_set_option('anonymous-password','anon')# no undo support in testsself.global_set_option('undo-support','')defmain_config_file(self):"""return instance's control configuration file"""returnjoin(self.apphome,'%s.conf'%self.name)defbootstrap_cubes(self):try:super(TestServerConfiguration,self).bootstrap_cubes()exceptIOError:# no cubesself.init_cubes(())sourcefile=Nonedefsources_file(self):"""define in subclasses self.sourcefile if necessary"""ifself.sourcefile:print'Reading sources from',self.sourcefilesourcefile=self.sourcefileifnotisabs(sourcefile):sourcefile=join(self.apphome,sourcefile)else:sourcefile=super(TestServerConfiguration,self).sources_file()returnsourcefiledefsources(self):"""By default, we run tests with the sqlite DB backend. One may use its own configuration by just creating a 'sources' file in the test directory from wich tests are launched or by specifying an alternative sources file using self.sourcefile. """sources=super(TestServerConfiguration,self).sources()ifnotsources:sources=DEFAULT_SOURCESreturnsources# web config methods needed here for cases when we use this config as a web# configdefinstance_md5_version(self):return''defdefault_base_url(self):returnBASE_URLclassBaseApptestConfiguration(TestServerConfiguration,TwistedConfiguration):repo_method='inmemory'options=cwconfig.merge_options(TestServerConfiguration.options+TwistedConfiguration.options)cubicweb_appobject_path=TestServerConfiguration.cubicweb_appobject_path|TwistedConfiguration.cubicweb_appobject_pathcube_appobject_path=TestServerConfiguration.cube_appobject_path|TwistedConfiguration.cube_appobject_pathdefavailable_languages(self,*args):return('en','fr','de')defpyro_enabled(self):# but export PYRO_MULTITHREAD=0 or you get problems with sqlite and# threadsreturnTrue# XXX merge with BaseApptestConfiguration ?classApptestConfiguration(BaseApptestConfiguration):def__init__(self,appid,apphome=None,log_threshold=logging.CRITICAL,sourcefile=None):BaseApptestConfiguration.__init__(self,appid,apphome,log_threshold=log_threshold)self.init_repository=sourcefileisNoneself.sourcefile=sourcefileclassRealDatabaseConfiguration(ApptestConfiguration):"""configuration class for tests to run on a real database. The intialization is done by specifying a source file path. Important note: init_test_database / reset_test_database steps are skipped. It's thus up to the test developer to implement setUp/tearDown accordingly. Example usage:: class MyTests(CubicWebTC): _config = RealDatabseConfiguration('myapp', sourcefile='/path/to/sources') def test_something(self): rset = self.execute('Any X WHERE X is CWUser') self.view('foaf', rset) """db_require_setup=False# skip init_db / reset_db stepsread_instance_schema=True# read schema from database# test database handling #######################################################definit_test_database(config=None,configdir='data'):"""init a test database for a specific driver"""fromcubicweb.dbapiimportin_memory_cnxconfig=configorTestServerConfiguration(configdir)sources=config.sources()driver=sources['system']['db-driver']ifconfig.db_require_setup:ifdriver=='sqlite':init_test_database_sqlite(config)elifdriver=='postgres':init_test_database_postgres(config)else:raiseValueError('no initialization function for driver %r'%driver)config._cubes=None# avoid assertion errorrepo,cnx=in_memory_cnx(config,unicode(sources['admin']['login']),password=sources['admin']['password']or'xxx')ifdriver=='sqlite':install_sqlite_patch(repo.querier)returnrepo,cnxdefreset_test_database(config):"""init a test database for a specific driver"""ifnotconfig.db_require_setup:returndriver=config.sources()['system']['db-driver']ifdriver=='sqlite':reset_test_database_sqlite(config)elifdriver=='postgres':init_test_database_postgres(config)else:raiseValueError('no reset function for driver %r'%driver)### postgres test database handling ############################################definit_test_database_postgres(config):"""initialize a fresh postgresql databse used for testing purpose"""fromlogilab.databaseimportget_db_helperfromcubicweb.serverimportinit_repositoryfromcubicweb.server.serverctlimport(createdb,system_source_cnx,_db_sys_cnx)source=config.sources()['system']dbname=source['db-name']templdbname=dbname+'_template'helper=get_db_helper('postgres')# connect on the dbms system base to create our basedbcnx=_db_sys_cnx(source,'CREATE DATABASE and / or USER',verbose=0)cursor=dbcnx.cursor()try:ifdbnameinhelper.list_databases(cursor):cursor.execute('DROP DATABASE %s'%dbname)ifnottempldbnameinhelper.list_databases(cursor):source['db-name']=templdbnamecreatedb(helper,source,dbcnx,cursor)dbcnx.commit()cnx=system_source_cnx(source,special_privs='LANGUAGE C',verbose=0)templcursor=cnx.cursor()# XXX factorize with db-create codehelper.init_fti_extensions(templcursor)# install plpythonu/plpgsql language if not installed by the cubelangs=sys.platform=='win32'and('plpgsql',)or('plpythonu','plpgsql')forextlanginlangs:helper.create_language(templcursor,extlang)cnx.commit()templcursor.close()cnx.close()init_repository(config,interactive=False)source['db-name']=dbnameexcept:dbcnx.rollback()# XXX drop templateraisecreatedb(helper,source,dbcnx,cursor,template=templdbname)dbcnx.commit()dbcnx.close()### sqlserver2005 test database handling #######################################definit_test_database_sqlserver2005(config):"""initialize a fresh sqlserver databse used for testing purpose"""ifconfig.init_repository:fromcubicweb.serverimportinit_repositoryinit_repository(config,interactive=False,drop=True)### sqlite test database handling ##############################################defcleanup_sqlite(dbfile,removetemplate=False):try:os.remove(dbfile)os.remove('%s-journal'%dbfile)exceptOSError:passifremovetemplate:try:os.remove('%s-template'%dbfile)exceptOSError:passdefreset_test_database_sqlite(config):importshutildbfile=config.sources()['system']['db-name']cleanup_sqlite(dbfile)template='%s-template'%dbfileifexists(template):shutil.copy(template,dbfile)returnTruereturnFalsedefinit_test_database_sqlite(config):"""initialize a fresh sqlite databse used for testing purpose"""# remove database file if it existsifnotreset_test_database_sqlite(config):# initialize the databaseimportshutilfromcubicweb.serverimportinit_repositoryinit_repository(config,interactive=False)dbfile=config.sources()['system']['db-name']shutil.copy(dbfile,'%s-template'%dbfile)definstall_sqlite_patch(querier):"""This patch hotfixes the following sqlite bug : - http://www.sqlite.org/cvstrac/tktview?tn=1327,33 (some dates are returned as strings rather thant date objects) """ifhasattr(querier.__class__,'_devtools_sqlite_patched'):return# already monkey patcheddefwrap_execute(base_execute):defnew_execute(*args,**kwargs):rset=base_execute(*args,**kwargs)ifrset.description:found_date=Falseforrow,rowdescinzip(rset,rset.description):forcellindex,(value,vtype)inenumerate(zip(row,rowdesc)):ifvtypein('Date','Datetime')andtype(value)isunicode:found_date=Truevalue=value.rsplit('.',1)[0]try:row[cellindex]=strptime(value,'%Y-%m-%d %H:%M:%S')except:row[cellindex]=strptime(value,'%Y-%m-%d')ifvtype=='Time'andtype(value)isunicode:found_date=Truetry:row[cellindex]=strptime(value,'%H:%M:%S')except:# DateTime used as Time?row[cellindex]=strptime(value,'%Y-%m-%d %H:%M:%S')ifvtype=='Interval'andtype(value)isint:found_date=Truerow[cellindex]=timedelta(0,value,0)# XXX value is in number of seconds?ifnotfound_date:breakreturnrsetreturnnew_executequerier.__class__.execute=wrap_execute(querier.__class__.execute)querier.__class__._devtools_sqlite_patched=True