• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

zopefoundation / ZODB / 11457046461

22 Oct 2024 09:13AM UTC coverage: 83.911% (+0.2%) from 83.745%
11457046461

Pull #403

github

Sebatyne
fixup! repozo: factorize code doing the actual recover (write), in preparation to the implementation of the incremental recover
Pull Request #403: Repozo incremental recover

2445 of 3554 branches covered (68.8%)

214 of 215 new or added lines in 2 files covered. (99.53%)

1 existing line in 1 file now uncovered.

13466 of 16048 relevant lines covered (83.91%)

0.84 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

99.22
/src/ZODB/tests/TransactionalUndoStorage.py
1
##############################################################################
2
#
3
# Copyright (c) 2001, 2002 Zope Foundation and Contributors.
4
# All Rights Reserved.
5
#
6
# This software is subject to the provisions of the Zope Public License,
7
# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
8
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
9
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
10
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
11
# FOR A PARTICULAR PURPOSE.
12
#
13
##############################################################################
14
"""Check undo().
15

16
Any storage that supports undo() must pass these tests.
17
"""
18
import time
1✔
19

20
import transaction
1✔
21
from persistent import Persistent
1✔
22

23
from ZODB import DB
1✔
24
from ZODB import POSException
1✔
25
from ZODB.Connection import TransactionMetaData
1✔
26
from ZODB.serialize import referencesf
1✔
27
from ZODB.tests.MinPO import MinPO
1✔
28
from ZODB.tests.StorageTestBase import ZERO
1✔
29
from ZODB.tests.StorageTestBase import zodb_pickle
1✔
30
from ZODB.tests.StorageTestBase import zodb_unpickle
1✔
31
from ZODB.utils import load_current
1✔
32
from ZODB.utils import p64
1✔
33

34

35
class C(Persistent):
1✔
36
    pass
1✔
37

38

39
def snooze():
1✔
40
    # In Windows, it's possible that two successive time.time() calls return
41
    # the same value.  Tim guarantees that time never runs backwards.  You
42
    # usually want to call this before you pack a storage, or must make other
43
    # guarantees about increasing timestamps.
44
    now = time.time()
1✔
45
    while now == time.time():
1!
UNCOV
46
        time.sleep(0.1)
×
47

48

49
def listeq(L1, L2):
1✔
50
    """Return True if L1.sort() == L2.sort()
51

52
    Also support iterators.
53
    """
54
    return sorted(L1) == sorted(L2)
1✔
55

56

57
class TransactionalUndoStorage:
1✔
58

59
    def _multi_obj_transaction(self, objs):
1✔
60
        t = TransactionMetaData()
1✔
61
        self._storage.tpc_begin(t)
1✔
62
        for oid, rev, data in objs:
1✔
63
            self._storage.store(oid, rev, data, '', t)
1✔
64
        self._storage.tpc_vote(t)
1✔
65
        return self._storage.tpc_finish(t)
1✔
66

67
    def _iterate(self):
1✔
68
        """Iterate over the storage in its final state."""
69
        # This is testing that the iterator() code works correctly.
70
        # The hasattr() guards against ZEO, which doesn't support iterator.
71
        if not hasattr(self._storage, "iterator"):
1!
72
            return
×
73
        iter = self._storage.iterator()
1✔
74
        for txn in iter:
1✔
75
            for rec in txn:
1✔
76
                pass
1✔
77

78
    def _begin_undos_vote(self, t, *tids):
1✔
79
        self._storage.tpc_begin(t)
1✔
80
        oids = set()
1✔
81
        for tid in tids:
1✔
82
            undo_result = self._storage.undo(tid, t)
1✔
83
            if undo_result:
1!
84
                oids.update(undo_result[1])
1✔
85
        oids.update(self._storage.tpc_vote(t) or ())
1✔
86
        return oids
1✔
87

88
    def undo(self, tid, note=None):
1✔
89
        t = TransactionMetaData()
1✔
90
        if note is not None:
1✔
91
            t.note(note)
1✔
92
        oids = self._begin_undos_vote(t, tid)
1✔
93
        self._storage.tpc_finish(t)
1✔
94
        return oids
1✔
95

96
    def testSimpleTransactionalUndo(self):
1✔
97
        eq = self.assertEqual
1✔
98
        oid = self._storage.new_oid()
1✔
99
        revid = self._dostore(oid, data=MinPO(23))
1✔
100
        revid = self._dostore(oid, revid=revid, data=MinPO(24))
1✔
101
        revid = self._dostore(oid, revid=revid, data=MinPO(25))
