diff options
| author | Nasser Grainawi <nasser.grainawi@oss.qualcomm.com> | 2026-03-05 10:44:21 -0800 |
|---|---|---|
| committer | gerrit-scoped@luci-project-accounts.iam.gserviceaccount.com <gerrit-scoped@luci-project-accounts.iam.gserviceaccount.com> | 2026-04-23 17:52:09 -0700 |
| commit | 134eeb024bb172f425ef0823485b5cb537112f6c (patch) | |
| tree | 037b748e817368aed38a1840a4df5620e63b86c0 | |
| parent | 03fb18109fb0db2522f36309242d16d0f83f17d2 (diff) | |
| download | git-repo-134eeb024bb172f425ef0823485b5cb537112f6c.tar.gz | |
tests: Add tests for repo status output
Build out status subcommand unit coverage using a minimal fake repo
checkout wired through XmlManifest.
The new tests verify:
- clean status output prints the expected project header
- modified tracked files appear with the expected status marker
- `-o` output includes the orphan section and orphan entries
- branch names shown in status reflect a started non-default branch
Change-Id: Ia7c22593d0bbdc4aed81faeb168b846f3e4016ab
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/558501
Reviewed-by: Mike Frysinger <vapier@google.com>
Tested-by: Nasser Grainawi <nasser.grainawi@oss.qualcomm.com>
Commit-Queue: Nasser Grainawi <nasser.grainawi@oss.qualcomm.com>
Reviewed-by: Gavin Mak <gavinmak@google.com>
| -rw-r--r-- | tests/test_subcmds_status.py | 220 |
1 files changed, 220 insertions, 0 deletions
diff --git a/tests/test_subcmds_status.py b/tests/test_subcmds_status.py new file mode 100644 index 000000000..6007878b2 --- /dev/null +++ b/tests/test_subcmds_status.py | |||
| @@ -0,0 +1,220 @@ | |||
| 1 | # Copyright (C) 2026 The Android Open Source Project | ||
| 2 | # | ||
| 3 | # Licensed under the Apache License, Version 2.0 (the "License"); | ||
| 4 | # you may not use this file except in compliance with the License. | ||
| 5 | # You may obtain a copy of the License at | ||
| 6 | # | ||
| 7 | # http://www.apache.org/licenses/LICENSE-2.0 | ||
| 8 | # | ||
| 9 | # Unless required by applicable law or agreed to in writing, software | ||
| 10 | # distributed under the License is distributed on an "AS IS" BASIS, | ||
| 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| 12 | # See the License for the specific language governing permissions and | ||
| 13 | # limitations under the License. | ||
| 14 | |||
| 15 | """Unittests for the status subcmd.""" | ||
| 16 | |||
| 17 | import contextlib | ||
| 18 | import io | ||
| 19 | import os | ||
| 20 | from pathlib import Path | ||
| 21 | import subprocess | ||
| 22 | from typing import List, Tuple | ||
| 23 | from unittest import mock | ||
| 24 | |||
| 25 | import pytest | ||
| 26 | import utils_for_test | ||
| 27 | |||
| 28 | import manifest_xml | ||
| 29 | import subcmds | ||
| 30 | |||
| 31 | |||
| 32 | @pytest.fixture | ||
| 33 | def repo_client_checkout( | ||
| 34 | tmp_path: Path, | ||
| 35 | ) -> Tuple[Path, manifest_xml.XmlManifest]: | ||
| 36 | """Create a basic repo client checkout for status tests.""" | ||
| 37 | # Create in a subdir to avoid noise (like the repo_trace file). | ||
| 38 | topdir = tmp_path / "client_checkout" | ||
| 39 | repodir = topdir / ".repo" | ||
| 40 | manifest_dir = repodir / "manifests" | ||
| 41 | manifest_file = repodir / manifest_xml.MANIFEST_FILE_NAME | ||
| 42 | |||
| 43 | repodir.mkdir(parents=True) | ||
| 44 | manifest_dir.mkdir() | ||
| 45 | |||
| 46 | gitdir = repodir / "manifests.git" | ||
| 47 | gitdir.mkdir() | ||
| 48 | (gitdir / "config").write_text( | ||
| 49 | """[remote "origin"] | ||
| 50 | url = https://localhost:0/manifest | ||
| 51 | verbose = false | ||
| 52 | """ | ||
| 53 | ) | ||
| 54 | |||
| 55 | _init_temp_git_tree(manifest_dir) | ||
| 56 | |||
| 57 | manifest_file.write_text( | ||
| 58 | """ | ||
| 59 | <manifest> | ||
| 60 | <remote name="origin" fetch="http://localhost" /> | ||
| 61 | <default remote="origin" revision="refs/heads/main" /> | ||
| 62 | <project name="proj" path="src/proj" /> | ||
| 63 | </manifest> | ||
| 64 | """, | ||
| 65 | encoding="utf-8", | ||
| 66 | ) | ||
| 67 | |||
| 68 | (repodir / "projects" / "src" / "proj.git").mkdir(parents=True) | ||
| 69 | (repodir / "project-objects" / "proj.git").mkdir(parents=True) | ||
| 70 | |||
| 71 | worktree = topdir / "src" / "proj" | ||
| 72 | worktree.parent.mkdir(parents=True, exist_ok=True) | ||
| 73 | _init_temp_git_tree(worktree) | ||
| 74 | |||
| 75 | manifest = manifest_xml.XmlManifest(str(repodir), str(manifest_file)) | ||
| 76 | return topdir, manifest | ||
| 77 | |||
| 78 | |||
| 79 | def _init_temp_git_tree(git_dir: Path) -> None: | ||
| 80 | """Create a new git checkout with an initial commit for testing.""" | ||
| 81 | utils_for_test.init_git_tree(git_dir) | ||
| 82 | (git_dir / "README").write_text("init") | ||
| 83 | subprocess.check_call(["git", "add", "README"], cwd=git_dir) | ||
| 84 | subprocess.check_call(["git", "commit", "-q", "-m", "init"], cwd=git_dir) | ||
| 85 | |||
| 86 | |||
| 87 | def _run_status(manifest: manifest_xml.XmlManifest, argv: List[str]) -> None: | ||
| 88 | """Run the status subcommand with parsed options against a test manifest.""" | ||
| 89 | cmd = subcmds.status.Status() | ||
| 90 | cmd.manifest = manifest | ||
| 91 | cmd.client = mock.MagicMock(globalConfig=manifest.globalConfig) | ||
| 92 | |||
| 93 | opts, args = cmd.OptionParser.parse_args(argv + ["--jobs=1"]) | ||
| 94 | cmd.CommonValidateOptions(opts, args) | ||
| 95 | |||
| 96 | cmd.Execute(opts, args) | ||
| 97 | |||
| 98 | |||
| 99 | def _status_lines(output: str) -> List[str]: | ||
| 100 | """Normalize path separators and split command output into lines.""" | ||
| 101 | return output.replace(os.sep, "/").splitlines() | ||
| 102 | |||
| 103 | |||
| 104 | def _assert_project_header(line: str, project_path: str, branch: str) -> None: | ||
| 105 | """Assert a status project header line for a project and branch.""" | ||
| 106 | expected = f"project {(project_path + '/ '):<40}branch {branch}" | ||
| 107 | assert line == expected | ||
| 108 | |||
| 109 | |||
| 110 | def _assert_orphan_block(lines: List[str], expected: List[str]) -> None: | ||
| 111 | """Assert orphan block header and entries, independent of entry ordering.""" | ||
| 112 | assert lines | ||
| 113 | assert lines[0] == ("Objects not within a project (orphans)") | ||
| 114 | orphan_lines = lines[1:] | ||
| 115 | assert len(orphan_lines) == len(expected) | ||
| 116 | assert sorted(orphan_lines) == sorted(expected) | ||
| 117 | |||
| 118 | |||
| 119 | def test_orphans_basic( | ||
| 120 | repo_client_checkout: Tuple[Path, manifest_xml.XmlManifest], | ||
| 121 | ) -> None: | ||
| 122 | """Verify -o output includes project header and orphan block.""" | ||
| 123 | topdir, manifest = repo_client_checkout | ||
| 124 | project_path = next(iter(manifest.paths.keys())) | ||
| 125 | |||
| 126 | (topdir / "src" / "orphan_dir").mkdir(parents=True) | ||
| 127 | (topdir / "orphan.txt").write_text("data") | ||
| 128 | |||
| 129 | with contextlib.redirect_stdout(io.StringIO()) as stdout: | ||
| 130 | _run_status(manifest, ["-o"]) | ||
| 131 | |||
| 132 | lines = _status_lines(stdout.getvalue()) | ||
| 133 | _assert_project_header(lines[0], project_path, "main") | ||
| 134 | _assert_orphan_block( | ||
| 135 | lines[1:], | ||
| 136 | [ | ||
| 137 | " --\torphan.txt", | ||
| 138 | " --\tsrc/orphan_dir/", | ||
| 139 | ], | ||
| 140 | ) | ||
| 141 | |||
| 142 | |||
| 143 | def test_empty_status_without_orphans( | ||
| 144 | repo_client_checkout: Tuple[Path, manifest_xml.XmlManifest], | ||
| 145 | ) -> None: | ||
| 146 | """Verify clean status without -o prints only the project header line.""" | ||
| 147 | _, manifest = repo_client_checkout | ||
| 148 | project_path = next(iter(manifest.paths.keys())) | ||
| 149 | |||
| 150 | with contextlib.redirect_stdout(io.StringIO()) as stdout: | ||
| 151 | _run_status(manifest, []) | ||
| 152 | |||
| 153 | lines = _status_lines(stdout.getvalue()) | ||
| 154 | assert len(lines) == 1 | ||
| 155 | _assert_project_header(lines[0], project_path, "main") | ||
| 156 | |||
| 157 | |||
| 158 | def test_status_without_orphans( | ||
| 159 | repo_client_checkout: Tuple[Path, manifest_xml.XmlManifest], | ||
| 160 | ) -> None: | ||
| 161 | """Verify modified tracked file appears in status output without -o.""" | ||
| 162 | topdir, manifest = repo_client_checkout | ||
| 163 | project_path = next(iter(manifest.paths.keys())) | ||
| 164 | |||
| 165 | (topdir / project_path / "README").write_text("updated") | ||
| 166 | |||
| 167 | with contextlib.redirect_stdout(io.StringIO()) as stdout: | ||
| 168 | _run_status(manifest, []) | ||
| 169 | |||
| 170 | lines = _status_lines(stdout.getvalue()) | ||
| 171 | assert len(lines) == 2 | ||
| 172 | _assert_project_header(lines[0], project_path, "main") | ||
| 173 | assert lines[1] == " -m\tREADME" | ||
| 174 | |||
| 175 | |||
| 176 | def test_status_with_orphans_and_modified_file( | ||
| 177 | repo_client_checkout: Tuple[Path, manifest_xml.XmlManifest], | ||
| 178 | ) -> None: | ||
| 179 | """Verify modified-file status plus orphan block.""" | ||
| 180 | topdir, manifest = repo_client_checkout | ||
| 181 | project_path = next(iter(manifest.paths.keys())) | ||
| 182 | |||
| 183 | (topdir / project_path / "README").write_text("updated") | ||
| 184 | (topdir / "src" / "orphan_dir").mkdir(parents=True) | ||
| 185 | (topdir / "orphan.txt").write_text("data") | ||
| 186 | |||
| 187 | with contextlib.redirect_stdout(io.StringIO()) as stdout: | ||
| 188 | _run_status(manifest, ["-o"]) | ||
| 189 | |||
| 190 | lines = _status_lines(stdout.getvalue()) | ||
| 191 | _assert_project_header(lines[0], project_path, "main") | ||
| 192 | assert lines[1] == " -m\tREADME" | ||
| 193 | _assert_orphan_block( | ||
| 194 | lines[2:], | ||
| 195 | [ | ||
| 196 | " --\torphan.txt", | ||
| 197 | " --\tsrc/orphan_dir/", | ||
| 198 | ], | ||
| 199 | ) | ||
| 200 | |||
| 201 | |||
| 202 | def test_empty_status_after_start_shows_started_branch( | ||
| 203 | repo_client_checkout: Tuple[Path, manifest_xml.XmlManifest], | ||
| 204 | ) -> None: | ||
| 205 | """Verify status shows the started branch name when the tree is clean.""" | ||
| 206 | topdir, manifest = repo_client_checkout | ||
| 207 | |||
| 208 | project_path = next(iter(manifest.paths.keys())) | ||
| 209 | project_worktree = topdir / project_path | ||
| 210 | started_branch = "topic/test-status-branch" | ||
| 211 | subprocess.check_call( | ||
| 212 | ["git", "checkout", "-q", "-b", started_branch], cwd=project_worktree | ||
| 213 | ) | ||
| 214 | |||
| 215 | with contextlib.redirect_stdout(io.StringIO()) as stdout: | ||
| 216 | _run_status(manifest, []) | ||
| 217 | |||
| 218 | lines = _status_lines(stdout.getvalue()) | ||
| 219 | assert len(lines) == 1 | ||
| 220 | _assert_project_header(lines[0], project_path, started_branch) | ||
