devtools/testlib.py
brancholdstable
changeset 7078 bad26a22fe29
parent 7071 db7608cb32bc
child 7088 76e0dba5f8f3
equal deleted inserted replaced
7074:e4580e5f0703 7078:bad26a22fe29
    47 from cubicweb.sobjects import notification
    47 from cubicweb.sobjects import notification
    48 from cubicweb.web import Redirect, application
    48 from cubicweb.web import Redirect, application
    49 from cubicweb.server.session import security_enabled
    49 from cubicweb.server.session import security_enabled
    50 from cubicweb.server.hook import SendMailOp
    50 from cubicweb.server.hook import SendMailOp
    51 from cubicweb.devtools import SYSTEM_ENTITIES, SYSTEM_RELATIONS, VIEW_VALIDATORS
    51 from cubicweb.devtools import SYSTEM_ENTITIES, SYSTEM_RELATIONS, VIEW_VALIDATORS
    52 from cubicweb.devtools import BASE_URL, fake, htmlparser
    52 from cubicweb.devtools import BASE_URL, fake, htmlparser, DEFAULT_EMPTY_DB_ID
    53 from cubicweb.utils import json
    53 from cubicweb.utils import json
    54 
    54 
    55 # low-level utilities ##########################################################
    55 # low-level utilities ##########################################################
    56 
    56 
    57 class CubicWebDebugger(Debugger):
    57 class CubicWebDebugger(Debugger):
    59     html into a temporary file and open a web browser to examinate it.
    59     html into a temporary file and open a web browser to examinate it.
    60     """
    60     """
    61     def do_view(self, arg):
    61     def do_view(self, arg):
    62         import webbrowser
    62         import webbrowser
    63         data = self._getval(arg)
    63         data = self._getval(arg)
    64         file('/tmp/toto.html', 'w').write(data)
    64         with file('/tmp/toto.html', 'w') as toto:
       
    65             toto.write(data)
    65         webbrowser.open('file:///tmp/toto.html')
    66         webbrowser.open('file:///tmp/toto.html')
    66 
    67 
    67 def line_context_filter(line_no, center, before=3, after=None):
    68 def line_context_filter(line_no, center, before=3, after=None):
    68     """return true if line are in context
    69     """return true if line are in context
    69 
    70 
    80     if strict:
    81     if strict:
    81         protected_entities = yams.schema.BASE_TYPES
    82         protected_entities = yams.schema.BASE_TYPES
    82     else:
    83     else:
    83         protected_entities = yams.schema.BASE_TYPES.union(SYSTEM_ENTITIES)
    84         protected_entities = yams.schema.BASE_TYPES.union(SYSTEM_ENTITIES)
    84     return set(schema.entities()) - protected_entities
    85     return set(schema.entities()) - protected_entities
    85 
       
    86 def refresh_repo(repo, resetschema=False, resetvreg=False):
       
    87     for pool in repo.pools:
       
    88         pool.close(True)
       
    89     repo.system_source.shutdown()
       
    90     devtools.reset_test_database(repo.config)
       
    91     for pool in repo.pools:
       
    92         pool.reconnect()
       
    93     repo._type_source_cache = {}
       
    94     repo._extid_cache = {}
       
    95     repo.querier._rql_cache = {}
       
    96     for source in repo.sources:
       
    97         source.reset_caches()
       
    98     if resetschema:
       
    99         repo.set_schema(repo.config.load_schema(), resetvreg=resetvreg)
       
   100 
       
   101 
    86 
   102 # email handling, to test emails sent by an application ########################
    87 # email handling, to test emails sent by an application ########################
   103 
    88 
   104 MAILBOX = []
    89 MAILBOX = []
   105 
    90 
   189     """
   174     """
   190     appid = 'data'
   175     appid = 'data'
   191     configcls = devtools.ApptestConfiguration
   176     configcls = devtools.ApptestConfiguration
   192     reset_schema = reset_vreg = False # reset schema / vreg between tests
   177     reset_schema = reset_vreg = False # reset schema / vreg between tests
   193     tags = TestCase.tags | Tags('cubicweb', 'cw_repo')
   178     tags = TestCase.tags | Tags('cubicweb', 'cw_repo')
       
   179     test_db_id = DEFAULT_EMPTY_DB_ID
       
   180     _cnxs = set() # establised connection
       
   181     _cnx  = None  # current connection
       
   182 
       
   183     # Too much complicated stuff. the class doesn't need to bear the repo anymore
       
   184     @classmethod
       
   185     def set_cnx(cls, cnx):
       
   186         cls._cnxs.add(cnx)
       
   187         cls._cnx = cnx
       
   188 
       
   189     @property
       
   190     def cnx(self):
       
   191         return self.__class__._cnx
   194 
   192 
   195     @classproperty
   193     @classproperty
   196     def config(cls):
   194     def config(cls):
   197         """return the configuration object
   195         """return the configuration object
   198 
   196 
   199         Configuration is cached on the test class.
   197         Configuration is cached on the test class.
   200         """
   198         """
   201         try:
   199         try:
       
   200             assert not cls is CubicWebTC, "Don't use CubicWebTC directly to prevent database caching issue"
   202             return cls.__dict__['_config']
   201             return cls.__dict__['_config']
   203         except KeyError:
   202         except KeyError:
   204             home = abspath(join(dirname(sys.modules[cls.__module__].__file__), cls.appid))
   203             home = abspath(join(dirname(sys.modules[cls.__module__].__file__), cls.appid))
   205             config = cls._config = cls.configcls(cls.appid, apphome=home)
   204             config = cls._config = cls.configcls(cls.appid, apphome=home)
   206             config.mode = 'test'
   205             config.mode = 'test'
   235         try:
   234         try:
   236             config.global_set_option('embed-allowed', re.compile('.*'))
   235             config.global_set_option('embed-allowed', re.compile('.*'))
   237         except: # not in server only configuration
   236         except: # not in server only configuration
   238             pass
   237             pass
   239 
   238 
       
   239     #XXX this doesn't need to a be classmethod anymore
   240     @classmethod
   240     @classmethod
   241     def _init_repo(cls):
   241     def _init_repo(cls):
   242         """init the repository and connection to it.
   242         """init the repository and connection to it.
   243 
   243         """
   244         Repository and connection are cached on the test class. Once
   244         # setup configuration for test
   245         initialized, we simply reset connections and repository caches.
       
   246         """
       
   247         if not 'repo' in cls.__dict__:
       
   248             cls._build_repo()
       
   249         else:
       
   250             try:
       
   251                 cls.cnx.rollback()
       
   252             except ProgrammingError:
       
   253                 pass
       
   254             cls._refresh_repo()
       
   255 
       
   256     @classmethod
       
   257     def _build_repo(cls):
       
   258         cls.repo, cls.cnx = devtools.init_test_database(config=cls.config)
       
   259         cls.init_config(cls.config)
   245         cls.init_config(cls.config)
   260         cls.repo.hm.call_hooks('server_startup', repo=cls.repo)
   246         # get or restore and working db.
       
   247         db_handler = devtools.get_test_db_handler(cls.config)
       
   248         db_handler.build_db_cache(cls.test_db_id, cls.pre_setup_database)
       
   249 
       
   250         cls.repo, cnx = db_handler.get_repo_and_cnx(cls.test_db_id)
       
   251         # no direct assignation to cls.cnx anymore.
       
   252         # cnx is now an instance property that use a class protected attributes.
       
   253         cls.set_cnx(cnx)
   261         cls.vreg = cls.repo.vreg
   254         cls.vreg = cls.repo.vreg
   262         cls.websession = DBAPISession(cls.cnx, cls.admlogin,
   255         cls.websession = DBAPISession(cnx, cls.admlogin,
   263                                       {'password': cls.admpassword})
   256                                       {'password': cls.admpassword})
   264         cls._orig_cnx = (cls.cnx, cls.websession)
   257         cls._orig_cnx = (cnx, cls.websession)
   265         cls.config.repository = lambda x=None: cls.repo
   258         cls.config.repository = lambda x=None: cls.repo
   266 
   259 
   267     @classmethod
   260     def _close_cnx(self):
   268     def _refresh_repo(cls):
   261         for cnx in list(self._cnxs):
   269         refresh_repo(cls.repo, cls.reset_schema, cls.reset_vreg)
   262             if not cnx._closed:
       
   263                 cnx.rollback()
       
   264                 cnx.close()
       
   265             self._cnxs.remove(cnx)
   270 
   266 
   271     # global resources accessors ###############################################
   267     # global resources accessors ###############################################
   272 
   268 
   273     @property
   269     @property
   274     def schema(self):
   270     def schema(self):
   306 
   302 
   307     # default test setup and teardown #########################################
   303     # default test setup and teardown #########################################
   308 
   304 
   309     def setUp(self):
   305     def setUp(self):
   310         # monkey patch send mail operation so emails are sent synchronously
   306         # monkey patch send mail operation so emails are sent synchronously
   311         self._old_mail_postcommit_event = SendMailOp.postcommit_event
   307         self._patch_SendMailOp()
   312         SendMailOp.postcommit_event = SendMailOp.sendmails
       
   313         pause_tracing()
   308         pause_tracing()
   314         previous_failure = self.__class__.__dict__.get('_repo_init_failed')
   309         previous_failure = self.__class__.__dict__.get('_repo_init_failed')
   315         if previous_failure is not None:
   310         if previous_failure is not None:
   316             self.skipTest('repository is not initialised: %r' % previous_failure)
   311             self.skipTest('repository is not initialised: %r' % previous_failure)
   317         try:
   312         try:
   318             self._init_repo()
   313             self._init_repo()
       
   314             self.addCleanup(self._close_cnx)
   319         except Exception, ex:
   315         except Exception, ex:
   320             self.__class__._repo_init_failed = ex
   316             self.__class__._repo_init_failed = ex
   321             raise
   317             raise
   322         resume_tracing()
   318         resume_tracing()
   323         self._cnxs = []
       
   324         self.setup_database()
   319         self.setup_database()
   325         self.commit()
   320         self.commit()
   326         MAILBOX[:] = [] # reset mailbox
   321         MAILBOX[:] = [] # reset mailbox
   327 
   322 
   328     def tearDown(self):
   323     def tearDown(self):
   329         if not self.cnx._closed:
   324         # XXX hack until logilab.common.testlib is fixed
   330             self.cnx.rollback()
   325         while self._cleanups:
   331         for cnx in self._cnxs:
   326             cleanup, args, kwargs = self._cleanups.pop(-1)
   332             if not cnx._closed:
   327             cleanup(*args, **kwargs)
   333                 cnx.close()
   328 
   334         SendMailOp.postcommit_event = self._old_mail_postcommit_event
   329     def _patch_SendMailOp(self):
       
   330         # monkey patch send mail operation so emails are sent synchronously
       
   331         _old_mail_postcommit_event = SendMailOp.postcommit_event
       
   332         SendMailOp.postcommit_event = SendMailOp.sendmails
       
   333         def reverse_SendMailOp_monkey_patch():
       
   334             SendMailOp.postcommit_event = _old_mail_postcommit_event
       
   335         self.addCleanup(reverse_SendMailOp_monkey_patch)
   335 
   336 
   336     def setup_database(self):
   337     def setup_database(self):
   337         """add your database setup code by overriding this method"""
   338         """add your database setup code by overriding this method"""
       
   339 
       
   340     @classmethod
       
   341     def pre_setup_database(cls, session, config):
       
   342         """add your pre database setup code by overriding this method
       
   343 
       
   344         Do not forget to set the cls.test_db_id value to enable caching of the
       
   345         result.
       
   346         """
   338 
   347 
   339     # user / session management ###############################################
   348     # user / session management ###############################################
   340 
   349 
   341     def user(self, req=None):
   350     def user(self, req=None):
   342         """return the application schema"""
   351         """return the application schema"""
   370             # definitly don't want autoclose when used as a context manager
   379             # definitly don't want autoclose when used as a context manager
   371             return self.cnx
   380             return self.cnx
   372         autoclose = kwargs.pop('autoclose', True)
   381         autoclose = kwargs.pop('autoclose', True)
   373         if not kwargs:
   382         if not kwargs:
   374             kwargs['password'] = str(login)
   383             kwargs['password'] = str(login)
   375         self.cnx = repo_connect(self.repo, unicode(login), **kwargs)
   384         self.set_cnx(repo_connect(self.repo, unicode(login), **kwargs))
   376         self.websession = DBAPISession(self.cnx)
   385         self.websession = DBAPISession(self.cnx)
   377         self._cnxs.append(self.cnx)
       
   378         if login == self.vreg.config.anonymous_user()[0]:
   386         if login == self.vreg.config.anonymous_user()[0]:
   379             self.cnx.anonymous_connection = True
   387             self.cnx.anonymous_connection = True
   380         if autoclose:
   388         if autoclose:
   381             return TestCaseConnectionProxy(self, self.cnx)
   389             return TestCaseConnectionProxy(self, self.cnx)
   382         return self.cnx
   390         return self.cnx
   383 
   391 
   384     def restore_connection(self):
   392     def restore_connection(self):
   385         if not self.cnx is self._orig_cnx[0]:
   393         if not self.cnx is self._orig_cnx[0]:
   386             if not self.cnx._closed:
   394             if not self.cnx._closed:
   387                 self.cnx.close()
   395                 self.cnx.close()
   388             try:
   396         cnx, self.websession = self._orig_cnx
   389                 self._cnxs.remove(self.cnx)
   397         self.set_cnx(cnx)
   390             except ValueError:
       
   391                 pass
       
   392         self.cnx, self.websession = self._orig_cnx
       
   393 
   398 
   394     # db api ##################################################################
   399     # db api ##################################################################
   395 
   400 
   396     @nocoverage
   401     @nocoverage
   397     def cursor(self, req=None):
   402     def cursor(self, req=None):
   951 
   956 
   952 class AutoPopulateTest(CubicWebTC):
   957 class AutoPopulateTest(CubicWebTC):
   953     """base class for test with auto-populating of the database"""
   958     """base class for test with auto-populating of the database"""
   954     __abstract__ = True
   959     __abstract__ = True
   955 
   960 
       
   961     test_db_id = 'autopopulate'
       
   962 
   956     tags = CubicWebTC.tags | Tags('autopopulated')
   963     tags = CubicWebTC.tags | Tags('autopopulated')
   957 
   964 
   958     pdbclass = CubicWebDebugger
   965     pdbclass = CubicWebDebugger
   959     # this is a hook to be able to define a list of rql queries
   966     # this is a hook to be able to define a list of rql queries
   960     # that are application dependent and cannot be guessed automatically
   967     # that are application dependent and cannot be guessed automatically
  1084     """import this if you wan automatic tests to be ran"""
  1091     """import this if you wan automatic tests to be ran"""
  1085 
  1092 
  1086     tags = AutoPopulateTest.tags | Tags('web', 'generated')
  1093     tags = AutoPopulateTest.tags | Tags('web', 'generated')
  1087 
  1094 
  1088     def setUp(self):
  1095     def setUp(self):
  1089         AutoPopulateTest.setUp(self)
  1096         assert not self.__class__ is AutomaticWebTest, 'Please subclass AutomaticWebTest to pprevent database caching issue'
       
  1097         super(AutomaticWebTest, self).setUp()
       
  1098 
  1090         # access to self.app for proper initialization of the authentication
  1099         # access to self.app for proper initialization of the authentication
  1091         # machinery (else some views may fail)
  1100         # machinery (else some views may fail)
  1092         self.app
  1101         self.app
  1093 
  1102 
  1094     ## one each
  1103     ## one each