1#
2# Copyright (C) 2023 The Android Open Source Project
3#
4# Licensed under the Apache License, Version 2.0 (the 'License');
5# you may not use this file except in compliance with the License.
6# You may obtain a copy of the License at
7#
8#      http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an 'AS IS' BASIS,
12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13# See the License for the specific language governing permissions and
14# limitations under the License.
15#
16"""Tests for manifest.py."""
17import textwrap
18from pathlib import Path
19
20import pytest
21
22from manifest import Manifest, ManifestParser, find_manifest_xml_for_tree
23
24
25class TestFindManifestXmlForTree:
26    """Tests for find_manifest_xml_for_tree."""
27
28    def test_repo_tree(self, repo_tree: Path) -> None:
29        """Tests that the correct manifest file is found in a repo tree."""
30        manifest_dir = Path(repo_tree / ".repo/manifests")
31        manifest_dir.mkdir()
32        manifest_path = manifest_dir / "default.xml"
33        manifest_path.touch()
34        assert find_manifest_xml_for_tree(repo_tree) == manifest_path
35
36    def test_no_manifest(self, tmp_path: Path) -> None:
37        """Tests that an error is raised when no manifest is found."""
38        with pytest.raises(FileNotFoundError):
39            find_manifest_xml_for_tree(tmp_path)
40
41
42class TestManifestParser:
43    """Tests for ManifestParser."""
44
45    def test_default_missing(self, tmp_path: Path) -> None:
46        """Tests that an error is raised when the default node is missing."""
47        manifest_path = tmp_path / "manifest.xml"
48        manifest_path.write_text(
49            textwrap.dedent(
50                """\
51                <?xml version="1.0" encoding="UTF-8"?>
52                <manifest>
53                    <project path="external/project" revision="master" />
54                </manifest>
55                """
56            )
57        )
58        with pytest.raises(RuntimeError):
59            ManifestParser(manifest_path).parse()
60
61    def test_name_missing(self, tmp_path: Path) -> None:
62        """Tests that an error is raised when neither name nor path is defined for a project."""
63        manifest_path = tmp_path / "manifest.xml"
64        manifest_path.write_text(
65            textwrap.dedent(
66                """\
67                <?xml version="1.0" encoding="UTF-8"?>
68                <manifest>
69                    <default revision="main" remote="aosp" />
70
71                    <project />
72                </manifest>
73                """
74            )
75        )
76        with pytest.raises(RuntimeError):
77            ManifestParser(manifest_path).parse()
78
79
80    def test_multiple_default(self, tmp_path: Path) -> None:
81        """Tests that an error is raised when there is more than one default node."""
82        manifest = tmp_path / "manifest.xml"
83        manifest.write_text(
84            textwrap.dedent(
85                """\
86                <?xml version="1.0" encoding="UTF-8"?>
87                <manifest>
88                    <default revision="main" remote="aosp" />
89                    <default revision="main" remote="aosp" />
90
91                    <project path="external/project" revision="master" />
92                </manifest>
93                """
94            )
95        )
96        with pytest.raises(RuntimeError):
97            ManifestParser(manifest).parse()
98
99    def test_remote_default(self, tmp_path: Path) -> None:
100        """Tests that the default remote is used when not defined by the project."""
101        manifest_path = tmp_path / "manifest.xml"
102        manifest_path.write_text(
103            textwrap.dedent(
104                """\
105                <?xml version="1.0" encoding="UTF-8"?>
106                <manifest>
107                    <remote name="aosp" />
108                    <default revision="main" remote="aosp" />
109
110                    <project path="external/project" />
111                </manifest>
112                """
113            )
114        )
115        manifest = ManifestParser(manifest_path).parse()
116        assert manifest.project_with_path("external/project").remote == "aosp"
117
118    def test_revision_default(self, tmp_path: Path) -> None:
119        """Tests that the default revision is used when not defined by the project."""
120        manifest_path = tmp_path / "manifest.xml"
121        manifest_path.write_text(
122            textwrap.dedent(
123                """\
124                <?xml version="1.0" encoding="UTF-8"?>
125                <manifest>
126                    <default revision="main" remote="aosp" />
127
128                    <project path="external/project" />
129                </manifest>
130                """
131            )
132        )
133        manifest = ManifestParser(manifest_path).parse()
134        assert manifest.project_with_path("external/project").revision == "main"
135
136    def test_path_default(self, tmp_path: Path) -> None:
137        """Tests that the default path is used when not defined by the project."""
138        manifest_path = tmp_path / "manifest.xml"
139        manifest_path.write_text(
140            textwrap.dedent(
141                """\
142                <?xml version="1.0" encoding="UTF-8"?>
143                <manifest>
144                    <default revision="main" remote="aosp" />
145
146                    <project name="external/project" />
147                </manifest>
148                """
149            )
150        )
151        manifest = ManifestParser(manifest_path).parse()
152        assert manifest.project_with_path("external/project") is not None
153
154    def test_remote_explicit(self, tmp_path: Path) -> None:
155        """Tests that the project remote is used when defined."""
156        manifest_path = tmp_path / "manifest.xml"
157        manifest_path.write_text(
158            textwrap.dedent(
159                """\
160                <?xml version="1.0" encoding="UTF-8"?>
161                <manifest>
162                    <default revision="main" remote="aosp" />
163
164                    <project path="external/project" remote="origin" />
165                </manifest>
166                """
167            )
168        )
169        manifest = ManifestParser(manifest_path).parse()
170        assert manifest.project_with_path("external/project").remote == "origin"
171
172    def test_revision_explicit(self, tmp_path: Path) -> None:
173        """Tests that the project revision is used when defined."""
174        manifest_path = tmp_path / "manifest.xml"
175        manifest_path.write_text(
176            textwrap.dedent(
177                """\
178                <?xml version="1.0" encoding="UTF-8"?>
179                <manifest>
180                    <default revision="main" remote="aosp" />
181
182                    <project path="external/project" revision="master" />
183                </manifest>
184                """
185            )
186        )
187        manifest = ManifestParser(manifest_path).parse()
188        assert manifest.project_with_path("external/project").revision == "master"
189
190    def test_path_explicit(self, tmp_path: Path) -> None:
191        """Tests that the project path is used when defined."""
192        manifest_path = tmp_path / "manifest.xml"
193        manifest_path.write_text(
194            textwrap.dedent(
195                """\
196                <?xml version="1.0" encoding="UTF-8"?>
197                <manifest>
198                    <default revision="main" remote="aosp" />
199
200                    <project name="external/project" path="other/path" />
201                </manifest>
202                """
203            )
204        )
205        manifest = ManifestParser(manifest_path).parse()
206        assert manifest.project_with_path("other/path") is not None
207
208class TestManifest:
209    """Tests for Manifest."""
210
211    def test_for_tree(self, repo_tree: Path) -> None:
212        """Tests the Manifest.for_tree constructor."""
213        manifest_dir = Path(repo_tree / ".repo/manifests")
214        manifest_dir.mkdir()
215        (manifest_dir / "default.xml").write_text(
216            textwrap.dedent(
217                """\
218                <?xml version="1.0" encoding="UTF-8"?>
219                <manifest>
220                    <default remote="aosp" revision="main" />
221
222                    <project path="external/a" />
223                    <project path="external/b" />
224                    <project path="external/c" />
225                </manifest>
226                """
227            )
228        )
229        manifest = Manifest.for_tree(repo_tree)
230        assert len(manifest.projects_by_path) == 3
231
232    def test_project_with_path(self, repo_tree: Path) -> None:
233        """Tests that Manifest.project_with_path returns the correct project."""
234        manifest_dir = Path(repo_tree / ".repo/manifests")
235        manifest_dir.mkdir()
236        (manifest_dir / "default.xml").write_text(
237            textwrap.dedent(
238                """\
239                <?xml version="1.0" encoding="UTF-8"?>
240                <manifest>
241                    <default remote="aosp" revision="main" />
242
243                    <project path="external/a" />
244                    <project path="external/b" />
245                    <project path="external/c" />
246                </manifest>
247                """
248            )
249        )
250        manifest = Manifest.for_tree(repo_tree)
251        assert manifest.project_with_path("external/b").path == "external/b"
252
253    def test_project_with_path_missing(self, repo_tree: Path) -> None:
254        """Tests that Manifest.project_with_path raises an error when not found."""
255        manifest_dir = Path(repo_tree / ".repo/manifests")
256        manifest_dir.mkdir()
257        (manifest_dir / "default.xml").write_text(
258            textwrap.dedent(
259                """\
260                <?xml version="1.0" encoding="UTF-8"?>
261                <manifest>
262                    <default remote="aosp" revision="main" />
263
264                    <project path="external/a" />
265                    <project path="external/b" />
266                    <project path="external/c" />
267                </manifest>
268                """
269            )
270        )
271        manifest = Manifest.for_tree(repo_tree)
272        with pytest.raises(KeyError):
273            manifest.project_with_path("external/d")
274