# Copyright (C) 2026 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. """Unittests for the command.py module.""" from typing import Iterable, List, Optional import pytest from command import Command class FakeProject: """Minimal project double for Command.GetProjects tests.""" def __init__( self, name, relpath, *, gitdir=None, derived_subprojects=None, sync_s=False, exists: bool = True, ): self.name = name self.relpath = relpath self.worktree = f"/work/{relpath}" self.manifest = None self.gitdir = gitdir or f"/git/{relpath}" self.sync_s = sync_s self.Exists = exists self._derived_subprojects = derived_subprojects or [] def GetDerivedSubprojects(self): return list(self._derived_subprojects) def MatchesGroups(self, _groups): return True def RelPath(self, local=True): return self.relpath class FakeManifest: """Minimal manifest double for Command.GetProjects tests.""" def __init__( self, projects: Iterable[FakeProject], *, all_projects: Optional[Iterable[FakeProject]] = None, effective_groups: str = "default", ): self.projects = list(projects) self.all_projects = ( list(self.projects) if all_projects is None else list(all_projects) ) self._effective_groups = effective_groups self.path_prefix = "" # all_projects may include projects owned by child manifests, # so only set this manifest on its direct projects. for project in self.projects: self._set_project_manifest(project) def _set_project_manifest(self, project: FakeProject) -> None: project.manifest = self for subproject in project.GetDerivedSubprojects(): self._set_project_manifest(subproject) def GetManifestGroupsStr(self): return self._effective_groups def GetProjectsWithName( self, name: str, all_manifests: bool = False ) -> List[FakeProject]: projects = self.all_projects if all_manifests else self.projects return [project for project in projects if project.name == name] class GroupMatchingFakeProject(FakeProject): """Fake project with predictable group matches for GetProjects tests. This lets the tests check which groups GetProjects uses without reimplementing Project.MatchesGroups. """ def __init__( self, name: str, relpath: str, *, matching_groups: Iterable[str], ): super().__init__(name, relpath) self._matching_groups = set(matching_groups) def MatchesGroups(self, groups: Iterable[str]) -> bool: return bool(self._matching_groups.intersection(groups)) def test_get_projects_keeps_derived_subprojects_for_repeated_repo(): """Derived subprojects are keyed by checkout path, not repo identity.""" submodule_a = FakeProject( "submodule", "src/one/submodule", gitdir="/shared/modules/submodule.git", ) submodule_b = FakeProject( "submodule", "src/two/submodule", gitdir="/shared/modules/submodule.git", ) project_a = FakeProject( "project", "src/one", derived_subprojects=[submodule_a], sync_s=True, ) project_b = FakeProject( "project", "src/two", derived_subprojects=[submodule_b], sync_s=True, ) manifest = FakeManifest([project_a, project_b]) cmd = Command(manifest=manifest) projects = cmd.GetProjects([]) assert set(projects) == {project_a, project_b, submodule_a, submodule_b} @pytest.mark.parametrize( "submodules_ok, sync_s, includes_submodule", [ (None, False, False), (None, True, True), (True, False, True), (True, True, True), (False, False, False), (False, True, False), ], ) def test_get_projects_submodule_override( submodules_ok, sync_s, includes_submodule ): """The CLI override takes precedence over a project's sync-s setting.""" submodule = FakeProject("submodule", "project/submodule") project = FakeProject( "project", "project", derived_subprojects=[submodule], sync_s=sync_s, ) cmd = Command(manifest=FakeManifest([project])) projects = cmd.GetProjects([], submodules_ok=submodules_ok) assert (submodule in projects) is includes_submodule @pytest.mark.parametrize( ("groups", "expected_relpaths"), [ (None, ["outer", "sub/child"]), ("", ["outer", "sub/child"]), ("override-group", ["sub/override"]), ], ids=("groups-omitted", "groups-empty", "explicit-override"), ) def test_get_projects_uses_groups_from_each_manifest_unless_overridden( groups: Optional[str], expected_relpaths: List[str], ) -> None: """Use each manifest's effective groups unless the caller overrides them.""" outer_project = GroupMatchingFakeProject( "outer", "outer", matching_groups={"outer-group"}, ) # Both child projects also match "outer". Reusing the outer manifest's # groups would therefore select both child projects. child_project = GroupMatchingFakeProject( "child", "sub/child", matching_groups={"outer-group", "child-group"}, ) override_project = GroupMatchingFakeProject( "override", "sub/override", matching_groups={"outer-group", "override-group"}, ) child_manifest = FakeManifest( [child_project, override_project], effective_groups="child-group", ) outer_manifest = FakeManifest( [outer_project], all_projects=[outer_project, *child_manifest.projects], effective_groups="outer-group", ) cmd = Command(manifest=outer_manifest) projects = cmd.GetProjects( [], manifest=outer_manifest, groups=groups, all_manifests=True, ) assert [project.relpath for project in projects] == expected_relpaths def test_get_projects_by_name_uses_groups_from_each_manifest() -> None: """Name matches use the groups from each project's owning manifest.""" outer_project = GroupMatchingFakeProject( "shared", "outer/shared", matching_groups={"outer-group"}, ) child_project = GroupMatchingFakeProject( "shared", "sub/shared", matching_groups={"child-group"}, ) child_manifest = FakeManifest( [child_project], effective_groups="child-group", ) outer_manifest = FakeManifest( [outer_project], all_projects=[outer_project, *child_manifest.projects], effective_groups="outer-group", ) cmd = Command(manifest=outer_manifest) projects = cmd.GetProjects( ["shared"], manifest=outer_manifest, all_manifests=True, ) assert [project.relpath for project in projects] == [ "outer/shared", "sub/shared", ] def test_find_projects_uses_groups_from_each_manifest() -> None: """Use each manifest's effective groups for regex selection.""" outer_project = GroupMatchingFakeProject( "match-outer", "outer", matching_groups={"outer-group"}, ) child_project = GroupMatchingFakeProject( "match-child", "sub/child", matching_groups={"child-group"}, ) excluded_project = GroupMatchingFakeProject( "match-excluded", "sub/excluded", matching_groups={"other-group"}, ) child_manifest = FakeManifest( [child_project, excluded_project], effective_groups="child-group", ) child_manifest.path_prefix = "sub" outer_manifest = FakeManifest( [outer_project], all_projects=[ outer_project, *child_manifest.projects, ], effective_groups="outer-group", ) outer_manifest.outer_client = outer_manifest cmd = Command(manifest=outer_manifest) projects = cmd.FindProjects(["match"], all_manifests=True) assert [project.relpath for project in projects] == [ "outer", "sub/child", ] def test_find_projects_uses_explicit_groups() -> None: """Use explicit groups for regex selection.""" default_project = GroupMatchingFakeProject( "default", "default", matching_groups={"default-group"}, ) override_project = GroupMatchingFakeProject( "override", "override", matching_groups={"override-group"}, ) manifest = FakeManifest( [default_project, override_project], effective_groups="default-group", ) cmd = Command(manifest=manifest) projects = cmd.FindProjects( ["override"], groups="override-group", ) assert projects == [override_project] def test_find_projects_allows_missing_projects() -> None: """Allow regex selection to include projects without a checkout.""" project = FakeProject( "missing", "missing", exists=False, ) cmd = Command(manifest=FakeManifest([project])) projects = cmd.FindProjects( ["missing"], missing_ok=True, ) assert projects == [project]