1
# Copyright (C) 2006 by 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'):
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
#f3 = self.get_file('1')
58
#f3.add_lines('r0', ['a\n', 'b\n'], [])
59
#f3.add_lines('r1', ['c\n', 'b\n'], ['r0'])
60
#f4 = self.get_file('2')
62
#self.assertTrue(f4.has_version('r0'))
63
#self.assertFalse(f4.has_version('r1'))
65
def test_gets_expected_inter_worker(self):
66
source = self.get_source()
67
target = self.get_target()
68
inter = versionedfile.InterVersionedFile.get(source, target)
69
self.assertTrue(isinstance(inter, self.interversionedfile_class))
71
def test_join_versions_joins_ancestors_not_siblings(self):
72
# joining with a version list should bring in ancestors of the
73
# named versions but not siblings thereof.
74
target = self.get_target()
75
target.add_lines('base', [], [])
76
source = self.get_source()
77
source.add_lines('base', [], [])
78
source.add_lines('sibling', ['base'], [])
79
source.add_lines('ancestorleft', ['base'], [])
80
source.add_lines('ancestorright', ['base'], [])
81
source.add_lines('namedleft', ['ancestorleft'], [])
82
source.add_lines('namedright', ['ancestorright'], [])
83
target.join(source, version_ids=['namedleft', 'namedright'])
84
self.assertFalse(target.has_version('sibling'))
85
self.assertTrue(target.has_version('ancestorleft'))
86
self.assertTrue(target.has_version('ancestorright'))
87
self.assertTrue(target.has_version('namedleft'))
88
self.assertTrue(target.has_version('namedright'))
90
def test_join_add_parents(self):
91
"""Join inserting new parents into existing versions
93
The new version must have the right parent list and must identify
94
lines originating in another parent.
96
w1 = self.get_target('w1')
97
w2 = self.get_source('w2')
98
w1.add_lines('v-1', [], ['line 1\n'])
99
w2.add_lines('v-2', [], ['line 2\n'])
100
w1.add_lines('v-3', ['v-1'], ['line 1\n'])
101
w2.add_lines('v-3', ['v-2'], ['line 1\n'])
103
self.assertEqual(sorted(w1.versions()),
104
'v-1 v-2 v-3'.split())
105
self.assertEqualDiff(w1.get_text('v-3'),
107
self.assertEqual(sorted(w1.get_parents('v-3')),
109
ann = list(w1.annotate('v-3'))
110
self.assertEqual(len(ann), 1)
111
self.assertEqual(ann[0][0], 'v-1')
112
self.assertEqual(ann[0][1], 'line 1\n')
114
def build_weave1(self):
115
weave1 = self.get_source()
116
self.lines1 = ['hello\n']
117
self.lines3 = ['hello\n', 'cruel\n', 'world\n']
118
weave1.add_lines('v1', [], self.lines1)
119
weave1.add_lines('v2', ['v1'], ['hello\n', 'world\n'])
120
weave1.add_lines('v3', ['v2'], self.lines3)
123
def test_join_with_empty(self):
124
"""Reweave adding empty weave"""
125
wb = self.get_target()
126
w1 = self.build_weave1()
128
self.verify_weave1(w1)
130
def verify_weave1(self, w1):
131
self.assertEqual(sorted(w1.versions()), ['v1', 'v2', 'v3'])
132
self.assertEqual(w1.get_lines('v1'), ['hello\n'])
133
self.assertEqual([], w1.get_parents('v1'))
134
self.assertEqual(w1.get_lines('v2'), ['hello\n', 'world\n'])
135
self.assertEqual(['v1'], w1.get_parents('v2'))
136
self.assertEqual(w1.get_lines('v3'), ['hello\n', 'cruel\n', 'world\n'])
137
self.assertEqual(['v2'], w1.get_parents('v3'))
139
def test_join_source_has_less_parents_preserves_parents(self):
140
# when the target has a text with more parent info, join
142
s = self.get_source()
143
s.add_lines('base', [], [])
144
s.add_lines('text', [], [])
145
t = self.get_target()
146
t.add_lines('base', [], [])
147
t.add_lines('text', ['base'], [])
149
self.assertEqual(['base'], t.get_parents('text'))
151
def test_join_with_ghosts_merges_parents(self):
152
"""Join combined parent lists"""
153
wa = self.build_weave1()
154
wb = self.get_target()
155
wb.add_lines('x1', [], ['line from x1\n'])
156
wb.add_lines('v1', [], ['hello\n'])
157
wb.add_lines('v2', ['v1', 'x1'], ['hello\n', 'world\n'])
159
self.assertEqual(['v1','x1'], wa.get_parents('v2'))
161
def test_join_with_ghosts(self):
162
"""Join that inserts parents of an existing revision.
164
This can happen when merging from another branch who
165
knows about revisions the destination does not. In
166
this test the second weave knows of an additional parent of
167
v2. Any revisions which are in common still have to have the
170
w1 = self.build_weave1()
171
wb = self.get_target()
172
wb.add_lines('x1', [], ['line from x1\n'])
173
wb.add_lines('v1', [], ['hello\n'])
174
wb.add_lines('v2', ['v1', 'x1'], ['hello\n', 'world\n'])
176
eq = self.assertEquals
177
eq(sorted(w1.versions()), ['v1', 'v2', 'v3', 'x1',])
178
eq(w1.get_text('x1'), 'line from x1\n')
179
eq(w1.get_lines('v2'), ['hello\n', 'world\n'])
180
eq(w1.get_parents('v2'), ['v1', 'x1'])
182
def test_join_with_ignore_missing_versions(self):
183
# test that ignore_missing=True makes a listed but absent version id
184
# be ignored, and that unlisted version_ids are not integrated.
185
w1 = self.build_weave1()
186
wb = self.get_target()
187
wb.add_lines('x1', [], ['line from x1\n'])
188
wb.add_lines('v1', [], ['hello\n'])
189
wb.add_lines('v2', ['v1', 'x1'], ['hello\n', 'world\n'])
190
w1.join(wb, version_ids=['x1', 'z1'], ignore_missing=True)
191
eq = self.assertEquals
192
eq(sorted(w1.versions()), ['v1', 'v2', 'v3', 'x1'])
193
eq(w1.get_text('x1'), 'line from x1\n')
194
eq(w1.get_lines('v2'), ['hello\n', 'world\n'])
195
eq(w1.get_parents('v2'), ['v1'])
197
def build_source_weave(self, name, *pattern):
198
w = self.get_source(name)
199
for version, parents in pattern:
200
w.add_lines(version, parents, [])
203
def build_target_weave(self, name, *pattern):
204
w = self.get_target(name)
205
for version, parents in pattern:
206
w.add_lines(version, parents, [])
209
def test_join_reorder(self):
210
"""Reweave requiring reordering of versions.
212
Weaves must be stored such that parents come before children. When
213
reweaving, we may add new parents to some children, but it is required
214
that there must be *some* valid order that can be found, otherwise the
215
ancestries are contradictory. (For the specific case of inserting
216
ghost revisions there will be no disagreement, only partial knowledge
219
Note that the weaves are only partially ordered: when there are two
220
versions where neither is an ancestor of the other the order in which
221
they occur is unconstrained. When we join those versions into
222
another weave, they may become more constrained and it may be
223
necessary to change their order.
225
One simple case of this is
230
We need to recognize that the final weave must show the ordering
231
a[], b[a], c[b]. The version that must be first in the result is
232
not first in either of the input weaves.
234
w1 = self.build_target_weave('1', ('c', []), ('a', []), ('b', ['a']))
235
w2 = self.build_source_weave('2', ('b', []), ('c', ['b']), ('a', []))
237
self.assertEqual([], w1.get_parents('a'))
238
self.assertEqual(['a'], w1.get_parents('b'))
239
self.assertEqual(['b'], w1.get_parents('c'))
241
def test_joining_ghosts(self):
242
# some versioned file formats allow lines to be added with parent
243
# information that is > than that in the format. Formats that do
244
# not support this need to raise NotImplementedError on the
245
# add_lines_with_ghosts api.
246
# files with ghost information when joined into a file which
247
# supports that must preserve it, when joined into a file which
248
# does not must discard it, and when filling a ghost for a listed
249
# ghost must reconcile it
250
source = self.get_source()
252
source.has_ghost('a')
254
except NotImplementedError:
255
source_ghosts = False
256
target = self.get_target()
258
target.has_ghost('a')
260
except NotImplementedError:
261
target_ghosts = False
263
if not source_ghosts and not target_ghosts:
266
if source_ghosts and not target_ghosts:
267
# switch source and target so source is ghostless
271
source_ghosts = False
273
# now target always supports ghosts.
275
# try filling target with ghosts and filling in reverse -
276
target.add_lines_with_ghosts('notbase', ['base'], [])
278
# legacy apis should behave
279
self.assertEqual(['notbase'], source.get_ancestry(['notbase']))
280
self.assertEqual([], source.get_parents('notbase'))
281
self.assertEqual({'notbase':[]}, source.get_graph())
282
self.assertFalse(source.has_version('base'))
284
# ghost data should have been preserved
285
self.assertEqual(['base', 'notbase'], source.get_ancestry_with_ghosts(['notbase']))
286
self.assertEqual(['base'], source.get_parents_with_ghosts('notbase'))
287
self.assertEqual({'notbase':['base']}, source.get_graph_with_ghosts())
288
self.assertTrue(source.has_ghost('base'))
290
# if we add something that is fills out what is a ghost, then
291
# when joining into a ghost aware join it should flesh out the ghosts.
292
source.add_lines('base', [], [])
293
target.join(source, version_ids=['base'])
294
self.assertEqual(['base', 'notbase'], target.get_ancestry(['notbase']))
295
self.assertEqual(['base'], target.get_parents('notbase'))
296
self.assertEqual({'base':[],
300
self.assertTrue(target.has_version('base'))
301
# we have _with_ghost apis to give us ghost information.
302
self.assertEqual(['base', 'notbase'], target.get_ancestry_with_ghosts(['notbase']))
303
self.assertEqual(['base'], target.get_parents_with_ghosts('notbase'))
304
self.assertEqual({'base':[],
307
target.get_graph_with_ghosts())
308
self.assertFalse(target.has_ghost('base'))