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.symbol_versioning import one_five
22
from bzrlib.tests import TestCaseWithTransport
23
from bzrlib.transport import get_transport
24
import bzrlib.versionedfile as versionedfile
27
class TestJoin(TestCaseWithTransport):
28
#Tests have self.versionedfile_factory and self.versionedfile_factory_to
29
#available to create source and target versioned files respectively.
31
def get_source(self, name='source'):
32
"""Get a versioned file we will be joining from."""
33
return self.versionedfile_factory(name,
34
get_transport(self.get_url()),
37
def get_target(self, name='target', create=True):
38
""""Get an empty versioned file to join into."""
39
return self.versionedfile_factory_to(name,
40
get_transport(self.get_url()),
44
f1 = self.get_source()
45
f1.add_lines('r0', [], ['a\n', 'b\n'])
46
f1.add_lines('r1', ['r0'], ['c\n', 'b\n'])
47
f2 = self.get_target()
48
self.applyDeprecated(one_five, f2.join, f1, None)
50
self.assertTrue(f.has_version('r0'))
51
self.assertTrue(f.has_version('r1'))
53
verify_file(self.get_target())
55
self.assertRaises(errors.RevisionNotPresent,
56
self.applyDeprecated, one_five, f2.join, f1, version_ids=['r3'])
58
def test_gets_expected_inter_worker(self):
59
source = self.get_source()
60
target = self.get_target()
61
inter = versionedfile.InterVersionedFile.get(source, target)
62
self.assertTrue(isinstance(inter, self.interversionedfile_class))
64
def test_join_versions_joins_ancestors_not_siblings(self):
65
# joining with a version list should bring in ancestors of the
66
# named versions but not siblings thereof.
67
target = self.get_target()
68
target.add_lines('base', [], [])
69
source = self.get_source()
70
source.add_lines('base', [], [])
71
source.add_lines('sibling', ['base'], [])
72
source.add_lines('ancestorleft', ['base'], [])
73
source.add_lines('ancestorright', ['base'], [])
74
source.add_lines('namedleft', ['ancestorleft'], [])
75
source.add_lines('namedright', ['ancestorright'], [])
76
self.applyDeprecated(one_five, target.join, source,
77
version_ids=['namedleft', 'namedright'])
78
self.assertFalse(target.has_version('sibling'))
79
self.assertTrue(target.has_version('ancestorleft'))
80
self.assertTrue(target.has_version('ancestorright'))
81
self.assertTrue(target.has_version('namedleft'))
82
self.assertTrue(target.has_version('namedright'))
84
def test_join_different_parents_existing_version(self):
85
"""This may either ignore or error."""
86
w1 = self.get_target('w1')
87
w2 = self.get_source('w2')
88
w1.add_lines('v-1', [], ['line 1\n'])
89
w2.add_lines('v-2', [], ['line 2\n'])
90
w1.add_lines('v-3', ['v-1'], ['line 1\n'])
91
w2.add_lines('v-3', ['v-2'], ['line 1\n'])
93
self.applyDeprecated(one_five, w1.join, w2)
94
except errors.WeaveParentMismatch:
95
# Acceptable behaviour:
97
self.assertEqual(sorted(w1.versions()),
98
'v-1 v-2 v-3'.split())
99
self.assertEqualDiff(w1.get_text('v-3'),
101
self.assertEqual({'v-3':('v-1',)}, w1.get_parent_map(['v-3']))
102
ann = list(w1.annotate('v-3'))
103
self.assertEqual(len(ann), 1)
104
self.assertEqual(ann[0][0], 'v-1')
105
self.assertEqual(ann[0][1], 'line 1\n')
107
def build_weave1(self):
108
weave1 = self.get_source()
109
self.lines1 = ['hello\n']
110
self.lines3 = ['hello\n', 'cruel\n', 'world\n']
111
weave1.add_lines('v1', [], self.lines1)
112
weave1.add_lines('v2', ['v1'], ['hello\n', 'world\n'])
113
weave1.add_lines('v3', ['v2'], self.lines3)
116
def test_join_with_empty(self):
117
"""Reweave adding empty weave"""
118
wb = self.get_target()
119
w1 = self.build_weave1()
120
self.applyDeprecated(one_five, w1.join, wb)
121
self.verify_weave1(w1)
123
def verify_weave1(self, w1):
124
self.assertEqual(sorted(w1.versions()), ['v1', 'v2', 'v3'])
125
self.assertEqual(w1.get_lines('v1'), ['hello\n'])
126
self.assertEqual({'v1':()}, w1.get_parent_map(['v1']))
127
self.assertEqual(w1.get_lines('v2'), ['hello\n', 'world\n'])
128
self.assertEqual({'v2':('v1',)}, w1.get_parent_map(['v2']))
129
self.assertEqual(w1.get_lines('v3'), ['hello\n', 'cruel\n', 'world\n'])
130
self.assertEqual({'v3':('v2',)}, w1.get_parent_map(['v3']))
132
def test_join_source_has_less_parents_preserves_parents(self):
133
# when the target has a text with more parent info, join
135
s = self.get_source()
136
s.add_lines('base', [], [])
137
s.add_lines('text', [], [])
138
t = self.get_target()
139
t.add_lines('base', [], [])
140
t.add_lines('text', ['base'], [])
141
self.applyDeprecated(one_five, t.join, s)
142
self.assertEqual({'text':('base',)}, t.get_parent_map(['text']))
144
def test_join_with_ghosts(self):
145
"""Join that inserts parents of an existing revision.
147
This can happen when merging from another branch who knows about
148
revisions the destination does not, and the destinations index is
149
incorrect because it was or is using a ghost-unaware format to
150
represent the index. In this test the second weave knows of an
151
additional parent of v2.
153
However v2 must not be changed because we consider indexes immutable:
154
instead a check or reconcile operation locally should pickup that v2 is
155
wrong and regenerate the index at a later time. So either this errors,
156
or leaves v2 unaltered.
158
w1 = self.build_weave1()
159
wb = self.get_target()
160
wb.add_lines('x1', [], ['line from x1\n'])
161
wb.add_lines('v1', [], ['hello\n'])
162
wb.add_lines('v2', ['v1', 'x1'], ['hello\n', 'world\n'])
164
self.applyDeprecated(one_five, w1.join, wb)
165
except errors.WeaveParentMismatch:
166
# Acceptable behaviour:
168
self.assertEqual(['v1', 'v2', 'v3', 'x1',], sorted(w1.versions()))
169
self.assertEqual('line from x1\n', w1.get_text('x1'))
170
self.assertEqual(['hello\n', 'world\n'], w1.get_lines('v2'))
171
self.assertEqual({'v2':('v1',)}, w1.get_parent_map(['v2']))
173
def test_join_with_ignore_missing_versions(self):
174
# test that ignore_missing=True makes a listed but absent version id
175
# be ignored, and that unlisted version_ids are not integrated.
176
w1 = self.build_weave1()
177
wb = self.get_target()
178
wb.add_lines('x1', [], ['line from x1\n'])
179
wb.add_lines('v1', [], ['hello\n'])
180
wb.add_lines('v2', ['v1', 'x1'], ['hello\n', 'world\n'])
181
self.applyDeprecated(one_five, w1.join, wb, version_ids=['x1', 'z1'],
183
eq = self.assertEquals
184
eq(sorted(w1.versions()), ['v1', 'v2', 'v3', 'x1'])
185
eq(w1.get_text('x1'), 'line from x1\n')
186
eq(w1.get_lines('v2'), ['hello\n', 'world\n'])
187
self.assertEqual({'v2':('v1',)}, w1.get_parent_map(['v2']))
189
def build_source_weave(self, name, *pattern):
190
w = self.get_source(name)
191
for version, parents in pattern:
192
w.add_lines(version, parents, [])
195
def build_target_weave(self, name, *pattern):
196
w = self.get_target(name)
197
for version, parents in pattern:
198
w.add_lines(version, parents, [])
201
def test_joining_ghosts(self):
202
# some versioned file formats allow lines to be added with parent
203
# information that is > than that in the format. Formats that do
204
# not support this need to raise NotImplementedError on the
205
# add_lines_with_ghosts api.
206
# files with ghost information when joined into a file which
207
# supports that must preserve it, when joined into a file which
208
# does not must discard it, and when filling a ghost for a listed
209
# ghost must reconcile it
210
source = self.get_source()
211
target = self.get_target()
212
# try filling target with ghosts and filling in reverse -
214
target.add_lines_with_ghosts('notbase', ['base'], [])
215
except NotImplementedError:
216
# The target does not support ghosts; the test is irrelevant.
219
self.applyDeprecated(one_five, source.join, target)
220
except errors.RevisionNotPresent:
222
# legacy apis should behave
223
self.assertEqual(['notbase'], source.get_ancestry(['notbase']))
224
self.assertFalse(source.has_version('base'))
225
# ghost data should have been preserved
226
self.assertEqual(['base', 'notbase'], source.get_ancestry_with_ghosts(['notbase']))
227
self.assertEqual(['base'], source.get_parents_with_ghosts('notbase'))
228
self.assertEqual({'notbase':('base',)},
229
source.get_parent_map(source.versions()))
231
# if we add something that is fills out what is a ghost, then
232
# when joining into a ghost aware join it should flesh out the ghosts.
233
source.add_lines('base', [], [])
234
self.applyDeprecated(one_five, target.join, source, version_ids=['base'])
235
self.assertEqual(['base', 'notbase'], target.get_ancestry(['notbase']))
236
self.assertEqual({'base':(),
237
'notbase':('base', ),
239
target.get_parent_map(target.versions()))
240
self.assertTrue(target.has_version('base'))
241
# we have _with_ghost apis to give us ghost information.
242
self.assertEqual(['base', 'notbase'], target.get_ancestry_with_ghosts(['notbase']))
243
self.assertEqual(['base'], target.get_parents_with_ghosts('notbase'))
244
self.assertEqual({'base':(),
247
source.get_parent_map(source.versions()))
249
def test_restricted_join_into_empty(self):
250
# joining into an empty versioned file with a version_ids list
251
# should only grab the selected versions.
252
source = self.get_source()
253
source.add_lines('skip_me', [], ['a\n'])
254
source.add_lines('inherit_me', [], ['b\n'])
255
source.add_lines('select_me', ['inherit_me'], ['b\n'])
256
target = self.get_target()
257
self.applyDeprecated(one_five, target.join, source, version_ids=['select_me'])
258
self.assertEqual(['inherit_me', 'select_me'], target.versions())
260
def test_join_odd_records(self):
261
# this tests that joining the 1st, 3rd and 5th records and not the
262
# 2nd and 4th works, particularly after reopening the file.
263
# this test is designed to test versioned files that perform
264
# optimisations on the join. Grabbing partial data and reopening the
265
# file make it likely to trigger a fault.
266
source = self.get_source()
267
source.add_lines('1', [], ['1st\n'])
268
source.add_lines('2', [], ['2nd\n'])
269
source.add_lines('3', ['1'], ['1st\n', '2nd\n'])
270
source.add_lines('4', ['2'], ['1st\n'])
271
source.add_lines('5', ['3'], ['1st\n', '2nd\n', '3rd\n'])
272
target = self.get_target()
273
self.applyDeprecated(one_five, target.join, source, version_ids=['1', '3', '5'])
274
target = self.get_target(create=False)
275
self.assertEqual(set(['1', '3', '5']), set(target.versions()))
276
self.assertEqual(3, len(target.versions()))
277
self.assertEqual(['1st\n'], target.get_lines('1'))
278
self.assertEqual(['1st\n', '2nd\n'], target.get_lines('3'))
279
self.assertEqual(['1st\n', '2nd\n', '3rd\n'], target.get_lines('5'))