diff options
Diffstat (limited to 'tests/test_manifest_xml.py')
-rw-r--r-- | tests/test_manifest_xml.py | 845 |
1 files changed, 845 insertions, 0 deletions
diff --git a/tests/test_manifest_xml.py b/tests/test_manifest_xml.py new file mode 100644 index 00000000..cb3eb855 --- /dev/null +++ b/tests/test_manifest_xml.py | |||
@@ -0,0 +1,845 @@ | |||
1 | # 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 | |||
17 | import os | ||
18 | import platform | ||
19 | import re | ||
20 | import shutil | ||
21 | import tempfile | ||
22 | import unittest | ||
23 | import xml.dom.minidom | ||
24 | |||
25 | import error | ||
26 | import manifest_xml | ||
27 | |||
28 | |||
29 | # Invalid paths that we don't want in the filesystem. | ||
30 | INVALID_FS_PATHS = ( | ||
31 | '', | ||
32 | '.', | ||
33 | '..', | ||
34 | '../', | ||
35 | './', | ||
36 | './/', | ||
37 | 'foo/', | ||
38 | './foo', | ||
39 | '../foo', | ||
40 | 'foo/./bar', | ||
41 | 'foo/../../bar', | ||
42 | '/foo', | ||
43 | './../foo', | ||
44 | '.git/foo', | ||
45 | # Check case folding. | ||
46 | '.GIT/foo', | ||
47 | 'blah/.git/foo', | ||
48 | '.repo/foo', | ||
49 | '.repoconfig', | ||
50 | # Block ~ due to 8.3 filenames on Windows filesystems. | ||
51 | '~', | ||
52 | 'foo~', | ||
53 | 'blah/foo~', | ||
54 | # Block Unicode characters that get normalized out by filesystems. | ||
55 | u'foo\u200Cbar', | ||
56 | # Block newlines. | ||
57 | 'f\n/bar', | ||
58 | 'f\r/bar', | ||
59 | ) | ||
60 | |||
61 | # Make sure platforms that use path separators (e.g. Windows) are also | ||
62 | # rejected properly. | ||
63 | if os.path.sep != '/': | ||
64 | INVALID_FS_PATHS += tuple(x.replace('/', os.path.sep) for x in INVALID_FS_PATHS) | ||
65 | |||
66 | |||
67 | def sort_attributes(manifest): | ||
68 | """Sort the attributes of all elements alphabetically. | ||
69 | |||
70 | This is needed because different versions of the toxml() function from | ||
71 | xml.dom.minidom outputs the attributes of elements in different orders. | ||
72 | Before Python 3.8 they were output alphabetically, later versions preserve | ||
73 | the order specified by the user. | ||
74 | |||
75 | Args: | ||
76 | manifest: String containing an XML manifest. | ||
77 | |||
78 | Returns: | ||
79 | The XML manifest with the attributes of all elements sorted alphabetically. | ||
80 | """ | ||
81 | new_manifest = '' | ||
82 | # This will find every element in the XML manifest, whether they have | ||
83 | # attributes or not. This simplifies recreating the manifest below. | ||
84 | matches = re.findall(r'(<[/?]?[a-z-]+\s*)((?:\S+?="[^"]+"\s*?)*)(\s*[/?]?>)', manifest) | ||
85 | for head, attrs, tail in matches: | ||
86 | m = re.findall(r'\S+?="[^"]+"', attrs) | ||
87 | new_manifest += head + ' '.join(sorted(m)) + tail | ||
88 | return new_manifest | ||
89 | |||
90 | |||
91 | class ManifestParseTestCase(unittest.TestCase): | ||
92 | """TestCase for parsing manifests.""" | ||
93 | |||
94 | def setUp(self): | ||
95 | self.tempdir = tempfile.mkdtemp(prefix='repo_tests') | ||
96 | self.repodir = os.path.join(self.tempdir, '.repo') | ||
97 | self.manifest_dir = os.path.join(self.repodir, 'manifests') | ||
98 | self.manifest_file = os.path.join( | ||
99 | self.repodir, manifest_xml.MANIFEST_FILE_NAME) | ||
100 | self.local_manifest_dir = os.path.join( | ||
101 | self.repodir, manifest_xml.LOCAL_MANIFESTS_DIR_NAME) | ||
102 | os.mkdir(self.repodir) | ||
103 | os.mkdir(self.manifest_dir) | ||
104 | |||
105 | # The manifest parsing really wants a git repo currently. | ||
106 | gitdir = os.path.join(self.repodir, 'manifests.git') | ||
107 | os.mkdir(gitdir) | ||
108 | with open(os.path.join(gitdir, 'config'), 'w') as fp: | ||
109 | fp.write("""[remote "origin"] | ||
110 | url = https://localhost:0/manifest | ||
111 | """) | ||
112 | |||
113 | def tearDown(self): | ||
114 | shutil.rmtree(self.tempdir, ignore_errors=True) | ||
115 | |||
116 | def getXmlManifest(self, data): | ||
117 | """Helper to initialize a manifest for testing.""" | ||
118 | with open(self.manifest_file, 'w') as fp: | ||
119 | fp.write(data) | ||
120 | return manifest_xml.XmlManifest(self.repodir, self.manifest_file) | ||
121 | |||
122 | @staticmethod | ||
123 | def encodeXmlAttr(attr): | ||
124 | """Encode |attr| using XML escape rules.""" | ||
125 | return attr.replace('\r', '
').replace('\n', '
') | ||
126 | |||
127 | |||
128 | class ManifestValidateFilePaths(unittest.TestCase): | ||
129 | """Check _ValidateFilePaths helper. | ||
130 | |||
131 | This doesn't access a real filesystem. | ||
132 | """ | ||
133 | |||
134 | def check_both(self, *args): | ||
135 | manifest_xml.XmlManifest._ValidateFilePaths('copyfile', *args) | ||
136 | manifest_xml.XmlManifest._ValidateFilePaths('linkfile', *args) | ||
137 | |||
138 | def test_normal_path(self): | ||
139 | """Make sure good paths are accepted.""" | ||
140 | self.check_both('foo', 'bar') | ||
141 | self.check_both('foo/bar', 'bar') | ||
142 | self.check_both('foo', 'bar/bar') | ||
143 | self.check_both('foo/bar', 'bar/bar') | ||
144 | |||
145 | def test_symlink_targets(self): | ||
146 | """Some extra checks for symlinks.""" | ||
147 | def check(*args): | ||
148 | manifest_xml.XmlManifest._ValidateFilePaths('linkfile', *args) | ||
149 | |||
150 | # We allow symlinks to end in a slash since we allow them to point to dirs | ||
151 | # in general. Technically the slash isn't necessary. | ||
152 | check('foo/', 'bar') | ||
153 | # We allow a single '.' to get a reference to the project itself. | ||
154 | check('.', 'bar') | ||
155 | |||
156 | def test_bad_paths(self): | ||
157 | """Make sure bad paths (src & dest) are rejected.""" | ||
158 | for path in INVALID_FS_PATHS: | ||
159 | self.assertRaises( | ||
160 | error.ManifestInvalidPathError, self.check_both, path, 'a') | ||
161 | self.assertRaises( | ||
162 | error.ManifestInvalidPathError, self.check_both, 'a', path) | ||
163 | |||
164 | |||
165 | class ValueTests(unittest.TestCase): | ||
166 | """Check utility parsing code.""" | ||
167 | |||
168 | def _get_node(self, text): | ||
169 | return xml.dom.minidom.parseString(text).firstChild | ||
170 | |||
171 | def test_bool_default(self): | ||
172 | """Check XmlBool default handling.""" | ||
173 | node = self._get_node('<node/>') | ||
174 | self.assertIsNone(manifest_xml.XmlBool(node, 'a')) | ||
175 | self.assertIsNone(manifest_xml.XmlBool(node, 'a', None)) | ||
176 | self.assertEqual(123, manifest_xml.XmlBool(node, 'a', 123)) | ||
177 | |||
178 | node = self._get_node('<node a=""/>') | ||
179 | self.assertIsNone(manifest_xml.XmlBool(node, 'a')) | ||
180 | |||
181 | def test_bool_invalid(self): | ||
182 | """Check XmlBool invalid handling.""" | ||
183 | node = self._get_node('<node a="moo"/>') | ||
184 | self.assertEqual(123, manifest_xml.XmlBool(node, 'a', 123)) | ||
185 | |||
186 | def test_bool_true(self): | ||
187 | """Check XmlBool true values.""" | ||
188 | for value in ('yes', 'true', '1'): | ||
189 | node = self._get_node('<node a="%s"/>' % (value,)) | ||
190 | self.assertTrue(manifest_xml.XmlBool(node, 'a')) | ||
191 | |||
192 | def test_bool_false(self): | ||
193 | """Check XmlBool false values.""" | ||
194 | for value in ('no', 'false', '0'): | ||
195 | node = self._get_node('<node a="%s"/>' % (value,)) | ||
196 | self.assertFalse(manifest_xml.XmlBool(node, 'a')) | ||
197 | |||
198 | def test_int_default(self): | ||
199 | """Check XmlInt default handling.""" | ||
200 | node = self._get_node('<node/>') | ||
201 | self.assertIsNone(manifest_xml.XmlInt(node, 'a')) | ||
202 | self.assertIsNone(manifest_xml.XmlInt(node, 'a', None)) | ||
203 | self.assertEqual(123, manifest_xml.XmlInt(node, 'a', 123)) | ||
204 | |||
205 | node = self._get_node('<node a=""/>') | ||
206 | self.assertIsNone(manifest_xml.XmlInt(node, 'a')) | ||
207 | |||
208 | def test_int_good(self): | ||
209 | """Check XmlInt numeric handling.""" | ||
210 | for value in (-1, 0, 1, 50000): | ||
211 | node = self._get_node('<node a="%s"/>' % (value,)) | ||
212 | self.assertEqual(value, manifest_xml.XmlInt(node, 'a')) | ||
213 | |||
214 | def test_int_invalid(self): | ||
215 | """Check XmlInt invalid handling.""" | ||
216 | with self.assertRaises(error.ManifestParseError): | ||
217 | node = self._get_node('<node a="xx"/>') | ||
218 | manifest_xml.XmlInt(node, 'a') | ||
219 | |||
220 | |||
221 | class XmlManifestTests(ManifestParseTestCase): | ||
222 | """Check manifest processing.""" | ||
223 | |||
224 | def test_empty(self): | ||
225 | """Parse an 'empty' manifest file.""" | ||
226 | manifest = self.getXmlManifest( | ||
227 | '<?xml version="1.0" encoding="UTF-8"?>' | ||
228 | '<manifest></manifest>') | ||
229 | self.assertEqual(manifest.remotes, {}) | ||
230 | self.assertEqual(manifest.projects, []) | ||
231 | |||
232 | def test_link(self): | ||
233 | """Verify Link handling with new names.""" | ||
234 | manifest = manifest_xml.XmlManifest(self.repodir, self.manifest_file) | ||
235 | with open(os.path.join(self.manifest_dir, 'foo.xml'), 'w') as fp: | ||
236 | fp.write('<manifest></manifest>') | ||
237 | manifest.Link('foo.xml') | ||
238 | with open(self.manifest_file) as fp: | ||
239 | self.assertIn('<include name="foo.xml" />', fp.read()) | ||
240 | |||
241 | def test_toxml_empty(self): | ||
242 | """Verify the ToXml() helper.""" | ||
243 | manifest = self.getXmlManifest( | ||
244 | '<?xml version="1.0" encoding="UTF-8"?>' | ||
245 | '<manifest></manifest>') | ||
246 | self.assertEqual(manifest.ToXml().toxml(), '<?xml version="1.0" ?><manifest/>') | ||
247 | |||
248 | def test_todict_empty(self): | ||
249 | """Verify the ToDict() helper.""" | ||
250 | manifest = self.getXmlManifest( | ||
251 | '<?xml version="1.0" encoding="UTF-8"?>' | ||
252 | '<manifest></manifest>') | ||
253 | self.assertEqual(manifest.ToDict(), {}) | ||
254 | |||
255 | def test_repo_hooks(self): | ||
256 | """Check repo-hooks settings.""" | ||
257 | manifest = self.getXmlManifest(""" | ||
258 | <manifest> | ||
259 | <remote name="test-remote" fetch="http://localhost" /> | ||
260 | <default remote="test-remote" revision="refs/heads/main" /> | ||
261 | <project name="repohooks" path="src/repohooks"/> | ||
262 | <repo-hooks in-project="repohooks" enabled-list="a, b"/> | ||
263 | </manifest> | ||
264 | """) | ||
265 | self.assertEqual(manifest.repo_hooks_project.name, 'repohooks') | ||
266 | self.assertEqual(manifest.repo_hooks_project.enabled_repo_hooks, ['a', 'b']) | ||
267 | |||
268 | def test_repo_hooks_unordered(self): | ||
269 | """Check repo-hooks settings work even if the project def comes second.""" | ||
270 | manifest = self.getXmlManifest(""" | ||
271 | <manifest> | ||
272 | <remote name="test-remote" fetch="http://localhost" /> | ||
273 | <default remote="test-remote" revision="refs/heads/main" /> | ||
274 | <repo-hooks in-project="repohooks" enabled-list="a, b"/> | ||
275 | <project name="repohooks" path="src/repohooks"/> | ||
276 | </manifest> | ||
277 | """) | ||
278 | self.assertEqual(manifest.repo_hooks_project.name, 'repohooks') | ||
279 | self.assertEqual(manifest.repo_hooks_project.enabled_repo_hooks, ['a', 'b']) | ||
280 | |||
281 | def test_unknown_tags(self): | ||
282 | """Check superproject settings.""" | ||
283 | manifest = self.getXmlManifest(""" | ||
284 | <manifest> | ||
285 | <remote name="test-remote" fetch="http://localhost" /> | ||
286 | <default remote="test-remote" revision="refs/heads/main" /> | ||
287 | <superproject name="superproject"/> | ||
288 | <iankaz value="unknown (possible) future tags are ignored"/> | ||
289 | <x-custom-tag>X tags are always ignored</x-custom-tag> | ||
290 | </manifest> | ||
291 | """) | ||
292 | self.assertEqual(manifest.superproject['name'], 'superproject') | ||
293 | self.assertEqual(manifest.superproject['remote'].name, 'test-remote') | ||
294 | self.assertEqual( | ||
295 | sort_attributes(manifest.ToXml().toxml()), | ||
296 | '<?xml version="1.0" ?><manifest>' | ||
297 | '<remote fetch="http://localhost" name="test-remote"/>' | ||
298 | '<default remote="test-remote" revision="refs/heads/main"/>' | ||
299 | '<superproject name="superproject"/>' | ||
300 | '</manifest>') | ||
301 | |||
302 | def test_remote_annotations(self): | ||
303 | """Check remote settings.""" | ||
304 | manifest = self.getXmlManifest(""" | ||
305 | <manifest> | ||
306 | <remote name="test-remote" fetch="http://localhost"> | ||
307 | <annotation name="foo" value="bar"/> | ||
308 | </remote> | ||
309 | </manifest> | ||
310 | """) | ||
311 | self.assertEqual(manifest.remotes['test-remote'].annotations[0].name, 'foo') | ||
312 | self.assertEqual(manifest.remotes['test-remote'].annotations[0].value, 'bar') | ||
313 | self.assertEqual( | ||
314 | sort_attributes(manifest.ToXml().toxml()), | ||
315 | '<?xml version="1.0" ?><manifest>' | ||
316 | '<remote fetch="http://localhost" name="test-remote">' | ||
317 | '<annotation name="foo" value="bar"/>' | ||
318 | '</remote>' | ||
319 | '</manifest>') | ||
320 | |||
321 | |||
322 | class IncludeElementTests(ManifestParseTestCase): | ||
323 | """Tests for <include>.""" | ||
324 | |||
325 | def test_group_levels(self): | ||
326 | root_m = os.path.join(self.manifest_dir, 'root.xml') | ||
327 | with open(root_m, 'w') as fp: | ||
328 | fp.write(""" | ||
329 | <manifest> | ||
330 | <remote name="test-remote" fetch="http://localhost" /> | ||
331 | <default remote="test-remote" revision="refs/heads/main" /> | ||
332 | <include name="level1.xml" groups="level1-group" /> | ||
333 | <project name="root-name1" path="root-path1" /> | ||
334 | <project name="root-name2" path="root-path2" groups="r2g1,r2g2" /> | ||
335 | </manifest> | ||
336 | """) | ||
337 | with open(os.path.join(self.manifest_dir, 'level1.xml'), 'w') as fp: | ||
338 | fp.write(""" | ||
339 | <manifest> | ||
340 | <include name="level2.xml" groups="level2-group" /> | ||
341 | <project name="level1-name1" path="level1-path1" /> | ||
342 | </manifest> | ||
343 | """) | ||
344 | with open(os.path.join(self.manifest_dir, 'level2.xml'), 'w') as fp: | ||
345 | fp.write(""" | ||
346 | <manifest> | ||
347 | <project name="level2-name1" path="level2-path1" groups="l2g1,l2g2" /> | ||
348 | </manifest> | ||
349 | """) | ||
350 | include_m = manifest_xml.XmlManifest(self.repodir, root_m) | ||
351 | for proj in include_m.projects: | ||
352 | if proj.name == 'root-name1': | ||
353 | # Check include group not set on root level proj. | ||
354 | self.assertNotIn('level1-group', proj.groups) | ||
355 | if proj.name == 'root-name2': | ||
356 | # Check root proj group not removed. | ||
357 | self.assertIn('r2g1', proj.groups) | ||
358 | if proj.name == 'level1-name1': | ||
359 | # Check level1 proj has inherited group level 1. | ||
360 | self.assertIn('level1-group', proj.groups) | ||
361 | if proj.name == 'level2-name1': | ||
362 | # Check level2 proj has inherited group levels 1 and 2. | ||
363 | self.assertIn('level1-group', proj.groups) | ||
364 | self.assertIn('level2-group', proj.groups) | ||
365 | # Check level2 proj group not removed. | ||
366 | self.assertIn('l2g1', proj.groups) | ||
367 | |||
368 | def test_allow_bad_name_from_user(self): | ||
369 | """Check handling of bad name attribute from the user's input.""" | ||
370 | def parse(name): | ||
371 | name = self.encodeXmlAttr(name) | ||
372 | manifest = self.getXmlManifest(f""" | ||
373 | <manifest> | ||
374 | <remote name="default-remote" fetch="http://localhost" /> | ||
375 | <default remote="default-remote" revision="refs/heads/main" /> | ||
376 | <include name="{name}" /> | ||
377 | </manifest> | ||
378 | """) | ||
379 | # Force the manifest to be parsed. | ||
380 | manifest.ToXml() | ||
381 | |||
382 | # Setup target of the include. | ||
383 | target = os.path.join(self.tempdir, 'target.xml') | ||
384 | with open(target, 'w') as fp: | ||
385 | fp.write('<manifest></manifest>') | ||
386 | |||
387 | # Include with absolute path. | ||
388 | parse(os.path.abspath(target)) | ||
389 | |||
390 | # Include with relative path. | ||
391 | parse(os.path.relpath(target, self.manifest_dir)) | ||
392 | |||
393 | def test_bad_name_checks(self): | ||
394 | """Check handling of bad name attribute.""" | ||
395 | def parse(name): | ||
396 | name = self.encodeXmlAttr(name) | ||
397 | # Setup target of the include. | ||
398 | with open(os.path.join(self.manifest_dir, 'target.xml'), 'w') as fp: | ||
399 | fp.write(f'<manifest><include name="{name}"/></manifest>') | ||
400 | |||
401 | manifest = self.getXmlManifest(""" | ||
402 | <manifest> | ||
403 | <remote name="default-remote" fetch="http://localhost" /> | ||
404 | <default remote="default-remote" revision="refs/heads/main" /> | ||
405 | <include name="target.xml" /> | ||
406 | </manifest> | ||
407 | """) | ||
408 | # Force the manifest to be parsed. | ||
409 | manifest.ToXml() | ||
410 | |||
411 | # Handle empty name explicitly because a different codepath rejects it. | ||
412 | with self.assertRaises(error.ManifestParseError): | ||
413 | parse('') | ||
414 | |||
415 | for path in INVALID_FS_PATHS: | ||
416 | if not path: | ||
417 | continue | ||
418 | |||
419 | with self.assertRaises(error.ManifestInvalidPathError): | ||
420 | parse(path) | ||
421 | |||
422 | |||
423 | class ProjectElementTests(ManifestParseTestCase): | ||
424 | """Tests for <project>.""" | ||
425 | |||
426 | def test_group(self): | ||
427 | """Check project group settings.""" | ||
428 | manifest = self.getXmlManifest(""" | ||
429 | <manifest> | ||
430 | <remote name="test-remote" fetch="http://localhost" /> | ||
431 | <default remote="test-remote" revision="refs/heads/main" /> | ||
432 | <project name="test-name" path="test-path"/> | ||
433 | <project name="extras" path="path" groups="g1,g2,g1"/> | ||
434 | </manifest> | ||
435 | """) | ||
436 | self.assertEqual(len(manifest.projects), 2) | ||
437 | # Ordering isn't guaranteed. | ||
438 | result = { | ||
439 | manifest.projects[0].name: manifest.projects[0].groups, | ||
440 | manifest.projects[1].name: manifest.projects[1].groups, | ||
441 | } | ||
442 | project = manifest.projects[0] | ||
443 | self.assertCountEqual( | ||
444 | result['test-name'], | ||
445 | ['name:test-name', 'all', 'path:test-path']) | ||
446 | self.assertCountEqual( | ||
447 | result['extras'], | ||
448 | ['g1', 'g2', 'g1', 'name:extras', 'all', 'path:path']) | ||
449 | groupstr = 'default,platform-' + platform.system().lower() | ||
450 | self.assertEqual(groupstr, manifest.GetGroupsStr()) | ||
451 | groupstr = 'g1,g2,g1' | ||
452 | manifest.manifestProject.config.SetString('manifest.groups', groupstr) | ||
453 | self.assertEqual(groupstr, manifest.GetGroupsStr()) | ||
454 | |||
455 | def test_set_revision_id(self): | ||
456 | """Check setting of project's revisionId.""" | ||
457 | manifest = self.getXmlManifest(""" | ||
458 | <manifest> | ||
459 | <remote name="default-remote" fetch="http://localhost" /> | ||
460 | <default remote="default-remote" revision="refs/heads/main" /> | ||
461 | <project name="test-name"/> | ||
462 | </manifest> | ||
463 | """) | ||
464 | self.assertEqual(len(manifest.projects), 1) | ||
465 | project = manifest.projects[0] | ||
466 | project.SetRevisionId('ABCDEF') | ||
467 | self.assertEqual( | ||
468 | sort_attributes(manifest.ToXml().toxml()), | ||
469 | '<?xml version="1.0" ?><manifest>' | ||
470 | '<remote fetch="http://localhost" name="default-remote"/>' | ||
471 | '<default remote="default-remote" revision="refs/heads/main"/>' | ||
472 | '<project name="test-name" revision="ABCDEF" upstream="refs/heads/main"/>' | ||
473 | '</manifest>') | ||
474 | |||
475 | def test_trailing_slash(self): | ||
476 | """Check handling of trailing slashes in attributes.""" | ||
477 | def parse(name, path): | ||
478 | name = self.encodeXmlAttr(name) | ||
479 | path = self.encodeXmlAttr(path) | ||
480 | return self.getXmlManifest(f""" | ||
481 | <manifest> | ||
482 | <remote name="default-remote" fetch="http://localhost" /> | ||
483 | <default remote="default-remote" revision="refs/heads/main" /> | ||
484 | <project name="{name}" path="{path}" /> | ||
485 | </manifest> | ||
486 | """) | ||
487 | |||
488 | manifest = parse('a/path/', 'foo') | ||
489 | self.assertEqual(manifest.projects[0].gitdir, | ||
490 | os.path.join(self.tempdir, '.repo/projects/foo.git')) | ||
491 | self.assertEqual(manifest.projects[0].objdir, | ||
492 | os.path.join(self.tempdir, '.repo/project-objects/a/path.git')) | ||
493 | |||
494 | manifest = parse('a/path', 'foo/') | ||
495 | self.assertEqual(manifest.projects[0].gitdir, | ||
496 | os.path.join(self.tempdir, '.repo/projects/foo.git')) | ||
497 | self.assertEqual(manifest.projects[0].objdir, | ||
498 | os.path.join(self.tempdir, '.repo/project-objects/a/path.git')) | ||
499 | |||
500 | manifest = parse('a/path', 'foo//////') | ||
501 | self.assertEqual(manifest.projects[0].gitdir, | ||
502 | os.path.join(self.tempdir, '.repo/projects/foo.git')) | ||
503 | self.assertEqual(manifest.projects[0].objdir, | ||
504 | os.path.join(self.tempdir, '.repo/project-objects/a/path.git')) | ||
505 | |||
506 | def test_toplevel_path(self): | ||
507 | """Check handling of path=. specially.""" | ||
508 | def parse(name, path): | ||
509 | name = self.encodeXmlAttr(name) | ||
510 | path = self.encodeXmlAttr(path) | ||
511 | return self.getXmlManifest(f""" | ||
512 | <manifest> | ||
513 | <remote name="default-remote" fetch="http://localhost" /> | ||
514 | <default remote="default-remote" revision="refs/heads/main" /> | ||
515 | <project name="{name}" path="{path}" /> | ||
516 | </manifest> | ||
517 | """) | ||
518 | |||
519 | for path in ('.', './', './/', './//'): | ||
520 | manifest = parse('server/path', path) | ||
521 | self.assertEqual(manifest.projects[0].gitdir, | ||
522 | os.path.join(self.tempdir, '.repo/projects/..git')) | ||
523 | |||
524 | def test_bad_path_name_checks(self): | ||
525 | """Check handling of bad path & name attributes.""" | ||
526 | def parse(name, path): | ||
527 | name = self.encodeXmlAttr(name) | ||
528 | path = self.encodeXmlAttr(path) | ||
529 | manifest = self.getXmlManifest(f""" | ||
530 | <manifest> | ||
531 | <remote name="default-remote" fetch="http://localhost" /> | ||
532 | <default remote="default-remote" revision="refs/heads/main" /> | ||
533 | <project name="{name}" path="{path}" /> | ||
534 | </manifest> | ||
535 | """) | ||
536 | # Force the manifest to be parsed. | ||
537 | manifest.ToXml() | ||
538 | |||
539 | # Verify the parser is valid by default to avoid buggy tests below. | ||
540 | parse('ok', 'ok') | ||
541 | |||
542 | # Handle empty name explicitly because a different codepath rejects it. | ||
543 | # Empty path is OK because it defaults to the name field. | ||
544 | with self.assertRaises(error.ManifestParseError): | ||
545 | parse('', 'ok') | ||
546 | |||
547 | for path in INVALID_FS_PATHS: | ||
548 | if not path or path.endswith('/'): | ||
549 | continue | ||
550 | |||
551 | with self.assertRaises(error.ManifestInvalidPathError): | ||
552 | parse(path, 'ok') | ||
553 | |||
554 | # We have a dedicated test for path=".". | ||
555 | if path not in {'.'}: | ||
556 | with self.assertRaises(error.ManifestInvalidPathError): | ||
557 | parse('ok', path) | ||
558 | |||
559 | |||
560 | class SuperProjectElementTests(ManifestParseTestCase): | ||
561 | """Tests for <superproject>.""" | ||
562 | |||
563 | def test_superproject(self): | ||
564 | """Check superproject settings.""" | ||
565 | manifest = self.getXmlManifest(""" | ||
566 | <manifest> | ||
567 | <remote name="test-remote" fetch="http://localhost" /> | ||
568 | <default remote="test-remote" revision="refs/heads/main" /> | ||
569 | <superproject name="superproject"/> | ||
570 | </manifest> | ||
571 | """) | ||
572 | self.assertEqual(manifest.superproject['name'], 'superproject') | ||
573 | self.assertEqual(manifest.superproject['remote'].name, 'test-remote') | ||
574 | self.assertEqual(manifest.superproject['remote'].url, 'http://localhost/superproject') | ||
575 | self.assertEqual(manifest.superproject['revision'], 'refs/heads/main') | ||
576 | self.assertEqual( | ||
577 | sort_attributes(manifest.ToXml().toxml()), | ||
578 | '<?xml version="1.0" ?><manifest>' | ||
579 | '<remote fetch="http://localhost" name="test-remote"/>' | ||
580 | '<default remote="test-remote" revision="refs/heads/main"/>' | ||
581 | '<superproject name="superproject"/>' | ||
582 | '</manifest>') | ||
583 | |||
584 | def test_superproject_revision(self): | ||
585 | """Check superproject settings with a different revision attribute""" | ||
586 | self.maxDiff = None | ||
587 | manifest = self.getXmlManifest(""" | ||
588 | <manifest> | ||
589 | <remote name="test-remote" fetch="http://localhost" /> | ||
590 | <default remote="test-remote" revision="refs/heads/main" /> | ||
591 | <superproject name="superproject" revision="refs/heads/stable" /> | ||
592 | </manifest> | ||
593 | """) | ||
594 | self.assertEqual(manifest.superproject['name'], 'superproject') | ||
595 | self.assertEqual(manifest.superproject['remote'].name, 'test-remote') | ||
596 | self.assertEqual(manifest.superproject['remote'].url, 'http://localhost/superproject') | ||
597 | self.assertEqual(manifest.superproject['revision'], 'refs/heads/stable') | ||
598 | self.assertEqual( | ||
599 | sort_attributes(manifest.ToXml().toxml()), | ||
600 | '<?xml version="1.0" ?><manifest>' | ||
601 | '<remote fetch="http://localhost" name="test-remote"/>' | ||
602 | '<default remote="test-remote" revision="refs/heads/main"/>' | ||
603 | '<superproject name="superproject" revision="refs/heads/stable"/>' | ||
604 | '</manifest>') | ||
605 | |||
606 | def test_superproject_revision_default_negative(self): | ||
607 | """Check superproject settings with a same revision attribute""" | ||
608 | self.maxDiff = None | ||
609 | manifest = self.getXmlManifest(""" | ||
610 | <manifest> | ||
611 | <remote name="test-remote" fetch="http://localhost" /> | ||
612 | <default remote="test-remote" revision="refs/heads/stable" /> | ||
613 | <superproject name="superproject" revision="refs/heads/stable" /> | ||
614 | </manifest> | ||
615 | """) | ||
616 | self.assertEqual(manifest.superproject['name'], 'superproject') | ||
617 | self.assertEqual(manifest.superproject['remote'].name, 'test-remote') | ||
618 | self.assertEqual(manifest.superproject['remote'].url, 'http://localhost/superproject') | ||
619 | self.assertEqual(manifest.superproject['revision'], 'refs/heads/stable') | ||
620 | self.assertEqual( | ||
621 | sort_attributes(manifest.ToXml().toxml()), | ||
622 | '<?xml version="1.0" ?><manifest>' | ||
623 | '<remote fetch="http://localhost" name="test-remote"/>' | ||
624 | '<default remote="test-remote" revision="refs/heads/stable"/>' | ||
625 | '<superproject name="superproject"/>' | ||
626 | '</manifest>') | ||
627 | |||
628 | def test_superproject_revision_remote(self): | ||
629 | """Check superproject settings with a same revision attribute""" | ||
630 | self.maxDiff = None | ||
631 | manifest = self.getXmlManifest(""" | ||
632 | <manifest> | ||
633 | <remote name="test-remote" fetch="http://localhost" revision="refs/heads/main" /> | ||
634 | <default remote="test-remote" /> | ||
635 | <superproject name="superproject" revision="refs/heads/stable" /> | ||
636 | </manifest> | ||
637 | """) | ||
638 | self.assertEqual(manifest.superproject['name'], 'superproject') | ||
639 | self.assertEqual(manifest.superproject['remote'].name, 'test-remote') | ||
640 | self.assertEqual(manifest.superproject['remote'].url, 'http://localhost/superproject') | ||
641 | self.assertEqual(manifest.superproject['revision'], 'refs/heads/stable') | ||
642 | self.assertEqual( | ||
643 | sort_attributes(manifest.ToXml().toxml()), | ||
644 | '<?xml version="1.0" ?><manifest>' | ||
645 | '<remote fetch="http://localhost" name="test-remote" revision="refs/heads/main"/>' | ||
646 | '<default remote="test-remote"/>' | ||
647 | '<superproject name="superproject" revision="refs/heads/stable"/>' | ||
648 | '</manifest>') | ||
649 | |||
650 | def test_remote(self): | ||
651 | """Check superproject settings with a remote.""" | ||
652 | manifest = self.getXmlManifest(""" | ||
653 | <manifest> | ||
654 | <remote name="default-remote" fetch="http://localhost" /> | ||
655 | <remote name="superproject-remote" fetch="http://localhost" /> | ||
656 | <default remote="default-remote" revision="refs/heads/main" /> | ||
657 | <superproject name="platform/superproject" remote="superproject-remote"/> | ||
658 | </manifest> | ||
659 | """) | ||
660 | self.assertEqual(manifest.superproject['name'], 'platform/superproject') | ||
661 | self.assertEqual(manifest.superproject['remote'].name, 'superproject-remote') | ||
662 | self.assertEqual(manifest.superproject['remote'].url, 'http://localhost/platform/superproject') | ||
663 | self.assertEqual(manifest.superproject['revision'], 'refs/heads/main') | ||
664 | self.assertEqual( | ||
665 | sort_attributes(manifest.ToXml().toxml()), | ||
666 | '<?xml version="1.0" ?><manifest>' | ||
667 | '<remote fetch="http://localhost" name="default-remote"/>' | ||
668 | '<remote fetch="http://localhost" name="superproject-remote"/>' | ||
669 | '<default remote="default-remote" revision="refs/heads/main"/>' | ||
670 | '<superproject name="platform/superproject" remote="superproject-remote"/>' | ||
671 | '</manifest>') | ||
672 | |||
673 | def test_defalut_remote(self): | ||
674 | """Check superproject settings with a default remote.""" | ||
675 | manifest = self.getXmlManifest(""" | ||
676 | <manifest> | ||
677 | <remote name="default-remote" fetch="http://localhost" /> | ||
678 | <default remote="default-remote" revision="refs/heads/main" /> | ||
679 | <superproject name="superproject" remote="default-remote"/> | ||
680 | </manifest> | ||
681 | """) | ||
682 | self.assertEqual(manifest.superproject['name'], 'superproject') | ||
683 | self.assertEqual(manifest.superproject['remote'].name, 'default-remote') | ||
684 | self.assertEqual(manifest.superproject['revision'], 'refs/heads/main') | ||
685 | self.assertEqual( | ||
686 | sort_attributes(manifest.ToXml().toxml()), | ||
687 | '<?xml version="1.0" ?><manifest>' | ||
688 | '<remote fetch="http://localhost" name="default-remote"/>' | ||
689 | '<default remote="default-remote" revision="refs/heads/main"/>' | ||
690 | '<superproject name="superproject"/>' | ||
691 | '</manifest>') | ||
692 | |||
693 | |||
694 | class ContactinfoElementTests(ManifestParseTestCase): | ||
695 | """Tests for <contactinfo>.""" | ||
696 | |||
697 | def test_contactinfo(self): | ||
698 | """Check contactinfo settings.""" | ||
699 | bugurl = 'http://localhost/contactinfo' | ||
700 | manifest = self.getXmlManifest(f""" | ||
701 | <manifest> | ||
702 | <contactinfo bugurl="{bugurl}"/> | ||
703 | </manifest> | ||
704 | """) | ||
705 | self.assertEqual(manifest.contactinfo.bugurl, bugurl) | ||
706 | self.assertEqual( | ||
707 | manifest.ToXml().toxml(), | ||
708 | '<?xml version="1.0" ?><manifest>' | ||
709 | f'<contactinfo bugurl="{bugurl}"/>' | ||
710 | '</manifest>') | ||
711 | |||
712 | |||
713 | class DefaultElementTests(ManifestParseTestCase): | ||
714 | """Tests for <default>.""" | ||
715 | |||
716 | def test_default(self): | ||
717 | """Check default settings.""" | ||
718 | a = manifest_xml._Default() | ||
719 | a.revisionExpr = 'foo' | ||
720 | a.remote = manifest_xml._XmlRemote(name='remote') | ||
721 | b = manifest_xml._Default() | ||
722 | b.revisionExpr = 'bar' | ||
723 | self.assertEqual(a, a) | ||
724 | self.assertNotEqual(a, b) | ||
725 | self.assertNotEqual(b, a.remote) | ||
726 | self.assertNotEqual(a, 123) | ||
727 | self.assertNotEqual(a, None) | ||
728 | |||
729 | |||
730 | class RemoteElementTests(ManifestParseTestCase): | ||
731 | """Tests for <remote>.""" | ||
732 | |||
733 | def test_remote(self): | ||
734 | """Check remote settings.""" | ||
735 | a = manifest_xml._XmlRemote(name='foo') | ||
736 | a.AddAnnotation('key1', 'value1', 'true') | ||
737 | b = manifest_xml._XmlRemote(name='foo') | ||
738 | b.AddAnnotation('key2', 'value1', 'true') | ||
739 | c = manifest_xml._XmlRemote(name='foo') | ||
740 | c.AddAnnotation('key1', 'value2', 'true') | ||
741 | d = manifest_xml._XmlRemote(name='foo') | ||
742 | d.AddAnnotation('key1', 'value1', 'false') | ||
743 | self.assertEqual(a, a) | ||
744 | self.assertNotEqual(a, b) | ||
745 | self.assertNotEqual(a, c) | ||
746 | self.assertNotEqual(a, d) | ||
747 | self.assertNotEqual(a, manifest_xml._Default()) | ||
748 | self.assertNotEqual(a, 123) | ||
749 | self.assertNotEqual(a, None) | ||
750 | |||
751 | |||
752 | class RemoveProjectElementTests(ManifestParseTestCase): | ||
753 | """Tests for <remove-project>.""" | ||
754 | |||
755 | def test_remove_one_project(self): | ||
756 | manifest = self.getXmlManifest(""" | ||
757 | <manifest> | ||
758 | <remote name="default-remote" fetch="http://localhost" /> | ||
759 | <default remote="default-remote" revision="refs/heads/main" /> | ||
760 | <project name="myproject" /> | ||
761 | <remove-project name="myproject" /> | ||
762 | </manifest> | ||
763 | """) | ||
764 | self.assertEqual(manifest.projects, []) | ||
765 | |||
766 | def test_remove_one_project_one_remains(self): | ||
767 | manifest = self.getXmlManifest(""" | ||
768 | <manifest> | ||
769 | <remote name="default-remote" fetch="http://localhost" /> | ||
770 | <default remote="default-remote" revision="refs/heads/main" /> | ||
771 | <project name="myproject" /> | ||
772 | <project name="yourproject" /> | ||
773 | <remove-project name="myproject" /> | ||
774 | </manifest> | ||
775 | """) | ||
776 | |||
777 | self.assertEqual(len(manifest.projects), 1) | ||
778 | self.assertEqual(manifest.projects[0].name, 'yourproject') | ||
779 | |||
780 | def test_remove_one_project_doesnt_exist(self): | ||
781 | with self.assertRaises(manifest_xml.ManifestParseError): | ||
782 | manifest = self.getXmlManifest(""" | ||
783 | <manifest> | ||
784 | <remote name="default-remote" fetch="http://localhost" /> | ||
785 | <default remote="default-remote" revision="refs/heads/main" /> | ||
786 | <remove-project name="myproject" /> | ||
787 | </manifest> | ||
788 | """) | ||
789 | manifest.projects | ||
790 | |||
791 | def test_remove_one_optional_project_doesnt_exist(self): | ||
792 | manifest = self.getXmlManifest(""" | ||
793 | <manifest> | ||
794 | <remote name="default-remote" fetch="http://localhost" /> | ||
795 | <default remote="default-remote" revision="refs/heads/main" /> | ||
796 | <remove-project name="myproject" optional="true" /> | ||
797 | </manifest> | ||
798 | """) | ||
799 | self.assertEqual(manifest.projects, []) | ||
800 | |||
801 | |||
802 | class ExtendProjectElementTests(ManifestParseTestCase): | ||
803 | """Tests for <extend-project>.""" | ||
804 | |||
805 | def test_extend_project_dest_path_single_match(self): | ||
806 | manifest = self.getXmlManifest(""" | ||
807 | <manifest> | ||
808 | <remote name="default-remote" fetch="http://localhost" /> | ||
809 | <default remote="default-remote" revision="refs/heads/main" /> | ||
810 | <project name="myproject" /> | ||
811 | <extend-project name="myproject" dest-path="bar" /> | ||
812 | </manifest> | ||
813 | """) | ||
814 | self.assertEqual(len(manifest.projects), 1) | ||
815 | self.assertEqual(manifest.projects[0].relpath, 'bar') | ||
816 | |||
817 | def test_extend_project_dest_path_multi_match(self): | ||
818 | with self.assertRaises(manifest_xml.ManifestParseError): | ||
819 | manifest = self.getXmlManifest(""" | ||
820 | <manifest> | ||
821 | <remote name="default-remote" fetch="http://localhost" /> | ||
822 | <default remote="default-remote" revision="refs/heads/main" /> | ||
823 | <project name="myproject" path="x" /> | ||
824 | <project name="myproject" path="y" /> | ||
825 | <extend-project name="myproject" dest-path="bar" /> | ||
826 | </manifest> | ||
827 | """) | ||
828 | manifest.projects | ||
829 | |||
830 | def test_extend_project_dest_path_multi_match_path_specified(self): | ||
831 | manifest = self.getXmlManifest(""" | ||
832 | <manifest> | ||
833 | <remote name="default-remote" fetch="http://localhost" /> | ||
834 | <default remote="default-remote" revision="refs/heads/main" /> | ||
835 | <project name="myproject" path="x" /> | ||
836 | <project name="myproject" path="y" /> | ||
837 | <extend-project name="myproject" path="x" dest-path="bar" /> | ||
838 | </manifest> | ||
839 | """) | ||
840 | self.assertEqual(len(manifest.projects), 2) | ||
841 | if manifest.projects[0].relpath == 'y': | ||
842 | self.assertEqual(manifest.projects[1].relpath, 'bar') | ||
843 | else: | ||
844 | self.assertEqual(manifest.projects[0].relpath, 'bar') | ||
845 | self.assertEqual(manifest.projects[1].relpath, 'y') | ||