1
# Copyright (C) 2005-2011, 2016 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
22
from ..errors import BinaryFile
23
from ..sixish import (
30
return BytesIO(t).readlines()
33
############################################################
34
# test case data from the gnu diffutils manual
36
TZU = split_lines(b""" The Nameless is the origin of Heaven and Earth;
37
The named is the mother of all things.
39
Therefore let there always be non-being,
40
so we may see their subtlety,
41
And let there always be being,
42
so we may see their outcome.
44
But after they are produced,
45
they have different names.
46
They both may be called deep and profound.
47
Deeper and more profound,
48
The door of all subtleties!
51
LAO = split_lines(b""" The Way that can be told of is not the eternal Way;
52
The name that can be named is not the eternal name.
53
The Nameless is the origin of Heaven and Earth;
54
The Named is the mother of all things.
55
Therefore let there always be non-being,
56
so we may see their subtlety,
57
And let there always be being,
58
so we may see their outcome.
60
But after they are produced,
61
they have different names.
65
TAO = split_lines(b""" The Way that can be told of is not the eternal Way;
66
The name that can be named is not the eternal name.
67
The Nameless is the origin of Heaven and Earth;
68
The named is the mother of all things.
70
Therefore let there always be non-being,
71
so we may see their subtlety,
72
And let there always be being,
73
so we may see their result.
75
But after they are produced,
76
they have different names.
78
-- The Way of Lao-Tzu, tr. Wing-tsit Chan
82
MERGED_RESULT = split_lines(b""" The Way that can be told of is not the eternal Way;
83
The name that can be named is not the eternal name.
84
The Nameless is the origin of Heaven and Earth;
85
The Named is the mother of all things.
86
Therefore let there always be non-being,
87
so we may see their subtlety,
88
And let there always be being,
89
so we may see their result.
91
But after they are produced,
92
they have different names.
96
-- The Way of Lao-Tzu, tr. Wing-tsit Chan
102
class TestMerge3(tests.TestCase):
104
def test_no_changes(self):
105
"""No conflicts because nothing changed"""
106
m3 = merge3.Merge3([b'aaa', b'bbb'],
110
self.assertEqual(m3.find_unconflicted(),
113
self.assertEqual(list(m3.find_sync_regions()),
119
self.assertEqual(list(m3.merge_regions()),
120
[('unchanged', 0, 2)])
122
self.assertEqual(list(m3.merge_groups()),
123
[('unchanged', [b'aaa', b'bbb'])])
125
def test_front_insert(self):
126
m3 = merge3.Merge3([b'zz'],
127
[b'aaa', b'bbb', b'zz'],
130
# todo: should use a sentinal at end as from get_matching_blocks
131
# to match without zz
132
self.assertEqual(list(m3.find_sync_regions()),
134
(1, 1, 3, 3, 1, 1), ])
136
self.assertEqual(list(m3.merge_regions()),
138
('unchanged', 0, 1)])
140
self.assertEqual(list(m3.merge_groups()),
141
[('a', [b'aaa', b'bbb']),
142
('unchanged', [b'zz'])])
144
def test_null_insert(self):
145
m3 = merge3.Merge3([],
148
# todo: should use a sentinal at end as from get_matching_blocks
149
# to match without zz
150
self.assertEqual(list(m3.find_sync_regions()),
151
[(0, 0, 2, 2, 0, 0)])
153
self.assertEqual(list(m3.merge_regions()),
156
self.assertEqual(list(m3.merge_lines()),
159
def test_no_conflicts(self):
160
"""No conflicts because only one side changed"""
161
m3 = merge3.Merge3([b'aaa', b'bbb'],
162
[b'aaa', b'111', b'bbb'],
165
self.assertEqual(m3.find_unconflicted(),
168
self.assertEqual(list(m3.find_sync_regions()),
171
(2, 2, 3, 3, 2, 2), ])
173
self.assertEqual(list(m3.merge_regions()),
174
[('unchanged', 0, 1),
176
('unchanged', 1, 2), ])
178
def test_append_a(self):
179
m3 = merge3.Merge3([b'aaa\n', b'bbb\n'],
180
[b'aaa\n', b'bbb\n', b'222\n'],
181
[b'aaa\n', b'bbb\n'])
183
self.assertEqual(b''.join(m3.merge_lines()),
186
def test_append_b(self):
187
m3 = merge3.Merge3([b'aaa\n', b'bbb\n'],
188
[b'aaa\n', b'bbb\n'],
189
[b'aaa\n', b'bbb\n', b'222\n'])
191
self.assertEqual(b''.join(m3.merge_lines()),
194
def test_append_agreement(self):
195
m3 = merge3.Merge3([b'aaa\n', b'bbb\n'],
196
[b'aaa\n', b'bbb\n', b'222\n'],
197
[b'aaa\n', b'bbb\n', b'222\n'])
199
self.assertEqual(b''.join(m3.merge_lines()),
202
def test_append_clash(self):
203
m3 = merge3.Merge3([b'aaa\n', b'bbb\n'],
204
[b'aaa\n', b'bbb\n', b'222\n'],
205
[b'aaa\n', b'bbb\n', b'333\n'])
207
ml = m3.merge_lines(name_a=b'a',
212
self.assertEqual(b''.join(ml),
223
def test_insert_agreement(self):
224
m3 = merge3.Merge3([b'aaa\n', b'bbb\n'],
225
[b'aaa\n', b'222\n', b'bbb\n'],
226
[b'aaa\n', b'222\n', b'bbb\n'])
228
ml = m3.merge_lines(name_a=b'a',
233
self.assertEqual(b''.join(ml), b'aaa\n222\nbbb\n')
235
def test_insert_clash(self):
236
"""Both try to insert lines in the same place."""
237
m3 = merge3.Merge3([b'aaa\n', b'bbb\n'],
238
[b'aaa\n', b'111\n', b'bbb\n'],
239
[b'aaa\n', b'222\n', b'bbb\n'])
241
self.assertEqual(m3.find_unconflicted(),
244
self.assertEqual(list(m3.find_sync_regions()),
247
(2, 2, 3, 3, 3, 3), ])
249
self.assertEqual(list(m3.merge_regions()),
250
[('unchanged', 0, 1),
251
('conflict', 1, 1, 1, 2, 1, 2),
252
('unchanged', 1, 2)])
254
self.assertEqual(list(m3.merge_groups()),
255
[('unchanged', [b'aaa\n']),
256
('conflict', [], [b'111\n'], [b'222\n']),
257
('unchanged', [b'bbb\n']),
260
ml = m3.merge_lines(name_a=b'a',
265
self.assertEqual(b''.join(ml),
275
def test_replace_clash(self):
276
"""Both try to insert lines in the same place."""
277
m3 = merge3.Merge3([b'aaa', b'000', b'bbb'],
278
[b'aaa', b'111', b'bbb'],
279
[b'aaa', b'222', b'bbb'])
281
self.assertEqual(m3.find_unconflicted(),
284
self.assertEqual(list(m3.find_sync_regions()),
287
(3, 3, 3, 3, 3, 3), ])
289
def test_replace_multi(self):
290
"""Replacement with regions of different size."""
291
m3 = merge3.Merge3([b'aaa', b'000', b'000', b'bbb'],
292
[b'aaa', b'111', b'111', b'111', b'bbb'],
293
[b'aaa', b'222', b'222', b'222', b'222', b'bbb'])
295
self.assertEqual(m3.find_unconflicted(),
298
self.assertEqual(list(m3.find_sync_regions()),
301
(4, 4, 5, 5, 6, 6), ])
303
def test_merge_poem(self):
304
"""Test case from diff3 manual"""
305
m3 = merge3.Merge3(TZU, LAO, TAO)
306
ml = list(m3.merge_lines(b'LAO', b'TAO'))
307
self.log('merge result:')
308
self.log(b''.join(ml))
309
self.assertEqual(ml, MERGED_RESULT)
311
def test_minimal_conflicts_common(self):
313
base_text = (b"a\n" * 20).splitlines(True)
314
this_text = (b"a\n" * 10 + b"b\n" * 10).splitlines(True)
315
other_text = (b"a\n" * 10 + b"c\n" + b"b\n" *
316
8 + b"c\n").splitlines(True)
317
m3 = merge3.Merge3(base_text, other_text, this_text)
318
m_lines = m3.merge_lines(b'OTHER', b'THIS', reprocess=True)
319
merged_text = b"".join(list(m_lines))
320
optimal_text = (b"a\n" * 10 + b"<<<<<<< OTHER\nc\n"
321
+ 8 * b"b\n" + b"c\n=======\n"
322
+ 10 * b"b\n" + b">>>>>>> THIS\n")
323
self.assertEqualDiff(optimal_text, merged_text)
325
def test_minimal_conflicts_unique(self):
327
"""Add a newline to each entry in the string"""
328
return [(int2byte(x) + b'\n') for x in bytearray(s)]
330
base_text = add_newline(b"abcdefghijklm")
331
this_text = add_newline(b"abcdefghijklmNOPQRSTUVWXYZ")
332
other_text = add_newline(b"abcdefghijklm1OPQRSTUVWXY2")
333
m3 = merge3.Merge3(base_text, other_text, this_text)
334
m_lines = m3.merge_lines(b'OTHER', b'THIS', reprocess=True)
335
merged_text = b"".join(list(m_lines))
336
optimal_text = b''.join(add_newline(b"abcdefghijklm")
337
+ [b"<<<<<<< OTHER\n1\n=======\nN\n>>>>>>> THIS\n"]
338
+ add_newline(b'OPQRSTUVWXY')
339
+ [b"<<<<<<< OTHER\n2\n=======\nZ\n>>>>>>> THIS\n"]
341
self.assertEqualDiff(optimal_text, merged_text)
343
def test_minimal_conflicts_nonunique(self):
345
"""Add a newline to each entry in the string"""
346
return [(int2byte(x) + b'\n') for x in bytearray(s)]
348
base_text = add_newline(b"abacddefgghij")
349
this_text = add_newline(b"abacddefgghijkalmontfprz")
350
other_text = add_newline(b"abacddefgghijknlmontfprd")
351
m3 = merge3.Merge3(base_text, other_text, this_text)
352
m_lines = m3.merge_lines(b'OTHER', b'THIS', reprocess=True)
353
merged_text = b"".join(list(m_lines))
354
optimal_text = b''.join(add_newline(b"abacddefgghijk")
355
+ [b"<<<<<<< OTHER\nn\n=======\na\n>>>>>>> THIS\n"]
356
+ add_newline(b'lmontfpr')
357
+ [b"<<<<<<< OTHER\nd\n=======\nz\n>>>>>>> THIS\n"]
359
self.assertEqualDiff(optimal_text, merged_text)
361
def test_reprocess_and_base(self):
362
"""Reprocessing and showing base breaks correctly"""
363
base_text = (b"a\n" * 20).splitlines(True)
364
this_text = (b"a\n" * 10 + b"b\n" * 10).splitlines(True)
365
other_text = (b"a\n" * 10 + b"c\n" + b"b\n" *
366
8 + b"c\n").splitlines(True)
367
m3 = merge3.Merge3(base_text, other_text, this_text)
368
m_lines = m3.merge_lines(b'OTHER', b'THIS', reprocess=True,
369
base_marker=b'|||||||')
370
self.assertRaises(merge3.CantReprocessAndShowBase, list, m_lines)
372
def test_binary(self):
373
self.assertRaises(BinaryFile, merge3.Merge3, [b'\x00'], [b'a'], [b'b'])
375
def test_dos_text(self):
378
other_text = b'c\r\n'
379
m3 = merge3.Merge3(base_text.splitlines(True),
380
other_text.splitlines(True),
381
this_text.splitlines(True))
382
m_lines = m3.merge_lines(b'OTHER', b'THIS')
383
self.assertEqual(b'<<<<<<< OTHER\r\nc\r\n=======\r\nb\r\n'
384
b'>>>>>>> THIS\r\n'.splitlines(True), list(m_lines))
386
def test_mac_text(self):
390
m3 = merge3.Merge3(base_text.splitlines(True),
391
other_text.splitlines(True),
392
this_text.splitlines(True))
393
m_lines = m3.merge_lines(b'OTHER', b'THIS')
394
self.assertEqual(b'<<<<<<< OTHER\rc\r=======\rb\r'
395
b'>>>>>>> THIS\r'.splitlines(True), list(m_lines))
397
def test_merge3_cherrypick(self):
398
base_text = b"a\nb\n"
400
other_text = b"a\nb\nc\n"
401
# When cherrypicking, lines in base are not part of the conflict
402
m3 = merge3.Merge3(base_text.splitlines(True),
403
this_text.splitlines(True),
404
other_text.splitlines(True), is_cherrypick=True)
405
m_lines = m3.merge_lines()
406
self.assertEqualDiff(b'a\n<<<<<<<\n=======\nc\n>>>>>>>\n',
409
# This is not symmetric
410
m3 = merge3.Merge3(base_text.splitlines(True),
411
other_text.splitlines(True),
412
this_text.splitlines(True), is_cherrypick=True)
413
m_lines = m3.merge_lines()
414
self.assertEqualDiff(b'a\n<<<<<<<\nb\nc\n=======\n>>>>>>>\n',
417
def test_merge3_cherrypick_w_mixed(self):
418
base_text = b'a\nb\nc\nd\ne\n'
419
this_text = b'a\nb\nq\n'
420
other_text = b'a\nb\nc\nd\nf\ne\ng\n'
421
# When cherrypicking, lines in base are not part of the conflict
422
m3 = merge3.Merge3(base_text.splitlines(True),
423
this_text.splitlines(True),
424
other_text.splitlines(True), is_cherrypick=True)
425
m_lines = m3.merge_lines()
426
self.assertEqualDiff(b'a\n'
439
def test_allow_objects(self):
440
"""Objects other than strs may be used with Merge3 when
443
merge_groups and merge_regions work with non-str input. Methods that
444
return lines like merge_lines fail.
446
base = [(x, x) for x in 'abcde']
447
a = [(x, x) for x in 'abcdef']
448
b = [(x, x) for x in 'Zabcde']
449
m3 = merge3.Merge3(base, a, b, allow_objects=True)
454
list(m3.merge_regions()))
456
[('b', [('Z', 'Z')]),
457
('unchanged', [(x, x) for x in 'abcde']),
458
('a', [('f', 'f')])],
459
list(m3.merge_groups()))