1
# Copyright (C) 2007 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 that branch classes implement hook callouts correctly."""
19
from bzrlib.errors import HookFailed, TipChangeRejected
20
from bzrlib.branch import Branch, ChangeBranchTipParams
21
from bzrlib.revision import NULL_REVISION
22
from bzrlib.tests import TestCaseWithMemoryTransport
25
class TestSetRevisionHistoryHook(TestCaseWithMemoryTransport):
29
TestCaseWithMemoryTransport.setUp(self)
31
def capture_set_rh_hook(self, branch, rev_history):
32
"""Capture post set-rh hook calls to self.hook_calls.
34
The call is logged, as is some state of the branch.
36
self.hook_calls.append(
37
('set_rh', branch, rev_history, branch.is_locked()))
39
def test_set_rh_empty_history(self):
40
branch = self.make_branch('source')
41
Branch.hooks.install_named_hook('set_rh', self.capture_set_rh_hook,
43
branch.set_revision_history([])
44
self.assertEqual(self.hook_calls,
45
[('set_rh', branch, [], True)])
47
def test_set_rh_nonempty_history(self):
48
tree = self.make_branch_and_memory_tree('source')
51
tree.commit('another commit', rev_id='f\xc2\xb5')
52
tree.commit('empty commit', rev_id='foo')
55
Branch.hooks.install_named_hook('set_rh', self.capture_set_rh_hook,
57
# some branches require that their history be set to a revision in the
59
branch.set_revision_history(['f\xc2\xb5'])
60
self.assertEqual(self.hook_calls,
61
[('set_rh', branch, ['f\xc2\xb5'], True)])
63
def test_set_rh_branch_is_locked(self):
64
branch = self.make_branch('source')
65
Branch.hooks.install_named_hook('set_rh', self.capture_set_rh_hook,
67
branch.set_revision_history([])
68
self.assertEqual(self.hook_calls,
69
[('set_rh', branch, [], True)])
71
def test_set_rh_calls_all_hooks_no_errors(self):
72
branch = self.make_branch('source')
73
Branch.hooks.install_named_hook('set_rh', self.capture_set_rh_hook,
75
Branch.hooks.install_named_hook('set_rh', self.capture_set_rh_hook,
77
branch.set_revision_history([])
78
self.assertEqual(self.hook_calls,
79
[('set_rh', branch, [], True),
80
('set_rh', branch, [], True),
84
class ChangeBranchTipTestCase(TestCaseWithMemoryTransport):
85
"""Base TestCase for testing pre/post_change_branch_tip hooks."""
87
def install_logging_hook(self, prefix):
88
"""Add a hook that logs calls made to it.
90
:returns: the list that the calls will be appended to.
93
Branch.hooks.install_named_hook(
94
prefix + '_change_branch_tip', hook_calls.append, None)
97
def make_branch_with_revision_ids(self, *revision_ids):
98
"""Makes a branch with the given commits."""
99
tree = self.make_branch_and_memory_tree('source')
102
for revision_id in revision_ids:
103
tree.commit(u'Message of ' + revision_id.decode('utf8'),
110
class TestPreChangeBranchTip(ChangeBranchTipTestCase):
111
"""Tests for pre_change_branch_tip hook.
113
Most of these tests are very similar to the tests in
114
TestPostChangeBranchTip.
117
def test_hook_runs_before_change(self):
118
"""The hook runs *before* the branch's last_revision_info has changed.
120
branch = self.make_branch_with_revision_ids('revid-one')
121
def assertBranchAtRevision1(params):
123
(1, 'revid-one'), params.branch.last_revision_info())
124
Branch.hooks.install_named_hook(
125
'pre_change_branch_tip', assertBranchAtRevision1, None)
126
branch.set_last_revision_info(0, NULL_REVISION)
128
def test_hook_failure_prevents_change(self):
129
"""If a hook raises an exception, the change does not take effect.
131
Also, a HookFailed exception will be raised.
133
branch = self.make_branch_with_revision_ids(
134
'one-\xc2\xb5', 'two-\xc2\xb5')
135
class PearShapedError(Exception):
137
def hook_that_raises(params):
138
raise PearShapedError()
139
Branch.hooks.install_named_hook(
140
'pre_change_branch_tip', hook_that_raises, None)
141
hook_failed_exc = self.assertRaises(
142
HookFailed, branch.set_last_revision_info, 0, NULL_REVISION)
143
self.assertIsInstance(hook_failed_exc.exc_value, PearShapedError)
144
# The revision info is unchanged.
145
self.assertEqual((2, 'two-\xc2\xb5'), branch.last_revision_info())
147
def test_empty_history(self):
148
branch = self.make_branch('source')
149
hook_calls = self.install_logging_hook('pre')
150
branch.set_last_revision_info(0, NULL_REVISION)
151
expected_params = ChangeBranchTipParams(
152
branch, 0, 0, NULL_REVISION, NULL_REVISION)
153
self.assertEqual([expected_params], hook_calls)
155
def test_nonempty_history(self):
156
# some branches require that their history be set to a revision in the
157
# repository, so we need to make a branch with non-empty history for
159
branch = self.make_branch_with_revision_ids(
160
'one-\xc2\xb5', 'two-\xc2\xb5')
161
hook_calls = self.install_logging_hook('pre')
162
branch.set_last_revision_info(1, 'one-\xc2\xb5')
163
expected_params = ChangeBranchTipParams(
164
branch, 2, 1, 'two-\xc2\xb5', 'one-\xc2\xb5')
165
self.assertEqual([expected_params], hook_calls)
167
def test_branch_is_locked(self):
168
branch = self.make_branch('source')
169
def assertBranchIsLocked(params):
170
self.assertTrue(params.branch.is_locked())
171
Branch.hooks.install_named_hook(
172
'pre_change_branch_tip', assertBranchIsLocked, None)
173
branch.set_last_revision_info(0, NULL_REVISION)
175
def test_calls_all_hooks_no_errors(self):
176
"""If multiple hooks are registered, all are called (if none raise
179
branch = self.make_branch('source')
180
hook_calls_1 = self.install_logging_hook('pre')
181
hook_calls_2 = self.install_logging_hook('pre')
182
self.assertIsNot(hook_calls_1, hook_calls_2)
183
branch.set_last_revision_info(0, NULL_REVISION)
184
# Both hooks are called.
185
self.assertEqual(len(hook_calls_1), 1)
186
self.assertEqual(len(hook_calls_2), 1)
188
def test_explicit_reject_by_hook(self):
189
"""If a hook raises TipChangeRejected, the change does not take effect.
191
TipChangeRejected exceptions are propagated, not wrapped in HookFailed.
193
branch = self.make_branch_with_revision_ids(
194
'one-\xc2\xb5', 'two-\xc2\xb5')
195
def hook_that_rejects(params):
196
raise TipChangeRejected('rejection message')
197
Branch.hooks.install_named_hook(
198
'pre_change_branch_tip', hook_that_rejects, None)
200
TipChangeRejected, branch.set_last_revision_info, 0, NULL_REVISION)
201
# The revision info is unchanged.
202
self.assertEqual((2, 'two-\xc2\xb5'), branch.last_revision_info())
205
class TestPostChangeBranchTip(ChangeBranchTipTestCase):
206
"""Tests for post_change_branch_tip hook.
208
Most of these tests are very similar to the tests in
209
TestPostChangeBranchTip.
212
def test_hook_runs_after_change(self):
213
"""The hook runs *after* the branch's last_revision_info has changed.
215
branch = self.make_branch_with_revision_ids('revid-one')
216
def assertBranchAtRevision1(params):
218
(0, NULL_REVISION), params.branch.last_revision_info())
219
Branch.hooks.install_named_hook(
220
'post_change_branch_tip', assertBranchAtRevision1, None)
221
branch.set_last_revision_info(0, NULL_REVISION)
223
def test_empty_history(self):
224
branch = self.make_branch('source')
225
hook_calls = self.install_logging_hook('post')
226
branch.set_last_revision_info(0, NULL_REVISION)
227
expected_params = ChangeBranchTipParams(
228
branch, 0, 0, NULL_REVISION, NULL_REVISION)
229
self.assertEqual([expected_params], hook_calls)
231
def test_nonempty_history(self):
232
# some branches require that their history be set to a revision in the
233
# repository, so we need to make a branch with non-empty history for
235
branch = self.make_branch_with_revision_ids(
236
'one-\xc2\xb5', 'two-\xc2\xb5')
237
hook_calls = self.install_logging_hook('post')
238
branch.set_last_revision_info(1, 'one-\xc2\xb5')
239
expected_params = ChangeBranchTipParams(
240
branch, 2, 1, 'two-\xc2\xb5', 'one-\xc2\xb5')
241
self.assertEqual([expected_params], hook_calls)
243
def test_branch_is_locked(self):
244
"""The branch passed to the hook is locked."""
245
branch = self.make_branch('source')
246
def assertBranchIsLocked(params):
247
self.assertTrue(params.branch.is_locked())
248
Branch.hooks.install_named_hook(
249
'post_change_branch_tip', assertBranchIsLocked, None)
250
branch.set_last_revision_info(0, NULL_REVISION)
252
def test_calls_all_hooks_no_errors(self):
253
"""If multiple hooks are registered, all are called (if none raise
256
branch = self.make_branch('source')
257
hook_calls_1 = self.install_logging_hook('post')
258
hook_calls_2 = self.install_logging_hook('post')
259
self.assertIsNot(hook_calls_1, hook_calls_2)
260
branch.set_last_revision_info(0, NULL_REVISION)
261
# Both hooks are called.
262
self.assertEqual(len(hook_calls_1), 1)
263
self.assertEqual(len(hook_calls_2), 1)
266
class TestAllMethodsThatChangeTipWillRunHooks(ChangeBranchTipTestCase):
267
"""Every method of Branch that changes a branch tip will invoke the
268
pre/post_change_branch_tip hooks.
272
ChangeBranchTipTestCase.setUp(self)
273
self.installPreAndPostHooks()
275
def installPreAndPostHooks(self):
276
self.pre_hook_calls = self.install_logging_hook('pre')
277
self.post_hook_calls = self.install_logging_hook('post')
279
def resetHookCalls(self):
280
del self.pre_hook_calls[:], self.post_hook_calls[:]
282
def assertPreAndPostHooksWereInvoked(self):
283
# Check for len == 1, because the hooks should only be be invoked once
285
self.assertEqual(1, len(self.pre_hook_calls))
286
self.assertEqual(1, len(self.post_hook_calls))
288
def test_set_revision_history(self):
289
branch = self.make_branch('')
290
branch.set_revision_history([])
291
self.assertPreAndPostHooksWereInvoked()
293
def test_set_last_revision_info(self):
294
branch = self.make_branch('')
295
branch.set_last_revision_info(0, NULL_REVISION)
296
self.assertPreAndPostHooksWereInvoked()
298
def test_generate_revision_history(self):
299
branch = self.make_branch('')
300
branch.generate_revision_history(NULL_REVISION)
301
self.assertPreAndPostHooksWereInvoked()
304
source_branch = self.make_branch_with_revision_ids('rev-1', 'rev-2')
305
self.resetHookCalls()
306
destination_branch = self.make_branch('destination')
307
destination_branch.pull(source_branch)
308
self.assertPreAndPostHooksWereInvoked()
311
source_branch = self.make_branch_with_revision_ids('rev-1', 'rev-2')
312
self.resetHookCalls()
313
destination_branch = self.make_branch('destination')
314
source_branch.push(destination_branch)
315
self.assertPreAndPostHooksWereInvoked()