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

zopefoundation / transaction / 16399678488

18 Sep 2024 07:25AM UTC coverage: 99.793% (+0.1%) from 99.696%
16399678488

push

github

dataflake
- vb [ci skip]

299 of 306 branches covered (97.71%)

Branch coverage included in aggregate %.

3083 of 3083 relevant lines covered (100.0%)

1.0 hits per line

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

99.49
/src/transaction/tests/test__manager.py
1
##############################################################################
2
#
3
# Copyright (c) 2012 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
import unittest
1✔
15
from unittest import mock
1✔
16

17
import zope.interface.verify
1✔
18

19
from .. import interfaces
1✔
20

21

22
class TransactionManagerTests(unittest.TestCase):
1✔
23

24
    def _getTargetClass(self):
1✔
25
        from transaction import TransactionManager
1✔
26
        return TransactionManager
1✔
27

28
    def _makeOne(self):
1✔
29
        return self._getTargetClass()()
1✔
30

31
    def _makePopulated(self):
1✔
32
        mgr = self._makeOne()
1✔
33
        sub1 = DataObject(mgr)
1✔
34
        sub2 = DataObject(mgr)
1✔
35
        sub3 = DataObject(mgr)
1✔
36
        nosub1 = DataObject(mgr, nost=1)
1✔
37
        return mgr, sub1, sub2, sub3, nosub1
1✔
38

39
    def test_interface(self):
1✔
40
        zope.interface.verify.verifyObject(interfaces.ITransactionManager,
1✔
41
                                           self._makeOne())
42

43
    def test_ctor(self):
1✔
44
        tm = self._makeOne()
1✔
45
        self.assertIsNone(tm._txn)
1✔
46
        self.assertEqual(len(tm._synchs), 0)
1✔
47

48
    def test_begin_wo_existing_txn_wo_synchs(self):
1✔
49
        from transaction._transaction import Transaction
1✔
50
        tm = self._makeOne()
1✔
51
        tm.begin()
1✔
52
        self.assertIsInstance(tm._txn, Transaction)
1✔
53

54
    def test_begin_wo_existing_txn_w_synchs(self):
1✔
55
        from transaction._transaction import Transaction
1✔
56
        tm = self._makeOne()
1✔
57
        synch = DummySynch()
1✔
58
        tm.registerSynch(synch)
1✔
59
        tm.begin()
1✔
60
        self.assertIsInstance(tm._txn, Transaction)
1✔
61
        self.assertIn(tm._txn, synch._txns)
1✔
62

63
    def test_begin_w_existing_txn(self):
1✔
64
        class Existing:
1✔
65
            _aborted = False
1✔
66

67
            def abort(self):
1✔
68
                self._aborted = True
1✔
69
        tm = self._makeOne()
1✔
70
        tm._txn = txn = Existing()
1✔
71
        tm.begin()
1✔
72
        self.assertIsNot(tm._txn, txn)
1✔
73
        self.assertTrue(txn._aborted)
1✔
74

75
    def test_get_wo_existing_txn(self):
1✔
76
        from transaction._transaction import Transaction
1✔
77
        tm = self._makeOne()
1✔
78
        txn = tm.get()
1✔
79
        self.assertIsInstance(txn, Transaction)
1✔
80

81
    def test_get_w_existing_txn(self):
1✔
82
        class Existing:
1✔
83
            _aborted = False
1✔
84

85
            def abort(self):
1✔
86
                raise AssertionError("This is not actually called")
87
        tm = self._makeOne()
1✔
88
        tm._txn = txn = Existing()
1✔
89
        self.assertIs(tm.get(), txn)
1✔
90

91
    def test_free_w_other_txn(self):
1✔
92
        from transaction._transaction import Transaction
1✔
93
        tm = self._makeOne()
1✔
94
        txn = Transaction()
1✔
95
        tm.begin()
1✔
96
        self.assertRaises(ValueError, tm.free, txn)
1✔
97

98
    def test_free_w_existing_txn(self):
1✔
99
        class Existing:
1✔
100
            _aborted = False
1✔
101

102
            def abort(self):
1✔
103
                raise AssertionError("This is not actually called")
104
        tm = self._makeOne()
1✔
105
        tm._txn = txn = Existing()
1✔
106
        tm.free(txn)
1✔
107
        self.assertIsNone(tm._txn)
1✔
108

109
    def test_registerSynch(self):
1✔
110
        tm = self._makeOne()
1✔
111
        synch = DummySynch()
1✔
112
        tm.registerSynch(synch)
1✔
113
        self.assertEqual(len(tm._synchs), 1)
1✔
114
        self.assertIn(synch, tm._synchs)
1✔
115

116
    def test_unregisterSynch(self):
1✔
117
        tm = self._makeOne()
1✔
118
        synch1 = DummySynch()
1✔
119
        synch2 = DummySynch()
1✔
120
        self.assertFalse(tm.registeredSynchs())
1✔
121
        tm.registerSynch(synch1)
1✔
122
        self.assertTrue(tm.registeredSynchs())
1✔
123
        tm.registerSynch(synch2)
1✔
124
        self.assertTrue(tm.registeredSynchs())
1✔
125
        tm.unregisterSynch(synch1)
1✔
126
        self.assertTrue(tm.registeredSynchs())
1✔
127
        self.assertEqual(len(tm._synchs), 1)
