Files
git-repo/tests/test_color.py
T
Brian Gan 35bbf701d0 color: Treat "true" and "yes" as "auto", not "always"
Per the git documentation for color.ui [1], setting color.ui to "true"
(or "yes") should behave identically to "auto", enabling color only
when output is written to a terminal or an active pager. Previously,
repo was equating "true" and "yes" with "always", which caused color
escape codes to be emitted unconditionally, even when output was piped
or redirected.

Replace the duplicated string-matching logic in SetDefaultColoring and
Coloring.__init__ with a single CONFIG_TO_COLOR_SETTING dict that maps
all git color config values to their behavior. This makes the mapping
easy to verify against the git docs and impossible to get out of sync
between the two call sites.

Added tests for SetDefaultColoring and Coloring.__init__ covering
all color mode values (auto, true, yes, always, never, no, false),
case insensitivity, TTY vs pipe behavior, active pager detection,
and unrecognised input.

[1] https://git-scm.com/docs/git-config#Documentation/git-config.txt-colorui

Bug: 295841573
Change-Id: I8a04b9c7e4154de37ed7518c010233039e0afdc9
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/602981
Tested-by: Brian Gan <brgan@google.com>
Reviewed-by: Gavin Mak <gavinmak@google.com>
Commit-Queue: Brian Gan <brgan@google.com>
2026-07-01 15:59:45 -07:00

155 lines
4.8 KiB
Python

# Copyright (C) 2024 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 color.py module."""
from unittest import mock
import pytest
import utils_for_test
import color
import git_config
@pytest.fixture
def coloring() -> color.Coloring:
"""Create a Coloring object for testing."""
return _make_coloring("always")
def _make_coloring(default_state: str) -> color.Coloring:
"""Set the default color mode and return a Coloring using test config."""
config_fixture = utils_for_test.FIXTURES_DIR / "test.gitconfig"
config = git_config.GitConfig(config_fixture)
color.SetDefaultColoring(default_state)
return color.Coloring(config, "status")
def test_Color_Parse_all_params_none(coloring: color.Coloring) -> None:
"""all params are None"""
val = coloring._parse(None, None, None, None)
assert val == ""
def test_Color_Parse_first_parameter_none(coloring: color.Coloring) -> None:
"""check fg & bg & attr"""
val = coloring._parse(None, "black", "red", "ul")
assert val == "\x1b[4;30;41m"
def test_Color_Parse_one_entry(coloring: color.Coloring) -> None:
"""check fg"""
val = coloring._parse("one", None, None, None)
assert val == "\033[33m"
def test_Color_Parse_two_entry(coloring: color.Coloring) -> None:
"""check fg & bg"""
val = coloring._parse("two", None, None, None)
assert val == "\033[35;46m"
def test_Color_Parse_three_entry(coloring: color.Coloring) -> None:
"""check fg & bg & attr"""
val = coloring._parse("three", None, None, None)
assert val == "\033[4;30;41m"
def test_Color_Parse_reset_entry(coloring: color.Coloring) -> None:
"""check reset entry"""
val = coloring._parse("reset", None, None, None)
assert val == "\033[m"
def test_Color_Parse_empty_entry(coloring: color.Coloring) -> None:
"""check empty entry"""
val = coloring._parse("none", "blue", "white", "dim")
assert val == "\033[2;34;47m"
val = coloring._parse("empty", "green", "white", "bold")
assert val == "\033[1;32;47m"
class TestSetDefaultColoring:
"""Tests for SetDefaultColoring."""
def test_none_leaves_default_unchanged(self) -> None:
color.DEFAULT = "auto"
color.SetDefaultColoring(None)
assert color.DEFAULT == "auto"
@pytest.mark.parametrize(
"value, expected",
(
# auto/true/yes all store their lowercase form.
("auto", "auto"),
("Auto", "auto"),
("true", "true"),
("True", "true"),
("yes", "yes"),
("Yes", "yes"),
# "always" stores as "always".
("always", "always"),
("Always", "always"),
# never/no/false store their lowercase form.
("never", "never"),
("no", "no"),
("false", "false"),
),
)
def test_maps_to_expected(self, value: str, expected: str) -> None:
color.SetDefaultColoring(value)
assert color.DEFAULT == expected
def test_unrecognised_leaves_default_unchanged(self) -> None:
color.DEFAULT = "auto"
color.SetDefaultColoring("garbage")
assert color.DEFAULT == "auto"
class TestColoringInit:
"""Tests for Coloring.__init__ color mode logic."""
@pytest.mark.parametrize(
"state, isatty, pager_active, expected",
(
# "always" enables color unconditionally.
("always", False, False, True),
# "never" disables color unconditionally.
("never", True, True, False),
# auto/true/yes enable color only on a TTY or active pager.
("auto", True, False, True),
("auto", False, False, False),
("auto", False, True, True),
("true", True, False, True),
("true", False, False, False),
("true", False, True, True),
("yes", True, False, True),
("yes", False, False, False),
("yes", False, True, True),
),
)
def test_color_mode(
self,
state: str,
isatty: bool,
pager_active: bool,
expected: bool,
) -> None:
with mock.patch("os.isatty", return_value=isatty), mock.patch(
"pager.active", pager_active
):
c = _make_coloring(state)
assert c.is_on is expected