1✔
102

103
        info = self._storage.undoInfo()
1✔
104
        # Now start an undo transaction
105
        self._undo(info[0]["id"], [oid], note="undo1")
1✔
106
        data, revid = load_current(self._storage, oid)
1✔
107
        eq(zodb_unpickle(data), MinPO(24))
1✔
108

109
        # Do another one
110
        info = self._storage.undoInfo()
1✔
111
        self._undo(info[2]["id"], [oid], note="undo2")
1✔
112
        data, revid = load_current(self._storage, oid)
1✔
113
        eq(zodb_unpickle(data), MinPO(23))
1✔
114

115
        # Try to undo the first record
116
        info = self._storage.undoInfo()
1✔
117
        self._undo(info[4]["id"], [oid], note="undo3")
1✔
118
        # This should fail since we've undone the object's creation
119
        self.assertRaises(KeyError, load_current, self._storage, oid)
1✔
120

121
        # And now let's try to redo the object's creation
122
        info = self._storage.undoInfo()
1✔
123
        self._undo(info[0]["id"], [oid])
1✔
124
        data, revid = load_current(self._storage, oid)
1✔
125
        eq(zodb_unpickle(data), MinPO(23))
1✔
126
        self._iterate()
1✔
127

128
    def testCreationUndoneGetTid(self):
1✔
129
        # create an object
130
        oid = self._storage.new_oid()
1✔
131
        self._dostore(oid, data=MinPO(23))
1✔
132
        # undo its creation
133
        info = self._storage.undoInfo()
1✔
134
        tid = info[0]['id']
1✔
135
        self.undo(tid, 'undo1')
1✔
136
        # Check that calling getTid on an uncreated object raises a KeyError
137
        # The current version of FileStorage fails this test
138
        self.assertRaises(KeyError, self._storage.getTid, oid)
1✔
139

140
    def testUndoCreationBranch1(self):
1✔
141
        eq = self.assertEqual
1✔
142
        oid = self._storage.new_oid()
1✔
143
        revid = self._dostore(oid, data=MinPO(11))
1✔
144
        revid = self._dostore(oid, revid=revid, data=MinPO(12))
1✔
145
        # Undo the last transaction
146
        info = self._storage.undoInfo()
1✔
147
        self._undo(info[0]['id'], [oid])
1✔
148
        data, revid = load_current(self._storage, oid)
1✔
149
        eq(zodb_unpickle(data), MinPO(11))
1✔
150

151
        # Now from here, we can either redo the last undo, or undo the object
152
        # creation.  Let's undo the object creation.
153
        info = self._storage.undoInfo()
1✔
154
        self._undo(info[2]['id'], [oid])
1✔
155
        self.assertRaises(KeyError, load_current, self._storage, oid)
1✔
156

157
        # Loading current data via loadBefore should raise a POSKeyError too:
158
        self.assertRaises(KeyError, self._storage.loadBefore, oid,
1✔
159
                          b'\x7f\xff\xff\xff\xff\xff\xff\xff')
160
        self._iterate()
1✔
161

162
    def testUndoCreationBranch2(self):
1✔
163
        eq = self.assertEqual
1✔
164
        oid = self._storage.new_oid()
1✔
165
        revid = self._dostore(oid, data=MinPO(11))
1✔
166
        revid = self._dostore(oid, revid=revid, data=MinPO(12))
1✔
167
        # Undo the last transaction
168
        info = self._storage.undoInfo()
1✔
169
        self._undo(info[0]['id'], [oid])
1✔
170
        data, revid = load_current(self._storage, oid)
1✔
171
        eq(zodb_unpickle(data), MinPO(11))
1✔
172
        # Now from here, we can either redo the last undo, or undo the object
173
        # creation.  Let's redo the last undo
174
        info = self._storage.undoInfo()
1✔
175
        self._undo(info[0]['id'], [oid])
1✔
176
        data, revid = load_current(self._storage, oid)
1✔
177
        eq(zodb_unpickle(data), MinPO(12))
1✔
178
        self._iterate()
1✔
179

180
    def testTwoObjectUndo(self):
1✔
181
        eq = self.assertEqual
1✔
182
        # Convenience
183
        p31, p32, p51, p52 = map(zodb_pickle,
1✔
184
                                 map(MinPO, (31, 32, 51, 52)))
185
        oid1 = self._storage.new_oid()
1✔
186
        oid2 = self._storage.new_oid()
1✔
187
        revid1 = revid2 = ZERO
