214 def test_something(self): |
257 def test_something(self): |
215 rset = self.execute('Any X WHERE X is CWUser') |
258 rset = self.execute('Any X WHERE X is CWUser') |
216 self.view('foaf', rset) |
259 self.view('foaf', rset) |
217 |
260 |
218 """ |
261 """ |
219 db_require_setup = False # skip init_db / reset_db steps |
|
220 read_instance_schema = True # read schema from database |
262 read_instance_schema = True # read schema from database |
221 |
263 |
222 |
264 |
223 # test database handling ####################################################### |
265 # test database handling ####################################################### |
224 |
266 |
225 def init_test_database(config=None, appid='data', apphome=None): |
267 DEFAULT_EMPTY_DB_ID = '__default_empty_db__' |
226 """init a test database for a specific driver""" |
268 |
227 from cubicweb.dbapi import in_memory_repo_cnx |
269 class TestDataBaseHandler(object): |
228 config = config or TestServerConfiguration(appid, apphome=apphome) |
270 DRIVER = None |
229 sources = config.sources() |
271 db_cache = {} |
230 driver = sources['system']['db-driver'] |
272 explored_glob = set() |
231 if config.db_require_setup: |
273 |
232 if driver == 'sqlite': |
274 def __init__(self, config): |
233 init_test_database_sqlite(config) |
275 self.config = config |
234 elif driver == 'postgres': |
276 self._repo = None |
235 init_test_database_postgres(config) |
277 # pure consistency check |
|
278 assert self.system_source['db-driver'] == self.DRIVER |
|
279 |
|
280 def _ensure_test_backup_db_dir(self): |
|
281 """Return path of directory for database backup. |
|
282 |
|
283 The function create it if necessary""" |
|
284 backupdir = join(self.config.apphome, 'database') |
|
285 if not isdir(backupdir): |
|
286 os.makedirs(backupdir) |
|
287 return backupdir |
|
288 |
|
289 def config_path(self, db_id): |
|
290 """Path for config backup of a given database id""" |
|
291 return self.absolute_backup_file(db_id, 'config') |
|
292 |
|
293 def absolute_backup_file(self, db_id, suffix): |
|
294 """Path for config backup of a given database id""" |
|
295 dbname = self.dbname.replace('-', '_') |
|
296 assert '.' not in db_id |
|
297 filename = '%s-%s.%s' % (dbname, db_id, suffix) |
|
298 return join(self._ensure_test_backup_db_dir(), filename) |
|
299 |
|
300 def db_cache_key(self, db_id, dbname=None): |
|
301 """Build a database cache key for a db_id with the current config |
|
302 |
|
303 This key is meant to be used in the cls.db_cache mapping""" |
|
304 if dbname is None: |
|
305 dbname = self.dbname |
|
306 dbname = os.path.basename(dbname) |
|
307 dbname = dbname.replace('-', '_') |
|
308 return (self.config.apphome, dbname, db_id) |
|
309 |
|
310 def backup_database(self, db_id): |
|
311 """Store the content of the current database as <db_id> |
|
312 |
|
313 The config used are also stored.""" |
|
314 backup_data = self._backup_database(db_id) |
|
315 config_path = self.config_path(db_id) |
|
316 # XXX we dump a dict of the config |
|
317 # This is an experimental to help config dependant setup (like BFSS) to |
|
318 # be propertly restored |
|
319 with open(config_path, 'wb') as conf_file: |
|
320 conf_file.write(pickle.dumps(dict(self.config))) |
|
321 self.db_cache[self.db_cache_key(db_id)] = (backup_data, config_path) |
|
322 |
|
323 def _backup_database(self, db_id): |
|
324 """Actual backup the current database. |
|
325 |
|
326 return a value to be stored in db_cache to allow restoration""" |
|
327 raise NotImplementedError() |
|
328 |
|
329 def restore_database(self, db_id): |
|
330 """Restore a database. |
|
331 |
|
332 takes as argument value stored in db_cache by self._backup_database""" |
|
333 # XXX set a clearer error message ??? |
|
334 backup_coordinates, config_path = self.db_cache[self.db_cache_key(db_id)] |
|
335 # reload the config used to create the database. |
|
336 config = pickle.loads(open(config_path, 'rb').read()) |
|
337 # shutdown repo before changing database content |
|
338 if self._repo is not None: |
|
339 self._repo.turn_repo_off() |
|
340 self._restore_database(backup_coordinates, config) |
|
341 |
|
342 def _restore_database(self, backup_coordinates, config): |
|
343 """Actual restore of the current database. |
|
344 |
|
345 Use the value tostored in db_cache as input """ |
|
346 raise NotImplementedError() |
|
347 |
|
348 def get_repo(self, startup=False): |
|
349 """ return Repository object on the current database. |
|
350 |
|
351 (turn the current repo object "on" if there is one or recreate one) |
|
352 if startup is True, server startup server hooks will be called if needed |
|
353 """ |
|
354 if self._repo is None: |
|
355 self._repo = self._new_repo(self.config) |
|
356 repo = self._repo |
|
357 repo.turn_repo_on() |
|
358 if startup and not repo._has_started: |
|
359 repo.hm.call_hooks('server_startup', repo=repo) |
|
360 repo._has_started = True |
|
361 return repo |
|
362 |
|
363 def _new_repo(self, config): |
|
364 """Factory method to create a new Repository Instance""" |
|
365 from cubicweb.dbapi import in_memory_repo |
|
366 config._cubes = None |
|
367 repo = in_memory_repo(config) |
|
368 # extending Repository class |
|
369 repo._has_started = False |
|
370 repo._needs_refresh = False |
|
371 repo.turn_repo_on = partial(turn_repo_on, repo) |
|
372 repo.turn_repo_off = partial(turn_repo_off, repo) |
|
373 return repo |
|
374 |
|
375 |
|
376 def get_cnx(self): |
|
377 """return Connection object ont he current repository""" |
|
378 from cubicweb.dbapi import in_memory_cnx |
|
379 repo = self.get_repo() |
|
380 sources = self.config.sources() |
|
381 login = unicode(sources['admin']['login']) |
|
382 password = sources['admin']['password'] or 'xxx' |
|
383 cnx = in_memory_cnx(repo, login, password=password) |
|
384 return cnx |
|
385 |
|
386 def get_repo_and_cnx(self, db_id=DEFAULT_EMPTY_DB_ID): |
|
387 """Reset database with the current db_id and return (repo, cnx) |
|
388 |
|
389 A database *MUST* have been build with the current <db_id> prior to |
|
390 call this method. See the ``build_db_cache`` method. The returned |
|
391 repository have it's startup hooks called and the connection is |
|
392 establised as admin.""" |
|
393 |
|
394 self.restore_database(db_id) |
|
395 repo = self.get_repo(startup=True) |
|
396 cnx = self.get_cnx() |
|
397 return repo, cnx |
|
398 |
|
399 @property |
|
400 def system_source(self): |
|
401 sources = self.config.sources() |
|
402 return sources['system'] |
|
403 |
|
404 @property |
|
405 def dbname(self): |
|
406 return self.system_source['db-name'] |
|
407 |
|
408 def init_test_database(): |
|
409 """actual initialisation of the database""" |
|
410 raise ValueError('no initialization function for driver %r' % driver) |
|
411 |
|
412 def has_cache(self, db_id): |
|
413 """Check if a given database id exist in cb cache for the current config""" |
|
414 cache_glob = self.absolute_backup_file('*', '*') |
|
415 if cache_glob not in self.explored_glob: |
|
416 self.discover_cached_db() |
|
417 return self.db_cache_key(db_id) in self.db_cache |
|
418 |
|
419 def discover_cached_db(self): |
|
420 """Search available db_if for the current config""" |
|
421 cache_glob = self.absolute_backup_file('*', '*') |
|
422 directory = os.path.dirname(cache_glob) |
|
423 entries={} |
|
424 candidates = glob.glob(cache_glob) |
|
425 for filepath in candidates: |
|
426 data = os.path.basename(filepath) |
|
427 # database backup are in the forms are <dbname>-<db_id>.<backtype> |
|
428 dbname, data = data.split('-', 1) |
|
429 db_id, filetype = data.split('.', 1) |
|
430 entries.setdefault((dbname, db_id), {})[filetype] = filepath |
|
431 for (dbname, db_id), entry in entries.iteritems(): |
|
432 # apply necessary transformation from the driver |
|
433 value = self.process_cache_entry(directory, dbname, db_id, entry) |
|
434 assert 'config' in entry |
|
435 if value is not None: # None value means "not handled by this driver |
|
436 # XXX Ignored value are shadowed to other Handler if cache are common. |
|
437 key = self.db_cache_key(db_id, dbname=dbname) |
|
438 self.db_cache[key] = value, entry['config'] |
|
439 self.explored_glob.add(cache_glob) |
|
440 |
|
441 def process_cache_entry(self, directory, dbname, db_id, entry): |
|
442 """Transforms potential cache entry to proper backup coordinate |
|
443 |
|
444 entry argument is a "filetype" -> "filepath" mapping |
|
445 Return None if an entry should be ignored.""" |
|
446 return None |
|
447 |
|
448 def build_db_cache(self, test_db_id=DEFAULT_EMPTY_DB_ID, pre_setup_func=None): |
|
449 """Build Database cache for ``test_db_id`` if a cache doesn't exist |
|
450 |
|
451 if ``test_db_id is DEFAULT_EMPTY_DB_ID`` self.init_test_database is |
|
452 called. otherwise, DEFAULT_EMPTY_DB_ID is build/restored and |
|
453 ``pre_setup_func`` to setup the database. |
|
454 |
|
455 This function backup any database it build""" |
|
456 |
|
457 if self.has_cache(test_db_id): |
|
458 return #test_db_id, 'already in cache' |
|
459 if test_db_id is DEFAULT_EMPTY_DB_ID: |
|
460 self.init_test_database() |
236 else: |
461 else: |
237 raise ValueError('no initialization function for driver %r' % driver) |
462 print 'Building %s for database %s' % (test_db_id, self.dbname) |
238 config._cubes = None # avoid assertion error |
463 self.build_db_cache(DEFAULT_EMPTY_DB_ID) |
239 repo, cnx = in_memory_repo_cnx(config, unicode(sources['admin']['login']), |
464 self.restore_database(DEFAULT_EMPTY_DB_ID) |
240 password=sources['admin']['password'] or 'xxx') |
465 repo = self.get_repo(startup=True) |
241 if driver == 'sqlite': |
466 cnx = self.get_cnx() |
242 install_sqlite_patch(repo.querier) |
467 session = repo._sessions[cnx.sessionid] |
243 return repo, cnx |
468 session.set_pool() |
244 |
469 _commit = session.commit |
245 def reset_test_database(config): |
470 def always_pooled_commit(): |
246 """init a test database for a specific driver""" |
471 _commit() |
247 if not config.db_require_setup: |
472 session.set_pool() |
248 return |
473 session.commit = always_pooled_commit |
249 driver = config.sources()['system']['db-driver'] |
474 pre_setup_func(session, self.config) |
250 if driver == 'sqlite': |
475 session.commit() |
251 reset_test_database_sqlite(config) |
476 cnx.close() |
252 elif driver == 'postgres': |
477 self.backup_database(test_db_id) |
253 init_test_database_postgres(config) |
|
254 else: |
|
255 raise ValueError('no reset function for driver %r' % driver) |
|
256 |
|
257 |
478 |
258 ### postgres test database handling ############################################ |
479 ### postgres test database handling ############################################ |
259 |
480 |
260 def init_test_database_postgres(config): |
481 class PostgresTestDataBaseHandler(TestDataBaseHandler): |
261 """initialize a fresh postgresql databse used for testing purpose""" |
482 |
262 from logilab.database import get_db_helper |
483 # XXX |
263 from cubicweb.server import init_repository |
484 # XXX PostgresTestDataBaseHandler Have not been tested at all. |
264 from cubicweb.server.serverctl import (createdb, system_source_cnx, |
485 # XXX |
265 _db_sys_cnx) |
486 DRIVER = 'postgres' |
266 source = config.sources()['system'] |
487 |
267 dbname = source['db-name'] |
488 @property |
268 templdbname = dbname + '_template' |
489 @cached |
269 helper = get_db_helper('postgres') |
490 def helper(self): |
270 # connect on the dbms system base to create our base |
491 from logilab.database import get_db_helper |
271 dbcnx = _db_sys_cnx(source, 'CREATE DATABASE and / or USER', verbose=0) |
492 return get_db_helper('postgres') |
272 cursor = dbcnx.cursor() |
493 |
273 try: |
494 @property |
274 if dbname in helper.list_databases(cursor): |
495 @cached |
275 cursor.execute('DROP DATABASE %s' % dbname) |
496 def dbcnx(self): |
276 if not templdbname in helper.list_databases(cursor): |
497 from cubicweb.server.serverctl import _db_sys_cnx |
277 source['db-name'] = templdbname |
498 return _db_sys_cnx(self.system_source, 'CREATE DATABASE and / or USER', verbose=0) |
278 createdb(helper, source, dbcnx, cursor) |
499 |
279 dbcnx.commit() |
500 @property |
280 cnx = system_source_cnx(source, special_privs='LANGUAGE C', verbose=0) |
501 @cached |
|
502 def cursor(self): |
|
503 return self.dbcnx.cursor() |
|
504 |
|
505 def init_test_database(self): |
|
506 """initialize a fresh postgresql databse used for testing purpose""" |
|
507 from cubicweb.server import init_repository |
|
508 from cubicweb.server.serverctl import system_source_cnx, createdb |
|
509 # connect on the dbms system base to create our base |
|
510 try: |
|
511 self._drop(self.dbname) |
|
512 |
|
513 createdb(self.helper, self.system_source, self.dbcnx, self.cursor) |
|
514 self.dbcnx.commit() |
|
515 cnx = system_source_cnx(self.system_source, special_privs='LANGUAGE C', verbose=0) |
281 templcursor = cnx.cursor() |
516 templcursor = cnx.cursor() |
282 # XXX factorize with db-create code |
517 try: |
283 helper.init_fti_extensions(templcursor) |
518 # XXX factorize with db-create code |
284 # install plpythonu/plpgsql language if not installed by the cube |
519 self.helper.init_fti_extensions(templcursor) |
285 langs = sys.platform == 'win32' and ('plpgsql',) or ('plpythonu', 'plpgsql') |
520 # install plpythonu/plpgsql language if not installed by the cube |
286 for extlang in langs: |
521 langs = sys.platform == 'win32' and ('plpgsql',) or ('plpythonu', 'plpgsql') |
287 helper.create_language(templcursor, extlang) |
522 for extlang in langs: |
288 cnx.commit() |
523 self.helper.create_language(templcursor, extlang) |
289 templcursor.close() |
524 cnx.commit() |
290 cnx.close() |
525 finally: |
291 init_repository(config, interactive=False) |
526 templcursor.close() |
292 source['db-name'] = dbname |
527 cnx.close() |
293 except: |
528 init_repository(self.config, interactive=False) |
294 dbcnx.rollback() |
529 except: |
295 # XXX drop template |
530 self.dbcnx.rollback() |
296 raise |
531 print >> sys.stderr, 'building', self.dbname, 'failed' |
297 createdb(helper, source, dbcnx, cursor, template=templdbname) |
532 #self._drop(self.dbname) |
298 dbcnx.commit() |
533 raise |
299 dbcnx.close() |
534 |
|
535 def helper_clear_cache(self): |
|
536 self.dbcnx.commit() |
|
537 self.dbcnx.close() |
|
538 clear_cache(self, 'dbcnx') |
|
539 clear_cache(self, 'helper') |
|
540 clear_cache(self, 'cursor') |
|
541 |
|
542 def __del__(self): |
|
543 self.helper_clear_cache() |
|
544 |
|
545 @property |
|
546 def _config_id(self): |
|
547 return hashlib.sha1(self.config.apphome).hexdigest()[:10] |
|
548 |
|
549 def _backup_name(self, db_id): # merge me with parent |
|
550 backup_name = '_'.join(('cache', self._config_id, self.dbname, db_id)) |
|
551 return backup_name.lower() |
|
552 |
|
553 def _drop(self, db_name): |
|
554 if db_name in self.helper.list_databases(self.cursor): |
|
555 #print 'dropping overwritted database:', db_name |
|
556 self.cursor.execute('DROP DATABASE %s' % db_name) |
|
557 self.dbcnx.commit() |
|
558 |
|
559 def _backup_database(self, db_id): |
|
560 """Actual backup the current database. |
|
561 |
|
562 return a value to be stored in db_cache to allow restoration""" |
|
563 from cubicweb.server.serverctl import createdb |
|
564 orig_name = self.system_source['db-name'] |
|
565 try: |
|
566 backup_name = self._backup_name(db_id) |
|
567 #print 'storing postgres backup as', backup_name |
|
568 self._drop(backup_name) |
|
569 self.system_source['db-name'] = backup_name |
|
570 createdb(self.helper, self.system_source, self.dbcnx, self.cursor, template=orig_name) |
|
571 self.dbcnx.commit() |
|
572 return backup_name |
|
573 finally: |
|
574 self.system_source['db-name'] = orig_name |
|
575 |
|
576 def _restore_database(self, backup_coordinates, config): |
|
577 from cubicweb.server.serverctl import createdb |
|
578 """Actual restore of the current database. |
|
579 |
|
580 Use the value tostored in db_cache as input """ |
|
581 #print 'restoring postgrest backup from', backup_coordinates |
|
582 self._drop(self.dbname) |
|
583 createdb(self.helper, self.system_source, self.dbcnx, self.cursor, |
|
584 template=backup_coordinates) |
|
585 self.dbcnx.commit() |
|
586 |
|
587 |
300 |
588 |
301 ### sqlserver2005 test database handling ####################################### |
589 ### sqlserver2005 test database handling ####################################### |
302 |
590 |
303 def init_test_database_sqlserver2005(config): |
591 class SQLServerTestDataBaseHandler(TestDataBaseHandler): |
304 """initialize a fresh sqlserver databse used for testing purpose""" |
592 DRIVER = 'sqlserver' |
305 if config.init_repository: |
593 |
306 from cubicweb.server import init_repository |
594 # XXX complete me |
307 init_repository(config, interactive=False, drop=True) |
595 |
|
596 def init_test_database(self): |
|
597 """initialize a fresh sqlserver databse used for testing purpose""" |
|
598 if self.config.init_repository: |
|
599 from cubicweb.server import init_repository |
|
600 init_repository(config, interactive=False, drop=True) |
308 |
601 |
309 ### sqlite test database handling ############################################## |
602 ### sqlite test database handling ############################################## |
310 |
603 |
311 def cleanup_sqlite(dbfile, removetemplate=False): |
604 class SQLiteTestDataBaseHandler(TestDataBaseHandler): |
312 try: |
605 DRIVER = 'sqlite' |
313 os.remove(dbfile) |
606 |
314 os.remove('%s-journal' % dbfile) |
607 @staticmethod |
315 except OSError: |
608 def _cleanup_database(dbfile): |
316 pass |
|
317 if removetemplate: |
|
318 try: |
609 try: |
319 os.remove('%s-template' % dbfile) |
610 os.remove(dbfile) |
|
611 os.remove('%s-journal' % dbfile) |
320 except OSError: |
612 except OSError: |
321 pass |
613 pass |
322 |
614 |
323 def reset_test_database_sqlite(config): |
615 def absolute_dbfile(self): |
324 import shutil |
616 """absolute path of current database file""" |
325 dbfile = config.sources()['system']['db-name'] |
617 dbfile = join(self._ensure_test_backup_db_dir(), |
326 cleanup_sqlite(dbfile) |
618 self.config.sources()['system']['db-name']) |
327 template = '%s-template' % dbfile |
619 self.config.sources()['system']['db-name'] = dbfile |
328 if exists(template): |
620 return dbfile |
329 shutil.copy(template, dbfile) |
621 |
330 return True |
622 |
331 return False |
623 def process_cache_entry(self, directory, dbname, db_id, entry): |
332 |
624 return entry.get('sqlite') |
333 def init_test_database_sqlite(config): |
625 |
334 """initialize a fresh sqlite databse used for testing purpose""" |
626 def _backup_database(self, db_id=DEFAULT_EMPTY_DB_ID): |
335 # remove database file if it exists |
627 # XXX remove database file if it exists ??? |
336 dbfile = join(config.apphome, config.sources()['system']['db-name']) |
628 dbfile = self.absolute_dbfile() |
337 config.sources()['system']['db-name'] = dbfile |
629 backup_file = self.absolute_backup_file(db_id, 'sqlite') |
338 if not reset_test_database_sqlite(config): |
630 shutil.copy(dbfile, backup_file) |
|
631 # Usefull to debug WHO write a database |
|
632 # backup_stack = self.absolute_backup_file(db_id, '.stack') |
|
633 #with open(backup_stack, 'w') as backup_stack_file: |
|
634 # import traceback |
|
635 # traceback.print_stack(file=backup_stack_file) |
|
636 return backup_file |
|
637 |
|
638 def _new_repo(self, config): |
|
639 repo = super(SQLiteTestDataBaseHandler, self)._new_repo(config) |
|
640 install_sqlite_patch(repo.querier) |
|
641 return repo |
|
642 |
|
643 def _restore_database(self, backup_coordinates, _config): |
|
644 # remove database file if it exists ? |
|
645 dbfile = self.absolute_dbfile() |
|
646 self._cleanup_database(dbfile) |
|
647 #print 'resto from', backup_coordinates |
|
648 shutil.copy(backup_coordinates, dbfile) |
|
649 repo = self.get_repo() |
|
650 |
|
651 def init_test_database(self): |
|
652 """initialize a fresh sqlite databse used for testing purpose""" |
339 # initialize the database |
653 # initialize the database |
340 import shutil |
|
341 from cubicweb.server import init_repository |
654 from cubicweb.server import init_repository |
342 init_repository(config, interactive=False) |
655 self._cleanup_database(self.absolute_dbfile()) |
343 shutil.copy(dbfile, '%s-template' % dbfile) |
656 init_repository(self.config, interactive=False) |
|
657 |
344 |
658 |
345 def install_sqlite_patch(querier): |
659 def install_sqlite_patch(querier): |
346 """This patch hotfixes the following sqlite bug : |
660 """This patch hotfixes the following sqlite bug : |
347 - http://www.sqlite.org/cvstrac/tktview?tn=1327,33 |
661 - http://www.sqlite.org/cvstrac/tktview?tn=1327,33 |
348 (some dates are returned as strings rather thant date objects) |
662 (some dates are returned as strings rather thant date objects) |