forked from Pylons/pyramid_debugtoolbar
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
SQLAlchemy Panel - log transactional events
Use the SQLAlchemy Events system to add transactional markers (such as `begin`, `commit`, `rollback`) to the debug payload * redo of Pylons#299 * closes Pylons#236
- Loading branch information
Showing
4 changed files
with
347 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -50,6 +50,7 @@ testing = | |
WebTest | ||
nose | ||
coverage | ||
sqlalchemy | ||
docs = | ||
Sphinx >= 1.7.5 | ||
pylons-sphinx-themes >= 0.3 | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,266 @@ | ||
from pyramid import testing | ||
from pyramid.request import Request | ||
import sqlalchemy | ||
from sqlalchemy.sql import text as sqla_text | ||
import sys | ||
|
||
from pyramid_debugtoolbar.compat import PY3 | ||
|
||
from ._utils import ( | ||
_TestDebugtoolbarPanel, | ||
ok_response_factory, | ||
re_toolbar_link, | ||
) | ||
|
||
|
||
class _TestSQLAlchemyPanel(_TestDebugtoolbarPanel): | ||
""" | ||
Base class for testing SQLAlchemy panel | ||
""" | ||
|
||
config = None | ||
app = None | ||
|
||
def _sqlalchemy_view(self, context, request): | ||
""" | ||
This function should define a Pyramid view | ||
* (potentially) invoke SQLAlchemy | ||
* return a Response | ||
""" | ||
raise NotImplementedError() | ||
|
||
def setUp(self): | ||
self.config = config = testing.setUp() | ||
config.include("pyramid_debugtoolbar") | ||
config.add_view(self._sqlalchemy_view) | ||
self.app = config.make_wsgi_app() | ||
|
||
def tearDown(self): | ||
testing.tearDown() | ||
|
||
def _makeOne(self): | ||
""" | ||
Makes a request to the main App | ||
* which invokes `self._sqlalchemy_view` | ||
* Make a request to the toolbar | ||
* return the toolbar Response | ||
""" | ||
# make the app | ||
app = self.config.make_wsgi_app() | ||
# make a request | ||
req1 = Request.blank("/") | ||
req1.remote_addr = "127.0.0.1" | ||
resp1 = req1.get_response(app) | ||
self.assertEqual(resp1.status_code, 200) | ||
self.assertIn("http://localhost/_debug_toolbar/", resp1.text) | ||
|
||
# check the toolbar | ||
links = re_toolbar_link.findall(resp1.text) | ||
self.assertIsNotNone(links) | ||
self.assertIsInstance(links, list) | ||
self.assertEqual(len(links), 1) | ||
toolbar_link = links[0] | ||
|
||
req2 = Request.blank(toolbar_link) | ||
req2.remote_addr = "127.0.0.1" | ||
resp2 = req2.get_response(app) | ||
|
||
return resp2 | ||
|
||
def _check_rendered__panel(self, resp): | ||
""" | ||
Ensure the rendered panel exists with statements | ||
""" | ||
self.assertIn('<li class="" id="pDebugPanel-sqlalchemy">', resp.text) | ||
self.assertIn( | ||
'<div id="pDebugPanel-sqlalchemy-content" class="panelContent" ' | ||
'style="display: none;">', | ||
resp.text, | ||
) | ||
|
||
def _check_rendered__select_null(self, resp): | ||
""" | ||
Ensure the rendered panel has the "SELECT NULL" statement rendered | ||
Note: the <pre> styles are different on the Py2 and Py3 libraries | ||
""" | ||
self.assertIn( | ||
'<span style="color: #008800; font-weight: bold">SELECT</span> ' | ||
'<span style="color: #008800; font-weight: bold">NULL</span>', | ||
resp.text, | ||
) | ||
|
||
def _check_rendered__begin_rollback(self, resp): | ||
self.assertIn( | ||
'<span style="color: #008800; font-weight: bold">begin</span>;\n', | ||
resp.text, | ||
) | ||
self.assertIn( | ||
'<span style="color: #008800; font-weight: bold">rollback</span>;' | ||
'\n', | ||
resp.text, | ||
) | ||
|
||
def _check_rendered__begin_commit(self, resp): | ||
self.assertIn( | ||
'<span style="color: #008800; font-weight: bold">begin</span>;\n', | ||
resp.text, | ||
) | ||
self.assertIn( | ||
'<span style="color: #008800; font-weight: bold">commit</span>;\n', | ||
resp.text, | ||
) | ||
|
||
|
||
class TestNone(_TestSQLAlchemyPanel): | ||
""" | ||
No SQLAlchemy queries | ||
""" | ||
|
||
def _sqlalchemy_view(self, context, request): | ||
return ok_response_factory() | ||
|
||
def test_panel(self): | ||
resp = self._makeOne() | ||
self.assertEqual(resp.status_code, 200) | ||
self.assertIn( | ||
'<li class="disabled" id="pDebugPanel-sqlalchemy">', resp.text | ||
) | ||
self.assertNotIn( | ||
'<div id="pDebugPanel-sqlalchemy-content" class="panelContent" ' | ||
'style="display: none;">', | ||
resp.text, | ||
) | ||
|
||
|
||
class TestSimpleSelect(_TestSQLAlchemyPanel): | ||
""" | ||
A simple SELECT | ||
""" | ||
|
||
def _sqlalchemy_view(self, context, request): | ||
engine = sqlalchemy.create_engine("sqlite://", isolation_level=None) | ||
conn = engine.connect() | ||
stmt = sqla_text("SELECT NULL;") | ||
conn.execute(stmt) # noqa | ||
return ok_response_factory() | ||
|
||
def test_panel(self): | ||
resp = self._makeOne() | ||
self.assertEqual(resp.status_code, 200) | ||
self._check_rendered__panel(resp) | ||
self._check_rendered__select_null(resp) | ||
|
||
|
||
class TestTransactionCommit(_TestSQLAlchemyPanel): | ||
""" | ||
A simple transaction | ||
""" | ||
|
||
def _sqlalchemy_view(self, context, request): | ||
engine = sqlalchemy.create_engine("sqlite://", isolation_level=None) | ||
conn = engine.connect() | ||
with conn.begin(): | ||
stmt = sqla_text("SELECT NULL;") | ||
conn.execute(stmt) # noqa | ||
return ok_response_factory() | ||
|
||
def test_panel(self): | ||
resp = self._makeOne() | ||
self.assertEqual(resp.status_code, 200) | ||
self._check_rendered__panel(resp) | ||
self._check_rendered__select_null(resp) | ||
self._check_rendered__begin_commit(resp) | ||
|
||
|
||
class TestTransactionRollback(_TestSQLAlchemyPanel): | ||
def _sqlalchemy_view(self, context, request): | ||
engine = sqlalchemy.create_engine("sqlite://", isolation_level=None) | ||
conn = engine.connect() | ||
try: | ||
with conn.begin(): | ||
stmt = sqla_text("SELECT NULL;") | ||
conn.execute(stmt) # noqa | ||
raise ValueError("EXPECTED") | ||
except ValueError: | ||
# SQLAlchemy's ContextManager will call a rollback | ||
pass | ||
return ok_response_factory() | ||
|
||
def test_panel(self): | ||
resp = self._makeOne() | ||
self.assertEqual(resp.status_code, 200) | ||
self._check_rendered__panel(resp) | ||
self._check_rendered__select_null(resp) | ||
self._check_rendered__begin_rollback(resp) | ||
|
||
|
||
class TestTransactionComplex(_TestSQLAlchemyPanel): | ||
def _sqlalchemy_view(self, context, request): | ||
""" | ||
Test to ensure the following listeners do not raise Exceptions: | ||
[+] begin | ||
[+] commit | ||
[+] rollback | ||
[+] savepoint | ||
[+] rollback_savepoint | ||
[+] release_savepoint | ||
[+] begin_twophase | ||
[ ] prepare_twophase | ||
[ ] commit_twophase | ||
[ ] rollback_twophase | ||
""" | ||
|
||
engine = sqlalchemy.create_engine("sqlite://", isolation_level=None) | ||
if not PY3 or (sys.version_info[1] <= 5): | ||
# under Python2-Python3.5, Sqlite needs workaround to support savepoints | ||
# see https://docs.sqlalchemy.org/en/13/dialects/sqlite.html | ||
|
||
@sqlalchemy.event.listens_for(engine, "connect") | ||
def _do_connect(dbapi_connection, connection_record): | ||
# disable pysqlite's emitting of the BEGIN statement entirely. | ||
# also stops it from emitting COMMIT before any DDL. | ||
dbapi_connection.isolation_level = None | ||
|
||
@sqlalchemy.event.listens_for(engine, "begin") | ||
def _do_begin(conn): | ||
conn.execute("BEGIN") | ||
|
||
conn = engine.connect() | ||
|
||
# tests: `begin`, `commit` | ||
t1 = conn.begin() # `begin` | ||
conn.execute(sqla_text("SELECT NULL; -- a")) | ||
t1.commit() # `commit` | ||
|
||
# tests: `begin`, `rollback` | ||
t1 = conn.begin() # `begin` | ||
conn.execute(sqla_text("SELECT NULL; -- b")) | ||
t1.rollback() # `rollback` | ||
|
||
# tests: `begin`, `savepoint`, `rollback_savepoint`, `release_savepoint` | ||
t1 = conn.begin() # `begin` | ||
conn.execute(sqla_text("SELECT NULL; -- c1")) | ||
t2 = conn.begin_nested() # `savepoint` | ||
conn.execute(sqla_text("SELECT NULL; -- c2")) | ||
t2.rollback() # `rollback_savepoint` | ||
t2 = conn.begin_nested() # `savepoint` | ||
conn.execute(sqla_text("SELECT NULL; -- c3")) | ||
t2.commit() # `release_savepoint` | ||
conn.execute(sqla_text("SELECT NULL; -- c4")) | ||
t1.commit() # commit | ||
|
||
# tests: `begin_twophase` | ||
try: | ||
conn.begin_twophase() # `begin_twophase` | ||
except NotImplementedError: | ||
pass | ||
|
||
# untested: `prepare_twophase`, `commit_twophase`, `rollback_twophase` | ||
|
||
return ok_response_factory() | ||
|
||
def test_panel(self): | ||
resp = self._makeOne() | ||
self.assertEqual(resp.status_code, 200) | ||
self._check_rendered__panel(resp) |