1✔
188
        # Store two objects in the same transaction
189
        t = TransactionMetaData()
1✔
190
        self._storage.tpc_begin(t)
1✔
191
        self._storage.store(oid1, revid1, p31, '', t)
1✔
192
        self._storage.store(oid2, revid2, p51, '', t)
1✔
193
        # Finish the transaction
194
        self._storage.tpc_vote(t)
1✔
195
        tid = self._storage.tpc_finish(t)
1✔
196
        # Update those same two objects
197
        t = TransactionMetaData()
1✔
198
        self._storage.tpc_begin(t)
1✔
199
        self._storage.store(oid1, tid, p32, '', t)
1✔
200
        self._storage.store(oid2, tid, p52, '', t)
1✔
201
        # Finish the transaction
202
        self._storage.tpc_vote(t)
1✔
203
        self._storage.tpc_finish(t)
1✔
204
        # Make sure the objects have the current value
205
        data, revid1 = load_current(self._storage, oid1)
1✔
206
        eq(zodb_unpickle(data), MinPO(32))
1✔
207
        data, revid2 = load_current(self._storage, oid2)
1✔
208
        eq(zodb_unpickle(data), MinPO(52))
1✔
209

210
        # Now attempt to undo the transaction containing two objects
211
        info = self._storage.undoInfo()
1✔
212
        self._undo(info[0]['id'], [oid1, oid2])
1✔
213
        data, revid1 = load_current(self._storage, oid1)
1✔
214
        eq(zodb_unpickle(data), MinPO(31))
1✔
215
        data, revid2 = load_current(self._storage, oid2)
1✔
216
        eq(zodb_unpickle(data), MinPO(51))
1✔
217
        self._iterate()
1✔
218

219
    def testTwoObjectUndoAtOnce(self):
1✔
220
        # Convenience
221
        eq = self.assertEqual
1✔
222
        p30, p31, p32, p50, p51, p52 = map(zodb_pickle,
1✔
223
                                           map(MinPO,
224
                                               (30, 31, 32, 50, 51, 52)))
225
        oid1 = self._storage.new_oid()
1✔
226
        oid2 = self._storage.new_oid()
1✔
227
        # Store two objects in the same transaction
228
        tid = self._multi_obj_transaction([(oid1, ZERO, p30),
1✔
229
                                           (oid2, ZERO, p50),
230
                                           ])
231
        # Update those same two objects
232
        tid = self._multi_obj_transaction([(oid1, tid, p31),
1✔
233
                                           (oid2, tid, p51),
234
                                           ])
235
        # Update those same two objects
236
        tid = self._multi_obj_transaction([(oid1, tid, p32),
1✔
237
                                           (oid2, tid, p52),
238
                                           ])
239
        # Make sure the objects have the current value
240
        data, revid1 = load_current(self._storage, oid1)
1✔
241
        eq(zodb_unpickle(data), MinPO(32))
1✔
242
        data, revid2 = load_current(self._storage, oid2)
1✔
243
        eq(zodb_unpickle(data), MinPO(52))
1✔
244
        # Now attempt to undo the transaction containing two objects
245
        info = self._storage.undoInfo()
1✔
246
        tid = info[0]['id']
1✔
247
        tid1 = info[1]['id']
1✔
248
        t = TransactionMetaData()
1✔
249
        oids = self._begin_undos_vote(t, tid, tid1)
1✔
250
        serial = self._storage.tpc_finish(t)
1✔
251
        # We may get the finalization stuff called an extra time,
252
        # depending on the implementation.
253
        if serial is None:
1!
254
            self.assertEqual(oids, {oid1, oid2})
×
255
        data, revid1 = load_current(self._storage, oid1)
1✔
256
        eq(zodb_unpickle(data), MinPO(30))
1✔
257
        data, revid2 = load_current(self._storage, oid2)
1✔
258
        eq(zodb_unpickle(data), MinPO(50))
1✔
259

260
        # Now try to undo the one we just did to undo, whew
261
        info = self._storage.undoInfo()
1✔
262
        self._undo(info[0]['id'], [oid1, oid2])
1✔
263
        data, revid1 = load_current(self._storage, oid1)
1✔
264
        eq(zodb_unpickle(data), MinPO(32))
1✔
265
        data, revid2 = load_current(self._storage, oid2)
1✔
266
        eq(zodb_unpickle(data), MinPO(52))
1✔
267
        self._iterate()
1✔
268

269
    def testTwoObjectUndoAgain(self):
1✔
270
        eq = self.assertEqual
