diff options
author | Android Build Coastguard Worker <android-build-coastguard-worker@google.com> | 2023-12-15 00:44:53 +0000 |
---|---|---|
committer | Android Build Coastguard Worker <android-build-coastguard-worker@google.com> | 2023-12-15 00:44:53 +0000 |
commit | 966c01016bbf451e29cd9c135773dbf8316451b0 (patch) | |
tree | 25d96943bba27c6e95d1dad56c91fe194ee583f4 | |
parent | ff21b28b584e3ff7eb9325fa9e4cb385f6932269 (diff) | |
parent | 522e58fe3f52001eab32107bb7bd4733c984fcb4 (diff) | |
download | external_updater-android14-qpr2-s1-release.tar.gz |
Snap for 11220357 from 522e58fe3f52001eab32107bb7bd4733c984fcb4 to 24Q1-releaseandroid-14.0.0_r33android-14.0.0_r32android-14.0.0_r31android-14.0.0_r30android-14.0.0_r29android14-qpr2-s3-releaseandroid14-qpr2-s2-releaseandroid14-qpr2-s1-releaseandroid14-qpr2-release
Change-Id: Ia60973fed5f9d9013124c442235a1da8739f3ff9
-rw-r--r-- | Android.bp | 7 | ||||
-rw-r--r-- | tests/__init__.py | 0 | ||||
-rw-r--r-- | tests/endtoend/__init__.py | 0 | ||||
-rw-r--r-- | tests/endtoend/conftest.py | 85 | ||||
-rw-r--r-- | tests/endtoend/test_check.py | 86 | ||||
-rw-r--r-- | tests/endtoend/treebuilder/__init__.py | 4 | ||||
-rw-r--r-- | tests/endtoend/treebuilder/fakeproject.py | 94 | ||||
-rw-r--r-- | tests/endtoend/treebuilder/fakerepo.py | 108 | ||||
-rw-r--r-- | tests/endtoend/treebuilder/gitrepo.py | 114 | ||||
-rw-r--r-- | tests/endtoend/treebuilder/test_fakeproject.py | 61 | ||||
-rw-r--r-- | tests/endtoend/treebuilder/test_fakerepo.py | 85 | ||||
-rw-r--r-- | tests/endtoend/treebuilder/test_gitrepo.py | 67 | ||||
-rw-r--r-- | tests/endtoend/treebuilder/test_treebuilder.py | 38 | ||||
-rw-r--r-- | tests/endtoend/treebuilder/treebuilder.py | 34 |
14 files changed, 783 insertions, 0 deletions
@@ -100,3 +100,10 @@ python_test_host { unit_test: true, }, } + +// The tests in tests/endtoend are not built as a Soong module because we can't +// run those tests via python_test_host. It's an end-to-end test so it needs to +// run `repo`, but `repo init` will try to clone the real repo source (the thing +// in PATH is just a launcher), which our Python can't do (the SSL module's +// certificate validation is not configured for that). Run those tests with +// `pytest tests/endtoend`. diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/tests/__init__.py diff --git a/tests/endtoend/__init__.py b/tests/endtoend/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/tests/endtoend/__init__.py diff --git a/tests/endtoend/conftest.py b/tests/endtoend/conftest.py new file mode 100644 index 0000000..68e1f4e --- /dev/null +++ b/tests/endtoend/conftest.py @@ -0,0 +1,85 @@ +# +# Copyright (C) 2023 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +"""Common tools for end-to-end tests.""" +import subprocess +from pathlib import Path + +import pytest + +from .treebuilder import TreeBuilder + +THIS_DIR = Path(__file__).parent.resolve() +EXTERNAL_UPDATER_DIR = THIS_DIR.parent.parent +ANDROID_DIR = EXTERNAL_UPDATER_DIR.parent.parent + + +def pytest_addoption(parser: pytest.Parser) -> None: + """Add custom options to pytest.""" + parser.addoption( + "--build-updater", + action="store_true", + default=True, + help=( + "Build external_updater before running tests. This is the default behavior." + ), + ) + parser.addoption( + "--no-build-updater", + action="store_false", + dest="build_updater", + help=( + "Do not build external_updater before running tests. Only use this option " + "if you've manually built external_updater. It will make test startup " + "faster." + ), + ) + + +@pytest.fixture(name="should_build_updater", scope="session") +def should_build_updater_fixture(request: pytest.FixtureRequest) -> bool: + """True if external_updater should be built before running tests.""" + return request.config.getoption("--build-updater") + + +# Session scope means that this fixture will only run the first time it's used. We don't +# want to re-run soong for every test because it's horrendously slow to do so. +@pytest.fixture(scope="session") +def updater_cmd(should_build_updater: bool) -> list[str]: + """The command to run for external_updater. + + The result is the prefix of the command that should be used with subprocess.run or + similar. + """ + # Running updater.sh should be a more accurate test, but doing so isn't really + # feasible with a no-op `m external_updater` taking ~10 seconds. Doing that would + # add 10 seconds to every test. Build the updater once for the first thing that + # requires it (that's the "session" scope above) and run the PAR directly. + if should_build_updater: + subprocess.run( + [ + ANDROID_DIR / "build/soong/soong_ui.bash", + "--make-mode", + "external_updater", + ], + check=True, + ) + return [str("external_updater")] + + +@pytest.fixture(name="tree_builder") +def tree_builder_fixture(tmp_path: Path) -> TreeBuilder: + """Creates a TreeBuilder for making test repo trees.""" + return TreeBuilder(tmp_path) diff --git a/tests/endtoend/test_check.py b/tests/endtoend/test_check.py new file mode 100644 index 0000000..74a00ae --- /dev/null +++ b/tests/endtoend/test_check.py @@ -0,0 +1,86 @@ +# +# Copyright (C) 2023 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +"""End-to-end tests for external_updater.""" +import subprocess +from pathlib import Path + +from .treebuilder import TreeBuilder + + +class TestCheck: + """Tests for `external_updater check`.""" + + def check(self, updater_cmd: list[str], paths: list[Path]) -> str: + """Runs `external_updater check` with the given arguments. + + Returns: + The output of the command. + """ + return subprocess.run( + updater_cmd + ["check"] + [str(p) for p in paths], + check=True, + capture_output=True, + text=True, + ).stdout + + def test_git_up_to_date( + self, tree_builder: TreeBuilder, updater_cmd: list[str] + ) -> None: + """Tests that up-to-date projects are identified.""" + tree = tree_builder.repo_tree("tree") + a = tree.project("platform/external/foo", "external/foo") + a.upstream.commit( + "Add README.md.", + update_files={ + "README.md": "Hello, world!\n", + }, + ) + tree.create_manifest_repo() + a.initial_import() + tree.init_and_sync() + output = self.check(updater_cmd, [a.local.path]) + current_version = a.upstream.head() + assert output == ( + f"Checking {a.local.path}. Current version: {current_version}. Latest " + f"version: {current_version} Up to date.\n" + ) + + def test_git_out_of_date( + self, tree_builder: TreeBuilder, updater_cmd: list[str] + ) -> None: + """Tests that out-of-date projects are identified.""" + tree = tree_builder.repo_tree("tree") + a = tree.project("platform/external/foo", "external/foo") + a.upstream.commit( + "Add README.md.", + update_files={ + "README.md": "Hello, world!\n", + }, + ) + tree.create_manifest_repo() + a.initial_import() + current_version = a.upstream.head() + tree.init_and_sync() + a.upstream.commit( + "Update the project.", + update_files={"README.md": "This project is deprecated.\n"}, + ) + output = self.check(updater_cmd, [a.local.path]) + latest_version = a.upstream.head() + assert output == ( + f"Checking {a.local.path}. Current version: {current_version}. Latest " + f"version: {latest_version} Out of date!\n" + ) diff --git a/tests/endtoend/treebuilder/__init__.py b/tests/endtoend/treebuilder/__init__.py new file mode 100644 index 0000000..19dce12 --- /dev/null +++ b/tests/endtoend/treebuilder/__init__.py @@ -0,0 +1,4 @@ +"""Tools for creating temporary repo trees in tests.""" +from .fakeproject import FakeProject +from .fakerepo import FakeRepo +from .treebuilder import TreeBuilder diff --git a/tests/endtoend/treebuilder/fakeproject.py b/tests/endtoend/treebuilder/fakeproject.py new file mode 100644 index 0000000..e0a72a9 --- /dev/null +++ b/tests/endtoend/treebuilder/fakeproject.py @@ -0,0 +1,94 @@ +# +# Copyright (C) 2023 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +"""Git repository fakes for use in tests.""" +import textwrap +from pathlib import Path + +from .gitrepo import GitRepo + + +class FakeProject: # pylint: disable=too-few-public-methods + """A collection of git repositories for use in tests. + + This shouldn't be used directly. Use the tree_builder fixture. + + A project contains the three git repos that are used in the Android upstream + mirroring process: + + 1. The true upstream repository. Where the third-party commits their work. + 2. The Android repository. Where Gerrit submits work. + 3. The local repository. This is where work happens before being uploaded to Gerrit. + """ + + def __init__( + self, tree_path: Path, upstream_path: Path, android_mirror_path: Path + ) -> None: + self.local = GitRepo(tree_path) + self.upstream = GitRepo(upstream_path) + self.android_mirror = GitRepo(android_mirror_path) + self._initialize_repo(self.upstream) + + def _initialize_repo(self, repo: GitRepo) -> None: + """Create a git repo and initial commit in the upstream repository.""" + repo.init(branch_name="main") + repo.commit("Initial commit.", allow_empty=True) + + def initial_import(self) -> None: + """Perform the initial import of the upstream repo into the mirror repo. + + These are an approximation of the steps that would be taken for the initial + import as part of go/android3p: + + * A new git repo is created with a single empty commit. That commit is **not** + present in the upstream repo. Android imports begin with unrelated histories. + * The upstream repository is merged into the local tree. + * METADATA, NOTICE, MODULE_LICENSE_*, and OWNERS files are added to the local + tree. We only bother with METADATA for now since that's all the tests need. + """ + self.android_mirror.init() + self.android_mirror.commit("Initial commit.", allow_empty=True) + + upstream_sha = self.upstream.head() + self.android_mirror.fetch(self.upstream) + self.android_mirror.merge( + "FETCH_HEAD", allow_fast_forward=False, allow_unrelated_histories=True + ) + + self.android_mirror.commit( + "Add metadata files.", + update_files={ + "OWNERS": "nobody", + "METADATA": textwrap.dedent( + f"""\ + name: "test" + description: "It's a test." + third_party {{ + license_type: UNENCUMBERED + last_upgrade_date {{ + year: 2023 + month: 12 + day: 1 + }} + identifier {{ + type: "GIT" + value: "{self.upstream.path.as_uri()}" + version: "{upstream_sha}" + }} + }} + """ + ), + }, + ) diff --git a/tests/endtoend/treebuilder/fakerepo.py b/tests/endtoend/treebuilder/fakerepo.py new file mode 100644 index 0000000..6517afd --- /dev/null +++ b/tests/endtoend/treebuilder/fakerepo.py @@ -0,0 +1,108 @@ +# +# Copyright (C) 2023 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +"""repo tree fakes for use in tests.""" +import contextlib +import subprocess +from pathlib import Path +from xml.etree import ElementTree + +from .fakeproject import FakeProject + + +class FakeRepo: + """A repo tree for use in tests. + + This shouldn't be used directly. Use the tree_builder fixture. + """ + + def __init__(self, temp_dir: Path) -> None: + self.root = temp_dir / "tree" + self.mirror_dir = temp_dir / "mirrors" + self.upstream_dir = temp_dir / "upstreams" + self.manifest_repo = temp_dir / "manifest" + self.projects: list[FakeProject] = [] + self._used_git_subpaths: set[str] = set() + self._used_tree_subpaths: set[str] = set() + + def project(self, git_subpath: str, tree_subpath: str) -> FakeProject: + """Creates a new project in the repo.""" + if git_subpath in self._used_git_subpaths: + raise KeyError(f"A project with git path {git_subpath} already exists") + if tree_subpath in self._used_tree_subpaths: + raise KeyError(f"A project with tree path {tree_subpath} already exists") + project = FakeProject( + self.root / tree_subpath, + self.upstream_dir / tree_subpath, + self.mirror_dir / git_subpath, + ) + self.projects.append(project) + self._used_git_subpaths.add(git_subpath) + self._used_tree_subpaths.add(tree_subpath) + return project + + def init_and_sync(self) -> None: + """Runs repo init and repo sync to clone the repo tree.""" + self.root.mkdir(parents=True) + with contextlib.chdir(self.root): + subprocess.run( + ["repo", "init", "-c", "-u", str(self.manifest_repo), "-b", "main"], + check=True, + ) + subprocess.run(["repo", "sync", "-c"], check=True) + + def create_manifest_repo(self) -> None: + """Creates the git repo for the manifest, commits the manifest XML.""" + self.manifest_repo.mkdir(parents=True) + with contextlib.chdir(self.manifest_repo): + subprocess.run(["git", "init"], check=True) + Path("default.xml").write_bytes( + ElementTree.tostring(self._create_manifest_xml(), encoding="utf-8") + ) + subprocess.run(["git", "add", "default.xml"], check=True) + subprocess.run(["git", "commit", "-m", "Initial commit."], check=True) + + def _create_manifest_xml(self) -> ElementTree.Element: + # Example manifest: + # + # <manifest> + # <remote name="aosp" fetch="$URL" /> + # <default revision="main" remote="aosp" /> + # + # <project path="external/project" name="platform/external/project" + # revision="master" remote="goog" /> + # ... + # </manifest> + # + # The revision and remote attributes of project are optional. + root = ElementTree.Element("manifest") + ElementTree.SubElement( + root, + "remote", + {"name": "aosp", "fetch": self.mirror_dir.resolve().as_uri()}, + ) + ElementTree.SubElement(root, "default", {"revision": "main", "remote": "aosp"}) + for project in self.projects: + ElementTree.SubElement( + root, + "project", + { + "path": str(project.local.path.relative_to(self.root)), + "name": str( + project.android_mirror.path.relative_to(self.mirror_dir) + ), + }, + ) + return root diff --git a/tests/endtoend/treebuilder/gitrepo.py b/tests/endtoend/treebuilder/gitrepo.py new file mode 100644 index 0000000..46bba86 --- /dev/null +++ b/tests/endtoend/treebuilder/gitrepo.py @@ -0,0 +1,114 @@ +# +# Copyright (C) 2023 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +"""APIs for interacting with git repositories.""" +# TODO: This should be partially merged with the git_utils APIs. +# The bulk of this should be lifted out of the tests and used by the rest of +# external_updater, but we'll want to keep a few of the APIs just in the tests because +# they're not particularly sensible elsewhere (specifically the shorthand for commit +# with the update_files and delete_files arguments). It's probably easiest to do that by +# reworking the git_utils APIs into a class like this and then deriving this one from +# that. +from __future__ import annotations + +import subprocess +from pathlib import Path + + +class GitRepo: + """A git repository for use in tests.""" + + def __init__(self, path: Path) -> None: + self.path = path + + def run(self, command: list[str]) -> str: + """Runs the given git command in the repository, returning the output.""" + return subprocess.run( + ["git", "-C", str(self.path)] + command, + check=True, + capture_output=True, + text=True, + ).stdout + + def init(self, branch_name: str | None = None) -> None: + """Initializes a new git repository.""" + self.path.mkdir(parents=True) + cmd = ["init"] + if branch_name is not None: + cmd.extend(["-b", branch_name]) + self.run(cmd) + + def head(self) -> str: + """Returns the SHA of the current HEAD.""" + return self.run(["rev-parse", "HEAD"]).strip() + + def fetch(self, ref_or_repo: str | GitRepo) -> None: + """Fetches the given ref or repo.""" + if isinstance(ref_or_repo, GitRepo): + ref_or_repo = str(ref_or_repo.path) + self.run(["fetch", ref_or_repo]) + + def commit( + self, + message: str, + allow_empty: bool = False, + update_files: dict[str, str] | None = None, + delete_files: set[str] | None = None, + ) -> None: + """Create a commit in the repository.""" + if update_files is None: + update_files = {} + if delete_files is None: + delete_files = set() + + for delete_file in delete_files: + self.run(["rm", delete_file]) + + for update_file, contents in update_files.items(): + (self.path / update_file).write_text(contents, encoding="utf-8") + self.run(["add", update_file]) + + commit_cmd = ["commit", "-m", message] + if allow_empty: + commit_cmd.append("--allow-empty") + self.run(commit_cmd) + + def merge( + self, + ref: str, + allow_fast_forward: bool = True, + allow_unrelated_histories: bool = False, + ) -> None: + """Merges the upstream ref into the repo.""" + cmd = ["merge"] + if not allow_fast_forward: + cmd.append("--no-ff") + if allow_unrelated_histories: + cmd.append("--allow-unrelated-histories") + self.run(cmd + [ref]) + + def commit_message_at_revision(self, revision: str) -> str: + """Returns the commit message of the given revision.""" + # %B is the raw commit body + # %- eats the separator newline + # Note that commit messages created with `git commit` will always end with a + # trailing newline. + return self.run(["log", "--format=%B%-", "-n1", revision]) + + def file_contents_at_revision(self, revision: str, path: str) -> str: + """Returns the commit message of the given revision.""" + # %B is the raw commit body + # %- eats the separator newline + return self.run(["show", "--format=%B%-", f"{revision}:{path}"]) diff --git a/tests/endtoend/treebuilder/test_fakeproject.py b/tests/endtoend/treebuilder/test_fakeproject.py new file mode 100644 index 0000000..09aadb2 --- /dev/null +++ b/tests/endtoend/treebuilder/test_fakeproject.py @@ -0,0 +1,61 @@ +# +# Copyright (C) 2023 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +"""Tests for fakeproject.""" +from pathlib import Path + +from .fakeproject import FakeProject + + +class TestFakeProject: + """Tests for fakeproject.FakeProject.""" + + def test_constructor_initializes_upstream_repo(self, tmp_path: Path) -> None: + """Tests that the constructor initializes the "upstream" git repo.""" + project = FakeProject( + tmp_path / "local", tmp_path / "upstream", tmp_path / "mirror" + ) + assert ( + project.upstream.commit_message_at_revision("HEAD") == "Initial commit.\n" + ) + + def test_initial_import(self, tmp_path: Path) -> None: + """Tests that initial_import merges and creates metadata files.""" + project = FakeProject( + tmp_path / "local", tmp_path / "upstream", tmp_path / "mirror" + ) + project.upstream.commit( + "Add README.md.", update_files={"README.md": "Hello, world!"} + ) + + upstream_sha = project.upstream.head() + project.initial_import() + + # The import is done in the mirror repository. The cloned repository in the repo + # tree should not be created until init_and_sync() is called. + assert not project.local.path.exists() + + assert ( + project.android_mirror.commit_message_at_revision("HEAD^") + == f"Merge {project.upstream.path}\n" + ) + assert ( + project.android_mirror.commit_message_at_revision("HEAD") + == "Add metadata files.\n" + ) + metadata = project.android_mirror.file_contents_at_revision("HEAD", "METADATA") + assert 'type: "GIT"' in metadata + assert f'value: "{project.upstream.path.as_uri()}"' in metadata + assert f'version: "{upstream_sha}"' in metadata diff --git a/tests/endtoend/treebuilder/test_fakerepo.py b/tests/endtoend/treebuilder/test_fakerepo.py new file mode 100644 index 0000000..92fb22b --- /dev/null +++ b/tests/endtoend/treebuilder/test_fakerepo.py @@ -0,0 +1,85 @@ +# +# Copyright (C) 2023 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +"""Tests for fakerepo.""" +from pathlib import Path +from xml.etree import ElementTree + +import pytest + +from .fakerepo import FakeRepo + + +class TestFakeRepo: + """Tests for fakerepo.FakeRepo.""" + + def test_project_created(self, tmp_path: Path) -> None: + """Tests that FakeRepo.project creates a new FakeProject.""" + repo = FakeRepo(tmp_path) + project = repo.project("platform/foo", "foo") + assert project.local.path == repo.root / "foo" + assert project.android_mirror.path == repo.mirror_dir / "platform/foo" + assert project.upstream.path == repo.upstream_dir / "foo" + + def test_project_error_if_path_reused(self, tmp_path: Path) -> None: + """Tests that KeyError is raised if a project path is reused.""" + repo = FakeRepo(tmp_path) + repo.project("platform/foo", "foo") + repo.project("platform/bar", "bar") + with pytest.raises(KeyError): + repo.project("platform/baz", "foo") + with pytest.raises(KeyError): + repo.project("platform/foo", "baz") + + def test_create_manifest_repo_xml_structure(self, tmp_path: Path) -> None: + """Tests that the correct manifest XML is created.""" + repo = FakeRepo(tmp_path) + repo.project("platform/foo", "foo") + repo.project("platform/external/bar", "external/bar") + repo.create_manifest_repo() + + manifest_path = repo.manifest_repo / "default.xml" + assert manifest_path.exists() + root = ElementTree.parse(manifest_path) + remotes = root.findall("./remote") + assert len(remotes) == 1 + remote = remotes[0] + assert remote.attrib["name"] == "aosp" + assert remote.attrib["fetch"] == repo.mirror_dir.as_uri() + + defaults = root.findall("./default") + assert len(defaults) == 1 + default = defaults[0] + assert default.attrib["remote"] == "aosp" + assert default.attrib["revision"] == "main" + + projects = root.findall("./project") + assert len(projects) == 2 + + assert projects[0].attrib["name"] == "platform/foo" + assert projects[0].attrib["path"] == "foo" + + assert projects[1].attrib["name"] == "platform/external/bar" + assert projects[1].attrib["path"] == "external/bar" + + def test_init_and_sync(self, tmp_path: Path) -> None: + """Tests that init_and_sync initializes and syncs the tree.""" + repo = FakeRepo(tmp_path) + project = repo.project("platform/foo", "foo") + repo.create_manifest_repo() + project.initial_import() + repo.init_and_sync() + + assert project.local.head() == project.android_mirror.head() diff --git a/tests/endtoend/treebuilder/test_gitrepo.py b/tests/endtoend/treebuilder/test_gitrepo.py new file mode 100644 index 0000000..7d2daba --- /dev/null +++ b/tests/endtoend/treebuilder/test_gitrepo.py @@ -0,0 +1,67 @@ +# +# Copyright (C) 2023 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +"""Tests for gitrepo.""" +import subprocess +from pathlib import Path + +from .gitrepo import GitRepo + + +class TestGitRepo: + """Tests for gitrepo.GitRepo.""" + + def test_commit_adds_files(self, tmp_path: Path) -> None: + """Tests that new files in commit are added to the repo.""" + repo = GitRepo(tmp_path / "repo") + repo.init() + repo.commit("Add README.md.", update_files={"README.md": "Hello, world!"}) + assert repo.commit_message_at_revision("HEAD") == "Add README.md.\n" + assert repo.file_contents_at_revision("HEAD", "README.md") == "Hello, world!" + + def test_commit_updates_files(self, tmp_path: Path) -> None: + """Tests that updated files in commit are modified.""" + repo = GitRepo(tmp_path / "repo") + repo.init() + repo.commit("Add README.md.", update_files={"README.md": "Hello, world!"}) + repo.commit("Update README.md.", update_files={"README.md": "Goodbye, world!"}) + assert repo.commit_message_at_revision("HEAD^") == "Add README.md.\n" + assert repo.file_contents_at_revision("HEAD^", "README.md") == "Hello, world!" + assert repo.commit_message_at_revision("HEAD") == "Update README.md.\n" + assert repo.file_contents_at_revision("HEAD", "README.md") == "Goodbye, world!" + + def test_commit_deletes_files(self, tmp_path: Path) -> None: + """Tests that files deleted by commit are removed from the repo.""" + repo = GitRepo(tmp_path / "repo") + repo.init() + repo.commit("Add README.md.", update_files={"README.md": "Hello, world!"}) + repo.commit("Remove README.md.", delete_files={"README.md"}) + assert repo.commit_message_at_revision("HEAD^") == "Add README.md.\n" + assert repo.file_contents_at_revision("HEAD^", "README.md") == "Hello, world!" + assert repo.commit_message_at_revision("HEAD") == "Remove README.md.\n" + assert ( + subprocess.run( + [ + "git", + "-C", + str(repo.path), + "ls-files", + "--error-unmatch", + "README.md", + ], + check=False, + ).returncode + != 0 + ) diff --git a/tests/endtoend/treebuilder/test_treebuilder.py b/tests/endtoend/treebuilder/test_treebuilder.py new file mode 100644 index 0000000..1a5f04d --- /dev/null +++ b/tests/endtoend/treebuilder/test_treebuilder.py @@ -0,0 +1,38 @@ +# +# Copyright (C) 2023 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +"""Tests for treebuilder.""" +from pathlib import Path + +import pytest + +from .treebuilder import TreeBuilder + + +class TestTreeBuilder: + """Tests for treebuilder.TreeBuilder""" + + def test_repo_tree(self, tmp_path: Path) -> None: + """Tests that a TreeBuilder is created.""" + builder = TreeBuilder(tmp_path) + assert builder.temp_dir == tmp_path + + def test_repo_tree_error_if_name_reused(self, tmp_path: Path) -> None: + """Tests that KeyError is raised if a tree name is reused.""" + builder = TreeBuilder(tmp_path) + builder.repo_tree("foo") + builder.repo_tree("bar") + with pytest.raises(KeyError): + builder.repo_tree("foo") diff --git a/tests/endtoend/treebuilder/treebuilder.py b/tests/endtoend/treebuilder/treebuilder.py new file mode 100644 index 0000000..2218d4e --- /dev/null +++ b/tests/endtoend/treebuilder/treebuilder.py @@ -0,0 +1,34 @@ +# +# Copyright (C) 2023 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +"""Builder for creating repo trees for use in testing.""" +from pathlib import Path + +from .fakerepo import FakeRepo + + +class TreeBuilder: # pylint: disable=too-few-public-methods + """Creates test repo trees in a temporary directory.""" + + def __init__(self, temp_dir: Path) -> None: + self.temp_dir = temp_dir + self._trees: set[str] = set() + + def repo_tree(self, name: str) -> FakeRepo: + """Creates a new repo tree with the given name.""" + if name in self._trees: + raise KeyError(f"A repo tree named {name} already exists") + self._trees.add(name) + return FakeRepo(self.temp_dir / name) |