Mike Frysinger | 04122b7 | 2019-07-31 23:32:58 -0400 | [diff] [blame] | 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 | |
Mike Frysinger | d925459 | 2020-02-19 22:36:26 -0500 | [diff] [blame] | 17 | import os |
Mike Frysinger | 8c1e9cb | 2020-09-06 14:53:18 -0400 | [diff] [blame] | 18 | import shutil |
| 19 | import tempfile |
Mike Frysinger | 04122b7 | 2019-07-31 23:32:58 -0400 | [diff] [blame] | 20 | import unittest |
Mike Frysinger | bb8ee7f | 2020-02-22 05:30:12 -0500 | [diff] [blame] | 21 | import xml.dom.minidom |
Mike Frysinger | 04122b7 | 2019-07-31 23:32:58 -0400 | [diff] [blame] | 22 | |
| 23 | import error |
| 24 | import manifest_xml |
| 25 | |
| 26 | |
Mike Frysinger | 37ac3d6 | 2021-02-25 04:54:56 -0500 | [diff] [blame^] | 27 | class ManifestParseTestCase(unittest.TestCase): |
| 28 | """TestCase for parsing manifests.""" |
| 29 | |
| 30 | def setUp(self): |
| 31 | self.tempdir = tempfile.mkdtemp(prefix='repo_tests') |
| 32 | self.repodir = os.path.join(self.tempdir, '.repo') |
| 33 | self.manifest_dir = os.path.join(self.repodir, 'manifests') |
| 34 | self.manifest_file = os.path.join( |
| 35 | self.repodir, manifest_xml.MANIFEST_FILE_NAME) |
| 36 | self.local_manifest_dir = os.path.join( |
| 37 | self.repodir, manifest_xml.LOCAL_MANIFESTS_DIR_NAME) |
| 38 | os.mkdir(self.repodir) |
| 39 | os.mkdir(self.manifest_dir) |
| 40 | |
| 41 | # The manifest parsing really wants a git repo currently. |
| 42 | gitdir = os.path.join(self.repodir, 'manifests.git') |
| 43 | os.mkdir(gitdir) |
| 44 | with open(os.path.join(gitdir, 'config'), 'w') as fp: |
| 45 | fp.write("""[remote "origin"] |
| 46 | url = https://localhost:0/manifest |
| 47 | """) |
| 48 | |
| 49 | def tearDown(self): |
| 50 | shutil.rmtree(self.tempdir, ignore_errors=True) |
| 51 | |
| 52 | def getXmlManifest(self, data): |
| 53 | """Helper to initialize a manifest for testing.""" |
| 54 | with open(self.manifest_file, 'w') as fp: |
| 55 | fp.write(data) |
| 56 | return manifest_xml.XmlManifest(self.repodir, self.manifest_file) |
| 57 | |
| 58 | |
Mike Frysinger | 04122b7 | 2019-07-31 23:32:58 -0400 | [diff] [blame] | 59 | class ManifestValidateFilePaths(unittest.TestCase): |
| 60 | """Check _ValidateFilePaths helper. |
| 61 | |
| 62 | This doesn't access a real filesystem. |
| 63 | """ |
| 64 | |
| 65 | def check_both(self, *args): |
| 66 | manifest_xml.XmlManifest._ValidateFilePaths('copyfile', *args) |
| 67 | manifest_xml.XmlManifest._ValidateFilePaths('linkfile', *args) |
| 68 | |
| 69 | def test_normal_path(self): |
| 70 | """Make sure good paths are accepted.""" |
| 71 | self.check_both('foo', 'bar') |
| 72 | self.check_both('foo/bar', 'bar') |
| 73 | self.check_both('foo', 'bar/bar') |
| 74 | self.check_both('foo/bar', 'bar/bar') |
| 75 | |
| 76 | def test_symlink_targets(self): |
| 77 | """Some extra checks for symlinks.""" |
| 78 | def check(*args): |
| 79 | manifest_xml.XmlManifest._ValidateFilePaths('linkfile', *args) |
| 80 | |
| 81 | # We allow symlinks to end in a slash since we allow them to point to dirs |
| 82 | # in general. Technically the slash isn't necessary. |
| 83 | check('foo/', 'bar') |
Mike Frysinger | ae62541 | 2020-02-10 17:10:03 -0500 | [diff] [blame] | 84 | # We allow a single '.' to get a reference to the project itself. |
| 85 | check('.', 'bar') |
Mike Frysinger | 04122b7 | 2019-07-31 23:32:58 -0400 | [diff] [blame] | 86 | |
| 87 | def test_bad_paths(self): |
| 88 | """Make sure bad paths (src & dest) are rejected.""" |
| 89 | PATHS = ( |
| 90 | '..', |
| 91 | '../', |
| 92 | './', |
| 93 | 'foo/', |
| 94 | './foo', |
| 95 | '../foo', |
| 96 | 'foo/./bar', |
| 97 | 'foo/../../bar', |
| 98 | '/foo', |
| 99 | './../foo', |
| 100 | '.git/foo', |
| 101 | # Check case folding. |
| 102 | '.GIT/foo', |
| 103 | 'blah/.git/foo', |
| 104 | '.repo/foo', |
| 105 | '.repoconfig', |
| 106 | # Block ~ due to 8.3 filenames on Windows filesystems. |
| 107 | '~', |
| 108 | 'foo~', |
| 109 | 'blah/foo~', |
| 110 | # Block Unicode characters that get normalized out by filesystems. |
| 111 | u'foo\u200Cbar', |
| 112 | ) |
Mike Frysinger | d925459 | 2020-02-19 22:36:26 -0500 | [diff] [blame] | 113 | # Make sure platforms that use path separators (e.g. Windows) are also |
| 114 | # rejected properly. |
| 115 | if os.path.sep != '/': |
| 116 | PATHS += tuple(x.replace('/', os.path.sep) for x in PATHS) |
| 117 | |
Mike Frysinger | 04122b7 | 2019-07-31 23:32:58 -0400 | [diff] [blame] | 118 | for path in PATHS: |
| 119 | self.assertRaises( |
| 120 | error.ManifestInvalidPathError, self.check_both, path, 'a') |
| 121 | self.assertRaises( |
| 122 | error.ManifestInvalidPathError, self.check_both, 'a', path) |
Mike Frysinger | bb8ee7f | 2020-02-22 05:30:12 -0500 | [diff] [blame] | 123 | |
| 124 | |
| 125 | class ValueTests(unittest.TestCase): |
| 126 | """Check utility parsing code.""" |
| 127 | |
| 128 | def _get_node(self, text): |
| 129 | return xml.dom.minidom.parseString(text).firstChild |
| 130 | |
| 131 | def test_bool_default(self): |
| 132 | """Check XmlBool default handling.""" |
| 133 | node = self._get_node('<node/>') |
| 134 | self.assertIsNone(manifest_xml.XmlBool(node, 'a')) |
| 135 | self.assertIsNone(manifest_xml.XmlBool(node, 'a', None)) |
| 136 | self.assertEqual(123, manifest_xml.XmlBool(node, 'a', 123)) |
| 137 | |
| 138 | node = self._get_node('<node a=""/>') |
| 139 | self.assertIsNone(manifest_xml.XmlBool(node, 'a')) |
| 140 | |
| 141 | def test_bool_invalid(self): |
| 142 | """Check XmlBool invalid handling.""" |
| 143 | node = self._get_node('<node a="moo"/>') |
| 144 | self.assertEqual(123, manifest_xml.XmlBool(node, 'a', 123)) |
| 145 | |
| 146 | def test_bool_true(self): |
| 147 | """Check XmlBool true values.""" |
| 148 | for value in ('yes', 'true', '1'): |
| 149 | node = self._get_node('<node a="%s"/>' % (value,)) |
| 150 | self.assertTrue(manifest_xml.XmlBool(node, 'a')) |
| 151 | |
| 152 | def test_bool_false(self): |
| 153 | """Check XmlBool false values.""" |
| 154 | for value in ('no', 'false', '0'): |
| 155 | node = self._get_node('<node a="%s"/>' % (value,)) |
| 156 | self.assertFalse(manifest_xml.XmlBool(node, 'a')) |
| 157 | |
| 158 | def test_int_default(self): |
| 159 | """Check XmlInt default handling.""" |
| 160 | node = self._get_node('<node/>') |
| 161 | self.assertIsNone(manifest_xml.XmlInt(node, 'a')) |
| 162 | self.assertIsNone(manifest_xml.XmlInt(node, 'a', None)) |
| 163 | self.assertEqual(123, manifest_xml.XmlInt(node, 'a', 123)) |
| 164 | |
| 165 | node = self._get_node('<node a=""/>') |
| 166 | self.assertIsNone(manifest_xml.XmlInt(node, 'a')) |
| 167 | |
| 168 | def test_int_good(self): |
| 169 | """Check XmlInt numeric handling.""" |
| 170 | for value in (-1, 0, 1, 50000): |
| 171 | node = self._get_node('<node a="%s"/>' % (value,)) |
| 172 | self.assertEqual(value, manifest_xml.XmlInt(node, 'a')) |
| 173 | |
| 174 | def test_int_invalid(self): |
| 175 | """Check XmlInt invalid handling.""" |
| 176 | with self.assertRaises(error.ManifestParseError): |
| 177 | node = self._get_node('<node a="xx"/>') |
| 178 | manifest_xml.XmlInt(node, 'a') |
Mike Frysinger | 8c1e9cb | 2020-09-06 14:53:18 -0400 | [diff] [blame] | 179 | |
| 180 | |
Mike Frysinger | 37ac3d6 | 2021-02-25 04:54:56 -0500 | [diff] [blame^] | 181 | class XmlManifestTests(ManifestParseTestCase): |
Mike Frysinger | 8c1e9cb | 2020-09-06 14:53:18 -0400 | [diff] [blame] | 182 | """Check manifest processing.""" |
| 183 | |
Mike Frysinger | 8c1e9cb | 2020-09-06 14:53:18 -0400 | [diff] [blame] | 184 | def test_empty(self): |
| 185 | """Parse an 'empty' manifest file.""" |
| 186 | manifest = self.getXmlManifest( |
| 187 | '<?xml version="1.0" encoding="UTF-8"?>' |
| 188 | '<manifest></manifest>') |
| 189 | self.assertEqual(manifest.remotes, {}) |
| 190 | self.assertEqual(manifest.projects, []) |
| 191 | |
| 192 | def test_link(self): |
| 193 | """Verify Link handling with new names.""" |
| 194 | manifest = manifest_xml.XmlManifest(self.repodir, self.manifest_file) |
| 195 | with open(os.path.join(self.manifest_dir, 'foo.xml'), 'w') as fp: |
| 196 | fp.write('<manifest></manifest>') |
| 197 | manifest.Link('foo.xml') |
| 198 | with open(self.manifest_file) as fp: |
| 199 | self.assertIn('<include name="foo.xml" />', fp.read()) |
| 200 | |
| 201 | def test_toxml_empty(self): |
| 202 | """Verify the ToXml() helper.""" |
| 203 | manifest = self.getXmlManifest( |
| 204 | '<?xml version="1.0" encoding="UTF-8"?>' |
| 205 | '<manifest></manifest>') |
| 206 | self.assertEqual(manifest.ToXml().toxml(), '<?xml version="1.0" ?><manifest/>') |
| 207 | |
| 208 | def test_todict_empty(self): |
| 209 | """Verify the ToDict() helper.""" |
| 210 | manifest = self.getXmlManifest( |
| 211 | '<?xml version="1.0" encoding="UTF-8"?>' |
| 212 | '<manifest></manifest>') |
| 213 | self.assertEqual(manifest.ToDict(), {}) |
| 214 | |
Mike Frysinger | 51e39d5 | 2020-12-04 05:32:06 -0500 | [diff] [blame] | 215 | def test_repo_hooks(self): |
| 216 | """Check repo-hooks settings.""" |
| 217 | manifest = self.getXmlManifest(""" |
| 218 | <manifest> |
| 219 | <remote name="test-remote" fetch="http://localhost" /> |
| 220 | <default remote="test-remote" revision="refs/heads/main" /> |
| 221 | <project name="repohooks" path="src/repohooks"/> |
| 222 | <repo-hooks in-project="repohooks" enabled-list="a, b"/> |
| 223 | </manifest> |
| 224 | """) |
| 225 | self.assertEqual(manifest.repo_hooks_project.name, 'repohooks') |
| 226 | self.assertEqual(manifest.repo_hooks_project.enabled_repo_hooks, ['a', 'b']) |
| 227 | |
Raman Tenneti | 48b2d10 | 2021-01-11 12:18:47 -0800 | [diff] [blame] | 228 | def test_unknown_tags(self): |
| 229 | """Check superproject settings.""" |
| 230 | manifest = self.getXmlManifest(""" |
| 231 | <manifest> |
| 232 | <remote name="test-remote" fetch="http://localhost" /> |
| 233 | <default remote="test-remote" revision="refs/heads/main" /> |
| 234 | <superproject name="superproject"/> |
| 235 | <iankaz value="unknown (possible) future tags are ignored"/> |
| 236 | <x-custom-tag>X tags are always ignored</x-custom-tag> |
| 237 | </manifest> |
| 238 | """) |
| 239 | self.assertEqual(manifest.superproject['name'], 'superproject') |
| 240 | self.assertEqual(manifest.superproject['remote'].name, 'test-remote') |
| 241 | self.assertEqual( |
| 242 | manifest.ToXml().toxml(), |
| 243 | '<?xml version="1.0" ?><manifest>' + |
| 244 | '<remote name="test-remote" fetch="http://localhost"/>' + |
| 245 | '<default remote="test-remote" revision="refs/heads/main"/>' + |
| 246 | '<superproject name="superproject"/>' + |
| 247 | '</manifest>') |
| 248 | |
Mike Frysinger | 8c1e9cb | 2020-09-06 14:53:18 -0400 | [diff] [blame] | 249 | def test_project_group(self): |
| 250 | """Check project group settings.""" |
| 251 | manifest = self.getXmlManifest(""" |
| 252 | <manifest> |
| 253 | <remote name="test-remote" fetch="http://localhost" /> |
| 254 | <default remote="test-remote" revision="refs/heads/main" /> |
| 255 | <project name="test-name" path="test-path"/> |
| 256 | <project name="extras" path="path" groups="g1,g2,g1"/> |
| 257 | </manifest> |
| 258 | """) |
| 259 | self.assertEqual(len(manifest.projects), 2) |
| 260 | # Ordering isn't guaranteed. |
| 261 | result = { |
| 262 | manifest.projects[0].name: manifest.projects[0].groups, |
| 263 | manifest.projects[1].name: manifest.projects[1].groups, |
| 264 | } |
| 265 | project = manifest.projects[0] |
| 266 | self.assertCountEqual( |
| 267 | result['test-name'], |
| 268 | ['name:test-name', 'all', 'path:test-path']) |
| 269 | self.assertCountEqual( |
| 270 | result['extras'], |
| 271 | ['g1', 'g2', 'g1', 'name:extras', 'all', 'path:path']) |
Fredrik de Groot | 352c93b | 2020-10-06 12:55:14 +0200 | [diff] [blame] | 272 | |
Raman Tenneti | b5c5a5e | 2021-02-06 09:44:15 -0800 | [diff] [blame] | 273 | def test_project_set_revision_id(self): |
| 274 | """Check setting of project's revisionId.""" |
| 275 | manifest = self.getXmlManifest(""" |
| 276 | <manifest> |
| 277 | <remote name="default-remote" fetch="http://localhost" /> |
| 278 | <default remote="default-remote" revision="refs/heads/main" /> |
| 279 | <project name="test-name"/> |
| 280 | </manifest> |
| 281 | """) |
| 282 | self.assertEqual(len(manifest.projects), 1) |
| 283 | project = manifest.projects[0] |
| 284 | project.SetRevisionId('ABCDEF') |
| 285 | self.assertEqual( |
| 286 | manifest.ToXml().toxml(), |
| 287 | '<?xml version="1.0" ?><manifest>' + |
| 288 | '<remote name="default-remote" fetch="http://localhost"/>' + |
| 289 | '<default remote="default-remote" revision="refs/heads/main"/>' + |
| 290 | '<project name="test-name" revision="ABCDEF"/>' + |
| 291 | '</manifest>') |
| 292 | |
Fredrik de Groot | 352c93b | 2020-10-06 12:55:14 +0200 | [diff] [blame] | 293 | def test_include_levels(self): |
| 294 | root_m = os.path.join(self.manifest_dir, 'root.xml') |
| 295 | with open(root_m, 'w') as fp: |
| 296 | fp.write(""" |
| 297 | <manifest> |
| 298 | <remote name="test-remote" fetch="http://localhost" /> |
| 299 | <default remote="test-remote" revision="refs/heads/main" /> |
| 300 | <include name="level1.xml" groups="level1-group" /> |
| 301 | <project name="root-name1" path="root-path1" /> |
| 302 | <project name="root-name2" path="root-path2" groups="r2g1,r2g2" /> |
| 303 | </manifest> |
| 304 | """) |
| 305 | with open(os.path.join(self.manifest_dir, 'level1.xml'), 'w') as fp: |
| 306 | fp.write(""" |
| 307 | <manifest> |
| 308 | <include name="level2.xml" groups="level2-group" /> |
| 309 | <project name="level1-name1" path="level1-path1" /> |
| 310 | </manifest> |
| 311 | """) |
| 312 | with open(os.path.join(self.manifest_dir, 'level2.xml'), 'w') as fp: |
| 313 | fp.write(""" |
| 314 | <manifest> |
| 315 | <project name="level2-name1" path="level2-path1" groups="l2g1,l2g2" /> |
| 316 | </manifest> |
| 317 | """) |
| 318 | include_m = manifest_xml.XmlManifest(self.repodir, root_m) |
| 319 | for proj in include_m.projects: |
| 320 | if proj.name == 'root-name1': |
| 321 | # Check include group not set on root level proj. |
| 322 | self.assertNotIn('level1-group', proj.groups) |
| 323 | if proj.name == 'root-name2': |
| 324 | # Check root proj group not removed. |
| 325 | self.assertIn('r2g1', proj.groups) |
| 326 | if proj.name == 'level1-name1': |
| 327 | # Check level1 proj has inherited group level 1. |
| 328 | self.assertIn('level1-group', proj.groups) |
| 329 | if proj.name == 'level2-name1': |
| 330 | # Check level2 proj has inherited group levels 1 and 2. |
| 331 | self.assertIn('level1-group', proj.groups) |
| 332 | self.assertIn('level2-group', proj.groups) |
| 333 | # Check level2 proj group not removed. |
| 334 | self.assertIn('l2g1', proj.groups) |
Mike Frysinger | 37ac3d6 | 2021-02-25 04:54:56 -0500 | [diff] [blame^] | 335 | |
| 336 | |
| 337 | class SuperProjectTests(ManifestParseTestCase): |
| 338 | """Tests for <superproject>.""" |
| 339 | |
| 340 | def test_superproject(self): |
| 341 | """Check superproject settings.""" |
| 342 | manifest = self.getXmlManifest(""" |
| 343 | <manifest> |
| 344 | <remote name="test-remote" fetch="http://localhost" /> |
| 345 | <default remote="test-remote" revision="refs/heads/main" /> |
| 346 | <superproject name="superproject"/> |
| 347 | </manifest> |
| 348 | """) |
| 349 | self.assertEqual(manifest.superproject['name'], 'superproject') |
| 350 | self.assertEqual(manifest.superproject['remote'].name, 'test-remote') |
| 351 | self.assertEqual(manifest.superproject['remote'].url, 'http://localhost/superproject') |
| 352 | self.assertEqual( |
| 353 | manifest.ToXml().toxml(), |
| 354 | '<?xml version="1.0" ?><manifest>' + |
| 355 | '<remote name="test-remote" fetch="http://localhost"/>' + |
| 356 | '<default remote="test-remote" revision="refs/heads/main"/>' + |
| 357 | '<superproject name="superproject"/>' + |
| 358 | '</manifest>') |
| 359 | |
| 360 | def test_remote(self): |
| 361 | """Check superproject settings with a remote.""" |
| 362 | manifest = self.getXmlManifest(""" |
| 363 | <manifest> |
| 364 | <remote name="default-remote" fetch="http://localhost" /> |
| 365 | <remote name="superproject-remote" fetch="http://localhost" /> |
| 366 | <default remote="default-remote" revision="refs/heads/main" /> |
| 367 | <superproject name="platform/superproject" remote="superproject-remote"/> |
| 368 | </manifest> |
| 369 | """) |
| 370 | self.assertEqual(manifest.superproject['name'], 'platform/superproject') |
| 371 | self.assertEqual(manifest.superproject['remote'].name, 'superproject-remote') |
| 372 | self.assertEqual(manifest.superproject['remote'].url, 'http://localhost/platform/superproject') |
| 373 | self.assertEqual( |
| 374 | manifest.ToXml().toxml(), |
| 375 | '<?xml version="1.0" ?><manifest>' + |
| 376 | '<remote name="default-remote" fetch="http://localhost"/>' + |
| 377 | '<remote name="superproject-remote" fetch="http://localhost"/>' + |
| 378 | '<default remote="default-remote" revision="refs/heads/main"/>' + |
| 379 | '<superproject name="platform/superproject" remote="superproject-remote"/>' + |
| 380 | '</manifest>') |
| 381 | |
| 382 | def test_defalut_remote(self): |
| 383 | """Check superproject settings with a default remote.""" |
| 384 | manifest = self.getXmlManifest(""" |
| 385 | <manifest> |
| 386 | <remote name="default-remote" fetch="http://localhost" /> |
| 387 | <default remote="default-remote" revision="refs/heads/main" /> |
| 388 | <superproject name="superproject" remote="default-remote"/> |
| 389 | </manifest> |
| 390 | """) |
| 391 | self.assertEqual(manifest.superproject['name'], 'superproject') |
| 392 | self.assertEqual(manifest.superproject['remote'].name, 'default-remote') |
| 393 | self.assertEqual( |
| 394 | manifest.ToXml().toxml(), |
| 395 | '<?xml version="1.0" ?><manifest>' + |
| 396 | '<remote name="default-remote" fetch="http://localhost"/>' + |
| 397 | '<default remote="default-remote" revision="refs/heads/main"/>' + |
| 398 | '<superproject name="superproject"/>' + |
| 399 | '</manifest>') |