1✔
271
        p31, p32, p33, p51, p52, p53 = map(
1✔
272
            zodb_pickle,
273
            map(MinPO, (31, 32, 33, 51, 52, 53)))
274
        # Like the above, but the first revision of the objects are stored in
275
        # different transactions.
276
        oid1 = self._storage.new_oid()
1✔
277
        oid2 = self._storage.new_oid()
1✔
278
        revid1 = self._dostore(oid1, data=p31, already_pickled=1)
1✔
279
        revid2 = self._dostore(oid2, data=p51, already_pickled=1)
1✔
280
        # Update those same two objects
281
        t = TransactionMetaData()
1✔
282
        self._storage.tpc_begin(t)
1✔
283
        self._storage.store(oid1, revid1, p32, '', t)
1✔
284
        self._storage.store(oid2, revid2, p52, '', t)
1✔
285
        # Finish the transaction
286
        self._storage.tpc_vote(t)
1✔
287
        self._storage.tpc_finish(t)
1✔
288
        # Now attempt to undo the transaction containing two objects
289
        info = self._storage.undoInfo()
1✔
290
        self._undo(info[0]["id"], [oid1, oid2])
1✔
291
        data, revid1 = load_current(self._storage, oid1)
1✔
292
        eq(zodb_unpickle(data), MinPO(31))
1✔
293
        data, revid2 = load_current(self._storage, oid2)
1✔
294
        eq(zodb_unpickle(data), MinPO(51))
1✔
295
        # Like the above, but this time, the second transaction contains only
296
        # one object.
297
        t = TransactionMetaData()
1✔
298
        self._storage.tpc_begin(t)
1✔
299
        self._storage.store(oid1, revid1, p33, '', t)
1✔
300
        self._storage.store(oid2, revid2, p53, '', t)
1✔
301
        # Finish the transaction
302
        self._storage.tpc_vote(t)
1✔
303
        tid = self._storage.tpc_finish(t)
1✔
304
        # Update in different transactions
305
        revid1 = self._dostore(oid1, revid=tid, data=MinPO(34))
1✔
306
        revid2 = self._dostore(oid2, revid=tid, data=MinPO(54))
1✔
307
        # Now attempt to undo the transaction containing two objects
308
        info = self._storage.undoInfo()
1✔
309
        self.undo(info[1]['id'])
1✔
310
        data, revid1 = load_current(self._storage, oid1)
1✔
311
        eq(zodb_unpickle(data), MinPO(33))
1✔
312
        data, revid2 = load_current(self._storage, oid2)
1✔
313
        eq(zodb_unpickle(data), MinPO(54))
1✔
314
        self._iterate()
1✔
315

316
    def testNotUndoable(self):
1✔
317
        eq = self.assertEqual
1✔
318
        # Set things up so we've got a transaction that can't be undone
319
        oid = self._storage.new_oid()
1✔
320
        revid_a = self._dostore(oid, data=MinPO(51))
1✔
321
        revid_b = self._dostore(oid, revid=revid_a, data=MinPO(52))
1✔
322
        revid_c = self._dostore(oid, revid=revid_b, data=MinPO(53))
1✔
323
        # Start the undo
324
        info = self._storage.undoInfo()
1✔
325
        tid = info[1]['id']
1✔
326
        t = TransactionMetaData()
1✔
327
        self.assertRaises(POSException.UndoError,
1✔
328
                          self._begin_undos_vote, t, tid)
329
        self._storage.tpc_abort(t)
1✔
330
        # Now have more fun: object1 and object2 are in the same transaction,
331
        # which we'll try to undo to, but one of them has since modified in
332
        # different transaction, so the undo should fail.
333
        oid1 = oid
1✔
334
        revid1 = revid_c
1✔
335
        oid2 = self._storage.new_oid()
1✔
336
        revid2 = ZERO
1✔
337
        p81, p82, p91, p92 = map(zodb_pickle,
1✔
338
                                 map(MinPO, (81, 82, 91, 92)))
339

340
        t = TransactionMetaData()
1✔
341
        self._storage.tpc_begin(t)
1✔
342
        self._storage.store(oid1, revid1, p81, '', t)
1✔
343
        self._storage.store(oid2, revid2, p91, '', t)
1✔
344
        self._storage.tpc_vote(t)
1✔
345
        tid = self._storage.tpc_finish(t)
1✔
346
        # Make sure the objects have the expected values
347
        data, revid_11 = load_current(self._storage, oid1)
1✔
348
        eq(zodb_unpickle(data), MinPO(81))
