1
# Copyright (C) 2006 Canonical Ltd
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.
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.
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
17
"""Tests for LockDir"""
19
from cStringIO import StringIO
20
from threading import Thread, Lock
28
from bzrlib.errors import (
30
LockContention, LockError, UnlockableTransport,
31
LockNotHeld, LockBroken
33
from bzrlib.lockdir import LockDir, _DEFAULT_TIMEOUT_SECONDS
34
from bzrlib.tests import TestCaseWithTransport
35
from bzrlib.trace import note
37
# These tests sometimes use threads to test the behaviour of lock files with
38
# concurrent actors. This is not a typical (or necessarily supported) use;
39
# they're really meant for guarding between processes.
41
# These tests are run on the default transport provided by the test framework
42
# (typically a local disk transport). That can be changed by the --transport
43
# option to bzr selftest. The required properties of the transport
44
# implementation are tested separately. (The main requirement is just that
45
# they don't allow overwriting nonempty directories.)
47
class TestLockDir(TestCaseWithTransport):
48
"""Test LockDir operations"""
50
def logging_report_function(self, fmt, *args):
51
self._logged_reports.append((fmt, args))
53
def setup_log_reporter(self, lock_dir):
54
self._logged_reports = []
55
lock_dir._report_function = self.logging_report_function
57
def test_00_lock_creation(self):
58
"""Creation of lock file on a transport"""
59
t = self.get_transport()
60
lf = LockDir(t, 'test_lock')
61
self.assertFalse(lf.is_held)
63
def test_01_lock_repr(self):
64
"""Lock string representation"""
65
lf = LockDir(self.get_transport(), 'test_lock')
67
self.assertContainsRe(r, r'^LockDir\(.*/test_lock\)$')
69
def test_02_unlocked_peek(self):
70
lf = LockDir(self.get_transport(), 'test_lock')
71
self.assertEqual(lf.peek(), None)
74
return LockDir(self.get_transport(), 'test_lock')
76
def test_unlock_after_break_raises(self):
81
ld2.force_break(ld2.peek())
82
self.assertRaises(LockBroken, ld.unlock)
84
def test_03_readonly_peek(self):
85
lf = LockDir(self.get_readonly_transport(), 'test_lock')
86
self.assertEqual(lf.peek(), None)
88
def test_10_lock_uncontested(self):
89
"""Acquire and release a lock"""
90
t = self.get_transport()
91
lf = LockDir(t, 'test_lock')
95
self.assertTrue(lf.is_held)
98
self.assertFalse(lf.is_held)
100
def test_11_create_readonly_transport(self):
101
"""Fail to create lock on readonly transport"""
102
t = self.get_readonly_transport()
103
lf = LockDir(t, 'test_lock')
104
self.assertRaises(UnlockableTransport, lf.create)
106
def test_12_lock_readonly_transport(self):
107
"""Fail to lock on readonly transport"""
108
lf = LockDir(self.get_transport(), 'test_lock')
110
lf = LockDir(self.get_readonly_transport(), 'test_lock')
111
self.assertRaises(UnlockableTransport, lf.attempt_lock)
113
def test_20_lock_contested(self):
114
"""Contention to get a lock"""
115
t = self.get_transport()
116
lf1 = LockDir(t, 'test_lock')
119
lf2 = LockDir(t, 'test_lock')
121
# locking is between LockDir instances; aliases within
122
# a single process are not detected
124
self.fail('Failed to detect lock collision')
125
except LockContention, e:
126
self.assertEqual(e.lock, lf2)
127
self.assertContainsRe(str(e),
128
r'^Could not acquire.*test_lock.*$')
131
def test_20_lock_peek(self):
132
"""Peek at the state of a lock"""
133
t = self.get_transport()
134
lf1 = LockDir(t, 'test_lock')
137
# lock is held, should get some info on it
139
self.assertEqual(set(info1.keys()),
140
set(['user', 'nonce', 'hostname', 'pid', 'start_time']))
141
# should get the same info if we look at it through a different
143
info2 = LockDir(t, 'test_lock').peek()
144
self.assertEqual(info1, info2)
145
# locks which are never used should be not-held
146
self.assertEqual(LockDir(t, 'other_lock').peek(), None)
148
def test_21_peek_readonly(self):
149
"""Peek over a readonly transport"""
150
t = self.get_transport()
151
lf1 = LockDir(t, 'test_lock')
153
lf2 = LockDir(self.get_readonly_transport(), 'test_lock')
154
self.assertEqual(lf2.peek(), None)
157
self.assertTrue(info2)
158
self.assertEqual(info2['nonce'], lf1.nonce)
160
def test_30_lock_wait_fail(self):
161
"""Wait on a lock, then fail
163
We ask to wait up to 400ms; this should fail within at most one
164
second. (Longer times are more realistic but we don't want the test
165
suite to take too long, and this should do for now.)
167
t = self.get_transport()
168
lf1 = LockDir(t, 'test_lock')
170
lf2 = LockDir(t, 'test_lock')
171
self.setup_log_reporter(lf2)
175
self.assertRaises(LockContention, lf2.wait_lock,
176
timeout=0.4, poll=0.1)
178
# it should only take about 0.4 seconds, but we allow more time in
179
# case the machine is heavily loaded
180
self.assertTrue(after - before <= 8.0,
181
"took %f seconds to detect lock contention" % (after - before))
184
lock_base = lf2.transport.abspath(lf2.path)
185
self.assertEqual(1, len(self._logged_reports))
186
self.assertEqual('%s %s\n'
188
'Will continue to try until %s\n',
189
self._logged_reports[0][0])
190
args = self._logged_reports[0][1]
191
self.assertEqual('Unable to obtain', args[0])
192
self.assertEqual('lock %s' % (lock_base,), args[1])
193
self.assertStartsWith(args[2], 'held by ')
194
self.assertStartsWith(args[3], 'locked ')
195
self.assertEndsWith(args[3], ' ago')
196
self.assertContainsRe(args[4], r'\d\d:\d\d:\d\d')
198
def test_31_lock_wait_easy(self):
199
"""Succeed when waiting on a lock with no contention.
201
t = self.get_transport()
202
lf1 = LockDir(t, 'test_lock')
204
self.setup_log_reporter(lf1)
207
lf1.wait_lock(timeout=0.4, poll=0.1)
209
self.assertTrue(after - before <= 1.0)
212
self.assertEqual([], self._logged_reports)
214
def test_32_lock_wait_succeed(self):
215
"""Succeed when trying to acquire a lock that gets released
217
One thread holds on a lock and then releases it; another
220
t = self.get_transport()
221
lf1 = LockDir(t, 'test_lock')
225
def wait_and_unlock():
228
unlocker = Thread(target=wait_and_unlock)
231
lf2 = LockDir(t, 'test_lock')
232
self.setup_log_reporter(lf2)
235
lf2.wait_lock(timeout=0.4, poll=0.1)
237
self.assertTrue(after - before <= 1.0)
241
# There should be only 1 report, even though it should have to
243
lock_base = lf2.transport.abspath(lf2.path)
244
self.assertEqual(1, len(self._logged_reports))
245
self.assertEqual('%s %s\n'
247
'Will continue to try until %s\n',
248
self._logged_reports[0][0])
249
args = self._logged_reports[0][1]
250
self.assertEqual('Unable to obtain', args[0])
251
self.assertEqual('lock %s' % (lock_base,), args[1])
252
self.assertStartsWith(args[2], 'held by ')
253
self.assertStartsWith(args[3], 'locked ')
254
self.assertEndsWith(args[3], ' ago')
255
self.assertContainsRe(args[4], r'\d\d:\d\d:\d\d')
257
def test_33_wait(self):
258
"""Succeed when waiting on a lock that gets released
260
The difference from test_32_lock_wait_succeed is that the second
261
caller does not actually acquire the lock, but just waits for it
262
to be released. This is done over a readonly transport.
264
t = self.get_transport()
265
lf1 = LockDir(t, 'test_lock')
269
def wait_and_unlock():
272
unlocker = Thread(target=wait_and_unlock)
275
lf2 = LockDir(self.get_readonly_transport(), 'test_lock')
277
# wait but don't lock
278
lf2.wait(timeout=0.4, poll=0.1)
280
self.assertTrue(after - before <= 1.0)
284
def test_34_lock_write_waits(self):
285
"""LockDir.lock_write() will wait for the lock."""
286
t = self.get_transport()
287
lf1 = LockDir(t, 'test_lock')
291
def wait_and_unlock():
294
unlocker = Thread(target=wait_and_unlock)
297
lf2 = LockDir(t, 'test_lock')
298
self.setup_log_reporter(lf2)
306
# There should be only 1 report, even though it should have to
308
lock_base = lf2.transport.abspath(lf2.path)
309
self.assertEqual(1, len(self._logged_reports))
310
self.assertEqual('%s %s\n'
312
'Will continue to try until %s\n',
313
self._logged_reports[0][0])
314
args = self._logged_reports[0][1]
315
self.assertEqual('Unable to obtain', args[0])
316
self.assertEqual('lock %s' % (lock_base,), args[1])
317
self.assertStartsWith(args[2], 'held by ')
318
self.assertStartsWith(args[3], 'locked ')
319
self.assertEndsWith(args[3], ' ago')
320
self.assertContainsRe(args[4], r'\d\d:\d\d:\d\d')
322
def test_35_wait_lock_changing(self):
323
"""LockDir.wait_lock() will report if the lock changes underneath.
325
This is the stages we want to happen:
327
0) Synchronization locks are created and locked.
328
1) Lock1 obtains the lockdir, and releases the 'check' lock.
329
2) Lock2 grabs the 'check' lock, and checks the lockdir.
330
It sees the lockdir is already acquired, reports the fact,
331
and unsets the 'checked' lock.
332
3) Thread1 blocks on acquiring the 'checked' lock, and then tells
333
Lock1 to release and acquire the lockdir. This resets the 'check'
335
4) Lock2 acquires the 'check' lock, and checks again. It notices
336
that the holder of the lock has changed, and so reports a new
338
5) Thread1 blocks on the 'checked' lock, this time, it completely
339
unlocks the lockdir, allowing Lock2 to acquire the lock.
342
wait_to_check_lock = Lock()
343
wait_until_checked_lock = Lock()
345
wait_to_check_lock.acquire()
346
wait_until_checked_lock.acquire()
347
note('locked check and checked locks')
349
class LockDir1(LockDir):
350
"""Use the synchronization points for the first lock."""
352
def attempt_lock(self):
353
# Once we have acquired the lock, it is okay for
354
# the other lock to check it
356
return super(LockDir1, self).attempt_lock()
358
note('lock1: releasing check lock')
359
wait_to_check_lock.release()
361
class LockDir2(LockDir):
362
"""Use the synchronization points for the second lock."""
364
def attempt_lock(self):
365
note('lock2: waiting for check lock')
366
wait_to_check_lock.acquire()
367
note('lock2: acquired check lock')
369
return super(LockDir2, self).attempt_lock()
371
note('lock2: releasing checked lock')
372
wait_until_checked_lock.release()
374
t = self.get_transport()
375
lf1 = LockDir1(t, 'test_lock')
378
lf2 = LockDir2(t, 'test_lock')
379
self.setup_log_reporter(lf2)
381
def wait_and_switch():
383
# Block until lock2 has had a chance to check
384
note('lock1: waiting 1 for checked lock')
385
wait_until_checked_lock.acquire()
386
note('lock1: acquired for checked lock')
387
note('lock1: released lockdir')
389
note('lock1: acquiring lockdir')
390
# Create a new nonce, so the lock looks different.
391
lf1.nonce = osutils.rand_chars(20)
393
note('lock1: acquired lockdir')
395
# Block until lock2 has peeked again
396
note('lock1: waiting 2 for checked lock')
397
wait_until_checked_lock.acquire()
398
note('lock1: acquired for checked lock')
399
# Now unlock, and let lock 2 grab the lock
401
wait_to_check_lock.release()
403
unlocker = Thread(target=wait_and_switch)
406
# Wait and play against the other thread
407
lf2.wait_lock(timeout=1.0, poll=0.01)
412
# There should be 2 reports, because the lock changed
413
lock_base = lf2.transport.abspath(lf2.path)
414
self.assertEqual(2, len(self._logged_reports))
416
self.assertEqual('%s %s\n'
418
'Will continue to try until %s\n',
419
self._logged_reports[0][0])
420
args = self._logged_reports[0][1]
421
self.assertEqual('Unable to obtain', args[0])
422
self.assertEqual('lock %s' % (lock_base,), args[1])
423
self.assertStartsWith(args[2], 'held by ')
424
self.assertStartsWith(args[3], 'locked ')
425
self.assertEndsWith(args[3], ' ago')
426
self.assertContainsRe(args[4], r'\d\d:\d\d:\d\d')
428
self.assertEqual('%s %s\n'
430
'Will continue to try until %s\n',
431
self._logged_reports[1][0])
432
args = self._logged_reports[1][1]
433
self.assertEqual('Lock owner changed for', args[0])
434
self.assertEqual('lock %s' % (lock_base,), args[1])
435
self.assertStartsWith(args[2], 'held by ')
436
self.assertStartsWith(args[3], 'locked ')
437
self.assertEndsWith(args[3], ' ago')
438
self.assertContainsRe(args[4], r'\d\d:\d\d:\d\d')
440
def test_40_confirm_easy(self):
441
"""Confirm a lock that's already held"""
442
t = self.get_transport()
443
lf1 = LockDir(t, 'test_lock')
448
def test_41_confirm_not_held(self):
449
"""Confirm a lock that's already held"""
450
t = self.get_transport()
451
lf1 = LockDir(t, 'test_lock')
453
self.assertRaises(LockNotHeld, lf1.confirm)
455
def test_42_confirm_broken_manually(self):
456
"""Confirm a lock broken by hand"""
457
t = self.get_transport()
458
lf1 = LockDir(t, 'test_lock')
461
t.move('test_lock', 'lock_gone_now')
462
self.assertRaises(LockBroken, lf1.confirm)
464
def test_43_break(self):
465
"""Break a lock whose caller has forgotten it"""
466
t = self.get_transport()
467
lf1 = LockDir(t, 'test_lock')
470
# we incorrectly discard the lock object without unlocking it
472
# someone else sees it's still locked
473
lf2 = LockDir(t, 'test_lock')
474
holder_info = lf2.peek()
475
self.assertTrue(holder_info)
476
lf2.force_break(holder_info)
477
# now we should be able to take it
481
def test_44_break_already_released(self):
482
"""Lock break races with regular release"""
483
t = self.get_transport()
484
lf1 = LockDir(t, 'test_lock')
487
# someone else sees it's still locked
488
lf2 = LockDir(t, 'test_lock')
489
holder_info = lf2.peek()
490
# in the interim the lock is released
492
# break should succeed
493
lf2.force_break(holder_info)
494
# now we should be able to take it
498
def test_45_break_mismatch(self):
499
"""Lock break races with someone else acquiring it"""
500
t = self.get_transport()
501
lf1 = LockDir(t, 'test_lock')
504
# someone else sees it's still locked
505
lf2 = LockDir(t, 'test_lock')
506
holder_info = lf2.peek()
507
# in the interim the lock is released
509
lf3 = LockDir(t, 'test_lock')
511
# break should now *fail*
512
self.assertRaises(LockBreakMismatch, lf2.force_break,
516
def test_46_fake_read_lock(self):
517
t = self.get_transport()
518
lf1 = LockDir(t, 'test_lock')
523
def test_50_lockdir_representation(self):
524
"""Check the on-disk representation of LockDirs is as expected.
526
There should always be a top-level directory named by the lock.
527
When the lock is held, there should be a lockname/held directory
528
containing an info file.
530
t = self.get_transport()
531
lf1 = LockDir(t, 'test_lock')
533
self.assertTrue(t.has('test_lock'))
535
self.assertTrue(t.has('test_lock/held/info'))
537
self.assertFalse(t.has('test_lock/held/info'))
539
def test_break_lock(self):
540
# the ui based break_lock routine should Just Work (tm)
541
ld1 = self.get_lock()
542
ld2 = self.get_lock()
545
# do this without IO redirection to ensure it doesn't prompt.
546
self.assertRaises(AssertionError, ld1.break_lock)
547
orig_factory = bzrlib.ui.ui_factory
548
# silent ui - no need for stdout
549
bzrlib.ui.ui_factory = bzrlib.ui.SilentUIFactory()
550
bzrlib.ui.ui_factory.stdin = StringIO("y\n")
553
self.assertRaises(LockBroken, ld1.unlock)
555
bzrlib.ui.ui_factory = orig_factory
557
def test_create_missing_base_directory(self):
558
"""If LockDir.path doesn't exist, it can be created
560
Some people manually remove the entire lock/ directory trying
561
to unlock a stuck repository/branch/etc. Rather than failing
562
after that, just create the lock directory when needed.
564
t = self.get_transport()
565
lf1 = LockDir(t, 'test_lock')
568
self.failUnless(t.has('test_lock'))
571
self.failIf(t.has('test_lock'))
573
# This will create 'test_lock' if it needs to
575
self.failUnless(t.has('test_lock'))
576
self.failUnless(t.has('test_lock/held/info'))
579
self.failIf(t.has('test_lock/held/info'))
581
def test__format_lock_info(self):
582
ld1 = self.get_lock()
586
info_list = ld1._format_lock_info(ld1.peek())
589
self.assertEqual('lock %s' % (ld1.transport.abspath(ld1.path),),
591
self.assertContainsRe(info_list[1],
592
r'^held by .* on host .* \[process #\d*\]$')
593
self.assertContainsRe(info_list[2], r'locked \d+ seconds? ago$')
595
def test_lock_without_email(self):
596
global_config = config.GlobalConfig()
597
# Intentionally has no email address
598
global_config.set_user_option('email', 'User Identity')
599
ld1 = self.get_lock()