1✔
128
        self.assertNotIn(synch1, tm._synchs)
1✔
129
        self.assertIn(synch2, tm._synchs)
1✔
130
        tm.unregisterSynch(synch2)
1✔
131
        self.assertFalse(tm.registeredSynchs())
1✔
132

133
    def test_clearSynchs(self):
1✔
134
        tm = self._makeOne()
1✔
135
        synch1 = DummySynch()
1✔
136
        synch2 = DummySynch()
1✔
137
        tm.registerSynch(synch1)
1✔
138
        tm.registerSynch(synch2)
1✔
139
        tm.clearSynchs()
1✔
140
        self.assertEqual(len(tm._synchs), 0)
1✔
141

142
    def test_isDoomed_wo_existing_txn(self):
1✔
143
        tm = self._makeOne()
1✔
144
        self.assertFalse(tm.isDoomed())
1✔
145
        tm._txn.doom()
1✔
146
        self.assertTrue(tm.isDoomed())
1✔
147

148
    def test_isDoomed_w_existing_txn(self):
1✔
149
        class Existing:
1✔
150
            _doomed = False
1✔
151

152
            def isDoomed(self):
1✔
153
                return self._doomed
1✔
154
        tm = self._makeOne()
1✔
155
        tm._txn = txn = Existing()
1✔
156
        self.assertFalse(tm.isDoomed())
1✔
157
        txn._doomed = True
1✔
158
        self.assertTrue(tm.isDoomed())
1✔
159

160
    def test_doom(self):
1✔
161
        tm = self._makeOne()
1✔
162
        txn = tm.get()
1✔
163
        self.assertFalse(txn.isDoomed())
1✔
164
        tm.doom()
1✔
165
        self.assertTrue(txn.isDoomed())
1✔
166
        self.assertTrue(tm.isDoomed())
1✔
167

168
    def test_commit_w_existing_txn(self):
1✔
169
        class Existing:
1✔
170
            _committed = False
1✔
171

172
            def commit(self):
1✔
173
                self._committed = True
1✔
174
        tm = self._makeOne()
1✔
175
        tm._txn = txn = Existing()
1✔
176
        tm.commit()
1✔
177
        self.assertTrue(txn._committed)
1✔
178

179
    def test_abort_w_existing_txn(self):
1✔
180
        class Existing:
1✔
181
            _aborted = False
1✔
182

183
            def abort(self):
1✔
184
                self._aborted = True
1✔
185
        tm = self._makeOne()
1✔
186
        tm._txn = txn = Existing()
1✔
187
        tm.abort()
1✔
188
        self.assertTrue(txn._aborted)
1✔
189

190
    def test_as_context_manager_wo_error(self):
1✔
191
        class _Test:
1✔
192
            _committed = False
1✔
193
            _aborted = False
1✔
194

195
            def commit(self):
1✔
196
                self._committed = True
1✔
197

198
            def abort(self):
1✔
199
                raise AssertionError("This should not be called")
200
        tm = self._makeOne()
1✔
201
        with tm:
1✔
202
            tm._txn = txn = _Test()
1✔
203
        self.assertTrue(txn._committed)
1✔
204
        self.assertFalse(txn._aborted)
1✔
205

206
    def test_as_context_manager_w_error(self):
1✔
207
        class _Test:
1✔
208
            _committed = False
1✔
209
            _aborted = False
1✔
210

211
            def commit(self):
1✔
212
                raise AssertionError("This should not be called")
213

214
            def abort(self):
1✔
215
                self._aborted = True
1✔
216
        tm = self._makeOne()
1✔
217

218
        with self.assertRaises(ZeroDivisionError):
1✔
219
            with tm:
1✔
220
                tm._txn = txn = _Test()
1✔
221
                raise ZeroDivisionError()
1✔
222

223
        self.assertFalse(txn._committed)
1✔
224
        self.assertTrue(txn._aborted)
1✔
225

226
    def test_savepoint_default(self):
1✔
227
        class _Test:
1✔
228
            _sp = None
1✔
229

230
            def savepoint(self, optimistic):
1✔
231
                self._sp = optimistic
1✔
232
        tm = self._makeOne()
1✔
233
        tm._txn = txn = _Test()
1✔
234
        tm.savepoint()
1✔
235
        self.assertFalse(txn._sp)
1✔
236

237
    def test_savepoint_explicit(self):
1✔
238
        class _Test:
1✔
239
            _sp = None
1✔
240

241
            def savepoint(self, optimistic):
1✔
242
                self._sp = optimistic
1✔
243
        tm = self._makeOne()
1✔
244
        tm._txn = txn = _Test()
1✔
245
        tm.savepoint(True)
1✔
246
        self.assertTrue(txn._sp)
1✔
247

248
    def test_attempts_w_invalid_count(self):
1✔
249
        tm = self._makeOne()
1✔
250
        self.assertRaises(ValueError, list, tm.attempts(0))
1✔
251
        self.assertRaises(ValueError, list, tm.attempts(-1))
1✔
252
        self.assertRaises(ValueError, list, tm.attempts(-10))
1✔
253

254
    def test_attempts_w_valid_count(self):
1✔
255
        tm = self._makeOne()
1✔
256
        found = list(tm.attempts(1))
1✔
257
        self.assertEqual(len(found), 1)
