1# Copyright (C) 2005-2011 Canonical Ltd
2#
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.
7#
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.
12#
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
16
17from io import BytesIO
18
19from ... import (
20    errors,
21    fifo_cache,
22    )
23from .. import (
24    inventory,
25    serializer,
26    xml6,
27    xml7,
28    xml8,
29    )
30from ..inventory import Inventory
31from . import TestCase
32import breezy.bzr.xml5
33
34_revision_v5 = b"""<revision committer="Martin Pool &lt;mbp@sourcefrog.net&gt;"
35    inventory_sha1="e79c31c1deb64c163cf660fdedd476dd579ffd41"
36    revision_id="mbp@sourcefrog.net-20050905080035-e0439293f8b6b9f9"
37    timestamp="1125907235.212"
38    timezone="36000">
39<message>- start splitting code for xml (de)serialization away from objects
40  preparatory to supporting multiple formats by a single library
41</message>
42<parents>
43<revision_ref revision_id="mbp@sourcefrog.net-20050905063503-43948f59fa127d92"/>
44</parents>
45</revision>
46"""
47
48_revision_v5_utc = b"""\
49<revision committer="Martin Pool &lt;mbp@sourcefrog.net&gt;"
50    inventory_sha1="e79c31c1deb64c163cf660fdedd476dd579ffd41"
51    revision_id="mbp@sourcefrog.net-20050905080035-e0439293f8b6b9f9"
52    timestamp="1125907235.212"
53    timezone="0">
54<message>- start splitting code for xml (de)serialization away from objects
55  preparatory to supporting multiple formats by a single library
56</message>
57<parents>
58<revision_ref revision_id="mbp@sourcefrog.net-20050905063503-43948f59fa127d92"/>
59</parents>
60</revision>
61"""
62
63_committed_inv_v5 = b"""<inventory>
64<file file_id="bar-20050901064931-73b4b1138abc9cd2"
65      name="bar" parent_id="TREE_ROOT"
66      revision="mbp@foo-123123"
67      text_sha1="A" text_size="1"/>
68<directory name="subdir"
69           file_id="foo-20050801201819-4139aa4a272f4250"
70           parent_id="TREE_ROOT"
71           revision="mbp@foo-00"/>
72<file executable="yes" file_id="bar-20050824000535-6bc48cfad47ed134"
73      name="bar" parent_id="foo-20050801201819-4139aa4a272f4250"
74      revision="mbp@foo-00"
75      text_sha1="B" text_size="0"/>
76</inventory>
77"""
78
79_basis_inv_v5 = b"""<inventory revision_id="mbp@sourcefrog.net-20050905063503-43948f59fa127d92">
80<file file_id="bar-20050901064931-73b4b1138abc9cd2"
81      name="bar" parent_id="TREE_ROOT"
82      revision="mbp@foo-123123"/>
83<directory name="subdir"
84           file_id="foo-20050801201819-4139aa4a272f4250"
85           parent_id="TREE_ROOT"
86           revision="mbp@foo-00"/>
87<file file_id="bar-20050824000535-6bc48cfad47ed134"
88      name="bar" parent_id="foo-20050801201819-4139aa4a272f4250"
89      revision="mbp@foo-00"/>
90</inventory>
91"""
92
93
94# DO NOT REFLOW THIS. Its the exact revision we want.
95_expected_rev_v5 = b"""<revision committer="Martin Pool &lt;mbp@sourcefrog.net&gt;" format="5" inventory_sha1="e79c31c1deb64c163cf660fdedd476dd579ffd41" revision_id="mbp@sourcefrog.net-20050905080035-e0439293f8b6b9f9" timestamp="1125907235.212" timezone="36000">
96<message>- start splitting code for xml (de)serialization away from objects
97  preparatory to supporting multiple formats by a single library
98</message>
99<parents>
100<revision_ref revision_id="mbp@sourcefrog.net-20050905063503-43948f59fa127d92" />
101</parents>
102</revision>
103"""
104
105
106# DO NOT REFLOW THIS. Its the exact inventory we want.
107_expected_inv_v5 = b"""<inventory format="5">
108<file file_id="bar-20050901064931-73b4b1138abc9cd2" name="bar" revision="mbp@foo-123123" text_sha1="A" text_size="1" />
109<directory file_id="foo-20050801201819-4139aa4a272f4250" name="subdir" revision="mbp@foo-00" />
110<file executable="yes" file_id="bar-20050824000535-6bc48cfad47ed134" name="bar" parent_id="foo-20050801201819-4139aa4a272f4250" revision="mbp@foo-00" text_sha1="B" text_size="0" />
111</inventory>
112"""
113
114
115_expected_inv_v5_root = b"""<inventory file_id="f&lt;" format="5" revision_id="mother!">
116<file file_id="bar-20050901064931-73b4b1138abc9cd2" name="bar" parent_id="f&lt;" revision="mbp@foo-123123" text_sha1="A" text_size="1" />
117<directory file_id="foo-20050801201819-4139aa4a272f4250" name="subdir" parent_id="f&lt;" revision="mbp@foo-00" />
118<file executable="yes" file_id="bar-20050824000535-6bc48cfad47ed134" name="bar" parent_id="foo-20050801201819-4139aa4a272f4250" revision="mbp@foo-00" text_sha1="B" text_size="0" />
119<symlink file_id="link-1" name="link" parent_id="foo-20050801201819-4139aa4a272f4250" revision="mbp@foo-00" symlink_target="a" />
120</inventory>
121"""
122
123_expected_inv_v6 = b"""<inventory format="6" revision_id="rev_outer">
124<directory file_id="tree-root-321" name="" revision="rev_outer" />
125<directory file_id="dir-id" name="dir" parent_id="tree-root-321" revision="rev_outer" />
126<file file_id="file-id" name="file" parent_id="tree-root-321" revision="rev_outer" text_sha1="A" text_size="1" />
127<symlink file_id="link-id" name="link" parent_id="tree-root-321" revision="rev_outer" symlink_target="a" />
128</inventory>
129"""
130
131_expected_inv_v7 = b"""<inventory format="7" revision_id="rev_outer">
132<directory file_id="tree-root-321" name="" revision="rev_outer" />
133<directory file_id="dir-id" name="dir" parent_id="tree-root-321" revision="rev_outer" />
134<file file_id="file-id" name="file" parent_id="tree-root-321" revision="rev_outer" text_sha1="A" text_size="1" />
135<symlink file_id="link-id" name="link" parent_id="tree-root-321" revision="rev_outer" symlink_target="a" />
136<tree-reference file_id="nested-id" name="nested" parent_id="tree-root-321" revision="rev_outer" reference_revision="rev_inner" />
137</inventory>
138"""
139
140_expected_rev_v8 = b"""<revision committer="Martin Pool &lt;mbp@sourcefrog.net&gt;" format="8" inventory_sha1="e79c31c1deb64c163cf660fdedd476dd579ffd41" revision_id="mbp@sourcefrog.net-20050905080035-e0439293f8b6b9f9" timestamp="1125907235.212" timezone="36000">
141<message>- start splitting code for xml (de)serialization away from objects
142  preparatory to supporting multiple formats by a single library
143</message>
144<parents>
145<revision_ref revision_id="mbp@sourcefrog.net-20050905063503-43948f59fa127d92" />
146</parents>
147</revision>
148"""
149
150_expected_inv_v8 = b"""<inventory format="8" revision_id="rev_outer">
151<directory file_id="tree-root-321" name="" revision="rev_outer" />
152<directory file_id="dir-id" name="dir" parent_id="tree-root-321" revision="rev_outer" />
153<file file_id="file-id" name="file" parent_id="tree-root-321" revision="rev_outer" text_sha1="A" text_size="1" />
154<symlink file_id="link-id" name="link" parent_id="tree-root-321" revision="rev_outer" symlink_target="a" />
155</inventory>
156"""
157
158_revision_utf8_v5 = b"""<revision committer="Erik B&#229;gfors &lt;erik@foo.net&gt;"
159    inventory_sha1="e79c31c1deb64c163cf660fdedd476dd579ffd41"
160    revision_id="erik@b&#229;gfors-02"
161    timestamp="1125907235.212"
162    timezone="36000">
163<message>Include &#181;nicode characters
164</message>
165<parents>
166<revision_ref revision_id="erik@b&#229;gfors-01"/>
167</parents>
168</revision>
169"""
170
171_expected_rev_v8_complex = b"""<revision committer="Erik B&#229;gfors &lt;erik@foo.net&gt;" format="8" inventory_sha1="e79c31c1deb64c163cf660fdedd476dd579ffd41" revision_id="erik@b&#229;gfors-02" timestamp="1125907235.212" timezone="36000">
172<message>Include &#181;nicode characters
173</message>
174<parents>
175<revision_ref revision_id="erik@b&#229;gfors-01" />
176<revision_ref revision_id="erik@bagfors-02" />
177</parents>
178<properties><property name="bar" />
179<property name="foo">this has a
180newline in it</property>
181</properties>
182</revision>
183"""
184
185
186
187_inventory_utf8_v5 = b"""<inventory file_id="TRE&#233;_ROOT" format="5"
188                                   revision_id="erik@b&#229;gfors-02">
189<file file_id="b&#229;r-01"
190      name="b&#229;r" parent_id="TRE&#233;_ROOT"
191      revision="erik@b&#229;gfors-01"/>
192<directory name="s&#181;bdir"
193           file_id="s&#181;bdir-01"
194           parent_id="TRE&#233;_ROOT"
195           revision="erik@b&#229;gfors-01"/>
196<file executable="yes" file_id="b&#229;r-02"
197      name="b&#229;r" parent_id="s&#181;bdir-01"
198      revision="erik@b&#229;gfors-02"/>
199</inventory>
200"""
201
202# Before revision_id was always stored as an attribute
203_inventory_v5a = b"""<inventory format="5">
204</inventory>
205"""
206
207# Before revision_id was always stored as an attribute
208_inventory_v5b = b"""<inventory format="5" revision_id="a-rev-id">
209</inventory>
210"""
211
212
213class TestSerializer(TestCase):
214    """Test XML serialization"""
215
216    def test_unpack_revision_5(self):
217        """Test unpacking a canned revision v5"""
218        inp = BytesIO(_revision_v5)
219        rev = breezy.bzr.xml5.serializer_v5.read_revision(inp)
220        eq = self.assertEqual
221        eq(rev.committer,
222           "Martin Pool <mbp@sourcefrog.net>")
223        eq(len(rev.parent_ids), 1)
224        eq(rev.timezone, 36000)
225        eq(rev.parent_ids[0],
226           b"mbp@sourcefrog.net-20050905063503-43948f59fa127d92")
227
228    def test_unpack_revision_5_utc(self):
229        inp = BytesIO(_revision_v5_utc)
230        rev = breezy.bzr.xml5.serializer_v5.read_revision(inp)
231        eq = self.assertEqual
232        eq(rev.committer,
233           "Martin Pool <mbp@sourcefrog.net>")
234        eq(len(rev.parent_ids), 1)
235        eq(rev.timezone, 0)
236        eq(rev.parent_ids[0],
237           b"mbp@sourcefrog.net-20050905063503-43948f59fa127d92")
238
239    def test_unpack_inventory_5(self):
240        """Unpack canned new-style inventory"""
241        inp = BytesIO(_committed_inv_v5)
242        inv = breezy.bzr.xml5.serializer_v5.read_inventory(inp)
243        eq = self.assertEqual
244        eq(len(inv), 4)
245        ie = inv.get_entry(b'bar-20050824000535-6bc48cfad47ed134')
246        eq(ie.kind, 'file')
247        eq(ie.revision, b'mbp@foo-00')
248        eq(ie.name, 'bar')
249        eq(inv.get_entry(ie.parent_id).kind, 'directory')
250
251    def test_unpack_basis_inventory_5(self):
252        """Unpack canned new-style inventory"""
253        inv = breezy.bzr.xml5.serializer_v5.read_inventory_from_lines(
254            breezy.osutils.split_lines(_basis_inv_v5))
255        eq = self.assertEqual
256        eq(len(inv), 4)
257        eq(inv.revision_id,
258           b'mbp@sourcefrog.net-20050905063503-43948f59fa127d92')
259        ie = inv.get_entry(b'bar-20050824000535-6bc48cfad47ed134')
260        eq(ie.kind, 'file')
261        eq(ie.revision, b'mbp@foo-00')
262        eq(ie.name, 'bar')
263        eq(inv.get_entry(ie.parent_id).kind, 'directory')
264
265    def test_unpack_inventory_5a(self):
266        inv = breezy.bzr.xml5.serializer_v5.read_inventory_from_lines(
267            breezy.osutils.split_lines(_inventory_v5a), revision_id=b'test-rev-id')
268        self.assertEqual(b'test-rev-id', inv.root.revision)
269
270    def test_unpack_inventory_5a_cache_and_copy(self):
271        # Passing an entry_cache should get populated with the objects
272        # But the returned objects should be copies if return_from_cache is
273        # False
274        entry_cache = fifo_cache.FIFOCache()
275        inv = breezy.bzr.xml5.serializer_v5.read_inventory_from_lines(
276            breezy.osutils.split_lines(_inventory_v5a), revision_id=b'test-rev-id',
277            entry_cache=entry_cache, return_from_cache=False)
278        for entry in inv.iter_just_entries():
279            key = (entry.file_id, entry.revision)
280            if entry.file_id is inv.root.file_id:
281                # The root id is inferred for xml v5
282                self.assertFalse(key in entry_cache)
283            else:
284                self.assertIsNot(entry, entry_cache[key])
285
286    def test_unpack_inventory_5a_cache_no_copy(self):
287        # Passing an entry_cache should get populated with the objects
288        # The returned objects should be exact if return_from_cache is
289        # True
290        entry_cache = fifo_cache.FIFOCache()
291        inv = breezy.bzr.xml5.serializer_v5.read_inventory_from_lines(
292            breezy.osutils.split_lines(_inventory_v5a), revision_id=b'test-rev-id',
293            entry_cache=entry_cache, return_from_cache=True)
294        for entry in inv.iter_just_entries():
295            key = (entry.file_id, entry.revision)
296            if entry.file_id is inv.root.file_id:
297                # The root id is inferred for xml v5
298                self.assertFalse(key in entry_cache)
299            else:
300                self.assertIs(entry, entry_cache[key])
301
302    def test_unpack_inventory_5b(self):
303        inv = breezy.bzr.xml5.serializer_v5.read_inventory_from_lines(
304            breezy.osutils.split_lines(_inventory_v5b), revision_id=b'test-rev-id')
305        self.assertEqual(b'a-rev-id', inv.root.revision)
306
307    def test_repack_inventory_5(self):
308        inv = breezy.bzr.xml5.serializer_v5.read_inventory_from_lines(
309            breezy.osutils.split_lines(_committed_inv_v5))
310        outp = BytesIO()
311        breezy.bzr.xml5.serializer_v5.write_inventory(inv, outp)
312        self.assertEqualDiff(_expected_inv_v5, outp.getvalue())
313        inv2 = breezy.bzr.xml5.serializer_v5.read_inventory_from_lines(
314            breezy.osutils.split_lines(outp.getvalue()))
315        self.assertEqual(inv, inv2)
316
317    def assertRoundTrips(self, xml_string):
318        inp = BytesIO(xml_string)
319        inv = breezy.bzr.xml5.serializer_v5.read_inventory(inp)
320        outp = BytesIO()
321        breezy.bzr.xml5.serializer_v5.write_inventory(inv, outp)
322        self.assertEqualDiff(xml_string, outp.getvalue())
323        lines = breezy.bzr.xml5.serializer_v5.write_inventory_to_lines(inv)
324        outp.seek(0)
325        self.assertEqual(outp.readlines(), lines)
326        inv2 = breezy.bzr.xml5.serializer_v5.read_inventory(
327            BytesIO(outp.getvalue()))
328        self.assertEqual(inv, inv2)
329
330    def tests_serialize_inventory_v5_with_root(self):
331        self.assertRoundTrips(_expected_inv_v5_root)
332
333    def check_repack_revision(self, txt):
334        """Check that repacking a revision yields the same information"""
335        inp = BytesIO(txt)
336        rev = breezy.bzr.xml5.serializer_v5.read_revision(inp)
337        outfile_contents = breezy.bzr.xml5.serializer_v5.write_revision_to_string(rev)
338        rev2 = breezy.bzr.xml5.serializer_v5.read_revision(
339            BytesIO(outfile_contents))
340        self.assertEqual(rev, rev2)
341
342    def test_repack_revision_5(self):
343        """Round-trip revision to XML v5"""
344        self.check_repack_revision(_revision_v5)
345
346    def test_repack_revision_5_utc(self):
347        self.check_repack_revision(_revision_v5_utc)
348
349    def test_pack_revision_5(self):
350        """Pack revision to XML v5"""
351        # fixed 20051025, revisions should have final newline
352        rev = breezy.bzr.xml5.serializer_v5.read_revision_from_string(
353            _revision_v5)
354        outfile_contents = breezy.bzr.xml5.serializer_v5.write_revision_to_string(rev)
355        self.assertEqual(outfile_contents[-1:], b'\n')
356        self.assertEqualDiff(
357            outfile_contents,
358            b''.join(breezy.bzr.xml5.serializer_v5.write_revision_to_lines(rev)))
359        self.assertEqualDiff(outfile_contents, _expected_rev_v5)
360
361    def test_empty_property_value(self):
362        """Create an empty property value check that it serializes correctly"""
363        s_v5 = breezy.bzr.xml5.serializer_v5
364        rev = s_v5.read_revision_from_string(_revision_v5)
365        props = {'empty': '', 'one': 'one'}
366        rev.properties = props
367        txt = b''.join(s_v5.write_revision_to_lines(rev))
368        new_rev = s_v5.read_revision_from_string(txt)
369        self.assertEqual(props, new_rev.properties)
370
371    def get_sample_inventory(self):
372        inv = Inventory(b'tree-root-321', revision_id=b'rev_outer')
373        inv.add(inventory.InventoryFile(b'file-id', 'file', b'tree-root-321'))
374        inv.add(inventory.InventoryDirectory(b'dir-id', 'dir',
375                                             b'tree-root-321'))
376        inv.add(inventory.InventoryLink(b'link-id', 'link', b'tree-root-321'))
377        inv.get_entry(b'tree-root-321').revision = b'rev_outer'
378        inv.get_entry(b'dir-id').revision = b'rev_outer'
379        inv.get_entry(b'file-id').revision = b'rev_outer'
380        inv.get_entry(b'file-id').text_sha1 = b'A'
381        inv.get_entry(b'file-id').text_size = 1
382        inv.get_entry(b'link-id').revision = b'rev_outer'
383        inv.get_entry(b'link-id').symlink_target = 'a'
384        return inv
385
386    def test_roundtrip_inventory_v7(self):
387        inv = self.get_sample_inventory()
388        inv.add(inventory.TreeReference(b'nested-id', 'nested', b'tree-root-321',
389                                        b'rev_outer', b'rev_inner'))
390        lines = xml7.serializer_v7.write_inventory_to_lines(inv)
391        self.assertEqualDiff(_expected_inv_v7, b''.join(lines))
392        inv2 = xml7.serializer_v7.read_inventory_from_lines(lines)
393        self.assertEqual(5, len(inv2))
394        for path, ie in inv.iter_entries():
395            self.assertEqual(ie, inv2.get_entry(ie.file_id))
396
397    def test_roundtrip_inventory_v6(self):
398        inv = self.get_sample_inventory()
399        lines = xml6.serializer_v6.write_inventory_to_lines(inv)
400        self.assertEqualDiff(_expected_inv_v6, b''.join(lines))
401        inv2 = xml6.serializer_v6.read_inventory_from_lines(lines)
402        self.assertEqual(4, len(inv2))
403        for path, ie in inv.iter_entries():
404            self.assertEqual(ie, inv2.get_entry(ie.file_id))
405
406    def test_wrong_format_v7(self):
407        """Can't accidentally open a file with wrong serializer"""
408        s_v6 = breezy.bzr.xml6.serializer_v6
409        s_v7 = xml7.serializer_v7
410        self.assertRaises(serializer.UnexpectedInventoryFormat,
411                          s_v7.read_inventory_from_lines,
412                          breezy.osutils.split_lines(_expected_inv_v5))
413        self.assertRaises(serializer.UnexpectedInventoryFormat,
414                          s_v6.read_inventory_from_lines,
415                          breezy.osutils.split_lines(_expected_inv_v7))
416
417    def test_tree_reference(self):
418        s_v5 = breezy.bzr.xml5.serializer_v5
419        s_v6 = breezy.bzr.xml6.serializer_v6
420        s_v7 = xml7.serializer_v7
421        inv = Inventory(b'tree-root-321', revision_id=b'rev-outer')
422        inv.root.revision = b'root-rev'
423        inv.add(inventory.TreeReference(b'nested-id', 'nested', b'tree-root-321',
424                                        b'rev-outer', b'rev-inner'))
425        self.assertRaises(serializer.UnsupportedInventoryKind,
426                          s_v5.write_inventory_to_lines, inv)
427        self.assertRaises(serializer.UnsupportedInventoryKind,
428                          s_v6.write_inventory_to_lines, inv)
429        lines = s_v7.write_inventory_to_chunks(inv)
430        inv2 = s_v7.read_inventory_from_lines(lines)
431        self.assertEqual(b'tree-root-321',
432                         inv2.get_entry(b'nested-id').parent_id)
433        self.assertEqual(b'rev-outer', inv2.get_entry(b'nested-id').revision)
434        self.assertEqual(
435            b'rev-inner', inv2.get_entry(b'nested-id').reference_revision)
436
437    def test_roundtrip_inventory_v8(self):
438        inv = self.get_sample_inventory()
439        lines = xml8.serializer_v8.write_inventory_to_lines(inv)
440        inv2 = xml8.serializer_v8.read_inventory_from_lines(lines)
441        self.assertEqual(4, len(inv2))
442        for path, ie in inv.iter_entries():
443            self.assertEqual(ie, inv2.get_entry(ie.file_id))
444
445    def test_inventory_text_v8(self):
446        inv = self.get_sample_inventory()
447        lines = xml8.serializer_v8.write_inventory_to_lines(inv)
448        self.assertEqualDiff(_expected_inv_v8, b''.join(lines))
449
450    def test_revision_text_v6(self):
451        """Pack revision to XML v6"""
452        rev = breezy.bzr.xml6.serializer_v6.read_revision_from_string(
453            _expected_rev_v5)
454        serialized = breezy.bzr.xml6.serializer_v6.write_revision_to_lines(
455            rev)
456        self.assertEqualDiff(b''.join(serialized), _expected_rev_v5)
457
458    def test_revision_text_v7(self):
459        """Pack revision to XML v7"""
460        rev = breezy.bzr.xml7.serializer_v7.read_revision_from_string(
461            _expected_rev_v5)
462        serialized = breezy.bzr.xml7.serializer_v7.write_revision_to_lines(
463            rev)
464        self.assertEqualDiff(b''.join(serialized), _expected_rev_v5)
465
466    def test_revision_text_v8(self):
467        """Pack revision to XML v8"""
468        rev = breezy.bzr.xml8.serializer_v8.read_revision_from_string(
469            _expected_rev_v8)
470        serialized = breezy.bzr.xml8.serializer_v8.write_revision_to_lines(
471            rev)
472        self.assertEqualDiff(b''.join(serialized), _expected_rev_v8)
473
474    def test_revision_text_v8_complex(self):
475        """Pack revision to XML v8"""
476        rev = breezy.bzr.xml8.serializer_v8.read_revision_from_string(
477            _expected_rev_v8_complex)
478        serialized = breezy.bzr.xml8.serializer_v8.write_revision_to_lines(
479            rev)
480        self.assertEqualDiff(b''.join(serialized), _expected_rev_v8_complex)
481
482    def test_revision_ids_are_utf8(self):
483        """Parsed revision_ids should all be utf-8 strings, not unicode."""
484        s_v5 = breezy.bzr.xml5.serializer_v5
485        rev = s_v5.read_revision_from_string(_revision_utf8_v5)
486        self.assertEqual(b'erik@b\xc3\xa5gfors-02', rev.revision_id)
487        self.assertIsInstance(rev.revision_id, bytes)
488        self.assertEqual([b'erik@b\xc3\xa5gfors-01'], rev.parent_ids)
489        for parent_id in rev.parent_ids:
490            self.assertIsInstance(parent_id, bytes)
491        self.assertEqual(u'Include \xb5nicode characters\n', rev.message)
492        self.assertIsInstance(rev.message, str)
493
494        # ie.revision should either be None or a utf-8 revision id
495        inv = s_v5.read_inventory_from_lines(breezy.osutils.split_lines(_inventory_utf8_v5))
496        rev_id_1 = u'erik@b\xe5gfors-01'.encode('utf8')
497        rev_id_2 = u'erik@b\xe5gfors-02'.encode('utf8')
498        fid_root = u'TRE\xe9_ROOT'.encode('utf8')
499        fid_bar1 = u'b\xe5r-01'.encode('utf8')
500        fid_sub = u's\xb5bdir-01'.encode('utf8')
501        fid_bar2 = u'b\xe5r-02'.encode('utf8')
502        expected = [(u'', fid_root, None, rev_id_2),
503                    (u'b\xe5r', fid_bar1, fid_root, rev_id_1),
504                    (u's\xb5bdir', fid_sub, fid_root, rev_id_1),
505                    (u's\xb5bdir/b\xe5r', fid_bar2, fid_sub, rev_id_2),
506                    ]
507        self.assertEqual(rev_id_2, inv.revision_id)
508        self.assertIsInstance(inv.revision_id, bytes)
509
510        actual = list(inv.iter_entries_by_dir())
511        for ((exp_path, exp_file_id, exp_parent_id, exp_rev_id),
512             (act_path, act_ie)) in zip(expected, actual):
513            self.assertEqual(exp_path, act_path)
514            self.assertIsInstance(act_path, str)
515            self.assertEqual(exp_file_id, act_ie.file_id)
516            self.assertIsInstance(act_ie.file_id, bytes)
517            self.assertEqual(exp_parent_id, act_ie.parent_id)
518            if exp_parent_id is not None:
519                self.assertIsInstance(act_ie.parent_id, bytes)
520            self.assertEqual(exp_rev_id, act_ie.revision)
521            if exp_rev_id is not None:
522                self.assertIsInstance(act_ie.revision, bytes)
523
524        self.assertEqual(len(expected), len(actual))
525
526    def test_serialization_error(self):
527        s_v5 = breezy.bzr.xml5.serializer_v5
528        e = self.assertRaises(
529            serializer.UnexpectedInventoryFormat,
530            s_v5.read_inventory_from_lines, [b"<Notquitexml"])
531        self.assertEqual(str(e), "unclosed token: line 1, column 0")
532
533
534class TestEncodeAndEscape(TestCase):
535    """Whitebox testing of the _encode_and_escape function."""
536
537    def setUp(self):
538        super(TestEncodeAndEscape, self).setUp()
539        # Keep the cache clear before and after the test
540        breezy.bzr.xml_serializer._clear_cache()
541        self.addCleanup(breezy.bzr.xml_serializer._clear_cache)
542
543    def test_simple_ascii(self):
544        # _encode_and_escape always appends a final ", because these parameters
545        # are being used in xml attributes, and by returning it now, we have to
546        # do fewer string operations later.
547        val = breezy.bzr.xml_serializer.encode_and_escape('foo bar')
548        self.assertEqual(b'foo bar', val)
549        # The second time should be cached
550        val2 = breezy.bzr.xml_serializer.encode_and_escape('foo bar')
551        self.assertIs(val2, val)
552
553    def test_ascii_with_xml(self):
554        self.assertEqual(b'&amp;&apos;&quot;&lt;&gt;',
555                         breezy.bzr.xml_serializer.encode_and_escape('&\'"<>'))
556
557    def test_utf8_with_xml(self):
558        # u'\xb5\xe5&\u062c'
559        utf8_str = b'\xc2\xb5\xc3\xa5&\xd8\xac'
560        self.assertEqual(b'&#181;&#229;&amp;&#1580;',
561                         breezy.bzr.xml_serializer.encode_and_escape(utf8_str))
562
563    def test_unicode(self):
564        uni_str = u'\xb5\xe5&\u062c'
565        self.assertEqual(b'&#181;&#229;&amp;&#1580;',
566                         breezy.bzr.xml_serializer.encode_and_escape(uni_str))
567
568
569class TestMisc(TestCase):
570
571    def test_unescape_xml(self):
572        """We get some kind of error when malformed entities are passed"""
573        self.assertRaises(KeyError, breezy.bzr.xml8._unescape_xml, b'foo&bar;')
574