~launchpad-pqm/launchpad/devel

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