1✔
258
        self.assertIs(found[0], tm)
1✔
259

260
    def test_attempts_stop_on_success(self):
1✔
261
        tm = self._makeOne()
1✔
262

263
        i = 0
1✔
264
        for attempt in tm.attempts():
1✔
265
            with attempt:
1✔
266
                i += 1
1✔
267

268
        self.assertEqual(i, 1)
1✔
269

270
    def test_attempts_retries(self):
1✔
271
        import transaction.interfaces
1✔
272

273
        class Retry(transaction.interfaces.TransientError):
1✔
274
            pass
1✔
275

276
        tm = self._makeOne()
1✔
277
        i = 0
1✔
278
        for attempt in tm.attempts(4):
1✔
279
            with attempt:
1✔
280
                i += 1
1✔
281
                if i < 4:
1✔
282
                    raise Retry
1✔
283

284
        self.assertEqual(i, 4)
1✔
285

286
    def test_attempts_retries_but_gives_up(self):
1✔
287
        import transaction.interfaces
1✔
288

289
        class Retry(transaction.interfaces.TransientError):
1✔
290
            pass
1✔
291

292
        tm = self._makeOne()
1✔
293
        i = 0
1✔
294

295
        with self.assertRaises(Retry):
1✔
296
            for attempt in tm.attempts(4):
1!
297
                with attempt:
1✔
298
                    i += 1
1✔
299
                    raise Retry
1✔
300

301
        self.assertEqual(i, 4)
1✔
302

303
    def test_attempts_propigates_errors(self):
1✔
304
        tm = self._makeOne()
1✔
305
        with self.assertRaises(ValueError):
1✔
306
            for attempt in tm.attempts(4):
1!
307
                with attempt:
1✔
308
                    raise ValueError
1✔
309

310
    def test_attempts_defer_to_dm(self):
1✔
311
        import transaction.tests.savepointsample
1✔
312

313
        class DM(transaction.tests.savepointsample.SampleSavepointDataManager):
1✔
314
            def should_retry(self, e):
1✔
315
                if 'should retry' in str(e):
1!
316
                    return True
1✔
317

318
        ntry = 0
1✔
319
        dm = transaction.tests.savepointsample.SampleSavepointDataManager()
1✔
320
        dm2 = DM()
1✔
321
        with transaction.manager:
1✔
322
            dm2['ntry'] = 0
1✔
323

324
        for attempt in transaction.manager.attempts():
1✔
325
            with attempt:
1✔
326
                ntry += 1
1✔
327
                dm['ntry'] = ntry
1✔
328
                dm2['ntry'] = ntry
1✔
329
                if ntry % 3:
1✔
330
                    raise ValueError('we really should retry this')
1✔
331

332
        self.assertEqual(ntry, 3)
1✔
333

334
    def test_attempts_w_default_count(self):
1✔
335
        from transaction._manager import Attempt
1✔
336
        tm = self._makeOne()
1✔
337
        found = list(tm.attempts())
1✔
338
        self.assertEqual(len(found), 3)
1✔
339
        for attempt in found[:-1]:
1✔
340
            self.assertIsInstance(attempt, Attempt)
1✔
341
            self.assertIs(attempt.manager, tm)
1✔
342
        self.assertIs(found[-1], tm)
1✔
343

344
    def test_run(self):
1✔
345
        import transaction.interfaces
1✔
346

347
        class Retry(transaction.interfaces.TransientError):
1✔
348
            pass
1✔
349

350
        tm = self._makeOne()
1✔
351
        i = [0, None]
1✔
352

353
        @tm.run()
1✔
354
        def meaning():
1✔
355
            """Nice doc"""
356
            i[0] += 1
1✔
357
            i[1] = tm.get()
1✔
358
            if i[0] < 3:
1✔
359
                raise Retry
1✔
360
            return 42
1✔
361

362
        self.assertEqual(i[0], 3)
1✔
363
        self.assertEqual(meaning, 42)
1✔
364
        self.assertEqual(i[1].description, "meaning\n\nNice doc")
1✔
365

366
    def test_run_no_name_explicit_tries(self):
1✔
367
        import transaction.interfaces
1✔
368

369
        class Retry(transaction.interfaces.TransientError):
1✔
370
            pass
1✔
371

372
        tm = self._makeOne()
1✔
373
        i = [0, None]
1✔
374

375
        @tm.run(4)
1✔
376
        def _():
1✔
377
            """Nice doc"""
378
            i[0] += 1
1✔
379
            i[1] = tm.get()
1✔
380
            if i[0] < 4:
1✔
381
                raise Retry
1✔
382

383
        self.assertEqual(i[0], 4)
1✔
384
        self.assertEqual(i[1].description, "Nice doc")
1✔
385

386
    def test_run_pos_tries(self):
1✔
387
        tm = self._makeOne()
1✔
388

389
        with self.assertRaises(ValueError):
1✔
390
            tm.run(0)(lambda: None)
1✔
391
        with self.assertRaises(ValueError):
1✔
392
            @tm.run(-1)
1✔
393
            def _():
1✔
394
                raise AssertionError("Never called")
395

396
    def test_run_stop_on_success(self):
1✔
397
        tm = self._makeOne()
1✔
398
        i = [0, None]
1✔
399

400
        @tm.run()