1✔
349
        data, revid_22 = load_current(self._storage, oid2)
1✔
350
        eq(zodb_unpickle(data), MinPO(91))
1✔
351
        eq(revid_11, tid)
1✔
352
        eq(revid_22, tid)
1✔
353
        # Now modify oid2
354
        revid2 = self._dostore(oid2, tid, MinPO(92))
1✔
355
        self.assertNotEqual(tid, revid2)
1✔
356
        info = self._storage.undoInfo()
1✔
357
        tid = info[1]['id']
1✔
358
        t = TransactionMetaData()
1✔
359
        self.assertRaises(POSException.UndoError,
1✔
360
                          self._begin_undos_vote, t, tid)
361
        self._storage.tpc_abort(t)
1✔
362
        self._iterate()
1✔
363

364
    def testTransactionalUndoAfterPack(self):
1✔
365
        # bwarsaw Date: Thu Mar 28 21:04:43 2002 UTC
366
        # This is a test which should provoke the underlying bug in
367
        # transactionalUndo() on a standby storage.  If our hypothesis
368
        # is correct, the bug is in FileStorage, and is caused by
369
        # encoding the file position in the `id' field of the undoLog
370
        # information.  Note that Full just encodes the tid, but this
371
        # is a problem for FileStorage (we have a strategy for fixing
372
        # this).
373

374
        # So, basically, this makes sure that undo info doesn't depend
375
        # on file positions.  We change the file positions in an undo
376
        # record by packing.
377

378
        # Add a few object revisions
379
        oid = b'\0'*8
1✔
380
        revid0 = self._dostore(oid, data=MinPO(50))
1✔
381
        revid1 = self._dostore(oid, revid=revid0, data=MinPO(51))
1✔
382
        snooze()
1✔
383
        packtime = time.time()
1✔
384
        snooze()                # time.time() now distinct from packtime
1✔
385
        revid2 = self._dostore(oid, revid=revid1, data=MinPO(52))
1✔
386
        self._dostore(oid, revid=revid2, data=MinPO(53))
1✔
387
        # Now get the undo log
388
        info = self._storage.undoInfo()
1✔
389
        self.assertEqual(len(info), 4)
1✔
390
        tid = info[0]['id']
1✔
391
        # Now pack just the initial revision of the object.  We need the
392
        # second revision otherwise we won't be able to undo the third
393
        # revision!
394
        self._storage.pack(packtime, referencesf)
1✔
395
        # Make some basic assertions about the undo information now
396
        info2 = self._storage.undoInfo()
1✔
397
        self.assertEqual(len(info2), 2)
1✔
398
        # And now attempt to undo the last transaction
399
        undone, = self.undo(tid)
1✔
400
        self.assertEqual(undone, oid)
1✔
401
        data, revid = load_current(self._storage, oid)
1✔
402
        # The object must now be at the second state
403
        self.assertEqual(zodb_unpickle(data), MinPO(52))
1✔
404
        self._iterate()
1✔
405

406
    def testTransactionalUndoAfterPackWithObjectUnlinkFromRoot(self):
1✔
407
        eq = self.assertEqual
1✔
408
        db = DB(self._storage)
1✔
409
        conn = db.open()
1✔
410
        try:
1✔
411
            root = conn.root()
1✔
412

413
            o1 = C()
1✔
414
            o2 = C()
1✔
415
            root['obj'] = o1
1✔
416
            o1.obj = o2
1✔
417
            txn = transaction.get()
1✔
418
            txn.note('o1 -> o2')
1✔
419
            txn.commit()
1✔
420
            now = packtime = time.time()
1✔
421
            while packtime <= now:
1✔
422
                packtime = time.time()
1✔
423

424
            o3 = C()
1✔
425
            o2.obj = o3
1✔
426
            txn = transaction.get()
1✔
427
            txn.note('o1 -> o2 -> o3')
1✔
428
            txn.commit()
1✔
429

430
            o1.obj = o3
1✔
431
            txn = transaction.get()
1✔
432
            txn.note('o1 -> o3')
1✔
433
            txn.commit()
1✔
434

435
            log = self._storage.undoLog()
1✔
436
            eq(len(log), 4)
1✔
437
            for entry in zip(log, (b'o1 -> o3', b'o1 -> o2 -> o3',
1✔
438
                                   b'o1 -> o2', b'initial database creation')):
439
                eq(entry[0]['description'], entry[1])
1✔
440

441
            self._storage.pack(packtime, referencesf)
1✔
442

443
            log = self._storage.undoLog()
