blob: 72698db26a535fe7dadf9893c610935a5630814a [file] [edit]
# 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]