1✔
401
        def meaning():
1✔
402
            i[0] += 1
1✔
403
            i[1] = tm.get()
1✔
404
            return 43
1✔
405

406
        self.assertEqual(i[0], 1)
1✔
407
        self.assertEqual(meaning, 43)
1✔
408
        self.assertEqual(i[1].description, "meaning")
1✔
409

410
    def test_run_retries_but_gives_up(self):
1✔
411
        import transaction.interfaces
1✔
412

413
        class Retry(transaction.interfaces.TransientError):
1✔
414
            pass
1✔
415

416
        tm = self._makeOne()
1✔
417
        i = [0]
1✔
418

419
        with self.assertRaises(Retry):
1✔
420
            @tm.run()
1✔
421
            def _():
1✔
422
                i[0] += 1
1✔
423
                raise Retry
1✔
424

425
        self.assertEqual(i[0], 3)
1✔
426

427
    def test_run_propigates_errors(self):
1✔
428
        tm = self._makeOne()
1✔
429
        with self.assertRaises(ValueError):
1✔
430
            @tm.run
1✔
431
            def _():
1✔
432
                raise ValueError
1✔
433

434
    def test_run_defer_to_dm(self):
1✔
435
        import transaction.tests.savepointsample
1✔
436

437
        class DM(transaction.tests.savepointsample.SampleSavepointDataManager):
1✔
438
            def should_retry(self, e):
1✔
439
                if 'should retry' in str(e):
1!
440
                    return True
1✔
441

442
        ntry = [0]
1✔
443
        dm = transaction.tests.savepointsample.SampleSavepointDataManager()
1✔
444
        dm2 = DM()
1✔
445
        with transaction.manager:
1✔
446
            dm2['ntry'] = 0
1✔
447

448
        @transaction.manager.run
1✔
449
        def _():
1✔
450
            ntry[0] += 1
1✔
451
            dm['ntry'] = ntry[0]
1✔
452
            dm2['ntry'] = ntry[0]
1✔
453
            if ntry[0] % 3:
1✔
454
                raise ValueError('we really should retry this')
1✔
455

456
        self.assertEqual(ntry[0], 3)
1✔
457

458
    def test_run_callable_with_bytes_doc(self):
1✔
459
        import transaction
1✔
460

461
        class Callable:
1✔
462

463
            def __init__(self):
1✔
464
                self.__doc__ = b'some bytes'
1✔
465
                self.__name__ = b'more bytes'
1✔
466

467
            def __call__(self):
1✔
468
                return 42
1✔
469

470
        result = transaction.manager.run(Callable())
1✔
471
        self.assertEqual(result, 42)
1✔
472

473
    def test__retryable_w_transient_error(self):
1✔
474
        from transaction.interfaces import TransientError
1✔
475
        tm = self._makeOne()
1✔
476
        self.assertTrue(tm._retryable(TransientError, object()))
1✔
477

478
    def test__retryable_w_transient_subclass(self):
1✔
479
        from transaction.interfaces import TransientError
1✔
480

481
        class _Derived(TransientError):
1✔
482
            pass
1✔
483
        tm = self._makeOne()
1✔
484
        self.assertTrue(tm._retryable(_Derived, object()))
1✔
485

486
    def test__retryable_w_normal_exception_no_resources(self):
1✔
487
        tm = self._makeOne()
1✔
488
        self.assertFalse(tm._retryable(Exception, object()))
1✔
489

490
    def test__retryable_w_normal_exception_w_resource_voting_yes(self):
1✔
491
        class _Resource:
1✔
492
            def should_retry(self, err):
1✔
493
                return True
1✔
494
        tm = self._makeOne()
1✔
495
        tm.get()._resources.append(_Resource())
1✔
496
        self.assertTrue(tm._retryable(Exception, object()))
1✔
497

498
    def test__retryable_w_multiple(self):
1✔
499
        class _Resource:
1✔
500
            _should = True
1✔
501

502
            def should_retry(self, err):
1✔
503
                return self._should
1✔
504
        tm = self._makeOne()
1✔
505
        res1 = _Resource()
1✔
506
        res1._should = False
1✔
507
        res2 = _Resource()
1✔
508
        tm.get()._resources.append(res1)
1✔
509
        tm.get()._resources.append(res2)
1✔
510
        self.assertTrue(tm._retryable(Exception, object()))
1✔
511

512
    # basic tests with two sub trans jars
513
    # really we only need one, so tests for
514
    # sub1 should identical to tests for sub2
515
    def test_commit_normal(self):
1✔
516

517
        mgr, sub1, sub2, sub3, nosub1 = self._makePopulated()
1✔
518
        sub1.modify()
1✔
519
        sub2.modify()
1✔
520

521
        mgr.commit()
1✔
522

523
        assert sub1._p_jar.ccommit_sub == 0
1✔
524
        assert sub1._p_jar.ctpc_finish == 1
1✔
525

526
    def test_abort_normal(self):
1✔
527

528
        mgr, sub1, sub2, sub3, nosub1 = self._makePopulated()
1✔
529
        sub1.modify()
1✔
530
        sub2.modify()
1✔
531

532
        mgr.abort()
1✔
533

534
        assert sub2._p_jar.cabort == 1
1✔
535

536
    # repeat adding in a nonsub trans jars
537

538
    def test_commit_w_nonsub_jar(self):