1✔
444
            for entry in zip(log, (b'o1 -> o3', b'o1 -> o2 -> o3')):
1✔
445
                eq(entry[0]['description'], entry[1])
1✔
446

447
            tid = log[0]['id']
1✔
448
            db.undo(tid)
1✔
449
            txn = transaction.get()
1✔
450
            txn.note('undo')
1✔
451
            txn.commit()
1✔
452
            # undo does a txn-undo, but doesn't invalidate
453
            conn.sync()
1✔
454

455
            log = self._storage.undoLog()
1✔
456
            for entry in zip(log, (b'undo', b'o1 -> o3', b'o1 -> o2 -> o3')):
1✔
457
                eq(entry[0]['description'], entry[1])
1✔
458

459
            eq(o1.obj, o2)
1✔
460
            eq(o1.obj.obj, o3)
1✔
461
            self._iterate()
1✔
462
        finally:
463
            conn.close()
1✔
464
            db.close()
1✔
465

466
    def testPackAfterUndoDeletion(self):
1✔
467
        db = DB(self._storage)
1✔
468
        cn = db.open()
1✔
469
        try:
1✔
470
            root = cn.root()
1✔
471

472
            pack_times = []
1✔
473

474
            def set_pack_time():
1✔
475
                pack_times.append(time.time())
1✔
476
                snooze()
1✔
477

478
            root["key0"] = MinPO(0)
1✔
479
            root["key1"] = MinPO(1)
1✔
480
            root["key2"] = MinPO(2)
1✔
481
            txn = transaction.get()
1✔
482
            txn.note("create 3 keys")
1✔
483
            txn.commit()
1✔
484

485
            set_pack_time()
1✔
486

487
            del root["key1"]
1✔
488
            txn = transaction.get()
1✔
489
            txn.note("delete 1 key")
1✔
490
            txn.commit()
1✔
491

492
            set_pack_time()
1✔
493

494
            root._p_deactivate()
1✔
495
            cn.sync()
1✔
496
            self.assertTrue(listeq(root.keys(), ["key0", "key2"]))
1✔
497

498
            L = db.undoInfo()
1✔
499
            db.undo(L[0]["id"])
1✔
500
            txn = transaction.get()
1✔
501
            txn.note("undo deletion")
1✔
502
            txn.commit()
1✔
503

504
            set_pack_time()
1✔
505

506
            root._p_deactivate()
1✔
507
            cn.sync()
1✔
508
            self.assertTrue(listeq(root.keys(), ["key0", "key1", "key2"]))
1✔
509

510
            for t in pack_times:
1✔
511
                self._storage.pack(t, referencesf)
1✔
512

513
                root._p_deactivate()
1✔
514
                cn.sync()
1✔
515
                self.assertTrue(listeq(root.keys(), ["key0", "key1", "key2"]))
1✔
516
                for i in range(3):
1✔
517
                    obj = root["key%d" % i]
1✔
518
                    self.assertEqual(obj.value, i)
1✔
519
                root.items()
1✔
520
                self._inter_pack_pause()
1✔
521
        finally:
522
            cn.close()
1✔
523
            db.close()
1✔
524

525
    def testPackAfterUndoManyTimes(self):
1✔
526
        db = DB(self._storage)
1✔
527
        cn = db.open()
1✔
528
        try:
1✔
529
            rt = cn.root()
1✔
530

531
            rt["test"] = MinPO(1)
1✔
532
            transaction.commit()
1✔
533
            rt["test2"] = MinPO(2)
1✔
534
            transaction.commit()
1✔
535
            rt["test"] = MinPO(3)
1✔
536
            txn = transaction.get()
1✔
537
            txn.note("root of undo")
1✔
538
            txn.commit()
1✔
539

540
            packtimes = []
1✔
541
            for i in range(10):
1✔
542
                L = db.undoInfo()
1✔
543
                db.undo(L[0]["id"])
1✔
544
                txn = transaction.get()
1✔
545
                txn.note("undo %d" % i)
1✔
546
                txn.commit()
1✔
547
                rt._p_deactivate()
1✔
548
                cn.sync()
1✔
549

550
                self.assertEqual(rt["test"].value, i % 2 and 3 or 1)
1✔
551
                self.assertEqual(rt["test2"].value, 2)
1✔
552

553
                packtimes.append(time.time())
1✔
554
                snooze()
1✔
555

556
            for t in packtimes:
1✔
557
                self._storage.pack(t, referencesf)
1✔
558
                cn.sync()
1✔
559

