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 join between versioned files."""
20
import bzrlib.errors as errors
21
from bzrlib.tests import TestCaseWithTransport
22
from bzrlib.transport import get_transport
23
import bzrlib.versionedfile as versionedfile
26
class TestJoin(TestCaseWithTransport):
27
#Tests have self.versionedfile_factory and self.versionedfile_factory_to
28
#available to create source and target versioned files respectively.
30
def get_source(self, name='source'):
31
"""Get a versioned file we will be joining from."""
32
return self.versionedfile_factory(name,
33
get_transport(self.get_url()),
36
def get_target(self, name='target', create=True):
37
""""Get an empty versioned file to join into."""
38
return self.versionedfile_factory_to(name,
39
get_transport(self.get_url()),
43
f1 = self.get_source()
44
f1.add_lines('r0', [], ['a\n', 'b\n'])
45
f1.add_lines('r1', ['r0'], ['c\n', 'b\n'])
46
f2 = self.get_target()
49
self.assertTrue(f.has_version('r0'))
50
self.assertTrue(f.has_version('r1'))
52
verify_file(self.get_target())
54
self.assertRaises(errors.RevisionNotPresent,
55
f2.join, f1, version_ids=['r3'])
57
def test_gets_expected_inter_worker(self):
58
source = self.get_source()
59
target = self.get_target()
60
inter = versionedfile.InterVersionedFile.get(source, target)
61
self.assertTrue(isinstance(inter, self.interversionedfile_class))
63
def test_join_versions_joins_ancestors_not_siblings(self):
64
# joining with a version list should bring in ancestors of the
65
# named versions but not siblings thereof.
66
target = self.get_target()
67
target.add_lines('base', [], [])
68
source = self.get_source()
69
source.add_lines('base', [], [])
70
source.add_lines('sibling', ['base'], [])
71
source.add_lines('ancestorleft', ['base'], [])
72
source.add_lines('ancestorright', ['base'], [])
73
source.add_lines('namedleft', ['ancestorleft'], [])
74
source.add_lines('namedright', ['ancestorright'], [])
75
target.join(source, version_ids=['namedleft', 'namedright'])
76
self.assertFalse(target.has_version('sibling'))
77
self.assertTrue(target.has_version('ancestorleft'))
78
self.assertTrue(target.has_version('ancestorright'))
79
self.assertTrue(target.has_version('namedleft'))
80
self.assertTrue(target.has_version('namedright'))
82
def test_join_different_parents_existing_version(self):
83
"""This may either ignore or error."""
84
w1 = self.get_target('w1')
85
w2 = self.get_source('w2')
86
w1.add_lines('v-1', [], ['line 1\n'])
87
w2.add_lines('v-2', [], ['line 2\n'])
88
w1.add_lines('v-3', ['v-1'], ['line 1\n'])
89
w2.add_lines('v-3', ['v-2'], ['line 1\n'])
92
except errors.WeaveParentMismatch:
93
# Acceptable behaviour:
95
self.assertEqual(sorted(w1.versions()),
96
'v-1 v-2 v-3'.split())
97
self.assertEqualDiff(w1.get_text('v-3'),
99
self.assertEqual(sorted(w1.get_parents('v-3')),
101
ann = list(w1.annotate('v-3'))
102
self.assertEqual(len(ann), 1)
103
self.assertEqual(ann[0][0], 'v-1')
104
self.assertEqual(ann[0][1], 'line 1\n')
106
def build_weave1(self):
107
weave1 = self.get_source()
108
self.lines1 = ['hello\n']
109
self.lines3 = ['hello\n', 'cruel\n', 'world\n']
110
weave1.add_lines('v1', [], self.lines1)
111
weave1.add_lines('v2', ['v1'], ['hello\n', 'world\n'])
112
weave1.add_lines('v3', ['v2'], self.lines3)
115
def test_join_with_empty(self):
116
"""Reweave adding empty weave"""
117
wb = self.get_target()
118
w1 = self.build_weave1()
120
self.verify_weave1(w1)
122
def verify_weave1(self, w1):
123
self.assertEqual(sorted(w1.versions()), ['v1', 'v2', 'v3'])
124
self.assertEqual(w1.get_lines('v1'), ['hello\n'])
125
self.assertEqual([], w1.get_parents('v1'))
126
self.assertEqual(w1.get_lines('v2'), ['hello\n', 'world\n'])
127
self.assertEqual(['v1'], w1.get_parents('v2'))
128
self.assertEqual(w1.get_lines('v3'), ['hello\n', 'cruel\n', 'world\n'])
129
self.assertEqual(['v2'], w1.get_parents('v3'))
131
def test_join_source_has_less_parents_preserves_parents(self):
132
# when the target has a text with more parent info, join
134
s = self.get_source()
135
s.add_lines('base', [], [])
136
s.add_lines('text', [], [])
137
t = self.get_target()
138
t.add_lines('base', [], [])
139
t.add_lines('text', ['base'], [])
141
self.assertEqual(['base'], t.get_parents('text'))
143
def test_join_with_ghosts(self):
144
"""Join that inserts parents of an existing revision.
146
This can happen when merging from another branch who knows about
147
revisions the destination does not, and the destinations index is
148
incorrect because it was or is using a ghost-unaware format to
149
represent the index. In this test the second weave knows of an
150
additional parent of v2.
152
However v2 must not be changed because we consider indexes immutable:
153
instead a check or reconcile operation locally should pickup that v2 is
154
wrong and regenerate the index at a later time. So either this errors,
155
or leaves v2 unaltered.
157
w1 = self.build_weave1()
158
wb = self.get_target()
159
wb.add_lines('x1', [], ['line from x1\n'])
160
wb.add_lines('v1', [], ['hello\n'])
161
wb.add_lines('v2', ['v1', 'x1'], ['hello\n', 'world\n'])
164
except errors.WeaveParentMismatch:
165
# Acceptable behaviour:
167
self.assertEqual(['v1', 'v2', 'v3', 'x1',], sorted(w1.versions()))
168
self.assertEqual('line from x1\n', w1.get_text('x1'))
169
self.assertEqual(['hello\n', 'world\n'], w1.get_lines('v2'))
170
self.assertEqual(['v1'], w1.get_parents('v2'))
172
def test_join_with_ignore_missing_versions(self):
173
# test that ignore_missing=True makes a listed but absent version id
174
# be ignored, and that unlisted version_ids are not integrated.
175
w1 = self.build_weave1()
176
wb = self.get_target()
177
wb.add_lines('x1', [], ['line from x1\n'])
178
wb.add_lines('v1', [], ['hello\n'])
179
wb.add_lines('v2', ['v1', 'x1'], ['hello\n', 'world\n'])
180
w1.join(wb, version_ids=['x1', 'z1'], ignore_missing=True)
181
eq = self.assertEquals
182
eq(sorted(w1.versions()), ['v1', 'v2', 'v3', 'x1'])
183
eq(w1.get_text('x1'), 'line from x1\n')
184
eq(w1.get_lines('v2'), ['hello\n', 'world\n'])
185
eq(w1.get_parents('v2'), ['v1'])
187
def build_source_weave(self, name, *pattern):
188
w = self.get_source(name)
189
for version, parents in pattern:
190
w.add_lines(version, parents, [])
193
def build_target_weave(self, name, *pattern):
194
w = self.get_target(name)
195
for version, parents in pattern:
196
w.add_lines(version, parents, [])
199
def test_joining_ghosts(self):
200
# some versioned file formats allow lines to be added with parent
201
# information that is > than that in the format. Formats that do
202
# not support this need to raise NotImplementedError on the
203
# add_lines_with_ghosts api.
204
# files with ghost information when joined into a file which
205
# supports that must preserve it, when joined into a file which
206
# does not must discard it, and when filling a ghost for a listed
207
# ghost must reconcile it
208
source = self.get_source()
210
source.has_ghost('a')
212
except NotImplementedError:
213
source_ghosts = False
214
target = self.get_target()
216
target.has_ghost('a')
218
except NotImplementedError:
219
target_ghosts = False
221
if not source_ghosts and not target_ghosts:
224
if source_ghosts and not target_ghosts:
225
# switch source and target so source is ghostless
229
source_ghosts = False
231
# now target always supports ghosts.
233
# try filling target with ghosts and filling in reverse -
234
target.add_lines_with_ghosts('notbase', ['base'], [])
236
# legacy apis should behave
237
self.assertEqual(['notbase'], source.get_ancestry(['notbase']))
238
self.assertEqual([], source.get_parents('notbase'))
239
self.assertEqual({'notbase':()}, source.get_graph())
240
self.assertFalse(source.has_version('base'))
242
# ghost data should have been preserved
243
self.assertEqual(['base', 'notbase'], source.get_ancestry_with_ghosts(['notbase']))
244
self.assertEqual(['base'], source.get_parents_with_ghosts('notbase'))
245
self.assertEqual({'notbase':['base']}, source.get_graph_with_ghosts())
246
self.assertTrue(source.has_ghost('base'))
248
# if we add something that is fills out what is a ghost, then
249
# when joining into a ghost aware join it should flesh out the ghosts.
250
source.add_lines('base', [], [])
251
target.join(source, version_ids=['base'])
252
self.assertEqual(['base', 'notbase'], target.get_ancestry(['notbase']))
253
self.assertEqual(['base'], target.get_parents('notbase'))
254
self.assertEqual({'base':(),
255
'notbase':('base', ),
258
self.assertTrue(target.has_version('base'))
259
# we have _with_ghost apis to give us ghost information.
260
self.assertEqual(['base', 'notbase'], target.get_ancestry_with_ghosts(['notbase']))
261
self.assertEqual(['base'], target.get_parents_with_ghosts('notbase'))
262
self.assertEqual({'base':[],
265
target.get_graph_with_ghosts())
266
self.assertFalse(target.has_ghost('base'))
268
def test_restricted_join_into_empty(self):
269
# joining into an empty versioned file with a version_ids list
270
# should only grab the selected versions.
271
source = self.get_source()
272
source.add_lines('skip_me', [], ['a\n'])
273
source.add_lines('inherit_me', [], ['b\n'])
274
source.add_lines('select_me', ['inherit_me'], ['b\n'])
275
target = self.get_target()
276
target.join(source, version_ids=['select_me'])
277
self.assertEqual(['inherit_me', 'select_me'], target.versions())
279
def test_join_odd_records(self):
280
# this tests that joining the 1st, 3rd and 5th records and not the
281
# 2nd and 4th works, particularly after reopening the file.
282
# this test is designed to test versioned files that perform
283
# optimisations on the join. Grabbing partial data and reopening the
284
# file make it likely to trigger a fault.
285
source = self.get_source()
286
source.add_lines('1', [], ['1st\n'])
287
source.add_lines('2', [], ['2nd\n'])
288
source.add_lines('3', ['1'], ['1st\n', '2nd\n'])
289
source.add_lines('4', ['2'], ['1st\n'])
290
source.add_lines('5', ['3'], ['1st\n', '2nd\n', '3rd\n'])
291
target = self.get_target()
292
target.join(source, version_ids=['1', '3', '5'])
293
target = self.get_target(create=False)
294
self.assertEqual(set(['1', '3', '5']), set(target.versions()))
295
self.assertEqual(3, len(target.versions()))
296
self.assertEqual(['1st\n'], target.get_lines('1'))
297
self.assertEqual(['1st\n', '2nd\n'], target.get_lines('3'))
298
self.assertEqual(['1st\n', '2nd\n', '3rd\n'], target.get_lines('5'))