1
# Copyright (C) 2007-2010 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
"""Tests that branch classes implement hook callouts correctly."""
20
branch as _mod_branch,
26
from bzrlib.tests import test_server
28
class ChangeBranchTipTestCase(tests.TestCaseWithMemoryTransport):
29
"""Base TestCase for testing pre/post_change_branch_tip hooks."""
31
def install_logging_hook(self, prefix):
32
"""Add a hook that logs calls made to it.
34
:returns: the list that the calls will be appended to.
37
_mod_branch.Branch.hooks.install_named_hook(
38
prefix + '_change_branch_tip', hook_calls.append, None)
41
def make_branch_with_revision_ids(self, *revision_ids):
42
"""Makes a branch with the given commits."""
43
tree = self.make_branch_and_memory_tree('source')
46
for revision_id in revision_ids:
47
tree.commit(u'Message of ' + revision_id.decode('utf8'),
53
def assertHookCalls(self, expected_params, branch, hook_calls=None,
55
if hook_calls is None:
56
hook_calls = self.hook_calls
57
if isinstance(branch, remote.RemoteBranch):
58
# For a remote branch, both the server and the client will raise
59
# this hook, and we see both in the test environment. The remote
60
# instance comes in between the clients - the client doe pre, the
61
# server does pre, the server does post, the client does post.
66
self.assertEqual(expected_params, hook_calls[offset])
67
self.assertEqual(2, len(hook_calls))
69
self.assertEqual([expected_params], hook_calls)
72
class TestSetRevisionHistoryHook(ChangeBranchTipTestCase):
76
super(TestSetRevisionHistoryHook, self).setUp()
78
def capture_set_rh_hook(self, branch, rev_history):
79
"""Capture post set-rh hook calls to self.hook_calls.
81
The call is logged, as is some state of the branch.
83
self.hook_calls.append(
84
('set_rh', branch, rev_history, branch.is_locked()))
86
def test_set_rh_empty_history(self):
87
branch = self.make_branch('source')
88
_mod_branch.Branch.hooks.install_named_hook(
89
'set_rh', self.capture_set_rh_hook, None)
90
branch.set_revision_history([])
91
expected_params = ('set_rh', branch, [], True)
92
self.assertHookCalls(expected_params, branch)
94
def test_set_rh_nonempty_history(self):
95
tree = self.make_branch_and_memory_tree('source')
98
tree.commit('another commit', rev_id='f\xc2\xb5')
99
tree.commit('empty commit', rev_id='foo')
102
_mod_branch.Branch.hooks.install_named_hook(
103
'set_rh', self.capture_set_rh_hook, None)
104
# some branches require that their history be set to a revision in the
106
branch.set_revision_history(['f\xc2\xb5'])
107
expected_params =('set_rh', branch, ['f\xc2\xb5'], True)
108
self.assertHookCalls(expected_params, branch)
110
def test_set_rh_branch_is_locked(self):
111
branch = self.make_branch('source')
112
_mod_branch.Branch.hooks.install_named_hook(
113
'set_rh', self.capture_set_rh_hook, None)
114
branch.set_revision_history([])
115
expected_params = ('set_rh', branch, [], True)
116
self.assertHookCalls(expected_params, branch)
118
def test_set_rh_calls_all_hooks_no_errors(self):
119
branch = self.make_branch('source')
120
_mod_branch.Branch.hooks.install_named_hook(
121
'set_rh', self.capture_set_rh_hook, None)
122
_mod_branch.Branch.hooks.install_named_hook(
123
'set_rh', self.capture_set_rh_hook, None)
124
branch.set_revision_history([])
125
expected_calls = [('set_rh', branch, [], True),
126
('set_rh', branch, [], True),
128
if isinstance(branch, remote.RemoteBranch):
129
# For a remote branch, both the server and the client will raise
130
# set_rh, and the server will do so first because that is where
131
# the change takes place.
132
self.assertEqual(expected_calls, self.hook_calls[2:])
133
self.assertEqual(4, len(self.hook_calls))
135
self.assertEqual(expected_calls, self.hook_calls)
138
class TestOpen(tests.TestCaseWithMemoryTransport):
140
def capture_hook(self, branch):
141
self.hook_calls.append(branch)
143
def install_hook(self):
145
_mod_branch.Branch.hooks.install_named_hook(
146
'open', self.capture_hook, None)
148
def test_create(self):
150
b = self.make_branch('.')
151
if isinstance(b, remote.RemoteBranch):
152
# RemoteBranch creation:
153
if (self.transport_readonly_server
154
== test_server.ReadonlySmartTCPServer_for_testing_v2_only):
156
self.assertEqual(3, len(self.hook_calls))
157
# creates the branch via the VFS (for older servers)
158
self.assertEqual(b._real_branch, self.hook_calls[0])
159
# creates a RemoteBranch object
160
self.assertEqual(b, self.hook_calls[1])
161
# get_stacked_on_url RPC
162
self.assertRealBranch(self.hook_calls[2])
164
self.assertEqual(2, len(self.hook_calls))
166
self.assertRealBranch(self.hook_calls[0])
167
# create RemoteBranch locally
168
self.assertEqual(b, self.hook_calls[1])
170
self.assertEqual([b], self.hook_calls)
173
branch_url = self.make_branch('.').bzrdir.root_transport.base
175
b = _mod_branch.Branch.open(branch_url)
176
if isinstance(b, remote.RemoteBranch):
177
self.assertEqual(3, len(self.hook_calls))
179
self.assertRealBranch(self.hook_calls[0])
180
# create RemoteBranch locally
181
self.assertEqual(b, self.hook_calls[1])
182
# get_stacked_on_url RPC
183
self.assertRealBranch(self.hook_calls[2])
185
self.assertEqual([b], self.hook_calls)
187
def assertRealBranch(self, b):
188
# Branches opened on the server don't have comparable URLs, so we just
189
# assert that it is not a RemoteBranch.
190
self.assertIsInstance(b, _mod_branch.Branch)
191
self.assertFalse(isinstance(b, remote.RemoteBranch))
194
class TestPreChangeBranchTip(ChangeBranchTipTestCase):
195
"""Tests for pre_change_branch_tip hook.
197
Most of these tests are very similar to the tests in
198
TestPostChangeBranchTip.
201
def test_hook_runs_before_change(self):
202
"""The hook runs *before* the branch's last_revision_info has changed.
204
branch = self.make_branch_with_revision_ids('revid-one')
205
def assertBranchAtRevision1(params):
207
(1, 'revid-one'), params.branch.last_revision_info())
208
_mod_branch.Branch.hooks.install_named_hook(
209
'pre_change_branch_tip', assertBranchAtRevision1, None)
210
branch.set_last_revision_info(0, revision.NULL_REVISION)
212
def test_hook_failure_prevents_change(self):
213
"""If a hook raises an exception, the change does not take effect."""
214
branch = self.make_branch_with_revision_ids(
215
'one-\xc2\xb5', 'two-\xc2\xb5')
216
class PearShapedError(Exception):
218
def hook_that_raises(params):
219
raise PearShapedError()
220
_mod_branch.Branch.hooks.install_named_hook(
221
'pre_change_branch_tip', hook_that_raises, None)
222
hook_failed_exc = self.assertRaises(
224
branch.set_last_revision_info, 0, revision.NULL_REVISION)
225
# The revision info is unchanged.
226
self.assertEqual((2, 'two-\xc2\xb5'), branch.last_revision_info())
228
def test_empty_history(self):
229
branch = self.make_branch('source')
230
hook_calls = self.install_logging_hook('pre')
231
branch.set_last_revision_info(0, revision.NULL_REVISION)
232
expected_params = _mod_branch.ChangeBranchTipParams(
233
branch, 0, 0, revision.NULL_REVISION, revision.NULL_REVISION)
234
self.assertHookCalls(expected_params, branch, hook_calls, pre=True)
236
def test_nonempty_history(self):
237
# some branches require that their history be set to a revision in the
238
# repository, so we need to make a branch with non-empty history for
240
branch = self.make_branch_with_revision_ids(
241
'one-\xc2\xb5', 'two-\xc2\xb5')
242
hook_calls = self.install_logging_hook('pre')
243
branch.set_last_revision_info(1, 'one-\xc2\xb5')
244
expected_params = _mod_branch.ChangeBranchTipParams(
245
branch, 2, 1, 'two-\xc2\xb5', 'one-\xc2\xb5')
246
self.assertHookCalls(expected_params, branch, hook_calls, pre=True)
248
def test_branch_is_locked(self):
249
branch = self.make_branch('source')
250
def assertBranchIsLocked(params):
251
self.assertTrue(params.branch.is_locked())
252
_mod_branch.Branch.hooks.install_named_hook(
253
'pre_change_branch_tip', assertBranchIsLocked, None)
254
branch.set_last_revision_info(0, revision.NULL_REVISION)
256
def test_calls_all_hooks_no_errors(self):
257
"""If multiple hooks are registered, all are called (if none raise
260
branch = self.make_branch('source')
261
hook_calls_1 = self.install_logging_hook('pre')
262
hook_calls_2 = self.install_logging_hook('pre')
263
self.assertIsNot(hook_calls_1, hook_calls_2)
264
branch.set_last_revision_info(0, revision.NULL_REVISION)
265
# Both hooks are called.
266
if isinstance(branch, remote.RemoteBranch):
270
self.assertEqual(len(hook_calls_1), count)
271
self.assertEqual(len(hook_calls_2), count)
273
def test_explicit_reject_by_hook(self):
274
"""If a hook raises TipChangeRejected, the change does not take effect.
276
TipChangeRejected exceptions are propagated, not wrapped in HookFailed.
278
branch = self.make_branch_with_revision_ids(
279
'one-\xc2\xb5', 'two-\xc2\xb5')
280
def hook_that_rejects(params):
281
raise errors.TipChangeRejected('rejection message')
282
_mod_branch.Branch.hooks.install_named_hook(
283
'pre_change_branch_tip', hook_that_rejects, None)
285
errors.TipChangeRejected,
286
branch.set_last_revision_info, 0, revision.NULL_REVISION)
287
# The revision info is unchanged.
288
self.assertEqual((2, 'two-\xc2\xb5'), branch.last_revision_info())
291
class TestPostChangeBranchTip(ChangeBranchTipTestCase):
292
"""Tests for post_change_branch_tip hook.
294
Most of these tests are very similar to the tests in
295
TestPostChangeBranchTip.
298
def test_hook_runs_after_change(self):
299
"""The hook runs *after* the branch's last_revision_info has changed.
301
branch = self.make_branch_with_revision_ids('revid-one')
302
def assertBranchAtRevision1(params):
304
(0, revision.NULL_REVISION), params.branch.last_revision_info())
305
_mod_branch.Branch.hooks.install_named_hook(
306
'post_change_branch_tip', assertBranchAtRevision1, None)
307
branch.set_last_revision_info(0, revision.NULL_REVISION)
309
def test_empty_history(self):
310
branch = self.make_branch('source')
311
hook_calls = self.install_logging_hook('post')
312
branch.set_last_revision_info(0, revision.NULL_REVISION)
313
expected_params = _mod_branch.ChangeBranchTipParams(
314
branch, 0, 0, revision.NULL_REVISION, revision.NULL_REVISION)
315
self.assertHookCalls(expected_params, branch, hook_calls)
317
def test_nonempty_history(self):
318
# some branches require that their history be set to a revision in the
319
# repository, so we need to make a branch with non-empty history for
321
branch = self.make_branch_with_revision_ids(
322
'one-\xc2\xb5', 'two-\xc2\xb5')
323
hook_calls = self.install_logging_hook('post')
324
branch.set_last_revision_info(1, 'one-\xc2\xb5')
325
expected_params = _mod_branch.ChangeBranchTipParams(
326
branch, 2, 1, 'two-\xc2\xb5', 'one-\xc2\xb5')
327
self.assertHookCalls(expected_params, branch, hook_calls)
329
def test_branch_is_locked(self):
330
"""The branch passed to the hook is locked."""
331
branch = self.make_branch('source')
332
def assertBranchIsLocked(params):
333
self.assertTrue(params.branch.is_locked())
334
_mod_branch.Branch.hooks.install_named_hook(
335
'post_change_branch_tip', assertBranchIsLocked, None)
336
branch.set_last_revision_info(0, revision.NULL_REVISION)
338
def test_calls_all_hooks_no_errors(self):
339
"""If multiple hooks are registered, all are called (if none raise
342
branch = self.make_branch('source')
343
hook_calls_1 = self.install_logging_hook('post')
344
hook_calls_2 = self.install_logging_hook('post')
345
self.assertIsNot(hook_calls_1, hook_calls_2)
346
branch.set_last_revision_info(0, revision.NULL_REVISION)
347
# Both hooks are called.
348
if isinstance(branch, remote.RemoteBranch):
352
self.assertEqual(len(hook_calls_1), count)
353
self.assertEqual(len(hook_calls_2), count)
356
class TestAllMethodsThatChangeTipWillRunHooks(ChangeBranchTipTestCase):
357
"""Every method of Branch that changes a branch tip will invoke the
358
pre/post_change_branch_tip hooks.
362
ChangeBranchTipTestCase.setUp(self)
363
self.installPreAndPostHooks()
365
def installPreAndPostHooks(self):
366
self.pre_hook_calls = self.install_logging_hook('pre')
367
self.post_hook_calls = self.install_logging_hook('post')
369
def resetHookCalls(self):
370
del self.pre_hook_calls[:], self.post_hook_calls[:]
372
def assertPreAndPostHooksWereInvoked(self, branch, smart_enabled):
373
"""assert that both pre and post hooks were called
375
:param smart_enabled: The method invoked is one that should be
378
# Check for the number of invocations expected. One invocation is
379
# local, one is remote (if the branch is remote).
380
if smart_enabled and isinstance(branch, remote.RemoteBranch):
384
self.assertEqual(length, len(self.pre_hook_calls))
385
self.assertEqual(length, len(self.post_hook_calls))
387
def test_set_revision_history(self):
388
branch = self.make_branch('')
389
branch.set_revision_history([])
390
self.assertPreAndPostHooksWereInvoked(branch, True)
392
def test_set_last_revision_info(self):
393
branch = self.make_branch('')
394
branch.set_last_revision_info(0, revision.NULL_REVISION)
395
self.assertPreAndPostHooksWereInvoked(branch, True)
397
def test_generate_revision_history(self):
398
branch = self.make_branch('')
399
branch.generate_revision_history(revision.NULL_REVISION)
400
# NB: for HPSS protocols < v3, the server does not invoke branch tip
401
# change events on generate_revision_history, as the change is done
402
# directly by the client over the VFS.
403
self.assertPreAndPostHooksWereInvoked(branch, True)
406
source_branch = self.make_branch_with_revision_ids('rev-1', 'rev-2')
407
self.resetHookCalls()
408
destination_branch = self.make_branch('destination')
409
destination_branch.pull(source_branch)
410
self.assertPreAndPostHooksWereInvoked(destination_branch, False)
413
source_branch = self.make_branch_with_revision_ids('rev-1', 'rev-2')
414
self.resetHookCalls()
415
destination_branch = self.make_branch('destination')
416
source_branch.push(destination_branch)
417
self.assertPreAndPostHooksWereInvoked(destination_branch, True)