8687.15.18
by Karl Fogel
Add the copyright header block to files under lib/canonical/. |
1 |
# Copyright 2009 Canonical Ltd. This software is licensed under the
|
2 |
# GNU Affero General Public License version 3 (see the file LICENSE).
|
|
5398.8.2
by Stuart Bishop
Mock database work in progress |
3 |
|
4 |
"""A self maintaining mock database for tests.
|
|
5 |
||
5398.8.19
by Stuart Bishop
Review updates |
6 |
The first time a `MockDbConnection` is used, it functions as a proxy
|
7 |
to the real database connection. Queries and results are recorded into
|
|
5398.8.22
by Stuart Bishop
Review part 1 updates |
8 |
a script.
|
5398.8.2
by Stuart Bishop
Mock database work in progress |
9 |
|
10 |
For subsequent runs, if the same queries are issued in the same order
|
|
5398.8.22
by Stuart Bishop
Review part 1 updates |
11 |
then results are returned from the script and the real database is
|
12 |
not used. If the script is detected as being invalid, it is removed and
|
|
5398.8.19
by Stuart Bishop
Review updates |
13 |
a `RetryTest` exception raised for the test runner to deal with.
|
5398.8.2
by Stuart Bishop
Mock database work in progress |
14 |
"""
|
15 |
||
16 |
__metaclass__ = type |
|
5398.8.13
by Stuart Bishop
Ensure unittests don't leak RetryTest exceptions |
17 |
__all__ = [ |
5398.8.22
by Stuart Bishop
Review part 1 updates |
18 |
'MockDbConnection', 'script_filename', |
19 |
'ScriptPlayer', 'ScriptRecorder', |
|
5398.8.13
by Stuart Bishop
Ensure unittests don't leak RetryTest exceptions |
20 |
]
|
5398.8.2
by Stuart Bishop
Mock database work in progress |
21 |
|
14612.2.3
by William Grant
Rerun over lib/lp |
22 |
import cPickle as pickle |
5398.8.2
by Stuart Bishop
Mock database work in progress |
23 |
import gzip |
24 |
import os.path |
|
5398.8.5
by Stuart Bishop
Connection parameter checking, safe cache filenames |
25 |
import urllib |
5398.8.2
by Stuart Bishop
Mock database work in progress |
26 |
|
5821.2.32
by James Henstridge
* s/psycopg/psycopg2/ in a few more places. |
27 |
import psycopg2 |
14612.2.1
by William Grant
format-imports on lib/. So many imports. |
28 |
|
8521.3.2
by Gary Poster
get retry tests to pass |
29 |
# from zope.testing.testrunner import RetryTest
|
5398.8.2
by Stuart Bishop
Mock database work in progress |
30 |
|
14605.1.1
by Curtis Hovey
Moved canonical.config to lp.services. |
31 |
from lp.services.config import config |
5398.8.2
by Stuart Bishop
Mock database work in progress |
32 |
|
5398.8.4
by Stuart Bishop
Exception handling and many more tests |
33 |
|
5398.8.22
by Stuart Bishop
Review part 1 updates |
34 |
SCRIPT_DIR = os.path.join(config.root, 'mockdbscripts~') |
35 |
||
36 |
||
37 |
def script_filename(key): |
|
38 |
"""Calculate and return the script filename to use."""
|
|
5398.8.5
by Stuart Bishop
Connection parameter checking, safe cache filenames |
39 |
key = urllib.quote(key, safe='') |
5398.8.22
by Stuart Bishop
Review part 1 updates |
40 |
return os.path.join(SCRIPT_DIR, key) + '.pickle.gz' |
41 |
||
42 |
||
43 |
class ScriptEntry: |
|
5398.8.2
by Stuart Bishop
Mock database work in progress |
44 |
"""An entry in our test's log of database calls."""
|
45 |
||
46 |
# The connection number used for this command. All connections used
|
|
47 |
# by a test store their commands in a single list to preserve global
|
|
48 |
# ordering, and we use connection_number to differentiate them.
|
|
49 |
connection_number = None |
|
50 |
||
51 |
# If the command raised an exception, it is stored here.
|
|
5398.8.4
by Stuart Bishop
Exception handling and many more tests |
52 |
exception = None |
5398.8.2
by Stuart Bishop
Mock database work in progress |
53 |
|
54 |
def __init__(self, connection): |
|
5398.8.20
by Stuart Bishop
Refactor connection logic so failed connection attempts are logged. |
55 |
if connection is not None: |
56 |
self.connection_number = connection.connection_number |
|
5398.8.2
by Stuart Bishop
Mock database work in progress |
57 |
|
58 |
||
5398.8.22
by Stuart Bishop
Review part 1 updates |
59 |
class ConnectScriptEntry(ScriptEntry): |
5398.8.6
by Stuart Bishop
Working prototype |
60 |
"""An entry created instantiating a Connection."""
|
5398.8.19
by Stuart Bishop
Review updates |
61 |
args = None # Arguments passed to the connect() method. |
62 |
kw = None # Keyword arguments passed to the connect() method. |
|
5398.8.6
by Stuart Bishop
Working prototype |
63 |
|
64 |
def __init__(self, connection, *args, **kw): |
|
5398.8.22
by Stuart Bishop
Review part 1 updates |
65 |
super(ConnectScriptEntry, self).__init__(connection) |
5398.8.6
by Stuart Bishop
Working prototype |
66 |
self.args = args |
67 |
self.kw = kw |
|
68 |
||
69 |
||
5398.8.22
by Stuart Bishop
Review part 1 updates |
70 |
class ExecuteScriptEntry(ScriptEntry): |
5398.8.2
by Stuart Bishop
Mock database work in progress |
71 |
"""An entry created via Cursor.execute()."""
|
5398.8.19
by Stuart Bishop
Review updates |
72 |
query = None # Query passed to Cursor.execute(). |
73 |
params = None # Parameters passed to Cursor.execute(). |
|
74 |
results = None # Cursor.fetchall() results as a list. |
|
75 |
description = None # Cursor.description as per DB-API. |
|
76 |
rowcount = None # Cursor.rowcount after Cursor.fetchall() as per DB-API. |
|
77 |
||
78 |
def __init__(self, connection, query, params): |
|
5398.8.22
by Stuart Bishop
Review part 1 updates |
79 |
super(ExecuteScriptEntry, self).__init__(connection) |
5398.8.19
by Stuart Bishop
Review updates |
80 |
self.query = query |
81 |
self.params = params |
|
5398.8.2
by Stuart Bishop
Mock database work in progress |
82 |
|
83 |
||
5398.8.22
by Stuart Bishop
Review part 1 updates |
84 |
class CloseScriptEntry(ScriptEntry): |
5398.8.2
by Stuart Bishop
Mock database work in progress |
85 |
"""An entry created via Connection.close()."""
|
86 |
||
87 |
||
5398.8.22
by Stuart Bishop
Review part 1 updates |
88 |
class CommitScriptEntry(ScriptEntry): |
5398.8.2
by Stuart Bishop
Mock database work in progress |
89 |
"""An entry created via Connection.commit()."""
|
90 |
||
91 |
||
5398.8.22
by Stuart Bishop
Review part 1 updates |
92 |
class RollbackScriptEntry(ScriptEntry): |
5398.8.2
by Stuart Bishop
Mock database work in progress |
93 |
"""An entry created via Connection.rollback()."""
|
94 |
||
95 |
||
5398.8.22
by Stuart Bishop
Review part 1 updates |
96 |
class SetIsolationLevelScriptEntry(ScriptEntry): |
5398.8.6
by Stuart Bishop
Working prototype |
97 |
"""An entry created via Connection.set_isolation_level()."""
|
98 |
level = None # The requested isolation level |
|
99 |
def __init__(self, connection, level): |
|
5398.8.22
by Stuart Bishop
Review part 1 updates |
100 |
super(SetIsolationLevelScriptEntry, self).__init__(connection) |
5398.8.6
by Stuart Bishop
Working prototype |
101 |
self.level = level |
102 |
||
103 |
||
5398.8.22
by Stuart Bishop
Review part 1 updates |
104 |
class ScriptRecorder: |
105 |
key = None # A unique key identifying this test. |
|
106 |
script_filename = None # Path to our script file. |
|
5398.8.2
by Stuart Bishop
Mock database work in progress |
107 |
log = None |
108 |
connections = None |
|
109 |
||
5398.8.5
by Stuart Bishop
Connection parameter checking, safe cache filenames |
110 |
def __init__(self, key): |
111 |
self.key = key |
|
5398.8.22
by Stuart Bishop
Review part 1 updates |
112 |
self.script_filename = script_filename(key) |
5398.8.2
by Stuart Bishop
Mock database work in progress |
113 |
self.log = [] |
114 |
self.connections = [] |
|
115 |
||
5398.8.20
by Stuart Bishop
Refactor connection logic so failed connection attempts are logged. |
116 |
def connect(self, connect_func, *args, **kw): |
117 |
"""Open a connection to the database, returning a `MockDbConnection`.
|
|
118 |
"""
|
|
119 |
try: |
|
120 |
connection = connect_func(*args, **kw) |
|
121 |
exception = None |
|
5821.2.32
by James Henstridge
* s/psycopg/psycopg2/ in a few more places. |
122 |
except (psycopg2.Warning, psycopg2.Error), connect_exception: |
5398.8.20
by Stuart Bishop
Refactor connection logic so failed connection attempts are logged. |
123 |
connection = None |
124 |
exception = connect_exception |
|
125 |
||
126 |
connection = MockDbConnection(self, connection, *args, **kw) |
|
127 |
||
5398.8.6
by Stuart Bishop
Working prototype |
128 |
self.connections.append(connection) |
5398.8.20
by Stuart Bishop
Refactor connection logic so failed connection attempts are logged. |
129 |
if connection is not None: |
130 |
connection.connection_number = self.connections.index(connection) |
|
5398.8.22
by Stuart Bishop
Review part 1 updates |
131 |
entry = ConnectScriptEntry(connection, *args, **kw) |
5398.8.6
by Stuart Bishop
Working prototype |
132 |
self.log.append(entry) |
5398.8.20
by Stuart Bishop
Refactor connection logic so failed connection attempts are logged. |
133 |
if exception: |
134 |
entry.exception = exception |
|
5398.8.27
by Stuart Bishop
Review and lint fixes |
135 |
#pylint: disable-msg=W0706
|
5398.8.20
by Stuart Bishop
Refactor connection logic so failed connection attempts are logged. |
136 |
raise exception |
137 |
return connection |
|
5398.8.5
by Stuart Bishop
Connection parameter checking, safe cache filenames |
138 |
|
5398.8.22
by Stuart Bishop
Review part 1 updates |
139 |
def cursor(self, connection): |
140 |
"""Return a MockDbCursor."""
|
|
141 |
real_cursor = connection.real_connection.cursor() |
|
142 |
return MockDbCursor(connection, real_cursor) |
|
143 |
||
5398.8.2
by Stuart Bishop
Mock database work in progress |
144 |
def execute(self, cursor, query, params=None): |
145 |
"""Handle Cursor.execute()."""
|
|
5398.8.4
by Stuart Bishop
Exception handling and many more tests |
146 |
con = cursor.connection |
5398.8.22
by Stuart Bishop
Review part 1 updates |
147 |
entry = ExecuteScriptEntry(con, query, params) |
5398.8.2
by Stuart Bishop
Mock database work in progress |
148 |
|
149 |
real_cursor = cursor.real_cursor |
|
5398.8.4
by Stuart Bishop
Exception handling and many more tests |
150 |
try: |
151 |
real_cursor.execute(query, params) |
|
5821.2.32
by James Henstridge
* s/psycopg/psycopg2/ in a few more places. |
152 |
except (psycopg2.Warning, psycopg2.Error), exception: |
5398.8.4
by Stuart Bishop
Exception handling and many more tests |
153 |
entry.exception = exception |
154 |
self.log.append(entry) |
|
155 |
raise
|
|
5398.8.2
by Stuart Bishop
Mock database work in progress |
156 |
|
5398.8.17
by Stuart Bishop
Review feedback and test improvements |
157 |
entry.rowcount = real_cursor.rowcount |
5398.8.4
by Stuart Bishop
Exception handling and many more tests |
158 |
try: |
159 |
entry.results = list(real_cursor.fetchall()) |
|
160 |
entry.description = real_cursor.description |
|
5398.8.19
by Stuart Bishop
Review updates |
161 |
# Might have changed now fetchall() has been done.
|
162 |
entry.rowcount = real_cursor.rowcount |
|
5821.4.22
by James Henstridge
fix the canonical.testing tests |
163 |
except psycopg2.ProgrammingError, exc: |
164 |
if exc.args[0] == 'no results to fetch': |
|
165 |
# No results, such as an UPDATE query.
|
|
166 |
entry.results = None |
|
167 |
else: |
|
168 |
raise
|
|
5398.8.2
by Stuart Bishop
Mock database work in progress |
169 |
|
170 |
self.log.append(entry) |
|
171 |
return entry |
|
172 |
||
173 |
def close(self, connection): |
|
174 |
"""Handle Connection.close()."""
|
|
5398.8.22
by Stuart Bishop
Review part 1 updates |
175 |
entry = CloseScriptEntry(connection) |
5398.8.4
by Stuart Bishop
Exception handling and many more tests |
176 |
try: |
5398.8.20
by Stuart Bishop
Refactor connection logic so failed connection attempts are logged. |
177 |
if connection.real_connection is not None: |
178 |
connection.real_connection.close() |
|
5821.2.32
by James Henstridge
* s/psycopg/psycopg2/ in a few more places. |
179 |
except (psycopg2.Warning, psycopg2.Error), exception: |
5398.8.4
by Stuart Bishop
Exception handling and many more tests |
180 |
entry.exception = exception |
181 |
self.log.append(entry) |
|
182 |
raise
|
|
5398.8.24
by Stuart Bishop
Review feedback |
183 |
else: |
184 |
self.log.append(entry) |
|
5398.8.2
by Stuart Bishop
Mock database work in progress |
185 |
|
186 |
def commit(self, connection): |
|
187 |
"""Handle Connection.commit()."""
|
|
5398.8.22
by Stuart Bishop
Review part 1 updates |
188 |
entry = CommitScriptEntry(connection) |
5398.8.4
by Stuart Bishop
Exception handling and many more tests |
189 |
try: |
190 |
connection.real_connection.commit() |
|
5821.2.32
by James Henstridge
* s/psycopg/psycopg2/ in a few more places. |
191 |
except (psycopg2.Warning, psycopg2.Error), exception: |
5398.8.4
by Stuart Bishop
Exception handling and many more tests |
192 |
entry.exception = exception |
193 |
self.log.append(entry) |
|
5398.8.17
by Stuart Bishop
Review feedback and test improvements |
194 |
raise
|
5398.8.24
by Stuart Bishop
Review feedback |
195 |
else: |
196 |
self.log.append(entry) |
|
5398.8.2
by Stuart Bishop
Mock database work in progress |
197 |
|
198 |
def rollback(self, connection): |
|
199 |
"""Handle Connection.rollback()."""
|
|
5398.8.22
by Stuart Bishop
Review part 1 updates |
200 |
entry = RollbackScriptEntry(connection) |
5398.8.4
by Stuart Bishop
Exception handling and many more tests |
201 |
try: |
202 |
connection.real_connection.rollback() |
|
5821.2.32
by James Henstridge
* s/psycopg/psycopg2/ in a few more places. |
203 |
except (psycopg2.Warning, psycopg2.Error), exception: |
5398.8.4
by Stuart Bishop
Exception handling and many more tests |
204 |
entry.exception = exception |
205 |
self.log.append(entry) |
|
5398.8.17
by Stuart Bishop
Review feedback and test improvements |
206 |
raise
|
5398.8.24
by Stuart Bishop
Review feedback |
207 |
else: |
208 |
self.log.append(entry) |
|
5398.8.2
by Stuart Bishop
Mock database work in progress |
209 |
|
5398.8.6
by Stuart Bishop
Working prototype |
210 |
def set_isolation_level(self, connection, level): |
211 |
"""Handle Connection.set_isolation_level()."""
|
|
5398.8.22
by Stuart Bishop
Review part 1 updates |
212 |
entry = SetIsolationLevelScriptEntry(connection, level) |
5398.8.6
by Stuart Bishop
Working prototype |
213 |
try: |
214 |
connection.real_connection.set_isolation_level(level) |
|
5821.2.32
by James Henstridge
* s/psycopg/psycopg2/ in a few more places. |
215 |
except (psycopg2.Warning, psycopg2.Error), exception: |
5398.8.6
by Stuart Bishop
Working prototype |
216 |
entry.exception = exception |
217 |
self.log.append(entry) |
|
5398.8.17
by Stuart Bishop
Review feedback and test improvements |
218 |
raise
|
5398.8.24
by Stuart Bishop
Review feedback |
219 |
else: |
220 |
self.log.append(entry) |
|
5398.8.6
by Stuart Bishop
Working prototype |
221 |
|
5398.8.2
by Stuart Bishop
Mock database work in progress |
222 |
def store(self): |
5398.8.22
by Stuart Bishop
Review part 1 updates |
223 |
"""Store the script for future use by a ScriptPlayer."""
|
224 |
# Create script directory if necessary.
|
|
225 |
if not os.path.isdir(SCRIPT_DIR): |
|
226 |
os.makedirs(SCRIPT_DIR, mode=0700) |
|
5398.8.5
by Stuart Bishop
Connection parameter checking, safe cache filenames |
227 |
|
5398.8.22
by Stuart Bishop
Review part 1 updates |
228 |
# Save log to a pickle. The pickle contains the key for this test,
|
229 |
# followed by a list of ScriptEntry-derived objects.
|
|
5398.8.10
by Stuart Bishop
Remove cruft from cache |
230 |
obj_to_store = [self.key] + self.log |
5398.8.2
by Stuart Bishop
Mock database work in progress |
231 |
pickle.dump( |
5398.8.22
by Stuart Bishop
Review part 1 updates |
232 |
obj_to_store, gzip.open(self.script_filename, 'wb'), |
5398.8.2
by Stuart Bishop
Mock database work in progress |
233 |
pickle.HIGHEST_PROTOCOL |
234 |
)
|
|
235 |
||
236 |
# Trash all the connected connections. This isn't strictly necessary
|
|
237 |
# but protects us from silly mistakes.
|
|
238 |
while self.connections: |
|
239 |
con = self.connections.pop() |
|
5398.8.20
by Stuart Bishop
Refactor connection logic so failed connection attempts are logged. |
240 |
if con is not None and not con._closed: |
5398.8.2
by Stuart Bishop
Mock database work in progress |
241 |
con.close() |
242 |
||
243 |
||
5398.8.15
by Stuart Bishop
Make mock db survive Layer teardowns |
244 |
def noop_if_invalid(func): |
245 |
"""Decorator that causes the decorated method to be a noop if the
|
|
5398.8.22
by Stuart Bishop
Review part 1 updates |
246 |
script this method belongs too is invalid.
|
5398.8.15
by Stuart Bishop
Make mock db survive Layer teardowns |
247 |
|
5398.8.22
by Stuart Bishop
Review part 1 updates |
248 |
This allows teardown to complete when a ReplayScript has
|
5398.8.15
by Stuart Bishop
Make mock db survive Layer teardowns |
249 |
raised a RetryTest exception. Normally during teardown DB operations
|
250 |
are made when doing things like aborting the transaction.
|
|
251 |
"""
|
|
252 |
def dont_retry_func(self, *args, **kw): |
|
253 |
if self.invalid: |
|
254 |
return None |
|
255 |
else: |
|
256 |
return func(self, *args, **kw) |
|
257 |
return dont_retry_func |
|
258 |
||
259 |
||
5398.8.22
by Stuart Bishop
Review part 1 updates |
260 |
class ScriptPlayer: |
261 |
"""Replay database queries from a script."""
|
|
5398.8.5
by Stuart Bishop
Connection parameter checking, safe cache filenames |
262 |
|
5398.8.19
by Stuart Bishop
Review updates |
263 |
key = None # Unique key identifying this test. |
5398.8.22
by Stuart Bishop
Review part 1 updates |
264 |
script_filename = None # File storing our statement/result script. |
265 |
log = None # List of ScriptEntry objects loaded from _script_filename. |
|
266 |
connections = None # List of connections using this script. |
|
5398.8.5
by Stuart Bishop
Connection parameter checking, safe cache filenames |
267 |
|
5398.8.22
by Stuart Bishop
Review part 1 updates |
268 |
invalid = False # If True, the script is invalid and we are tearing down. |
5398.8.15
by Stuart Bishop
Make mock db survive Layer teardowns |
269 |
|
5398.8.5
by Stuart Bishop
Connection parameter checking, safe cache filenames |
270 |
def __init__(self, key): |
271 |
self.key = key |
|
5398.8.22
by Stuart Bishop
Review part 1 updates |
272 |
self.script_filename = script_filename(key) |
273 |
self.log = pickle.load(gzip.open(self.script_filename, 'rb')) |
|
5398.8.5
by Stuart Bishop
Connection parameter checking, safe cache filenames |
274 |
try: |
275 |
stored_key = self.log.pop(0) |
|
5398.8.22
by Stuart Bishop
Review part 1 updates |
276 |
assert stored_key == key, "Script loaded for wrong key." |
5398.8.5
by Stuart Bishop
Connection parameter checking, safe cache filenames |
277 |
except IndexError: |
5398.8.22
by Stuart Bishop
Review part 1 updates |
278 |
self.handleInvalidScript( |
279 |
"Connection key not stored in script."
|
|
5398.8.5
by Stuart Bishop
Connection parameter checking, safe cache filenames |
280 |
)
|
281 |
||
282 |
self.connections = [] |
|
283 |
||
284 |
def getNextEntry(self, connection, expected_entry_class): |
|
5398.8.22
by Stuart Bishop
Review part 1 updates |
285 |
"""Pull the next entry from the script.
|
5398.8.5
by Stuart Bishop
Connection parameter checking, safe cache filenames |
286 |
|
5398.8.22
by Stuart Bishop
Review part 1 updates |
287 |
Invokes handleInvalidScript on error, including some entry validation.
|
5398.8.5
by Stuart Bishop
Connection parameter checking, safe cache filenames |
288 |
"""
|
289 |
try: |
|
290 |
entry = self.log.pop(0) |
|
291 |
except IndexError: |
|
5398.8.22
by Stuart Bishop
Review part 1 updates |
292 |
self.handleInvalidScript('Ran out of commands.') |
5398.8.10
by Stuart Bishop
Remove cruft from cache |
293 |
|
5398.8.22
by Stuart Bishop
Review part 1 updates |
294 |
# This guards against file format changes as well as file corruption.
|
295 |
if not isinstance(entry, ScriptEntry): |
|
296 |
self.handleInvalidScript('Unexpected object type in script') |
|
5398.8.5
by Stuart Bishop
Connection parameter checking, safe cache filenames |
297 |
|
298 |
if connection.connection_number != entry.connection_number: |
|
5398.8.22
by Stuart Bishop
Review part 1 updates |
299 |
self.handleInvalidScript( |
5398.8.20
by Stuart Bishop
Refactor connection logic so failed connection attempts are logged. |
300 |
'Expected query to connection %s ' |
301 |
'but got query to connection %s' |
|
5398.8.5
by Stuart Bishop
Connection parameter checking, safe cache filenames |
302 |
% (entry.connection_number, connection.connection_number) |
303 |
)
|
|
304 |
||
305 |
if not isinstance(entry, expected_entry_class): |
|
5398.8.22
by Stuart Bishop
Review part 1 updates |
306 |
self.handleInvalidScript( |
5398.8.5
by Stuart Bishop
Connection parameter checking, safe cache filenames |
307 |
'Expected %s but got %s' |
308 |
% (expected_entry_class, entry.__class__) |
|
309 |
)
|
|
310 |
||
311 |
return entry |
|
312 |
||
5398.8.15
by Stuart Bishop
Make mock db survive Layer teardowns |
313 |
@noop_if_invalid
|
5398.8.20
by Stuart Bishop
Refactor connection logic so failed connection attempts are logged. |
314 |
def connect(self, connect_func, *args, **kw): |
315 |
"""Return a `MockDbConnection`.
|
|
316 |
|
|
317 |
Does not actually connect to the database - we are in replay mode.
|
|
318 |
"""
|
|
319 |
connection = MockDbConnection(self, None, *args, **kw) |
|
5398.8.6
by Stuart Bishop
Working prototype |
320 |
self.connections.append(connection) |
321 |
connection.connection_number = self.connections.index(connection) |
|
5398.8.22
by Stuart Bishop
Review part 1 updates |
322 |
entry = self.getNextEntry(connection, ConnectScriptEntry) |
5398.8.6
by Stuart Bishop
Working prototype |
323 |
if (entry.args, entry.kw) != (args, kw): |
5398.8.22
by Stuart Bishop
Review part 1 updates |
324 |
self.handleInvalidScript("Connection parameters have changed.") |
5398.8.20
by Stuart Bishop
Refactor connection logic so failed connection attempts are logged. |
325 |
if entry.exception is not None: |
326 |
raise entry.exception |
|
327 |
return connection |
|
5398.8.6
by Stuart Bishop
Working prototype |
328 |
|
5398.8.22
by Stuart Bishop
Review part 1 updates |
329 |
def cursor(self, connection): |
330 |
"""Return a MockDbCursor."""
|
|
331 |
return MockDbCursor(connection, real_cursor=None) |
|
332 |
||
5398.8.15
by Stuart Bishop
Make mock db survive Layer teardowns |
333 |
@noop_if_invalid
|
5398.8.5
by Stuart Bishop
Connection parameter checking, safe cache filenames |
334 |
def execute(self, cursor, query, params=None): |
335 |
"""Handle Cursor.execute()."""
|
|
336 |
connection = cursor.connection |
|
5398.8.22
by Stuart Bishop
Review part 1 updates |
337 |
entry = self.getNextEntry(connection, ExecuteScriptEntry) |
5398.8.5
by Stuart Bishop
Connection parameter checking, safe cache filenames |
338 |
|
339 |
if query != entry.query: |
|
5398.8.22
by Stuart Bishop
Review part 1 updates |
340 |
self.handleInvalidScript( |
5398.8.5
by Stuart Bishop
Connection parameter checking, safe cache filenames |
341 |
'Unexpected command. Expected %s. Got %s.' |
5398.8.6
by Stuart Bishop
Working prototype |
342 |
% (entry.query, query) |
5398.8.5
by Stuart Bishop
Connection parameter checking, safe cache filenames |
343 |
)
|
344 |
||
345 |
if params != entry.params: |
|
5398.8.22
by Stuart Bishop
Review part 1 updates |
346 |
self.handleInvalidScript( |
5398.8.5
by Stuart Bishop
Connection parameter checking, safe cache filenames |
347 |
'Unexpected parameters. Expected %r. Got %r.' |
348 |
% (entry.params, params) |
|
349 |
)
|
|
350 |
||
351 |
if entry.exception is not None: |
|
352 |
raise entry.exception |
|
353 |
||
354 |
return entry |
|
355 |
||
5398.8.15
by Stuart Bishop
Make mock db survive Layer teardowns |
356 |
@noop_if_invalid
|
5398.8.5
by Stuart Bishop
Connection parameter checking, safe cache filenames |
357 |
def close(self, connection): |
358 |
"""Handle Connection.close()."""
|
|
5398.8.22
by Stuart Bishop
Review part 1 updates |
359 |
entry = self.getNextEntry(connection, CloseScriptEntry) |
5398.8.5
by Stuart Bishop
Connection parameter checking, safe cache filenames |
360 |
if entry.exception is not None: |
361 |
raise entry.exception |
|
362 |
||
363 |
def commit(self, connection): |
|
364 |
"""Handle Connection.commit()."""
|
|
5398.8.22
by Stuart Bishop
Review part 1 updates |
365 |
entry = self.getNextEntry(connection, CommitScriptEntry) |
5398.8.5
by Stuart Bishop
Connection parameter checking, safe cache filenames |
366 |
if entry.exception is not None: |
367 |
raise entry.exception |
|
368 |
||
5398.8.15
by Stuart Bishop
Make mock db survive Layer teardowns |
369 |
@noop_if_invalid
|
5398.8.5
by Stuart Bishop
Connection parameter checking, safe cache filenames |
370 |
def rollback(self, connection): |
371 |
"""Handle Connection.rollback()."""
|
|
5398.8.22
by Stuart Bishop
Review part 1 updates |
372 |
entry = self.getNextEntry(connection, RollbackScriptEntry) |
5398.8.5
by Stuart Bishop
Connection parameter checking, safe cache filenames |
373 |
if entry.exception is not None: |
374 |
raise entry.exception |
|
375 |
||
5398.8.15
by Stuart Bishop
Make mock db survive Layer teardowns |
376 |
@noop_if_invalid
|
5398.8.6
by Stuart Bishop
Working prototype |
377 |
def set_isolation_level(self, connection, level): |
378 |
"""Handle Connection.set_isolation_level()."""
|
|
5398.8.22
by Stuart Bishop
Review part 1 updates |
379 |
entry = self.getNextEntry(connection, SetIsolationLevelScriptEntry) |
5398.8.6
by Stuart Bishop
Working prototype |
380 |
if entry.level != level: |
5398.8.22
by Stuart Bishop
Review part 1 updates |
381 |
self.handleInvalidScript("Different isolation level requested.") |
5398.8.6
by Stuart Bishop
Working prototype |
382 |
if entry.exception is not None: |
383 |
raise entry.exception |
|
384 |
||
5398.8.22
by Stuart Bishop
Review part 1 updates |
385 |
def handleInvalidScript(self, reason): |
386 |
"""Remove the script from disk and raise a RetryTest exception."""
|
|
387 |
if os.path.exists(self.script_filename): |
|
388 |
os.unlink(self.script_filename) |
|
5398.8.15
by Stuart Bishop
Make mock db survive Layer teardowns |
389 |
self.invalid = True |
8521.3.2
by Gary Poster
get retry tests to pass |
390 |
raise RetryTest(reason) # Leaving this as a name error: this should |
391 |
# not be called unless we reinstate the retry behavior in zope.testing.
|
|
5398.8.5
by Stuart Bishop
Connection parameter checking, safe cache filenames |
392 |
|
393 |
||
5398.8.2
by Stuart Bishop
Mock database work in progress |
394 |
class MockDbConnection: |
395 |
"""Connection to our Mock database."""
|
|
396 |
||
397 |
real_connection = None |
|
398 |
connection_number = None |
|
5398.8.22
by Stuart Bishop
Review part 1 updates |
399 |
script = None |
5398.8.2
by Stuart Bishop
Mock database work in progress |
400 |
|
5398.8.22
by Stuart Bishop
Review part 1 updates |
401 |
def __init__(self, script, real_connection, *args, **kw): |
5398.8.20
by Stuart Bishop
Refactor connection logic so failed connection attempts are logged. |
402 |
"""Initialize the `MockDbConnection`.
|
403 |
||
404 |
`MockDbConnection` intances are generally only created in the
|
|
5398.8.22
by Stuart Bishop
Review part 1 updates |
405 |
*Script.connect() methods as the attempt needs to be recorded
|
5398.8.20
by Stuart Bishop
Refactor connection logic so failed connection attempts are logged. |
406 |
(even if it fails).
|
5398.8.2
by Stuart Bishop
Mock database work in progress |
407 |
|
408 |
If we have a real_connection, we are proxying and recording results.
|
|
5398.8.22
by Stuart Bishop
Review part 1 updates |
409 |
If real_connection is None, we are replaying results from the script.
|
5398.8.5
by Stuart Bishop
Connection parameter checking, safe cache filenames |
410 |
|
411 |
*args and **kw are the arguments passed to open the real connection
|
|
5398.8.22
by Stuart Bishop
Review part 1 updates |
412 |
and are used by the script to confirm the db connection details have
|
413 |
not been changed; a `RetryTest` exception may be raised in replay mode.
|
|
5398.8.2
by Stuart Bishop
Mock database work in progress |
414 |
"""
|
5398.8.22
by Stuart Bishop
Review part 1 updates |
415 |
self.script = script |
5398.8.20
by Stuart Bishop
Refactor connection logic so failed connection attempts are logged. |
416 |
self.real_connection = real_connection |
5398.8.2
by Stuart Bishop
Mock database work in progress |
417 |
|
418 |
def cursor(self): |
|
419 |
"""As per DB-API."""
|
|
5398.8.22
by Stuart Bishop
Review part 1 updates |
420 |
return self.script.cursor(self) |
5398.8.2
by Stuart Bishop
Mock database work in progress |
421 |
|
422 |
_closed = False |
|
423 |
||
424 |
def _checkClosed(self): |
|
425 |
"""Guard that raises an exception if the connection is closed."""
|
|
426 |
if self._closed is True: |
|
5821.2.32
by James Henstridge
* s/psycopg/psycopg2/ in a few more places. |
427 |
raise psycopg2.InterfaceError('Connection closed.') |
5398.8.2
by Stuart Bishop
Mock database work in progress |
428 |
|
429 |
def close(self): |
|
430 |
"""As per DB-API."""
|
|
5398.8.20
by Stuart Bishop
Refactor connection logic so failed connection attempts are logged. |
431 |
# DB-API says closing a closed connection should raise an exception
|
432 |
# ("exception will be raised if any operation is attempted
|
|
433 |
# wht the [closed] connection"), but psycopg1 doesn't do this.
|
|
434 |
# It would be nice if our wrapper could be more strict than psycopg1,
|
|
435 |
# but unfortunately the sqlos/sqlobject combination relies on this
|
|
436 |
# behavior. So we have to emulate it.
|
|
437 |
if self._closed: |
|
438 |
return
|
|
5398.8.22
by Stuart Bishop
Review part 1 updates |
439 |
#self._checkClosed()
|
440 |
self.script.close(self) |
|
5398.8.2
by Stuart Bishop
Mock database work in progress |
441 |
self._closed = True |
442 |
||
443 |
def commit(self): |
|
444 |
"""As per DB-API."""
|
|
445 |
self._checkClosed() |
|
5398.8.22
by Stuart Bishop
Review part 1 updates |
446 |
self.script.commit(self) |
5398.8.2
by Stuart Bishop
Mock database work in progress |
447 |
|
448 |
def rollback(self): |
|
449 |
"""As per DB-API."""
|
|
450 |
self._checkClosed() |
|
5398.8.22
by Stuart Bishop
Review part 1 updates |
451 |
self.script.rollback(self) |
5398.8.2
by Stuart Bishop
Mock database work in progress |
452 |
|
5398.8.6
by Stuart Bishop
Working prototype |
453 |
def set_isolation_level(self, level): |
454 |
"""As per psycopg1 extension."""
|
|
455 |
self._checkClosed() |
|
5398.8.22
by Stuart Bishop
Review part 1 updates |
456 |
self.script.set_isolation_level(self, level) |
5398.8.6
by Stuart Bishop
Working prototype |
457 |
|
458 |
# Exceptions exposed on connection, as per optional DB-API extension.
|
|
459 |
## Disabled, as psycopg1 does not implement this extension.
|
|
460 |
## Warning = psycopg.Warning
|
|
461 |
## Error = psycopg.Error
|
|
462 |
## InterfaceError = psycopg.InterfaceError
|
|
463 |
## DatabaseError = psycopg.DatabaseError
|
|
464 |
## DataError = psycopg.DataError
|
|
465 |
## OperationalError = psycopg.OperationalError
|
|
466 |
## IntegrityError = psycopg.IntegrityError
|
|
467 |
## InternalError = psycopg.InternalError
|
|
468 |
## ProgrammingError = psycopg.ProgrammingError
|
|
469 |
## NotSupportedError = psycopg.NotSupportedError
|
|
470 |
||
5398.8.2
by Stuart Bishop
Mock database work in progress |
471 |
|
472 |
class MockDbCursor: |
|
5398.8.22
by Stuart Bishop
Review part 1 updates |
473 |
"""A fake DB-API cursor as produced by MockDbConnection.cursor.
|
474 |
|
|
475 |
The real work is done by the associated ScriptRecorder or ScriptPlayer
|
|
5398.8.24
by Stuart Bishop
Review feedback |
476 |
using the common interface, making this class independent on what
|
477 |
mode the mock database is running in.
|
|
5398.8.22
by Stuart Bishop
Review part 1 updates |
478 |
"""
|
5398.8.24
by Stuart Bishop
Review feedback |
479 |
# The ExecuteScriptEntry for the in progress query, if there is one.
|
480 |
# It stores any unfetched results and cursor metadata such as the
|
|
481 |
# resultset description.
|
|
5398.8.22
by Stuart Bishop
Review part 1 updates |
482 |
_script_entry = None |
5398.8.2
by Stuart Bishop
Mock database work in progress |
483 |
|
5398.8.19
by Stuart Bishop
Review updates |
484 |
arraysize = 100 # As per DB-API. |
485 |
connection = None # As per DB-API optional extension. |
|
5398.8.22
by Stuart Bishop
Review part 1 updates |
486 |
real_cursor = None # The real cursor if there is one. |
5398.8.4
by Stuart Bishop
Exception handling and many more tests |
487 |
|
5398.8.22
by Stuart Bishop
Review part 1 updates |
488 |
def __init__(self, connection, real_cursor=None): |
5398.8.2
by Stuart Bishop
Mock database work in progress |
489 |
self.connection = connection |
5398.8.22
by Stuart Bishop
Review part 1 updates |
490 |
self.real_cursor = real_cursor |
5398.8.2
by Stuart Bishop
Mock database work in progress |
491 |
|
492 |
@property
|
|
493 |
def description(self): |
|
5398.8.22
by Stuart Bishop
Review part 1 updates |
494 |
"""As per DB-API, pulled from the script entry."""
|
495 |
if self._script_entry is None: |
|
5398.8.2
by Stuart Bishop
Mock database work in progress |
496 |
return None |
5398.8.22
by Stuart Bishop
Review part 1 updates |
497 |
return self._script_entry.description |
5398.8.2
by Stuart Bishop
Mock database work in progress |
498 |
|
499 |
@property
|
|
500 |
def rowcount(self): |
|
501 |
"""Return the rowcount only if all the results have been consumed.
|
|
5398.8.22
by Stuart Bishop
Review part 1 updates |
502 |
|
503 |
As per DB-API, pulled from the script entry.
|
|
5398.8.2
by Stuart Bishop
Mock database work in progress |
504 |
"""
|
5398.8.22
by Stuart Bishop
Review part 1 updates |
505 |
if not isinstance(self._script_entry, ExecuteScriptEntry): |
5398.8.2
by Stuart Bishop
Mock database work in progress |
506 |
return -1 |
5398.8.17
by Stuart Bishop
Review feedback and test improvements |
507 |
|
5398.8.22
by Stuart Bishop
Review part 1 updates |
508 |
results = self._script_entry.results |
5398.8.17
by Stuart Bishop
Review feedback and test improvements |
509 |
|
5398.8.19
by Stuart Bishop
Review updates |
510 |
if results is None: # DELETE or UPDATE set rowcount. |
5398.8.22
by Stuart Bishop
Review part 1 updates |
511 |
return self._script_entry.rowcount |
5398.8.17
by Stuart Bishop
Review feedback and test improvements |
512 |
|
5398.8.4
by Stuart Bishop
Exception handling and many more tests |
513 |
if results is None or self._fetch_position < len(results): |
5398.8.2
by Stuart Bishop
Mock database work in progress |
514 |
return -1 |
5398.8.22
by Stuart Bishop
Review part 1 updates |
515 |
return self._script_entry.rowcount |
5398.8.2
by Stuart Bishop
Mock database work in progress |
516 |
|
517 |
_closed = False |
|
518 |
||
519 |
def close(self): |
|
5398.8.9
by Stuart Bishop
Tweaks from quick review and copyright updates |
520 |
"""As per DB-API."""
|
5398.8.2
by Stuart Bishop
Mock database work in progress |
521 |
self._checkClosed() |
522 |
self._closed = True |
|
5398.8.22
by Stuart Bishop
Review part 1 updates |
523 |
if self.real_cursor is not None: |
524 |
self.real_cursor.close() |
|
525 |
self.real_cursor = None |
|
5398.8.2
by Stuart Bishop
Mock database work in progress |
526 |
self.connection = None |
527 |
||
528 |
def _checkClosed(self): |
|
529 |
"""Raise an exception if the cursor or connection is closed."""
|
|
530 |
if self._closed is True: |
|
5821.2.32
by James Henstridge
* s/psycopg/psycopg2/ in a few more places. |
531 |
raise psycopg2.Error('Cursor closed.') |
5398.8.2
by Stuart Bishop
Mock database work in progress |
532 |
self.connection._checkClosed() |
533 |
||
534 |
# Index in our results that the next fetch will return. We don't consume
|
|
5398.8.24
by Stuart Bishop
Review feedback |
535 |
# the results list: if we are recording we need to keep the results
|
536 |
# so we can serialize at the end of the test.
|
|
5398.8.2
by Stuart Bishop
Mock database work in progress |
537 |
_fetch_position = 0 |
538 |
||
539 |
def execute(self, query, parameters=None): |
|
540 |
"""As per DB-API."""
|
|
541 |
self._checkClosed() |
|
5398.8.22
by Stuart Bishop
Review part 1 updates |
542 |
self._script_entry = self.connection.script.execute( |
5398.8.2
by Stuart Bishop
Mock database work in progress |
543 |
self, query, parameters |
544 |
)
|
|
545 |
self._fetch_position = 0 |
|
546 |
||
547 |
def executemany(self, query, seq_of_parameters=None): |
|
548 |
"""As per DB-API."""
|
|
549 |
self._checkClosed() |
|
550 |
raise NotImplementedError('executemany') |
|
551 |
||
552 |
def fetchone(self): |
|
553 |
"""As per DB-API."""
|
|
554 |
self._checkClosed() |
|
5398.8.22
by Stuart Bishop
Review part 1 updates |
555 |
if self._script_entry is None: |
5821.2.32
by James Henstridge
* s/psycopg/psycopg2/ in a few more places. |
556 |
raise psycopg2.Error("No query issued yet") |
5398.8.22
by Stuart Bishop
Review part 1 updates |
557 |
if self._script_entry.results is None: |
5821.2.32
by James Henstridge
* s/psycopg/psycopg2/ in a few more places. |
558 |
raise psycopg2.Error("Query returned no results") |
5398.8.2
by Stuart Bishop
Mock database work in progress |
559 |
try: |
5398.8.22
by Stuart Bishop
Review part 1 updates |
560 |
row = self._script_entry.results[self._fetch_position] |
5398.8.2
by Stuart Bishop
Mock database work in progress |
561 |
self._fetch_position += 1 |
562 |
return row |
|
563 |
except IndexError: |
|
564 |
return None |
|
565 |
||
566 |
def fetchmany(self, size=None): |
|
567 |
"""As per DB-API."""
|
|
568 |
self._checkClosed() |
|
569 |
raise NotImplementedError('fetchmany') |
|
570 |
||
571 |
def fetchall(self): |
|
572 |
"""As per DB-API."""
|
|
573 |
self._checkClosed() |
|
5398.8.22
by Stuart Bishop
Review part 1 updates |
574 |
if self._script_entry is None: |
5821.2.32
by James Henstridge
* s/psycopg/psycopg2/ in a few more places. |
575 |
raise psycopg2.Error('No query issued yet') |
5398.8.22
by Stuart Bishop
Review part 1 updates |
576 |
if self._script_entry.results is None: |
5821.2.32
by James Henstridge
* s/psycopg/psycopg2/ in a few more places. |
577 |
raise psycopg2.Error('Query returned no results') |
5398.8.22
by Stuart Bishop
Review part 1 updates |
578 |
results = self._script_entry.results[self._fetch_position:] |
5398.8.2
by Stuart Bishop
Mock database work in progress |
579 |
self._fetch_position = len(results) |
580 |
return results |
|
581 |
||
582 |
def nextset(self): |
|
583 |
"""As per DB-API."""
|
|
584 |
self._checkClosed() |
|
585 |
raise NotImplementedError('nextset') |
|
586 |
||
587 |
def setinputsizes(self, sizes): |
|
588 |
"""As per DB-API."""
|
|
589 |
self._checkClosed() |
|
5398.8.19
by Stuart Bishop
Review updates |
590 |
return # No-op. |
5398.8.2
by Stuart Bishop
Mock database work in progress |
591 |
|
592 |
def setoutputsize(self, size, column=None): |
|
593 |
"""As per DB-API."""
|
|
594 |
self._checkClosed() |
|
5398.8.19
by Stuart Bishop
Review updates |
595 |
return # No-op. |
5398.8.2
by Stuart Bishop
Mock database work in progress |
596 |
|
5398.8.4
by Stuart Bishop
Exception handling and many more tests |
597 |
## psycopg1 does not support this extension.
|
598 |
##
|
|
599 |
## def next(self):
|
|
600 |
## """As per iterator spec and DB-API optional extension."""
|
|
601 |
## row = self.fetchone()
|
|
602 |
## if row is None:
|
|
5398.8.22
by Stuart Bishop
Review part 1 updates |
603 |
## raise StopIteration
|
5398.8.4
by Stuart Bishop
Exception handling and many more tests |
604 |
## else:
|
605 |
## return row
|
|
606 |
||
607 |
## def __iter__(self):
|
|
608 |
## """As per iterator spec and DB-API optional extension."""
|
|
609 |
## return self
|
|
5398.8.2
by Stuart Bishop
Mock database work in progress |
610 |