"""this module contains base classes for web tests:organization: Logilab:copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2.:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr:license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses"""__docformat__="restructuredtext en"importsysfrommathimportlogfromlogilab.common.debuggerimportDebuggerfromlogilab.common.testlibimportInnerTestfromlogilab.common.pytestimportnocoveragefromcubicweb.devtoolsimportVIEW_VALIDATORSfromcubicweb.devtools.apptestimportEnvBasedTCfromcubicweb.devtools._apptestimportunprotected_entities,SYSTEM_RELATIONSfromcubicweb.devtools.htmlparserimportDTDValidator,SaxOnlyValidator,HTMLValidatorfromcubicweb.devtools.fillimportinsert_entity_queries,make_relations_queriesfromcubicweb.sobjects.notificationimportNotificationViewfromcubicweb.vregistryimportNoSelectableObject## TODO ################ creation tests: make sure an entity was actually created# Existing Test EnvironmentclassCubicWebDebugger(Debugger):defdo_view(self,arg):importwebbrowserdata=self._getval(arg)file('/tmp/toto.html','w').write(data)webbrowser.open('file:///tmp/toto.html')defhow_many_dict(schema,cursor,how_many,skip):"""compute how many entities by type we need to be able to satisfy relations cardinality """# compute how many entities by type we need to be able to satisfy relation constraintrelmap={}forrschemainschema.relations():ifrschema.is_final():continueforsubj,objinrschema.iter_rdefs():card=rschema.rproperty(subj,obj,'cardinality')ifcard[0]in'1?'andlen(rschema.subjects(obj))==1:relmap.setdefault((rschema,subj),[]).append(str(obj))ifcard[1]in'1?'andlen(rschema.objects(subj))==1:relmap.setdefault((rschema,obj),[]).append(str(subj))unprotected=unprotected_entities(schema)foretypeinskip:unprotected.add(etype)howmanydict={}foretypeinunprotected_entities(schema,strict=True):howmanydict[str(etype)]=cursor.execute('Any COUNT(X) WHERE X is %s'%etype)[0][0]ifetypeinunprotected:howmanydict[str(etype)]+=how_manyfor(rschema,etype),targetsinrelmap.iteritems():# XXX should 1. check no cycle 2. propagate changesrelfactor=sum(howmanydict[e]foreintargets)howmanydict[str(etype)]=max(relfactor,howmanydict[etype])returnhowmanydictdefline_context_filter(line_no,center,before=3,after=None):"""return true if line are in context if after is None: after = before"""ifafterisNone:after=beforereturncenter-before<=line_no<=center+after## base webtest class #########################################################VALMAP={None:None,'dtd':DTDValidator,'xml':SaxOnlyValidator}classWebTest(EnvBasedTC):"""base class for web tests"""__abstract__=Truepdbclass=CubicWebDebugger# this is a hook to be able to define a list of rql queries# that are application dependent and cannot be guessed automaticallyapplication_rql=[]# validators are used to validate (XML, DTD, whatever) view's content# validators availables are :# DTDValidator : validates XML + declared DTD# SaxOnlyValidator : guarantees XML is well formed# None : do not try to validate anything# validators used must be imported from from.devtools.htmlparsercontent_type_validators={# maps MIME type : validator name## do not set html validators here, we need HTMLValidator for html# snippets#'text/html': DTDValidator,#'application/xhtml+xml': DTDValidator,'application/xml':SaxOnlyValidator,'text/xml':SaxOnlyValidator,'text/plain':None,'text/comma-separated-values':None,'text/x-vcard':None,'text/calendar':None,'application/json':None,'image/png':None,}# maps vid : validator name (override content_type_validators)vid_validators=dict((vid,VALMAP[valkey])forvid,valkeyinVIEW_VALIDATORS.iteritems())no_auto_populate=()ignored_relations=()defcustom_populate(self,how_many,cursor):passdefpost_populate(self,cursor):pass@nocoveragedefauto_populate(self,how_many):"""this method populates the database with `how_many` entities of each possible type. It also inserts random relations between them """cu=self.cursor()self.custom_populate(how_many,cu)vreg=self.vreghowmanydict=how_many_dict(self.schema,cu,how_many,self.no_auto_populate)foretypeinunprotected_entities(self.schema):ifetypeinself.no_auto_populate:continuenb=howmanydict.get(etype,how_many)forrql,argsininsert_entity_queries(etype,self.schema,vreg,nb):cu.execute(rql,args)edict={}foretypeinunprotected_entities(self.schema,strict=True):rset=cu.execute('%s X'%etype)edict[str(etype)]=set(row[0]forrowinrset.rows)existingrels={}ignored_relations=SYSTEM_RELATIONS+self.ignored_relationsforrschemainself.schema.relations():ifrschema.is_final()orrschemainignored_relations:continuerset=cu.execute('DISTINCT Any X,Y WHERE X %s Y'%rschema)existingrels.setdefault(rschema.type,set()).update((x,y)forx,yinrset)q=make_relations_queries(self.schema,edict,cu,ignored_relations,existingrels=existingrels)forrql,argsinq:cu.execute(rql,args)self.post_populate(cu)self.commit()@nocoveragedef_check_html(self,output,view,template='main-template'):"""raises an exception if the HTML is invalid"""try:validatorclass=self.vid_validators[view.id]exceptKeyError:iftemplateisNone:default_validator=HTMLValidatorelse:default_validator=DTDValidatorvalidatorclass=self.content_type_validators.get(view.content_type,default_validator)ifvalidatorclassisNone:returnNonevalidator=validatorclass()returnvalidator.parse_string(output.strip())defview(self,vid,rset=None,req=None,template='main-template',**kwargs):"""This method tests the view `vid` on `rset` using `template` If no error occured while rendering the view, the HTML is analyzed and parsed. :returns: an instance of `cubicweb.devtools.htmlparser.PageInfo` encapsulation the generated HTML """req=reqorrsetandrset.reqorself.request()req.form['vid']=vidkwargs['rset']=rsetview=self.vreg.select('views',vid,req,**kwargs)# set explicit test descriptionifrsetisnotNone:self.set_description("testing %s, mod=%s (%s)"%(vid,view.__module__,rset.printable_rql()))else:self.set_description("testing %s, mod=%s (no rset)"%(vid,view.__module__))iftemplateisNone:# raw view testing, no templateviewfunc=view.renderelse:kwargs['view']=viewtemplateview=self.vreg.select('views',template,req,**kwargs)viewfunc=lambda**k:self.vreg.main_template(req,template,**kwargs)kwargs.pop('rset')returnself._test_view(viewfunc,view,template,kwargs)def_test_view(self,viewfunc,view,template='main-template',kwargs={}):"""this method does the actual call to the view If no error occured while rendering the view, the HTML is analyzed and parsed. :returns: an instance of `cubicweb.devtools.htmlparser.PageInfo` encapsulation the generated HTML """output=Nonetry:output=viewfunc(**kwargs)returnself._check_html(output,view,template)except(SystemExit,KeyboardInterrupt):raiseexcept:# hijack exception: generative tests stop when the exception# is not an AssertionErrorklass,exc,tcbk=sys.exc_info()try:msg='[%s in %s] %s'%(klass,view.id,exc)except:msg='[%s in %s] undisplayable exception'%(klass,view.id)ifoutputisnotNone:position=getattr(exc,"position",(0,))[0]ifposition:# define filteroutput=output.splitlines()width=int(log(len(output),10))+1line_template=" %"+("%i"%width)+"i: %s"# XXX no need to iterate the whole file except to get# the line numberoutput='\n'.join(line_template%(idx+1,line)foridx,lineinenumerate(output)ifline_context_filter(idx+1,position))msg+='\nfor output:\n%s'%outputraiseAssertionError,msg,tcbkdefto_test_etypes(self):returnunprotected_entities(self.schema,strict=True)defiter_automatic_rsets(self,limit=10):"""generates basic resultsets for each entity type"""etypes=self.to_test_etypes()ifnotetypes:returnforetypeinetypes:yieldself.execute('Any X LIMIT %s WHERE X is %s'%(limit,etype))etype1=etypes.pop()try:etype2=etypes.pop()exceptKeyError:etype2=etype1# test a mixed query (DISTINCT/GROUP to avoid getting duplicate# X which make muledit view failing for instance (html validation fails# because of some duplicate "id" attributes)yieldself.execute('DISTINCT Any X, MAX(Y) GROUPBY X WHERE X is %s, Y is %s'%(etype1,etype2))# test some application-specific queries if definedforrqlinself.application_rql:yieldself.execute(rql)deflist_views_for(self,rset):"""returns the list of views that can be applied on `rset`"""req=rset.reqonly_once_vids=('primary','secondary','text')req.data['ex']=ValueError("whatever")forvid,viewsinself.vreg.registry('views').items():ifvid[0]=='_':continueifrset.rowcount>1andvidinonly_once_vids:continueviews=[viewforviewinviewsifview.category!='startupview'andnotissubclass(view,NotificationView)]ifviews:try:view=self.vreg.select_best(views,req,rset=rset)ifview.linkable():yieldviewelse:not_selected(self.vreg,view)# else the view is expected to be used as subview and should# not be tested directlyexceptNoSelectableObject:continuedeflist_actions_for(self,rset):"""returns the list of actions that can be applied on `rset`"""req=rset.reqforactioninself.vreg.possible_objects('actions',req,rset=rset):yieldactiondeflist_boxes_for(self,rset):"""returns the list of boxes that can be applied on `rset`"""req=rset.reqforboxinself.vreg.possible_objects('boxes',req,rset=rset):yieldboxdeflist_startup_views(self):"""returns the list of startup views"""req=self.request()forviewinself.vreg.possible_views(req,None):ifview.category=='startupview':yieldview.idelse:not_selected(self.vreg,view)def_test_everything_for(self,rset):"""this method tries to find everything that can be tested for `rset` and yields a callable test (as needed in generative tests) """propdefs=self.vreg['propertydefs']# make all components visiblefork,vinpropdefs.items():ifk.endswith('visible')andnotv['default']:propdefs[k]['default']=Trueforviewinself.list_views_for(rset):backup_rset=rset._prepare_copy(rset.rows,rset.description)yieldInnerTest(self._testname(rset,view.id,'view'),self.view,view.id,rset,rset.req.reset_headers(),'main-template')# We have to do this because some views modify the# resultset's syntax treerset=backup_rsetforactioninself.list_actions_for(rset):yieldInnerTest(self._testname(rset,action.id,'action'),action.url)forboxinself.list_boxes_for(rset):yieldInnerTest(self._testname(rset,box.id,'box'),box.render)@staticmethoddef_testname(rset,objid,objtype):return'%s_%s_%s'%('_'.join(rset.column_types(0)),objid,objtype)classAutomaticWebTest(WebTest):"""import this if you wan automatic tests to be ran"""## one eachdeftest_one_each_config(self):self.auto_populate(1)forrsetinself.iter_automatic_rsets(limit=1):fortestargsinself._test_everything_for(rset):yieldtestargs## ten eachdeftest_ten_each_config(self):self.auto_populate(10)forrsetinself.iter_automatic_rsets(limit=10):fortestargsinself._test_everything_for(rset):yieldtestargs## startup viewsdeftest_startup_views(self):forvidinself.list_startup_views():req=self.request()yieldself.view,vid,None,reqclassRealDBTest(WebTest):defiter_individual_rsets(self,etypes=None,limit=None):etypes=etypesorunprotected_entities(self.schema,strict=True)foretypeinetypes:rset=self.execute('Any X WHERE X is %s'%etype)forrowinxrange(len(rset)):iflimitandrow>limit:breakrset2=rset.limit(limit=1,offset=row)yieldrset2defnot_selected(vreg,vobject):try:vreg._selected[vobject.__class__]-=1except(KeyError,AttributeError):passdefvreg_instrumentize(testclass):fromcubicweb.devtools.apptestimportTestEnvironmentenv=testclass._env=TestEnvironment('data',configcls=testclass.configcls,requestcls=testclass.requestcls)vreg=env.vregvreg._selected={}orig_select_best=vreg.__class__.select_bestdefinstr_select_best(self,*args,**kwargs):selected=orig_select_best(self,*args,**kwargs)try:self._selected[selected.__class__]+=1exceptKeyError:self._selected[selected.__class__]=1exceptAttributeError:pass# occurs on vreg used to restore databasereturnselectedvreg.__class__.select_best=instr_select_bestdefprint_untested_objects(testclass,skipregs=('hooks','etypes')):vreg=testclass._env.vregforregistry,vobjectsdictinvreg.items():ifregistryinskipregs:continueforvobjectsinvobjectsdict.values():forvobjectinvobjects:ifnotvreg._selected.get(vobject):print'not tested',registry,vobject