devtools/testlib.py
changeset 10974 6557833657d6
parent 10937 eb05348b0e2d
child 10997 da712d3f0601
equal deleted inserted replaced
10973:0939ad2edf63 10974:6557833657d6
    16 # You should have received a copy of the GNU Lesser General Public License along
    16 # You should have received a copy of the GNU Lesser General Public License along
    17 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
    17 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
    18 """this module contains base classes and utilities for cubicweb tests"""
    18 """this module contains base classes and utilities for cubicweb tests"""
    19 from __future__ import print_function
    19 from __future__ import print_function
    20 
    20 
    21 __docformat__ = "restructuredtext en"
       
    22 
       
    23 import sys
    21 import sys
    24 import re
    22 import re
    25 from os.path import dirname, join, abspath
    23 from os.path import dirname, join, abspath
    26 from math import log
    24 from math import log
    27 from contextlib import contextmanager
    25 from contextlib import contextmanager
    28 from warnings import warn
       
    29 from itertools import chain
    26 from itertools import chain
    30 
    27 
    31 from six import text_type, string_types
    28 from six import text_type, string_types
    32 from six.moves import range
    29 from six.moves import range
    33 from six.moves.urllib.parse import urlparse, parse_qs, unquote as urlunquote
    30 from six.moves.urllib.parse import urlparse, parse_qs, unquote as urlunquote
    41 from logilab.common.decorators import cached, classproperty, clear_cache, iclassmethod
    38 from logilab.common.decorators import cached, classproperty, clear_cache, iclassmethod
    42 from logilab.common.deprecation import deprecated, class_deprecated
    39 from logilab.common.deprecation import deprecated, class_deprecated
    43 from logilab.common.shellutils import getlogin
    40 from logilab.common.shellutils import getlogin
    44 
    41 
    45 from cubicweb import (ValidationError, NoSelectableObject, AuthenticationError,
    42 from cubicweb import (ValidationError, NoSelectableObject, AuthenticationError,
    46                       ProgrammingError, BadConnectionId)
    43                       BadConnectionId)
    47 from cubicweb import cwconfig, devtools, web, server, repoapi
    44 from cubicweb import cwconfig, devtools, web, server, repoapi
    48 from cubicweb.utils import json
    45 from cubicweb.utils import json
    49 from cubicweb.sobjects import notification
    46 from cubicweb.sobjects import notification
    50 from cubicweb.web import Redirect, application, eid_param
    47 from cubicweb.web import Redirect, application, eid_param
    51 from cubicweb.server.hook import SendMailOp
    48 from cubicweb.server.hook import SendMailOp
    52 from cubicweb.server.session import Session
    49 from cubicweb.server.session import Session
    53 from cubicweb.devtools import SYSTEM_ENTITIES, SYSTEM_RELATIONS, VIEW_VALIDATORS
    50 from cubicweb.devtools import SYSTEM_ENTITIES, SYSTEM_RELATIONS, VIEW_VALIDATORS
    54 from cubicweb.devtools import fake, htmlparser, DEFAULT_EMPTY_DB_ID
    51 from cubicweb.devtools import fake, htmlparser, DEFAULT_EMPTY_DB_ID
    55 from cubicweb.utils import json
    52 
    56 
    53 
    57 # low-level utilities ##########################################################
    54 # low-level utilities ##########################################################
    58 
    55 
    59 class CubicWebDebugger(Debugger):
    56 class CubicWebDebugger(Debugger):
    60     """special debugger class providing a 'view' function which saves some
    57     """special debugger class providing a 'view' function which saves some
    65         data = self._getval(arg)
    62         data = self._getval(arg)
    66         with open('/tmp/toto.html', 'w') as toto:
    63         with open('/tmp/toto.html', 'w') as toto:
    67             toto.write(data)
    64             toto.write(data)
    68         webbrowser.open('file:///tmp/toto.html')
    65         webbrowser.open('file:///tmp/toto.html')
    69 
    66 
       
    67 
    70 def line_context_filter(line_no, center, before=3, after=None):
    68 def line_context_filter(line_no, center, before=3, after=None):
    71     """return true if line are in context
    69     """return true if line are in context
    72 
    70 
    73     if after is None: after = before
    71     if after is None: after = before
    74     """
    72     """
    75     if after is None:
    73     if after is None:
    76         after = before
    74         after = before
    77     return center - before <= line_no <= center + after
    75     return center - before <= line_no <= center + after
       
    76 
    78 
    77 
    79 def unprotected_entities(schema, strict=False):
    78 def unprotected_entities(schema, strict=False):
    80     """returned a set of each non final entity type, excluding "system" entities
    79     """returned a set of each non final entity type, excluding "system" entities
    81     (eg CWGroup, CWUser...)
    80     (eg CWGroup, CWUser...)
    82     """
    81     """
    84         protected_entities = yams.schema.BASE_TYPES
    83         protected_entities = yams.schema.BASE_TYPES
    85     else:
    84     else:
    86         protected_entities = yams.schema.BASE_TYPES.union(SYSTEM_ENTITIES)
    85         protected_entities = yams.schema.BASE_TYPES.union(SYSTEM_ENTITIES)
    87     return set(schema.entities()) - protected_entities
    86     return set(schema.entities()) - protected_entities
    88 
    87 
       
    88 
    89 class JsonValidator(object):
    89 class JsonValidator(object):
    90     def parse_string(self, data):
    90     def parse_string(self, data):
    91         return json.loads(data.decode('ascii'))
    91         return json.loads(data.decode('ascii'))
       
    92 
    92 
    93 
    93 @contextmanager
    94 @contextmanager
    94 def real_error_handling(app):
    95 def real_error_handling(app):
    95     """By default, CubicWebTC `app` attribute (ie the publisher) is monkey
    96     """By default, CubicWebTC `app` attribute (ie the publisher) is monkey
    96     patched so that unexpected error are raised rather than going through the
    97     patched so that unexpected error are raised rather than going through the
   110     # return the app
   111     # return the app
   111     yield app
   112     yield app
   112     # restore
   113     # restore
   113     app.error_handler = fake_error_handler
   114     app.error_handler = fake_error_handler
   114 
   115 
       
   116 
   115 # email handling, to test emails sent by an application ########################
   117 # email handling, to test emails sent by an application ########################
   116 
   118 
   117 MAILBOX = []
   119 MAILBOX = []
       
   120 
   118 
   121 
   119 class Email(object):
   122 class Email(object):
   120     """you'll get instances of Email into MAILBOX during tests that trigger
   123     """you'll get instances of Email into MAILBOX during tests that trigger
   121     some notification.
   124     some notification.
   122 
   125 
   144 
   147 
   145     def __repr__(self):
   148     def __repr__(self):
   146         return '<Email to %s with subject %s>' % (','.join(self.recipients),
   149         return '<Email to %s with subject %s>' % (','.join(self.recipients),
   147                                                   self.message.get('Subject'))
   150                                                   self.message.get('Subject'))
   148 
   151 
       
   152 
   149 # the trick to get email into MAILBOX instead of actually sent: monkey patch
   153 # the trick to get email into MAILBOX instead of actually sent: monkey patch
   150 # cwconfig.SMTP object
   154 # cwconfig.SMTP object
   151 class MockSMTP:
   155 class MockSMTP:
       
   156 
   152     def __init__(self, server, port):
   157     def __init__(self, server, port):
   153         pass
   158         pass
       
   159 
   154     def close(self):
   160     def close(self):
   155         pass
   161         pass
       
   162 
   156     def sendmail(self, fromaddr, recipients, msg):
   163     def sendmail(self, fromaddr, recipients, msg):
   157         MAILBOX.append(Email(fromaddr, recipients, msg))
   164         MAILBOX.append(Email(fromaddr, recipients, msg))
   158 
   165 
   159 cwconfig.SMTP = MockSMTP
   166 cwconfig.SMTP = MockSMTP
   160 
   167 
   244                                         schema=1)
   251                                         schema=1)
   245             yield mih
   252             yield mih
   246             cnx.commit()
   253             cnx.commit()
   247 
   254 
   248 
   255 
   249 
       
   250 # base class for cubicweb tests requiring a full cw environments ###############
   256 # base class for cubicweb tests requiring a full cw environments ###############
   251 
   257 
   252 class CubicWebTC(TestCase):
   258 class CubicWebTC(TestCase):
   253     """abstract class for test using an apptest environment
   259     """abstract class for test using an apptest environment
   254 
   260 
   294     def _close_access(self):
   300     def _close_access(self):
   295         while self._open_access:
   301         while self._open_access:
   296             try:
   302             try:
   297                 self._open_access.pop().close()
   303                 self._open_access.pop().close()
   298             except BadConnectionId:
   304             except BadConnectionId:
   299                 continue # already closed
   305                 continue  # already closed
   300 
   306 
   301     @property
   307     @property
   302     def session(self):
   308     def session(self):
   303         """return admin session"""
   309         """return admin session"""
   304         return self._admin_session
   310         return self._admin_session
   305 
   311 
   306     #XXX this doesn't need to a be classmethod anymore
   312     # XXX this doesn't need to a be classmethod anymore
   307     def _init_repo(self):
   313     def _init_repo(self):
   308         """init the repository and connection to it.
   314         """init the repository and connection to it.
   309         """
   315         """
   310         # get or restore and working db.
   316         # get or restore and working db.
   311         db_handler = devtools.get_test_db_handler(self.config, self.init_config)
   317         db_handler = devtools.get_test_db_handler(self.config, self.init_config)
   314         self.repo = db_handler.get_repo(startup=True)
   320         self.repo = db_handler.get_repo(startup=True)
   315         # get an admin session (without actual login)
   321         # get an admin session (without actual login)
   316         login = text_type(db_handler.config.default_admin_config['login'])
   322         login = text_type(db_handler.config.default_admin_config['login'])
   317         self.admin_access = self.new_access(login)
   323         self.admin_access = self.new_access(login)
   318         self._admin_session = self.admin_access._session
   324         self._admin_session = self.admin_access._session
   319 
       
   320 
   325 
   321     # config management ########################################################
   326     # config management ########################################################
   322 
   327 
   323     @classproperty
   328     @classproperty
   324     def config(cls):
   329     def config(cls):
   336             home = abspath(join(dirname(sys.modules[cls.__module__].__file__), cls.appid))
   341             home = abspath(join(dirname(sys.modules[cls.__module__].__file__), cls.appid))
   337             config = cls._config = cls.configcls(cls.appid, apphome=home)
   342             config = cls._config = cls.configcls(cls.appid, apphome=home)
   338             config.mode = 'test'
   343             config.mode = 'test'
   339             return config
   344             return config
   340 
   345 
   341     @classmethod # XXX could be turned into a regular method
   346     @classmethod  # XXX could be turned into a regular method
   342     def init_config(cls, config):
   347     def init_config(cls, config):
   343         """configuration initialization hooks.
   348         """configuration initialization hooks.
   344 
   349 
   345         You may only want to override here the configuraton logic.
   350         You may only want to override here the configuraton logic.
   346 
   351 
   352         """
   357         """
   353         admincfg = config.default_admin_config
   358         admincfg = config.default_admin_config
   354         cls.admlogin = text_type(admincfg['login'])
   359         cls.admlogin = text_type(admincfg['login'])
   355         cls.admpassword = admincfg['password']
   360         cls.admpassword = admincfg['password']
   356         # uncomment the line below if you want rql queries to be logged
   361         # uncomment the line below if you want rql queries to be logged
   357         #config.global_set_option('query-log-file',
   362         # config.global_set_option('query-log-file',
   358         #                         '/tmp/test_rql_log.' + `os.getpid()`)
   363         #                          '/tmp/test_rql_log.' + `os.getpid()`)
   359         config.global_set_option('log-file', None)
   364         config.global_set_option('log-file', None)
   360         # set default-dest-addrs to a dumb email address to avoid mailbox or
   365         # set default-dest-addrs to a dumb email address to avoid mailbox or
   361         # mail queue pollution
   366         # mail queue pollution
   362         config.global_set_option('default-dest-addrs', ['whatever'])
   367         config.global_set_option('default-dest-addrs', ['whatever'])
   363         send_to =  '%s@logilab.fr' % getlogin()
   368         send_to = '%s@logilab.fr' % getlogin()
   364         config.global_set_option('sender-addr', send_to)
   369         config.global_set_option('sender-addr', send_to)
   365         config.global_set_option('default-dest-addrs', send_to)
   370         config.global_set_option('default-dest-addrs', send_to)
   366         config.global_set_option('sender-name', 'cubicweb-test')
   371         config.global_set_option('sender-name', 'cubicweb-test')
   367         config.global_set_option('sender-addr', 'cubicweb-test@logilab.fr')
   372         config.global_set_option('sender-addr', 'cubicweb-test@logilab.fr')
   368         # default_base_url on config class isn't enough for TestServerConfiguration
   373         # default_base_url on config class isn't enough for TestServerConfiguration
   369         config.global_set_option('base-url', config.default_base_url())
   374         config.global_set_option('base-url', config.default_base_url())
   370         # web resources
   375         # web resources
   371         try:
   376         try:
   372             config.global_set_option('embed-allowed', re.compile('.*'))
   377             config.global_set_option('embed-allowed', re.compile('.*'))
   373         except Exception: # not in server only configuration
   378         except Exception:  # not in server only configuration
   374             pass
   379             pass
   375 
   380 
   376     @property
   381     @property
   377     def vreg(self):
   382     def vreg(self):
   378         return self.repo.vreg
   383         return self.repo.vreg
   379 
       
   380 
   384 
   381     # global resources accessors ###############################################
   385     # global resources accessors ###############################################
   382 
   386 
   383     @property
   387     @property
   384     def schema(self):
   388     def schema(self):
   409                 self.__class__._repo_init_failed = ex
   413                 self.__class__._repo_init_failed = ex
   410                 raise
   414                 raise
   411             self.addCleanup(self._close_access)
   415             self.addCleanup(self._close_access)
   412         self.config.set_anonymous_allowed(self.anonymous_allowed)
   416         self.config.set_anonymous_allowed(self.anonymous_allowed)
   413         self.setup_database()
   417         self.setup_database()
   414         MAILBOX[:] = [] # reset mailbox
   418         MAILBOX[:] = []  # reset mailbox
   415 
   419 
   416     def tearDown(self):
   420     def tearDown(self):
   417         # XXX hack until logilab.common.testlib is fixed
   421         # XXX hack until logilab.common.testlib is fixed
   418         if self._admin_session is not None:
   422         if self._admin_session is not None:
   419             self.repo.close(self._admin_session.sessionid)
   423             self.repo.close(self._admin_session.sessionid)
   425 
   429 
   426     def _patch_SendMailOp(self):
   430     def _patch_SendMailOp(self):
   427         # monkey patch send mail operation so emails are sent synchronously
   431         # monkey patch send mail operation so emails are sent synchronously
   428         _old_mail_postcommit_event = SendMailOp.postcommit_event
   432         _old_mail_postcommit_event = SendMailOp.postcommit_event
   429         SendMailOp.postcommit_event = SendMailOp.sendmails
   433         SendMailOp.postcommit_event = SendMailOp.sendmails
       
   434 
   430         def reverse_SendMailOp_monkey_patch():
   435         def reverse_SendMailOp_monkey_patch():
   431             SendMailOp.postcommit_event = _old_mail_postcommit_event
   436             SendMailOp.postcommit_event = _old_mail_postcommit_event
       
   437 
   432         self.addCleanup(reverse_SendMailOp_monkey_patch)
   438         self.addCleanup(reverse_SendMailOp_monkey_patch)
   433 
   439 
   434     def setup_database(self):
   440     def setup_database(self):
   435         """add your database setup code by overriding this method"""
   441         """add your database setup code by overriding this method"""
   436 
   442 
   450         if req is None:
   456         if req is None:
   451             return self.request().user
   457             return self.request().user
   452         else:
   458         else:
   453             return req.user
   459             return req.user
   454 
   460 
   455     @iclassmethod # XXX turn into a class method
   461     @iclassmethod  # XXX turn into a class method
   456     def create_user(self, req, login=None, groups=('users',), password=None,
   462     def create_user(self, req, login=None, groups=('users',), password=None,
   457                     email=None, commit=True, **kwargs):
   463                     email=None, commit=True, **kwargs):
   458         """create and return a new user entity"""
   464         """create and return a new user entity"""
   459         if password is None:
   465         if password is None:
   460             password = login
   466             password = login
   469             req.create_entity('EmailAddress', address=text_type(email),
   475             req.create_entity('EmailAddress', address=text_type(email),
   470                               reverse_primary_email=user)
   476                               reverse_primary_email=user)
   471         user.cw_clear_relation_cache('in_group', 'subject')
   477         user.cw_clear_relation_cache('in_group', 'subject')
   472         if commit:
   478         if commit:
   473             try:
   479             try:
   474                 req.commit() # req is a session
   480                 req.commit()  # req is a session
   475             except AttributeError:
   481             except AttributeError:
   476                 req.cnx.commit()
   482                 req.cnx.commit()
   477         return user
   483         return user
   478 
       
   479 
   484 
   480     # other utilities #########################################################
   485     # other utilities #########################################################
   481 
   486 
   482     @contextmanager
   487     @contextmanager
   483     def temporary_appobjects(self, *appobjects):
   488     def temporary_appobjects(self, *appobjects):
   553     def assertPossibleTransitions(self, entity, expected):
   558     def assertPossibleTransitions(self, entity, expected):
   554         transitions = entity.cw_adapt_to('IWorkflowable').possible_transitions()
   559         transitions = entity.cw_adapt_to('IWorkflowable').possible_transitions()
   555         self.assertListEqual(sorted(tr.name for tr in transitions),
   560         self.assertListEqual(sorted(tr.name for tr in transitions),
   556                              sorted(expected))
   561                              sorted(expected))
   557 
   562 
   558 
       
   559     # views and actions registries inspection ##################################
   563     # views and actions registries inspection ##################################
   560 
   564 
   561     def pviews(self, req, rset):
   565     def pviews(self, req, rset):
   562         return sorted((a.__regid__, a.__class__)
   566         return sorted((a.__regid__, a.__class__)
   563                       for a in self.vreg['views'].possible_views(req, rset=rset))
   567                       for a in self.vreg['views'].possible_views(req, rset=rset))
   589     def _test_action(self, action):
   593     def _test_action(self, action):
   590         class fake_menu(list):
   594         class fake_menu(list):
   591             @property
   595             @property
   592             def items(self):
   596             def items(self):
   593                 return self
   597                 return self
       
   598 
   594         class fake_box(object):
   599         class fake_box(object):
   595             def action_link(self, action, **kwargs):
   600             def action_link(self, action, **kwargs):
   596                 return (action.title, action.url())
   601                 return (action.title, action.url())
   597         submenu = fake_menu()
   602         submenu = fake_menu()
   598         action.fill_menu(fake_box(), submenu)
   603         action.fill_menu(fake_box(), submenu)
   615                      and not isinstance(view, class_deprecated)]
   620                      and not isinstance(view, class_deprecated)]
   616             if views:
   621             if views:
   617                 try:
   622                 try:
   618                     view = viewsvreg._select_best(views, req, rset=rset)
   623                     view = viewsvreg._select_best(views, req, rset=rset)
   619                     if view is None:
   624                     if view is None:
   620                         raise NoSelectableObject((req,), {'rset':rset}, views)
   625                         raise NoSelectableObject((req,), {'rset': rset}, views)
   621                     if view.linkable():
   626                     if view.linkable():
   622                         yield view
   627                         yield view
   623                     else:
   628                     else:
   624                         not_selected(self.vreg, view)
   629                         not_selected(self.vreg, view)
   625                     # else the view is expected to be used as subview and should
   630                     # else the view is expected to be used as subview and should
   646                 if view.category == 'startupview':
   651                 if view.category == 'startupview':
   647                     yield view.__regid__
   652                     yield view.__regid__
   648                 else:
   653                 else:
   649                     not_selected(self.vreg, view)
   654                     not_selected(self.vreg, view)
   650 
   655 
   651 
       
   652     # web ui testing utilities #################################################
   656     # web ui testing utilities #################################################
   653 
   657 
   654     @property
   658     @property
   655     @cached
   659     @cached
   656     def app(self):
   660     def app(self):
   657         """return a cubicweb publisher"""
   661         """return a cubicweb publisher"""
   658         publisher = application.CubicWebPublisher(self.repo, self.config)
   662         publisher = application.CubicWebPublisher(self.repo, self.config)
       
   663 
   659         def raise_error_handler(*args, **kwargs):
   664         def raise_error_handler(*args, **kwargs):
   660             raise
   665             raise
       
   666 
   661         publisher.error_handler = raise_error_handler
   667         publisher.error_handler = raise_error_handler
   662         return publisher
   668         return publisher
   663 
   669 
   664     @deprecated('[3.19] use the .remote_calling method')
   670     @deprecated('[3.19] use the .remote_calling method')
   665     def remote_call(self, fname, *args):
   671     def remote_call(self, fname, *args):
   707 
   713 
   708         * `entity_field_dicts`, list of (entity, dictionary) where dictionary contains name:value
   714         * `entity_field_dicts`, list of (entity, dictionary) where dictionary contains name:value
   709           for fields that are not tied to the given entity
   715           for fields that are not tied to the given entity
   710         """
   716         """
   711         assert field_dict or entity_field_dicts, \
   717         assert field_dict or entity_field_dicts, \
   712                 'field_dict and entity_field_dicts arguments must not be both unspecified'
   718             'field_dict and entity_field_dicts arguments must not be both unspecified'
   713         if field_dict is None:
   719         if field_dict is None:
   714             field_dict = {}
   720             field_dict = {}
   715         form = {'__form_id': formid}
   721         form = {'__form_id': formid}
   716         fields = []
   722         fields = []
   717         for field, value in field_dict.items():
   723         for field, value in field_dict.items():
   718             fields.append(field)
   724             fields.append(field)
   719             form[field] = value
   725             form[field] = value
       
   726 
   720         def _add_entity_field(entity, field, value):
   727         def _add_entity_field(entity, field, value):
   721             entity_fields.append(field)
   728             entity_fields.append(field)
   722             form[eid_param(field, entity.eid)] = value
   729             form[eid_param(field, entity.eid)] = value
       
   730 
   723         for entity, field_dict in entity_field_dicts:
   731         for entity, field_dict in entity_field_dicts:
   724             if '__maineid' not in form:
   732             if '__maineid' not in form:
   725                 form['__maineid'] = entity.eid
   733                 form['__maineid'] = entity.eid
   726             entity_fields = []
   734             entity_fields = []
   727             form.setdefault('eid', []).append(entity.eid)
   735             form.setdefault('eid', []).append(entity.eid)
   740 
   748 
   741         req.form will be setup using the url's query string
   749         req.form will be setup using the url's query string
   742         """
   750         """
   743         req = self.request(url=url)
   751         req = self.request(url=url)
   744         if isinstance(url, unicode):
   752         if isinstance(url, unicode):
   745             url = url.encode(req.encoding) # req.setup_params() expects encoded strings
   753             url = url.encode(req.encoding)  # req.setup_params() expects encoded strings
   746         querystring = urlparse(url)[-2]
   754         querystring = urlparse(url)[-2]
   747         params = parse_qs(querystring)
   755         params = parse_qs(querystring)
   748         req.setup_params(params)
   756         req.setup_params(params)
   749         return req
   757         return req
   750 
   758 
   754 
   762 
   755         req.form will be setup using the url's query string
   763         req.form will be setup using the url's query string
   756         """
   764         """
   757         with self.admin_access.web_request(url=url) as req:
   765         with self.admin_access.web_request(url=url) as req:
   758             if isinstance(url, unicode):
   766             if isinstance(url, unicode):
   759                 url = url.encode(req.encoding) # req.setup_params() expects encoded strings
   767                 url = url.encode(req.encoding)  # req.setup_params() expects encoded strings
   760             querystring = urlparse(url)[-2]
   768             querystring = urlparse(url)[-2]
   761             params = parse_qs(querystring)
   769             params = parse_qs(querystring)
   762             req.setup_params(params)
   770             req.setup_params(params)
   763             yield req
   771             yield req
   764 
   772 
   797             path = location
   805             path = location
   798             params = {}
   806             params = {}
   799         else:
   807         else:
   800             cleanup = lambda p: (p[0], urlunquote(p[1]))
   808             cleanup = lambda p: (p[0], urlunquote(p[1]))
   801             params = dict(cleanup(p.split('=', 1)) for p in params.split('&') if p)
   809             params = dict(cleanup(p.split('=', 1)) for p in params.split('&') if p)
   802         if path.startswith(req.base_url()): # may be relative
   810         if path.startswith(req.base_url()):  # may be relative
   803             path = path[len(req.base_url()):]
   811             path = path[len(req.base_url()):]
   804         return path, params
   812         return path, params
   805 
   813 
   806     def expect_redirect(self, callback, req):
   814     def expect_redirect(self, callback, req):
   807         """call the given callback with req as argument, expecting to get a
   815         """call the given callback with req as argument, expecting to get a
   816 
   824 
   817     def expect_redirect_handle_request(self, req, path='edit'):
   825     def expect_redirect_handle_request(self, req, path='edit'):
   818         """call the publish method of the application publisher, expecting to
   826         """call the publish method of the application publisher, expecting to
   819         get a Redirect exception
   827         get a Redirect exception
   820         """
   828         """
   821         result = self.app_handle_request(req, path)
   829         self.app_handle_request(req, path)
   822         self.assertTrue(300 <= req.status_out <400, req.status_out)
   830         self.assertTrue(300 <= req.status_out < 400, req.status_out)
   823         location = req.get_response_header('location')
   831         location = req.get_response_header('location')
   824         return self._parse_location(req, location)
   832         return self._parse_location(req, location)
   825 
   833 
   826     @deprecated("[3.15] expect_redirect_handle_request is the new and better way"
   834     @deprecated("[3.15] expect_redirect_handle_request is the new and better way"
   827                 " (beware of small semantic changes)")
   835                 " (beware of small semantic changes)")
   828     def expect_redirect_publish(self, *args, **kwargs):
   836     def expect_redirect_publish(self, *args, **kwargs):
   829         return self.expect_redirect_handle_request(*args, **kwargs)
   837         return self.expect_redirect_handle_request(*args, **kwargs)
   830 
       
   831 
   838 
   832     def set_auth_mode(self, authmode, anonuser=None):
   839     def set_auth_mode(self, authmode, anonuser=None):
   833         self.set_option('auth-mode', authmode)
   840         self.set_option('auth-mode', authmode)
   834         self.set_option('anonymous-user', anonuser)
   841         self.set_option('anonymous-user', anonuser)
   835         if anonuser is None:
   842         if anonuser is None:
   875     content_type_validators = {
   882     content_type_validators = {
   876         # maps MIME type : validator name
   883         # maps MIME type : validator name
   877         #
   884         #
   878         # do not set html validators here, we need HTMLValidator for html
   885         # do not set html validators here, we need HTMLValidator for html
   879         # snippets
   886         # snippets
   880         #'text/html': DTDValidator,
   887         # 'text/html': DTDValidator,
   881         #'application/xhtml+xml': DTDValidator,
   888         # 'application/xhtml+xml': DTDValidator,
   882         'application/xml': htmlparser.XMLValidator,
   889         'application/xml': htmlparser.XMLValidator,
   883         'text/xml': htmlparser.XMLValidator,
   890         'text/xml': htmlparser.XMLValidator,
   884         'application/json': JsonValidator,
   891         'application/json': JsonValidator,
   885         'text/plain': None,
   892         'text/plain': None,
   886         'text/comma-separated-values': None,
   893         'text/comma-separated-values': None,
   889         'image/png': None,
   896         'image/png': None,
   890         }
   897         }
   891     # maps vid : validator name (override content_type_validators)
   898     # maps vid : validator name (override content_type_validators)
   892     vid_validators = dict((vid, htmlparser.VALMAP[valkey])
   899     vid_validators = dict((vid, htmlparser.VALMAP[valkey])
   893                           for vid, valkey in VIEW_VALIDATORS.items())
   900                           for vid, valkey in VIEW_VALIDATORS.items())
   894 
       
   895 
   901 
   896     def view(self, vid, rset=None, req=None, template='main-template',
   902     def view(self, vid, rset=None, req=None, template='main-template',
   897              **kwargs):
   903              **kwargs):
   898         """This method tests the view `vid` on `rset` using `template`
   904         """This method tests the view `vid` on `rset` using `template`
   899 
   905 
   919             self.set_description("testing vid=%s defined in %s with (%s)" % (
   925             self.set_description("testing vid=%s defined in %s with (%s)" % (
   920                 vid, view.__module__, rql))
   926                 vid, view.__module__, rql))
   921         else:
   927         else:
   922             self.set_description("testing vid=%s defined in %s without rset" % (
   928             self.set_description("testing vid=%s defined in %s without rset" % (
   923                 vid, view.__module__))
   929                 vid, view.__module__))
   924         if template is None: # raw view testing, no template
   930         if template is None:  # raw view testing, no template
   925             viewfunc = view.render
   931             viewfunc = view.render
   926         else:
   932         else:
   927             kwargs['view'] = view
   933             kwargs['view'] = view
   928             viewfunc = lambda **k: viewsreg.main_template(req, template,
   934             viewfunc = lambda **k: viewsreg.main_template(req, template,
   929                                                           rset=rset, **kwargs)
   935                                                           rset=rset, **kwargs)
   930         return self._test_view(viewfunc, view, template, kwargs)
   936         return self._test_view(viewfunc, view, template, kwargs)
   931 
       
   932 
   937 
   933     def _test_view(self, viewfunc, view, template='main-template', kwargs={}):
   938     def _test_view(self, viewfunc, view, template='main-template', kwargs={}):
   934         """this method does the actual call to the view
   939         """this method does the actual call to the view
   935 
   940 
   936         If no error occurred while rendering the view, the HTML is analyzed
   941         If no error occurred while rendering the view, the HTML is analyzed
   987         if isinstance(output, text_type):
   992         if isinstance(output, text_type):
   988             # XXX
   993             # XXX
   989             output = output.encode('utf-8')
   994             output = output.encode('utf-8')
   990         validator = self.get_validator(view, output=output)
   995         validator = self.get_validator(view, output=output)
   991         if validator is None:
   996         if validator is None:
   992             return output # return raw output if no validator is defined
   997             return output  # return raw output if no validator is defined
   993         if isinstance(validator, htmlparser.DTDValidator):
   998         if isinstance(validator, htmlparser.DTDValidator):
   994             # XXX remove <canvas> used in progress widget, unknown in html dtd
   999             # XXX remove <canvas> used in progress widget, unknown in html dtd
   995             output = re.sub('<canvas.*?></canvas>', '', output)
  1000             output = re.sub('<canvas.*?></canvas>', '', output)
   996         return self.assertWellFormed(validator, output.strip(), context= view.__regid__)
  1001         return self.assertWellFormed(validator, output.strip(), context=view.__regid__)
   997 
  1002 
   998     def assertWellFormed(self, validator, content, context=None):
  1003     def assertWellFormed(self, validator, content, context=None):
   999         try:
  1004         try:
  1000             return validator.parse_string(content)
  1005             return validator.parse_string(content)
  1001         except Exception:
  1006         except Exception:
  1067 
  1072 
  1068 from cubicweb.devtools.fill import insert_entity_queries, make_relations_queries
  1073 from cubicweb.devtools.fill import insert_entity_queries, make_relations_queries
  1069 
  1074 
  1070 # XXX cleanup unprotected_entities & all mess
  1075 # XXX cleanup unprotected_entities & all mess
  1071 
  1076 
       
  1077 
  1072 def how_many_dict(schema, cnx, how_many, skip):
  1078 def how_many_dict(schema, cnx, how_many, skip):
  1073     """given a schema, compute how many entities by type we need to be able to
  1079     """given a schema, compute how many entities by type we need to be able to
  1074     satisfy relations cardinality.
  1080     satisfy relations cardinality.
  1075 
  1081 
  1076     The `how_many` argument tells how many entities of which type we want at
  1082     The `how_many` argument tells how many entities of which type we want at
  1095                 relmap.setdefault((rschema, subj), []).append(str(obj))
  1101                 relmap.setdefault((rschema, subj), []).append(str(obj))
  1096             if card[1] in '1+' and card[0] in '1?':
  1102             if card[1] in '1+' and card[0] in '1?':
  1097                 # reverse subj and obj in the above explanation
  1103                 # reverse subj and obj in the above explanation
  1098                 relmap.setdefault((rschema, obj), []).append(str(subj))
  1104                 relmap.setdefault((rschema, obj), []).append(str(subj))
  1099     unprotected = unprotected_entities(schema)
  1105     unprotected = unprotected_entities(schema)
  1100     for etype in skip: # XXX (syt) duh? explain or kill
  1106     for etype in skip:  # XXX (syt) duh? explain or kill
  1101         unprotected.add(etype)
  1107         unprotected.add(etype)
  1102     howmanydict = {}
  1108     howmanydict = {}
  1103     # step 1, compute a base number of each entity types: number of already
  1109     # step 1, compute a base number of each entity types: number of already
  1104     # existing entities of this type + `how_many`
  1110     # existing entities of this type + `how_many`
  1105     for etype in unprotected_entities(schema, strict=True):
  1111     for etype in unprotected_entities(schema, strict=True):
  1140     def custom_populate(self, how_many, cnx):
  1146     def custom_populate(self, how_many, cnx):
  1141         pass
  1147         pass
  1142 
  1148 
  1143     def post_populate(self, cnx):
  1149     def post_populate(self, cnx):
  1144         pass
  1150         pass
  1145 
       
  1146 
  1151 
  1147     @nocoverage
  1152     @nocoverage
  1148     def auto_populate(self, how_many):
  1153     def auto_populate(self, how_many):
  1149         """this method populates the database with `how_many` entities
  1154         """this method populates the database with `how_many` entities
  1150         of each possible type. It also inserts random relations between them
  1155         of each possible type. It also inserts random relations between them
  1181             try:
  1186             try:
  1182                 cnx.execute(rql, args)
  1187                 cnx.execute(rql, args)
  1183             except ValidationError as ex:
  1188             except ValidationError as ex:
  1184                 # failed to satisfy some constraint
  1189                 # failed to satisfy some constraint
  1185                 print('error in automatic db population', ex)
  1190                 print('error in automatic db population', ex)
  1186                 cnx.commit_state = None # reset uncommitable flag
  1191                 cnx.commit_state = None  # reset uncommitable flag
  1187         self.post_populate(cnx)
  1192         self.post_populate(cnx)
  1188 
  1193 
  1189     def iter_individual_rsets(self, etypes=None, limit=None):
  1194     def iter_individual_rsets(self, etypes=None, limit=None):
  1190         etypes = etypes or self.to_test_etypes()
  1195         etypes = etypes or self.to_test_etypes()
  1191         with self.admin_access.web_request() as req:
  1196         with self.admin_access.web_request() as req:
  1216             except KeyError:
  1221             except KeyError:
  1217                 etype2 = etype1
  1222                 etype2 = etype1
  1218             # test a mixed query (DISTINCT/GROUP to avoid getting duplicate
  1223             # test a mixed query (DISTINCT/GROUP to avoid getting duplicate
  1219             # X which make muledit view failing for instance (html validation fails
  1224             # X which make muledit view failing for instance (html validation fails
  1220             # because of some duplicate "id" attributes)
  1225             # because of some duplicate "id" attributes)
  1221             yield req.execute('DISTINCT Any X, MAX(Y) GROUPBY X WHERE X is %s, Y is %s' % (etype1, etype2))
  1226             yield req.execute('DISTINCT Any X, MAX(Y) GROUPBY X WHERE X is %s, Y is %s' %
       
  1227                               (etype1, etype2))
  1222             # test some application-specific queries if defined
  1228             # test some application-specific queries if defined
  1223             for rql in self.application_rql:
  1229             for rql in self.application_rql:
  1224                 yield req.execute(rql)
  1230                 yield req.execute(rql)
  1225 
  1231 
  1226     def _test_everything_for(self, rset):
  1232     def _test_everything_for(self, rset):
  1239                             rset.req.reset_headers(), 'main-template')
  1245                             rset.req.reset_headers(), 'main-template')
  1240             # We have to do this because some views modify the
  1246             # We have to do this because some views modify the
  1241             # resultset's syntax tree
  1247             # resultset's syntax tree
  1242             rset = backup_rset
  1248             rset = backup_rset
  1243         for action in self.list_actions_for(rset):
  1249         for action in self.list_actions_for(rset):
  1244             yield InnerTest(self._testname(rset, action.__regid__, 'action'), self._test_action, action)
  1250             yield InnerTest(self._testname(rset, action.__regid__, 'action'),
       
  1251                             self._test_action, action)
  1245         for box in self.list_boxes_for(rset):
  1252         for box in self.list_boxes_for(rset):
  1246             w = [].append
  1253             w = [].append
  1247             yield InnerTest(self._testname(rset, box.__regid__, 'box'), box.render, w)
  1254             yield InnerTest(self._testname(rset, box.__regid__, 'box'), box.render, w)
  1248 
  1255 
  1249     @staticmethod
  1256     @staticmethod
  1267 
  1274 
  1268         # access to self.app for proper initialization of the authentication
  1275         # access to self.app for proper initialization of the authentication
  1269         # machinery (else some views may fail)
  1276         # machinery (else some views may fail)
  1270         self.app
  1277         self.app
  1271 
  1278 
  1272     ## one each
       
  1273     def test_one_each_config(self):
  1279     def test_one_each_config(self):
  1274         self.auto_populate(1)
  1280         self.auto_populate(1)
  1275         for rset in self.iter_automatic_rsets(limit=1):
  1281         for rset in self.iter_automatic_rsets(limit=1):
  1276             for testargs in self._test_everything_for(rset):
  1282             for testargs in self._test_everything_for(rset):
  1277                 yield testargs
  1283                 yield testargs
  1278 
  1284 
  1279     ## ten each
       
  1280     def test_ten_each_config(self):
  1285     def test_ten_each_config(self):
  1281         self.auto_populate(10)
  1286         self.auto_populate(10)
  1282         for rset in self.iter_automatic_rsets(limit=10):
  1287         for rset in self.iter_automatic_rsets(limit=10):
  1283             for testargs in self._test_everything_for(rset):
  1288             for testargs in self._test_everything_for(rset):
  1284                 yield testargs
  1289                 yield testargs
  1285 
  1290 
  1286     ## startup views
       
  1287     def test_startup_views(self):
  1291     def test_startup_views(self):
  1288         for vid in self.list_startup_views():
  1292         for vid in self.list_startup_views():
  1289             with self.admin_access.web_request() as req:
  1293             with self.admin_access.web_request() as req:
  1290                 yield self.view, vid, None, req
  1294                 yield self.view, vid, None, req
  1291 
  1295