branch | oldstable |
changeset 7074 | e4580e5f0703 |
parent 7071 | db7608cb32bc |
child 7075 | 4751d77394b1 |
child 7078 | bad26a22fe29 |
6749:48f468f33704 | 7074:e4580e5f0703 |
---|---|
1 # copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved. |
1 # copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved. |
2 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr |
2 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr |
3 # |
3 # |
4 # This file is part of CubicWeb. |
4 # This file is part of CubicWeb. |
5 # |
5 # |
6 # CubicWeb is free software: you can redistribute it and/or modify it under the |
6 # CubicWeb is free software: you can redistribute it and/or modify it under the |
23 |
23 |
24 import os |
24 import os |
25 import sys |
25 import sys |
26 import re |
26 import re |
27 import urlparse |
27 import urlparse |
28 from os.path import dirname, join |
28 from os.path import dirname, join, abspath |
29 from urllib import unquote |
29 from urllib import unquote |
30 from math import log |
30 from math import log |
31 from contextlib import contextmanager |
31 from contextlib import contextmanager |
32 from warnings import warn |
32 from warnings import warn |
33 |
33 |
36 from logilab.common.testlib import TestCase, InnerTest, Tags |
36 from logilab.common.testlib import TestCase, InnerTest, Tags |
37 from logilab.common.pytest import nocoverage, pause_tracing, resume_tracing |
37 from logilab.common.pytest import nocoverage, pause_tracing, resume_tracing |
38 from logilab.common.debugger import Debugger |
38 from logilab.common.debugger import Debugger |
39 from logilab.common.umessage import message_from_string |
39 from logilab.common.umessage import message_from_string |
40 from logilab.common.decorators import cached, classproperty, clear_cache |
40 from logilab.common.decorators import cached, classproperty, clear_cache |
41 from logilab.common.deprecation import deprecated |
41 from logilab.common.deprecation import deprecated, class_deprecated |
42 from logilab.common.shellutils import getlogin |
42 from logilab.common.shellutils import getlogin |
43 |
43 |
44 from cubicweb import ValidationError, NoSelectableObject, AuthenticationError |
44 from cubicweb import ValidationError, NoSelectableObject, AuthenticationError |
45 from cubicweb import cwconfig, devtools, web, server |
45 from cubicweb import cwconfig, devtools, web, server |
46 from cubicweb.dbapi import ProgrammingError, DBAPISession, repo_connect |
46 from cubicweb.dbapi import ProgrammingError, DBAPISession, repo_connect |
183 * `session`, server side session associated to `cnx` |
183 * `session`, server side session associated to `cnx` |
184 * `app`, the cubicweb publisher (for web testing) |
184 * `app`, the cubicweb publisher (for web testing) |
185 * `repo`, the repository object |
185 * `repo`, the repository object |
186 * `admlogin`, login of the admin user |
186 * `admlogin`, login of the admin user |
187 * `admpassword`, password of the admin user |
187 * `admpassword`, password of the admin user |
188 * `shell`, create and use shell environment |
|
188 """ |
189 """ |
189 appid = 'data' |
190 appid = 'data' |
190 configcls = devtools.ApptestConfiguration |
191 configcls = devtools.ApptestConfiguration |
191 reset_schema = reset_vreg = False # reset schema / vreg between tests |
192 reset_schema = reset_vreg = False # reset schema / vreg between tests |
192 tags = TestCase.tags | Tags('cubicweb', 'cw_repo') |
193 tags = TestCase.tags | Tags('cubicweb', 'cw_repo') |
198 Configuration is cached on the test class. |
199 Configuration is cached on the test class. |
199 """ |
200 """ |
200 try: |
201 try: |
201 return cls.__dict__['_config'] |
202 return cls.__dict__['_config'] |
202 except KeyError: |
203 except KeyError: |
203 home = join(dirname(sys.modules[cls.__module__].__file__), cls.appid) |
204 home = abspath(join(dirname(sys.modules[cls.__module__].__file__), cls.appid)) |
204 config = cls._config = cls.configcls(cls.appid, apphome=home) |
205 config = cls._config = cls.configcls(cls.appid, apphome=home) |
205 config.mode = 'test' |
206 config.mode = 'test' |
206 return config |
207 return config |
207 |
208 |
208 @classmethod |
209 @classmethod |
284 @property |
285 @property |
285 def adminsession(self): |
286 def adminsession(self): |
286 """return current server side session (using default manager account)""" |
287 """return current server side session (using default manager account)""" |
287 return self.repo._sessions[self._orig_cnx[0].sessionid] |
288 return self.repo._sessions[self._orig_cnx[0].sessionid] |
288 |
289 |
290 def shell(self): |
|
291 """return a shell session object""" |
|
292 from cubicweb.server.migractions import ServerMigrationHelper |
|
293 return ServerMigrationHelper(None, repo=self.repo, cnx=self.cnx, |
|
294 interactive=False, |
|
295 # hack so it don't try to load fs schema |
|
296 schema=1) |
|
297 |
|
289 def set_option(self, optname, value): |
298 def set_option(self, optname, value): |
290 self.config.global_set_option(optname, value) |
299 self.config.global_set_option(optname, value) |
291 |
300 |
292 def set_debug(self, debugmode): |
301 def set_debug(self, debugmode): |
293 server.set_debug(debugmode) |
302 server.set_debug(debugmode) |
294 |
303 |
304 def debugged(self, debugmode): |
|
305 return server.debugged(debugmode) |
|
306 |
|
295 # default test setup and teardown ######################################### |
307 # default test setup and teardown ######################################### |
296 |
308 |
297 def setUp(self): |
309 def setUp(self): |
298 # monkey patch send mail operation so emails are sent synchronously |
310 # monkey patch send mail operation so emails are sent synchronously |
299 self._old_mail_commit_event = SendMailOp.commit_event |
311 self._old_mail_postcommit_event = SendMailOp.postcommit_event |
300 SendMailOp.commit_event = SendMailOp.sendmails |
312 SendMailOp.postcommit_event = SendMailOp.sendmails |
301 pause_tracing() |
313 pause_tracing() |
302 previous_failure = self.__class__.__dict__.get('_repo_init_failed') |
314 previous_failure = self.__class__.__dict__.get('_repo_init_failed') |
303 if previous_failure is not None: |
315 if previous_failure is not None: |
304 self.skipTest('repository is not initialised: %r' % previous_failure) |
316 self.skipTest('repository is not initialised: %r' % previous_failure) |
305 try: |
317 try: |
317 if not self.cnx._closed: |
329 if not self.cnx._closed: |
318 self.cnx.rollback() |
330 self.cnx.rollback() |
319 for cnx in self._cnxs: |
331 for cnx in self._cnxs: |
320 if not cnx._closed: |
332 if not cnx._closed: |
321 cnx.close() |
333 cnx.close() |
322 SendMailOp.commit_event = self._old_mail_commit_event |
334 SendMailOp.postcommit_event = self._old_mail_postcommit_event |
323 |
335 |
324 def setup_database(self): |
336 def setup_database(self): |
325 """add your database setup code by overriding this method""" |
337 """add your database setup code by overriding this method""" |
326 |
338 |
327 # user / session management ############################################### |
339 # user / session management ############################################### |
342 if req is None: |
354 if req is None: |
343 req = self._orig_cnx[0].request() |
355 req = self._orig_cnx[0].request() |
344 user = req.create_entity('CWUser', login=unicode(login), |
356 user = req.create_entity('CWUser', login=unicode(login), |
345 upassword=password, **kwargs) |
357 upassword=password, **kwargs) |
346 req.execute('SET X in_group G WHERE X eid %%(x)s, G name IN(%s)' |
358 req.execute('SET X in_group G WHERE X eid %%(x)s, G name IN(%s)' |
347 % ','.join(repr(g) for g in groups), |
359 % ','.join(repr(str(g)) for g in groups), |
348 {'x': user.eid}) |
360 {'x': user.eid}) |
349 user.cw_clear_relation_cache('in_group', 'subject') |
361 user.cw_clear_relation_cache('in_group', 'subject') |
350 if commit: |
362 if commit: |
351 req.cnx.commit() |
363 req.cnx.commit() |
352 return user |
364 return user |
421 self.session.set_pool() |
433 self.session.set_pool() |
422 return self.session.execute(rql, args) |
434 return self.session.execute(rql, args) |
423 |
435 |
424 # other utilities ######################################################### |
436 # other utilities ######################################################### |
425 |
437 |
438 def grant_permission(self, entity, group, pname, plabel=None): |
|
439 """insert a permission on an entity. Will have to commit the main |
|
440 connection to be considered |
|
441 """ |
|
442 pname = unicode(pname) |
|
443 plabel = plabel and unicode(plabel) or unicode(group) |
|
444 e = entity.eid |
|
445 with security_enabled(self.session, False, False): |
|
446 peid = self.execute( |
|
447 'INSERT CWPermission X: X name %(pname)s, X label %(plabel)s,' |
|
448 'X require_group G, E require_permission X ' |
|
449 'WHERE G name %(group)s, E eid %(e)s', |
|
450 locals())[0][0] |
|
451 return peid |
|
452 |
|
426 @contextmanager |
453 @contextmanager |
427 def temporary_appobjects(self, *appobjects): |
454 def temporary_appobjects(self, *appobjects): |
428 self.vreg._loadedmods.setdefault(self.__module__, {}) |
455 self.vreg._loadedmods.setdefault(self.__module__, {}) |
429 for obj in appobjects: |
456 for obj in appobjects: |
430 self.vreg.register(obj) |
457 self.vreg.register(obj) |
432 yield |
459 yield |
433 finally: |
460 finally: |
434 for obj in appobjects: |
461 for obj in appobjects: |
435 self.vreg.unregister(obj) |
462 self.vreg.unregister(obj) |
436 |
463 |
437 # vregistry inspection utilities ########################################### |
464 def assertModificationDateGreater(self, entity, olddate): |
465 entity.cw_attr_cache.pop('modification_date', None) |
|
466 self.failUnless(entity.modification_date > olddate) |
|
467 |
|
468 |
|
469 # workflow utilities ####################################################### |
|
470 |
|
471 def assertPossibleTransitions(self, entity, expected): |
|
472 transitions = entity.cw_adapt_to('IWorkflowable').possible_transitions() |
|
473 self.assertListEqual(sorted(tr.name for tr in transitions), |
|
474 sorted(expected)) |
|
475 |
|
476 |
|
477 # views and actions registries inspection ################################## |
|
438 |
478 |
439 def pviews(self, req, rset): |
479 def pviews(self, req, rset): |
440 return sorted((a.__regid__, a.__class__) |
480 return sorted((a.__regid__, a.__class__) |
441 for a in self.vreg['views'].possible_views(req, rset=rset)) |
481 for a in self.vreg['views'].possible_views(req, rset=rset)) |
442 |
482 |
466 class fake_menu(list): |
506 class fake_menu(list): |
467 @property |
507 @property |
468 def items(self): |
508 def items(self): |
469 return self |
509 return self |
470 class fake_box(object): |
510 class fake_box(object): |
471 def mk_action(self, label, url, **kwargs): |
511 def action_link(self, action, **kwargs): |
472 return (label, url) |
|
473 def box_action(self, action, **kwargs): |
|
474 return (action.title, action.url()) |
512 return (action.title, action.url()) |
475 submenu = fake_menu() |
513 submenu = fake_menu() |
476 action.fill_menu(fake_box(), submenu) |
514 action.fill_menu(fake_box(), submenu) |
477 return submenu |
515 return submenu |
478 |
516 |
487 continue |
525 continue |
488 if rset.rowcount > 1 and vid in only_once_vids: |
526 if rset.rowcount > 1 and vid in only_once_vids: |
489 continue |
527 continue |
490 views = [view for view in views |
528 views = [view for view in views |
491 if view.category != 'startupview' |
529 if view.category != 'startupview' |
492 and not issubclass(view, notification.NotificationView)] |
530 and not issubclass(view, notification.NotificationView) |
531 and not isinstance(view, class_deprecated)] |
|
493 if views: |
532 if views: |
494 try: |
533 try: |
495 view = viewsvreg._select_best(views, req, rset=rset) |
534 view = viewsvreg._select_best(views, req, rset=rset) |
496 if view.linkable(): |
535 if view.linkable(): |
497 yield view |
536 yield view |
509 yield action |
548 yield action |
510 |
549 |
511 def list_boxes_for(self, rset): |
550 def list_boxes_for(self, rset): |
512 """returns the list of boxes that can be applied on `rset`""" |
551 """returns the list of boxes that can be applied on `rset`""" |
513 req = rset.req |
552 req = rset.req |
514 for box in self.vreg['boxes'].possible_objects(req, rset=rset): |
553 for box in self.vreg['ctxcomponents'].possible_objects(req, rset=rset): |
515 yield box |
554 yield box |
516 |
555 |
517 def list_startup_views(self): |
556 def list_startup_views(self): |
518 """returns the list of startup views""" |
557 """returns the list of startup views""" |
519 req = self.request() |
558 req = self.request() |
618 return self.expect_redirect(lambda x: self.app_publish(x, path), req) |
657 return self.expect_redirect(lambda x: self.app_publish(x, path), req) |
619 |
658 |
620 def init_authentication(self, authmode, anonuser=None): |
659 def init_authentication(self, authmode, anonuser=None): |
621 self.set_option('auth-mode', authmode) |
660 self.set_option('auth-mode', authmode) |
622 self.set_option('anonymous-user', anonuser) |
661 self.set_option('anonymous-user', anonuser) |
662 if anonuser is None: |
|
663 self.config.anonymous_credential = None |
|
664 else: |
|
665 self.config.anonymous_credential = (anonuser, anonuser) |
|
623 req = self.request() |
666 req = self.request() |
624 origsession = req.session |
667 origsession = req.session |
625 req.session = req.cnx = None |
668 req.session = req.cnx = None |
626 del req.execute # get back to class implementation |
669 del req.execute # get back to class implementation |
627 sh = self.app.session_handler |
670 sh = self.app.session_handler |
719 and parsed. |
762 and parsed. |
720 |
763 |
721 :returns: an instance of `cubicweb.devtools.htmlparser.PageInfo` |
764 :returns: an instance of `cubicweb.devtools.htmlparser.PageInfo` |
722 encapsulation the generated HTML |
765 encapsulation the generated HTML |
723 """ |
766 """ |
724 output = None |
|
725 try: |
767 try: |
726 output = viewfunc(**kwargs) |
768 output = viewfunc(**kwargs) |
727 return self._check_html(output, view, template) |
|
728 except (SystemExit, KeyboardInterrupt): |
769 except (SystemExit, KeyboardInterrupt): |
729 raise |
770 raise |
730 except: |
771 except: |
731 # hijack exception: generative tests stop when the exception |
772 # hijack exception: generative tests stop when the exception |
732 # is not an AssertionError |
773 # is not an AssertionError |
733 klass, exc, tcbk = sys.exc_info() |
774 klass, exc, tcbk = sys.exc_info() |
734 try: |
775 try: |
735 msg = '[%s in %s] %s' % (klass, view.__regid__, exc) |
776 msg = '[%s in %s] %s' % (klass, view.__regid__, exc) |
736 except: |
777 except: |
737 msg = '[%s in %s] undisplayable exception' % (klass, view.__regid__) |
778 msg = '[%s in %s] undisplayable exception' % (klass, view.__regid__) |
738 if output is not None: |
779 raise AssertionError, msg, tcbk |
780 return self._check_html(output, view, template) |
|
781 |
|
782 def get_validator(self, view=None, content_type=None, output=None): |
|
783 if view is not None: |
|
784 try: |
|
785 return self.vid_validators[view.__regid__]() |
|
786 except KeyError: |
|
787 if content_type is None: |
|
788 content_type = view.content_type |
|
789 if content_type is None: |
|
790 content_type = 'text/html' |
|
791 if content_type in ('text/html', 'application/xhtml+xml'): |
|
792 if output and output.startswith('<?xml'): |
|
793 default_validator = htmlparser.DTDValidator |
|
794 else: |
|
795 default_validator = htmlparser.HTMLValidator |
|
796 else: |
|
797 default_validator = None |
|
798 validatorclass = self.content_type_validators.get(content_type, |
|
799 default_validator) |
|
800 if validatorclass is None: |
|
801 return |
|
802 return validatorclass() |
|
803 |
|
804 @nocoverage |
|
805 def _check_html(self, output, view, template='main-template'): |
|
806 """raises an exception if the HTML is invalid""" |
|
807 output = output.strip() |
|
808 validator = self.get_validator(view, output=output) |
|
809 if validator is None: |
|
810 return |
|
811 if isinstance(validator, htmlparser.DTDValidator): |
|
812 # XXX remove <canvas> used in progress widget, unknown in html dtd |
|
813 output = re.sub('<canvas.*?></canvas>', '', output) |
|
814 return self.assertWellFormed(validator, output.strip(), context= view.__regid__) |
|
815 |
|
816 def assertWellFormed(self, validator, content, context=None): |
|
817 try: |
|
818 return validator.parse_string(content) |
|
819 except (SystemExit, KeyboardInterrupt): |
|
820 raise |
|
821 except: |
|
822 # hijack exception: generative tests stop when the exception |
|
823 # is not an AssertionError |
|
824 klass, exc, tcbk = sys.exc_info() |
|
825 if context is None: |
|
826 msg = u'[%s]' % (klass,) |
|
827 else: |
|
828 msg = u'[%s in %s]' % (klass, context) |
|
829 msg = msg.encode(sys.getdefaultencoding(), 'replace') |
|
830 |
|
831 try: |
|
832 str_exc = str(exc) |
|
833 except: |
|
834 str_exc = 'undisplayable exception' |
|
835 msg += str_exc |
|
836 if content is not None: |
|
739 position = getattr(exc, "position", (0,))[0] |
837 position = getattr(exc, "position", (0,))[0] |
740 if position: |
838 if position: |
741 # define filter |
839 # define filter |
742 output = output.splitlines() |
840 if isinstance(content, str): |
743 width = int(log(len(output), 10)) + 1 |
841 content = unicode(content, sys.getdefaultencoding(), 'replace') |
842 content = content.splitlines() |
|
843 width = int(log(len(content), 10)) + 1 |
|
744 line_template = " %" + ("%i" % width) + "i: %s" |
844 line_template = " %" + ("%i" % width) + "i: %s" |
745 # XXX no need to iterate the whole file except to get |
845 # XXX no need to iterate the whole file except to get |
746 # the line number |
846 # the line number |
747 output = '\n'.join(line_template % (idx + 1, line) |
847 content = u'\n'.join(line_template % (idx + 1, line) |
748 for idx, line in enumerate(output) |
848 for idx, line in enumerate(content) |
749 if line_context_filter(idx+1, position)) |
849 if line_context_filter(idx+1, position)) |
750 msg += '\nfor output:\n%s' % output |
850 msg += u'\nfor content:\n%s' % content |
751 raise AssertionError, msg, tcbk |
851 raise AssertionError, msg, tcbk |
752 |
852 |
753 |
853 def assertDocTestFile(self, testfile): |
754 @nocoverage |
854 # doctest returns tuple (failure_count, test_count) |
755 def _check_html(self, output, view, template='main-template'): |
855 result = self.shell().process_script(testfile) |
756 """raises an exception if the HTML is invalid""" |
856 if result[0] and result[1]: |
757 try: |
857 raise self.failureException("doctest file '%s' failed" |
758 validatorclass = self.vid_validators[view.__regid__] |
858 % testfile) |
759 except KeyError: |
859 |
760 if view.content_type in ('text/html', 'application/xhtml+xml'): |
860 # notifications ############################################################ |
761 if template is None: |
861 |
762 default_validator = htmlparser.HTMLValidator |
862 def assertSentEmail(self, subject, recipients=None, nb_msgs=None): |
763 else: |
863 """test recipients in system mailbox for given email subject |
764 default_validator = htmlparser.DTDValidator |
864 |
765 else: |
865 :param subject: email subject to find in mailbox |
766 default_validator = None |
866 :param recipients: list of email recipients |
767 validatorclass = self.content_type_validators.get(view.content_type, |
867 :param nb_msgs: expected number of entries |
768 default_validator) |
868 :returns: list of matched emails |
769 if validatorclass is None: |
869 """ |
770 return output.strip() |
870 messages = [email for email in MAILBOX |
771 validator = validatorclass() |
871 if email.message.get('Subject') == subject] |
772 if isinstance(validator, htmlparser.DTDValidator): |
872 if recipients is not None: |
773 # XXX remove <canvas> used in progress widget, unknown in html dtd |
873 sent_to = set() |
774 output = re.sub('<canvas.*?></canvas>', '', output) |
874 for msg in messages: |
775 return validator.parse_string(output.strip()) |
875 sent_to.update(msg.recipients) |
876 self.assertSetEqual(set(recipients), sent_to) |
|
877 if nb_msgs is not None: |
|
878 self.assertEqual(len(MAILBOX), nb_msgs) |
|
879 return messages |
|
776 |
880 |
777 # deprecated ############################################################### |
881 # deprecated ############################################################### |
778 |
882 |
779 @deprecated('[3.8] use self.execute(...).get_entity(0, 0)') |
883 @deprecated('[3.8] use self.execute(...).get_entity(0, 0)') |
780 def entity(self, rql, args=None, eidkey=None, req=None): |
884 def entity(self, rql, args=None, eidkey=None, req=None): |
964 # resultset's syntax tree |
1068 # resultset's syntax tree |
965 rset = backup_rset |
1069 rset = backup_rset |
966 for action in self.list_actions_for(rset): |
1070 for action in self.list_actions_for(rset): |
967 yield InnerTest(self._testname(rset, action.__regid__, 'action'), self._test_action, action) |
1071 yield InnerTest(self._testname(rset, action.__regid__, 'action'), self._test_action, action) |
968 for box in self.list_boxes_for(rset): |
1072 for box in self.list_boxes_for(rset): |
969 yield InnerTest(self._testname(rset, box.__regid__, 'box'), box.render) |
1073 w = [].append |
1074 yield InnerTest(self._testname(rset, box.__regid__, 'box'), box.render, w) |
|
970 |
1075 |
971 @staticmethod |
1076 @staticmethod |
972 def _testname(rset, objid, objtype): |
1077 def _testname(rset, objid, objtype): |
973 return '%s_%s_%s' % ('_'.join(rset.column_types(0)), objid, objtype) |
1078 return '%s_%s_%s' % ('_'.join(rset.column_types(0)), objid, objtype) |
974 |
1079 |