devtools/__init__.py
brancholdstable
changeset 7078 bad26a22fe29
parent 7071 db7608cb32bc
child 7090 d9e6e79e023a
equal deleted inserted replaced
7074:e4580e5f0703 7078:bad26a22fe29
    20 __docformat__ = "restructuredtext en"
    20 __docformat__ = "restructuredtext en"
    21 
    21 
    22 import os
    22 import os
    23 import sys
    23 import sys
    24 import logging
    24 import logging
       
    25 import shutil
       
    26 import pickle
       
    27 import glob
       
    28 import warnings
    25 from datetime import timedelta
    29 from datetime import timedelta
    26 from os.path import (abspath, join, exists, basename, dirname, normpath, split,
    30 from os.path import (abspath, join, exists, basename, dirname, normpath, split,
    27                      isfile, isabs, splitext)
    31                      isfile, isabs, splitext, isdir, expanduser)
       
    32 from functools import partial
       
    33 import hashlib
    28 
    34 
    29 from logilab.common.date import strptime
    35 from logilab.common.date import strptime
    30 from cubicweb import CW_SOFTWARE_ROOT, ConfigurationError, schema, cwconfig
    36 from logilab.common.decorators import cached, clear_cache
       
    37 from cubicweb import CW_SOFTWARE_ROOT, ConfigurationError, schema, cwconfig, BadConnectionId
    31 from cubicweb.server.serverconfig import ServerConfiguration
    38 from cubicweb.server.serverconfig import ServerConfiguration
    32 from cubicweb.etwist.twconfig import TwistedConfiguration
    39 from cubicweb.etwist.twconfig import TwistedConfiguration
    33 
    40 
    34 cwconfig.CubicWebConfiguration.cls_adjust_sys_path()
    41 cwconfig.CubicWebConfiguration.cls_adjust_sys_path()
    35 
    42 
    76                    'admin' : {'login': u'admin',
    83                    'admin' : {'login': u'admin',
    77                               'password': u'gingkow',
    84                               'password': u'gingkow',
    78                               },
    85                               },
    79                    }
    86                    }
    80 
    87 
       
    88 def turn_repo_off(repo):
       
    89     """ Idea: this is less costly than a full re-creation of the repo object.
       
    90     off:
       
    91     * session are closed,
       
    92     * pools are closed
       
    93     * system source is shutdown
       
    94     """
       
    95     if not repo._needs_refresh:
       
    96         for sessionid in list(repo._sessions):
       
    97             warnings.warn('%s Open session found while turning repository off'
       
    98                           %sessionid, RuntimeWarning)
       
    99             try:
       
   100                 repo.close(sessionid)
       
   101             except BadConnectionId: #this is strange ? thread issue ?
       
   102                 print 'XXX unknown session', sessionid
       
   103         for pool in repo.pools:
       
   104             pool.close(True)
       
   105         repo.system_source.shutdown()
       
   106         repo._needs_refresh = True
       
   107         repo._has_started = False
       
   108 
       
   109 def turn_repo_on(repo):
       
   110     """Idea: this is less costly than a full re-creation of the repo object.
       
   111     on:
       
   112     * pools are connected
       
   113     * cache are cleared
       
   114     """
       
   115     if repo._needs_refresh:
       
   116         for pool in repo.pools:
       
   117             pool.reconnect()
       
   118         repo._type_source_cache = {}
       
   119         repo._extid_cache = {}
       
   120         repo.querier._rql_cache = {}
       
   121         for source in repo.sources:
       
   122             source.reset_caches()
       
   123         repo._needs_refresh = False
       
   124 
    81 
   125 
    82 class TestServerConfiguration(ServerConfiguration):
   126 class TestServerConfiguration(ServerConfiguration):
    83     mode = 'test'
   127     mode = 'test'
    84     set_language = False
   128     set_language = False
    85     read_instance_schema = False
   129     read_instance_schema = False
    86     init_repository = True
   130     init_repository = True
    87     db_require_setup = True
       
    88 
   131 
    89     def __init__(self, appid='data', apphome=None, log_threshold=logging.CRITICAL+10):
   132     def __init__(self, appid='data', apphome=None, log_threshold=logging.CRITICAL+10):
    90         # must be set before calling parent __init__
   133         # must be set before calling parent __init__
    91         if apphome is None:
   134         if apphome is None:
    92             if exists(appid):
   135             if exists(appid):
   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)
   377                         break
   691                         break
   378             return rset
   692             return rset
   379         return new_execute
   693         return new_execute
   380     querier.__class__.execute = wrap_execute(querier.__class__.execute)
   694     querier.__class__.execute = wrap_execute(querier.__class__.execute)
   381     querier.__class__._devtools_sqlite_patched = True
   695     querier.__class__._devtools_sqlite_patched = True
       
   696 
       
   697 
       
   698 
       
   699 HANDLERS = {}
       
   700 
       
   701 def register_handler(handlerkls):
       
   702     assert handlerkls is not None
       
   703     HANDLERS[handlerkls.DRIVER] = handlerkls
       
   704 
       
   705 register_handler(PostgresTestDataBaseHandler)
       
   706 register_handler(SQLiteTestDataBaseHandler)
       
   707 register_handler(SQLServerTestDataBaseHandler)
       
   708 
       
   709 
       
   710 class HCache(object):
       
   711     """Handler cache object: store database handler for a given configuration.
       
   712 
       
   713     We only keep one repo in cache to prevent too much objects to stay alive
       
   714     (database handler holds a reference to a repository). As at the moment a new
       
   715     handler is created for each TestCase class and all test methods are executed
       
   716     sequentialy whithin this class, there should not have more cache miss that
       
   717     if we had a wider cache as once a Handler stop being used it won't be used
       
   718     again.
       
   719     """
       
   720 
       
   721     def __init__(self):
       
   722         self.config = None
       
   723         self.handler = None
       
   724 
       
   725     def get(self, config):
       
   726         if config is self.config:
       
   727             return self.handler
       
   728         else:
       
   729             return None
       
   730 
       
   731     def set(self, config, handler):
       
   732         self.config = config
       
   733         self.handler = handler
       
   734 
       
   735 HCACHE = HCache()
       
   736 
       
   737 
       
   738 # XXX a class method on Test ?
       
   739 def get_test_db_handler(config):
       
   740     handler = HCACHE.get(config)
       
   741     if handler is not None:
       
   742         return handler
       
   743     sources = config.sources()
       
   744     driver = sources['system']['db-driver']
       
   745     key = (driver, config)
       
   746     handlerkls = HANDLERS.get(driver, None)
       
   747     if handlerkls is not None:
       
   748         handler = handlerkls(config)
       
   749         HCACHE.set(config, handler)
       
   750         return handler
       
   751     else:
       
   752         raise ValueError('no initialization function for driver %r' % driver)
       
   753 
       
   754 ### compatibility layer ##############################################
       
   755 from logilab.common.deprecation import deprecated
       
   756 
       
   757 @deprecated("please use the new DatabaseHandler mecanism")
       
   758 def init_test_database(config=None, configdir='data', apphome=None):
       
   759     """init a test database for a specific driver"""
       
   760     if config is None:
       
   761         config = TestServerConfiguration(apphome=apphome)
       
   762     handler = get_test_db_handler(config)
       
   763     handler.build_db_cache()
       
   764     return handler.get_repo_and_cnx()
       
   765 
       
   766