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 |