Skip to content

Commit

Permalink
[IMP] http tests: implement a test cursor that keeps a transaction op…
Browse files Browse the repository at this point in the history
…en accross requests

 - TestCursor subclasses Cursor, and simulates commit and rollback with savepoints
 - the registry manages a test mode, in which it only uses the test cursor
 - a reentrant lock forces the serialization of parallel requests

bzr revid: rco@openerp.com-20140408151736-j0guy68i2wjexy3d
  • Loading branch information
rco-odoo committed Apr 8, 2014
1 parent 6bc6050 commit f0fd48c
Show file tree
Hide file tree
Showing 4 changed files with 78 additions and 38 deletions.
16 changes: 4 additions & 12 deletions openerp/http.py
Original file line number Diff line number Diff line change
Expand Up @@ -235,10 +235,7 @@ def cr(self):
"""
# some magic to lazy create the cr
if not self._cr:
# Test cursors
self._cr = openerp.tests.common.acquire_test_cursor(self.session_id)
if not self._cr:
self._cr = self.registry.get_cursor()
self._cr = self.registry.get_cursor()
return self._cr

def __enter__(self):
Expand All @@ -249,14 +246,9 @@ def __exit__(self, exc_type, exc_value, traceback):
_request_stack.pop()

if self._cr:
# Dont close test cursors
if not openerp.tests.common.release_test_cursor(self._cr):
if exc_type is None and not self._failed:
self._cr.commit()
else:
# just to be explicit - happens at close() anyway
self._cr.rollback()
self._cr.close()
if exc_type is None and not self._failed:
self._cr.commit()
self._cr.close()
# just to be sure no one tries to re-use the request
self.disable_db = True
self.uid = None
Expand Down
23 changes: 23 additions & 0 deletions openerp/modules/registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,9 @@ def __init__(self, db_name):
self.db_name = db_name
self.db = openerp.sql_db.db_connect(db_name)

# special cursor for test mode; None means "normal" mode
self.test_cr = None

# Indicates that the registry is
self.ready = False

Expand Down Expand Up @@ -187,8 +190,28 @@ def setup_multi_process_signaling(cls, cr):
r, c)
return r, c

def enter_test_mode(self):
""" Enter the 'test' mode, where one cursor serves several requests. """
assert self.test_cr is None
self.test_cr = self.db.test_cursor()
RegistryManager.enter_test_mode()

def leave_test_mode(self):
""" Leave the test mode. """
assert self.test_cr is not None
self.test_cr.close(force=True) # close the cursor for real
self.test_cr = None
RegistryManager.leave_test_mode()

def get_cursor(self):
""" Return a new cursor for the database. """
if self.test_cr is not None:
# While in test mode, we use one special cursor across requests. The
# test cursor uses a reentrant lock to serialize accesses. The lock
# is granted here by get_cursor(), and automatically released by the
# cursor itself in its method close().
self.test_cr.acquire()
return self.test_cr
return self.db.cursor()

@contextmanager
Expand Down
46 changes: 46 additions & 0 deletions openerp/sql_db.py
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,47 @@ def savepoint(self):
def __getattr__(self, name):
return getattr(self._obj, name)

class TestCursor(Cursor):
""" A cursor to be used for tests. It keeps the transaction open across
several requests, and simulates committing, rolling back, and closing.
"""
def __init__(self, *args, **kwargs):
# in order to simulate commit and rollback, the cursor maintains a
# savepoint at its last commit
super(TestCursor, self).__init__(*args, **kwargs)
super(TestCursor, self).execute("SAVEPOINT test_cursor")
self._lock = threading.RLock()
self._auto_commit = False

def acquire(self):
self._lock.acquire()

def release(self):
self._lock.release()

def execute(self, *args, **kwargs):
super(TestCursor, self).execute(*args, **kwargs)
if self._auto_commit:
self.commit()

def close(self, force=False):
self.rollback() # for stuff that has not been committed
if force:
super(TestCursor, self).close()
else:
self.release()

def autocommit(self, on):
self._auto_commit = on

def commit(self):
super(TestCursor, self).execute("RELEASE SAVEPOINT test_cursor")
super(TestCursor, self).execute("SAVEPOINT test_cursor")

def rollback(self):
super(TestCursor, self).execute("ROLLBACK TO SAVEPOINT test_cursor")
super(TestCursor, self).execute("SAVEPOINT test_cursor")

class PsycoConnection(psycopg2.extensions.connection):
pass

Expand Down Expand Up @@ -491,6 +532,11 @@ def cursor(self, serialized=True):
_logger.debug('create %scursor to %r', cursor_type, self.dbname)
return Cursor(self._pool, self.dbname, serialized=serialized)

def test_cursor(self, serialized=True):
cursor_type = serialized and 'serialized ' or ''
_logger.debug('create test %scursor to %r', cursor_type, self.dbname)
return TestCursor(self._pool, self.dbname, serialized=serialized)

# serialized_cursor is deprecated - cursors are serialized by default
serialized_cursor = cursor

Expand Down
31 changes: 5 additions & 26 deletions openerp/tests/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import werkzeug

import openerp
from openerp.modules.registry import RegistryManager

_logger = logging.getLogger(__name__)

Expand All @@ -37,25 +38,6 @@
# Useless constant, tests are aware of the content of demo data
ADMIN_USER_ID = openerp.SUPERUSER_ID

# Magic session_id, unfortunately we have to serialize access to the cursors to
# serialize requests. We first tried to duplicate the database for each tests
# but this proved too slow. Any idea to improve this is welcome.
HTTP_SESSION = {}

def acquire_test_cursor(session_id):
if openerp.tools.config['test_enable']:
cr = HTTP_SESSION.get(session_id)
if cr:
cr._test_lock.acquire()
return cr

def release_test_cursor(cr):
if openerp.tools.config['test_enable']:
if hasattr(cr, '_test_lock'):
cr._test_lock.release()
return True
return False

def at_install(flag):
""" Sets the at-install state of a test, the flag is a boolean specifying
whether the test should (``True``) or should not (``False``) run during
Expand Down Expand Up @@ -120,7 +102,7 @@ class TransactionCase(BaseCase):
"""

def setUp(self):
self.registry = openerp.modules.registry.RegistryManager.get(DB)
self.registry = RegistryManager.get(DB)
self.cr = self.cursor()
self.uid = openerp.SUPERUSER_ID

Expand All @@ -137,7 +119,7 @@ class SingleTransactionCase(BaseCase):

@classmethod
def setUpClass(cls):
cls.registry = openerp.modules.registry.RegistryManager.get(DB)
cls.registry = RegistryManager.get(DB)
cls.cr = cls.registry.get_cursor()
cls.uid = openerp.SUPERUSER_ID

Expand All @@ -161,18 +143,15 @@ def __init__(self, methodName='runTest'):

def setUp(self):
super(HttpCase, self).setUp()
openerp.modules.registry.RegistryManager.enter_test_mode()
self.registry.enter_test_mode()
# setup a magic session_id that will be rollbacked
self.session = openerp.http.root.session_store.new()
self.session_id = self.session.sid
self.session.db = DB
openerp.http.root.session_store.save(self.session)
self.cr._test_lock = threading.RLock()
HTTP_SESSION[self.session_id] = self.cr

def tearDown(self):
del HTTP_SESSION[self.session_id]
openerp.modules.registry.RegistryManager.leave_test_mode()
self.registry.leave_test_mode()
super(HttpCase, self).tearDown()

def url_open(self, url, data=None, timeout=10):
Expand Down

0 comments on commit f0fd48c

Please sign in to comment.