1✔
539

540
        mgr, sub1, sub2, sub3, nosub1 = self._makePopulated()
1✔
541
        nosub1.modify()
1✔
542

543
        mgr.commit()
1✔
544

545
        assert nosub1._p_jar.ctpc_finish == 1
1✔
546

547
    def test_abort_w_nonsub_jar(self):
1✔
548

549
        mgr, sub1, sub2, sub3, nosub1 = self._makePopulated()
1✔
550
        nosub1.modify()
1✔
551

552
        mgr.abort()
1✔
553

554
        assert nosub1._p_jar.ctpc_finish == 0
1✔
555
        assert nosub1._p_jar.cabort == 1
1✔
556

557
    ###
558
    # Failure Mode Tests
559
    #
560
    # ok now we do some more interesting
561
    # tests that check the implementations
562
    # error handling by throwing errors from
563
    # various jar methods
564
    ###
565

566
    # first the recoverable errors
567

568
    def test_abort_w_broken_jar(self):
1✔
569
        from transaction import _transaction
1✔
570
        from transaction.tests.common import DummyLogger
1✔
571
        from transaction.tests.common import Monkey
1✔
572
        logger = DummyLogger()
1✔
573
        with Monkey(_transaction, _LOGGER=logger):
1✔
574
            mgr, sub1, sub2, sub3, nosub1 = self._makePopulated()
1✔
575
            sub1._p_jar = BasicJar(errors='abort')
1✔
576
            nosub1.modify()
1✔
577
            sub1.modify(nojar=1)
1✔
578
            sub2.modify()
1✔
579
            try:
1✔
580
                mgr.abort()
1✔
581
            except TestTxnException:
1✔
582
                pass
1✔
583

584
        assert nosub1._p_jar.cabort == 1
1✔
585
        assert sub2._p_jar.cabort == 1
1✔
586

587
    def test_commit_w_broken_jar_commit(self):
1✔
588
        from transaction import _transaction
1✔
589
        from transaction.tests.common import DummyLogger
1✔
590
        from transaction.tests.common import Monkey
1✔
591
        logger = DummyLogger()
1✔
592
        with Monkey(_transaction, _LOGGER=logger):
1✔
593
            mgr, sub1, sub2, sub3, nosub1 = self._makePopulated()
1✔
594
            sub1._p_jar = BasicJar(errors='commit')
1✔
595
            nosub1.modify()
1✔
596
            sub1.modify(nojar=1)
1✔
597
            try:
1✔
598
                mgr.commit()
1✔
599
            except TestTxnException:
1✔
600
                pass
1✔
601

602
        assert nosub1._p_jar.ctpc_finish == 0
1✔
603
        assert nosub1._p_jar.ccommit == 1
1✔
604
        assert nosub1._p_jar.ctpc_abort == 1
1✔
605

606
    def test_commit_w_broken_jar_tpc_vote(self):
1✔
607
        from transaction import _transaction
1✔
608
        from transaction.tests.common import DummyLogger
1✔
609
        from transaction.tests.common import Monkey
1✔
610
        logger = DummyLogger()
1✔
611
        with Monkey(_transaction, _LOGGER=logger):
1✔
612
            mgr, sub1, sub2, sub3, nosub1 = self._makePopulated()
1✔
613
            sub1._p_jar = BasicJar(errors='tpc_vote')
1✔
614
            nosub1.modify()
1✔
615
            sub1.modify(nojar=1)
1✔
616
            try:
1✔
617
                mgr.commit()
1✔
618
            except TestTxnException:
1✔
619
                pass
1✔
620

621
        assert nosub1._p_jar.ctpc_finish == 0
1✔
622
        assert nosub1._p_jar.ccommit == 1
1✔
623
        assert nosub1._p_jar.ctpc_abort == 1
1✔
624
        assert sub1._p_jar.ctpc_abort == 1
1✔
625

626
    def test_commit_w_broken_jar_tpc_begin(self):
1✔
627
        # ok this test reveals a bug in the TM.py
628
        # as the nosub tpc_abort there is ignored.
629

630
        # nosub calling method tpc_begin
631
        # nosub calling method commit
632
        # sub calling method tpc_begin
633
        # sub calling method abort
634
        # sub calling method tpc_abort
635
        # nosub calling method tpc_abort
636
        from transaction import _transaction
1✔
637
        from transaction.tests.common import DummyLogger
1✔
638
        from transaction.tests.common import Monkey
1✔
639
        logger = DummyLogger()
1✔
640
        with Monkey(_transaction, _LOGGER=logger):
1✔
641
            mgr, sub1, sub2, sub3, nosub1 = self._makePopulated()
1✔
642
            sub1._p_jar = BasicJar(errors='tpc_begin')
1✔
643
            nosub1.modify()
1✔
644
            sub1.modify(nojar=1)
1✔
645
            try:
1✔
646
                mgr.commit()
1✔
647
            except TestTxnException:
1✔
648
                pass
1✔
649

650
        assert nosub1._p_jar.ctpc_abort == 1
1✔
651
        assert sub1._p_jar.ctpc_abort == 1
1✔
652

653
    def test_commit_w_broken_jar_tpc_abort_tpc_vote(self):
1✔
654
        from transaction import _transaction
1✔
655
        from transaction.tests.common import DummyLogger
