blob: f69e9cf8197c7520c0d470f9b7ae093c7c442f62 [file] [log] [blame]
Mike Frysinger04122b72019-07-31 23:32:58 -04001# Copyright (C) 2019 The Android Open Source Project
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15"""Unittests for the manifest_xml.py module."""
16
Mike Frysingerd9254592020-02-19 22:36:26 -050017import os
Mike Frysinger8c1e9cb2020-09-06 14:53:18 -040018import shutil
19import tempfile
Mike Frysinger04122b72019-07-31 23:32:58 -040020import unittest
Mike Frysingerbb8ee7f2020-02-22 05:30:12 -050021import xml.dom.minidom
Mike Frysinger04122b72019-07-31 23:32:58 -040022
23import error
24import manifest_xml
25
26
Mike Frysingera29424e2021-02-25 21:53:49 -050027# Invalid paths that we don't want in the filesystem.
28INVALID_FS_PATHS = (
29 '',
30 '.',
31 '..',
32 '../',
33 './',
34 'foo/',
35 './foo',
36 '../foo',
37 'foo/./bar',
38 'foo/../../bar',
39 '/foo',
40 './../foo',
41 '.git/foo',
42 # Check case folding.
43 '.GIT/foo',
44 'blah/.git/foo',
45 '.repo/foo',
46 '.repoconfig',
47 # Block ~ due to 8.3 filenames on Windows filesystems.
48 '~',
49 'foo~',
50 'blah/foo~',
51 # Block Unicode characters that get normalized out by filesystems.
52 u'foo\u200Cbar',
53)
54
55# Make sure platforms that use path separators (e.g. Windows) are also
56# rejected properly.
57if os.path.sep != '/':
58 INVALID_FS_PATHS += tuple(x.replace('/', os.path.sep) for x in INVALID_FS_PATHS)
59
60
Mike Frysinger37ac3d62021-02-25 04:54:56 -050061class ManifestParseTestCase(unittest.TestCase):
62 """TestCase for parsing manifests."""
63
64 def setUp(self):
65 self.tempdir = tempfile.mkdtemp(prefix='repo_tests')
66 self.repodir = os.path.join(self.tempdir, '.repo')
67 self.manifest_dir = os.path.join(self.repodir, 'manifests')
68 self.manifest_file = os.path.join(
69 self.repodir, manifest_xml.MANIFEST_FILE_NAME)
70 self.local_manifest_dir = os.path.join(
71 self.repodir, manifest_xml.LOCAL_MANIFESTS_DIR_NAME)
72 os.mkdir(self.repodir)
73 os.mkdir(self.manifest_dir)
74
75 # The manifest parsing really wants a git repo currently.
76 gitdir = os.path.join(self.repodir, 'manifests.git')
77 os.mkdir(gitdir)
78 with open(os.path.join(gitdir, 'config'), 'w') as fp:
79 fp.write("""[remote "origin"]
80 url = https://localhost:0/manifest
81""")
82
83 def tearDown(self):
84 shutil.rmtree(self.tempdir, ignore_errors=True)
85
86 def getXmlManifest(self, data):
87 """Helper to initialize a manifest for testing."""
88 with open(self.manifest_file, 'w') as fp:
89 fp.write(data)
90 return manifest_xml.XmlManifest(self.repodir, self.manifest_file)
91
92
Mike Frysinger04122b72019-07-31 23:32:58 -040093class ManifestValidateFilePaths(unittest.TestCase):
94 """Check _ValidateFilePaths helper.
95
96 This doesn't access a real filesystem.
97 """
98
99 def check_both(self, *args):
100 manifest_xml.XmlManifest._ValidateFilePaths('copyfile', *args)
101 manifest_xml.XmlManifest._ValidateFilePaths('linkfile', *args)
102
103 def test_normal_path(self):
104 """Make sure good paths are accepted."""
105 self.check_both('foo', 'bar')
106 self.check_both('foo/bar', 'bar')
107 self.check_both('foo', 'bar/bar')
108 self.check_both('foo/bar', 'bar/bar')
109
110 def test_symlink_targets(self):
111 """Some extra checks for symlinks."""
112 def check(*args):
113 manifest_xml.XmlManifest._ValidateFilePaths('linkfile', *args)
114
115 # We allow symlinks to end in a slash since we allow them to point to dirs
116 # in general. Technically the slash isn't necessary.
117 check('foo/', 'bar')
Mike Frysingerae625412020-02-10 17:10:03 -0500118 # We allow a single '.' to get a reference to the project itself.
119 check('.', 'bar')
Mike Frysinger04122b72019-07-31 23:32:58 -0400120
121 def test_bad_paths(self):
122 """Make sure bad paths (src & dest) are rejected."""
Mike Frysingera29424e2021-02-25 21:53:49 -0500123 for path in INVALID_FS_PATHS:
Mike Frysinger04122b72019-07-31 23:32:58 -0400124 self.assertRaises(
125 error.ManifestInvalidPathError, self.check_both, path, 'a')
126 self.assertRaises(
127 error.ManifestInvalidPathError, self.check_both, 'a', path)
Mike Frysingerbb8ee7f2020-02-22 05:30:12 -0500128
129
130class ValueTests(unittest.TestCase):
131 """Check utility parsing code."""
132
133 def _get_node(self, text):
134 return xml.dom.minidom.parseString(text).firstChild
135
136 def test_bool_default(self):
137 """Check XmlBool default handling."""
138 node = self._get_node('<node/>')
139 self.assertIsNone(manifest_xml.XmlBool(node, 'a'))
140 self.assertIsNone(manifest_xml.XmlBool(node, 'a', None))
141 self.assertEqual(123, manifest_xml.XmlBool(node, 'a', 123))
142
143 node = self._get_node('<node a=""/>')
144 self.assertIsNone(manifest_xml.XmlBool(node, 'a'))
145
146 def test_bool_invalid(self):
147 """Check XmlBool invalid handling."""
148 node = self._get_node('<node a="moo"/>')
149 self.assertEqual(123, manifest_xml.XmlBool(node, 'a', 123))
150
151 def test_bool_true(self):
152 """Check XmlBool true values."""
153 for value in ('yes', 'true', '1'):
154 node = self._get_node('<node a="%s"/>' % (value,))
155 self.assertTrue(manifest_xml.XmlBool(node, 'a'))
156
157 def test_bool_false(self):
158 """Check XmlBool false values."""
159 for value in ('no', 'false', '0'):
160 node = self._get_node('<node a="%s"/>' % (value,))
161 self.assertFalse(manifest_xml.XmlBool(node, 'a'))
162
163 def test_int_default(self):
164 """Check XmlInt default handling."""
165 node = self._get_node('<node/>')
166 self.assertIsNone(manifest_xml.XmlInt(node, 'a'))
167 self.assertIsNone(manifest_xml.XmlInt(node, 'a', None))
168 self.assertEqual(123, manifest_xml.XmlInt(node, 'a', 123))
169
170 node = self._get_node('<node a=""/>')
171 self.assertIsNone(manifest_xml.XmlInt(node, 'a'))
172
173 def test_int_good(self):
174 """Check XmlInt numeric handling."""
175 for value in (-1, 0, 1, 50000):
176 node = self._get_node('<node a="%s"/>' % (value,))
177 self.assertEqual(value, manifest_xml.XmlInt(node, 'a'))
178
179 def test_int_invalid(self):
180 """Check XmlInt invalid handling."""
181 with self.assertRaises(error.ManifestParseError):
182 node = self._get_node('<node a="xx"/>')
183 manifest_xml.XmlInt(node, 'a')
Mike Frysinger8c1e9cb2020-09-06 14:53:18 -0400184
185
Mike Frysinger37ac3d62021-02-25 04:54:56 -0500186class XmlManifestTests(ManifestParseTestCase):
Mike Frysinger8c1e9cb2020-09-06 14:53:18 -0400187 """Check manifest processing."""
188
Mike Frysinger8c1e9cb2020-09-06 14:53:18 -0400189 def test_empty(self):
190 """Parse an 'empty' manifest file."""
191 manifest = self.getXmlManifest(
192 '<?xml version="1.0" encoding="UTF-8"?>'
193 '<manifest></manifest>')
194 self.assertEqual(manifest.remotes, {})
195 self.assertEqual(manifest.projects, [])
196
197 def test_link(self):
198 """Verify Link handling with new names."""
199 manifest = manifest_xml.XmlManifest(self.repodir, self.manifest_file)
200 with open(os.path.join(self.manifest_dir, 'foo.xml'), 'w') as fp:
201 fp.write('<manifest></manifest>')
202 manifest.Link('foo.xml')
203 with open(self.manifest_file) as fp:
204 self.assertIn('<include name="foo.xml" />', fp.read())
205
206 def test_toxml_empty(self):
207 """Verify the ToXml() helper."""
208 manifest = self.getXmlManifest(
209 '<?xml version="1.0" encoding="UTF-8"?>'
210 '<manifest></manifest>')
211 self.assertEqual(manifest.ToXml().toxml(), '<?xml version="1.0" ?><manifest/>')
212
213 def test_todict_empty(self):
214 """Verify the ToDict() helper."""
215 manifest = self.getXmlManifest(
216 '<?xml version="1.0" encoding="UTF-8"?>'
217 '<manifest></manifest>')
218 self.assertEqual(manifest.ToDict(), {})
219
Mike Frysinger51e39d52020-12-04 05:32:06 -0500220 def test_repo_hooks(self):
221 """Check repo-hooks settings."""
222 manifest = self.getXmlManifest("""
223<manifest>
224 <remote name="test-remote" fetch="http://localhost" />
225 <default remote="test-remote" revision="refs/heads/main" />
226 <project name="repohooks" path="src/repohooks"/>
227 <repo-hooks in-project="repohooks" enabled-list="a, b"/>
228</manifest>
229""")
230 self.assertEqual(manifest.repo_hooks_project.name, 'repohooks')
231 self.assertEqual(manifest.repo_hooks_project.enabled_repo_hooks, ['a', 'b'])
232
Raman Tenneti48b2d102021-01-11 12:18:47 -0800233 def test_unknown_tags(self):
234 """Check superproject settings."""
235 manifest = self.getXmlManifest("""
236<manifest>
237 <remote name="test-remote" fetch="http://localhost" />
238 <default remote="test-remote" revision="refs/heads/main" />
239 <superproject name="superproject"/>
240 <iankaz value="unknown (possible) future tags are ignored"/>
241 <x-custom-tag>X tags are always ignored</x-custom-tag>
242</manifest>
243""")
244 self.assertEqual(manifest.superproject['name'], 'superproject')
245 self.assertEqual(manifest.superproject['remote'].name, 'test-remote')
246 self.assertEqual(
247 manifest.ToXml().toxml(),
248 '<?xml version="1.0" ?><manifest>' +
249 '<remote name="test-remote" fetch="http://localhost"/>' +
250 '<default remote="test-remote" revision="refs/heads/main"/>' +
251 '<superproject name="superproject"/>' +
252 '</manifest>')
253
Fredrik de Groot352c93b2020-10-06 12:55:14 +0200254
Mike Frysingera29424e2021-02-25 21:53:49 -0500255class IncludeElementTests(ManifestParseTestCase):
256 """Tests for <include>."""
Raman Tennetib5c5a5e2021-02-06 09:44:15 -0800257
Mike Frysingera29424e2021-02-25 21:53:49 -0500258 def test_group_levels(self):
Fredrik de Groot352c93b2020-10-06 12:55:14 +0200259 root_m = os.path.join(self.manifest_dir, 'root.xml')
260 with open(root_m, 'w') as fp:
261 fp.write("""
262<manifest>
263 <remote name="test-remote" fetch="http://localhost" />
264 <default remote="test-remote" revision="refs/heads/main" />
265 <include name="level1.xml" groups="level1-group" />
266 <project name="root-name1" path="root-path1" />
267 <project name="root-name2" path="root-path2" groups="r2g1,r2g2" />
268</manifest>
269""")
270 with open(os.path.join(self.manifest_dir, 'level1.xml'), 'w') as fp:
271 fp.write("""
272<manifest>
273 <include name="level2.xml" groups="level2-group" />
274 <project name="level1-name1" path="level1-path1" />
275</manifest>
276""")
277 with open(os.path.join(self.manifest_dir, 'level2.xml'), 'w') as fp:
278 fp.write("""
279<manifest>
280 <project name="level2-name1" path="level2-path1" groups="l2g1,l2g2" />
281</manifest>
282""")
283 include_m = manifest_xml.XmlManifest(self.repodir, root_m)
284 for proj in include_m.projects:
285 if proj.name == 'root-name1':
286 # Check include group not set on root level proj.
287 self.assertNotIn('level1-group', proj.groups)
288 if proj.name == 'root-name2':
289 # Check root proj group not removed.
290 self.assertIn('r2g1', proj.groups)
291 if proj.name == 'level1-name1':
292 # Check level1 proj has inherited group level 1.
293 self.assertIn('level1-group', proj.groups)
294 if proj.name == 'level2-name1':
295 # Check level2 proj has inherited group levels 1 and 2.
296 self.assertIn('level1-group', proj.groups)
297 self.assertIn('level2-group', proj.groups)
298 # Check level2 proj group not removed.
299 self.assertIn('l2g1', proj.groups)
Mike Frysinger37ac3d62021-02-25 04:54:56 -0500300
Mike Frysingera29424e2021-02-25 21:53:49 -0500301 def test_bad_name_checks(self):
302 """Check handling of bad name attribute."""
303 def parse(name):
304 manifest = self.getXmlManifest(f"""
305<manifest>
306 <remote name="default-remote" fetch="http://localhost" />
307 <default remote="default-remote" revision="refs/heads/main" />
308 <include name="{name}" />
309</manifest>
310""")
311 # Force the manifest to be parsed.
312 manifest.ToXml()
Mike Frysinger37ac3d62021-02-25 04:54:56 -0500313
Mike Frysingera29424e2021-02-25 21:53:49 -0500314 # Handle empty name explicitly because a different codepath rejects it.
315 with self.assertRaises(error.ManifestParseError):
316 parse('')
317
318 for path in INVALID_FS_PATHS:
319 if not path:
320 continue
321
322 with self.assertRaises(error.ManifestInvalidPathError):
323 parse(path)
324
325
326class ProjectElementTests(ManifestParseTestCase):
327 """Tests for <project>."""
328
329 def test_group(self):
330 """Check project group settings."""
331 manifest = self.getXmlManifest("""
332<manifest>
333 <remote name="test-remote" fetch="http://localhost" />
334 <default remote="test-remote" revision="refs/heads/main" />
335 <project name="test-name" path="test-path"/>
336 <project name="extras" path="path" groups="g1,g2,g1"/>
337</manifest>
338""")
339 self.assertEqual(len(manifest.projects), 2)
340 # Ordering isn't guaranteed.
341 result = {
342 manifest.projects[0].name: manifest.projects[0].groups,
343 manifest.projects[1].name: manifest.projects[1].groups,
344 }
345 project = manifest.projects[0]
346 self.assertCountEqual(
347 result['test-name'],
348 ['name:test-name', 'all', 'path:test-path'])
349 self.assertCountEqual(
350 result['extras'],
351 ['g1', 'g2', 'g1', 'name:extras', 'all', 'path:path'])
352
353 def test_set_revision_id(self):
354 """Check setting of project's revisionId."""
355 manifest = self.getXmlManifest("""
356<manifest>
357 <remote name="default-remote" fetch="http://localhost" />
358 <default remote="default-remote" revision="refs/heads/main" />
359 <project name="test-name"/>
360</manifest>
361""")
362 self.assertEqual(len(manifest.projects), 1)
363 project = manifest.projects[0]
364 project.SetRevisionId('ABCDEF')
365 self.assertEqual(
366 manifest.ToXml().toxml(),
367 '<?xml version="1.0" ?><manifest>' +
368 '<remote name="default-remote" fetch="http://localhost"/>' +
369 '<default remote="default-remote" revision="refs/heads/main"/>' +
370 '<project name="test-name" revision="ABCDEF"/>' +
371 '</manifest>')
372
373 def test_trailing_slash(self):
374 """Check handling of trailing slashes in attributes."""
375 def parse(name, path):
376 return self.getXmlManifest(f"""
377<manifest>
378 <remote name="default-remote" fetch="http://localhost" />
379 <default remote="default-remote" revision="refs/heads/main" />
380 <project name="{name}" path="{path}" />
381</manifest>
382""")
383
384 manifest = parse('a/path/', 'foo')
385 self.assertEqual(manifest.projects[0].gitdir,
386 os.path.join(self.tempdir, '.repo/projects/foo.git'))
387 self.assertEqual(manifest.projects[0].objdir,
388 os.path.join(self.tempdir, '.repo/project-objects/a/path.git'))
389
390 manifest = parse('a/path', 'foo/')
391 self.assertEqual(manifest.projects[0].gitdir,
392 os.path.join(self.tempdir, '.repo/projects/foo.git'))
393 self.assertEqual(manifest.projects[0].objdir,
394 os.path.join(self.tempdir, '.repo/project-objects/a/path.git'))
395
396 def test_bad_path_name_checks(self):
397 """Check handling of bad path & name attributes."""
398 def parse(name, path):
399 manifest = self.getXmlManifest(f"""
400<manifest>
401 <remote name="default-remote" fetch="http://localhost" />
402 <default remote="default-remote" revision="refs/heads/main" />
403 <project name="{name}" path="{path}" />
404</manifest>
405""")
406 # Force the manifest to be parsed.
407 manifest.ToXml()
408
409 # Verify the parser is valid by default to avoid buggy tests below.
410 parse('ok', 'ok')
411
412 # Handle empty name explicitly because a different codepath rejects it.
413 # Empty path is OK because it defaults to the name field.
414 with self.assertRaises(error.ManifestParseError):
415 parse('', 'ok')
416
417 for path in INVALID_FS_PATHS:
418 if not path or path.endswith('/'):
419 continue
420
421 with self.assertRaises(error.ManifestInvalidPathError):
422 parse(path, 'ok')
423 with self.assertRaises(error.ManifestInvalidPathError):
424 parse('ok', path)
425
426
427class SuperProjectElementTests(ManifestParseTestCase):
Mike Frysinger37ac3d62021-02-25 04:54:56 -0500428 """Tests for <superproject>."""
429
430 def test_superproject(self):
431 """Check superproject settings."""
432 manifest = self.getXmlManifest("""
433<manifest>
434 <remote name="test-remote" fetch="http://localhost" />
435 <default remote="test-remote" revision="refs/heads/main" />
436 <superproject name="superproject"/>
437</manifest>
438""")
439 self.assertEqual(manifest.superproject['name'], 'superproject')
440 self.assertEqual(manifest.superproject['remote'].name, 'test-remote')
441 self.assertEqual(manifest.superproject['remote'].url, 'http://localhost/superproject')
442 self.assertEqual(
443 manifest.ToXml().toxml(),
444 '<?xml version="1.0" ?><manifest>' +
445 '<remote name="test-remote" fetch="http://localhost"/>' +
446 '<default remote="test-remote" revision="refs/heads/main"/>' +
447 '<superproject name="superproject"/>' +
448 '</manifest>')
449
450 def test_remote(self):
451 """Check superproject settings with a remote."""
452 manifest = self.getXmlManifest("""
453<manifest>
454 <remote name="default-remote" fetch="http://localhost" />
455 <remote name="superproject-remote" fetch="http://localhost" />
456 <default remote="default-remote" revision="refs/heads/main" />
457 <superproject name="platform/superproject" remote="superproject-remote"/>
458</manifest>
459""")
460 self.assertEqual(manifest.superproject['name'], 'platform/superproject')
461 self.assertEqual(manifest.superproject['remote'].name, 'superproject-remote')
462 self.assertEqual(manifest.superproject['remote'].url, 'http://localhost/platform/superproject')
463 self.assertEqual(
464 manifest.ToXml().toxml(),
465 '<?xml version="1.0" ?><manifest>' +
466 '<remote name="default-remote" fetch="http://localhost"/>' +
467 '<remote name="superproject-remote" fetch="http://localhost"/>' +
468 '<default remote="default-remote" revision="refs/heads/main"/>' +
469 '<superproject name="platform/superproject" remote="superproject-remote"/>' +
470 '</manifest>')
471
472 def test_defalut_remote(self):
473 """Check superproject settings with a default remote."""
474 manifest = self.getXmlManifest("""
475<manifest>
476 <remote name="default-remote" fetch="http://localhost" />
477 <default remote="default-remote" revision="refs/heads/main" />
478 <superproject name="superproject" remote="default-remote"/>
479</manifest>
480""")
481 self.assertEqual(manifest.superproject['name'], 'superproject')
482 self.assertEqual(manifest.superproject['remote'].name, 'default-remote')
483 self.assertEqual(
484 manifest.ToXml().toxml(),
485 '<?xml version="1.0" ?><manifest>' +
486 '<remote name="default-remote" fetch="http://localhost"/>' +
487 '<default remote="default-remote" revision="refs/heads/main"/>' +
488 '<superproject name="superproject"/>' +
489 '</manifest>')