560
                # TODO:  Is _cache supposed to have a clear() method, or not?
561
                # cn._cache.clear()
562

563
                # The last undo set the value to 3 and pack should
564
                # never change that.
565
                self.assertEqual(rt["test"].value, 3)
1✔
566
                self.assertEqual(rt["test2"].value, 2)
1✔
567
                self._inter_pack_pause()
1✔
568
        finally:
569
            cn.close()
1✔
570
            db.close()
1✔
571

572
    def _inter_pack_pause(self):
1✔
573
        # DirectoryStorage needs a pause between packs,
574
        # most other storages dont.
575
        pass
1✔
576

577
    def testTransactionalUndoIterator(self):
1✔
578
        # check that data_txn set in iterator makes sense
579
        if not hasattr(self._storage, "iterator"):
1!
580
            return
×
581

582
        s = self._storage
1✔
583

584
        BATCHES = 4
1✔
585
        OBJECTS = 4
1✔
586

587
        orig = []
1✔
588
        for i in range(BATCHES):
1✔
589
            t = TransactionMetaData()
1✔
590
            tid = p64(i + 1)
1✔
591
            s.tpc_begin(t, tid)
1✔
592
            for j in range(OBJECTS):
1✔
593
                oid = s.new_oid()
1✔
594
                obj = MinPO(i * OBJECTS + j)
1✔
595
                s.store(oid, None, zodb_pickle(obj), '', t)
1✔
596
                orig.append((tid, oid))
1✔
597
            s.tpc_vote(t)
1✔
598
            s.tpc_finish(t)
1✔
599

600
        orig = [(tid, oid, s.getTid(oid)) for tid, oid in orig]
1✔
601

602
        i = 0
1✔
603
        for tid, oid, revid in orig:
1✔
604
            self._dostore(oid, revid=revid, data=MinPO(revid),
1✔
605
                          description="update %s" % i)
606

607
        # Undo the OBJECTS transactions that modified objects created
608
        # in the ith original transaction.
609

610
        def undo(i):
1✔
611
            info = s.undoInfo()
1✔
612
            t = TransactionMetaData()
1✔
613
            s.tpc_begin(t)
1✔
614
            base = i * OBJECTS + i
1✔
615
            for j in range(OBJECTS):
1✔
616
                tid = info[base + j]['id']
1✔
617
                s.undo(tid, t)
1✔
618
            s.tpc_vote(t)
1✔
619
            s.tpc_finish(t)
1✔
620

621
        for i in range(BATCHES):
1✔
622
            undo(i)
1✔
623

624
        # There are now (2 + OBJECTS) * BATCHES transactions:
625
        #     BATCHES original transactions, followed by
626
        #     OBJECTS * BATCHES modifications, followed by
627
        #     BATCHES undos
628

629
        transactions = s.iterator()
1✔
630
        eq = self.assertEqual
1✔
631

632
        for i in range(BATCHES):
1✔
633
            txn = next(transactions)
1✔
634

635
            tid = p64(i + 1)
1✔
636
            eq(txn.tid, tid)
1✔
637

638
            L1 = {(rec.oid, rec.tid, rec.data_txn) for rec in txn}
1✔
639
            L2 = {(oid, revid, None) for _tid, oid, revid in orig
1✔
640
                  if _tid == tid}
641

642
            eq(L1, L2)
1✔
643

644
        for i in range(BATCHES * OBJECTS):
1✔
645
            txn = next(transactions)
1✔
646
            eq(len([rec for rec in txn if rec.data_txn is None]), 1)
1✔
647

648
        for i in range(BATCHES):
1✔
649
            txn = next(transactions)
1✔
650

651
            # The undos are performed in reverse order.
652
            otid = p64(BATCHES - i)
1✔
653
            L1 = [(rec.oid, rec.data_txn) for rec in txn]
1✔
654
            L2 = [(oid, otid) for _tid, oid, revid in orig
1✔
655
                  if _tid == otid]
656
            L1.sort()
1✔
657
            L2.sort()
1✔
658
            eq(L1, L2)
1✔
659

660
        self.assertRaises(StopIteration, next, transactions)
1✔
661

662
    def testUndoLogMetadata(self):
1✔
663
        # test that the metadata is correct in the undo log
664
        t = transaction.get()
1✔
665
        t.note('t1')
1✔
666
        t.setExtendedInfo('k2', 'this is transaction metadata')
1✔
667
        t.setUser('u3', path='p3')
1✔
668
        db = DB(self._storage)
1✔
669
        conn = db.open()
