13
13
# You should have received a copy of the GNU General Public License
14
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
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
17
"""Tests that branch classes implement hook callouts correctly."""
20
20
from bzrlib.errors import HookFailed, TipChangeRejected
21
21
from bzrlib.remote import RemoteBranch
22
22
from bzrlib.revision import NULL_REVISION
23
from bzrlib.smart import server
23
24
from bzrlib.tests import TestCaseWithMemoryTransport
26
class TestSetRevisionHistoryHook(TestCaseWithMemoryTransport):
27
class ChangeBranchTipTestCase(TestCaseWithMemoryTransport):
28
"""Base TestCase for testing pre/post_change_branch_tip hooks."""
30
def install_logging_hook(self, prefix):
31
"""Add a hook that logs calls made to it.
33
:returns: the list that the calls will be appended to.
36
Branch.hooks.install_named_hook(
37
prefix + '_change_branch_tip', hook_calls.append, None)
40
def make_branch_with_revision_ids(self, *revision_ids):
41
"""Makes a branch with the given commits."""
42
tree = self.make_branch_and_memory_tree('source')
45
for revision_id in revision_ids:
46
tree.commit(u'Message of ' + revision_id.decode('utf8'),
52
def assertHookCalls(self, expected_params, branch, hook_calls=None,
54
if hook_calls is None:
55
hook_calls = self.hook_calls
56
if isinstance(branch, RemoteBranch):
57
# For a remote branch, both the server and the client will raise
58
# this hook, and we see both in the test environment. The remote
59
# instance comes in between the clients - the client doe pre, the
60
# server does pre, the server does post, the client does post.
65
self.assertEqual(expected_params, hook_calls[offset])
66
self.assertEqual(2, len(hook_calls))
68
self.assertEqual([expected_params], hook_calls)
71
class TestSetRevisionHistoryHook(ChangeBranchTipTestCase):
29
74
self.hook_calls = []
42
87
Branch.hooks.install_named_hook('set_rh', self.capture_set_rh_hook,
44
89
branch.set_revision_history([])
45
self.assertEqual(self.hook_calls,
46
[('set_rh', branch, [], True)])
90
expected_params = ('set_rh', branch, [], True)
91
self.assertHookCalls(expected_params, branch)
48
93
def test_set_rh_nonempty_history(self):
49
94
tree = self.make_branch_and_memory_tree('source')
58
103
# some branches require that their history be set to a revision in the
60
105
branch.set_revision_history(['f\xc2\xb5'])
61
self.assertEqual(self.hook_calls,
62
[('set_rh', branch, ['f\xc2\xb5'], True)])
106
expected_params =('set_rh', branch, ['f\xc2\xb5'], True)
107
self.assertHookCalls(expected_params, branch)
64
109
def test_set_rh_branch_is_locked(self):
65
110
branch = self.make_branch('source')
66
111
Branch.hooks.install_named_hook('set_rh', self.capture_set_rh_hook,
68
113
branch.set_revision_history([])
69
self.assertEqual(self.hook_calls,
70
[('set_rh', branch, [], True)])
114
expected_params = ('set_rh', branch, [], True)
115
self.assertHookCalls(expected_params, branch)
72
117
def test_set_rh_calls_all_hooks_no_errors(self):
73
118
branch = self.make_branch('source')
76
121
Branch.hooks.install_named_hook('set_rh', self.capture_set_rh_hook,
78
123
branch.set_revision_history([])
79
self.assertEqual(self.hook_calls,
80
[('set_rh', branch, [], True),
81
('set_rh', branch, [], True),
85
class ChangeBranchTipTestCase(TestCaseWithMemoryTransport):
86
"""Base TestCase for testing pre/post_change_branch_tip hooks."""
88
def install_logging_hook(self, prefix):
89
"""Add a hook that logs calls made to it.
91
:returns: the list that the calls will be appended to.
94
Branch.hooks.install_named_hook(
95
prefix + '_change_branch_tip', hook_calls.append, None)
98
def make_branch_with_revision_ids(self, *revision_ids):
99
"""Makes a branch with the given commits."""
100
tree = self.make_branch_and_memory_tree('source')
103
for revision_id in revision_ids:
104
tree.commit(u'Message of ' + revision_id.decode('utf8'),
124
expected_calls = [('set_rh', branch, [], True),
125
('set_rh', branch, [], True),
127
if isinstance(branch, RemoteBranch):
128
# For a remote branch, both the server and the client will raise
129
# set_rh, and the server will do so first because that is where
130
# the change takes place.
131
self.assertEqual(expected_calls, self.hook_calls[2:])
132
self.assertEqual(4, len(self.hook_calls))
134
self.assertEqual(expected_calls, self.hook_calls)
111
137
class TestOpen(TestCaseWithMemoryTransport):
120
146
def test_create(self):
121
147
self.install_hook()
122
148
b = self.make_branch('.')
123
self.assertEqual([b], self.hook_calls)
149
if isinstance(b, RemoteBranch):
150
# RemoteBranch creation:
151
if (self.transport_readonly_server ==
152
server.ReadonlySmartTCPServer_for_testing_v2_only):
154
self.assertEqual(3, len(self.hook_calls))
155
# creates the branch via the VFS (for older servers)
156
self.assertEqual(b._real_branch, self.hook_calls[0])
157
# creates a RemoteBranch object
158
self.assertEqual(b, self.hook_calls[1])
159
# get_stacked_on_url RPC
160
self.assertRealBranch(self.hook_calls[2])
162
self.assertEqual(2, len(self.hook_calls))
164
self.assertRealBranch(self.hook_calls[0])
165
# create RemoteBranch locally
166
self.assertEqual(b, self.hook_calls[1])
168
self.assertEqual([b], self.hook_calls)
125
170
def test_open(self):
126
171
branch_url = self.make_branch('.').bzrdir.root_transport.base
127
172
self.install_hook()
128
173
b = Branch.open(branch_url)
129
174
if isinstance(b, RemoteBranch):
130
# RemoteBranch open always opens the backing branch to get stacking
131
# details. As that is done remotely we can't see the branch object
132
# nor even compare base url's etc. So we just assert that the first
133
# branch returned is the RemoteBranch, and that the second is a
134
# Branch but not a RemoteBranch.
135
self.assertEqual(2, len(self.hook_calls))
136
self.assertEqual(b, self.hook_calls[0])
137
self.assertIsInstance(self.hook_calls[1], Branch)
138
self.assertFalse(isinstance(self.hook_calls[1], RemoteBranch))
175
self.assertEqual(3, len(self.hook_calls))
177
self.assertRealBranch(self.hook_calls[0])
178
# create RemoteBranch locally
179
self.assertEqual(b, self.hook_calls[1])
180
# get_stacked_on_url RPC
181
self.assertRealBranch(self.hook_calls[2])
140
183
self.assertEqual([b], self.hook_calls)
185
def assertRealBranch(self, b):
186
# Branches opened on the server don't have comparable URLs, so we just
187
# assert that it is not a RemoteBranch.
188
self.assertIsInstance(b, Branch)
189
self.assertFalse(isinstance(b, RemoteBranch))
143
192
class TestPreChangeBranchTip(ChangeBranchTipTestCase):
144
193
"""Tests for pre_change_branch_tip hook.
146
195
Most of these tests are very similar to the tests in
147
196
TestPostChangeBranchTip.
159
208
branch.set_last_revision_info(0, NULL_REVISION)
161
210
def test_hook_failure_prevents_change(self):
162
"""If a hook raises an exception, the change does not take effect.
164
Also, a HookFailed exception will be raised.
211
"""If a hook raises an exception, the change does not take effect."""
166
212
branch = self.make_branch_with_revision_ids(
167
213
'one-\xc2\xb5', 'two-\xc2\xb5')
168
214
class PearShapedError(Exception):
172
218
Branch.hooks.install_named_hook(
173
219
'pre_change_branch_tip', hook_that_raises, None)
174
220
hook_failed_exc = self.assertRaises(
175
HookFailed, branch.set_last_revision_info, 0, NULL_REVISION)
176
self.assertIsInstance(hook_failed_exc.exc_value, PearShapedError)
221
PearShapedError, branch.set_last_revision_info, 0, NULL_REVISION)
177
222
# The revision info is unchanged.
178
223
self.assertEqual((2, 'two-\xc2\xb5'), branch.last_revision_info())
180
225
def test_empty_history(self):
181
226
branch = self.make_branch('source')
182
227
hook_calls = self.install_logging_hook('pre')
183
228
branch.set_last_revision_info(0, NULL_REVISION)
184
229
expected_params = ChangeBranchTipParams(
185
230
branch, 0, 0, NULL_REVISION, NULL_REVISION)
186
self.assertEqual([expected_params], hook_calls)
231
self.assertHookCalls(expected_params, branch, hook_calls, pre=True)
188
233
def test_nonempty_history(self):
189
234
# some branches require that their history be set to a revision in the
195
240
branch.set_last_revision_info(1, 'one-\xc2\xb5')
196
241
expected_params = ChangeBranchTipParams(
197
242
branch, 2, 1, 'two-\xc2\xb5', 'one-\xc2\xb5')
198
self.assertEqual([expected_params], hook_calls)
243
self.assertHookCalls(expected_params, branch, hook_calls, pre=True)
200
245
def test_branch_is_locked(self):
201
246
branch = self.make_branch('source')
215
260
self.assertIsNot(hook_calls_1, hook_calls_2)
216
261
branch.set_last_revision_info(0, NULL_REVISION)
217
262
# Both hooks are called.
218
self.assertEqual(len(hook_calls_1), 1)
219
self.assertEqual(len(hook_calls_2), 1)
263
if isinstance(branch, RemoteBranch):
267
self.assertEqual(len(hook_calls_1), count)
268
self.assertEqual(len(hook_calls_2), count)
221
270
def test_explicit_reject_by_hook(self):
222
271
"""If a hook raises TipChangeRejected, the change does not take effect.
224
273
TipChangeRejected exceptions are propagated, not wrapped in HookFailed.
226
275
branch = self.make_branch_with_revision_ids(
233
282
TipChangeRejected, branch.set_last_revision_info, 0, NULL_REVISION)
234
283
# The revision info is unchanged.
235
284
self.assertEqual((2, 'two-\xc2\xb5'), branch.last_revision_info())
238
287
class TestPostChangeBranchTip(ChangeBranchTipTestCase):
239
288
"""Tests for post_change_branch_tip hook.
259
308
branch.set_last_revision_info(0, NULL_REVISION)
260
309
expected_params = ChangeBranchTipParams(
261
310
branch, 0, 0, NULL_REVISION, NULL_REVISION)
262
self.assertEqual([expected_params], hook_calls)
311
self.assertHookCalls(expected_params, branch, hook_calls)
264
313
def test_nonempty_history(self):
265
314
# some branches require that their history be set to a revision in the
271
320
branch.set_last_revision_info(1, 'one-\xc2\xb5')
272
321
expected_params = ChangeBranchTipParams(
273
322
branch, 2, 1, 'two-\xc2\xb5', 'one-\xc2\xb5')
274
self.assertEqual([expected_params], hook_calls)
323
self.assertHookCalls(expected_params, branch, hook_calls)
276
325
def test_branch_is_locked(self):
277
326
"""The branch passed to the hook is locked."""
292
341
self.assertIsNot(hook_calls_1, hook_calls_2)
293
342
branch.set_last_revision_info(0, NULL_REVISION)
294
343
# Both hooks are called.
295
self.assertEqual(len(hook_calls_1), 1)
296
self.assertEqual(len(hook_calls_2), 1)
344
if isinstance(branch, RemoteBranch):
348
self.assertEqual(len(hook_calls_1), count)
349
self.assertEqual(len(hook_calls_2), count)
299
352
class TestAllMethodsThatChangeTipWillRunHooks(ChangeBranchTipTestCase):
312
365
def resetHookCalls(self):
313
366
del self.pre_hook_calls[:], self.post_hook_calls[:]
315
def assertPreAndPostHooksWereInvoked(self):
316
# Check for len == 1, because the hooks should only be be invoked once
318
self.assertEqual(1, len(self.pre_hook_calls))
319
self.assertEqual(1, len(self.post_hook_calls))
368
def assertPreAndPostHooksWereInvoked(self, branch, smart_enabled):
369
"""assert that both pre and post hooks were called
371
:param smart_enabled: The method invoked is one that should be
374
# Check for the number of invocations expected. One invocation is
375
# local, one is remote (if the branch is remote).
376
if smart_enabled and isinstance(branch, RemoteBranch):
380
self.assertEqual(length, len(self.pre_hook_calls))
381
self.assertEqual(length, len(self.post_hook_calls))
321
383
def test_set_revision_history(self):
322
384
branch = self.make_branch('')
323
385
branch.set_revision_history([])
324
self.assertPreAndPostHooksWereInvoked()
386
self.assertPreAndPostHooksWereInvoked(branch, True)
326
388
def test_set_last_revision_info(self):
327
389
branch = self.make_branch('')
328
390
branch.set_last_revision_info(0, NULL_REVISION)
329
self.assertPreAndPostHooksWereInvoked()
391
self.assertPreAndPostHooksWereInvoked(branch, True)
331
393
def test_generate_revision_history(self):
332
394
branch = self.make_branch('')
333
395
branch.generate_revision_history(NULL_REVISION)
334
self.assertPreAndPostHooksWereInvoked()
396
# NB: for HPSS protocols < v3, the server does not invoke branch tip
397
# change events on generate_revision_history, as the change is done
398
# directly by the client over the VFS.
399
self.assertPreAndPostHooksWereInvoked(branch, True)
336
401
def test_pull(self):
337
402
source_branch = self.make_branch_with_revision_ids('rev-1', 'rev-2')
338
403
self.resetHookCalls()
339
404
destination_branch = self.make_branch('destination')
340
405
destination_branch.pull(source_branch)
341
self.assertPreAndPostHooksWereInvoked()
406
self.assertPreAndPostHooksWereInvoked(destination_branch, False)
343
408
def test_push(self):
344
409
source_branch = self.make_branch_with_revision_ids('rev-1', 'rev-2')
345
410
self.resetHookCalls()
346
411
destination_branch = self.make_branch('destination')
347
412
source_branch.push(destination_branch)
348
self.assertPreAndPostHooksWereInvoked()
413
self.assertPreAndPostHooksWereInvoked(destination_branch, True)