# Copyright (C) 2025 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. import os import shutil from unittest import mock import pytest import project from subcmds import wipe def _create_mock_project(tempdir, name, objdir_path=None, has_changes=False): """Creates a mock project with necessary attributes and directories.""" worktree = os.path.join(tempdir, name) gitdir = os.path.join(tempdir, ".repo/projects", f"{name}.git") if objdir_path: objdir = objdir_path else: objdir = os.path.join(tempdir, ".repo/project-objects", f"{name}.git") os.makedirs(worktree, exist_ok=True) os.makedirs(gitdir, exist_ok=True) os.makedirs(objdir, exist_ok=True) proj = project.Project( manifest=mock.MagicMock(), name=name, remote=mock.MagicMock(), gitdir=gitdir, objdir=objdir, worktree=worktree, relpath=name, revisionExpr="main", revisionId="abcd", ) proj.HasChanges = mock.MagicMock(return_value=has_changes) def side_effect_delete_worktree(force=False, verbose=False): if os.path.exists(proj.worktree): shutil.rmtree(proj.worktree) if os.path.exists(proj.gitdir): shutil.rmtree(proj.gitdir) return True proj.DeleteWorktree = mock.MagicMock( side_effect=side_effect_delete_worktree ) return proj def _run_wipe(all_projects, projects_to_wipe_names, options=None): """Helper to run the Wipe command with mocked projects.""" cmd = wipe.Wipe() cmd.manifest = mock.MagicMock() def get_projects_mock(projects, all_manifests=False, **kwargs): if projects is None: return all_projects names_to_find = set(projects) return [p for p in all_projects if p.name in names_to_find] cmd.GetProjects = mock.MagicMock(side_effect=get_projects_mock) if options is None: options = [] opts = cmd.OptionParser.parse_args(options + projects_to_wipe_names)[0] cmd.CommonValidateOptions(opts, projects_to_wipe_names) cmd.ValidateOptions(opts, projects_to_wipe_names) cmd.Execute(opts, projects_to_wipe_names) def test_wipe_single_unshared_project(tmp_path): """Test wiping a single project that is not shared.""" p1 = _create_mock_project(str(tmp_path), "project/one") _run_wipe([p1], ["project/one"]) assert not os.path.exists(p1.worktree) assert not os.path.exists(p1.gitdir) assert not os.path.exists(p1.objdir) def test_wipe_multiple_unshared_projects(tmp_path): """Test wiping multiple projects that are not shared.""" p1 = _create_mock_project(str(tmp_path), "project/one") p2 = _create_mock_project(str(tmp_path), "project/two") _run_wipe([p1, p2], ["project/one", "project/two"]) assert not os.path.exists(p1.worktree) assert not os.path.exists(p1.gitdir) assert not os.path.exists(p1.objdir) assert not os.path.exists(p2.worktree) assert not os.path.exists(p2.gitdir) assert not os.path.exists(p2.objdir) def test_wipe_shared_project_no_force_raises_error(tmp_path): """Test that wiping a shared project without --force raises an error.""" shared_objdir = os.path.join( str(tmp_path), ".repo/project-objects", "shared.git" ) p1 = _create_mock_project( str(tmp_path), "project/one", objdir_path=shared_objdir ) p2 = _create_mock_project( str(tmp_path), "project/two", objdir_path=shared_objdir ) with pytest.raises(wipe.Error) as e: _run_wipe([p1, p2], ["project/one"]) assert "shared object directories" in str(e.value) assert "project/one" in str(e.value) assert "project/two" in str(e.value) assert os.path.exists(p1.worktree) assert os.path.exists(p1.gitdir) assert os.path.exists(p2.worktree) assert os.path.exists(p2.gitdir) assert os.path.exists(shared_objdir) def test_wipe_shared_project_with_force(tmp_path): """Test wiping a shared project with --force.""" shared_objdir = os.path.join( str(tmp_path), ".repo/project-objects", "shared.git" ) p1 = _create_mock_project( str(tmp_path), "project/one", objdir_path=shared_objdir ) p2 = _create_mock_project( str(tmp_path), "project/two", objdir_path=shared_objdir ) _run_wipe([p1, p2], ["project/one"], options=["--force"]) assert not os.path.exists(p1.worktree) assert not os.path.exists(p1.gitdir) assert os.path.exists(shared_objdir) assert os.path.exists(p2.worktree) assert os.path.exists(p2.gitdir) def test_wipe_all_sharing_projects(tmp_path): """Test wiping all projects that share an object directory.""" shared_objdir = os.path.join( str(tmp_path), ".repo/project-objects", "shared.git" ) p1 = _create_mock_project( str(tmp_path), "project/one", objdir_path=shared_objdir ) p2 = _create_mock_project( str(tmp_path), "project/two", objdir_path=shared_objdir ) _run_wipe([p1, p2], ["project/one", "project/two"]) assert not os.path.exists(p1.worktree) assert not os.path.exists(p1.gitdir) assert not os.path.exists(p2.worktree) assert not os.path.exists(p2.gitdir) assert not os.path.exists(shared_objdir) def test_wipe_with_uncommitted_changes_raises_error(tmp_path): """Test wiping a project with uncommitted changes raises an error.""" p1 = _create_mock_project(str(tmp_path), "project/one", has_changes=True) with pytest.raises(wipe.Error) as e: _run_wipe([p1], ["project/one"]) assert "uncommitted changes" in str(e.value) assert "project/one" in str(e.value) assert os.path.exists(p1.worktree) assert os.path.exists(p1.gitdir) assert os.path.exists(p1.objdir) def test_wipe_with_uncommitted_changes_with_force(tmp_path): """Test wiping a project with uncommitted changes with --force.""" p1 = _create_mock_project(str(tmp_path), "project/one", has_changes=True) _run_wipe([p1], ["project/one"], options=["--force"]) assert not os.path.exists(p1.worktree) assert not os.path.exists(p1.gitdir) assert not os.path.exists(p1.objdir) def test_wipe_uncommitted_and_shared_raises_combined_error(tmp_path): """Test that uncommitted and shared projects raise a combined error.""" shared_objdir = os.path.join( str(tmp_path), ".repo/project-objects", "shared.git" ) p1 = _create_mock_project( str(tmp_path), "project/one", objdir_path=shared_objdir, has_changes=True, ) p2 = _create_mock_project( str(tmp_path), "project/two", objdir_path=shared_objdir ) with pytest.raises(wipe.Error) as e: _run_wipe([p1, p2], ["project/one"]) assert "uncommitted changes" in str(e.value) assert "shared object directories" in str(e.value) assert "project/one" in str(e.value) assert "project/two" in str(e.value) assert os.path.exists(p1.worktree) assert os.path.exists(p1.gitdir) assert os.path.exists(p2.worktree) assert os.path.exists(p2.gitdir) assert os.path.exists(shared_objdir) def test_wipe_shared_project_with_force_shared(tmp_path): """Test wiping a shared project with --force-shared.""" shared_objdir = os.path.join( str(tmp_path), ".repo/project-objects", "shared.git" ) p1 = _create_mock_project( str(tmp_path), "project/one", objdir_path=shared_objdir ) p2 = _create_mock_project( str(tmp_path), "project/two", objdir_path=shared_objdir ) _run_wipe([p1, p2], ["project/one"], options=["--force-shared"]) assert not os.path.exists(p1.worktree) assert not os.path.exists(p1.gitdir) assert os.path.exists(shared_objdir) assert os.path.exists(p2.worktree) assert os.path.exists(p2.gitdir) def test_wipe_with_uncommitted_changes_with_force_uncommitted(tmp_path): """Test wiping uncommitted changes with --force-uncommitted.""" p1 = _create_mock_project(str(tmp_path), "project/one", has_changes=True) _run_wipe([p1], ["project/one"], options=["--force-uncommitted"]) assert not os.path.exists(p1.worktree) assert not os.path.exists(p1.gitdir) assert not os.path.exists(p1.objdir)