1✔
656
        from transaction.tests.common import Monkey
1✔
657
        logger = DummyLogger()
1✔
658
        with Monkey(_transaction, _LOGGER=logger):
1✔
659
            mgr, sub1, sub2, sub3, nosub1 = self._makePopulated()
1✔
660
            sub1._p_jar = BasicJar(errors=('tpc_abort', 'tpc_vote'))
1✔
661
            nosub1.modify()
1✔
662
            sub1.modify(nojar=1)
1✔
663
            try:
1✔
664
                mgr.commit()
1✔
665
            except TestTxnException:
1✔
666
                pass
1✔
667

668
        assert nosub1._p_jar.ctpc_abort == 1
1✔
669

670
    def test_notify_transaction_late_comers(self):
1✔
671
        # If a datamanager registers for synchonization after a
672
        # transaction has started, we should call newTransaction so it
673
        # can do necessry setup.
674
        from unittest import mock
1✔
675

676
        from .. import TransactionManager
1✔
677
        manager = TransactionManager()
1✔
678
        sync1 = mock.MagicMock()
1✔
679
        manager.registerSynch(sync1)
1✔
680
        sync1.newTransaction.assert_not_called()
1✔
681
        t = manager.begin()
1✔
682
        sync1.newTransaction.assert_called_with(t)
1✔
683
        sync2 = mock.MagicMock()
1✔
684
        manager.registerSynch(sync2)
1✔
685
        sync2.newTransaction.assert_called_with(t)
1✔
686

687
        # for, um, completeness
688
        t.commit()
1✔
689
        for s in sync1, sync2:
1✔
690
            s.beforeCompletion.assert_called_with(t)
1✔
691
            s.afterCompletion.assert_called_with(t)
1✔
692

693
    def test_unregisterSynch_on_transaction_manager_from_serparate_thread(
1✔
694
            self):
695
        # We should be able to get the underlying manager of the thread manager
696
        # and call methods from other threads.
697
        import threading
1✔
698

699
        import transaction
1✔
700

701
        started = threading.Event()
1✔
702
        stopped = threading.Event()
1✔
703

704
        synchronizer = self
1✔
705

706
        class Runner(threading.Thread):
1✔
707

708
            def __init__(self):
1✔
709
                threading.Thread.__init__(self)
1✔
710
                self.manager = transaction.manager.manager
1✔
711
                self.daemon = True
1✔
712
                self.start()
1✔
713

714
            def run(self):
1✔
715
                self.manager.registerSynch(synchronizer)
1✔
716
                started.set()
1✔
717
                stopped.wait()
1✔
718

719
        runner = Runner()
1✔
720
        started.wait()
1✔
721
        runner.manager.unregisterSynch(synchronizer)
1✔
722
        stopped.set()
1✔
723
        runner.join(1)
1✔
724

725
    # The lack of the method below caused a test failure in one run
726
    #   -- caused indirectly by the failure of another test (this
727
    #   indicates that the tests in this suite are not fully isolated).
728
    #   However, defining the methods below reduced the "test coverage"
729
    #   once the initial test failure has been fixed.
730
    #   We therefore comment them out.
731
    #
732
    # the preceeding test (maybe others as well) registers `self` as
733
    # synchronizer; satisfy the `ISynchronizer` requirements
734
    #
735
    # def newTransaction(self, transaction):
736
    #     pass
737
    #
738
    # beforeCompletion = afterCompletion = newTransaction
739

740

741
class TestThreadTransactionManager(unittest.TestCase):
1✔
742

743
    def test_interface(self):
1✔
744
        import transaction
1✔
745
        zope.interface.verify.verifyObject(interfaces.ITransactionManager,
1✔
746
                                           transaction.manager)
747

748
    def test_sync_registration_thread_local_manager(self):
1✔
749
        import transaction
1✔
750

751
        sync = mock.MagicMock()
1✔
752
        sync2 = mock.MagicMock()
1✔
753
        self.assertFalse(transaction.manager.registeredSynchs())
1✔
754
        transaction.manager.registerSynch(sync)
1✔
755
        self.assertTrue(transaction.manager.registeredSynchs())
1✔
756
        transaction.manager.registerSynch(sync2)
1✔
757
        self.assertTrue(transaction.manager.registeredSynchs())
1✔
758
        t = transaction.begin()
1✔
759
        sync.newTransaction.assert_called_with(t)
1✔
760
        transaction.abort()
1✔
761
        sync.beforeCompletion.assert_called_with(t)
1✔
762
        sync.afterCompletion.assert_called_with(t)
1✔
763
        transaction.manager.unregisterSynch(sync)
1✔
764
        self.assertTrue(transaction.manager.registeredSynchs())
1✔
765
        transaction.manager.unregisterSynch(sync2)
1✔
766
        self.assertFalse(transaction.manager.registeredSynchs())
1✔
767
        sync.reset_mock()
1✔
768
        transaction.begin()
1✔
769
        transaction.abort()
1✔
770
        sync.newTransaction.assert_not_called()
1✔
771
        sync.beforeCompletion.assert_not_called()
1✔
772
        sync.afterCompletion.assert_not_called()
1✔
773

774
        self.assertFalse(transaction.manager.registeredSynchs())
1✔
775
        transaction.manager.registerSynch(sync)
1✔
776
        transaction.manager.registerSynch(sync2)
1✔
777
        t = transaction.begin()
1✔
778
        sync.newTransaction.assert_called_with(t)
1✔
779
        self.assertTrue(transaction.manager.registeredSynchs())
1✔
780
        transaction.abort()
1✔
781
        sync.beforeCompletion.assert_called_with(t)
1✔
782
        sync.afterCompletion.assert_called_with(t)
1✔
783
        transaction.manager.clearSynchs()
1✔
784
        self.assertFalse(transaction.manager.registeredSynchs())
1✔
785
        sync.reset_mock()
1✔
786
        transaction.begin()
1✔
787
        transaction.abort()
1✔
788
        sync.newTransaction.assert_not_called()
1✔
789
        sync.beforeCompletion.assert_not_called()
1✔
790
        sync.afterCompletion.assert_not_called()
1✔
791

792
    def test_explicit_thread_local_manager(self):
1✔
793
        import transaction.interfaces
1✔
794

795
        self.assertFalse(transaction.manager.explicit)
1✔
796
        transaction.abort()
1✔
797
        transaction.manager.explicit = True
1✔
798
        self.assertTrue(transaction.manager.explicit)
1✔
799
        with self.assertRaises(transaction.interfaces.NoTransaction):
1✔
800
            transaction.abort()
1✔
801
        transaction.manager.explicit = False
1✔
802
        transaction.abort()
1✔
803

804

805
class AttemptTests(unittest.TestCase):
1✔
806

807
    def _makeOne(self, manager):
1✔
808
        from transaction._manager import Attempt
1✔
809
        return Attempt(manager)
1✔
810

811
    def test___enter__(self):
1✔
812
        manager = DummyManager()
1✔
813
        inst = self._makeOne(manager)
1✔
814
        inst.__enter__()
1✔
815
        self.assertTrue(manager.entered)
1✔
816

817
    def test___exit__no_exc_no_commit_exception(self):
1✔
818
        manager = DummyManager()
1✔
819
        inst = self._makeOne(manager)
1✔
820
        result = inst.__exit__(None, None, None)
1✔
821
        self.assertFalse(result)
1✔
822
        self.assertTrue(manager.committed)
1✔
823

824
    def test___exit__no_exc_nonretryable_commit_exception(self):
1✔
825
        manager = DummyManager(raise_on_commit=ValueError)
1✔
826
        inst = self._makeOne(manager)
1✔
827
        self.assertRaises(ValueError, inst.__exit__, None, None, None)
1✔
828
        self.assertTrue(manager.committed)
1✔
829
        self.assertTrue(manager.aborted)
1✔
830

831
    def test___exit__no_exc_abort_exception_after_nonretryable_commit_exc(
1✔
832
            self):
833
        manager = DummyManager(raise_on_abort=ValueError,
1✔
834
                               raise_on_commit=KeyError)
835
        inst = self._makeOne(manager)
1✔
836
        self.assertRaises(ValueError, inst.__exit__, None, None, None)
1✔
837
        self.assertTrue(manager.committed)
1✔
838
        self.assertTrue(manager.aborted)
1✔
839

840
    def test___exit__no_exc_retryable_commit_exception(self):
1✔
841
        from transaction.interfaces import TransientError
1✔
842
        manager = DummyManager(raise_on_commit=TransientError)
1✔
843
        inst = self._makeOne(manager)
1✔
844
        result = inst.__exit__(None, None, None)
1✔
845
        self.assertTrue(result)
1✔
846
        self.assertTrue(manager.committed)
1✔
847
        self.assertTrue(manager.aborted)
1✔
848

849
    def test___exit__with_exception_value_retryable(self):
1✔
850
        from transaction.interfaces import TransientError
1✔
851
        manager = DummyManager()
1✔
852
        inst = self._makeOne(manager)
1✔
853
        result = inst.__exit__(TransientError, TransientError(), None)
1✔
854
        self.assertTrue(result)
1✔
855
        self.assertFalse(manager.committed)
1✔
856
        self.assertTrue(manager.aborted)
1✔
857

858
    def test___exit__with_exception_value_nonretryable(self):
1✔
859
        manager = DummyManager()
1✔
860
        inst = self._makeOne(manager)
1✔
861
        self.assertRaises(KeyError, inst.__exit__, KeyError, KeyError(), None)
1✔
862
        self.assertFalse(manager.committed)
1✔
863
        self.assertTrue(manager.aborted)
1✔
864

865
    def test_explicit_mode(self):
1✔
866
        from .. import TransactionManager
1✔
867
        from ..interfaces import AlreadyInTransaction
1✔
868
        from ..interfaces import NoTransaction
1✔
869

870
        tm = TransactionManager()
1✔
871
        self.assertFalse(tm.explicit)
1✔
872

873
        tm = TransactionManager(explicit=True)
1✔
874
        self.assertTrue(tm.explicit)
1✔
875
        for name in 'get', 'commit', 'abort', 'doom', 'isDoomed', 'savepoint':
1✔
876
            with self.assertRaises(NoTransaction):
1✔
877
                getattr(tm, name)()
1✔
878

879
        t = tm.begin()
1✔
880
        with self.assertRaises(AlreadyInTransaction):
1✔
881
            tm.begin()
1✔
882

883
        self.assertIs(t, tm.get())
