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 |