1✔
670
        try:
1✔
671
            root = conn.root()
1✔
672
            o1 = C()
1✔
673
            root['obj'] = o1
1✔
674
            txn = transaction.get()
1✔
675
            txn.commit()
1✔
676
            log = self._storage.undoLog()
1✔
677
            self.assertEqual(len(log), 2)
1✔
678
            d = log[0]
1✔
679
            self.assertEqual(d['description'], b't1')
1✔
680
            self.assertEqual(d['k2'], 'this is transaction metadata')
1✔
681
            self.assertEqual(d['user_name'], b'p3 u3')
1✔
682
        finally:
683
            conn.close()
1✔
684
            db.close()
1✔
685

686
    # A common test body for index tests on undoInfo and undoLog.  Before
687
    # ZODB 3.4, they always returned a wrong number of results (one too
688
    # few _or_ too many, depending on how they were called).
689
    def _exercise_info_indices(self, method_name):
1✔
690
        db = DB(self._storage)
1✔
691
        info_func = getattr(db, method_name)
1✔
692
        cn = db.open()
1✔
693
        rt = cn.root()
1✔
694

695
        # Do some transactions.
696
        for key in "abcdefghijklmnopqrstuvwxyz":
1✔
697
            rt[key] = ord(key)
1✔
698
            transaction.commit()
1✔
699

700
        # 26 letters = 26 transactions, + the hidden transaction to make
701
        # the root object, == 27 expected.
702
        allofem = info_func(0, 100000)
1✔
703
        self.assertEqual(len(allofem), 27)
1✔
704

705
        # Asking for no more than 100000 should do the same.
706
        redundant = info_func(last=-1000000)
1✔
707
        self.assertEqual(allofem, redundant)
1✔
708

709
        # By default, we should get only 20 back.
710
        default = info_func()
1✔
711
        self.assertEqual(len(default), 20)
1✔
712
        # And they should be the most recent 20.
713
        self.assertEqual(default, allofem[:20])
1✔
714

715
        # If we ask for only one, we should get only the most recent.
716
        fresh = info_func(last=1)
1✔
717
        self.assertEqual(len(fresh), 1)
1✔
718
        self.assertEqual(fresh[0], allofem[0])
1✔
719

720
        # Another way of asking for only the most recent.
721
        redundant = info_func(last=-1)
1✔
722
        self.assertEqual(fresh, redundant)
1✔
723

724
        # Try a slice that doesn't start at 0.
725
        oddball = info_func(first=11, last=17)
1✔
726
        self.assertEqual(len(oddball), 17-11)
1✔
727
        self.assertEqual(oddball, allofem[11: 11+len(oddball)])
1✔
728

729
        # And another way to spell the same thing.
730
        redundant = info_func(first=11, last=-6)
1✔
731
        self.assertEqual(oddball, redundant)
1✔
732

733
        cn.close()
1✔
734
        # Caution:  don't close db; the framework does that.  If you close
735
        # it here, the ZODB tests still work, but the ZRS RecoveryStorageTests
736
        # fail (closing the DB here in those tests closes the ZRS primary
737
        # before a ZRS secondary even starts, and then the latter can't
738
        # find a server to recover from).
739

740
    def testIndicesInUndoInfo(self):
1✔
741
        self._exercise_info_indices("undoInfo")
1✔
742

743
    def testIndicesInUndoLog(self):
1✔
744
        self._exercise_info_indices("undoLog")
1✔
745

746
    def testUndoMultipleConflictResolution(self, reverse=False):
1✔
747
        from .ConflictResolution import PCounter
1✔
748
        db = DB(self._storage)
1✔
749
        cn = db.open()
1✔
750
        try:
1✔
751
            cn.root.x = PCounter()
1✔
752
            transaction.commit()
1✔
753

754
            for i in range(4):
1✔
755
                with db.transaction() as conn:
1✔
756
                    conn.transaction_manager.get().note(str(i))
1✔
757
                    conn.root.x.inc()
1✔
758

759
            ids = [log['id'] for log in db.undoLog(1, 3)]
1✔
760
            if reverse:
1✔
761
                ids.reverse()
1✔
762

763
            db.undoMultiple(ids)
1✔
764
            transaction.commit()
1✔
765

766
            self.assertEqual(cn.root.x._value, 2)
1✔
767
        finally:
768
            cn.close()
1✔
769
            db.close()
1✔
770

771
    def testUndoMultipleConflictResolutionReversed(self):
1✔
772
        self.testUndoMultipleConflictResolution(True)
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc