~bzr-pqm/bzr/bzr.dev

1553.5.12 by Martin Pool
New LockDir locking mechanism
1
# Copyright (C) 2006 Canonical Ltd
2
3
# This program is free software; you can redistribute it and/or modify
4
# it under the terms of the GNU General Public License as published by
5
# the Free Software Foundation; either version 2 of the License, or
6
# (at your option) any later version.
7
8
# This program is distributed in the hope that it will be useful,
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11
# GNU General Public License for more details.
12
13
# You should have received a copy of the GNU General Public License
14
# along with this program; if not, write to the Free Software
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
16
17
"""Tests for LockDir"""
18
19
from threading import Thread
20
import time
21
1553.5.27 by Martin Pool
Confirm that only the intended holder of a lock was broken.
22
from bzrlib.errors import (
23
        LockBreakMismatch,
24
        LockContention, LockError, UnlockableTransport,
1553.5.23 by Martin Pool
Start LockDir.confirm method and LockBroken exception
25
        LockNotHeld, LockBroken
1553.5.27 by Martin Pool
Confirm that only the intended holder of a lock was broken.
26
        )
1553.5.12 by Martin Pool
New LockDir locking mechanism
27
from bzrlib.lockdir import LockDir
1553.5.33 by Martin Pool
LockDir review comment fixes
28
from bzrlib.tests import TestCaseWithTransport
1553.5.12 by Martin Pool
New LockDir locking mechanism
29
30
# These tests sometimes use threads to test the behaviour of lock files with
31
# concurrent actors.  This is not a typical (or necessarily supported) use;
32
# they're really meant for guarding between processes.
33
1553.5.58 by Martin Pool
Change LockDirs to format "lock-name/held/info"
34
# These tests are run on the default transport provided by the test framework
35
# (typically a local disk transport).  That can be changed by the --transport
36
# option to bzr selftest.  The required properties of the transport
37
# implementation are tested separately.  (The main requirement is just that
38
# they don't allow overwriting nonempty directories.)
39
1553.5.12 by Martin Pool
New LockDir locking mechanism
40
class TestLockDir(TestCaseWithTransport):
41
    """Test LockDir operations"""
42
43
    def test_00_lock_creation(self):
44
        """Creation of lock file on a transport"""
45
        t = self.get_transport()
46
        lf = LockDir(t, 'test_lock')
1553.5.13 by Martin Pool
New Transport.rename that mustn't overwrite
47
        self.assertFalse(lf.is_held)
1553.5.12 by Martin Pool
New LockDir locking mechanism
48
49
    def test_01_lock_repr(self):
50
        """Lock string representation"""
51
        lf = LockDir(self.get_transport(), 'test_lock')
52
        r = repr(lf)
53
        self.assertContainsRe(r, r'^LockDir\(.*/test_lock\)$')
54
55
    def test_02_unlocked_peek(self):
56
        lf = LockDir(self.get_transport(), 'test_lock')
57
        self.assertEqual(lf.peek(), None)
58
59
    def test_03_readonly_peek(self):
60
        lf = LockDir(self.get_readonly_transport(), 'test_lock')
61
        self.assertEqual(lf.peek(), None)
62
63
    def test_10_lock_uncontested(self):
64
        """Acquire and release a lock"""
65
        t = self.get_transport()
66
        lf = LockDir(t, 'test_lock')
1553.5.58 by Martin Pool
Change LockDirs to format "lock-name/held/info"
67
        lf.create()
1553.5.12 by Martin Pool
New LockDir locking mechanism
68
        lf.attempt_lock()
69
        try:
1553.5.13 by Martin Pool
New Transport.rename that mustn't overwrite
70
            self.assertTrue(lf.is_held)
1553.5.12 by Martin Pool
New LockDir locking mechanism
71
        finally:
72
            lf.unlock()
1553.5.13 by Martin Pool
New Transport.rename that mustn't overwrite
73
            self.assertFalse(lf.is_held)
1553.5.12 by Martin Pool
New LockDir locking mechanism
74
1553.5.58 by Martin Pool
Change LockDirs to format "lock-name/held/info"
75
    def test_11_create_readonly_transport(self):
76
        """Fail to create lock on readonly transport"""
77
        t = self.get_readonly_transport()
78
        lf = LockDir(t, 'test_lock')
79
        self.assertRaises(UnlockableTransport, lf.create)
80
81
    def test_12_lock_readonly_transport(self):
1553.5.12 by Martin Pool
New LockDir locking mechanism
82
        """Fail to lock on readonly transport"""
1553.5.58 by Martin Pool
Change LockDirs to format "lock-name/held/info"
83
        lf = LockDir(self.get_transport(), 'test_lock')
84
        lf.create()
85
        lf = LockDir(self.get_readonly_transport(), 'test_lock')
1553.5.12 by Martin Pool
New LockDir locking mechanism
86
        self.assertRaises(UnlockableTransport, lf.attempt_lock)
87
88
    def test_20_lock_contested(self):
89
        """Contention to get a lock"""
90
        t = self.get_transport()
91
        lf1 = LockDir(t, 'test_lock')
1553.5.58 by Martin Pool
Change LockDirs to format "lock-name/held/info"
92
        lf1.create()
1553.5.12 by Martin Pool
New LockDir locking mechanism
93
        lf1.attempt_lock()
94
        lf2 = LockDir(t, 'test_lock')
95
        try:
96
            # locking is between LockDir instances; aliases within 
97
            # a single process are not detected
98
            lf2.attempt_lock()
99
            self.fail('Failed to detect lock collision')
100
        except LockContention, e:
101
            self.assertEqual(e.lock, lf2)
102
            self.assertContainsRe(str(e),
103
                    r'^Could not acquire.*test_lock.*$')
104
        lf1.unlock()
105
106
    def test_20_lock_peek(self):
107
        """Peek at the state of a lock"""
108
        t = self.get_transport()
109
        lf1 = LockDir(t, 'test_lock')
1553.5.58 by Martin Pool
Change LockDirs to format "lock-name/held/info"
110
        lf1.create()
1553.5.12 by Martin Pool
New LockDir locking mechanism
111
        lf1.attempt_lock()
112
        # lock is held, should get some info on it
113
        info1 = lf1.peek()
114
        self.assertEqual(set(info1.keys()),
115
                         set(['user', 'nonce', 'hostname', 'pid', 'start_time']))
116
        # should get the same info if we look at it through a different
117
        # instance
118
        info2 = LockDir(t, 'test_lock').peek()
119
        self.assertEqual(info1, info2)
120
        # locks which are never used should be not-held
121
        self.assertEqual(LockDir(t, 'other_lock').peek(), None)
122
123
    def test_21_peek_readonly(self):
124
        """Peek over a readonly transport"""
125
        t = self.get_transport()
126
        lf1 = LockDir(t, 'test_lock')
1553.5.58 by Martin Pool
Change LockDirs to format "lock-name/held/info"
127
        lf1.create()
1553.5.12 by Martin Pool
New LockDir locking mechanism
128
        lf2 = LockDir(self.get_readonly_transport(), 'test_lock')
129
        self.assertEqual(lf2.peek(), None)
130
        lf1.attempt_lock()
131
        info2 = lf2.peek()
132
        self.assertTrue(info2)
133
        self.assertEqual(info2['nonce'], lf1.nonce)
134
135
    def test_30_lock_wait_fail(self):
136
        """Wait on a lock, then fail
137
        
138
        We ask to wait up to 400ms; this should fail within at most one
139
        second.  (Longer times are more realistic but we don't want the test
140
        suite to take too long, and this should do for now.)
141
        """
142
        t = self.get_transport()
143
        lf1 = LockDir(t, 'test_lock')
1553.5.58 by Martin Pool
Change LockDirs to format "lock-name/held/info"
144
        lf1.create()
1553.5.12 by Martin Pool
New LockDir locking mechanism
145
        lf2 = LockDir(t, 'test_lock')
146
        lf1.attempt_lock()
147
        try:
148
            before = time.time()
149
            self.assertRaises(LockContention, lf2.wait_lock,
150
                              timeout=0.4, poll=0.1)
151
            after = time.time()
152
            self.assertTrue(after - before <= 1.0)
153
        finally:
154
            lf1.unlock()
155
156
    def test_31_lock_wait_easy(self):
157
        """Succeed when waiting on a lock with no contention.
158
        """
159
        t = self.get_transport()
1553.5.58 by Martin Pool
Change LockDirs to format "lock-name/held/info"
160
        lf1 = LockDir(t, 'test_lock')
161
        lf1.create()
1553.5.12 by Martin Pool
New LockDir locking mechanism
162
        try:
163
            before = time.time()
1553.5.58 by Martin Pool
Change LockDirs to format "lock-name/held/info"
164
            lf1.wait_lock(timeout=0.4, poll=0.1)
1553.5.12 by Martin Pool
New LockDir locking mechanism
165
            after = time.time()
166
            self.assertTrue(after - before <= 1.0)
167
        finally:
1553.5.58 by Martin Pool
Change LockDirs to format "lock-name/held/info"
168
            lf1.unlock()
1553.5.12 by Martin Pool
New LockDir locking mechanism
169
170
    def test_32_lock_wait_succeed(self):
171
        """Succeed when trying to acquire a lock that gets released
172
1553.5.77 by Martin Pool
doc
173
        One thread holds on a lock and then releases it; another 
174
        tries to lock it.
1553.5.12 by Martin Pool
New LockDir locking mechanism
175
        """
176
        t = self.get_transport()
177
        lf1 = LockDir(t, 'test_lock')
1553.5.58 by Martin Pool
Change LockDirs to format "lock-name/held/info"
178
        lf1.create()
1553.5.12 by Martin Pool
New LockDir locking mechanism
179
        lf1.attempt_lock()
180
181
        def wait_and_unlock():
182
            time.sleep(0.1)
183
            lf1.unlock()
184
        unlocker = Thread(target=wait_and_unlock)
185
        unlocker.start()
186
        try:
187
            lf2 = LockDir(t, 'test_lock')
188
            before = time.time()
189
            # wait and then lock
190
            lf2.wait_lock(timeout=0.4, poll=0.1)
191
            after = time.time()
192
            self.assertTrue(after - before <= 1.0)
193
        finally:
194
            unlocker.join()
195
196
    def test_33_wait(self):
197
        """Succeed when waiting on a lock that gets released
198
199
        The difference from test_32_lock_wait_succeed is that the second 
200
        caller does not actually acquire the lock, but just waits for it
201
        to be released.  This is done over a readonly transport.
202
        """
203
        t = self.get_transport()
204
        lf1 = LockDir(t, 'test_lock')
1553.5.58 by Martin Pool
Change LockDirs to format "lock-name/held/info"
205
        lf1.create()
1553.5.12 by Martin Pool
New LockDir locking mechanism
206
        lf1.attempt_lock()
207
208
        def wait_and_unlock():
209
            time.sleep(0.1)
210
            lf1.unlock()
211
        unlocker = Thread(target=wait_and_unlock)
212
        unlocker.start()
213
        try:
214
            lf2 = LockDir(self.get_readonly_transport(), 'test_lock')
215
            before = time.time()
216
            # wait but don't lock
217
            lf2.wait(timeout=0.4, poll=0.1)
218
            after = time.time()
219
            self.assertTrue(after - before <= 1.0)
220
        finally:
221
            unlocker.join()
1553.5.20 by Martin Pool
Start adding LockDir.confirm() method
222
223
    def test_40_confirm_easy(self):
224
        """Confirm a lock that's already held"""
225
        t = self.get_transport()
226
        lf1 = LockDir(t, 'test_lock')
1553.5.58 by Martin Pool
Change LockDirs to format "lock-name/held/info"
227
        lf1.create()
1553.5.20 by Martin Pool
Start adding LockDir.confirm() method
228
        lf1.attempt_lock()
229
        lf1.confirm()
230
231
    def test_41_confirm_not_held(self):
232
        """Confirm a lock that's already held"""
233
        t = self.get_transport()
234
        lf1 = LockDir(t, 'test_lock')
1553.5.58 by Martin Pool
Change LockDirs to format "lock-name/held/info"
235
        lf1.create()
1553.5.20 by Martin Pool
Start adding LockDir.confirm() method
236
        self.assertRaises(LockNotHeld, lf1.confirm)
1553.5.23 by Martin Pool
Start LockDir.confirm method and LockBroken exception
237
238
    def test_42_confirm_broken_manually(self):
239
        """Confirm a lock broken by hand"""
240
        t = self.get_transport()
241
        lf1 = LockDir(t, 'test_lock')
1553.5.58 by Martin Pool
Change LockDirs to format "lock-name/held/info"
242
        lf1.create()
1553.5.23 by Martin Pool
Start LockDir.confirm method and LockBroken exception
243
        lf1.attempt_lock()