1✔
884

885
        self.assertFalse(tm.isDoomed())
1✔
886
        tm.doom()
1✔
887
        self.assertTrue(tm.isDoomed())
1✔
888
        tm.abort()
1✔
889

890
        for name in 'get', 'commit', 'abort', 'doom', 'isDoomed', 'savepoint':
1✔
891
            with self.assertRaises(NoTransaction):
1✔
892
                getattr(tm, name)()
1✔
893

894
        t = tm.begin()
1✔
895
        self.assertFalse(tm.isDoomed())
1✔
896
        with self.assertRaises(AlreadyInTransaction):
1✔
897
            tm.begin()
1✔
898
        tm.savepoint()
1✔
899
        tm.commit()
1✔
900

901

902
class DummyManager:
1✔
903
    entered = False
1✔
904
    committed = False
1✔
905
    aborted = False
1✔
906

907
    def __init__(self, raise_on_commit=None, raise_on_abort=None):
1✔
908
        self.raise_on_commit = raise_on_commit
1✔
909
        self.raise_on_abort = raise_on_abort
1✔
910

911
    def _retryable(self, t, v):
1✔
912
        from transaction._manager import TransientError
1✔
913
        return issubclass(t, TransientError)
1✔
914

915
    def __enter__(self):
1✔
916
        self.entered = True
1✔
917

918
    def abort(self):
1✔
919
        self.aborted = True
1✔
920
        if self.raise_on_abort:
1✔
921
            raise self.raise_on_abort
1✔
922

923
    def commit(self):
1✔
924
        self.committed = True
1✔
925
        if self.raise_on_commit:
1✔
926
            raise self.raise_on_commit
1✔
927

928

929
class DataObject:
1✔
930

931
    def __init__(self, transaction_manager, nost=0):
1✔
932
        self.transaction_manager = transaction_manager
1✔
933
        self.nost = nost
1✔
934
        self._p_jar = None
1✔
935

936
    def modify(self, nojar=0, tracing=0):
1✔
937
        if not nojar:
1✔
938
            if self.nost:
1✔
939
                self._p_jar = BasicJar(tracing=tracing)
1✔
940
            else:
941
                self._p_jar = BasicJar(tracing=tracing)
1✔
942
        self.transaction_manager.get().join(self._p_jar)
1✔
943

944

945
class TestTxnException(Exception):
1✔
946
    pass
1✔
947

948

949
class BasicJar:
1✔
950

951
    def __init__(self, errors=(), tracing=0):
1✔
952
        if not isinstance(errors, tuple):
1✔
953
            errors = errors,
1✔
954
        self.errors = errors
1✔
955
        self.tracing = tracing
1✔
956
        self.cabort = 0
1✔
957
        self.ccommit = 0
1✔
958
        self.ctpc_begin = 0
1✔
959
        self.ctpc_abort = 0
1✔
960
        self.ctpc_vote = 0
1✔
961
        self.ctpc_finish = 0
1✔
962
        self.cabort_sub = 0
1✔
963
        self.ccommit_sub = 0
1✔
964

965
    def __repr__(self):
1✔
966
        return "<{} {:X} {}>".format(
1✔
967
            self.__class__.__name__, positive_id(self), self.errors)
968

969
    def sortKey(self):
1✔
970
        # All these jars use the same sort key, and Python's list.sort()
971
        # is stable.  These two
972
        return self.__class__.__name__
1✔
973

974
    def check(self, method):
1✔
975
        if self.tracing:  # pragma: no cover
976
            print(f'{str(self.tracing)} calling method {method}')
977

978
        if method in self.errors:
1✔
979
            raise TestTxnException("error %s" % method)
1✔
980

981
    # basic jar txn interface
982

983
    def abort(self, *args):
1✔
984
        self.check('abort')
1✔
985
        self.cabort += 1
1✔
986

987
    def commit(self, *args):
1✔
988
        self.check('commit')
1✔
989
        self.ccommit += 1
1✔
990

991
    def tpc_begin(self, txn, sub=0):
1✔
992
        self.check('tpc_begin')
1✔
993
        self.ctpc_begin += 1
1✔
994

995
    def tpc_vote(self, *args):
1✔
996
        self.check('tpc_vote')
1✔
997
        self.ctpc_vote += 1
1✔
998

999
    def tpc_abort(self, *args):
1✔
1000
        self.check('tpc_abort')
1✔
1001
        self.ctpc_abort += 1
1✔
1002

1003
    def tpc_finish(self, *args):
1✔
1004
        self.check('tpc_finish')
1✔
1005
        self.ctpc_finish += 1
1✔
1006

1007

1008
class DummySynch:
1✔
1009
    def __init__(self):
1✔
1010
        self._txns = set()
1✔
1011

1012
    def newTransaction(self, txn):
1✔
1013
        self._txns.add(txn)
1✔
1014

1015

1016
def positive_id(obj):
1✔
1017
    """Return id(obj) as a non-negative integer."""
1018
    import struct
1✔
1019
    _ADDRESS_MASK = 256 ** struct.calcsize('P')
1✔
1020

1021
    result = id(obj)
1✔
1022
    if result < 0:  # pragma: no cover
1023
        # Happens...on old 32-bit systems?
1024
        result += _ADDRESS_MASK
1025
        assert result > 0
1026
    return result
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