# 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 git_status.py module.""" import os from typing import Any, List from unittest import mock import pytest import git_status def test_parse_porcelain_v2_branch_and_paths() -> None: output = ( b"# branch.oid " + b"1" * 40 + b"\0" b"# branch.head topic\0" b"# branch.upstream origin/main\0" b"# branch.ab +2 -3\0" b"# stash 1\0" b"1 M. N... 100644 100644 100644 " + b"1" * 40 + b" " + b"2" * 40 + b" staged name\0" b"1 .M N... 100644 100644 100644 " + b"1" * 40 + b" " + b"2" * 40 + b" worktree name\0" b"2 R. N... 100644 100644 100644 " + b"1" * 40 + b" " + b"2" * 40 + b" R075 renamed\0old name\0" b"? untracked\0" ) status = git_status.ParsePorcelainV2(output) assert status.current_branch == "topic" assert status.upstream == "origin/main" assert (status.ahead, status.behind, status.stash_count) == (2, 3, 1) assert status.index_changes["staged name"].status == "M" assert status.worktree_changes["worktree name"].status == "M" renamed = status.index_changes["renamed"] assert (renamed.src_path, renamed.level) == ("old name", "75") assert status.untracked == ["untracked"] def test_parse_porcelain_v2_unmerged_and_non_utf8_path() -> None: path = b"bad-\xff-name" output = ( b"u UU N... 100644 100644 100644 100644 " + b"1" * 40 + b" " + b"2" * 40 + b" " + b"3" * 40 + b" " + path + b"\0" ) status = git_status.ParsePorcelainV2(output) decoded = os.fsdecode(path) assert status.index_changes[decoded].status == "U" assert status.worktree_changes[decoded].status == "U" assert os.fsencode(status.index_changes[decoded].path) == path def test_untracked_only_respects_consider_untracked() -> None: status = git_status.ParsePorcelainV2(b"? new file\0") assert status.is_dirty() assert not status.is_dirty(consider_untracked=False) def test_branch_headers_preserve_non_ascii_names() -> None: branch = "tópico" status = git_status.ParsePorcelainV2( b"# branch.oid " + b"1" * 40 + b"\0" b"# branch.head " + os.fsencode(branch) + b"\0" ) assert status.current_branch == branch def test_quick_ahead_behind_is_recorded_as_unknown() -> None: status = git_status.ParsePorcelainV2(b"# branch.ab +? -?\0") assert (status.ahead, status.behind) == (0, 0) assert not status.has_ahead_behind def test_unknown_head_is_not_a_current_branch() -> None: status = git_status.ParsePorcelainV2(b"# branch.head (unknown)\0") assert status.current_branch is None def test_malformed_output_is_rejected() -> None: with pytest.raises(git_status.StatusParseError): git_status.ParsePorcelainV2(b"2 R. truncated\0") def test_malformed_branch_ab_is_rejected() -> None: with pytest.raises(git_status.StatusParseError): git_status.ParsePorcelainV2(b"# branch.ab not-a-valid-ab\0") def test_ignored_records_are_skipped() -> None: status = git_status.ParsePorcelainV2(b"! ignored_file\0") assert not status.is_dirty() assert status.untracked == [] def test_get_status_uses_versioned_machine_options( monkeypatch: pytest.MonkeyPatch, ) -> None: commands = [] class FakeGitCommand: def __init__( self, _project: Any, cmdv: List[str], **kwargs: Any ) -> None: commands.append((cmdv, kwargs)) self.stdout = b"" def Wait(self) -> int: return 0 monkeypatch.setattr(git_status, "GitCommand", FakeGitCommand) monkeypatch.setattr(git_status, "git_require", lambda _version: True) git_status.GetStatus( mock.sentinel.project, mock.sentinel.gitdir, untracked_files="no", branch=True, ahead_behind=True, show_stash=True, ) cmd, kwargs = commands[0] assert cmd == [ "status", "--porcelain=v2", "-z", "--ignore-submodules=all", "--untracked-files=no", "--branch", "--ahead-behind", "--renames", "--show-stash", ] assert kwargs["capture_stdout_bytes"] def test_get_status_rejects_git_before_2_11( monkeypatch: pytest.MonkeyPatch, ) -> None: monkeypatch.setattr(git_status, "git_require", lambda _version: False) with pytest.raises(git_status.UnsupportedStatusError): git_status.GetStatus(mock.sentinel.project, mock.sentinel.gitdir) def test_get_status_omits_stash_header_before_2_35( monkeypatch: pytest.MonkeyPatch, ) -> None: commands = [] class FakeGitCommand: def __init__( self, _project: Any, cmdv: List[str], **_kwargs: Any ) -> None: commands.append(cmdv) self.stdout = b"" def Wait(self) -> int: return 0 monkeypatch.setattr(git_status, "GitCommand", FakeGitCommand) monkeypatch.setattr( git_status, "git_require", lambda version: version <= (2, 34, 0), ) git_status.GetStatus( mock.sentinel.project, mock.sentinel.gitdir, show_stash=True, ) assert "--show-stash" not in commands[0]