244
        t.move('test_lock', 'lock_gone_now')
245
        self.assertRaises(LockBroken, lf1.confirm)
1553.5.25 by Martin Pool
New LockDir.force_break and simple test case
246
247
    def test_43_break(self):
248
        """Break a lock whose caller has forgotten it"""
249
        t = self.get_transport()
250
        lf1 = LockDir(t, 'test_lock')
1553.5.58 by Martin Pool
Change LockDirs to format "lock-name/held/info"
251
        lf1.create()
1553.5.25 by Martin Pool
New LockDir.force_break and simple test case
252
        lf1.attempt_lock()
253
        # we incorrectly discard the lock object without unlocking it
254
        del lf1
255
        # someone else sees it's still locked
256
        lf2 = LockDir(t, 'test_lock')
1553.5.27 by Martin Pool
Confirm that only the intended holder of a lock was broken.
257
        holder_info = lf2.peek()
258
        self.assertTrue(holder_info)
259
        lf2.force_break(holder_info)
1553.5.25 by Martin Pool
New LockDir.force_break and simple test case
260
        # now we should be able to take it
261
        lf2.attempt_lock()
262
        lf2.confirm()
1553.5.26 by Martin Pool
Breaking an already-released lock should just succeed
263
264
    def test_44_break_already_released(self):
265
        """Lock break races with regular release"""
266
        t = self.get_transport()
267
        lf1 = LockDir(t, 'test_lock')
1553.5.58 by Martin Pool
Change LockDirs to format "lock-name/held/info"
268
        lf1.create()
1553.5.26 by Martin Pool
Breaking an already-released lock should just succeed
269
        lf1.attempt_lock()
270
        # someone else sees it's still locked
271
        lf2 = LockDir(t, 'test_lock')
272
        holder_info = lf2.peek()
273
        # in the interim the lock is released
274
        lf1.unlock()
275
        # break should succeed
1553.5.27 by Martin Pool
Confirm that only the intended holder of a lock was broken.
276
        lf2.force_break(holder_info)
1553.5.26 by Martin Pool
Breaking an already-released lock should just succeed
277
        # now we should be able to take it
278
        lf2.attempt_lock()
279
        lf2.confirm()
280
1553.5.27 by Martin Pool
Confirm that only the intended holder of a lock was broken.
281
    def test_45_break_mismatch(self):
282
        """Lock break races with someone else acquiring it"""
283
        t = self.get_transport()
284
        lf1 = LockDir(t, 'test_lock')
1553.5.58 by Martin Pool
Change LockDirs to format "lock-name/held/info"
285
        lf1.create()
1553.5.27 by Martin Pool
Confirm that only the intended holder of a lock was broken.
286
        lf1.attempt_lock()
287
        # someone else sees it's still locked
288
        lf2 = LockDir(t, 'test_lock')
289
        holder_info = lf2.peek()
290
        # in the interim the lock is released
291
        lf1.unlock()
292
        lf3 = LockDir(t, 'test_lock')
293
        lf3.attempt_lock()
294
        # break should now *fail*
295
        self.assertRaises(LockBreakMismatch, lf2.force_break,
296
                          holder_info)
297
        lf3.unlock()
1553.5.54 by Martin Pool
Add LockDir.read_lock fake method
298
299
    def test_46_fake_read_lock(self):
300
        t = self.get_transport()
301
        lf1 = LockDir(t, 'test_lock')
1553.5.58 by Martin Pool
Change LockDirs to format "lock-name/held/info"
302
        lf1.create()
1553.5.54 by Martin Pool
Add LockDir.read_lock fake method
303
        lf1.lock_read()
304
        lf1.unlock()
1553.5.58 by Martin Pool
Change LockDirs to format "lock-name/held/info"
305
306
    def test_50_lockdir_representation(self):
307
        """Check the on-disk representation of LockDirs is as expected.
308
309
        There should always be a top-level directory named by the lock.
310
        When the lock is held, there should be a lockname/held directory 
311
        containing an info file.
312
        """
313
        t = self.get_transport()
314
        lf1 = LockDir(t, 'test_lock')
315
        lf1.create()
316
        self.assertTrue(t.has('test_lock'))
317
        lf1.lock_write()
318
        self.assertTrue(t.has('test_lock/held/info'))
319
        lf1.unlock()
320
        self.assertFalse(t.has('test_lock/held/info'))