Snap for 11926388 from b81c243083f0b2961e4a7c92e4856e33cd2572ca to 24Q3-release

Change-Id: I3d78f659584248d2c1421ae376c4a9da9d5b68bc
diff --git a/manifest.py b/manifest.py
index de60413..3fd68c1 100644
--- a/manifest.py
+++ b/manifest.py
@@ -51,10 +51,11 @@
     ) -> Project:
         """Parses a Project from the given XML node."""
         try:
-            path = node.attrib["path"]
+            # Path is optional, defaults to project name per manifest spec
+            path = x if (x := node.attrib.get("path")) is not None else node.attrib["name"]
         except KeyError as ex:
             raise RuntimeError(
-                f"<project /> element missing required path attribute: {node}"
+                f"<project /> element missing required name attribute: {node}"
             ) from ex
 
         return Project(
diff --git a/test_manifest.py b/test_manifest.py
index ca6088a..2e9c8b9 100644
--- a/test_manifest.py
+++ b/test_manifest.py
@@ -58,6 +58,25 @@
         with pytest.raises(RuntimeError):
             ManifestParser(manifest_path).parse()
 
+    def test_name_missing(self, tmp_path: Path) -> None:
+        """Tests that an error is raised when neither name nor path is defined for a project."""
+        manifest_path = tmp_path / "manifest.xml"
+        manifest_path.write_text(
+            textwrap.dedent(
+                """\
+                <?xml version="1.0" encoding="UTF-8"?>
+                <manifest>
+                    <default revision="main" remote="aosp" />
+
+                    <project />
+                </manifest>
+                """
+            )
+        )
+        with pytest.raises(RuntimeError):
+            ManifestParser(manifest_path).parse()
+
+
     def test_multiple_default(self, tmp_path: Path) -> None:
         """Tests that an error is raised when there is more than one default node."""
         manifest = tmp_path / "manifest.xml"
@@ -114,6 +133,24 @@
         manifest = ManifestParser(manifest_path).parse()
         assert manifest.project_with_path("external/project").revision == "main"
 
+    def test_path_default(self, tmp_path: Path) -> None:
+        """Tests that the default path is used when not defined by the project."""
+        manifest_path = tmp_path / "manifest.xml"
+        manifest_path.write_text(
+            textwrap.dedent(
+                """\
+                <?xml version="1.0" encoding="UTF-8"?>
+                <manifest>
+                    <default revision="main" remote="aosp" />
+
+                    <project name="external/project" />
+                </manifest>
+                """
+            )
+        )
+        manifest = ManifestParser(manifest_path).parse()
+        assert manifest.project_with_path("external/project") is not None
+
     def test_remote_explicit(self, tmp_path: Path) -> None:
         """Tests that the project remote is used when defined."""
         manifest_path = tmp_path / "manifest.xml"
@@ -150,6 +187,23 @@
         manifest = ManifestParser(manifest_path).parse()
         assert manifest.project_with_path("external/project").revision == "master"
 
+    def test_path_explicit(self, tmp_path: Path) -> None:
+        """Tests that the project path is used when defined."""
+        manifest_path = tmp_path / "manifest.xml"
+        manifest_path.write_text(
+            textwrap.dedent(
+                """\
+                <?xml version="1.0" encoding="UTF-8"?>
+                <manifest>
+                    <default revision="main" remote="aosp" />
+
+                    <project name="external/project" path="other/path" />
+                </manifest>
+                """
+            )
+        )
+        manifest = ManifestParser(manifest_path).parse()
+        assert manifest.project_with_path("other/path") is not None
 
 class TestManifest:
     """Tests for Manifest."""