devtools/testlib.py
branchstable
changeset 7088 76e0dba5f8f3
parent 7075 4751d77394b1
parent 7078 bad26a22fe29
child 7163 d6d905d0344f
equal deleted inserted replaced
7087:376314ebf273 7088:76e0dba5f8f3
    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         cls._orig_cnx = (cls.cnx, cls.websession)
   256         cls._orig_cnx = (cnx, cls.websession)
   264         cls.config.repository = lambda x=None: cls.repo
   257         cls.config.repository = lambda x=None: cls.repo
   265 
   258 
   266     @classmethod
   259     def _close_cnx(self):
   267     def _refresh_repo(cls):
   260         for cnx in list(self._cnxs):
   268         refresh_repo(cls.repo, cls.reset_schema, cls.reset_vreg)
   261             if not cnx._closed:
       
   262                 cnx.rollback()
       
   263                 cnx.close()
       
   264             self._cnxs.remove(cnx)
   269 
   265 
   270     # global resources accessors ###############################################
   266     # global resources accessors ###############################################
   271 
   267 
   272     @property
   268     @property
   273     def schema(self):
   269     def schema(self):
   305 
   301 
   306     # default test setup and teardown #########################################
   302     # default test setup and teardown #########################################
   307 
   303 
   308     def setUp(self):
   304     def setUp(self):
   309         # monkey patch send mail operation so emails are sent synchronously
   305         # monkey patch send mail operation so emails are sent synchronously
   310         self._old_mail_postcommit_event = SendMailOp.postcommit_event
   306         self._patch_SendMailOp()
   311         SendMailOp.postcommit_event = SendMailOp.sendmails
       
   312         pause_tracing()
   307         pause_tracing()
   313         previous_failure = self.__class__.__dict__.get('_repo_init_failed')
   308         previous_failure = self.__class__.__dict__.get('_repo_init_failed')
   314         if previous_failure is not None:
   309         if previous_failure is not None:
   315             self.skipTest('repository is not initialised: %r' % previous_failure)
   310             self.skipTest('repository is not initialised: %r' % previous_failure)
   316         try:
   311         try:
   317             self._init_repo()
   312             self._init_repo()
       
   313             self.addCleanup(self._close_cnx)
   318         except Exception, ex:
   314         except Exception, ex:
   319             self.__class__._repo_init_failed = ex
   315             self.__class__._repo_init_failed = ex
   320             raise
   316             raise
   321         resume_tracing()
   317         resume_tracing()
   322         self._cnxs = []
       
   323         self.setup_database()
   318         self.setup_database()
   324         self.commit()
   319         self.commit()
   325         MAILBOX[:] = [] # reset mailbox
   320         MAILBOX[:] = [] # reset mailbox
   326 
   321 
   327     def tearDown(self):
   322     def tearDown(self):
   328         if not self.cnx._closed:
   323         # XXX hack until logilab.common.testlib is fixed
   329             self.cnx.rollback()
   324         while self._cleanups:
   330         for cnx in self._cnxs:
   325             cleanup, args, kwargs = self._cleanups.pop(-1)
   331             if not cnx._closed:
   326             cleanup(*args, **kwargs)
   332                 cnx.close()
   327 
   333         SendMailOp.postcommit_event = self._old_mail_postcommit_event
   328     def _patch_SendMailOp(self):
       
   329         # monkey patch send mail operation so emails are sent synchronously
       
   330         _old_mail_postcommit_event = SendMailOp.postcommit_event
       
   331         SendMailOp.postcommit_event = SendMailOp.sendmails
       
   332         def reverse_SendMailOp_monkey_patch():
       
   333             SendMailOp.postcommit_event = _old_mail_postcommit_event
       
   334         self.addCleanup(reverse_SendMailOp_monkey_patch)
   334 
   335 
   335     def setup_database(self):
   336     def setup_database(self):
   336         """add your database setup code by overriding this method"""
   337         """add your database setup code by overriding this method"""
       
   338 
       
   339     @classmethod
       
   340     def pre_setup_database(cls, session, config):
       
   341         """add your pre database setup code by overriding this method
       
   342 
       
   343         Do not forget to set the cls.test_db_id value to enable caching of the
       
   344         result.
       
   345         """
   337 
   346 
   338     # user / session management ###############################################
   347     # user / session management ###############################################
   339 
   348 
   340     def user(self, req=None):
   349     def user(self, req=None):
   341         """return the application schema"""
   350         """return the application schema"""
   369             # definitly don't want autoclose when used as a context manager
   378             # definitly don't want autoclose when used as a context manager
   370             return self.cnx
   379             return self.cnx
   371         autoclose = kwargs.pop('autoclose', True)
   380         autoclose = kwargs.pop('autoclose', True)
   372         if not kwargs:
   381         if not kwargs:
   373             kwargs['password'] = str(login)
   382             kwargs['password'] = str(login)
   374         self.cnx = repo_connect(self.repo, unicode(login), **kwargs)
   383         self.set_cnx(repo_connect(self.repo, unicode(login), **kwargs))
   375         self.websession = DBAPISession(self.cnx)
   384         self.websession = DBAPISession(self.cnx)
   376         self._cnxs.append(self.cnx)
       
   377         if login == self.vreg.config.anonymous_user()[0]:
   385         if login == self.vreg.config.anonymous_user()[0]:
   378             self.cnx.anonymous_connection = True
   386             self.cnx.anonymous_connection = True
   379         if autoclose:
   387         if autoclose:
   380             return TestCaseConnectionProxy(self, self.cnx)
   388             return TestCaseConnectionProxy(self, self.cnx)
   381         return self.cnx
   389         return self.cnx
   382 
   390 
   383     def restore_connection(self):
   391     def restore_connection(self):
   384         if not self.cnx is self._orig_cnx[0]:
   392         if not self.cnx is self._orig_cnx[0]:
   385             if not self.cnx._closed:
   393             if not self.cnx._closed:
   386                 self.cnx.close()
   394                 self.cnx.close()
   387             try:
   395         cnx, self.websession = self._orig_cnx
   388                 self._cnxs.remove(self.cnx)
   396         self.set_cnx(cnx)
   389             except ValueError:
       
   390                 pass
       
   391         self.cnx, self.websession = self._orig_cnx
       
   392 
   397 
   393     # db api ##################################################################
   398     # db api ##################################################################
   394 
   399 
   395     @nocoverage
   400     @nocoverage
   396     def cursor(self, req=None):
   401     def cursor(self, req=None):
   952 
   957 
   953 class AutoPopulateTest(CubicWebTC):
   958 class AutoPopulateTest(CubicWebTC):
   954     """base class for test with auto-populating of the database"""
   959     """base class for test with auto-populating of the database"""
   955     __abstract__ = True
   960     __abstract__ = True
   956 
   961 
       
   962     test_db_id = 'autopopulate'
       
   963 
   957     tags = CubicWebTC.tags | Tags('autopopulated')
   964     tags = CubicWebTC.tags | Tags('autopopulated')
   958 
   965 
   959     pdbclass = CubicWebDebugger
   966     pdbclass = CubicWebDebugger
   960     # this is a hook to be able to define a list of rql queries
   967     # this is a hook to be able to define a list of rql queries
   961     # that are application dependent and cannot be guessed automatically
   968     # that are application dependent and cannot be guessed automatically
  1085     """import this if you wan automatic tests to be ran"""
  1092     """import this if you wan automatic tests to be ran"""
  1086 
  1093 
  1087     tags = AutoPopulateTest.tags | Tags('web', 'generated')
  1094     tags = AutoPopulateTest.tags | Tags('web', 'generated')
  1088 
  1095 
  1089     def setUp(self):
  1096     def setUp(self):
  1090         AutoPopulateTest.setUp(self)
  1097         assert not self.__class__ is AutomaticWebTest, 'Please subclass AutomaticWebTest to pprevent database caching issue'
       
  1098         super(AutomaticWebTest, self).setUp()
       
  1099 
  1091         # access to self.app for proper initialization of the authentication
  1100         # access to self.app for proper initialization of the authentication
  1092         # machinery (else some views may fail)
  1101         # machinery (else some views may fail)
  1093         self.app
  1102         self.app
  1094 
  1103 
  1095     ## one each
  1104     ## one each